# Reverse Engineering

> Use when statically analysing an artefact you are authorised to examine — fingerprinting, decompilation, call-flow tracing.

- Skill: `yogvidwankhede/reverse-engineering` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add yogvidwankhede/reverse-engineering`
- Raw SKILL.md: https://api.skillmd.com/api/skills/yogvidwankhede/reverse-engineering/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: yogvidwankhede (https://skillmd.com/u/yogvidwankhede)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/yogvidwankhede/reverse-engineering

---


<!--
  Generated by Vishwakarma. Do not edit this file directly.
  Edit the source skill and run `vishwakarma sync` to regenerate.
-->

# Reverse Engineering and Structural Analysis

Fingerprint before you commit effort. A ten-second check that tells you whether the expensive path
is the right path is worth more than any optimisation applied to the wrong path, because the cost of
the wrong path is not its runtime — it is the time spent misreading its empty output as a hard
problem.

## 1. Scope and authorisation

Establish the boundary before the first command, because the technique is identical on both sides of
it and only the authorisation differs. Write down three things: what artefact you are examining, who
authorised the examination, and what you are permitted to do with the findings. If any of the three
is unclear, the analysis has not started.

Four situations are normally in scope: **your own software**, including builds you shipped and want
to verify; **publicly distributed binaries** examined for competitive or research purposes, where
you observe behaviour without redistributing code; **security review under a written scope
agreement** naming the artefact and the permitted activity; and **interoperability work** where an
undocumented format or protocol must be understood to implement against.

**The boundary is crossed the moment the goal shifts from understanding structure to defeating a
control.** Extracting credentials belonging to other people's users, circumventing content
protection, patching a binary to bypass licensing or integrity checks, and instrumenting a running
application to disable safety mechanisms are all outside this skill — and are a different activity
in practice, operating on runtime state rather than static structure. Nothing below produces them as
a by-product. One further discipline point: findings are **claims about the artefact**, not claims
about the vendor's servers. A base URL in a build-configuration class tells you the client is
compiled to talk to that host; it does not authorise you to send traffic to it.

## 2. Phase 0 — fingerprint before installing anything

The first action on any package is a file listing, not a decompilation, and what you are looking for
is the **framework marker**, because the framework determines the whole toolchain and the toolchains
do not overlap.

| Marker in the archive | Framework | Where the logic is |
|---|---|---|
| `lib/<abi>/libflutter.so` plus `libapp.so` | Flutter | AOT-compiled Dart; needs native tooling |
| `libhermes.so`, `assets/index.android.bundle`, `libreactnativejni.so` | React Native | Hermes bytecode or a JS bundle |
| `assets/www/cordova.js`, `assets/www/index.html` | Cordova / Ionic | HTML, CSS and JS in a WebView |
| `*.dll` with CLI headers, `assemblies/` | Xamarin / MAUI | CIL; needs a .NET decompiler |
| `classes*.dex` with substantial content and none of the above | Native Android | JVM bytecode; the Java path is correct |

```bash
unzip -l app.apk | grep -Ei 'libflutter|libapp\.so|libhermes|index\.android\.bundle|cordova\.js|\.dll'
```

If the fingerprint returns Flutter, React Native, Cordova or Xamarin, **stop and switch toolchains**.
Do not run the decompiler anyway "to see what comes out": what comes out is a plausible near-empty
result that costs an hour to disbelieve. React Native and Cordova are cheapest to analyse, their
logic being JavaScript and often only minified; Flutter is the most expensive.

## 3. What the fingerprint does not settle

Three cheap readings finish the triage, and **fingerprint-and-decompile** carries the grep lists and
the thresholds. Scan strings rather than reading code, because obfuscators rename only the symbols
they control while library machinery resolves by string at runtime — reflection lookups, service
loader entries, annotation retention, resource paths — so those literals are load-bearing and
removing them breaks the application. A package whose every class is named `a` still contains the
literal `retrofit2`, and `@SerializedName("user_id")` survives on a class called `c.d.a` because
the wire format depends on it.

Estimate obfuscation from the *shape* of the package tree before reading any code: an unobfuscated
root reads like an organisation, an obfuscated one is littered with one- and two-character names,
because the renamer allocates from the shortest alphabet to minimise size. More than roughly a
third of top-level packages at one or two characters means obfuscated. Where a scan for `http`
returns almost nothing on an application that plainly makes network calls, the strings are
encrypted and static endpoint extraction will not work — say so and stop rather than grinding.

Then confirm a JDK at version 17 or later before anything else, since modern decompilers fail on
older runtimes with class-version errors that read like a corrupt input, and run the **primary
decompiler alone**, escalating to a secondary only for classes where the primary warned or produced
obviously wrong control flow. One further trap is numeric rather than judgemental: modern
distribution splits an application into a base module plus configuration splits, and decompiling
the downloaded wrapper succeeds while yielding a small tree that reads as "heavily obfuscated".
**Fewer than roughly ten source files alongside one or more inner `.apk` entries means you
decompiled the wrapper** — re-target `base.apk`.

## 4. Tier the output, because inventory and analysis have different costs

The survey, name-recovery and call-tracing phases are in **analysis-and-reporting**; what belongs
here is the policy that governs how much of their result you write down.

**Tier one is a mandatory flat inventory**: every endpoint, every component with its exported
status, every permission, every library, and every build-configuration value, one row each and no
prose. It is complete by construction, scannable, and diffable against a later build. **Tier two is
deep detail capped at roughly ten items**, spent on the anomalies and on the flows the requester
actually named.

The split exists because inventory is near-free per item and analysis is not, so mixing them takes
the cost of analysis at the volume of inventory — a report that narrates all 184 endpoints costs
days and is read by nobody, while the same 184 as rows plus ten analysed flows costs hours and
answers the question. State coverage numerically rather than adjectivally: "184 of an estimated 190
endpoints; 6 classes failed to decompile" is a coverage claim, "comprehensive" is not. Throughout,
distinguish evidence from inference at every link, and label an inference as one.

## 5. The general shape

Fingerprint before committing, abort into different tooling when the fingerprint says so, tier the
output. This applies to any investigation with an expensive default step — a large unfamiliar
codebase, a failing test suite — because triage is not a preliminary to the work; it is the step
that decides which work is worth doing.

## Rules

### MUST NOT — Stop at understanding structure; do not cross into defeating a control.

*Why:* That transition is the boundary of legitimate analysis, and it is also a different activity in practice: those techniques operate on runtime state rather than static structure, so nothing in a static workflow produces them as a by-product and reaching them requires a deliberate change of method. Findings here are claims about the artefact, not authorisation to act against a live service or its users.

Incorrect:

```markdown
Patched the integrity check in `SecurityManager.a()` and hooked the licence callback at runtime to confirm the bypass works.
```

Correct:

```markdown
Recorded that an integrity check exists in a class reached from the launcher activity; noted the mechanism statically and stopped. Bypassing it is outside scope.
```

### MUST — Record the artefact under analysis, the source of authorisation, and the permitted use of findings before running the first command.

*Why:* The technique is identical on both sides of the authorisation boundary and only the authorisation differs, so nothing later in the workflow will surface the question for you — there is no error, no failed step, no tooling signal. Writing the three facts down before the first command is the only point at which the omission is still cheap to notice.

Incorrect:

```markdown
Analysing competitor.apk — pulled from the store.
```

Correct:

```markdown
Artefact: competitor.apk 8.2.1, publicly distributed. Authorisation: competitive research on a public binary; no redistribution of code. Permitted use: internal structural summary only.
```

### MUST — List the archive and grep for framework markers before installing or invoking any decompiler.

*Why:* The framework determines the entire toolchain and the toolchains do not overlap, so the listing decides which expensive step is even correct. It costs seconds, runs with no dependencies, and is the only cheap moment at which the wrong path is still avoidable — after decompilation you are reading output rather than choosing a tool.

Incorrect:

```bash
decompile app.apk -d out/   # first command run against an unknown package
```

Correct:

```bash
unzip -l app.apk | grep -Ei 'libflutter|libapp\.so|libhermes|index\.android\.bundle|cordova\.js|\.dll'
```

### MUST — When the framework marker indicates Flutter, React Native, Cordova, or Xamarin, stop and switch toolchains rather than running a Java decompiler to see what comes out.

*Why:* A Java decompiler pointed at a Flutter package does not error. It emits a few thin wrapper classes and exits, and that output is indistinguishable from a tool defeated by obfuscation — so the natural next moves are more settings and more engines, and the hour ends in a false conclusion. A wrong tool that returns nothing is more expensive than one that crashes, because a crash is unambiguous.

Incorrect:

```bash
# libflutter.so present; running the Java decompiler anyway
decompile app.apk -d out/
```

Correct:

```bash
# libflutter.so + libapp.so: logic is AOT-compiled Dart, not Dalvik.
# Switching to native tooling and a snapshot parser; Java path abandoned.
```

### MUST — Treat a decompiled tree of fewer than roughly ten source files alongside inner .apk entries as the wrapper, and re-target the base archive before drawing any conclusion.

*Why:* Modern distribution ships a base module plus configuration splits inside a bundle wrapper, and decompiling the wrapper succeeds without error — it simply yields the installer or loader logic. A tiny clean result therefore reads as "small or heavily obfuscated" when it means the application was never opened, and the file-count heuristic is what separates the two.

Incorrect:

```bash
# 9 source files recovered; reporting the app as heavily obfuscated
```

Correct:

```bash
unzip -l download.apk | grep -c "\.apk$"   # inner splits present
unzip -p download.apk base.apk > base.apk   # re-target and decompile this
```

### MUST — Anchor call-flow traces on annotation values, URL and path literals, and resource names rather than on method or class names in an obfuscated tree.

*Why:* Obfuscators rename the symbols they own but cannot touch what the runtime resolves by string: a Retrofit method is @GET("v3/users/{id}/orders") even when its interface is named f, because the library reads that value at request time, and R.layout.activity_checkout survives because the resource table resolves by name. Following renamed method names instead means navigating a wide graph in which no node carries meaning.

Incorrect:

```text
f.a() calls g.b() calls h.c() — traced by call graph, meaning unknown
```

Correct:

```text
Anchor: interface `f` declares @GET("v3/orders") [evidence]. References to `f` appear in `p.q` alongside R.layout.activity_checkout [evidence]; checkout screen triggers it [inference].
```

### MUST — When a scan for URL literals returns almost nothing on an application that plainly makes network calls, declare string encryption and static endpoint extraction infeasible rather than continuing.

*Why:* Aggressive configurations decrypt literals at runtime, so the constant pool holds nothing to find and no amount of further static searching changes that — the information is not in the artefact you are reading. The scan result is a reliable detector of the condition, and reporting it is a usable finding, whereas grinding produces hours of work and an empty section.

Incorrect:

```markdown
No endpoints found yet — continuing to search the decompiled tree.
```

Correct:

```markdown
Strings are encrypted: a scan for "http" across the tree returns 2 hits on an application with an OkHttp interceptor chain. Static endpoint extraction is infeasible; endpoint inventory not delivered.
```

### MUST — Deliver an exhaustive flat inventory as tier one and cap deep detail at roughly ten items, spent on anomalies and on the flows the requester named, with coverage stated numerically.

*Why:* Inventory and analysis have different cost curves: inventory is near-free per item once extraction is written, while analysis costs real time to produce and real attention to read. Writing a paragraph per endpoint takes the worst of both — the cost of analysis at the volume of inventory — and yields a document nobody reads, which is worth no more than no analysis at all.

Incorrect:

```markdown
## Endpoint 1 of 184 … ## Endpoint 2 of 184 … (one section each)
```

Correct:

```markdown
## Inventory
| Method | Path | Declaring class |
…184 rows…

## Detail (7 items)
Exported `SyncReceiver` with no permission guard; debug endpoint in a release build; …

Coverage: 184 of an estimated 190 endpoints; 6 classes failed to decompile (appendix).
```

### SHOULD — Run one decompiler alone and escalate to a second only for the specific classes that warned, bailed out, or produced visibly wrong control flow; retain the output rather than regenerating it.

*Why:* Decompilation is the most expensive step in the workflow, and engines diverge only on a narrow recognisable set — nested lambdas, complex generic bounds, long stream pipelines, switch on strings, Kotlin suspend state machines. Running both by default doubles that cost to produce byte-identical output for nearly every class, then leaves you diffing two large trees to find the few that differ.

Incorrect:

```bash
primary app.apk -d out-a/ && secondary app.apk -d out-b/ && diff -r out-a out-b
```

Correct:

```bash
primary app.apk -d out/ 2> warnings.txt
# then run the secondary only on the classes named in warnings.txt
```

### SHOULD — Label every architectural claim derived from class-name shape or recovered metadata as an inference with its evidence, and apply a recovered name mapping as an overlay rather than editing the decompiled tree.

*Why:* Name recovery is uneven — high for convention-following classes whose suffix and package position cross-check the metadata, low for data-transfer objects whose arbitrary names have no shape to check against — so an unlabelled claim built on it cannot be weighed by the reader. Keeping the mapping as an overlay preserves the property that every claim can still be traced back to the original artefact.

Incorrect:

```markdown
The app uses MVVM with a repository layer.
```

Correct:

```markdown
Inference: MVVM with a repository layer. Evidence: 23 classes recovered from @kotlin.Metadata with the `*ViewModel` suffix under `ui/`, and 9 `*Repository` names sitting between `*Api` and `*Dao`.
```

## Before reporting completion

Run these checks against your own output. Answer each question explicitly rather than
assuming the answer, because the point of the exercise is to notice what you did not
notice while building.

### Confirm the analysis is authorised and has stayed static and structural. (blocking)

- Are the artefact, the source of authorisation, and the permitted use of findings written down, and was that done before the first command?
- Did anything in this run move from reading structure to defeating a control — credential extraction, protection circumvention, patching a check, or runtime instrumentation?
- Is every finding phrased as a claim about the artefact rather than as licence to send traffic to a host named in it?
- If any part of the request required crossing that boundary, was it declined and named rather than quietly attempted?

### Confirm the fingerprint ran first and the right archive reached the right engine. (blocking)

- Was an archive listing checked for framework markers before any decompiler was installed or invoked?
- If a Flutter, React Native, Cordova or Xamarin marker was present, did the workflow abort into the correct toolchain rather than decompiling anyway?
- Did the decompiled tree contain fewer than about ten source files alongside inner .apk entries, and if so was the base archive re-targeted?
- Was the JDK confirmed at 17 or later, and was the primary decompiler run alone with the secondary reserved for classes that warned or broke?
- Was the raw decompiled output retained for later phases rather than regenerated?

### Confirm detection, name recovery, and traces rest on things that could not be renamed.

- Was a string scan run for network, dependency-injection and serialisation signals, and was the obfuscation level estimated from the share of one- and two-character package names?
- Did a scan for URL literals come back near-empty on a networked application, and if so was string encryption declared and extraction stopped?
- Is every trace anchored on an annotation value, URL literal, or resource name, and does each link say whether it is evidence or inference?
- Was @kotlin.Metadata mined where the application is Kotlin, with recovery confidence reported separately for convention-following classes and data-transfer objects?
- Was any recovered mapping applied as an overlay rather than by editing the decompiled tree in place?

### Confirm the report is an exhaustive inventory plus rationed depth, with coverage stated.

- Does the report contain a complete flat inventory of endpoints, components with exported status, permissions, libraries, and build-configuration values, one row each?
- Is deep detail capped at roughly ten items, and is each of them either an anomaly or a flow the requester named?
- Was the manifest read before any source file, and were all BuildConfig classes read, the application’s and each library’s?
- Is coverage stated numerically, including what failed to decompile or could not be recovered?

## Further reference

These are not loaded by default. Read one only when its question is the question you
currently have.

- `references/fingerprint-and-decompile.md` — What must be established before the first command, how do I tell in ten seconds which toolchain an artefact needs, which libraries can I detect without reading code, how obfuscated is this, and why did decompiling produce nine source files?
- `references/analysis-and-reporting.md` — What do I read first once the right archive is decompiled, how much of an obfuscated Kotlin application can be renamed back, how do I trace a flow with no readable method names, and how should the report be shaped so it is read?

