Add native tamper/suicide guidance, toolchain map, and three scripts
references/native-tamper-and-suicide.md - how a hardened library terminates its own process: the mechanisms and their signatures, how to tell which one actually fires, arranged faults that call nothing (defeating every PLT-level fix), resolving a stub to its symbol, forged section headers, function boundaries from PT_GNU_EH_FRAME, scanner traps, and neutralising by returning rather than spinning. references/toolchain.md - what to install, non-interactive invocation, GUI-only tools (ask for a human instead of silently substituting a weaker method), version-alignment traps, and working offline. scripts/elf_plt.py - PLT stub to imported symbol on x86_64 and aarch64 from the relocation table; caller listing; library byte-diff naming each changed stub's symbol. scripts/apk_diff.py - entry-level diff by content hash, so same-size replacements are caught. scripts/native_crash.py - locate a native death from a log or tombstone and flag an arranged fault. pitfalls P31-P36; SKILL.md gains a decision question, a hard constraint on neutralising terminate paths, and cross-references from packers/native-and-so.
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Entry-level APK diff: what actually changed between two builds.
|
||||
|
||||
Two jobs, both recurring:
|
||||
|
||||
1. **Audit a build you did not produce.** Given an original and a
|
||||
"cracked"/"modded" copy, this lists every entry that differs, every entry
|
||||
the author ADDED, and every entry they REMOVED. Added entries under
|
||||
`lib/`, `assets/`, or new dex files are the interesting ones -- that is
|
||||
where injected payloads live.
|
||||
|
||||
2. **Verify your own build was surgical.** After repacking, the diff against
|
||||
the original must contain exactly the entries you intended to touch and
|
||||
nothing else. A surprise entry is a bug in your pipeline, not in the app.
|
||||
|
||||
It compares *content*, not just sizes: two builds can have identical sizes and
|
||||
different bytes, and a same-size replacement is exactly the case a careless
|
||||
comparison misses.
|
||||
|
||||
Usage
|
||||
-----
|
||||
python apk_diff.py original.apk modified.apk
|
||||
python apk_diff.py original.apk modified.apk --hide-identical
|
||||
python apk_diff.py original.apk modified.apk --only 'classes.*\\.dex|lib/.*'
|
||||
python apk_diff.py original.apk modified.apk --summary
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
# Directories worth naming explicitly -- this is where injected code lives.
|
||||
INTERESTING = ("lib/", "assets/", "META-INF/services/")
|
||||
|
||||
|
||||
def snapshot(path):
|
||||
"""{entry_name: (size, sha256, compress_type, crc)}"""
|
||||
out = {}
|
||||
with zipfile.ZipFile(path) as z:
|
||||
for it in z.infolist():
|
||||
if it.is_dir():
|
||||
continue
|
||||
data = z.read(it.filename)
|
||||
out[it.filename] = (
|
||||
len(data),
|
||||
hashlib.sha256(data).hexdigest(),
|
||||
it.compress_type,
|
||||
it.CRC,
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def classify(name):
|
||||
if re.match(r"^classes\d*\.dex$", name):
|
||||
return "dex"
|
||||
if name.startswith("META-INF/") and name.endswith((".RSA", ".SF", ".MF", ".DSA", ".EC")):
|
||||
return "signature"
|
||||
if name.startswith(INTERESTING):
|
||||
return "code/assets"
|
||||
return "other"
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description="Entry-level APK diff.")
|
||||
ap.add_argument("original")
|
||||
ap.add_argument("modified")
|
||||
ap.add_argument("--only", help="regex: restrict output to matching entry names")
|
||||
ap.add_argument("--hide-identical", action="store_true",
|
||||
help="omit the identical-count line")
|
||||
ap.add_argument("--summary", action="store_true",
|
||||
help="counts only, no per-entry listing")
|
||||
ap.add_argument("--max", type=int, default=200,
|
||||
help="max entries listed per section (default 200)")
|
||||
args = ap.parse_args()
|
||||
|
||||
try:
|
||||
a = snapshot(args.original)
|
||||
b = snapshot(args.modified)
|
||||
except Exception as e:
|
||||
sys.exit("error reading APK: %s" % e)
|
||||
|
||||
only_a = sorted(set(a) - set(b))
|
||||
only_b = sorted(set(b) - set(a))
|
||||
common = sorted(set(a) & set(b))
|
||||
changed = [n for n in common if a[n][1] != b[n][1]]
|
||||
|
||||
rx = re.compile(args.only) if args.only else None
|
||||
|
||||
def keep(n):
|
||||
return rx.search(n) if rx else True
|
||||
|
||||
def sha(path, n):
|
||||
return a[n][1][:16] if n in a else b[n][1][:16]
|
||||
|
||||
print("A (original) %s entries=%d" % (args.original, len(a)))
|
||||
print("B (modified) %s entries=%d" % (args.modified, len(b)))
|
||||
print("")
|
||||
print("only in A (removed) : %d" % len(only_a))
|
||||
print("only in B (added) : %d" % len(only_b))
|
||||
print("content changed : %d" % len(changed))
|
||||
if not args.hide_identical:
|
||||
print("identical : %d" % (len(common) - len(changed)))
|
||||
print("")
|
||||
|
||||
# Signature entries always differ; say so once instead of listing them.
|
||||
sig_only = [n for n in (only_a + only_b) if classify(n) == "signature"]
|
||||
if sig_only:
|
||||
print("(note: %d signature entries account for most of the add/remove noise)"
|
||||
% len(sig_only))
|
||||
|
||||
if args.summary:
|
||||
return
|
||||
|
||||
if only_b:
|
||||
print("\n=== ADDED in B (injection candidates) ===")
|
||||
shown = 0
|
||||
for n in only_b:
|
||||
if not keep(n) or classify(n) == "signature":
|
||||
continue
|
||||
print(" + %-52s %9d B [%s]" % (n, b[n][0], classify(n)))
|
||||
shown += 1
|
||||
if shown >= args.max:
|
||||
print(" ... (truncated)")
|
||||
break
|
||||
|
||||
if only_a:
|
||||
print("\n=== REMOVED from A ===")
|
||||
shown = 0
|
||||
for n in only_a:
|
||||
if not keep(n) or classify(n) == "signature":
|
||||
continue
|
||||
print(" - %-52s %9d B [%s]" % (n, a[n][0], classify(n)))
|
||||
shown += 1
|
||||
if shown >= args.max:
|
||||
print(" ... (truncated)")
|
||||
break
|
||||
|
||||
if changed:
|
||||
print("\n=== CONTENT CHANGED ===")
|
||||
shown = 0
|
||||
for n in changed:
|
||||
if not keep(n):
|
||||
continue
|
||||
sa, sb = a[n], b[n]
|
||||
cmark = "" if sa[2] == sb[2] else " ctype %d->%d" % (sa[2], sb[2])
|
||||
same_size = " (SAME SIZE)" if sa[0] == sb[0] else ""
|
||||
print(" ~ %-52s A %9d %s B %9d %s%s%s"
|
||||
% (n, sa[0], sa[1][:16], sb[0], sb[1][:16], same_size, cmark))
|
||||
shown += 1
|
||||
if shown >= args.max:
|
||||
print(" ... (truncated)")
|
||||
break
|
||||
if any(a[n][0] == b[n][0] for n in changed if keep(n)):
|
||||
print("\n (SAME SIZE means the change is not a length difference --")
|
||||
print(" a size-only comparison would have missed it entirely)")
|
||||
|
||||
if not (only_a or only_b or changed):
|
||||
print("no differences at all")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user