Close the byte-patching gap, add the server-config path, fix two script crashes
Worked end-to-end on a real unobfuscated commercial app (single-dex, no packer,
server-driven launch screen) and folded back everything that cost time. Net +17
files, no new dependencies, no sample-specific identifiers.
New references
byte-level-patching.md
The technique that was missing entirely. Equal-length in-place edits beat
method rebuilding (measured on a real sample: method rewrite grew debug_info
924 B -> 22.8 KB and the dex 4.32 MB -> 7.73 MB), but four failure modes are
silent: same-length is not enough, branch polarity, verifier legality, and
stale dex header fields. Covers locating an instruction's exact offset (why
listings and byte-pattern guesses both fail), the instruction width table and
the two traps that desynchronise a decode, neutralise-a-branch vs redirect-it,
and the header's signature-then-checksum order.
server-config-and-updates.md
The most common shape of "ad" in a modern app, and the one usually
mis-diagnosed: the server supplies UI the client renders. The two-layer fetch
(local fallback then remote override) that proves it, finding the config DTOs
through the field names data classes keep, why you patch the decision and not
the data, deciding the scope of "remove", and remote re-enable / cached config
durability.
New scripts
dexutil.py dependency-free dex reader: structural walk, exact
instruction decode, dex header recompute/verify in the
correct order, branch-target and operand helpers
dex_find_insn.py locate an instruction by decoded semantics, print its exact
byte offset with context and BOTH sides of any branch
dex_patch_bytes.py equal-length byte patches from a JSON spec: semantic match,
polarity pin via expect_next, equal-length enforcement,
verifier check, header recompute, re-decode to prove it landed
dex_check_verifier.py does any conditional branch target a move-result (bypassing
its producer)? Compares builds and separates pre-existing
findings from regressions the patch introduced
coldstart.py cold-launch capture: timed screenshot burst + logcat signals
+ installed-build facts + launch timing, and a warning when
the foreground activity is not the app
check_repo.py maintenance check: every script parses and answers --help,
every referenced path resolves, every file is documented
Fixes found while running the above
repack.py wrote archives with a plain zipfile writer, which cannot satisfy
the Android R+ install gate: resources.arsc must be STORED *and*
4-byte aligned, and a zip extra record cannot express a 1-3 byte
pad (a stored filler entry is inserted instead). Also fixes the
central-directory pointer and adds an alignment gate to the output.
usb_net_proxy.py, dex_strpatch.py
crashed on --help with a traceback instead of printing usage.
Updated
toolchain.md ddc added (single-binary dex->Java with query subcommands,
with measured timings and its real limits); "not on PATH is
not not-installed" with the search commands; which signer
to use and why jarsigner breaks alignment
repack-and-sign.md alignment requirement and its arithmetic; dex header
recompute; jarsigner hazard; vendor install interception
([N] with no symbolic constant -> root pm install); the
installer can keep owning the screen after an install
recon.md resolve package identity from a tool, not from hand-read
AXML strings (a wrong package sends all later device queries
nowhere); snapshot the config surface during recon
long-task-discipline.md captures you never looked at are not evidence (incl. the
consistent-but-wrong-window variant); long-context decay and
the record shape that survives it
dynamic-frida.md when live attach cannot work at all: static instrumentation,
static read, and self-recorded evidence, with the control
build caveat each one carries
ad-removal.md server-driven config is usually the EASIEST case, not the
hardest; a three-way zero result is a positive finding;
scope note on local brand splash vs promotional content
pitfalls.md P37 stale dex header checksum, P38 [-124] alignment,
P39 bare-numeric install failure, P40 wrong-window evidence,
P41 inverted branch polarity, P42 decode desynchronisation
SKILL.md, README.md symptom index, reference/script indexes and requirements
updated for all of the above
This commit is contained in:
@@ -0,0 +1,582 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Minimal, dependency-free dex reader: structure walk + exact instruction decode.
|
||||
|
||||
WHY THIS EXISTS
|
||||
---------------
|
||||
Locating the exact byte offset of one instruction is where byte-level patching
|
||||
either works or wastes an afternoon. Two shortcuts do not work and are worth
|
||||
naming so nobody re-derives them:
|
||||
|
||||
* Reconstructing offsets from a baksmali listing. `.line` directives track
|
||||
SOURCE lines, and one source line can span several instructions, so the
|
||||
values repeat and do not map 1:1 onto code offsets.
|
||||
* Matching a guessed byte sequence. Encodings vary with register numbers and
|
||||
operand widths, and a plausible-looking opcode can belong to a different
|
||||
format than you assume (0x38 is `if-test`, 22t/4 bytes -- not `if-testz`).
|
||||
|
||||
So: walk class -> class_data_item -> method -> code_item, then decode forward with
|
||||
a complete format table, and match on decoded semantics instead of bytes.
|
||||
|
||||
Three details in the walk that silently produce wrong answers:
|
||||
|
||||
* `class_data_item` member indices are DELTAS against the previous entry in the
|
||||
same list. Reading a raw uleb as an absolute index yields real-looking wrong
|
||||
members.
|
||||
* dalvik encodes switch/array payloads as `00 <ident> <size>` with ident 1..3.
|
||||
A plain `00 00` is an ordinary one-unit nop; treating every 00 as a payload
|
||||
swallows the next instruction and desynchronises the rest of the method.
|
||||
* The header field order is easy to mis-remember. See OFFSETS below, and always
|
||||
sanity-check the parsed sizes against the file.
|
||||
|
||||
Pure standard library. Import it, or run it for a per-method dump:
|
||||
|
||||
python dexutil.py <dex-or-apk> <Class/Name;> <method> <descriptor>
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import struct
|
||||
import sys
|
||||
import zipfile
|
||||
|
||||
# Verified dex header offsets. After magic(8) + checksum(4) + signature(20):
|
||||
OFFSETS = {
|
||||
"file_size": 0x20, "header_size": 0x24, "endian_tag": 0x28,
|
||||
"link_size": 0x2C, "link_off": 0x30, "map_off": 0x34,
|
||||
"string_ids_size": 0x38, "string_ids_off": 0x3C,
|
||||
"type_ids_size": 0x40, "type_ids_off": 0x44,
|
||||
"proto_ids_size": 0x48, "proto_ids_off": 0x4C,
|
||||
"field_ids_size": 0x50, "field_ids_off": 0x54,
|
||||
"method_ids_size": 0x58, "method_ids_off": 0x5C,
|
||||
"class_defs_size": 0x60, "class_defs_off": 0x64,
|
||||
"data_size": 0x68, "data_off": 0x6C,
|
||||
}
|
||||
|
||||
OP_NAMES = {
|
||||
0x00: "nop", 0x01: "move", 0x02: "move/from16", 0x03: "move/16",
|
||||
0x04: "move-wide", 0x05: "move-wide/from16", 0x06: "move-wide/16",
|
||||
0x07: "move-object", 0x08: "move-object/from16", 0x09: "move-object/16",
|
||||
0x0A: "move-result", 0x0B: "move-result-wide", 0x0C: "move-result-object",
|
||||
0x0D: "move-exception", 0x0E: "return-void", 0x0F: "return",
|
||||
0x10: "return-wide", 0x11: "return-object", 0x12: "const/4",
|
||||
0x13: "const/16", 0x14: "const", 0x15: "const/high16",
|
||||
0x16: "const-wide/high16", 0x17: "const-wide/16", 0x18: "const-wide/32",
|
||||
0x19: "const-string", 0x1A: "const-string/jumbo", 0x1B: "const-class",
|
||||
0x1C: "monitor-enter", 0x1D: "monitor-exit", 0x1E: "check-cast",
|
||||
0x1F: "instance-of", 0x20: "array-length", 0x21: "new-instance",
|
||||
0x22: "new-array", 0x23: "filled-new-array", 0x24: "filled-new-array/range",
|
||||
0x25: "fill-array-data", 0x26: "throw", 0x27: "goto",
|
||||
0x28: "goto/16", 0x29: "goto/32", 0x2A: "packed-switch",
|
||||
0x2B: "sparse-switch",
|
||||
0x32: "if-eq", 0x33: "if-ne", 0x34: "if-lt", 0x35: "if-ge",
|
||||
0x36: "if-gt", 0x37: "if-le",
|
||||
0x38: "if-eqz", 0x39: "if-nez", 0x3A: "if-ltz", 0x3B: "if-gez",
|
||||
0x3C: "if-gtz", 0x3D: "if-lez",
|
||||
0x52: "iget", 0x53: "iget-wide", 0x54: "iget-object", 0x55: "iget-boolean",
|
||||
0x56: "iget-byte", 0x57: "iget-char", 0x58: "iget-short",
|
||||
0x59: "iput", 0x5A: "iput-wide", 0x5B: "iput-object", 0x5C: "iput-boolean",
|
||||
0x5D: "iput-byte", 0x5E: "iput-char", 0x5F: "iput-short",
|
||||
0x60: "sget", 0x61: "sget-wide", 0x62: "sget-object", 0x63: "sget-boolean",
|
||||
0x64: "sget-byte", 0x65: "sget-char", 0x66: "sget-short",
|
||||
0x67: "sput", 0x68: "sput-wide", 0x69: "sput-object", 0x6A: "sput-boolean",
|
||||
0x6B: "sput-byte", 0x6C: "sput-char", 0x6D: "sput-short",
|
||||
0x6E: "invoke-virtual", 0x6F: "invoke-super", 0x70: "invoke-direct",
|
||||
0x71: "invoke-static", 0x72: "invoke-interface",
|
||||
0x74: "invoke-virtual/range", 0x75: "invoke-super/range",
|
||||
0x76: "invoke-direct/range", 0x77: "invoke-static/range",
|
||||
0x78: "invoke-interface/range",
|
||||
}
|
||||
|
||||
# Opcode groups whose operands are a pair of registers plus an int16 offset.
|
||||
IF_TEST = set(range(0x32, 0x38)) # 22t: 2 register nibbles + int16
|
||||
IF_TESTZ = set(range(0x38, 0x3E)) # 21t: 1 register nibble + int16
|
||||
RETURN_OPS = (0x0F, 0x10, 0x11)
|
||||
MOVE_RESULT_OPS = (0x0A, 0x0B, 0x0C)
|
||||
INVOKE_OPS = tuple(range(0x6E, 0x73)) + tuple(range(0x74, 0x79))
|
||||
# opcodes that read an instance field: (dest_reg, object_reg, field_idx)
|
||||
IFIELD_OPS = tuple(range(0x52, 0x59))
|
||||
# opcodes that write an instance field: (value_reg, object_reg, field_idx)
|
||||
PFIELD_OPS = tuple(range(0x59, 0x60))
|
||||
SFIELD_OPS = tuple(range(0x60, 0x6E))
|
||||
|
||||
|
||||
def u16(b, o):
|
||||
return b[o] | (b[o + 1] << 8)
|
||||
|
||||
|
||||
def u32(b, o):
|
||||
return b[o] | (b[o + 1] << 8) | (b[o + 2] << 16) | (b[o + 3] << 24)
|
||||
|
||||
|
||||
def s16(v):
|
||||
return v - 0x10000 if v > 0x7FFF else v
|
||||
|
||||
|
||||
def read_uleb(b, o):
|
||||
"""ULEB128 -> (value, new_offset). Member indices in class_data are deltas."""
|
||||
result = 0
|
||||
shift = 0
|
||||
while True:
|
||||
x = b[o]
|
||||
o += 1
|
||||
result |= (x & 0x7F) << shift
|
||||
if (x & 0x80) == 0:
|
||||
break
|
||||
shift += 7
|
||||
return result, o
|
||||
|
||||
|
||||
def insn_units(op, data, pos, end):
|
||||
"""Instruction length in 16-bit code units.
|
||||
|
||||
One wrong width desynchronises everything after it, and the failure is quiet:
|
||||
the decode keeps producing plausible-looking instructions, just shifted, so
|
||||
every offset you derive from that point is wrong. Two width traps are common
|
||||
enough to call out:
|
||||
|
||||
* `0x32`-`0x3D` (if-test 22t / if-testz 21t) are **2** units, not 1. Treating
|
||||
them as 1 invents a fake second instruction at every branch.
|
||||
* `0x1A` (const-string/jumbo) is emitted as a **4-byte** form (op, register,
|
||||
uint16 string index) by real toolchains and read that way by ART and
|
||||
baksmali, even though the reference format for the opcode is 31c. Counting
|
||||
it as 3 units is the single most costly width error -- it shifts the rest
|
||||
of the method by one unit per occurrence.
|
||||
* `0x28` is `goto` (10t, **1** unit), `0x29` is `goto/16` (2 units) and
|
||||
`0x2A` is `goto/32` (3 units). Do not read 0x28 as a two-unit form.
|
||||
|
||||
Whenever a decode is used to derive a patch offset, assert that the walk ends
|
||||
exactly on `insns_off + insns_size*2`. See `decode_all`.
|
||||
"""
|
||||
if op == 0x00:
|
||||
# payload nop: `00 <ident> <size>` with ident 1..3; a plain `00 00` is a
|
||||
# one-unit nop. Never treat every 00 as a payload.
|
||||
if pos + 4 <= end and (data[pos + 1] & 0xFF) in (0x01, 0x02, 0x03):
|
||||
return 1 + int.from_bytes(data[pos + 2:pos + 4], "little")
|
||||
return 1
|
||||
# -- 1 unit -------------------------------------------------------------
|
||||
if op in (0x01, 0x04, 0x07, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
|
||||
0x11, 0x12, # 12x / 11x / 11n / 10x
|
||||
0x1C, 0x1D, 0x26, 0x27): # monitor-enter/exit, throw, goto
|
||||
return 1
|
||||
if 0x7B <= op <= 0x8F: # 12x unop
|
||||
return 1
|
||||
if 0xB0 <= op <= 0xCF: # 12x binop/2addr
|
||||
return 1
|
||||
if 0xE3 <= op <= 0xFC: # 10x family
|
||||
return 1
|
||||
# -- 2 units ------------------------------------------------------------
|
||||
if op in (0x02, 0x05, 0x08): # 22x move/from16 variants
|
||||
return 2
|
||||
if op in (0x13, 0x15, 0x16, 0x17, 0x19, 0x1A, 0x1B, 0x1E, 0x1F,
|
||||
0x20, 0x21, 0x23, 0x24): # 21s/21h/21c/22c/35c/3rc
|
||||
return 2
|
||||
if 0x2C <= op <= 0x31: # 23x cmp
|
||||
return 2
|
||||
if op in IF_TEST or op in IF_TESTZ: # 22t / 21t <-- 2, not 1
|
||||
return 2
|
||||
if 0x44 <= op <= 0x51: # 23x aget/aput
|
||||
return 2
|
||||
if 0x52 <= op <= 0x5F: # 22c iget/iput
|
||||
return 2
|
||||
if 0x60 <= op <= 0x6D: # 21c sget/sput
|
||||
return 2
|
||||
if 0x90 <= op <= 0xAF: # 23x binop
|
||||
return 2
|
||||
if 0xD0 <= op <= 0xD7: # 22s binop/lit16
|
||||
return 2
|
||||
if 0xD8 <= op <= 0xE2: # 22b binop/lit8
|
||||
return 2
|
||||
if op == 0x29: # goto/16 (20t)
|
||||
return 2
|
||||
# -- 3 units ------------------------------------------------------------
|
||||
if op in (0x03, 0x06, 0x09, # 32x
|
||||
0x14, 0x18, # 31i const, const-wide/32
|
||||
0x22, 0x25, # 22c new-array, 31t fill-array
|
||||
0x2A, 0x2B, # 31t packed/sparse-switch
|
||||
0x6E, 0x6F, 0x70, 0x71, 0x72, # 35c invoke
|
||||
0x74, 0x75, 0x76, 0x77, 0x78): # 3rc invoke/range
|
||||
return 3
|
||||
# -- payloads / unknown -------------------------------------------------
|
||||
return 1
|
||||
|
||||
|
||||
class Dex(object):
|
||||
"""Structural reader for one dex image."""
|
||||
|
||||
def __init__(self, data, name="classes.dex"):
|
||||
self.d = data
|
||||
self.name = name
|
||||
self.header = {k: u32(data, v) for k, v in OFFSETS.items()}
|
||||
|
||||
# ---- sanity -----------------------------------------------------------
|
||||
def check(self):
|
||||
"""Return a list of structural complaints; empty means it parses."""
|
||||
problems = []
|
||||
size = len(self.d)
|
||||
if self.d[:4] != b"dex\n":
|
||||
problems.append("not a dex: magic=%r" % self.d[:4])
|
||||
for key in ("string_ids_off", "type_ids_off", "proto_ids_off",
|
||||
"field_ids_off", "method_ids_off", "class_defs_off"):
|
||||
off = self.header[key]
|
||||
if not (0 < off < size):
|
||||
problems.append("%s=0x%x out of range (file size %d)"
|
||||
% (key, off, size))
|
||||
declared = self.header["file_size"]
|
||||
if declared and declared != size:
|
||||
problems.append("header file_size=%d but actual=%d" % (declared, size))
|
||||
return problems
|
||||
|
||||
# ---- index tables -----------------------------------------------------
|
||||
def string(self, idx):
|
||||
p = u32(self.d, self.header["string_ids_off"] + idx * 4)
|
||||
_utf16_len, p = read_uleb(self.d, p)
|
||||
end = self.d.index(b"\x00", p)
|
||||
return self.d[p:end].decode("utf-8", "replace")
|
||||
|
||||
def string_safe(self, idx):
|
||||
"""Like string() but never raises -- a desynced decode passes junk index.
|
||||
|
||||
A renderer that throws on a bad index hides the very symptom the caller is
|
||||
looking for (a decode that has drifted), so return a marker instead.
|
||||
"""
|
||||
try:
|
||||
if idx >= self.header["string_ids_size"]:
|
||||
return "<string_idx %d out of range>" % idx
|
||||
return self.string(idx)
|
||||
except Exception:
|
||||
return "<string_idx %d unreadable>" % idx
|
||||
|
||||
def type_(self, idx):
|
||||
return self.string(u32(self.d, self.header["type_ids_off"] + idx * 4))
|
||||
|
||||
def proto(self, idx):
|
||||
o = self.header["proto_ids_off"] + idx * 12
|
||||
ret = self.type_(u32(self.d, o + 4))
|
||||
poff = u32(self.d, o + 8)
|
||||
params = []
|
||||
if poff:
|
||||
n = u32(self.d, poff)
|
||||
p = poff + 4
|
||||
for _ in range(n):
|
||||
params.append(self.type_(u16(self.d, p)))
|
||||
p += 2
|
||||
return "(" + "".join(params) + ")" + ret
|
||||
|
||||
def field(self, idx):
|
||||
o = self.header["field_ids_off"] + idx * 8
|
||||
return (self.type_(u16(self.d, o)), self.string(u32(self.d, o + 4)),
|
||||
self.type_(u16(self.d, o + 2)))
|
||||
|
||||
def method(self, idx):
|
||||
o = self.header["method_ids_off"] + idx * 8
|
||||
return (self.type_(u16(self.d, o)), self.string(u32(self.d, o + 4)),
|
||||
self.proto(u16(self.d, o + 2)))
|
||||
|
||||
def find_class(self, fqcn):
|
||||
"""Return the class_def_item offset for a 'Lpkg/Name;' FQCN, or None."""
|
||||
for i in range(self.header["class_defs_size"]):
|
||||
o = self.header["class_defs_off"] + i * 32
|
||||
if self.type_(u32(self.d, o)) == fqcn:
|
||||
return o
|
||||
return None
|
||||
|
||||
def methods_of(self, fqcn):
|
||||
"""Yield (section, method_idx, class, name, descriptor, code_off)."""
|
||||
cd = self.find_class(fqcn)
|
||||
if cd is None:
|
||||
raise KeyError("class not found: %s" % fqcn)
|
||||
p = u32(self.d, cd + 24)
|
||||
if p == 0:
|
||||
return
|
||||
sf, p = read_uleb(self.d, p)
|
||||
inf, p = read_uleb(self.d, p)
|
||||
dm, p = read_uleb(self.d, p)
|
||||
vm, p = read_uleb(self.d, p)
|
||||
for _ in range(sf + inf): # skip field lists
|
||||
_i, p = read_uleb(self.d, p)
|
||||
_a, p = read_uleb(self.d, p)
|
||||
for section, count in (("direct", dm), ("virtual", vm)):
|
||||
running = 0
|
||||
for _ in range(count):
|
||||
diff, p = read_uleb(self.d, p)
|
||||
running += diff # indices are delta-encoded
|
||||
_acc, p = read_uleb(self.d, p)
|
||||
code_off, p = read_uleb(self.d, p)
|
||||
cls, name, desc = self.method(running)
|
||||
yield section, running, cls, name, desc, code_off
|
||||
|
||||
def find_method(self, fqcn, name, desc):
|
||||
"""Return (section, method_idx, code_off) or None."""
|
||||
for section, idx, _cls, nm, ds, code_off in self.methods_of(fqcn):
|
||||
if nm == name and ds == desc:
|
||||
return section, idx, code_off
|
||||
return None
|
||||
|
||||
def find_methods_named(self, fqcn, name):
|
||||
"""All overloads of a name -> list of (section, idx, desc, code_off)."""
|
||||
out = []
|
||||
for section, idx, _cls, nm, ds, code_off in self.methods_of(fqcn):
|
||||
if nm == name:
|
||||
out.append((section, idx, ds, code_off))
|
||||
return out
|
||||
|
||||
def class_names(self):
|
||||
for i in range(self.header["class_defs_size"]):
|
||||
o = self.header["class_defs_off"] + i * 32
|
||||
yield self.type_(u32(self.d, o))
|
||||
|
||||
# ---- code -------------------------------------------------------------
|
||||
def code_info(self, code_off):
|
||||
return {
|
||||
"registers": u16(self.d, code_off),
|
||||
"ins": u16(self.d, code_off + 2),
|
||||
"outs": u16(self.d, code_off + 4),
|
||||
"tries": u16(self.d, code_off + 6),
|
||||
"debug_info_off": u32(self.d, code_off + 8),
|
||||
"insns_size": u32(self.d, code_off + 12),
|
||||
"insns_off": code_off + 16,
|
||||
}
|
||||
|
||||
def decode(self, code_off):
|
||||
"""Yield dicts describing each instruction of one method body.
|
||||
|
||||
Callers should assert the walk ends exactly on insns_off+insns_size*2;
|
||||
landing short or long means the format table is wrong somewhere.
|
||||
"""
|
||||
info = self.code_info(code_off)
|
||||
pos = info["insns_off"]
|
||||
end = pos + info["insns_size"] * 2
|
||||
while pos < end:
|
||||
op = self.d[pos]
|
||||
units = insn_units(op, self.d, pos, end)
|
||||
if units < 1 or pos + units * 2 > end:
|
||||
return
|
||||
yield {
|
||||
"off": pos, "op": op, "units": units,
|
||||
"name": OP_NAMES.get(op, "op_%02x" % op),
|
||||
"raw": bytes(self.d[pos:pos + units * 2]),
|
||||
"registers": info["registers"],
|
||||
}
|
||||
pos += units * 2
|
||||
|
||||
def decode_all(self, code_off):
|
||||
"""Full listing as a list, plus a verdict on whether it ended cleanly."""
|
||||
insns = list(self.decode(code_off))
|
||||
info = self.code_info(code_off)
|
||||
expected = info["insns_off"] + info["insns_size"] * 2
|
||||
ended = insns[-1]["off"] + insns[-1]["units"] * 2 if insns else info["insns_off"]
|
||||
return insns, (ended == expected), expected
|
||||
|
||||
def insn_at(self, code_off, target_off):
|
||||
for insn in self.decode(code_off):
|
||||
if insn["off"] == target_off:
|
||||
return insn
|
||||
return None
|
||||
|
||||
# ---- operand helpers --------------------------------------------------
|
||||
def branch_target(self, insn):
|
||||
"""Absolute target of a branch, or None if the instruction is not one."""
|
||||
op, pos, raw = insn["op"], insn["off"], insn["raw"]
|
||||
if op in IF_TEST or op in IF_TESTZ:
|
||||
return pos + s16(u16(self.d, pos + 2)) * 2
|
||||
if op == 0x27: # goto (10t, signed byte)
|
||||
off = raw[1]
|
||||
if off > 127:
|
||||
off -= 256
|
||||
return pos + off * 2
|
||||
if op == 0x28:
|
||||
return pos + s16(u16(self.d, pos + 2)) * 2
|
||||
if op == 0x29:
|
||||
off = u32(self.d, pos + 2)
|
||||
if off > 0x7FFFFFFF:
|
||||
off -= 0x100000000
|
||||
return pos + off * 2
|
||||
return None
|
||||
|
||||
def branch_regs(self, insn):
|
||||
"""(regs_read, kind) for a conditional branch."""
|
||||
if insn["op"] in IF_TEST:
|
||||
return [insn["raw"][1] >> 4, insn["raw"][1] & 0xF], "if-test"
|
||||
if insn["op"] in IF_TESTZ:
|
||||
return [insn["raw"][1] & 0xF], "if-testz"
|
||||
return [], None
|
||||
|
||||
def all_targets(self, code_off):
|
||||
"""Set of every absolute branch target in one method."""
|
||||
targets = set()
|
||||
for insn in self.decode(code_off):
|
||||
t = self.branch_target(insn)
|
||||
if t is not None:
|
||||
targets.add(t)
|
||||
return targets
|
||||
|
||||
# ---- description ------------------------------------------------------
|
||||
def describe(self, insn):
|
||||
"""One-line human rendering with resolved field/method/string names."""
|
||||
op, pos, raw = insn["op"], insn["off"], insn["raw"]
|
||||
text = "0x%x: %-14s %s" % (pos, raw.hex(), insn["name"])
|
||||
if op in IF_TEST:
|
||||
text += " v%d,v%d -> 0x%x" % (raw[1] >> 4, raw[1] & 0xF,
|
||||
self.branch_target(insn))
|
||||
elif op in IF_TESTZ:
|
||||
text += " v%d -> 0x%x" % (raw[1] & 0xF, self.branch_target(insn))
|
||||
elif op in (0x27, 0x28, 0x29):
|
||||
text += " -> 0x%x" % self.branch_target(insn)
|
||||
elif op in IFIELD_OPS:
|
||||
cls, nm, ty = self.field(u16(self.d, pos + 2))
|
||||
text += " v%d <- v%d.%s:%s" % (raw[1] & 0xF, raw[1] >> 4, nm, ty)
|
||||
elif op in PFIELD_OPS:
|
||||
cls, nm, ty = self.field(u16(self.d, pos + 2))
|
||||
text += " v%d -> v%d.%s:%s" % (raw[1] & 0xF, raw[1] >> 4, nm, ty)
|
||||
elif op in SFIELD_OPS:
|
||||
cls, nm, ty = self.field(u16(self.d, pos + 2))
|
||||
text += " %s.%s:%s" % (cls, nm, ty)
|
||||
elif op in INVOKE_OPS:
|
||||
cls, nm, ds = self.method(u16(self.d, pos + 2))
|
||||
text += " %s.%s%s" % (cls, nm, ds)
|
||||
elif op in (0x19, 0x1A):
|
||||
# Both forms carry the string index as a uint16 at byte offset 2.
|
||||
# Reading a uint32 here gives a huge index that walks off the table.
|
||||
text += ' "%s"' % self.string_safe(u16(self.d, pos + 2))
|
||||
elif op == 0x12:
|
||||
lit = raw[1] & 0xF
|
||||
text += " v%d, %d" % (raw[1] >> 4, lit - 16 if lit > 7 else lit)
|
||||
return text
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# dex header integrity
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def fix_dex_header(data):
|
||||
"""Recompute a dex header's checksum and signature IN THE CORRECT ORDER.
|
||||
|
||||
Order is not cosmetic:
|
||||
bytes 12..32 = sha1(data[32:]) -- signature first
|
||||
bytes 8..12 = adler32(data[12:]) -- checksum last, covers the signature
|
||||
|
||||
Reversed, the adler32 is taken while the signature field is still zeroed, so
|
||||
the header never verifies. Android logs
|
||||
`Failure to verify dex file ...: Bad checksum (computed, expected)` -- the
|
||||
real value shows up as "expected" -- and falls back to interpreting the dex,
|
||||
which can surface as an unrelated ClassNotFoundException at startup.
|
||||
|
||||
Accepts and returns a bytearray; also returns the before/after values so a
|
||||
caller can print them.
|
||||
"""
|
||||
before_checksum = int.from_bytes(data[8:12], "little")
|
||||
before_signature = bytes(data[12:32])
|
||||
|
||||
data[12:32] = hashlib.sha1(bytes(data[32:])).digest()
|
||||
after_signature = bytes(data[12:32])
|
||||
|
||||
import zlib
|
||||
after_checksum = zlib.adler32(bytes(data[12:])) & 0xFFFFFFFF
|
||||
data[8:12] = after_checksum.to_bytes(4, "little")
|
||||
|
||||
return {
|
||||
"before_checksum": before_checksum, "after_checksum": after_checksum,
|
||||
"before_signature": before_signature, "after_signature": after_signature,
|
||||
}
|
||||
|
||||
|
||||
def verify_dex_header(data):
|
||||
"""Return (checksum_ok, signature_ok) for a bytearray/bytes dex."""
|
||||
import zlib
|
||||
stored_c = int.from_bytes(data[8:12], "little")
|
||||
calc_c = zlib.adler32(bytes(data[12:])) & 0xFFFFFFFF
|
||||
return stored_c == calc_c, bytes(data[12:32]) == hashlib.sha1(bytes(data[32:])).digest()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# loading
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def load_dex(source, entry=None):
|
||||
"""Load a Dex from a .dex path, an .apk/.zip path, or raw bytes.
|
||||
|
||||
For an archive, `entry` picks a member (default: the first classes*.dex).
|
||||
Returns (Dex, entry_name).
|
||||
"""
|
||||
if isinstance(source, (bytes, bytearray)):
|
||||
return Dex(bytes(source), entry or "<bytes>"), entry or "<bytes>"
|
||||
path = str(source)
|
||||
if path.lower().endswith((".apk", ".zip", ".jar", ".xapk", ".apks", ".apkm")):
|
||||
with zipfile.ZipFile(path) as z:
|
||||
if entry:
|
||||
return Dex(z.read(entry), entry), entry
|
||||
candidates = sorted(n for n in z.namelist()
|
||||
if n.startswith("classes") and n.endswith(".dex"))
|
||||
if not candidates:
|
||||
raise ValueError("no classes*.dex in %s" % path)
|
||||
name = candidates[0]
|
||||
return Dex(z.read(name), name), name
|
||||
with open(path, "rb") as fh:
|
||||
return Dex(fh.read(), path), path
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) != 5:
|
||||
print(__doc__)
|
||||
return 2
|
||||
target, fqcn, name, desc = argv[1:5]
|
||||
dex, entry = load_dex(target)
|
||||
print("== %s (entry %s, %d bytes)" % (target, entry, len(dex.d)))
|
||||
problems = dex.check()
|
||||
for p in problems:
|
||||
print(" [structure] %s" % p)
|
||||
if problems:
|
||||
print(" refusing to continue on a structurally broken read")
|
||||
return 1
|
||||
|
||||
found = dex.find_method(fqcn, name, desc)
|
||||
if not found:
|
||||
print("method not found: %s->%s%s" % (fqcn, name, desc))
|
||||
print("methods in class:")
|
||||
for section, idx, _c, nm, ds, code_off in dex.methods_of(fqcn):
|
||||
print(" [%s] %s%s code_off=0x%x" % (section, nm, ds, code_off))
|
||||
return 1
|
||||
|
||||
section, idx, code_off = found
|
||||
info = dex.code_info(code_off)
|
||||
insns, clean, expected = dex.decode_all(code_off)
|
||||
print("== %s.%s%s [%s] method_idx=%d" % (fqcn, name, desc, section, idx))
|
||||
print(" code_off=0x%x insns_off=0x%x insns_size=%d registers=%d"
|
||||
% (code_off, info["insns_off"], info["insns_size"], info["registers"]))
|
||||
print(" decoded %d instructions; ended cleanly: %s" % (len(insns), clean))
|
||||
if not clean:
|
||||
print(" WARNING: decode did not land on 0x%x -- offsets below are suspect"
|
||||
% expected)
|
||||
|
||||
regs = info["registers"]
|
||||
over = [i for i in insns if _max_reg(i) >= regs]
|
||||
if over:
|
||||
print(" WARNING: %d instruction(s) reference a register >= %d; "
|
||||
"desync likely" % (len(over), regs))
|
||||
|
||||
targets = dex.all_targets(code_off)
|
||||
print(" branch targets: %s" % ", ".join(sorted("0x%x" % t for t in targets)))
|
||||
print("-- listing --")
|
||||
for insn in insns:
|
||||
mark = " <== branch target" if insn["off"] in targets else ""
|
||||
print(" " + dex.describe(insn) + mark)
|
||||
return 0
|
||||
|
||||
|
||||
def _max_reg(insn):
|
||||
"""Highest register number named by an instruction (rough but sufficient)."""
|
||||
raw, op = insn["raw"], insn["op"]
|
||||
if op in IF_TEST or op in IFIELD_OPS or op in PFIELD_OPS:
|
||||
return max(raw[1] >> 4, raw[1] & 0xF)
|
||||
if op in IF_TESTZ:
|
||||
return raw[1] & 0xF
|
||||
if op == 0x12:
|
||||
return raw[1] >> 4
|
||||
if op in (0x01, 0x04, 0x07, 0x0A, 0x0B, 0x0C, 0x0D, 0x0F, 0x10, 0x11,
|
||||
0x1C, 0x1D, 0x1E):
|
||||
return raw[1] & 0xF
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
Reference in New Issue
Block a user