Autobot (First time using Binja!)
13 Apr 2020Autobot was a simple reversing challenge in ByteBandits CTF 2020. I solved it the Binja API (and attempted to do it using Qiling). Below is the writeup for the same.
'''
The challenge is straightforward. The server sends a base64 encoded ELF file over
the network. For all binaries, there is a buffer at offset 0xaa8
in the binary which contains some data (printable ASCII). The flag decryption
function is at 0x7da which loads some values onto the stack and uses them as
offsets into this data array, stitching them together to validate given input.
We simply emulate program behavior to find the flag. Pseudocode:
data = ...
index_arr = ..
input = ...
for i in range(len_of_flag): # len_of_flag is operand at 0x954
if input[i] != data[index_arr[i]]:
print("Failed!")
exit(0)
print("Good job!")
This pattern remains the same across all binaries -> including the function
offset, the data buffer address, etc. A good approach would be to solve the first
few problems by hardcoding these values in the automation script and generalising
as and when the server sends a file which is different.
'''
import binaryninja
from pwn import remote
import base64
conn = remote('pwn.byteband.it', 6000)
file_counter = 0
while True:
# Receive file from network as a base64 encoded string
elf_base64 = conn.recvline()
chal = elf_base64.strip()
elf_base64_decoded = base64.b64decode(chal)
# Write file onto disk
chal_name = 'chal_' + str(file_counter)
with open(chal_name, 'wb') as f:
f.write(elf_base64_decoded)
# Load file in BN and perform initial analysis
bv = binaryninja.BinaryViewType.get_view_of_file(chal_name)
bv.update_analysis_and_wait()
# There is a buffer at 0xaa8 which has 30 bytes, read that into data
data = bv.read(0xaa8, 30).decode('utf-8')
# Function which does flag computation is at 0x7da
solver_func = bv.get_function_at(0x7da)
# Get length of flag from this instruction (remains same for all binaries)
len_inst = solver_func.get_low_level_il_at(0x954)
flag_length = len_inst.operands[1].constant
flag = ""
# At this address, offsets are pushed into variables on the stack which are
# used to index into the array.
# Eg. (sequence of) mov dword [var_xx], 0x12
# TODO: Use instruction sequence instead of this
base_addr = 0x7f4
counter = 0
while counter < flag_length:
if counter <= 24:
inst_addr = base_addr + counter * 10
inst = solver_func.get_low_level_il_at(inst_addr)
else:
inst_addr = base_addr + 240 + (counter - 24) * 7
inst = solver_func.get_low_level_il_at(inst_addr)
counter += 1
# Get the offset and index into data to add character to flag
offset = inst.operands[1].constant
flag += data[offset]
# Send flag
conn.sendline(flag)
print("[+] Solved for " + str(file_counter) + " : " + flag)
file_counter += 1
Eventually, the last file written to disk has the flag stored in it. Flag: flag{0pt1mus_pr1m3_has_chosen_you}
. This was my first CTF challenge attempting to use Binja and I must say it was a pretty smooth experience. The API is highly usable and makes it easier to think about the problem rather the automation itself. +Rep!
I spent around 8 hours trying to solve this problem, instead I should have spent only around 1. Rusty CTF skills are a reason, but the other is a possible bug in Unicorn/Qiling. I had heard of Qiling beta release and decided to test it out. Qiling is a emulation engine which is an abstraction above the “Holy Trinity” of reverse-engineering tools - Capstone, Keystone and Unicorn. (Pseudo writeup below):
'''
The idea is simple.
1. TEST_FLAG = ""
2. Hook instruction with compare (flag_byte, input_byte)
2. Run Emulation
3. Check failure_byte (flag_byte at which comparison fails)
4. TEST_FLAG += failure_byte
5. Goto 2
6. We now haz flag!
'''
import sys
from qiling import *
# Suppress qiling syscall emulation debug output
f = open('/dev/null', 'w')
sys.stderr = f
base_path = "/home/chinmay_dd/ByteBanditsCTF/"
# Solving only one challenge for now
# conn = remote('pwn.byteband.it', 6000)
file_counter = 0
# base_flag will eventually contain the flag
base_flag = []
# Flag length collected from EAX value at 96b
length = 0
# Count of how many times the compare instruction ran
count = 0
# Compare function hook
# At the compare instruction, check value in dl which is what is being
# compared with our input. Add it to global_flag
def h_c(ql):
global count
global base_flag
count += 1
base_flag += chr(ql.uc.reg_read(UC_X86_REG_AL))
# Instruction hook which will tell us length of flag
def h_d(ql):
global length
length = ql.uc.reg_read(UC_X86_REG_EAX)
# Simple class provided with Qiling which will model a stdin/stdout pipe
class MyPipe():
def __init__(self):
self.buf = b''
self.offset = 0
def write(self, s):
self.buf += s
def read(self, size):
if size <= len(self.buf):
ret = self.buf[self.offset:self.offset + size]
self.buf = self.buf[self.offset + size:]
else:
ret = self.buf
self.buf = ''
return ret
def fileno(self):
return 0
def show(self):
pass
def clear(self):
pass
def flush(self):
pass
# Custom added. Might be wrong, but works.
def lseek(self, pos, how):
pass
if how == 0:
self.offset = pos
return pos
elif how == 1:
return self.offset
elif how == 2:
pass
def close(self):
self.outpipe.close()
def fstat(self):
return os.fstat(sys.stdin.fileno())
# Function to solve one problem
def solve_one(elf_path):
global base_flag
next_flag = ""
def run_one_round(test_flag):
# Create two pipes
stdin = MyPipe()
stdout = MyPipe()
# Initialize qiling instance with library path as given in the
# examples directory of the project. This is where the custom libraries
# are provided which will aid emulation
ql = Qiling([elf_path], "rootfs/x8664_linux", stdin=stdin, output="off", stdout=stdout)
# Hook compare
ql.hook_address(h_c, 0x5555555549b3)
# Hook length
ql.hook_address(h_d, 0x55555555496b)
# Write test flag in input
stdin.write(str.encode(test_flag))
# Run emulation
ql.run()
# Check output
out = str(stdout.read(1024))
if "good job" in out:
# We found what we wanted, now quit
return test_flag
# Clear current instances
del stdin
del stdout
del ql
# Failed output
return None
while True:
# Run the emulation once
sol = run_one_round(next_flag)
if sol is not None:
# WE GOT FLAG
return sol
# Next flag to try
next_flag = ''.join(base_flag)
base_flag = []
print(solve_one(base_path + "chal_0"))
This works. Mostly. For some binaries, what I was experiencing was something weird. The emulation loop was ending one iteration before the actual flag was computed. Qiling was (for some reason) not emulating the last iteration and prematurely printing “good job” without actually having computed the flag. I spent a lot of debugging this and couldnt get to the bottom of it. This was mainly happening with flags where the last character was ‘U’ or ‘m’. I reached 202 solves on the automation before encountering the issue. I was sure that my solution was correct and it was mostly likely a server issue.
Probably need to dig deeper.