challenge info
the challenge description already gives a ton of information by default;
You must call the
callme_one(),callme_two()andcallme_three()functions in that order, each with the arguments0xdeadbeef,0xcafebabe,0xd00df00de.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 | void callme_one(long arg1, long arg2, long arg3); |
keep in mind the x64 calling convention:
rdirsirdxrcxr8r9- 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():
- your code calls
callme_one@plt - PLT jumps to address in
callme_one@got - GOT initially points back to PLT resolver code
- resolver looks up real address & updates GOT
- jumps to actual
callme_one()
but, after this (subsequent calls):
- your code calls
callme_one@plt - PLT jumps to address in
callme_one@got - 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@pltin my ROP chain - AND, i still get FULL FUNCTIONALITY! THAT’S RIGHT!!!! CALLING THE PLT ENTRY WORKS EXACTLY LIKE CALLING THE REAL FUNCTION!!!

methodology
1 | $ file callme |
1 | pwndbg> checksec |
the challenge description recommended we use rabin2, so i’ll use that for once lol:
1 | $ rabin2 -i callme |
pwnme exploration
1 | pwndbg> disass pwnme |
vulnerable to bof:
1 | 0x00000000004008cf <+55>: lea rax,[rbp-0x20] |
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:
- noting the addresses of
callme_one,callme_two, andcallme_three, which i did via rabin2 (as noted above) - noting the values we’ll have to change:
0xdeadbeefdeadbeef,0xcafebabecafebabe, etc. - writing the initial payload to overflow the vulnerable buffer which i found via the
lea&movpattern, which was 40 bytes (32 byte buffer + 8 bytes for saved rbp = 40 bytes to reach RIP (return address)) - “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 | from pwn import * |
why this works:
1 | # The gadget executes: |
and all done!
note: if that exact gadget had NOT existed, i would have had to find individual pop calls:
1 | pop_rdi # 0x??????: pop rdi; ret |
and exploitation would have been more like:
1 | payload += pop_rdi_gadget |