Challenge / Mobile

Jigsaw

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

EasyPublished 2025-02-09Sanitized local writeup

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.

Jigsaw sanitized attack graph

Walkthrough flow

01

Artifact review

02

Hypothesis

03

Validated solve path

04

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.

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.

  • 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.so partthree_*

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.apk
  • analysis/static/kernel-strings-all.txt
  • analysis/jadx/sources/com/example/menascyber/MainActivity.java
  • analysis/jadx/sources/com/example/menascyber/MainActivityKt.java
  • analysis/jadx/sources/com/example/menascyber/piecesOf.java
  • analysis/static/libmenascyber-x86_64-objdump-head.txt
  • analysis/static/libmenascyber-x86_64-rodata.txt
  • analysis/local-validation/offline-decryption.md

Analysis

kernel-strings-all.txt exposes the overall structure in services.dart:

  • partone() deterministically shuffles hardcoded byte lists
  • getparttwo() calls Android MethodChannel("parttwo")
  • getAESKey() / getAESIV() call native functions partthree_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:

  • MainActivity wires MethodChannel("parttwo") into MainActivityKt.get_parttwo()
  • MainActivityKt returns slices from piecesOf.<secret redacted>getParttwo_1() and getParttwo_2()
  • piecesOf shows 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:

  1. Rebuilds the Dart partone shuffle exactly.
  2. Rebuilds the Android parttwo arrays using the signed-byte Kotlin shift semantics.
  3. Rebuilds the native partthree arrays from the rodata-backed transform.
  4. Concatenates the documented key/IV slices.
  5. AES-CBC decrypts the embedded base64 ciphertext.
  6. 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

FileSizeSHA256TypeNotes
files/a12c7374-a80b-460b-b665-458ba657c7e5.zip42609295<hash redacted>Zip archive data, at least v1.0 to extract, compression method=storezip entries: 2 shown in artifact inventory JSON

Evidence Ledger

TimeActionOutput/FileFindingConfidenceNext
2026-06-08T00:47:22Zharness initchallenge-state.jsonWorkspace initialized with deterministic state fileHighInventory artifacts
2026-06-08T00:47:38Zartifact inventoryanalysis/artifact-inventory.json1 artifact(s) inventoriedHighBuild or update hypotheses
2026-06-08T00:48:11Zhypothesis recordedhypothesis-board.mdStatic APK reverse engineering: identify framework, decompile resources/code, search for scattered secret fragments and validation logic, then reconstruct the flag offline.MediumExtract 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:47Zlocal memory recordanalysis/local-memory-records.mdPrior local notes reviewed as fallback/advisory contextMediumValidate against current evidence
2026-06-08T00:50:37Zevaluatoranalysis/evaluator-20260608T005037366650Z-a291588d.mdValidate firstHighUse Flutter decompilation or kernel string/constant extraction to recover partone and parttwo, then run offline AES decrypt.
2026-06-08T00:51:07Zcheckpoint recordedanalysis/checkpoint-triage-20260608T005107539520Z-b6dddf22.mdCheckpoint for TRIAGEHighUse checkpoint to drive next decision
2026-06-08T00:52:44Zsource auditanalysis/source-audit.mdSource audit recordedHighGate before exploit
2026-06-08T01:11:54Zevaluatoranalysis/evaluator-20260608T011154817425Z-612d17d0.mdProceedHighCapture the offline flag candidate from loot/flag-candidate.txt and complete the workspace.
2026-06-08T01:12:02Zflag captureloot/flag.txtHTB-format flag captured; raw value kept in loot onlyHighWrite solution and run completion gate
2026-06-08T01:12:39Zcompletion gatechallenge-state.jsonCompletion gate passed; state marked COMPLETEHighOptional 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 native libmenascyber.so libraries.
  • Flutter kernel strings expose challenge-specific paths and symbols for package:jigsaw/flag.dart, package:jigsaw/main.dart, and package:jigsaw/services.dart.
  • jadx decompilation recovered the Android MethodChannel("parttwo") handler in MainActivity and MainActivityKt, plus the backing obfuscation helper in piecesOf.
  • parttwo is 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.
  • partthree comes from libmenascyber.so and 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.dart computes 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:

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: 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.

RankPathEvidenceMissing ProofCheapest ValidationConfidenceStatus
1Static 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.MediumActive

Closed Branches

BranchEvidence TestedFailure OutputReason ClosedRevisit 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

FileSizeSHA256TypeNotes
files/a12c7374-a80b-460b-b665-458ba657c7e5.zip42609295<hash redacted>Zip archive data, at least v1.0 to extract, compression method=storezip entries: 2 shown in artifact inventory JSON

Evidence Ledger

TimeActionOutput/FileFindingConfidenceNext
2026-06-08T00:47:22Zharness initchallenge-state.jsonWorkspace initialized with deterministic state fileHighInventory artifacts
2026-06-08T00:47:38Zartifact inventoryanalysis/artifact-inventory.json1 artifact(s) inventoriedHighBuild or update hypotheses
2026-06-08T00: <REDACTED>, decompile resources/code, search for scattered secret fragments and validation logic, then reconstruct the flag offline.MediumExtract 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:47Zlocal memory recordanalysis/local-memory-records.mdPrior local notes reviewed as fallback/advisory contextMediumValidate against current evidence
2026-06-08T00:50:37Zevaluatoranalysis/evaluator-20260608T005037366650Z-a291588d.mdValidate firstHighUse Flutter decompilation or kernel string/constant extraction to recover partone and parttwo, then run offline AES decrypt.
2026-06-08T00:51:07Zcheckpoint recordedanalysis/checkpoint-triage-20260608T005107539520Z-b6dddf22.mdCheckpoint for TRIAGEHighUse checkpoint to drive next decision
2026-06-08T00:52:44Zsource auditanalysis/source-audit.mdSource audit recordedHighGate before exploit
2026-06-08T01: <REDACTED>
2026-06-08T01: <REDACTED>
2026-06-08T01:12:39Zcompletion gatechallenge-state.jsonCompletion gate passed; state marked COMPLETEHighOptional 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 native libmenascyber.so libraries.
  • Flutter kernel strings expose challenge-specific paths and symbols for package: <REDACTED>, package: <REDACTED>, and `package: <REDACTED>
  • jadx decompilation recovered the Android MethodChannel("parttwo") handler in MainActivity and MainActivityKt, plus the backing obfuscation helper in piecesOf.
  • parttwo is 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.
  • partthree comes from libmenascyber.so and 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.dart computes 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:

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.

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.