This challenge involves reverse engineering a polymorphic binary, one that modifies its own instructions during runtime.
Essentially, the binary checks if the current character equals a known value and xor decrypts the next section where the code jumps to. If the characters don’t match, the logic short-circuits and the program exits.
This process of checking the character and decrypting the next branch continues like opening up a Matryoshka doll until the last branch which returns instead of calling the decryption subroutine.
To begin, we need to have radare2 installed. Next, we will create a Python virtual environment and install the r2pipe package.
python -m venv env
source env/bin/activate # or activate.fish in my case
pip install r2pipe
We download the headache
binary and place the following script in our working directory:
import sys
import shutil
import binascii
import r2pipe
from dataclasses import dataclass
import struct
from typing import Optional
import os
if os.path.exists('headache_patched'):
os.unlink("headache_patched")
shutil.copy('headache', 'headache_patched')
r2 = r2pipe.open("headache_patched", flags=['-w'])
@dataclass
class Formula:
a: int
b: int
xor: int
def apply(self, flag):
a_exists = flag[self.a] is not None
b_exists = flag[self.b] is not None
if a_exists and not b_exists:
result = flag[self.a] ^ self.xor
if not valid_character(result):
return
flag[self.b] = result
elif b_exists and not a_exists:
result = flag[self.b] ^ self.xor
if not valid_character(result):
return
flag[self.a] = result
def unravel(base: int) -> (int, Optional[Formula]):
r2.cmd("af-")
r2.cmd("s {}".format(hex(base)))
r2.cmd("af")
pdr = r2.cmd("pi 10")
try:
r2.cmd("s +3")
exec(r2.cmd("pcp 1"), globals())
a = ord(buf)
r2.cmd("s +3")
b = 0
exec(r2.cmd("pcp 1"), globals())
if buf == b'\x7f':
r2.cmd("s +1")
exec(r2.cmd("pcp 1"), globals())
b = ord(buf)
r2.cmd("s +4")
exec(r2.cmd("pcp 1"), globals())
x = ord(buf)
f = Formula(a, b, x)
except:
base, None
jump_index = pdr.find("je ")
pdr = pdr[jump_index + 3:]
other_block = pdr[:8]
r2.cmd("s 0x" + other_block)
r2.cmd("af-")
r2.cmd("af")
# mov eax == b8 (one byte)
r2.cmd("s +1")
exec(r2.cmd("pcp 4"), globals())
xor_key = buf
r2.cmd("s +8")
# lea address is now in buf
exec(r2.cmd("pcp 4"), globals())
mutating_fn = struct.unpack("<I", buf)[0]
if mutating_fn == 0xffffffff:
return base, None
r2.cmd("s " + hex(mutating_fn))
while True:
exec(r2.cmd("pcp 4"), globals())
recovered = bytearray(x ^ y for x, y in zip(buf, xor_key))
unpacked = struct.unpack("<I", recovered)[0]
r2.cmd('wv {}'.format(unpacked))
r2.cmd("s +4")
if recovered == xor_key:
break
return mutating_fn, f
formulas = []
next_addr = 0x401290
while next_addr != 0x40261c:
next_addr, formula = unravel(next_addr)
if formula is None:
r2.quit()
r2 = r2pipe.open("headache_patched", flags=['-w'])
continue
print(hex(next_addr))
formulas.append(formula)
charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_"
def valid_character(c: int):
return c < 256 and chr(c) in charset
flag = [None] * 61
for i, c in enumerate(map(ord, "amateursCTF{")):
flag[i] = c
flag[-1] = ord('}')
while not all(flag):
for formula in formulas:
formula.apply(flag)
print(''.join(map(chr, flag)))
Note that we begin our binary parsing from the address 0x401290
since that is where the first condition and subsequent decryptions begin.
We also allocate a list of 61 None
singletons since the program exits if the input has a length other than 61.
Finally, we can run our script.
python main.py
Since Python is quite slow and my solution is not elegant, it will take anywhere between 2 to 4 minutes to decrypt all the sections. After this, we get the flag.
amateursCTF{i_h4v3_a_spli77ing_headache_1_r3qu1re_m04r_sl33p}