Files
apk-reverse/skills/apk-reverse/scripts/dexutil.py
T
inliver233 ab0a5ae0e2 dexutil: fix instruction widths that desynchronised the decode
The decoder could lose sync mid-method and keep producing plausible instructions,
only shifted -- so every offset derived after the fault point was silently wrong.
Found by differential testing against two independent decoders (dexlib2 and
androguard), not by reading the table and trusting it.

Fixed, each verified by asserting the walk ends exactly on
insns_off + insns_size*2 across three real dex images:

- Payload widths. The field after `00 <ident>` is a different quantity per payload
  kind: packed-switch has size(uint16)+first_key(int32), sparse-switch has
  size(uint16)+size*(key,target), fill-array-data has element_width(uint16)+
  size(uint32). All three were read as `1 + <one uint16>`, so a 30-element
  four-byte-wide array payload counted as 5 units instead of 64.
- 0x22 is 22c (2 units); 0x23/0x24 are 35c/3rc (3 units). They sat in each other's
  groups -- the inline comment already said `22c new-array` on a group returning 3.
- 0x20 was grouped as 2 units.

Result: methods that fail to land on the exact end went from 137/113/70 to 44/51/30
across the three images, a 55-68% reduction.

Two further slot-wise edits (shifting the goto family by one width) were made during
the investigation and REVERTED: measurement showed alignment got worse, because the
name table's misalignment had made the wrong slot look authoritative.

Remaining defect, recorded in-file rather than guessed at: OP_NAMES is misaligned with
the opcode slots from roughly 0x1a onward (the slot named `array-length` decodes as
`instance-of`, `goto/16` decodes as `goto`). Widths key on the opcode value, not the
name, so this does not corrupt a decode by itself -- but the names are unsafe as a
lookup key and they misled a reader here. Fixing it means rebuilding the table from an
independent decoder in one pass, not editing it slot by slot.

check_repo.py: 0 problems. check_refs.py: 182 reachable, 0 dangling.
2026-09-20 19:37:19 +08:00

645 lines
28 KiB
Python

#!/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,
}
# NOTE — known defect, measured, deliberately not guessed at.
#
# This NAME table is misaligned with the opcode slots from roughly 0x1a onward. Measured
# against dexlib2 by joining both decoders on instruction offset: the slot this table
# calls `array-length` decodes as `instance-of`, the slot called `new-instance` decodes
# as `array-length`, the slot called `goto/16` decodes as `goto`. Exact or not, the point
# is that the names are NOT a safe key for looking up an authoritative width.
#
# The width decisions in `insn_units` are keyed on the opcode VALUE, not on these names,
# so a wrong name does not by itself corrupt a decode -- but it will mislead a reader,
# and it misled one here into "fixing" two widths that were already right.
#
# Fixed and verified in this pass (method: full-decode alignment on three real dex
# images, asserting each walk ends exactly on insns_off + insns_size*2):
# * payload widths. The field after the `00 <ident>` is a DIFFERENT quantity per
# payload kind: packed-switch has size(uint16)+first_key(int32), sparse-switch has
# size(uint16)+size*(key,target), fill-array-data has element_width(uint16)+
# size(uint32). All three were previously read as `1 + <one uint16>`, so a 30-element
# 4-byte-wide array payload counted as 5 units instead of 64.
# * 0x22 is 22c (2 units) and 0x23/0x24 are 35c/3rc (3 units); they sat in each other's
# groups.
# * 0x20 was grouped as 2 units.
# Result: methods whose decode fails to land on the exact end went from 137/113/70 to
# 44/51/30 across the three images.
#
# Still open: the name misalignment above. It needs the table rebuilt from an
# independent decoder in one pass, not edited slot by slot -- two slot-wise edits made
# during this investigation were reverted after measurement showed they made alignment
# worse. Treat a name from this table as a hint, and check any width you intend to rely
# on against a second decoder.
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.
* The `goto` family is one slot off in the obvious reading: `0x27` is
`goto` (10t, **1** unit), `0x28` is `goto/16` (20t, **2** units) and
`0x29` is `goto/32` (30t, **3** units). Reading the offset width from the
mnemonic instead of the opcode desynchronises every branch of this shape.
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:
# Pseudo-instructions. `00 <ident>` with ident 1..3 is a switch or
# fill-array payload; a plain `00 00` is a one-unit nop, so never treat
# every 00 as a payload.
#
# Each payload has its OWN layout, and the field after the ident is not
# the same quantity in all three:
#
# 0x0100 packed-switch-payload: ident, size(uint16), first_key(int32),
# then size * int32 targets
# -> 2 + 2 + size*4 bytes = 4 + size*2 units
# 0x0200 sparse-switch-payload: ident, size(uint16), then size pairs of
# (int32 key, int32 target)
# -> 2 + 2 + size*8 bytes = 2 + size*4 units
# 0x0300 fill-array-data-payload: ident, element_width(uint16),
# size(uint32), then the raw data padded to an even byte count
# -> 4 units + ceil(size * element_width / 2)
#
# Reading size out of the wrong slot desynchronises silently: the walk
# keeps producing plausible instructions, only shifted. Measured on a
# real AOT class initialiser, a fill-array payload of 30 four-byte
# elements (64 units) was counted as 5, which is the bug this fixes.
if pos + 8 <= end:
ident = data[pos + 1] & 0xFF
if ident == 0x01:
return 4 + u16(data, pos + 2) * 2
if ident == 0x02:
return 2 + u16(data, pos + 2) * 4
if ident == 0x03:
width = u16(data, pos + 2)
count = u32(data, pos + 4)
return 4 + (count * width + 1) // 2
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, # monitor-enter / monitor-exit
0x20, # array-length is 12x, NOT 22c
0x26, 0x27): # 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,
0x21, 0x22): # 21s/21h/21c/22c
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: # measured: this slot is goto/16 (20t)
return 2
# -- 3 units ------------------------------------------------------------
if op in (0x03, 0x06, 0x09, # 32x
0x14, 0x18, # 31i const, const-wide/32
0x23, 0x24, # 35c / 3rc filled-new-array
0x25, # 31t fill-array-data
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))