Solving Hackpack 2023 Reverse Engineering Challenges with Automated Tools
Competition Overview
The Hackpack 2023 CTF featured several reverse engineering challenges available at https://ctf2023.hackpack.club/challenges. This writeup covers two main challenge categories: the Speed-Rev series and a WebAssembly challenge.
Speed-Rev Challenge Analysis
The Speed-Rev challenge required solving 6 reverse engineering problems within a 3-minute timeframe. The challenges progressively increased in complexity:
- Challenges 1-3: Direct string extraction and simple string splitting operations
- Challenges 4-6: More complex character relationship analysis requiring pattern matching
Network Communication Setup
The challenges utilized network-based flag submission, requiring automated solutions. The nclib library proved essential for handling TCP connections:
import nclib
import base64
import string
TIMEOUT = 2
connection = nclib.Netcat(('challenge.hackpack.club', 41702))
Solving Early Challenges
The first challenge involved extracting base64-encoded data from server responses:
response = connection.recv_all(timeout=TIMEOUT).decode()
encoded_data = response[response.find('b\'')+2:-20].encode()
decoded = base64.decodebytes(encoded_data)
flag_part = decoded[8196:8212]
print(flag_part.decode())
connection.send(flag_part + b'\n')
Challenges 2 and 3 used similar extraction patterns with additional string manipulation:
response = connection.recv_all(timeout=TIMEOUT).decode()
data = base64.decodebytes(response[response.find('b\'')+2:-20].encode())
fragments = data[4436:8212].split(b'<')
result = b''
for index in range(1, 17):
result += fragments[index][:1]
print(result.decode())
connection.send(result + b'\n')
Handling Ambiguous Solutions
Challenges 4-6 could produce multiple valid flags. The solution involved filtering results to ensure only alphanumeric characters:
def is_alphanumeric(candidate: str) -> bool:
return all(char in string.ascii_letters + string.digits for char in candidate)
response = connection.recv_all(timeout=TIMEOUT).decode()
data = base64.decodebytes(response[response.find('b\'')+2:-20].encode())
fragments = data[4448:5204].split(b't')
values = []
for segment in fragments:
if segment[-1] == 0 and segment[-5] == ord('='):
values.append(segment[-4])
else:
if segment[-1] != 233:
values.append(segment[-1])
candidates = []
for char in string.ascii_letters + string.digits:
attempt = [char]
valid = True
for idx in range(15):
if values[idx] - ord(attempt[idx]) > 0:
attempt.append(chr(values[idx] - ord(attempt[idx])))
else:
valid = False
break
if valid:
candidates.append(''.join(attempt))
for candidate in candidates:
if is_alphanumeric(candidate):
print(candidate)
connection.send(candidate.encode() + b'\n')
break
Automated Solution Using Angr
For a more robust approach, the angr symbolic execution framework can automatically solve binary validation functions:
#!/usr/bin/env python3
from angr import Project, SimState
from claripy import *
from pwn import remote
from base64 import b64decode
def extract_response(sock, lines=1):
data = b''
for _ in range(lines):
data += sock.recvline(keepends=True)
return data
def submit_solution(sock, payload):
if isinstance(payload, int):
payload = str(payload)
if isinstance(payload, str):
payload = payload.encode('utf-8')
sock.sendline(payload)
def solve_binary(filepath):
project = Project(filepath, auto_load_libs=False, main_opts={'base_addr': 0})
validation_func = project.loader.main_object.get_symbol('validate')
assert validation_func.is_function
func_address = validation_func.linked_addr
flag_buffer = BVS('flag', 128)
state = project.factory.call_state(func_address, flag_buffer)
for position in range(16):
state.solver.add(Or(
And(flag_buffer.get_byte(position) >= ord('A'),
flag_buffer.get_byte(position) <= ord('Z')),
And(flag_buffer.get_byte(position) >= ord('a'),
flag_buffer.get_byte(position) <= ord('z')),
And(flag_buffer.get_byte(position) >= ord('0'),
flag_buffer.get_byte(position) <= ord('9')),
))
state.memory.store(state.regs.rsp, flag_buffer)
state.regs.rdi = state.regs.rsp
state.stack_push(0)
state.stack_push(0)
simgr = project.factory.simulation_manager(state)
simgr.run()
result = b'1' * 16
for ended_state in simgr.deadended:
ended_state.solver.add(ended_state.regs.eax == 0)
if ended_state.satisfiable():
result = ended_state.solver.eval(flag_buffer, cast_to=bytes)
return result
def main():
socket = remote('challenge.hackpack.club', 41702)
extract_response(socket, 2)
for challenge_num in range(6):
while True:
response = extract_response(socket, echo=False)
if response.startswith(b'b\''):
break
challenge_data = b64decode(response[2:-2])
extract_response(socket)
filename = f'challenge_{challenge_num + 1}'
with open(filename, 'wb') as f:
f.write(challenge_data)
solution = solve_binary(filename)
submit_solution(socket, solution)
if extract_response(socket) == b'Wrong!\n':
break
else:
extract_response(socket)
if __name__ == '__main__':
main()
This script automatically downloads each challenge binary, uses angr to symbolically execute the validation function, and extracts the correct flag by constraining the input to alphanumeric characters and checking for successful validation (return value of 0).
WASM-Safe Challenge
The WebAssembly challenge required decompiling and analyzing WASM binaries. Tools like Jeb or dedicated WASM disassemblers can be used for this purpose. While not fully solved, working with this challenge provided valuable experience with WebAssembly reverse engineering techniques.
Key Takeaways
- Symbolic Execution: Using angr eliminates the need for manual binary analysis by automatically exploring execution paths
- Network Automation: The nclib libray simplifies TCP communication for CTF challenges
- Pattern Recognition: Identifying special byte sequences (like the 't' delimiter in this challenge) helps locate relevant data
- Solution Filtering: When multiple valid solutions exist, filtering by character constraints helps identify the correct flag