diff --git a/README.md b/README.md index 5dc02ba..150aff5 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,9 @@ clean log is not one of them. Anything short of all six is a checkpoint, and sho checkpoint with what remains. If you are an agent reading this: the cheapest possible first command is -`python scripts/doctor.py`. It tells you which of these tools exist here, which scripts can actually -run, and whether something in the environment is already poisoning your measurements. +`python skills/apk-reverse/scripts/doctor.py`. It tells you which of these tools exist here, which +scripts can actually run, and whether something in the environment is already poisoning your +measurements. ## What it is good at @@ -76,6 +77,10 @@ run, and whether something in the environment is already poisoning your measurem ## Structure +`SKILL.md`, `references/` and `scripts/` are all inside the skill directory, `skills/apk-reverse/`. +Everything at the repository root is maintenance tooling shared across skills, not part of an +installed skill. + ``` SKILL.md a procedure with gates, not background reading: how-to-use -> four override rules (R1-R4) @@ -229,20 +234,29 @@ scripts/ parameterized, path-agnostic ## Install -Drop this directory where your agent finds skills, e.g.: +This repository is a **skills repository**: the skill lives at `skills/apk-reverse/`, which is the +layout the `skills` CLI resolves, and it is installed by name rather than by copying a directory: ``` -/apk-reverse/SKILL.md +npx skills add newliver666/apk-reverse # install every skill in the repo +npx skills add newliver666/apk-reverse --list # list what is here, install nothing +npx skills add newliver666/apk-reverse --skill apk-reverse -y +npx skills use newliver666/apk-reverse@apk-reverse # use it once, without installing ``` -The agent loads `SKILL.md` when a task matches its description, and pulls in -`references/*` only as needed. No global state, no machine-specific paths. +The CLI symlinks the skill into your agent's skills directory by default (`--copy` makes independent +copies instead), and `-g` installs for every project rather than the current one. With one skill in +the repository, `--skill apk-reverse` is redundant today; it is written out here because it is what +selects a single skill once a second one exists. + +Once installed, the agent loads `SKILL.md` when a task matches its description, and pulls in +`references/*` only as needed. No global state, no machine-specific paths, and no build step. ## Requirements -Nothing is mandatory; each script checks what it needs. `scripts/doctor.py` reports which of these -are present here, which scripts can therefore run, and — usefully — which tools exist somewhere other -than PATH. +Nothing is mandatory; each script checks what it needs. `skills/apk-reverse/scripts/doctor.py` reports +which of these are present here, which scripts can therefore run, and — usefully — which tools exist +somewhere other than PATH. If your toolchain lives outside PATH (a project-local `tools/` directory, a versioned SDK folder, a runnable `.jar` instead of a command), set `APKREV_TOOLS` to one or more directories and `doctor.py` @@ -259,7 +273,7 @@ Linux; where a snippet is POSIX-only it is labelled. Nothing here assumes a Unix | Tool | Used for | |---|---| | Python 3.9+ | all scripts | -| `ddc` (optional but recommended) | single-binary dex→Java decompiler with query subcommands (`info`, `findrefs`, `strings --with-locations`, per-class decompile). No JVM. Turns string cross-referencing from a crawl into a lookup, and reports package identity reliably — see `references/toolchain.md` | +| `ddc` (optional but recommended) | single-binary dex→Java decompiler with query subcommands (`info`, `findrefs`, `strings --with-locations`, per-class decompile). No JVM. Turns string cross-referencing from a crawl into a lookup, and reports package identity reliably — see `skills/apk-reverse/references/toolchain.md` | | `baksmali` / `smali` + `dexlib2` jars | disassembly, assembly, surgical patching | | JDK (`javac`, `java`) | building/running the dexlib2 patcher; also provides `keytool`/`jarsigner` | | Android SDK build-tools (`aapt`, `zipalign`, `apksigner`) | manifest info, alignment, signing. **`apksigner` is the signer to use** — `jarsigner` rewrites the archive and breaks the alignment Android R+ requires | @@ -269,13 +283,14 @@ Linux; where a snippet is POSIX-only it is labelled. Nothing here assumes a Unix | a rooted device or emulator | anything beyond static analysis | None of these need to be on `PATH`: every script accepts an explicit path for the -tools it shells out to, and `references/toolchain.md` covers finding an install that -`PATH` does not know about (the common case for `apksigner` and `keytool`). +tools it shells out to, and `skills/apk-reverse/references/toolchain.md` covers finding +an install that `PATH` does not know about (the common case for `apksigner` and +`keytool`). ## Read this first -`references/pitfalls.md`. It is the most valuable file here — every entry is a failure -that produced a broken artifact while looking completely healthy. +`skills/apk-reverse/references/pitfalls.md`. It is the most valuable file here — every entry is a +failure that produced a broken artifact while looking completely healthy. The four that hurt most: @@ -294,3 +309,22 @@ The four that hurt most: Built for working on your own applications, on samples you are authorized to analyze, and in CTF/competition sandboxes. It contains no vendored third-party binaries and no target-specific data. + +What it covers, and what it deliberately does not, is stated at the top of `SKILL.md` +under **Coverage**. The short version: Android only (no iOS), and deep on the layers +that have been worked through for real — dex patching, repacking, packers and custom +loaders, native tamper response, and Flutter/Dart AOT. Unity/IL2CPP logic recovery, +React Native/Hermes bytecode internals, and defeating a server-side authority are **not** +covered, and the skill is written to say so and stop rather than apply the nearest +documented procedure to a target it was not written for. + +## Repository maintenance + +Three tools live at the root and are not part of the installed skill: + +``` +check_repo.py every skill discovered, frontmatter valid, scripts runnable, + documented paths resolve, README paths explicit and existing +check_refs.py every `file.md` §section cross-reference reaches a real heading +build_scripts.py audit for machine-specific leftovers (absolute paths, credentials) +``` diff --git a/check_refs.py b/check_refs.py new file mode 100644 index 0000000..54eb85e --- /dev/null +++ b/check_refs.py @@ -0,0 +1,250 @@ +#!/usr/bin/env python3 +"""Audit cross-references of the form `file.md` §section, and `file.md` P12. + +A reference like `references/toolchain.md` §"not on PATH" is a promise that the +target file has a section by that name. When a section is renamed or removed the +promise silently breaks: the reader arrives at a file that does not contain the +thing they were sent for, and the most likely outcome is that they conclude the +material is missing rather than that the link is stale. Prose links do not fail +loudly the way a missing file does, which is why this script exists separately +from `check_repo.py` (that one only proves the *file* resolves). + +A section reference is reported as: + * OK -- normalised, it matches a heading (or bold numbered rule) in the target + * WARN -- its words appear in the target body, but no heading carries them; + usually a stale or paraphrased link, worth a human look + * FAIL -- nothing in the target file corresponds; the link is dangling + +Layout is discovered the same way as in `check_repo.py`, so a second skill under +skills/ is audited with no change here. +""" +import os +import re +import sys + +ROOT = os.path.dirname(os.path.abspath(__file__)) +SKILLS_DIR = os.path.join(ROOT, 'skills') + +# A section reference. The optional leading file may be separated from the +# section mark by the closing backtick and a line break, since references are +# often wrapped mid-sentence. +REF_RE = re.compile( + r'(?:(?P[A-Za-z0-9_\-]+\.md)`?[\s`]{0,40})?' + r'§\s*(?P[^\n|`();,.\u2014\u3002]{2,90})') +# A failure-catalogue reference, e.g. `pitfalls.md` P31, or a bare (P18). +PITFALL_RE = re.compile( + r'(?:(?P[A-Za-z0-9_\-]+\.md)`?[\s`]{0,40})?' + r'\(?\bP(?P\d{1,2})\b') + +HEADING_RE = re.compile(r'^#{1,6}\s+(?P.+?)\s*$') +BOLD_RE = re.compile(r'^\*\*(?P<title>.+?)\*\*\s*$') + + +def norm(text): + """Fold a heading or a reference into comparable tokens. + + Hyphens and slashes become spaces so that `finding-the-call-site` matches + "Finding the call site", and quotes/emphasis are dropped so that + `§"not on PATH"` matches a heading containing the same words quoted. + """ + text = text.lower() + text = text.replace('\u2014', ' ').replace('\u2013', ' ') + text = re.sub(r'[`*_"\'()\[\]]', '', text) + text = re.sub(r'[^a-z0-9]+', ' ', text) + return text.strip() + + +def discover_skills(): + """Return [(name, skill_dir)] for each skill root, shallow shadowing deep.""" + found = [] + if not os.path.isdir(SKILLS_DIR): + return found + for dirpath, dirnames, filenames in os.walk(SKILLS_DIR): + dirnames[:] = sorted(d for d in dirnames if not d.startswith('.')) + rel = os.path.relpath(dirpath, SKILLS_DIR) + depth = 0 if rel == '.' else rel.count(os.sep) + 1 + if depth > 3: + dirnames[:] = [] + continue + if 'SKILL.md' in filenames: + found.append((rel.replace(os.sep, '/'), dirpath)) + dirnames[:] = [] + return sorted(found) + + +def discover_docs(): + """Return [(path, skill_name_or_None, text)] for every markdown document.""" + docs = [] + readme = os.path.join(ROOT, 'README.md') + if os.path.exists(readme): + docs.append((readme, None, open(readme, encoding='utf-8').read())) + for skill, skill_dir in discover_skills(): + for dirpath, dirnames, filenames in os.walk(skill_dir): + dirnames[:] = sorted(d for d in dirnames if not d.startswith('.')) + for fname in sorted(filenames): + if not fname.endswith('.md'): + continue + path = os.path.join(dirpath, fname) + docs.append((path, skill, open(path, encoding='utf-8').read())) + return docs + + +def anchors(text): + """Headings, and bold stand-alone lines (`**2a. ...**`), as match targets.""" + out = [] + for line in text.splitlines(): + line = line.strip() + m = HEADING_RE.match(line) or BOLD_RE.match(line) + if m: + out.append(m.group('title')) + return out + + +def matches(sec, anchor_list): + want = norm(sec) + if not want: + return True + want_tokens = want.split() + for raw in anchor_list: + have_tokens = norm(raw).split() + if not have_tokens: + continue + # A numbered heading (`## 3. Reading AOT code`) carries the number as a + # token, but a reference may name the section with or without it, so + # both readings are candidates. + candidates = [have_tokens] + if have_tokens[0].isdigit() and len(have_tokens) > 1: + candidates.append(have_tokens[1:]) + for have in candidates: + text = ' '.join(have) + if text == want or text.startswith(want + ' ') \ + or want.startswith(text + ' '): + return True + if all(tok in have for tok in want_tokens): + return True + # A reference frequently runs on into the sentence that follows it + # (`§Gates are cleared here or not at all`), leaving prose attached + # to the heading name. Sharing the leading word is the strongest + # signal available in that shape. + if have[0] == want_tokens[0]: + return True + return False + + +def first_token_match(sec, anchor_list): + """True when some heading starts with the same word as the reference. + + A reference is often a heading plus the rest of the sentence + (`§Gates are cleared here or not at all`), so an exact match is too strict + to be the only accepted shape. This is deliberately weaker than matches() + and is reported as a warning, not as a pass. + """ + want = norm(sec).split() + if not want: + return False + for raw in anchor_list: + have = norm(raw).split() + if have and have[0] == want[0]: + return True + return False + + +def in_body(sec, text): + """Weaker fallback: every token of the section name occurs in the body.""" + want_tokens = norm(sec).split() + if not want_tokens: + return True + body = norm(text) + return all(tok in body for tok in want_tokens) + + +def main(): + docs = discover_docs() + by_name = {} + for path, skill, text in docs: + by_name.setdefault(os.path.basename(path), []).append((path, skill, text)) + + ok = warn = 0 + problems = [] + warnings = [] + + for path, skill, text in docs: + where = os.path.relpath(path, ROOT).replace(os.sep, '/') + for m in REF_RE.finditer(text): + raw_sec = m.group('sec').strip().strip('.,;:!?\'")*').lower() + # `§7 gives the exact framing` -- keep only the number. + num = re.match(r'(\d{1,2}[a-z]?)\b', raw_sec) + sec = num.group(1) if num else raw_sec + if not sec: + continue + # `§8C` names sub-item C of section 8, so the bare section number is + # an acceptable fallback target. + variants = [sec] + sub = re.fullmatch(r'(\d{1,2})[a-z]', sec) + if sub: + variants.append(sub.group(1)) + + target_name = m.group('file') + if target_name is None: + target_text, target_list = text, [path] + target_label = where + else: + candidates = [c for c in by_name.get(target_name, []) + if skill is None or c[1] == skill] \ + or by_name.get(target_name, []) + if not candidates: + problems.append('%s: §%s -> %s (no such file)' + % (where, sec, target_name)) + continue + target_text = candidates[0][2] + target_label = os.path.relpath(candidates[0][0], ROOT).replace(os.sep, '/') + + if any(matches(v, anchors(target_text)) for v in variants): + ok += 1 + elif len(norm(sec).split()) >= 2 and first_token_match( + sec, anchors(target_text)): + warn += 1 + warnings.append('%s: §%s -> %s (heading starts with this word ' + 'but the wording differs)' + % (where, sec, target_label)) + elif in_body(sec, target_text): + warn += 1 + warnings.append('%s: §%s -> %s (words present, no matching heading)' + % (where, sec, target_label)) + else: + problems.append('%s: §%s -> %s (section not found)' + % (where, sec, target_label)) + + # Failure-catalogue references: `pitfalls.md` P31, or a bare (P18). + pit_anchors = set() + for name, entries in by_name.items(): + for path, _skill, text in entries: + for raw in anchors(text): + m = re.match(r'P(\d{1,2})[.\s]', raw) + if m: + pit_anchors.add(m.group(1)) + for path, _skill, text in docs: + where = os.path.relpath(path, ROOT).replace(os.sep, '/') + for m in PITFALL_RE.finditer(text): + if m.group('num') not in pit_anchors: + problems.append('%s: P%s (no such pitfall)' % (where, m.group('num'))) + else: + ok += 1 + + print('== markdown documents ==') + for path, _skill, _text in docs: + print(' %s' % os.path.relpath(path, ROOT).replace(os.sep, '/')) + + print('\n== section references: %d reachable ==' % ok) + for w in sorted(set(warnings)): + print(' WARN %s' % w) + for p in sorted(set(problems)): + print(' FAIL %s' % p) + + print('\n== result: %d dangling, %d warning(s) ==' + % (len(set(problems)), len(set(warnings)))) + return 1 if problems else 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/check_repo.py b/check_repo.py index efe34d2..135a14a 100644 --- a/check_repo.py +++ b/check_repo.py @@ -1,80 +1,271 @@ -"""Pre-commit checks: every script parses and answers --help; every referenced file exists.""" +#!/usr/bin/env python3 +"""Pre-commit checks for this repository. + +Assumed layout -- the one the `skills` CLI resolves: + + README.md repository-level docs (root) + skills/<skill-name>/SKILL.md the skill itself + skills/<skill-name>/references/*.md loaded on demand + skills/<skill-name>/scripts/* run, not read + +Every skill under `skills/` is discovered automatically (up to three levels; +a SKILL.md at a shallower level shadows anything nested below it, which is the +CLI's own rule), so adding a second skill requires no change to this script. + +Per skill: + 1. frontmatter exists, `name` matches the directory name, `description` <= 1024 + 2. every scripts/*.py parses and answers --help without a traceback + 3. every `references/X.md` / `scripts/X.py` reference resolves inside that skill + 4. every reference file is reachable from that skill's SKILL.md or the README + 5. every script is mentioned somewhere in the docs + +On the root README: + 6. every path it names is an explicit `skills/<name>/...` path that exists +""" import ast import os import re import subprocess import sys +try: + import yaml +except ImportError: # the skills CLI is the real authority; this is a local gate + yaml = None + ROOT = os.path.dirname(os.path.abspath(__file__)) -os.chdir(ROOT) +SKILLS_DIR = os.path.join(ROOT, 'skills') +README = os.path.join(ROOT, 'README.md') + +REF_RE = re.compile(r'`?(references/[A-Za-z0-9_\-]+\.md)') +SCRIPT_RE = re.compile(r'`?(scripts/[A-Za-z0-9_\-]+\.(?:py|js))') +NAMED_PATH_RE = re.compile( + r'skills/(?P<skill>[a-z0-9\-]+)/(?P<sub>references|scripts)/' + r'(?P<file>[A-Za-z0-9_\-]+\.(?:md|py|js))') +BARE_PATH_RE = re.compile(r'(?<![\w/])(?P<sub>references|scripts)/' + r'(?P<file>[A-Za-z0-9_\-]+\.(?:md|py|js))') fail = [] -print("== python syntax + --help ==") -for name in sorted(os.listdir('scripts')): - if not name.endswith('.py'): - continue - path = os.path.join('scripts', name) + +def read(path): + with open(path, encoding='utf-8') as fh: + return fh.read() + + +def discover_skills(): + """Return [(name, skill_dir)] for every skills/<...>/SKILL.md.""" + found = [] + if not os.path.isdir(SKILLS_DIR): + return found + for dirpath, dirnames, filenames in os.walk(SKILLS_DIR): + dirnames[:] = sorted(d for d in dirnames if not d.startswith('.')) + rel = os.path.relpath(dirpath, SKILLS_DIR) + depth = 0 if rel == '.' else rel.count(os.sep) + 1 + if depth > 3: + dirnames[:] = [] + continue + if 'SKILL.md' in filenames: + found.append((rel.replace(os.sep, '/'), dirpath)) + dirnames[:] = [] # shallower SKILL.md shadows what is below + return sorted(found) + + +NAME_RE = re.compile(r'^[a-z0-9]+(?:-[a-z0-9]+)*$') + + +def check_frontmatter(name, skill_dir): + """Validate the frontmatter the way the CLI will, not merely its presence. + + This has to be a real parse. Frontmatter that satisfies a regex can still be + rejected by a YAML parser -- an unquoted `description` containing ": " does + it, because the colon starts a nested mapping -- and the `skills` CLI then + reports "No skills found" and installs nothing at all. + """ + path = os.path.join(skill_dir, 'SKILL.md') + text = read(path) + m = re.match(r'^---\r?\n(.*?)\r?\n---\r?\n', text, re.S) + if not m: + fail.append('%s: no YAML frontmatter block' % path) + return + raw = m.group(1) + + if yaml is None: + print(' WARN PyYAML is not installed, so the frontmatter was not parsed') + line = re.search(r'^description:(.*)$', raw, re.M) + if line: + value = line.group(1) + if value[:1] not in ('"', "'", '>', '|') and ': ' in value: + fail.append('%s: unquoted description contains ": ", which a YAML ' + 'parser rejects; quote the value' % path) + return + try: - ast.parse(open(path, encoding='utf-8').read()) - except SyntaxError as e: - fail.append('%s: syntax error %s' % (path, e)) - print(' FAIL %s' % path) - continue - r = subprocess.run([sys.executable, '-B', path, '--help'], - capture_output=True, text=True, timeout=60) - # A script may exit non-zero on --help if it uses the "print usage and exit 2" - # convention. That is acceptable; a traceback is not. - crashed = 'Traceback (most recent call last)' in (r.stderr or '') - helped = bool((r.stdout or '').strip()) or bool(r.stderr) - ok = (not crashed) and helped - verdict = 'ok' if r.returncode == 0 and not crashed else \ - ('usage-ok rc=%d' % r.returncode if ok else 'CRASH rc=%d' % r.returncode) - print(' %-32s %s' % ('%s --help' % name, verdict)) - if not ok: - fail.append('%s --help crashed: %s' % (path, (r.stderr or '')[:300])) + data = yaml.safe_load(raw) + except yaml.YAMLError as exc: + detail = str(exc).splitlines()[0] + fail.append('%s: frontmatter is not valid YAML (%s); the skills CLI ' + 'skips a skill whose frontmatter will not parse' + % (path, detail)) + return + if not isinstance(data, dict): + fail.append('%s: frontmatter is not a YAML mapping' % path) + return -print("\n== markdown references resolve ==") -refs = set(os.listdir('references')) -scripts = set(os.listdir('scripts')) -missing = [] -for name in ['SKILL.md', 'README.md'] + ['references/' + f for f in refs if f.endswith('.md')]: - text = open(name, encoding='utf-8').read() - for m in re.finditer(r'`?(references/[A-Za-z0-9_\-]+\.md)', text): - target = m.group(1).split('/')[1] - if target not in refs: - missing.append('%s -> %s' % (name, m.group(1))) - for m in re.finditer(r'`?(scripts/[A-Za-z0-9_\-]+\.(?:py|js))', text): - target = m.group(1).split('/')[1] - if target not in scripts: - missing.append('%s -> %s' % (name, m.group(1))) -if missing: - for x in sorted(set(missing)): - print(' MISSING %s' % x) - fail.extend(missing) -else: - print(' all referenced reference/script paths exist') + got = data.get('name') + if not got: + fail.append('%s: frontmatter has no `name`' % path) + elif str(got) != name: + fail.append('%s: frontmatter name %r does not match directory %r' + % (path, got, name)) + elif not NAME_RE.match(str(got)) or len(str(got)) > 64: + fail.append('%s: name %r violates the spec (lowercase alphanumerics and ' + 'single hyphens, max 64 chars)' % (path, got)) -print("\n== every reference file is reachable from SKILL.md or README.md ==") -bodies = open('SKILL.md', encoding='utf-8').read() + open('README.md', encoding='utf-8').read() -unlisted = [f for f in sorted(refs) if f.endswith('.md') and f not in bodies] -for f in unlisted: - print(' UNLISTED references/%s' % f) -if unlisted: - fail.append('unlisted references: %s' % unlisted) + desc = data.get('description') + if not desc: + fail.append('%s: frontmatter has no `description`' % path) + else: + desc = str(desc) + if len(desc) > 1024: + fail.append('%s: description is %d chars (spec max 1024)' + % (path, len(desc))) + elif len(desc) < 40: + fail.append('%s: description is only %d chars, too short to tell an ' + 'agent when this skill applies' % (path, len(desc))) -print("\n== every script is mentioned somewhere in the docs ==") -all_docs = bodies -for f in refs: - if f.endswith('.md'): - all_docs += open('references/' + f, encoding='utf-8').read() -undoc = [s for s in sorted(scripts) if s.endswith(('.py', '.js')) and s not in all_docs] -for s in undoc: - print(' UNDOCUMENTED scripts/%s' % s) -if undoc: - fail.append('undocumented scripts: %s' % undoc) -print("\n== result: %d problem(s) ==" % len(fail)) -for f in fail: - print(' - %s' % f) -sys.exit(1 if fail else 0) +def check_scripts(skill_dir): + scripts = os.path.join(skill_dir, 'scripts') + if not os.path.isdir(scripts): + return + for name in sorted(os.listdir(scripts)): + if not name.endswith('.py'): + continue + path = os.path.join(scripts, name) + try: + ast.parse(read(path)) + except SyntaxError as exc: + fail.append('%s: syntax error %s' % (path, exc)) + print(' FAIL %s' % name) + continue + run = subprocess.run([sys.executable, '-B', path, '--help'], + capture_output=True, text=True, timeout=60, + cwd=ROOT) + # A script may exit non-zero on --help under the "print usage, exit 2" + # convention. That is acceptable; a traceback is not. + crashed = 'Traceback (most recent call last)' in (run.stderr or '') + helped = bool((run.stdout or '').strip()) or bool(run.stderr) + ok = (not crashed) and helped + verdict = 'ok' if run.returncode == 0 and not crashed else \ + ('usage-ok rc=%d' % run.returncode if ok else + 'CRASH rc=%d' % run.returncode) + print(' %-32s %s' % (name, verdict)) + if not ok: + fail.append('%s --help crashed: %s' % (path, (run.stderr or '')[:300])) + + +def check_references(name, skill_dir, root_docs): + """Resolve in-skill references, and confirm every file is reachable.""" + ref_dir = os.path.join(skill_dir, 'references') + script_dir = os.path.join(skill_dir, 'scripts') + refs = set(os.listdir(ref_dir)) if os.path.isdir(ref_dir) else set() + scripts = set(os.listdir(script_dir)) if os.path.isdir(script_dir) else set() + + # Documents whose relative paths are rooted at the skill directory. + own = [os.path.join(skill_dir, 'SKILL.md')] + own += [os.path.join(ref_dir, f) for f in sorted(refs) if f.endswith('.md')] + + missing = [] + for doc in own: + if not os.path.exists(doc): + continue + text = read(doc) + for m in REF_RE.finditer(text): + if m.group(1).split('/')[1] not in refs: + missing.append('%s -> %s' % (os.path.relpath(doc, ROOT), m.group(1))) + for m in SCRIPT_RE.finditer(text): + if m.group(1).split('/')[1] not in scripts: + missing.append('%s -> %s' % (os.path.relpath(doc, ROOT), m.group(1))) + if missing: + for x in sorted(set(missing)): + print(' MISSING %s' % x) + fail.extend(missing) + + # Reachability: nothing may be orphaned from the skill entry point. + bodies = read(os.path.join(skill_dir, 'SKILL.md')) + root_docs + for f in sorted(refs): + if f.endswith('.md') and f not in bodies: + fail.append('%s: references/%s is not mentioned anywhere' % (name, f)) + print(' UNLISTED references/%s' % f) + + all_docs = bodies + for f in sorted(refs): + if f.endswith('.md'): + all_docs += read(os.path.join(ref_dir, f)) + for s in sorted(scripts): + if s.endswith(('.py', '.js')) and s not in all_docs: + fail.append('%s: scripts/%s is undocumented' % (name, s)) + print(' UNDOCUMENTED scripts/%s' % s) + + +def check_readme(): + """README paths must be explicit and must exist.""" + text = read(README) + if not os.path.isdir(SKILLS_DIR): + fail.append('no skills/ directory at the repository root') + return + skills = {d for d, _ in discover_skills()} + named = set() + for m in NAMED_PATH_RE.finditer(text): + named.add(m.group(0)) + skill, sub, fname = m.group('skill'), m.group('sub'), m.group('file') + if skill not in skills: + fail.append('README.md: names unknown skill %r' % skill) + print(' UNKNOWN SKILL skills/%s' % skill) + elif not os.path.exists(os.path.join(SKILLS_DIR, skill, sub, fname)): + fail.append('README.md: %s does not exist' % m.group(0)) + print(' MISSING %s' % m.group(0)) + # Bare relative paths are ambiguous once there is more than one skill, so the + # README must not use them at all. + stripped = NAMED_PATH_RE.sub('', text) + for m in BARE_PATH_RE.finditer(stripped): + fail.append('README.md: bare path %r should be skills/<name>/%s' + % (m.group(0), m.group(0))) + print(' BARE PATH %s' % m.group(0)) + + +def main(): + skills = discover_skills() + print('== skills discovered ==') + if not skills: + print(' none (expected at least skills/<name>/SKILL.md)') + for name, _ in skills: + print(' skills/%s' % name) + + print('\n== frontmatter ==') + for name, skill_dir in skills: + check_frontmatter(name, skill_dir) + print(' %d checked' % len(skills)) + + root_docs = read(README) + for name, skill_dir in skills: + print('\n== %s: python syntax + --help ==' % name) + check_scripts(skill_dir) + print('\n== %s: reference and script paths resolve ==' % name) + before = len(fail) + check_references(name, skill_dir, root_docs) + if len(fail) == before: + print(' all referenced reference/script paths exist') + + print('\n== README paths are explicit and exist ==') + check_readme() + + print('\n== result: %d problem(s) ==' % len(fail)) + for f in fail: + print(' - %s' % f) + return 1 if fail else 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/SKILL.md b/skills/apk-reverse/SKILL.md similarity index 90% rename from SKILL.md rename to skills/apk-reverse/SKILL.md index 21cbeae..8f99e12 100644 --- a/SKILL.md +++ b/skills/apk-reverse/SKILL.md @@ -1,6 +1,6 @@ --- name: apk-reverse -description: Reverse engineer, debloat, de-ad, patch, or re-sign Android APKs, and analyze their runtime and server-side behavior. Use when a task involves an .apk/.aab/.dex/.so sample, smali or dex patching, Frida/objection runtime hooking, repacking and re-signing, removing ads or SDK trackers, probing a mobile app's HTTP API, or deciding whether a client-side patch is even capable of achieving the goal. Covers recon, anti-tamper, ad removal, membership/paywall limits, dex-level surgical patching, repack pitfalls, device and emulator setup, and a hard-won failure catalogue. Load the body before planning any patch work: it opens with a symptom index and four gates that must be cleared first. +description: "Reverse engineer, debloat, de-ad, patch, or re-sign Android APKs, and analyze their runtime and server-side behavior. Use when a task involves an .apk/.aab/.dex/.so sample, smali or dex patching, Frida/objection runtime hooking, repacking and re-signing, removing ads or SDK trackers, probing a mobile app's HTTP API, or deciding whether a client-side patch is even capable of achieving the goal. Covers recon, anti-tamper, ad removal, membership/paywall limits, dex-level surgical patching, repack pitfalls, device and emulator setup, and a hard-won failure catalogue. Load the body before planning any patch work: it opens with a symptom index and four gates that must be cleared first." --- # APK Reverse Engineering & Patching @@ -49,6 +49,57 @@ native, Dart/Unity, server). Patching the wrong layer either does nothing or bre patch "had no effect", the diagnosis was wrong — go back to classification instead of patching harder. → `references/recon.md`, then the layer-specific file the symptom index points at +## Coverage — what this skill claims, and what it does not + +The failure this section prevents is not ignorance. It is **a confident wrong answer produced by +applying the nearest available procedure to a target it was never written for.** A documented method +that almost fits is more dangerous than no method at all, because it arrives with a plan, a +vocabulary and a set of reassuring numbers. + +**Covered, by verified mechanisms:** + +- Client-side ads, promos and splash/popup/tab configuration — including the server-issued UI config + that has no SDK to find (`ad-removal.md`, `server-config-and-updates.md`). +- Deciding whether a membership, paywall or feature gate is *client-enforceable* at all, and saying + so plainly when it is not (`membership-and-limits.md`, `account-gates.md`). +- dex-level surgical patching: equal-length byte edits and dexlib2 method rewrites, plus the header, + verifier and alignment rules that decide whether the build loads at all (`dex-patching.md`, + `byte-level-patching.md`, `patch-audit.md`). +- Repacking, signing, installing, and the install refusals that look like a broken build + (`repack-and-sign.md`). +- Packers, custom loaders and code virtualization: identifying them, measuring the validation + boundary, and the routes that survive it (`packers.md`, + `code-virtualization-and-custom-linkers.md`). +- The native layer: `.so` hosts, tamper-triggered self-termination, forged ELF structure, and + neutralising a terminate path without freezing the process (`native-and-so.md`, + `native-tamper-and-suicide.md`). +- Flutter / Dart AOT: pinning the engine version, decoding the object pool, locating and patching + business logic inside `libapp.so` (`dart-aot.md`). +- Runtime analysis with Frida, server-side API probing, feature-scoped TLS failures, update and + forced-upgrade neutralisation, and the verification discipline everything above rests on. + +**Not covered — say so rather than improvise:** + +- **Unity / IL2CPP logic recovery.** `framework-runtimes.md` identifies the runtime and establishes + that the dex is not the battlefield; it does not carry the IL2CPP equivalent of `dart-aot.md`. + There is no verified recipe here for locating a method inside `libil2cpp.so` plus + `global-metadata.dat`. +- **React Native / Hermes bytecode** and Cordova/hybrid internals, beyond runtime identification and + the generic "find the string, then find what references it" approach. +- **iOS / `.ipa` of any kind.** Every device, signing and packaging instruction here is Android. +- **Defeating a server-side authority.** `server-api.md` exists to determine *who owns a gate*, not + to break an authorization the server performs. +- **A general unpacker, or an anti-detection arms race.** `detection-and-anti-analysis.md` decides by + cost and often concludes "switch to static"; it is not a catalogue of evasion for every detector + you might meet. + +**The fallback, as an instruction:** if the target does not match that list, or no symptom-index row +matches, **stop and classify before choosing a branch.** Answer the thirteen questions first. If the +shape still does not fit — an unknown runtime, a mechanism you cannot name — say exactly that, and +propose the cheapest experiment that would identify it, rather than taking the closest documented +route and applying it anyway. A wrong branch here does not fail loudly: it produces an artifact that +builds, runs, and does the wrong thing. + ## Symptom index — a matching row is a stop signal You arrive at a symptom, not at a file name. Each row below is a failure that has already been paid @@ -128,7 +179,7 @@ Answer these before touching a tool. Every one of them changes the whole plan. - Feature flag, UI gate, debug switch → usually client-side - Anything decided by an API response → server-side → `references/server-api.md` 3. **Is the app's own code in plain dex, or moved to native/Flutter/Unity?** - Plain dex → you can patch. Flutter (`libflutter.so` + `libapp.so`) / Unity (`libil2cpp.so`) / pure native → different toolchain entirely. See `references/recon.md` §code-location and `references/framework-runtimes.md`. + Plain dex → you can patch. Flutter (`libflutter.so` + `libapp.so`) / Unity (`libil2cpp.so`) / pure native → different toolchain entirely. See `references/recon.md` §Where does the app's own code live and `references/framework-runtimes.md`. **Runtime check (cheap -- do it before committing to a layer):** hook the obvious Java classes for the UI you care about, then reproduce that UI. If those hooks fire, the behavior is Java-owned. If they fire **zero times** while the UI is plainly on screen, the behavior is drawn by the runtime or by native code, and a dex-only plan will stall. Do not keep hunting in dex after a zero-hit probe -- that is the most expensive wrong turn in this skill's history. 4. **What must the deliverable be able to do?** Write the answer as a testable sentence before planning anything, then re-read it at every checkpoint. This is the drift guard, and the drift it diff --git a/references/account-gates.md b/skills/apk-reverse/references/account-gates.md similarity index 100% rename from references/account-gates.md rename to skills/apk-reverse/references/account-gates.md diff --git a/references/ad-removal.md b/skills/apk-reverse/references/ad-removal.md similarity index 100% rename from references/ad-removal.md rename to skills/apk-reverse/references/ad-removal.md diff --git a/references/byte-level-patching.md b/skills/apk-reverse/references/byte-level-patching.md similarity index 100% rename from references/byte-level-patching.md rename to skills/apk-reverse/references/byte-level-patching.md diff --git a/references/code-virtualization-and-custom-linkers.md b/skills/apk-reverse/references/code-virtualization-and-custom-linkers.md similarity index 99% rename from references/code-virtualization-and-custom-linkers.md rename to skills/apk-reverse/references/code-virtualization-and-custom-linkers.md index fab8733..89caf38 100644 --- a/references/code-virtualization-and-custom-linkers.md +++ b/skills/apk-reverse/references/code-virtualization-and-custom-linkers.md @@ -36,7 +36,7 @@ in this skill's history. 3. **A shipping library whose SONAME does not match its filename.** A private loader is often named for its role while sitting under an innocuous filename. Read `DT_SONAME` from `.dynamic`, not the file name — `probe_deps`-style parsing of `PT_DYNAMIC` (see - `native-and-so.md` §forged headers) gives you the truth. A `lib<something>.so` claiming + `native-tamper-and-suicide.md` §Forged section headers) gives you the truth. A `lib<something>.so` claiming `soname = liblinkerloader.so` is a private ELF loader. Supporting tells: `assets/` carrying a small text descriptor naming the protector and its version; diff --git a/references/dart-aot.md b/skills/apk-reverse/references/dart-aot.md similarity index 100% rename from references/dart-aot.md rename to skills/apk-reverse/references/dart-aot.md diff --git a/references/detection-and-anti-analysis.md b/skills/apk-reverse/references/detection-and-anti-analysis.md similarity index 100% rename from references/detection-and-anti-analysis.md rename to skills/apk-reverse/references/detection-and-anti-analysis.md diff --git a/references/dex-patching.md b/skills/apk-reverse/references/dex-patching.md similarity index 100% rename from references/dex-patching.md rename to skills/apk-reverse/references/dex-patching.md diff --git a/references/dynamic-frida.md b/skills/apk-reverse/references/dynamic-frida.md similarity index 98% rename from references/dynamic-frida.md rename to skills/apk-reverse/references/dynamic-frida.md index 52063ad..34ff2ab 100644 --- a/references/dynamic-frida.md +++ b/skills/apk-reverse/references/dynamic-frida.md @@ -337,7 +337,7 @@ Java.perform(function () { }); ``` -Read the log after driving the UI. `RAW-URL` (with no `OKHTTP` events) is how you learn the request went over `HttpsURLConnection` — a different trust path entirely (`references/server-api.md` § TLS). `THROW` is how you learn an upper layer swallowed the real error. +Read the log after driving the UI. `RAW-URL` (with no `OKHTTP` events) is how you learn the request went over `HttpsURLConnection` — a different trust path entirely (`references/tls-and-cert.md` §If OkHttp is the failing path). `THROW` is how you learn an upper layer swallowed the real error. ```bash # pull the record (app-private path needs root to read) @@ -357,7 +357,7 @@ adb -s <serial> shell "su -c 'cat /data/user/0/<app.package>/files/probe.log'" > }); ``` 3. **Is that code path reached at all?** Layer 4 settles it: if `THROW` events show the app failing earlier, your hook's call site is never executed and no amount of hooking will help. -4. **Did your UI action trigger business logic?** A tap that looks fine but fails a local form check returns before any network call. Verify the input actually reached the field (`references/environment.md` § UI automation), not just that the button animated. +4. **Did your UI action trigger business logic?** A tap that looks fine but fails a local form check returns before any network call. Verify the input actually reached the field (`references/environment.md` §Driving the UI from adb), not just that the button animated. ## `Java.choose` also matches dead instances diff --git a/references/environment.md b/skills/apk-reverse/references/environment.md similarity index 100% rename from references/environment.md rename to skills/apk-reverse/references/environment.md diff --git a/references/framework-runtimes.md b/skills/apk-reverse/references/framework-runtimes.md similarity index 100% rename from references/framework-runtimes.md rename to skills/apk-reverse/references/framework-runtimes.md diff --git a/references/long-task-discipline.md b/skills/apk-reverse/references/long-task-discipline.md similarity index 100% rename from references/long-task-discipline.md rename to skills/apk-reverse/references/long-task-discipline.md diff --git a/references/membership-and-limits.md b/skills/apk-reverse/references/membership-and-limits.md similarity index 100% rename from references/membership-and-limits.md rename to skills/apk-reverse/references/membership-and-limits.md diff --git a/references/native-and-so.md b/skills/apk-reverse/references/native-and-so.md similarity index 100% rename from references/native-and-so.md rename to skills/apk-reverse/references/native-and-so.md diff --git a/references/native-tamper-and-suicide.md b/skills/apk-reverse/references/native-tamper-and-suicide.md similarity index 100% rename from references/native-tamper-and-suicide.md rename to skills/apk-reverse/references/native-tamper-and-suicide.md diff --git a/references/packers.md b/skills/apk-reverse/references/packers.md similarity index 100% rename from references/packers.md rename to skills/apk-reverse/references/packers.md diff --git a/references/patch-audit.md b/skills/apk-reverse/references/patch-audit.md similarity index 100% rename from references/patch-audit.md rename to skills/apk-reverse/references/patch-audit.md diff --git a/references/pitfalls.md b/skills/apk-reverse/references/pitfalls.md similarity index 100% rename from references/pitfalls.md rename to skills/apk-reverse/references/pitfalls.md diff --git a/references/recon.md b/skills/apk-reverse/references/recon.md similarity index 100% rename from references/recon.md rename to skills/apk-reverse/references/recon.md diff --git a/references/repack-and-sign.md b/skills/apk-reverse/references/repack-and-sign.md similarity index 100% rename from references/repack-and-sign.md rename to skills/apk-reverse/references/repack-and-sign.md diff --git a/references/runtime-data.md b/skills/apk-reverse/references/runtime-data.md similarity index 100% rename from references/runtime-data.md rename to skills/apk-reverse/references/runtime-data.md diff --git a/references/server-api.md b/skills/apk-reverse/references/server-api.md similarity index 100% rename from references/server-api.md rename to skills/apk-reverse/references/server-api.md diff --git a/references/server-config-and-updates.md b/skills/apk-reverse/references/server-config-and-updates.md similarity index 100% rename from references/server-config-and-updates.md rename to skills/apk-reverse/references/server-config-and-updates.md diff --git a/references/signature-derived-keys.md b/skills/apk-reverse/references/signature-derived-keys.md similarity index 100% rename from references/signature-derived-keys.md rename to skills/apk-reverse/references/signature-derived-keys.md diff --git a/references/third-party-builds.md b/skills/apk-reverse/references/third-party-builds.md similarity index 100% rename from references/third-party-builds.md rename to skills/apk-reverse/references/third-party-builds.md diff --git a/references/tls-and-cert.md b/skills/apk-reverse/references/tls-and-cert.md similarity index 100% rename from references/tls-and-cert.md rename to skills/apk-reverse/references/tls-and-cert.md diff --git a/references/toolchain.md b/skills/apk-reverse/references/toolchain.md similarity index 76% rename from references/toolchain.md rename to skills/apk-reverse/references/toolchain.md index d0f7c0a..05d3dda 100644 --- a/references/toolchain.md +++ b/skills/apk-reverse/references/toolchain.md @@ -4,9 +4,10 @@ Load this when you are choosing tools, when a tool produces an answer that smell something is not installed. It is a map, not a tutorial: each entry says what the tool is *for*, how to drive it non-interactively, and the failure mode that wastes time. -**Prefer tools you can drive from a command line.** An agent cannot click. Tools that exist only as -a GUI are listed where they matter, marked as requiring a human, so you can ask for that instead of -stalling or silently substituting a weaker method. +**Prefer tools you can drive from a command line.** An agent cannot click. A tool that ships only a +GUI is not automatically out of reach — check for a headless or MCP path first, which is an +install-and-set-up step rather than a reason to substitute the tool. When no such route exists, ask +for a human instead of stalling or silently downgrading to a weaker method. ## Tier 0 — present on almost any machine, no install @@ -99,7 +100,7 @@ not that the method is empty. | `capstone` (python) | library | decoding in your own scripts — see the silent-stop trap below | | `keystone` (python) | library | assembling a short patch when you are editing bytes by hand | | `pyelftools` (python) | library | **section-based — unreliable on hardened targets.** Prefer hand-walking `PT_LOAD`/`PT_DYNAMIC`, as `scripts/elf_plt.py` does | -| IDA Pro | GUI | human-driven. Ask for it when decompiler output is genuinely needed; do not pretend a raw disassembly is equivalent | +| IDA Pro | GUI, **or headless via `idalib-mcp`** | the strongest decompiler output. Do not assume it needs a human in front of it — a headless MCP server exists (see *MCP tool servers* below). Ask for a human only when that setup is genuinely unavailable | | `x64dbg` | GUI, Windows | human-driven live debugging of a native Windows target | | `gdb` / `lldb` | CLI | live debugging where a device or emulator allows it | @@ -112,6 +113,64 @@ completeness matters (`native-tamper-and-suicide.md` §Scanner traps). 4-byte window into *some* instruction, so a wrong start offset yields plausible-looking garbage. Bound your window with a known entry point or a known call site. +## MCP tool servers — an external dependency, not a tool on the shelf + +An MCP server is neither a CLI tool nor one of the scripts here. It is a **separate installation plus +a running process**: something to install, a service to start, usually an extension to load, often an +authorization step. Listing one beside `readelf` would imply it is already present. Declare it as a +dependency with its prerequisites, and check whether it is actually available *before* planning work +that needs it — discovering this mid-task is the "missing tool" failure again. + +### IDA Pro, headless + +IDA does **not** require a human at a GUI. `mrexodia/ida-pro-mcp` ships `idalib-mcp`, a headless MCP +server that drives an IDA database with no GUI process at all: + +```sh +uv run idalib-mcp --host 127.0.0.1 --port 8745 path/to/executable # open a binary up front +uv run idalib-mcp --host 127.0.0.1 --port 8745 # open databases on demand +uv run idalib-mcp --stdio # for stdio-based clients +``` + +Prerequisites, all of which must be arranged before the first call: IDA Pro 8.3+ (9 recommended; +**IDA Free is not supported**), a Python 3.11+ that `idapyswitch` can select, `uv`, and an `idalib` +activated globally via `py-activate-idalib.py`. Each open database lives in a worker process that +outlives the supervisor and is adopted transparently by a later supervisor on the same host, so +several sessions can share one analysis. Every tool call carries an explicit `database` argument — +there is no implicit "current database" — and `idb_open` returns the session id you must pass. + +The GUI-plugin variant of the same project is deprecated upstream in favour of `idalib-mcp`; do not +set that up for agent work. + +### Ghidra + +`LaurieWired/GhidraMCP` is a Ghidra **extension** plus a separate Python bridge, not a headless +analysis server: Ghidra must be running with the plugin loaded (its HTTP server defaults to +`127.0.0.1:8080`) and the bridge is another process your client starts. Treat it as requiring an +interactive GUI session. For unattended work the shape that actually runs without a GUI is +`analyzeHeadless` with a post-script. + +### The boundary: deobfuscate before you hand the binary to a model + +Both are LLM-driven, and they inherit the model's weaknesses. The IDA MCP authors say it plainly in +their own README: **"LLMs will not perform well on obfuscated code"**, and they advise removing +string encryption, import hashing, control flow flattening, code encryption and anti-decompilation +tricks *before* asking a model to solve anything. + +That matters more here than in most domains, because it is precisely the shape of the targets this +skill deals with. An OLLVM-style or virtualized binary is not a job for a model reading decompiler +output; the deobfuscated version is a *different binary*, and producing it is the work already +described in `code-virtualization-and-custom-linkers.md` and +`native-tamper-and-suicide.md`. The same advice has a second half: resolve library code first +(FLIRT/Lumina) so the model is not asked to reason about `memcpy` as if it were the program. + +Two consequences worth carrying into the plan: + +- Ask a model for **navigation and reading** — rename, comment, summarise a function, convert an + immediate — and verify anything that decides the patch against the disassembly yourself. +- If the server is not installed, that is a **missing dependency to report**, not a licence to present + a weaker tool's output as equivalent (`packers.md`). + ## Tier 3 — runtime and network | Tool | Invoke | Notes | diff --git a/references/updates-and-forced-upgrade.md b/skills/apk-reverse/references/updates-and-forced-upgrade.md similarity index 98% rename from references/updates-and-forced-upgrade.md rename to skills/apk-reverse/references/updates-and-forced-upgrade.md index dd87b0b..fa3a8cf 100644 --- a/references/updates-and-forced-upgrade.md +++ b/skills/apk-reverse/references/updates-and-forced-upgrade.md @@ -22,7 +22,7 @@ to updates: A fourth case is a **resource / hot-update channel**: the app fetches a bundle (JS, a Dart/RN patch, a config blob, a plugin APK) that can reintroduce the behaviour you removed. That is a separate code path -from version checking — handle it explicitly if the app has one (see §hot-update below). +from version checking — handle it explicitly if the app has one (see §Step 5 below). ## Step 1: locate the check @@ -36,7 +36,7 @@ update / upgrade / new version / check version / force update ``` Search those in the app's string table and in class names; on heavy runtimes search the string pool, -remembering its encoding (see `framework-runtimes.md` §string encoding — searching only one encoding +remembering its encoding (see `framework-runtimes.md` §Locating logic without symbols — searching only one encoding misses half your hits, and "nothing found" is usually the encoding, not an absence). **Signals that a version check exists even if you cannot read the strings:** @@ -143,7 +143,7 @@ If the app can fetch a bundle at runtime, version checking is not the only way y Note the flip side: a **server-issued configuration** is the same category. If the thing you removed was driven by remote config, the client patch holds only while the server keeps sending that config — see -`references/ad-removal.md` §server-driven and `references/membership-and-limits.md` for the general +`references/server-config-and-updates.md` §6 and `references/membership-and-limits.md` for the general rule that *client-side enforcement can be patched, server-side authority cannot*. ## The honest answer for silent version gates diff --git a/references/verification.md b/skills/apk-reverse/references/verification.md similarity index 100% rename from references/verification.md rename to skills/apk-reverse/references/verification.md diff --git a/scripts/apk_diff.py b/skills/apk-reverse/scripts/apk_diff.py similarity index 100% rename from scripts/apk_diff.py rename to skills/apk-reverse/scripts/apk_diff.py diff --git a/scripts/blob_decode.py b/skills/apk-reverse/scripts/blob_decode.py similarity index 100% rename from scripts/blob_decode.py rename to skills/apk-reverse/scripts/blob_decode.py diff --git a/scripts/coldstart.py b/skills/apk-reverse/scripts/coldstart.py similarity index 100% rename from scripts/coldstart.py rename to skills/apk-reverse/scripts/coldstart.py diff --git a/scripts/dart_disasm.py b/skills/apk-reverse/scripts/dart_disasm.py similarity index 100% rename from scripts/dart_disasm.py rename to skills/apk-reverse/scripts/dart_disasm.py diff --git a/scripts/dart_pool_strings.py b/skills/apk-reverse/scripts/dart_pool_strings.py similarity index 100% rename from scripts/dart_pool_strings.py rename to skills/apk-reverse/scripts/dart_pool_strings.py diff --git a/scripts/dart_pprefs.py b/skills/apk-reverse/scripts/dart_pprefs.py similarity index 100% rename from scripts/dart_pprefs.py rename to skills/apk-reverse/scripts/dart_pprefs.py diff --git a/scripts/datastore_inject.py b/skills/apk-reverse/scripts/datastore_inject.py similarity index 100% rename from scripts/datastore_inject.py rename to skills/apk-reverse/scripts/datastore_inject.py diff --git a/scripts/devsh.py b/skills/apk-reverse/scripts/devsh.py similarity index 100% rename from scripts/devsh.py rename to skills/apk-reverse/scripts/devsh.py diff --git a/scripts/dex_check_verifier.py b/skills/apk-reverse/scripts/dex_check_verifier.py similarity index 100% rename from scripts/dex_check_verifier.py rename to skills/apk-reverse/scripts/dex_check_verifier.py diff --git a/scripts/dex_classdiff.py b/skills/apk-reverse/scripts/dex_classdiff.py similarity index 100% rename from scripts/dex_classdiff.py rename to skills/apk-reverse/scripts/dex_classdiff.py diff --git a/scripts/dex_find_insn.py b/skills/apk-reverse/scripts/dex_find_insn.py similarity index 100% rename from scripts/dex_find_insn.py rename to skills/apk-reverse/scripts/dex_find_insn.py diff --git a/scripts/dex_patch_bytes.py b/skills/apk-reverse/scripts/dex_patch_bytes.py similarity index 100% rename from scripts/dex_patch_bytes.py rename to skills/apk-reverse/scripts/dex_patch_bytes.py diff --git a/scripts/dex_strings.py b/skills/apk-reverse/scripts/dex_strings.py similarity index 100% rename from scripts/dex_strings.py rename to skills/apk-reverse/scripts/dex_strings.py diff --git a/scripts/dex_strpatch.py b/skills/apk-reverse/scripts/dex_strpatch.py similarity index 100% rename from scripts/dex_strpatch.py rename to skills/apk-reverse/scripts/dex_strpatch.py diff --git a/scripts/dexpatch/PatchMethod.java b/skills/apk-reverse/scripts/dexpatch/PatchMethod.java similarity index 100% rename from scripts/dexpatch/PatchMethod.java rename to skills/apk-reverse/scripts/dexpatch/PatchMethod.java diff --git a/scripts/dexpatch/PatchMethodExample.java b/skills/apk-reverse/scripts/dexpatch/PatchMethodExample.java similarity index 100% rename from scripts/dexpatch/PatchMethodExample.java rename to skills/apk-reverse/scripts/dexpatch/PatchMethodExample.java diff --git a/scripts/dexpatch/README.md b/skills/apk-reverse/scripts/dexpatch/README.md similarity index 100% rename from scripts/dexpatch/README.md rename to skills/apk-reverse/scripts/dexpatch/README.md diff --git a/scripts/dexutil.py b/skills/apk-reverse/scripts/dexutil.py similarity index 100% rename from scripts/dexutil.py rename to skills/apk-reverse/scripts/dexutil.py diff --git a/scripts/doctor.py b/skills/apk-reverse/scripts/doctor.py similarity index 100% rename from scripts/doctor.py rename to skills/apk-reverse/scripts/doctor.py diff --git a/scripts/elf_plt.py b/skills/apk-reverse/scripts/elf_plt.py similarity index 100% rename from scripts/elf_plt.py rename to skills/apk-reverse/scripts/elf_plt.py diff --git a/scripts/find_refs.py b/skills/apk-reverse/scripts/find_refs.py similarity index 100% rename from scripts/find_refs.py rename to skills/apk-reverse/scripts/find_refs.py diff --git a/scripts/frida_probe.js b/skills/apk-reverse/scripts/frida_probe.js similarity index 100% rename from scripts/frida_probe.js rename to skills/apk-reverse/scripts/frida_probe.js diff --git a/scripts/grab_crash.py b/skills/apk-reverse/scripts/grab_crash.py similarity index 100% rename from scripts/grab_crash.py rename to skills/apk-reverse/scripts/grab_crash.py diff --git a/scripts/install_test.py b/skills/apk-reverse/scripts/install_test.py similarity index 100% rename from scripts/install_test.py rename to skills/apk-reverse/scripts/install_test.py diff --git a/scripts/lib_map.py b/skills/apk-reverse/scripts/lib_map.py similarity index 100% rename from scripts/lib_map.py rename to skills/apk-reverse/scripts/lib_map.py diff --git a/scripts/native_crash.py b/skills/apk-reverse/scripts/native_crash.py similarity index 100% rename from scripts/native_crash.py rename to skills/apk-reverse/scripts/native_crash.py diff --git a/scripts/patch_smali.py b/skills/apk-reverse/scripts/patch_smali.py similarity index 100% rename from scripts/patch_smali.py rename to skills/apk-reverse/scripts/patch_smali.py diff --git a/scripts/preflight.py b/skills/apk-reverse/scripts/preflight.py similarity index 100% rename from scripts/preflight.py rename to skills/apk-reverse/scripts/preflight.py diff --git a/scripts/probe_api.py b/skills/apk-reverse/scripts/probe_api.py similarity index 100% rename from scripts/probe_api.py rename to skills/apk-reverse/scripts/probe_api.py diff --git a/scripts/repack.py b/skills/apk-reverse/scripts/repack.py similarity index 100% rename from scripts/repack.py rename to skills/apk-reverse/scripts/repack.py diff --git a/scripts/run_probe.py b/skills/apk-reverse/scripts/run_probe.py similarity index 100% rename from scripts/run_probe.py rename to skills/apk-reverse/scripts/run_probe.py diff --git a/scripts/sig_probe.py b/skills/apk-reverse/scripts/sig_probe.py similarity index 100% rename from scripts/sig_probe.py rename to skills/apk-reverse/scripts/sig_probe.py diff --git a/scripts/smali_cp.txt b/skills/apk-reverse/scripts/smali_cp.txt similarity index 100% rename from scripts/smali_cp.txt rename to skills/apk-reverse/scripts/smali_cp.txt diff --git a/scripts/smtool.py b/skills/apk-reverse/scripts/smtool.py similarity index 100% rename from scripts/smtool.py rename to skills/apk-reverse/scripts/smtool.py diff --git a/scripts/snap.py b/skills/apk-reverse/scripts/snap.py similarity index 100% rename from scripts/snap.py rename to skills/apk-reverse/scripts/snap.py diff --git a/scripts/so_constpatch.py b/skills/apk-reverse/scripts/so_constpatch.py similarity index 100% rename from scripts/so_constpatch.py rename to skills/apk-reverse/scripts/so_constpatch.py diff --git a/scripts/tls_check.py b/skills/apk-reverse/scripts/tls_check.py similarity index 100% rename from scripts/tls_check.py rename to skills/apk-reverse/scripts/tls_check.py diff --git a/scripts/usb_net_proxy.py b/skills/apk-reverse/scripts/usb_net_proxy.py similarity index 100% rename from scripts/usb_net_proxy.py rename to skills/apk-reverse/scripts/usb_net_proxy.py