2024 Cyber Apocalypse: Cubicle Riddle
Challenge Information
| Attribute | Details |
|---|---|
| Event | 2024 Cyber Apocalypse |
| Category | Misc |
| Challenge | Cubicle Riddle |
| Difficulty | Medium |
Summary
Cubicle Riddle challenges participants to construct a Python function in bytecode form that computes the minimum and maximum values of an array. A mysterious cube in the forest poses a riddle, and the answer is a sequence of bytecode instructions. By understanding Python bytecode, opcodes, and function construction, participants can craft the correct bytecode sequence and obtain the flag.
Analysis
The challenge structure:
- Server: Presents a riddle and awaits bytecode answer
- Expected Function:
_answer_func(num_list)returning(min, max) - Answer Format: Comma-separated bytecode integers (e.g., “100,83,1,…”)
- Bytecode Components:
- Start sequence (provided):
d\x01}\x01d\x02}\x02→ [100, 1, 125, 1, 100, 2, 125, 2] - Middle section (to find): Load argument, iterate, find min/max
- End sequence (provided):
|\x01|\x02f\x02S\x00→ [124, 1, 124, 2, 102, 2, 83, 0]
- Start sequence (provided):
Solution
Step 1: Understand Bytecode Structure
Python bytecode uses opcodes from the dis module:
import dis
# Example functiondef example(num_list): return min(num_list), max(num_list)
# Disassemble to see opcodesdis.dis(example)Output shows:
LOAD_GLOBAL(101): Load global functionLOAD_FAST(124): Load local variableCALL_FUNCTION(131): Call functionRETURN_VALUE(83): Return value
Step 2: Analyze Provided Sequences
Start sequence: [100, 1, 125, 1, 100, 2, 125, 2]
100 (LOAD_CONST)with value 1125 (STORE_FAST)to variable 1100 (LOAD_CONST)with value 2125 (STORE_FAST)to variable 2
End sequence: [124, 1, 124, 2, 102, 2, 83, 0]
124 (LOAD_FAST)variable 1124 (LOAD_FAST)variable 2102 (BUILD_TUPLE)with 2 elements83 (RETURN_VALUE)
Step 3: Construct Middle Bytecode
The middle part needs to:
- Load the
num_listargument - Call
min()and store in variable 1 - Call
max()and store in variable 2
# Key opcodes:# 124: LOAD_FAST - load variable# 101: LOAD_GLOBAL - load global function# 131: CALL_FUNCTION - call with N args
middle_bytecode = [ 124, 0, # LOAD_FAST 0 (load num_list) 101, 0, # LOAD_GLOBAL 0 (load min from names) 124, 0, # LOAD_FAST 0 (load num_list) 131, 1, # CALL_FUNCTION 1 arg 125, 1, # STORE_FAST 1 (store min result) 124, 0, # LOAD_FAST 0 (load num_list) 101, 1, # LOAD_GLOBAL 1 (load max from names) 124, 0, # LOAD_FAST 0 (load num_list) 131, 1, # CALL_FUNCTION 1 arg 125, 2, # STORE_FAST 2 (store max result)]Step 4: Complete Bytecode Sequence
start = [100, 1, 125, 1, 100, 2, 125, 2]middle = [124, 0, 101, 0, 124, 0, 131, 1, 125, 1, 124, 0, 101, 1, 124, 0, 131, 1, 125, 2]end = [124, 1, 124, 2, 102, 2, 83, 0]
full_bytecode = start + middle + endanswer = ','.join(map(str, full_bytecode))Step 5: Connect to Server and Submit
import socketimport re
def solve_riddle(): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('localhost', 1337))
# Read the riddle presentation data = sock.recv(4096) print(data.decode())
# Choose option 1 (approach the cube) sock.sendall(b'1\n')
# Read riddle data = sock.recv(4096) print(data.decode())
# Send bytecode answer bytecode_answer = '100,1,125,1,100,2,125,2,124,0,101,0,124,0,131,1,125,1,124,0,101,1,124,0,131,1,125,2,124,1,124,2,102,2,83,0' sock.sendall(bytecode_answer.encode() + b'\n')
# Receive flag response = sock.recv(4096) print(response.decode())
sock.close()
if __name__ == '__main__': solve_riddle()Step 6: Extract Flag from Response
The server responds with the flag embedded in the response text. Use regex to extract:
import re
flag_match = re.search(r'HTB\{[^}]+\}', response)if flag_match: flag = flag_match.group(0) print(f"Flag: {flag}")Python Bytecode Reference
Common Opcodes
| Opcode | Value | Description |
|---|---|---|
| LOAD_CONST | 100 | Load constant value |
| LOAD_GLOBAL | 101 | Load global variable/function |
| LOAD_FAST | 124 | Load local variable |
| STORE_FAST | 125 | Store to local variable |
| LOAD_ATTR | 106 | Load attribute |
| CALL_FUNCTION | 131 | Call function with N args |
| BUILD_TUPLE | 102 | Build tuple from stack |
| RETURN_VALUE | 83 | Return from function |
Understanding Co_consts and Co_varnames
code_obj = func.__code__
print(code_obj.co_consts) # Constant values (None, 1, 2, ...)print(code_obj.co_varnames) # Variable names (num_list, ...)print(code_obj.co_names) # Global names (min, max, ...)Disassembly Example
import dis
def answer_func(num_list): return min(num_list), max(num_list)
dis.dis(answer_func)Output:
2 0 LOAD_GLOBAL 0 (min) 2 LOAD_FAST 0 (num_list) 4 CALL_FUNCTION 1 6 LOAD_GLOBAL 1 (max) 8 LOAD_FAST 0 (num_list) 10 CALL_FUNCTION 1 12 BUILD_TUPLE 2 14 RETURN_VALUEKey Takeaways
- Python bytecode is directly manipulable and executable
- Understanding opcodes enables custom bytecode generation
- The
dismodule helps learn bytecode structure - Function construction from Code objects is possible but fragile
- Bytecode challenges require careful debugging and testing
- Using
dis.dis()to compare with expected output is crucial