2024 Cyber Apocalypse: Cubicle Riddle

Challenge Information

AttributeDetails
Event2024 Cyber Apocalypse
CategoryMisc
ChallengeCubicle Riddle
DifficultyMedium

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:

  1. Server: Presents a riddle and awaits bytecode answer
  2. Expected Function: _answer_func(num_list) returning (min, max)
  3. Answer Format: Comma-separated bytecode integers (e.g., “100,83,1,…”)
  4. 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]

Solution

Step 1: Understand Bytecode Structure

Python bytecode uses opcodes from the dis module:

import dis
# Example function
def example(num_list):
return min(num_list), max(num_list)
# Disassemble to see opcodes
dis.dis(example)

Output shows:

  • LOAD_GLOBAL (101): Load global function
  • LOAD_FAST (124): Load local variable
  • CALL_FUNCTION (131): Call function
  • RETURN_VALUE (83): Return value

Step 2: Analyze Provided Sequences

Start sequence: [100, 1, 125, 1, 100, 2, 125, 2]

  • 100 (LOAD_CONST) with value 1
  • 125 (STORE_FAST) to variable 1
  • 100 (LOAD_CONST) with value 2
  • 125 (STORE_FAST) to variable 2

End sequence: [124, 1, 124, 2, 102, 2, 83, 0]

  • 124 (LOAD_FAST) variable 1
  • 124 (LOAD_FAST) variable 2
  • 102 (BUILD_TUPLE) with 2 elements
  • 83 (RETURN_VALUE)

Step 3: Construct Middle Bytecode

The middle part needs to:

  1. Load the num_list argument
  2. Call min() and store in variable 1
  3. 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 + end
answer = ','.join(map(str, full_bytecode))

Step 5: Connect to Server and Submit

import socket
import 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

OpcodeValueDescription
LOAD_CONST100Load constant value
LOAD_GLOBAL101Load global variable/function
LOAD_FAST124Load local variable
STORE_FAST125Store to local variable
LOAD_ATTR106Load attribute
CALL_FUNCTION131Call function with N args
BUILD_TUPLE102Build tuple from stack
RETURN_VALUE83Return 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_VALUE

Key Takeaways

  • Python bytecode is directly manipulable and executable
  • Understanding opcodes enables custom bytecode generation
  • The dis module 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