-
-
Save aquynh/046523ec9d3ed0014b98188183a0e12d to your computer and use it in GitHub Desktop.
Reverse Engineering a Book Cover
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 ([email protected]) 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] | |
#print(chars) | |
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) | |
if eax == 0x45504f4e: | |
print("NOPE") | |
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) | |
# 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 with one extra byte at 0x98") | |
print() | |
print("Psuedo code for brute forcer:") | |
print("int value = 0") | |
print("int tableIndex = 22") | |
print("int *table = 0x3D") | |
print("char password[24]") | |
print("char *pwdpos = &password[22];") | |
print("while (tableIndex >= 0) {") | |
print(" for (char c = 0; c < 256; c++) {") | |
print(" ") | |
if True: | |
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:;" | |
) | |
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)): | |
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) | |
mu.emu_start(scratch_addr, scratch_addr+len(code)) | |
crc = mu.reg_read(UC_X86_REG_EAX) | |
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)) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment