Baby (Obfuscated) Flag Checker
Baby (Obfuscated) Flag Checker
Platform: uoftctf2026 | Category: Reversing | Type: Challenge | Difficulty: Easy-Medium | OS: NA | Author: XESXOR | Date: 2026-01-10 | Status: Solved Techniques: control_flow_analysis, regex_deobfuscation, static_analysis, xor_decryption
Summary
Task: Heavily obfuscated Python flag checker using similar variable names, XOR wrapper functions, and control flow flattening. Solution: Built a regex-based deobfuscator to resolve nested XOR function calls to constants, revealing the encrypted flag parts XOR-encoded with key 125 and arranged via a permutation array.
Baby (Obfuscated) Flag Checker - UofTCTF 2026
Description
"All this obfuscation has left Han Shangyan seeing double. Even Grunt refuses to untangle this mess for him, so it's up to you to do the real grunt work (hahaha get it???)."
Hint: This challenge can be solved without fully deobfuscating the script, but writing a deobfuscator might help you with the "ML Connoisseur" challenge...
Files
baby.py- Heavily obfuscated Python flag checker
Analysis
Obfuscation Techniques Identified
The code employed two main obfuscation techniques:
-
Variable Name Confusion: Similar-looking variable names using mixed case and numbers:
g0GOsquiD,G0g0sQu1D,gOg0sQuId,GgS,sQU1D,SqUId, etc.- The "seeing double" hint referenced this confusion
-
Control Flow Flattening: State machine pattern with
while Trueloops and numeric state variables controlling execution flow
Key Discovery: XOR Wrapper Functions
The core obfuscation relied on simple XOR wrapper functions:
python read-only • copy alloweddef G0g0sQu1D_116510(a, b): return a ^ b def g0GOsquiD(a, b): return a ^ b
All "complex" expressions like:
python read-only • copy allowedg0GOsquiD(G0g0sQu1D_116510(4493, 2401), G0g0sQu1D_116510(28, 6768))
Simply computed constants via nested XOR operations.
Solution
Step 1: Regex-Based Deobfuscation
Created a deobfuscator to replace all XOR function calls with their computed results:
python read-only • copy allowedimport re def deobfuscate(code): # Replace inner XOR calls first pattern1 = r'G0g0sQu1D_116510\((\d+),\s*(\d+)\)' def replace_xor1(match): a, b = int(match.group(1)), int(match.group(2)) return str(a ^ b) # Iteratively replace until no more matches while re.search(pattern1, code): code = re.sub(pattern1, replace_xor1, code) # Replace outer XOR calls pattern2 = r'g0GOsquiD\((\d+),\s*(\d+)\)' def replace_xor2(match): a, b = int(match.group(1)), int(match.group(2)) return str(a ^ b) while re.search(pattern2, code): code = re.sub(pattern2, replace_xor2, code) return code
Step 2: Revealed Logic Structure
After deobfuscation, the key structures became visible:
LCG Generator:
python read-only • copy alloweddef GGs(x): return (x * 1103515245 + 12345) & 2147483647
Permutation Generator:
python read-only • copy alloweddef Ggs(n): # Uses LCG with seed 195936478 ...
Encrypted Flag Data:
python read-only • copy allowedG0gosQu1D = [ [13, 73, 41, 30, 53, 34], # Part 0 [8, 18, 27, 9, 30, 9, 27, 6, 25, 76, 25, 34], # Part 1 [37, 57, 66, 66, 66, 0], # Part 2 [25, 78, 63, 8, 58, 34], # Part 3 [77, 19, 78, 34, 14, 21, 77, 74, 34], # Part 4 [73, 19, 34, 76, 49, 48, 34], # Part 5 [15, 78, 11, 34, 77, 15, 34], # Part 6 [9, 21, 76, 72, 34, 10, 76, 74, 21, 34], # Part 7 [4, 77, 8, 34, 16, 77, 19, 22, 78, 36, 34] # Part 8 ] GgS = 125 # XOR key sQU1D = [6, 12, 6, 6, 9, 7, 7, 10, 11] # Part lengths SqUId = [1, 8, 0, 3, 6, 4, 7, 5, 2] # Part order
Step 3: Flag Reconstruction
Decoded each part using XOR with key 125:
| Part | Decoded String |
|---|---|
| 0 | p4TcH_ |
| 1 | uoftctf{d1d_ |
| 2 | XD???} |
| 3 | d3BuG_ |
| 4 | 0n3_sh07_ |
| 5 | 4n_1LM_ |
| 6 | r3v_0r_ |
| 7 | th15_w17h_ |
| 8 | y0u_m0nk3Y_ |
Parts were placed at calculated offsets based on the SqUId order array.
Step 4: Solver Script
python read-only • copy allowed#!/usr/bin/env python3 """ Baby (Obfuscated) Flag Checker - UofTCTF 2026 Solver script """ def decode(arr, key): """XOR decode an array of integers to string""" return ''.join(chr(c ^ key) for c in arr) # XOR key extracted from deobfuscated code GgS = 125 # Encrypted flag parts G0gosQu1D = [ [13, 73, 41, 30, 53, 34], [8, 18, 27, 9, 30, 9, 27, 6, 25, 76, 25, 34], [37, 57, 66, 66, 66, 0], [25, 78, 63, 8, 58, 34], [77, 19, 78, 34, 14, 21, 77, 74, 34], [73, 19, 34, 76, 49, 48, 34], [15, 78, 11, 34, 77, 15, 34], [9, 21, 76, 72, 34, 10, 76, 74, 21, 34], [4, 77, 8, 34, 16, 77, 19, 22, 78, 36, 34] ] # Part lengths sQU1D = [6, 12, 6, 6, 9, 7, 7, 10, 11] # Part order (permutation) SqUId = [1, 8, 0, 3, 6, 4, 7, 5, 2] def calculate_offset(part_idx): """Calculate the offset for a given part index based on order""" offset = 0 for i in SqUId: if i == part_idx: break offset += sQU1D[i] return offset # Reconstruct flag total_length = sum(sQU1D) flag = ['?'] * total_length for part_idx in range(9): offset = calculate_offset(part_idx) decoded = decode(G0gosQu1D[part_idx], GgS) for j, c in enumerate(decoded): flag[offset + j] = c print(''.join(flag))
Flag Meaning
The flag reads: "did you monkey patch debug rev or one shot this with an ILM XD???"
This references different solving approaches:
- Monkey patching: Modifying the script at runtime to extract the flag
- Debug rev: Traditional reverse engineering with debugging
- One shot with an LLM: Using AI to analyze/deobfuscate the code
Lessons Learned
- XOR obfuscation is reversible: When XOR functions take constant arguments, they can be pre-computed
- Regex is powerful for deobfuscation: Pattern matching can automate tedious manual analysis
- Hints matter: "Seeing double" pointed to the variable name confusion technique
- Multiple approaches work: The flag itself acknowledges different valid solving methods
Alternative Approaches
- Monkey Patching: Override the comparison function to print the expected flag
- Dynamic Analysis: Use debugger to inspect variables at runtime
- LLM Assistance: Feed the obfuscated code to an AI for analysis (as hinted in the flag)