Jigsaw
Jigsaw is a sanitized challenge note from the local HTB archive, organized for quick review by category, difficulty, evidence flow, and reusable operator
Scenario
Jigsaw attack path
Jigsaw 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 Mobile evidence, validation, and reusable operator lessons.
Walkthrough flow
Artifact review
Hypothesis
Validated solve path
Proof captured
Source coverage
High source coverage
Status: complete. This article is generated from 6 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.
- Mobile/Jigsaw/writeup.md
- htb-challenge/Mobile/Jigsaw/notes.md
- htb-challenge/Mobile/Jigsaw/memory-summary.md
- htb-challenge/Mobile/Jigsaw/hypothesis-board.md
- HTB/_knowledge/exports/ctf-lightrag-latest-203412/documents/challenge__Mobile__Jigsaw__memory-summary.md.026c8fa65d.md
- HTB/_knowledge/exports/ctf-lightrag-latest-203412/documents/challenge__Mobile__Jigsaw__notes.md.0c311f48b8.md
Technical Walkthrough
Writeup
Challenge
- Name: Jigsaw
- Category: Mobile
- Difficulty: Easy
- Mode: file
Summary
This Flutter APK splits the AES key and IV across three layers:
- Flutter/Dart
partone - Android
MethodChannel("parttwo") - Native FFI
libmenascyber.sopartthree_*
The app really does decrypt the embedded ciphertext, but flag.dart returns a placeholder string instead of the computed plaintext. The solve is therefore an offline reconstruction of the AES material followed by local AES-CBC decryption.
Artifact Inventory
Relevant local artifacts:
files/extracted/mobile_jigsaw/Jigsaw.apkanalysis/static/kernel-strings-all.txtanalysis/jadx/sources/com/example/menascyber/MainActivity.javaanalysis/jadx/sources/com/example/menascyber/MainActivityKt.javaanalysis/jadx/sources/com/example/menascyber/piecesOf.javaanalysis/static/libmenascyber-x86_64-objdump-head.txtanalysis/static/libmenascyber-x86_64-rodata.txtanalysis/local-validation/offline-decryption.md
Analysis
kernel-strings-all.txt exposes the overall structure in services.dart:
partone()deterministically shuffles hardcoded byte listsgetparttwo()calls AndroidMethodChannel("parttwo")getAESKey()/getAESIV()call native functionspartthree_1/partthree_2- the final AES key and IV are assembled by slicing and concatenating those three fragments
jadx decompilation resolves the missing Android segment:
MainActivitywiresMethodChannel("parttwo")intoMainActivityKt.get_parttwo()MainActivityKtreturns slices frompiecesOf.<secret redacted>getParttwo_1()andgetParttwo_2()piecesOfshows the exact byte arrays and the transform used to derive them
The important pitfall is semantic, not structural: the Kotlin helper uses signed-byte right shifts. Treating that helper as an unsigned rotate produces the wrong key material and fails decryption.
The native library confirms the last fragment. libmenascyber.so exposes partthree_1 and partthree_2, and the objdump/rodata pair is enough to reconstruct their byte output offline.
Finally, flag.dart contains the decrypt call and then discards the result in favor of the literal placeholder string "Developer forgot to uncomment". That verifies the intended solve path: recover the real plaintext outside the app.
Solve
The reproducible solver is solve/solve.py.
It:
- Rebuilds the Dart
partoneshuffle exactly. - Rebuilds the Android
parttwoarrays using the signed-byte Kotlin shift semantics. - Rebuilds the native
partthreearrays from the rodata-backed transform. - Concatenates the documented key/IV slices.
- AES-CBC decrypts the embedded base64 ciphertext.
- Writes the raw candidate only to
loot/flag-candidate.txt.
Flag
Raw flag is stored in loot/flag.txt and intentionally not reproduced here.
Lessons
- Flutter release builds can still leak a lot of structural logic through kernel strings.
- Mixed-language mobile challenges often hide the key detail in language semantics rather than algorithm complexity.
- When Java/Kotlin byte shifts are involved, signedness needs to be treated as part of the cryptographic transform.
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: Jigsaw
- Category: Mobile
- Difficulty: Easy
- Mode: file
- Remote instance: none
- Start time: 2026-06-08T00:47:22Z
- 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/a12c7374-a80b-460b-b665-458ba657c7e5.zip | 42609295 | <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-08T00:47:22Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-08T00:47:38Z | artifact inventory | analysis/artifact-inventory.json | 1 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-08T00:48:11Z | hypothesis recorded | hypothesis-board.md | Static APK reverse engineering: identify framework, decompile resources/code, search for scattered secret fragments and validation logic, then reconstruct the flag offline. | Medium | Extract APK, inspect manifest/resources/classes/native libraries, run strings/file inventory, then choose jadx/apktool/IL2CPP/Flutter route based on artifact type. |
| 2026-06-08T00:48:47Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-08T00:50:37Z | evaluator | analysis/evaluator-20260608T005037366650Z-a291588d.md | Validate first | High | Use Flutter decompilation or kernel string/constant extraction to recover partone and parttwo, then run offline AES decrypt. |
| 2026-06-08T00:51:07Z | checkpoint recorded | analysis/checkpoint-triage-20260608T005107539520Z-b6dddf22.md | Checkpoint for TRIAGE | High | Use checkpoint to drive next decision |
| 2026-06-08T00:52:44Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-08T01:11:54Z | evaluator | analysis/evaluator-20260608T011154817425Z-612d17d0.md | Proceed | High | Capture the offline flag candidate from loot/flag-candidate.txt and complete the workspace. |
| 2026-06-08T01:12:02Z | flag capture | loot/flag.txt | HTB-format flag captured; raw value kept in loot only | High | Write solution and run completion gate |
| 2026-06-08T01:12:39Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
- The ZIP was moved from Downloads into
files/and extracted with the standard HTB archive password. - The extracted artifact is
files/extracted/mobile_jigsaw/Jigsaw.apk. - The APK is a Flutter Android application, with
kernel_blob.bin,libflutter.so, and custom nativelibmenascyber.solibraries. - Flutter kernel strings expose challenge-specific paths and symbols for
package:jigsaw/flag.dart,package:jigsaw/main.dart, andpackage:jigsaw/services.dart. jadxdecompilation recovered the AndroidMethodChannel("parttwo")handler inMainActivityandMainActivityKt, plus the backing obfuscation helper inpiecesOf.parttwois not a normal unsigned rotate. The Kotlin helper uses signed-byte right shifts, which changes the reconstructed bytes for values with the high bit set.partthreecomes fromlibmenascyber.soand matches the native rodata/objdump path already identified in the source audit.- Offline AES-CBC reconstruction succeeds once the three fragments are assembled in the order documented by
services.dart. flag.dartcomputes the decrypted value but intentionally returns the placeholder string"Developer forgot to uncomment", so extracting the ciphertext alone is not enough; the flag must be decrypted offline.- Full execution guidance is stored in
<secret redacted>md.
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: Mobile
- Challenge: Jigsaw
- Difficulty: Easy
- Source workspace:
<local workspace>
Validated Solve Chain
Concepts only. Do not include raw flags, reusable credentials, tokens, cookies, private keys, or live secrets.
1.
Reusable Lessons
-
Dead Ends
-
Tool Quirks
-
Evidence Paths
-
Ingestion Decision
- Proposed for LightRAG: yes/no
- 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 | Static APK reverse engineering: identify framework, decompile resources/code, search for scattered secret fragments and validation logic, then reconstruct the flag offline. | Archive contains a single Mobile APK artifact, Jigsaw.apk; scenario says hidden fragments/layers of logic/scattered clues. | Extract APK, inspect manifest/resources/classes/native libraries, run strings/file inventory, then choose jadx/apktool/IL2CPP/Flutter route based on artifact type. | Medium | Active |
Closed Branches
| Branch | Evidence Tested | Failure Output | Reason Closed | Revisit Condition |
|---|
Memory Summary
approval_required: true
Sanitized Memory Summary
Metadata
- Platform: HackTheBox Challenges
- Category: Mobile
- Challenge: Jigsaw
- Difficulty: Easy
- Source workspace:
<local workspace>
Validated Solve Chain
Concepts only. Do not include raw flags, reusable credentials, tokens, cookies, private keys, or live secrets.
1.
Reusable Lessons
-
Dead Ends
-
Tool Quirks
-
Evidence Paths
-
Ingestion Decision
- Proposed for LightRAG: yes/no
- Requires user approval before ingestion: yes
Notes
Notes
Scope
- Challenge: Jigsaw
- Category: Mobile
- Difficulty: Easy
- Mode: file
- Remote instance: none
- Start time: 2026-06-08T00:47:22Z
- 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/a12c7374-a80b-460b-b665-458ba657c7e5.zip | 42609295 | <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-08T00:47:22Z | harness init | challenge-state.json | Workspace initialized with deterministic state file | High | Inventory artifacts |
| 2026-06-08T00:47:38Z | artifact inventory | analysis/artifact-inventory.json | 1 artifact(s) inventoried | High | Build or update hypotheses |
| 2026-06-08T00: <REDACTED>, decompile resources/code, search for scattered secret fragments and validation logic, then reconstruct the flag offline. | Medium | Extract APK, inspect manifest/resources/classes/native libraries, run strings/file inventory, then choose jadx/apktool/IL2CPP/Flutter route based on artifact type. | |||
| 2026-06-08T00:48:47Z | local memory record | analysis/local-memory-records.md | Prior local notes reviewed as fallback/advisory context | Medium | Validate against current evidence |
| 2026-06-08T00:50:37Z | evaluator | analysis/evaluator-20260608T005037366650Z-a291588d.md | Validate first | High | Use Flutter decompilation or kernel string/constant extraction to recover partone and parttwo, then run offline AES decrypt. |
| 2026-06-08T00:51:07Z | checkpoint recorded | analysis/checkpoint-triage-20260608T005107539520Z-b6dddf22.md | Checkpoint for TRIAGE | High | Use checkpoint to drive next decision |
| 2026-06-08T00:52:44Z | source audit | analysis/source-audit.md | Source audit recorded | High | Gate before exploit |
| 2026-06-08T01: <REDACTED> | |||||
| 2026-06-08T01: <REDACTED> | |||||
| 2026-06-08T01:12:39Z | completion gate | challenge-state.json | Completion gate passed; state marked COMPLETE | High | Optional sanitized memory summary approval |
Key Findings
- The ZIP was moved from Downloads into
files/and extracted with the standard HTB archive password. - The extracted artifact is
files/extracted/mobile_jigsaw/Jigsaw.apk. - The APK is a Flutter Android application, with
kernel_blob.bin,libflutter.so, and custom nativelibmenascyber.solibraries. - Flutter kernel strings expose challenge-specific paths and symbols for
package: <REDACTED>,package: <REDACTED>, and `package: <REDACTED> jadxdecompilation recovered the AndroidMethodChannel("parttwo")handler inMainActivityandMainActivityKt, plus the backing obfuscation helper inpiecesOf.parttwois not a normal unsigned rotate. The Kotlin helper uses signed-byte right shifts, which changes the reconstructed bytes for values with the high bit set.partthreecomes fromlibmenascyber.soand matches the native rodata/objdump path already identified in the source audit.- Offline AES-CBC reconstruction succeeds once the three fragments are assembled in the order documented by
services.dart. flag.dartcomputes the decrypted value but intentionally returns the placeholder string"Developer forgot to uncomment", so extracting the ciphertext alone is not enough; the flag must be decrypted offline.- Full execution guidance is stored in
<secret redacted>md.
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.
Technical analogy
How to remember this solve
Think of the app like a packed suitcase. You unpack it, inspect the labels and hidden pockets, then trace which local file or network call contains the useful clue.
For Jigsaw, 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.