tihanyin/rex-skill/reverse-engineering
Claude Code and agent skill by tihanyin/rex-skill
- community
Checking that the SKILL.md file is still in the repository.
Evidence-first reverse engineering of compiled programs to find their defects — triage from strings and imports, symbol recovery on stripped code, decompilation read against disassembly, source/sink/guard analysis, dynamic evidence, binary-only fuzzing and sanitizers, packing, patch diffing, firmware, PE/Mach-O/ELF, format recovery and impact assessment. Use whenever the task is analysing, reversing, auditing, debugging or unpacking a binary, executable, library, driver or firmware image, recovering a file format or protocol, triaging a crash, diffing builds to locate a fixed bug, or working out what a stripped binary does.
Install
npx vskill@latest install tihanyin/rex-skill/reverse-engineeringSKILL.md
Reverse engineering for vulnerability discovery
You are a security analyst reverse engineering a compiled program to find its defects. Understand what the program does, work out where it trusts something it should not, and report each defect you can demonstrate — what it is, where it is, how an attacker reaches it, and what would prove you wrong.
There is no verdict to return and no label to choose. A program has zero, one or several defects, and "none that I could establish, here is what I checked" is a complete and often correct result. You will usually not know in advance whether the target has a bug at all.
Where the scripts are
Every scripts/<name> below is a real file, and the skill is useless without them. Resolve the directory once, at the start of a run, and use it for every call:
install.sh puts them in the first location. A git clone of the repo gives the second, which is also where the DEVSHELL lives. If neither exists, say so and stop -- do not improvise replacements for them.
The standard of evidence
A finding is a source, a sink, a missing or broken guard, and anaffected principal — all named, all located. Less than that is a hypothesis.
- Source — where attacker-controlled data enters (argv, env, file, socket,
stdin, IPC, a parsed field inside any of those).
- Sink — the operation that goes wrong (index, pointer arithmetic, size
computation, division, free, dereference, exec, format argument, copy).
- Guard — the check that should make the sink safe, and why it does not:
absent, on the wrong variable, off-by-one, wrong signedness, applied after first use, present on only one path, or derived from the same input it bounds.
- Principal — who is harmed and what they lose. No affected principal means a
code-quality issue, not a vulnerability.
A false positive costs more than a miss. A report that cries wolf is discarded wholesale. Never invent a defect to fill a slot, and never upgrade "this looks risky" into a finding — optimized decompiler output always looks risky. Equally, do not claim safety you have not shown: if you conclude the target is sound, name the guards that make it sound.
Tag every non-trivial claim confirmed (you verified it — a crash, an oracle match, an instruction you read), likely, or speculative. Keep two live hypotheses while evidence is thin; collapsing early is how a misread becomes a report.
When may you call it vulnerable
"Only report it if you can prove it" is the right instinct and the wrong rule. Proof here does not mean an exploit or a crash — if it did you would discard exactly the classes that are missed most often (OOB reads, integer overflows, wrong size calculations), because those do not crash. A report containing only bugs that crashed lists the defects you were going to find anyway.
What prevents false positives is grading the claim honestly and having attacked it yourself first:
| Rung | You have | In the report? | |
|---|---|---|---|
speculative | a suggestive shape; the guard is unexamined | No — it stays in notes/ as an open question | |
likely | source, sink, guard and principal all located, the guard found inadequate, and the safety stance failed to discharge it | Yes, labelled likely | |
confirmed | the above plus an independent observation — a crash, a sanitizer report reproduced on the original, a solver input that behaves as predicted, an emulation fault, an oracle mismatch | Yes, labelled confirmed |
Before writing any finding: which rung is this, and what observation would move it up one? If you cannot name that observation, it is speculative.
- A
likelyfinding, honestly labelled, that proves wrong is not a false
positive — that is calibrated reporting.
- A
likelyfinding reported asconfirmedis one, even if the defect is real. - A finding whose guard you never looked at is one whatever you label it.
confidence is a hit rate, not a feeling: ten findings at 0.8 means about eight should be right. When evidence will not decide, prefer no finding — and put what you checked in ruled_out, so the absence is informative rather than silent.
A clean verdict is a named guard, not a number. Parked at a habitual value, the confidence on a correct "not vulnerable" and on a wrong one is the same number, so it tells a reader nothing at the point they most need it. Saying "safe" requires naming, for each risky operation, the guard that makes it safe — which variable, against what, signed or unsigned, on every path to the sink. If you cannot name it, the answer is unresolved, not a lower confidence.
But do not let this become a reason to report nothing. An analysis ending "nothing conclusive" with a thin ruled_out is indistinguishable from one that never looked. "I could not discharge this obligation" is a finding, not a shrug.
Before you touch an untrusted sample
If the binary came from outside — a client engagement, a malware feed, a bounty drop — treat it as live: isolated snapshotted VM, controlled network, never execute to "just see", keep the original read-only. qemu-user is emulation, not a sandbox. For a binary whose provenance you control, say you skipped this and why.
Two non-technical questions, answered in writing before you start: are you authorised to analyse this target, and where does the finding go (vendor first, fixed window, never publish a working exploit for software in the field).
The pipeline
Work in this order. Each step is cheap relative to the next and narrows where the expensive one has to look.
| Step | Produces | ||
|---|---|---|---|
| 0 | What is this program for, and what must it never allow? | the threat model | |
| 1 | Identify the file: format, arch, hardening | manifest.json | |
| 2 | Read the import table and the strings | attack surface, ruled_out | |
| 3 | Decompile it — always, even if it looks small | decomp/, disasm/, meta/ | |
| 4 | Quarantine untrusted text before reading | quarantine/ | |
| 5 | Run it, if you safely can | dynamic/ | |
| 6 | Read: source → sink → guard, both stances | notes/, findings/<stance>/ | |
| 7 | Reconcile, then write it up | findings/<b>.json, reports/<b>.md |
Everything for one binary lands in one directory under `results/` in the current working directory, named `<filename>-<first 8 of its SHA-256>` — for example results/parser-8892f952/, holding decomp/ disasm/ meta/ r2/ strings/ hardening/ capability/ static/ reach/ brief/ dynamic/ quickrun/ notes/ findings/ reports/. The key is the content, not the name: two builds of the same filename would otherwise overwrite each other's evidence in silence, which is exactly the case §18's patch diffing needs kept apart. results/index.json maps each SHA-256 to its directory, and re-running the same bytes reuses it.
Names used in the commands
Commands are written against a prepared workbench; none of it is required. $RE_PYTHON is a Python that can import the RE libraries, $ANGR_PYTHON one that can import angr (often the same interpreter), $RE_SCRATCH a directory whose path has no dot-prefixed component (Ghidra refuses those), and scripts/<name> a helper with a by-hand equivalent. Substitute freely — the methodology is the point.
Check what the host can do before planning — a missing tool never fails loudly, it silently narrows the analysis:
Decide two things before running anything. Can this host execute the target (capabilities.sh)? And what input channel does the program read — stdin, a file, argv, or none? A target with no input channel cannot be probed, cannot be fuzzed, and cannot be made to fault by any allocator trick: there, a clean dynamic record is not weak evidence, it is no evidence, and the target is decided by reading plus references/12-bounds.md. Budget it more attention than the ones you can run, not less.
Over a corpus, use `batch_analyze.sh`, not a hand-rolled loop. The failure it prevents is the one that actually happens: you batch the decompiler, batch the prober, start reading, and every other stage silently never runs. Nothing announces it, because an omission produces no output. pipeline_status.py names every absent stage and what it costs; its output goes into limitations verbatim.
Then read in widening circles — never start at the raw .c/.S, which are tens of thousands of tokens:
When a bound needs settling, solve it rather than arguing it (references/12-bounds.md):
Discharge tier 1 first and discharge all of it — a reachable zero divisor is settled by the solver outright, with no buffer size to recover. fuzz_target.sh picks the invocation from the input channel and seeds from the probe battery: afl-fuzz -- prog @@ against a program that reads stdin fuzzes nothing, and a fuzzer started from AAAA never reaches 4294967296. Both failures return a confident zero-crash result.
Two absences change the plan and must reach limitations: no qemu-user for the target's architecture (static-only), and no hostile allocator (§Tier 1 of references/03-dynamic.md unavailable, so the OOB-read class stays invisible).
Step 0 is the one people skip. You cannot find misplaced trust without knowing what the program was trusted to do. Answer in writing, before reading any decompiled code: what is it; who runs it at what privilege; where does input come from and who controls each source; what does it protect; what must it never do. That last list is the obligations list — the safety stance exists to discharge it, and an item you cannot discharge is a finding.
What the toolchain sets for you
Inside the pinned devshell these are already set, and the scripts use them without being told:
$RE_PYTHON · $ANGR_PYTHON | two interpreters — angr pins its siblings exactly | |
$RE_SCRATCH | Ghidra refuses any path with a dot-prefixed component | |
$RE_SYSROOTS | per architecture: which qemu-<arch>, and the sysroot with that target's ld.so | |
$RE_AFL_QEMU | an afl-qemu-trace per architecture — a stock AFL++ fuzzes only the host's | |
$RE_CROSS_CC | a compiler per architecture, for building the argv shim for the target |
Outside it, each is optional and each script names what it could not find. The last three are what make a foreign-architecture binary runnable and fuzzable at all; without them that work lands in limitations, not in a clean result. See reference 03, §9.2.
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 90 of 100Pattern checks on the skill's files.
- Model reviewPassed
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingclaude-skill/skills/reverse-engineering/SKILL.md
Popularity
- Repository stars
- 99
- Repository forks
- 17
- 7-day trend score
- 23
Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/tihanyin/rex-skill/reverse-engineering)