Challenge / Crypto

Broken Decryptor

Broken Decryptor is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator

MediumPublished 2024-04-06Sanitized local writeup

Scenario

Broken Decryptor attack path

Broken Decryptor 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 Crypto evidence, validation, and reusable operator lessons.

Broken Decryptor sanitized attack graph

Walkthrough flow

01

Audited the Python source and identified AES-CTR...

02

Observed that encryption XORs ciphertext with random...

03

Modeled this as a missing-value oracle: for fixed...

04

Validated the oracle locally with a dummy proof.

05

Tested a shared-keystream parallel collection branch...

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.

100% coverage
Evidence verdict

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.

  • Crypto/Broken-Decryptor/writeup.md
  • htb-challenge/Crypto/Broken-Decryptor/notes.md
  • htb-challenge/Crypto/Broken-Decryptor/memory-summary.md
  • htb-challenge/Crypto/Broken-Decryptor/hypothesis-board.md

Technical Walkthrough

Writeup

Challenge

  • Name: Broken-Decryptor
  • Category: Crypto
  • Difficulty: Medium
  • Mode: hybrid

Summary

The provided source implements an AES-CTR service with a broken decrypt menu and a flawed encrypt menu. The encrypt function resets CTR to the same IV for each request, then XORs the ciphertext with random bytes where 0x00 is replaced by 0xff.

That replacement creates a missing-value oracle: for any fixed plaintext byte and position, every output byte can appear except plaintext_byte xor keystream_byte. Repeating zero encryptions and flag encryptions reveals two missing-value vectors; XORing those vectors recovers the flag.

The live deployment used fresh key/IV state per connection, so samples could not be mixed across sockets. The final solve grouped samples per connection, computed possible flag-byte candidates from each connection, and intersected the candidate sets until every flag byte was unique.

Artifact Inventory

  • files/a12c734a-18b6-4899-89cf-e9bc932b3f4b.zip: original challenge archive.
  • analysis/extracted/challenge.py: complete Python service source.
  • Remote service: <TARGET>:31068.
  • Hashes and file type details are in analysis/artifact-inventory.json, analysis/file-types.txt, and analysis/sha256sums.txt.

Analysis

Relevant source behavior from analysis/extracted/challenge.py:

  • key and iv are generated at process startup.
  • encrypt(data) constructs a fresh AES-CTR object with the same counter initial value each call.
  • The encrypt output is:
text
data xor keystream xor otp
  • otp comes from os.urandom(len(data)).replace(b'\x00', b'\xff').
  • Therefore an OTP byte can never be zero, and byte value 0xff is twice as likely as ordinary values.
  • For fixed plaintext and position, the impossible output value is plaintext_byte xor keystream_byte.

The decrypt path is not useful. main() passes decrypt(unhex(ct)), so decrypt() receives bytes, but then calls data.encode(), which raises and is swallowed by the broad exception handler.

Local validation:

  • analysis/local-simulation.txt and analysis/local-simulation-latest.txt show the missing-value recovery works against a dummy flag under the same distribution.
  • analysis/remote-solve.txt closed the first remote branch: mixing multiple sockets as if they shared one keystream produced impossible evidence for a single keystream.
  • analysis/remote-intersection-solve.txt shows the final per-connection intersection run. Round one resolved all positions uniquely.

Solve

The solve script is solve/solve.py.

The final remote command used:

bash
cd <local workspace>
python3 -u solve/solve.py \
  --host <TARGET> \
  --port 31068 \
  --strategy intersection \
  --workers 20 \
  --per-worker-samples 750 \
  --rounds 2 \
  --output loot/flag-candidate.txt

The script:

  1. Opens independent connections.
  2. For each connection, collects repeated get_flag and zero-plaintext encryption samples.
  3. Builds candidate flag-byte sets from missing output values within that connection only.
  4. Intersects candidate sets across connections.
  5. Stops when every position has one printable candidate and the result matches the expected HTB flag format.

The harness then copied the validated candidate to loot/flag.txt.

Flag

Raw flag is stored in loot/flag.txt and intentionally not reproduced here.

Lessons

  • When randomness excludes one byte value, repeated outputs can become a missing-value oracle.
  • Do not assume source-level process globals are shared across remote sockets; validate deployment behavior.
  • If each connection has independent crypto state, compute per-connection candidate sets and combine only the final plaintext candidates.

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: Broken-Decryptor
  • Category: Crypto
  • Difficulty: Medium
  • Mode: hybrid
  • Remote instance: <TARGET>:31068
  • Start time: 2026-06-13T12:57:32Z
  • 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

FileSizeSHA256TypeNotes
files/a12c734a-18b6-4899-89cf-e9bc932b3f4b.zip993<hash redacted>Zip archive data, at least v2.0 to extract, compression method=deflatezip entries: 1 shown in artifact inventory JSON

Evidence Ledger

TimeActionOutput/FileFindingConfidenceNext
2026-06-13T12:57:32Zharness initchallenge-state.jsonWorkspace initialized with deterministic state fileHighInventory artifacts
2026-06-13T12:57:51Zartifact inventoryanalysis/artifact-inventory.json1 artifact(s) inventoriedHighBuild or update hypotheses
2026-06-13T12:58:53Zhypothesis recordedhypothesis-board.mdUse repeated encryptions as a missing-value oracle: query encrypt(00..00) to recover the fixed AES-CTR keystream byte-by-byte, query get_flag repeatedly to recover flag^keystream byte-by-byte, then XOR the two missing-value vectors.HighBuild a local simulator from challenge.py semantics with a dummy flag, verify recovered flag, then run the same collection against the live service.
2026-06-13T12:58:53Zsource auditanalysis/source-audit.mdSource audit recordedHighGate before exploit
2026-06-13T12:59:49Zresearch taskanalysis/research/task-20260613T125949136851Z-b2449c50.mdResearch task created for advisory investigationMediumRecord research output
2026-06-13T12:59:49Zresearch recordanalysis/research/research-records.mdResearch tagged MATCHEDMediumValidate against current evidence
2026-06-13T13:01:11Zcheckpoint recordedanalysis/checkpoint-analysis-20260613T130111405483Z-46d8b286.mdCheckpoint for ANALYSISHighUse checkpoint to drive next decision
2026-06-13T13:01:11Zinstrumentation plananalysis/instrumentation-plan.mdProve and exploit the missing-value oracle created by AES-CTR keystream reuse plus nonzero random mask bytes.HighStop if the service closes, sample lengths change, or missing-value sets do not converge by --max-samples.
2026-06-13T13:01:44Zlocal memory recordanalysis/local-memory-records.mdPrior local notes reviewed as fallback/advisory contextMediumValidate against current evidence
2026-06-13T13:01:44Zevaluatoranalysis/evaluator-20260613T130144259174Z-2e5e8db7.mdProceedHighRun gate before exploit, execute solve.py against the live service, then capture-flag.
2026-06-13T13:08:51Zinstrumentation plananalysis/instrumentation-plan.mdProve and exploit the missing-value oracle created by AES-CTR keystream reuse plus nonzero random mask bytes.HighStop if sample lengths change, workers error repeatedly, or missing-value sets do not converge by --max-samples.
2026-06-13T13:08:51Zevaluatoranalysis/evaluator-20260613T130851440007Z-44ed1b5e.mdProceedHighRun exploit gate again, execute parallel remote solve, then capture-flag.
2026-06-13T13:16:07Zbranch closedhypothesis-board.mdAggregating zero-plaintext samples across workers observed all 256 byte values at some positions, which is impossible for one fixed keystream. The live deployment appears to issue fresh key/iv per connection.HighRerank hypotheses
2026-06-13T13:16:07Zinstrumentation plananalysis/instrumentation-plan.mdRecover the flag from per-connection missing-value candidate intersections without mixing different key/iv states.HighStop if no worker returns candidate sets, if candidate intersections do not converge within configured rounds, or if recovered bytes fail HTB format.
2026-06-13T13:16:07Zevaluatoranalysis/evaluator-20260613T131607422286Z-c15c72d6.mdProceedHighRun gate, execute intersection collector, then capture-flag if a single HTB-format candidate is produced.
2026-06-13T13:46:45Zflag captureloot/flag.txtHTB-format flag captured; raw value kept in loot onlyHighWrite solution and run completion gate
2026-06-13T13:48:02Zcompletion gatechallenge-state.jsonCompletion gate passed; state marked COMPLETEHighOptional sanitized memory summary approval

Key Findings

  • analysis/extracted/challenge.py is the full service source.
  • The service uses AES-CTR, but recreates the counter from the same iv on every encryption inside one process.
  • encrypt(data) returns AES_CTR(data) xor otp, where otp is random bytes with 0x00 replaced by 0xff.
  • Because otp can never contain 0x00, each byte position has exactly one impossible output value for a fixed plaintext and keystream.
  • Repeated zero-plaintext encryption samples reveal the missing keystream byte per position.
  • Repeated flag encryption samples reveal the missing flag_byte xor keystream_byte per position.
  • XORing those two missing-value vectors recovers the flag for one key/IV context.
  • The live service did not behave as a single global key/IV across sockets; mixed parallel samples observed all 256 values for zero plaintext, so that branch was closed.
  • The final solve used independent per-connection candidate sets and intersected them across 20 connections with 750 samples each.
  • solve/solve.py reproduces the final collection and writes the candidate to loot/flag-candidate.txt.

RAG / Advisory Memory

RAG output is advisory only. Record evaluated retrievals with:

bash
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: Crypto
  • Challenge: Broken-Decryptor
  • 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.

  1. Audited the Python source and identified AES-CTR reset to the same IV on each encryption request.
  2. Observed that encryption XORs ciphertext with random bytes after replacing 0x00 with 0xff.
  3. Modeled this as a missing-value oracle: for fixed plaintext and byte position, exactly one output value is impossible.
  4. Validated the oracle locally with a dummy flag.
  5. Tested a shared-keystream parallel collection branch and closed it when live samples showed fresh key/IV behavior per connection.
  6. Solved by grouping samples per connection, deriving candidate plaintext byte sets, and intersecting candidates across independent connections.

Reusable Lessons

  • Missing-byte distributions can be exploitable even when ciphertext is masked with fresh randomness.
  • Check remote deployment state before mixing samples across connections.
  • Candidate-set intersection can recover plaintext when each connection has independent keystream but the hidden message is constant.

Dead Ends

  • Treating all remote connections as one shared key/IV context. Live zero-plaintext samples eventually observed all 256 values, which cannot happen for one fixed missing-value oracle.

Tool Quirks

  • The local environment lacked some optional crypto tools (sympy, z3, sage, pwntools, hashcat), but Python and PyCryptodome were sufficient.
  • The remote service was slow per request, so the final collector needed bounded parallel per-connection sampling.

Evidence Paths

  • analysis/extracted/challenge.py
  • analysis/source-audit.md
  • analysis/research/direct-source-crypto-model.md
  • analysis/local-simulation.txt
  • analysis/remote-solve.txt
  • analysis/remote-intersection-solve.txt
  • solve/solve.py
  • loot/flag.txt

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.

RankPathEvidenceMissing ProofCheapest ValidationConfidenceStatus
1Use repeated encryptions as a missing-value oracle: query encrypt(00..00) to recover the fixed AES-CTR keystream byte-by-byte, query get_flag repeatedly to recover flag^keystream byte-by-byte, then XOR the two missing-value vectors.challenge.py resets AES-CTR with fixed key/iv each request and masks ciphertext with a random byte that can never be zero after replacement.Need local simulation to determine enough samples for reliable missing-value recovery and a remote script that preserves a single connection/process.Build a local simulator from challenge.py semantics with a dummy flag, verify recovered flag, then run the same collection against the live service.HighActive

Closed Branches

BranchEvidence TestedFailure OutputReason ClosedRevisit Condition
Shared-keystream parallel collection across multiple socketsanalysis/remote-solve.txtanalysis/remote-solve.txtAggregating zero-plaintext samples across workers observed all 256 byte values at some positions, which is impossible for one fixed keystream. The live deployment appears to issue fresh key/iv per connection.Only revisit if a single server process/global key is proven live; otherwise keep samples grouped per connection.

Technical analogy

How to remember this solve

Think of the challenge like a locked box where the lock is mathematical but slightly flawed. The goal is not to smash the box; it is to notice which part of the lock repeats, leaks, or trusts the wrong assumption.

For Broken Decryptor, 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.