← Back to Writeups
CUSTOMEasy-MediumReversing

Baby (Obfuscated) Flag Checker

XESXOR9/10/20265 min read
#reversing#custom#easy-medium#control_flow_analysis#regex_deobfuscation#static_analysis#xor_decryption

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:

  1. 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
  2. Control Flow Flattening: State machine pattern with while True loops and numeric state variables controlling execution flow

Key Discovery: XOR Wrapper Functions

The core obfuscation relied on simple XOR wrapper functions: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
def G0g0sQu1D_116510(a, b):
    return a ^ b

def g0GOsquiD(a, b):
    return a ^ b

All "complex" expressions like:

python
g0GOsquiD(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
import 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
def GGs(x):
    return (x * 1103515245 + 12345) & 2147483647

Permutation Generator:

python
def Ggs(n):  # Uses LCG with seed 195936478
    ...

Encrypted Flag Data:

python
G0gosQu1D = [
    [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:

PartDecoded String
0p4TcH_
1uoftctf{d1d_
2XD???}
3d3BuG_
40n3_sh07_
54n_1LM_
6r3v_0r_
7th15_w17h_
8y0u_m0nk3Y_

Parts were placed at calculated offsets based on the SqUId order array. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 4: Solver Script

python
#!/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

  1. XOR obfuscation is reversible: When XOR functions take constant arguments, they can be pre-computed
  2. Regex is powerful for deobfuscation: Pattern matching can automate tedious manual analysis
  3. Hints matter: "Seeing double" pointed to the variable name confusion technique
  4. Multiple approaches work: The flag itself acknowledges different valid solving methods ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Alternative Approaches

  1. Monkey Patching: Override the comparison function to print the expected flag
  2. Dynamic Analysis: Use debugger to inspect variables at runtime
  3. LLM Assistance: Feed the obfuscated code to an AI for analysis (as hinted in the flag) ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍