Skip to content

Instantly share code, notes, and snippets.

@John-K
Last active June 5, 2018 05:29
Show Gist options
  • Save John-K/890de26268727dfad1f1c4ad44d4c31c to your computer and use it in GitHub Desktop.
Save John-K/890de26268727dfad1f1c4ad44d4c31c to your computer and use it in GitHub Desktop.
Reverse Engineering a Book Cover
#!/bin/env python2
# -*- coding: utf-8 -*-
# Solution to Book Cover Crackme from "Praktyczna inżynieria wstecznia
# Edited by Gynvael Coldwind and Mateusz Jurczyk. (Applied Reverse Engineering)
# PWN Bookstore: https://ksiegarnia.pwn.pl/Praktyczna-inzynieria-wsteczna,622427233,p.html
#
# Props to @radekk for his excellent writeup and for capturing the flag. Read his
# writeup at https://vulnsec.com/2017/reverse-engineering-a-book-cover/
#
# This was a fun opportunity to learn how to use Unicorn Engine, Capstone Engine,
# and Keystone Engine to RE a crackme and solve it by mixing x86 asm and Python
#
# John Kelley (john@kelley.ca) 2017-03-01
#
from __future__ import print_function
import struct
import binascii
import struct
# THE RE TRIFORCE!
from unicorn import *
from unicorn.x86_const import *
from capstone import *
from keystone import *
# Array of bytes from the background of the book cover
bk_cvr = b"\xad\x52\x45\x52\x45\x0f\xc6\xbf\x40\x63\x8c\x63\x85\x03\xcf\xc6\x48\xcb\x45\x52\x45\xd6\x97\x26\x4d\xa0\x4a\x6a\xb5\x90\x04\xb9\xa8\x0b\x7c\xd6\xc8\x6f\x45\x52\x45\x27\x49\x13\xc5\xab\x52\x27\x9f\xea\x02\x3d\x2a\x36\x89\xea\x0b\x1d\x15\x17\x89\x9c\xd1\x1f\x13\xd7\x3c\x1a\x13\xb4\x23\x3b\x88\x6a\x10\x49\x3c\xe5\xfa\x92\xf8\xc1\xb0\x99\xcb\xf0\x81\x00\x83\xa5\xae\xf1\xd1\xcc\xf1\x21\x63\x4c\x36\xa5\xcc\x16\xae\x93\x77\xd1\xb5\xd4\x3d\x16\xfe\xb0\x17\xf8\xba\xaf\x82\x08\x45\xab\x2c\xfe\x06\x15\xa5\x76\xd0\x70\x01\x33\x6c\x51\xad\x4c\x4a\x91\xc9\xf1\x9b\x1e\x4d\xff\x94\x1a\xae\x12\xd2\xd2\xd5\x08\x68\x7b\xa1\x06\x30\x26\x24\x38\x12\x22\x2c\x21\x3f\x06\x24\x38\x2b\x37\x0d\x33\x36\x3e\x2a\x73\x64\x73\x45\x52\x00"
# Keystone Engine helper
class x86Assembler:
def __init__(self, base_address):
self.ks = Ks(KS_ARCH_X86, KS_MODE_32)
self.base_addr = base_address
def encode(self, text):
b = bytearray()
#print("Assembling '{}'".format(text))
code, count = self.ks.asm(text, self.base_addr)
# convert int into a byte
for c in code:
b += bytes(bytearray([c]))
return b
def hexdump(src, length=16):
try:
xrange(0,1);
except NameError:
xrange = range;
FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or '.' for x in range(256)])
lines = []
for c in xrange(0, len(src), length):
chars = src[c:c+length]
hex = ' '.join(["%02x" % x for x in chars])
printable = ''.join(["%s" % ((x <= 127 and FILTER[x]) or '.') for x in chars])
lines.append("%04x %-*s %s\n" % (c, length*3, hex, printable))
return ''.join(lines)
# callback for tracing invalid memory access (READ or WRITE)
def hook_mem_invalid(uc, access, address, size, value, user_data):
eip = uc.reg_read(UC_X86_REG_EIP)
print(">>> Missing memory at 0x%x, eip = 0x%x data size = %u, data value = 0x%x" \
%(address, eip, size, value))
if access == UC_MEM_WRITE_UNMAPPED:
print(">>> Missing memory is being WRITE at 0x%x, data size = %u, data value = 0x%x" \
%(address, size, value))
mem = uc.mem_read(eip, 5)
for i in cs.disasm(bytes(mem), eip):
print(" 0x%x:\t%s\t%s" % (i.address, i.mnemonic, i.op_str))
# return True to indicate we want to continue emulation
return True
if access == UC_ERR_READ_UNMAPPED:
print(">>> Missing memory is being WRITE at 0x%x, data size = %u, data value = 0x%x" \
%(address, size, value))
return True
else:
# return False to indicate we want to stop emulation
return False
def hook_code(uc, address, size, user_data):
print(">>> Tracing instruction at 0x%x, instruction size = 0x%x" %(address, size))
eip = uc.reg_read(UC_X86_REG_EFLAGS)
print(">>> --- EFLAGS is 0x%x" %(eip))
# needed to trap the end of execution
def hook_intr(uc, intno, user_data):
if intno == 3:
eax = uc.reg_read(UC_X86_REG_EAX)
# code in rwx_mem_addr calls int 3 with EAX set to NOPE or GOOD
# in response to invalid or valid password. We stop EMU after this
if eax == 0x45504f4e:
print("NOPE")
if eax == 0x646f6f47:
print("GOOD")
else:
print("INT3: {}".format(hex(eax)))
else:
print("Got unhandled int %d" % intno)
uc.emu_stop()
# Adresses to use
address = 0
bk_cvr_addr = 0x1000
rwx_mem_addr = 0x2000
scratch_addr = 0x4000
esp_base = 0x10000 # don't forget to setup a stack!
# capstone engine for disassembling
cs = Cs(CS_ARCH_X86, CS_MODE_32)
# unicorn engine for emulation
mu = Uc(UC_ARCH_X86, UC_MODE_32)
# assemble the shellcode from the cover
asm = x86Assembler(address);
sc = asm.encode("dcd:;"+
"lodsw;"+
"xor ax, 0x5245;"+
"stosw;"+
"loop dcd;")
shellcode = bytes(sc)
#because 2mb is enough for anyone... right?
mu.mem_map(address, 2*1024*1024)
# load shellcode to address 0
mu.mem_write(0, shellcode)
# put the book cover at 0x1000
mu.mem_write(bk_cvr_addr, bk_cvr)
# setup a hook for unmapped addresses (because we forgot to setup the stack!)
mu.hook_add(UC_HOOK_MEM_READ_UNMAPPED | UC_HOOK_MEM_WRITE_UNMAPPED, hook_mem_invalid)
mu.hook_add(UC_HOOK_MEM_UNMAPPED, hook_mem_invalid)
mu.hook_add(UC_HOOK_INTR, hook_intr)
# setup addresses for shellcode
print("Setting up register state:")
print("\tmov esp, 0x10000")
# Setup code from the first block of asm on the cover
mu.reg_write(UC_X86_REG_ESP, esp_base)
print("\tlea esi, [bk_cvr]")
mu.reg_write(UC_X86_REG_ESI, bk_cvr_addr)
print("\tlea edi [rxw_mem]")
mu.reg_write(UC_X86_REG_EDI, rwx_mem_addr)
print("\tmov ecx, 89")
mu.reg_write(UC_X86_REG_ECX, 89)
# 2nd cover box (loop) and the jmp to decoded memory
print("\nCode to emu:")
for i in cs.disasm(shellcode, 0):
print(" 0x%x:\t%s\t%s" % (i.address, i.mnemonic, i.op_str))
print("\nWe've got a XOR party with 'RE'!!");
print("\nEmulating...", end="")
# start emulation at address for len(shellcode) bytes
mu.emu_start(address, address+len(shellcode))
print("Done")
# read out rwx contents
rwx_mem = mu.mem_read(rwx_mem_addr, len(bk_cvr))
print("RWX contents:")
print(hexdump(rwx_mem))
print("Interesting strings: 'Good', 'NOPE', 'TutajWpiszTajneHaslo!!!'");
print("Google translate says: (Polish) Enter Secret Password Here!!!");
print()
print("RWX code to emu:")
for i in cs.disasm(bytes(rwx_mem), rwx_mem_addr):
print(" 0x%x:\t%s\t%s" % (i.address, i.mnemonic, i.op_str))
print()
print("0x2000 is 'calling' the next instruction which has the effect of pushing that address onto the stack")
print("0x2005 is taking that offset (0x2005) and loading it into the ebp register")
print("0x2006 is subtracting 5 from ebp, which is the size of the call instruction at 0x2000, effectively")
print(" making ebp hold the base address of this code chain")
print("0x2009-0x200b are clearing the ecx and eax registers")
print("0x200d push ecx onto stack")
print("0x200e load a byte from address 0x99 + ecx + ebp, since ebp is our base we can ignore that here in")
print(" understanding what's going on. Therefore dl = 0x99 + ecx")
print(" 0x2015 set the Zero Flag if dl is 0")
print(" 0x2017 jump to 0x2021 if the Zero Flag is set, effectively stopping when we hit the end of a string")
print(" 0x2019 update the crc32 in EAX with the byte in DL")
print(" 0x201e increment ecx, our byte pointer in the password")
print(" 0x201f jmp back to the start of our loop at 0x20e")
print("0x2021 pop ECX off of the stack")
print("0x2022 load 4 bytes from (0x3d * ECX*4) and compare it to EAX which holds our current CRC32 value")
print("0x2029 if the CRC32 in EAX doesn't match the value loaded, jump to 0x2037")
print("0x202b increment ECX to point to the next byte in the password")
print("0x202c compare lower byte of ECX to 23 (the length of the password)")
print("0x202f if CL isn't 23, then jump back into the loop (0x200b)")
print("0x2031 load 'Good' into EAX")
print("0x2036 int3 - software interrupt that signals we're done")
print("0x2037 load 'NOPE' into EAX")
print("0x203c int3 - software interrupt that signals we're done")
print("0x203d dead code?")
print()
print("Lets translate the above into pseudocode:")
print("int value = 0")
print("int counter = 0")
print("char *password = 0x99")
print("int *resultTable = 0x3D")
print()
print("loop:")
print("for (char *c = stringToTest + counter; c != '\\0'; c++) {")
print(" value = CRC32(value, *c)")
print("}")
print("if (resultTable[counter] != value) { // no *4 since incrementing an int* gives you the next int")
print(" int3('NOPE')")
print("}")
print("if (++counter != 0x17) {")
print(" goto loop")
print("}")
print()
print("int3('Good'")
print()
print()
print("So it looks like there is a table of valid CRC32 values for password[], password[1:], ... password[-1:]")
print("stored starting at offset 0x3D. We should be able to reverse this password by going backwards through")
print("this list! If we get the password right, it will only ever compare the first CRC32 so someone has been")
print("a sloppy programmer ;) From looking at the data dump, it sure looks like there are 23 integers between")
print("0x3D and the start of the password at 0x99")
print()
print("C code for brute forcer:")
print("int value = 0")
print("int tableIndex = 22")
print("int *table = (int *)(rwx_mem+0x3D)")
print("char password[24] = {}")
print("char *pwdpos = &password[22];")
print()
print("while (tableIndex >= 0) {")
print(" for (char c = 0; c < 256; c++) {")
print(" // start with the crc for our test character")
print(" int crc = crc32(0, c);")
print(" // add remaining characters in the string to the crc")
print(" for (char *d = pwdpos+1; (d - password) <= 22; d++) {")
print(" crc = crc32(crc, *d);")
print(" }")
print(" if (crc == table[tableIndex]) {")
print(" // we've found the correct character for this position")
print(" *pwdpos-- = c;")
print(" --tableIndex;")
print(" break;")
print(" } else if (c == 255) {")
print(" // we couldn't figure this position out")
print(" return -1;")
print(" }")
print(" }")
print("}")
print("printf(\"password is '%s'\n\", password);")
# generate the code for our check character asm function
password_addr = rwx_mem_addr + 0x99
password_len = 23
last_crc32 = rwx_mem_addr + 0x95
code = bytearray()
char = 0
asm = x86Assembler(scratch_addr)
code += asm.encode( "xor eax, eax;"+
"doCRC:;"+
"crc32 eax, bl;"+
"cmp ecx, 22;"+
"jge end;"+
"inc ecx;"+
"mov bl, byte ptr [edx + ecx];"+
"jmp doCRC;"+
"end:;"
)
# set asm check character function
mu.mem_write(scratch_addr, bytes(code))
data = mu.mem_read(rwx_mem_addr + 0x3D, 92)
crcs = struct.unpack("<23I", data);
print("Bruteforcing password characters (in reverse order):")
for index in range(0,23):
for char in reversed(range(0,256)):
# setup the arguments for check character function
mu.reg_write(UC_X86_REG_EBX, char)
mu.reg_write(UC_X86_REG_ECX, 22-index)
mu.reg_write(UC_X86_REG_EDX, password_addr)
#run the check character function
mu.emu_start(scratch_addr, scratch_addr+len(code))
crc = mu.reg_read(UC_X86_REG_EAX)
# check our CRC to see if it's correct
if crc == crcs[22 - index]:
val = bytes(bytearray([char]))
#print("Password[{}] is {}".format(22-index, val))
mu.mem_write(password_addr + password_len - 1 - index, val)
pwd = mu.mem_read(password_addr, password_len)
print(pwd)
break;
elif char == 0:
raise("Could not determine password character {}".format(index))
password = mu.mem_read(password_addr, password_len)
print("Pasword: '{}'".format(password))
print("Lets run the decoded code with our bruteforced password")
# NB: hooks from the top of the file are still active
# Interrupt hook should print out GOOD or NOPE
mu.emu_start(rwx_mem_addr, rwx_mem_addr+200)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment