TicTacToed
TicTacToed is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator
Scenario
TicTacToed attack path
TicTacToed is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator
Objective
Challenge walkthrough focused on Pwn evidence, validation, and reusable operator lessons.
Walkthrough flow
The outer Rust tictactoe binary contains a hidden 5x5...
obfuscate_pattern() builds the required move history...
The access-code material is split into three...
A successful credential check calls execute_c2(),...
The embedded C2 has a use-after-free primitive: E can...
Source coverage
High source coverage
Status: complete. This article is generated from 4 sanitized Markdown sources and keeps raw flags, credentials, keys, cookies, and reusable secrets out of the rendered blog.
High confidence: the page is reconstructed from a primary walkthrough plus multiple supporting notes or evidence sources. Treat the chain as source-backed, while still checking the listed source files for sensitive values.
- Pwn/TicTacToed/writeup.md
- htb-challenge/Pwn/TicTacToed/notes.md
- htb-challenge/Pwn/TicTacToed/memory-summary.md
- htb-challenge/Pwn/TicTacToed/hypothesis-board.md
Technical Walkthrough
Writeup
Challenge
- Name: TicTacToed
- Category: Pwn
- Difficulty: Medium
- Mode: hybrid
Summary
TicTacToed is a hybrid pwn challenge with an outer Rust tic-tac-toe gate and
an embedded C command-and-control ELF. The solve chain is:
- Play the hidden 5x5 tic-tac-toe move sequence that unlocks the operator
interface.
- Derive the access code from the XOR-obfuscated bytes in the Rust binary.
- Use the C2 menu to leak a PIE text pointer via
printID. - Trigger the stale-agent use-after-free with
E/Y, then reclaim the freed
chunk through F.
- Overwrite the stale function pointer with the live
getSecretaddress and
capture the HTB-format flag.
Artifact Inventory
Reference analysis/artifact-inventory.json and summarize the relevant files or remote surface.
files/extracted/pwn_tictactoed/tictactoe: outer 64-bit Rust PIE executable.analysis/C2_executable.full: carved embedded C2 ELF used after the hidden
game/access-code gate.
analysis/outer-*: disassembly notes for the Rust gate, key derivation, and
embedded C2 extraction.
analysis/C2-full-*: symbol, disassembly, and string evidence for the C2
menu, getSecret, printID, Hackupdate, and the stale agent pointer.
solve/solve.py: final reproducible remote solver.solve/probe_fast_offsets.py: bounded calibration helper used to resolve
remote C2 layout drift.
Analysis
Static analysis showed the Rust binary decrypts an access code by XORing an
embedded byte sequence with 0x5a. The resulting bytes hash to the embedded
SHA-256 value, proving the access-code derivation without storing the raw value
in the writeup.
The Rust check_winner path contains a hidden move-history condition. The
required sequence is:
X:00 O:04 X:11 O:13 X:22 O:31 X:33 O:40 X:44After that pattern is recognized, the binary prompts for operator credentials
and executes the embedded C2 as /tmp/C2_executable.
The C2 allocates a global agent structure and repeatedly calls the function
pointer at the start of that structure. Menu option H sets that pointer to
printID, which prints a text pointer for PIE calibration. Menu option E
sets the function pointer to exitProgram; confirming with Y frees the
global agent without clearing it. Menu option F calls Hackupdate, which
allocates 8 bytes and reads 8 raw bytes from stdin. This reclaims the freed
chunk and lets the solver overwrite the stale function pointer before
executeAction(agent) is called again.
The carved local C2 had getSecret at symbol offset 0x1269, but live
calibration proved the remote C2 layout was shifted. A bounded probe against
the active target found the correct live getSecret page offset at 0x259.
The final solver uses the leaked page base plus 0x259.
Solve
Run the final solver through the harness executor:
cd <local workspace>
python3 scripts/challenge_exec.py Pwn/TicTacToed --phase exploit --output analysis/flag-candidate-rerun.txt -- python3 Pwn/TicTacToed/solve/solve.py --output Pwn/TicTacToed/analysis/flag-candidate-rerun.txt
python3 scripts/challenge_harness.py capture-flag Pwn/TicTacToed --from analysis/flag-candidate-rerun.txtThe solver performs the hidden game sequence, derives the access code at
runtime, leaks the C2 text pointer, performs the UAF function-pointer overwrite,
and writes the captured candidate to analysis/flag-candidate-rerun.txt.
Flag
Raw flag is stored in loot/flag.txt and intentionally not reproduced here.
Lessons
- Validate CTF exploit chains against live behavior before assuming the local
carved binary and remote child process are byte-identical.
- For menu-driven UAFs that mix
scanfand rawread, send the raw overwrite
only after the read prompt. Pre-buffering binary pointer bytes can be
swallowed by stdio buffering.
- Keep broad probing bounded. The decisive step here was calibrating against a
known-good printID target, then probing only the plausible shifted
getSecret window.
Source-Backed Dossier
The sections below are merged from companion Markdown notes for the same case. They are rendered after sanitization so the article stays precise without publishing raw flags, credentials, or target-specific secrets.
Notes
Scope
- Challenge: TicTacToed
- Category: Pwn
- Difficulty: Medium
- Mode: hybrid
- Remote instance: none
- Start time: 2026-06-11T13:47:27Z
- Operator: harness
- State file:
challenge-state.json
Harness Status
- Current phase: see
challenge-state.json - Next allowed actions: see
next-action.json - Raw flags and sensitive material stay in
loot/only. Do not paste them here.
Artifact Inventory
| File | Size | SHA256 | Type | Notes |
|---|---|---|---|---|
files/a12c7379-3b7c-4871-89f7-3fae2eae22c7.zip | 1603456 | <hash redacted> | Zip archive data, at least v1.0 to extract, compression method=store | zip entries: 2 shown in artifact inventory JSON |
Evidence Ledger
| Time | Action | Output/File | Finding | Confidence | Next |
|---|---|---|---|---|---|
| 2026-06-11T13:47:27Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-11T13:47:38Z | artifact inventory | analysis/artifact-inventory.json | 1 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-11T13:50:39Z | hypothesis recorded | hypothesis-board.md | Exploit embedded C2 use-after-free: leak PIE via H/User ID, free agent via E/Y, allocate same tcache chunk with F, overwrite stale agent function pointer with getSecret, then let main execute it. | Medium | Interact with local or remote service to confirm H leaks generateUserID and E/Y followed by F accepts 8 raw bytes before executeAction. |
| 2026-06-11T13:50:39Z | hypothesis recorded | hypothesis-board.md | Resolve the outer Rust tic-tac-toe/access-code gate that drops and executes the embedded C2 executable. | Medium | Capture remote initial prompts and statically inspect outer validate_access_code/check_winner/decrypt_key to derive the required username/access code or winning board input. |
| 2026-06-11T13:50:40Z | instrumentation plan | analysis/instrumentation-plan.md | Reach and exploit the embedded C2 menu to call getSecret and read flag.txt. | High | Stop after two remote attempts without a new fact, or if the remote prompt differs from the carved C2/outer-gate model. |
| 2026-06-11T13:51:16Z | checkpoint recorded | analysis/checkpoint-analysis-20260611T135116391545Z-93872ad1.md | Checkpoint for ANALYSIS | High | Use checkpoint to drive next decision |
| 2026-06-11T13:57:03Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-11T13:57:16Z | RAG query | analysis/rag/rag-query-20260611T135703679266Z-cc983fbd.txt | RAG helper exited 0; output saved | Medium | Record retrieval tag and validation |
| 2026-06-11T13:57:52Z | RAG record | analysis/rag-records.md | Retrieved memory tagged GENERIC | Medium | Validate or reject with live evidence |
| 2026-06-11T13:59:07Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-11T13:59:19Z | evaluator | analysis/evaluator-20260611T135919386535Z-b3c84a12.md | Proceed | High | Execute solve/solve.py through challenge_exec.py, then capture-flag from analysis/flag-candidate.txt. |
| 2026-06-11T21:54:51Z | branch closed | hypothesis-board.md | Remote child layout drift invalidated the fixed local generateUserID offset assumption. Solver now derives base from leaked text-page offset instead. | High | Rerank hypotheses |
| 2026-06-11T21:55:07Z | checkpoint recorded | analysis/checkpoint-analysis-20260611T215507719409Z-cd2cfa04.md | Checkpoint for ANALYSIS | High | Use checkpoint to drive next decision |
| 2026-06-11T22:34:53Z | flag capture | loot/flag.txt | HTB-format flag captured; raw value kept in loot only | High | Write solution and run completion gate |
| 2026-06-11T22:34:53Z | remote endpoint update | analysis/calibrate-printid-newtarget.txt | Replacement target <TARGET>:32372 validated; hidden gate, access gate, and UAF overwrite still work | High | Resolve remote C2 layout drift |
| 2026-06-11T22:34:53Z | focused offset probe | analysis/fast-probe-newtarget-0x220-0x269.log | Live getSecret page offset found at 0x259; raw flag captured to analysis candidate then loot | High | Patch solver default and rerun |
| 2026-06-11T22:34:53Z | solver rerun | analysis/flag-candidate-rerun.txt | Final solve/solve.py reproduced the flag capture through challenge_exec.py | High | Complete harness state |
| 2026-06-11T22:36:53Z | flag capture | loot/flag.txt | HTB-format flag captured; raw value kept in loot only | High | Write solution and run completion gate |
| 2026-06-11T22:36:53Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
- Outer Rust binary requires a hidden 5x5 move-history sequence before the operator interface appears.
- The operator access code is derived by XORing embedded bytes with
0x5a; the solver derives it at runtime. - Embedded C2 has a stale global
agentpointer afterE/Yfrees it. - Menu option
Freclaims the freed chunk and reads 8 raw bytes, allowing a function-pointer overwrite. - The replacement remote C2 layout differs from the carved local ELF, so the final solver uses the live-calibrated
getSecretpage offset0x259.
RAG / Advisory Memory
RAG output is advisory only. Record evaluated retrievals with:
scripts/challenge_harness.py rag-record <workspace> --query "..." --tag MATCHED|PARTIAL|MISSING|<secret redacted>|GENERIC --validation "..."Secrets/Flags
Raw flags and sensitive material stay in loot/ only. Use scripts/challenge_harness.py capture-flag to validate and record flag capture without printing the value.
Memory Summary
Metadata
- Platform: HackTheBox Challenges
- Category: Pwn
- Challenge: TicTacToed
- Difficulty: Medium
- Source workspace:
<local workspace>
Validated Solve Chain
Concepts only. Do not include raw flags, reusable credentials, tokens, cookies, private keys, or live secrets.
- The outer Rust
tictactoebinary contains a hidden 5x5 move-history gate incheck_winner(). obfuscate_pattern()builds the required move history from nine 4-byte fragments:X:00,O:04,X:11,O:13,X:22,O:31,X:33,O:40,X:44.- The access-code material is split into three encrypted arrays. Each decrypt closure XORs bytes with
0x5a; the derived value matches the embedded SHA-256 constant. - A successful credential check calls
execute_c2(), which drops and executes an embedded Linux C ELF. - The embedded C2 has a use-after-free primitive:
Ecan free the globalagent, andFcan reallocate the same tcache-size chunk and write 8 attacker-controlled bytes. - The C2
Hmenu action leaks a text pointer; compute PIE base from the leaked text-page offset and overwrite the stale function pointer withgetSecret. getSecret()readsflag.txtin the C2 process working directory.
Reusable Lessons
- Rust wrappers can hide the real pwn target as an embedded child ELF; carve secondary ELF headers and audit both stages.
- A game win condition may be a hidden move-history regex rather than ordinary board logic.
- For small C menu binaries, stale global pointers after
free()plus same-sizemalloc()paths are enough for direct function-pointer redirection when a code pointer leak exists.
Dead Ends
- Normal tic-tac-toe win alone is insufficient because the credential/C2 path is tied to the hidden move-history regex.
- RAG had no useful TicTacToed-specific memory; the solve chain is validated from current local artifacts.
Tool Quirks
- Local macOS cannot execute the Linux ELFs directly; validation relied on static disassembly plus one controlled remote run.
- Docker was installed but the daemon was unavailable during tool readiness, so containerized local execution was not used.
Evidence Paths
analysis/binary-audit.mdanalysis/outer-decrypt-closures.txtanalysis/outer-obfuscate-pattern-disasm.txtanalysis/outer-check-winner-disasm.txtanalysis/outer-main-disasm.txtanalysis/outer-credentials-disasm.txtanalysis/C2-full-disasm.txtanalysis/C2-full-nm.txtanalysis/remote-unavailable-20260611.mdsolve/solve.py
Ingestion Decision
- Proposed for LightRAG: yes
- Requires user approval before ingestion: yes
Hypothesis Board
Keep no more than 3 active hypotheses on Easy/Medium and 5 on Hard unless the user explicitly asks for breadth.
| Rank | Path | Evidence | Missing Proof | Cheapest Validation | Confidence | Status |
|---|---|---|---|---|---|---|
| 1 | Exploit embedded C2 use-after-free: leak PIE via H/User ID, free agent via E/Y, allocate same tcache chunk with F, overwrite stale agent function pointer with getSecret, then let main execute it. | Embedded C2 ELF carved at analysis/C2_executable.full; symbols show getSecret=0x1269, generateUserID=0x143c, printID leaks generateUserID with User ID: %p; processInput frees global agent on E and Hackupdate malloc(8)+read(8) can overlap freed agent. | Interact with local or remote service to confirm H leaks generateUserID and E/Y followed by F accepts 8 raw bytes before executeAction. | Medium | Active | |
| 2 | Resolve the outer Rust tic-tac-toe/access-code gate that drops and executes the embedded C2 executable. | Outer symbols include detect_debugger, decrypt_key, validate_access_code, ask_for_credentials, check_winner, execute_c2, and embedded C2 path /tmp/C2_executable. | Capture remote initial prompts and statically inspect outer validate_access_code/check_winner/decrypt_key to derive the required username/access code or winning board input. | Medium | Active |
Closed Branches
| Branch | Evidence Tested | Failure Output | Reason Closed | Revisit Condition |
|---|---|---|---|---|
| Fixed local generateUserID-offset PIE calculation | Live C2 leaks consistently ended with low bits inconsistent with the local carved generateUserID offset; using leak - 0x143c + 0x1269 caused the C2 child to exit without getSecret output. | analysis/remote-unavailable-20260611.md | Remote child layout drift invalidated the fixed local generateUserID offset assumption. Solver now derives base from leaked text-page offset instead. | Only revisit if a restarted remote leak has low bits matching the local generateUserID offset, or if the patched page-offset calculation fails with a new transcript. |
Technical analogy
How to remember this solve
Think of the challenge as a small system with one rule that matters more than the rest. The solve is finding that rule, validating it, and using it carefully enough to reach the final proof.
For TicTacToed, keep the mental model simple: identify the trusted assumption, prove it with the smallest safe test, then automate or repeat only the part that directly leads to the flag.