#!/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 ` 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 """ 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 ` 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 + `, 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/16", 0x17: "const-wide/32", 0x18: "const-wide", 0x19: "const-wide/high16", 0x1A: "const-string", 0x1B: "const-string/jumbo", 0x1C: "const-class", 0x1D: "monitor-enter", 0x1E: "monitor-exit", 0x1F: "check-cast", 0x20: "instance-of", 0x21: "array-length", 0x22: "new-instance", 0x23: "new-array", 0x24: "filled-new-array", 0x25: "filled-new-array/range", 0x26: "fill-array-data", 0x27: "throw", 0x28: "goto", 0x29: "goto/16", 0x2A: "goto/32", 0x2B: "packed-switch", 0x2C: "sparse-switch", 0x2D: "cmpl-float", 0x2E: "cmpg-float", 0x2F: "cmpl-double", 0x30: "cmpg-double", 0x31: "cmp-long", 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", 0x44: "aget", 0x45: "aget-wide", 0x46: "aget-object", 0x47: "aget-boolean", 0x48: "aget-byte", 0x49: "aget-char", 0x4A: "aget-short", 0x4B: "aput", 0x4C: "aput-wide", 0x4D: "aput-object", 0x4E: "aput-boolean", 0x4F: "aput-byte", 0x50: "aput-char", 0x51: "aput-short", 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", 0x7B: "neg-int", 0x7C: "not-int", 0x7D: "neg-long", 0x7E: "not-long", 0x7F: "neg-float", 0x80: "neg-double", 0x81: "int-to-long", 0x82: "int-to-float", 0x83: "int-to-double", 0x84: "long-to-int", 0x85: "long-to-float", 0x86: "long-to-double", 0x87: "float-to-int", 0x88: "float-to-long", 0x89: "float-to-double", 0x8A: "double-to-int", 0x8B: "double-to-long", 0x8C: "double-to-float", 0x8D: "int-to-byte", 0x8E: "int-to-char", 0x8F: "int-to-short", 0x90: "add-int", 0x91: "sub-int", 0x92: "mul-int", 0x93: "div-int", 0x94: "rem-int", 0x95: "and-int", 0x96: "or-int", 0x97: "xor-int", 0x98: "shl-int", 0x99: "shr-int", 0x9A: "ushr-int", 0x9B: "add-long", 0x9C: "sub-long", 0x9D: "mul-long", 0x9E: "div-long", 0x9F: "rem-long", 0xA0: "and-long", 0xA1: "or-long", 0xA2: "xor-long", 0xA3: "shl-long", 0xA4: "shr-long", 0xA5: "ushr-long", 0xA6: "add-float", 0xA7: "sub-float", 0xA8: "mul-float", 0xA9: "div-float", 0xAA: "rem-float", 0xAB: "add-double", 0xAC: "sub-double", 0xAD: "mul-double", 0xAE: "div-double", 0xAF: "rem-double", 0xB0: "add-int/2addr", 0xB1: "sub-int/2addr", 0xB2: "mul-int/2addr", 0xB3: "div-int/2addr", 0xB4: "rem-int/2addr", 0xB5: "and-int/2addr", 0xB6: "or-int/2addr", 0xB7: "xor-int/2addr", 0xB8: "shl-int/2addr", 0xB9: "shr-int/2addr", 0xBA: "ushr-int/2addr", 0xBB: "add-long/2addr", 0xBC: "sub-long/2addr", 0xBD: "mul-long/2addr", 0xBE: "div-long/2addr", 0xBF: "rem-long/2addr", 0xC0: "and-long/2addr", 0xC1: "or-long/2addr", 0xC2: "xor-long/2addr", 0xC3: "shl-long/2addr", 0xC4: "shr-long/2addr", 0xC5: "ushr-long/2addr", 0xC6: "add-float/2addr", 0xC7: "sub-float/2addr", 0xC8: "mul-float/2addr", 0xC9: "div-float/2addr", 0xCA: "rem-float/2addr", 0xCB: "add-double/2addr", 0xCC: "sub-double/2addr", 0xCD: "mul-double/2addr", 0xCE: "div-double/2addr", 0xCF: "rem-double/2addr", 0xD0: "add-int/lit16", 0xD1: "rsub-int", 0xD2: "mul-int/lit16", 0xD3: "div-int/lit16", 0xD4: "rem-int/lit16", 0xD5: "and-int/lit16", 0xD6: "or-int/lit16", 0xD7: "xor-int/lit16", 0xD8: "add-int/lit8", 0xD9: "rsub-int/lit8", 0xDA: "mul-int/lit8", 0xDB: "div-int/lit8", 0xDC: "rem-int/lit8", 0xDD: "and-int/lit8", 0xDE: "or-int/lit8", 0xDF: "xor-int/lit8", 0xE0: "shl-int/lit8", 0xE1: "shr-int/lit8", 0xE2: "ushr-int/lit8", 0xFA: "invoke-polymorphic", 0xFB: "invoke-polymorphic/range", 0xFC: "invoke-custom", 0xFD: "invoke-custom/range", 0xFE: "const-method-handle", 0xFF: "const-method-type", } # Instruction length in 16-bit code units, one entry per valid opcode. # # Keyed by opcode from the format specification, NOT inferred from a mnemonic # and NOT copied from a run of neighbours. 0x16-0x2C alternates # 2,3,5,2,2,3,2,1,1,2,2,1,2,2,3,3,3,1,1,2,3,3,3 -- an entire run where the # plausible-looking "same family, same width" assumption is wrong at nine # opcodes. A single wrong entry shifts every instruction after it in the same # method, and the decode keeps producing plausible instructions, so nothing # looks broken until an operand index walks off its table. OP_UNITS = { 0x00: 1, 0x01: 1, 0x02: 2, 0x03: 3, 0x04: 1, 0x05: 2, 0x06: 3, 0x07: 1, 0x08: 2, 0x09: 3, 0x0A: 1, 0x0B: 1, 0x0C: 1, 0x0D: 1, 0x0E: 1, 0x0F: 1, 0x10: 1, 0x11: 1, 0x12: 1, 0x13: 2, 0x14: 3, 0x15: 2, 0x16: 2, 0x17: 3, 0x18: 5, 0x19: 2, 0x1A: 2, 0x1B: 3, 0x1C: 2, 0x1D: 1, 0x1E: 1, 0x1F: 2, 0x20: 2, 0x21: 1, 0x22: 2, 0x23: 2, 0x24: 3, 0x25: 3, 0x26: 3, 0x27: 1, 0x28: 1, 0x29: 2, 0x2A: 3, 0x2B: 3, 0x2C: 3, 0x2D: 2, 0x2E: 2, 0x2F: 2, 0x30: 2, 0x31: 2, 0x32: 2, 0x33: 2, 0x34: 2, 0x35: 2, 0x36: 2, 0x37: 2, 0x38: 2, 0x39: 2, 0x3A: 2, 0x3B: 2, 0x3C: 2, 0x3D: 2, 0x44: 2, 0x45: 2, 0x46: 2, 0x47: 2, 0x48: 2, 0x49: 2, 0x4A: 2, 0x4B: 2, 0x4C: 2, 0x4D: 2, 0x4E: 2, 0x4F: 2, 0x50: 2, 0x51: 2, 0x52: 2, 0x53: 2, 0x54: 2, 0x55: 2, 0x56: 2, 0x57: 2, 0x58: 2, 0x59: 2, 0x5A: 2, 0x5B: 2, 0x5C: 2, 0x5D: 2, 0x5E: 2, 0x5F: 2, 0x60: 2, 0x61: 2, 0x62: 2, 0x63: 2, 0x64: 2, 0x65: 2, 0x66: 2, 0x67: 2, 0x68: 2, 0x69: 2, 0x6A: 2, 0x6B: 2, 0x6C: 2, 0x6D: 2, 0x6E: 3, 0x6F: 3, 0x70: 3, 0x71: 3, 0x72: 3, 0x74: 3, 0x75: 3, 0x76: 3, 0x77: 3, 0x78: 3, 0x7B: 1, 0x7C: 1, 0x7D: 1, 0x7E: 1, 0x7F: 1, 0x80: 1, 0x81: 1, 0x82: 1, 0x83: 1, 0x84: 1, 0x85: 1, 0x86: 1, 0x87: 1, 0x88: 1, 0x89: 1, 0x8A: 1, 0x8B: 1, 0x8C: 1, 0x8D: 1, 0x8E: 1, 0x8F: 1, 0x90: 2, 0x91: 2, 0x92: 2, 0x93: 2, 0x94: 2, 0x95: 2, 0x96: 2, 0x97: 2, 0x98: 2, 0x99: 2, 0x9A: 2, 0x9B: 2, 0x9C: 2, 0x9D: 2, 0x9E: 2, 0x9F: 2, 0xA0: 2, 0xA1: 2, 0xA2: 2, 0xA3: 2, 0xA4: 2, 0xA5: 2, 0xA6: 2, 0xA7: 2, 0xA8: 2, 0xA9: 2, 0xAA: 2, 0xAB: 2, 0xAC: 2, 0xAD: 2, 0xAE: 2, 0xAF: 2, 0xB0: 1, 0xB1: 1, 0xB2: 1, 0xB3: 1, 0xB4: 1, 0xB5: 1, 0xB6: 1, 0xB7: 1, 0xB8: 1, 0xB9: 1, 0xBA: 1, 0xBB: 1, 0xBC: 1, 0xBD: 1, 0xBE: 1, 0xBF: 1, 0xC0: 1, 0xC1: 1, 0xC2: 1, 0xC3: 1, 0xC4: 1, 0xC5: 1, 0xC6: 1, 0xC7: 1, 0xC8: 1, 0xC9: 1, 0xCA: 1, 0xCB: 1, 0xCC: 1, 0xCD: 1, 0xCE: 1, 0xCF: 1, 0xD0: 2, 0xD1: 2, 0xD2: 2, 0xD3: 2, 0xD4: 2, 0xD5: 2, 0xD6: 2, 0xD7: 2, 0xD8: 2, 0xD9: 2, 0xDA: 2, 0xDB: 2, 0xDC: 2, 0xDD: 2, 0xDE: 2, 0xDF: 2, 0xE0: 2, 0xE1: 2, 0xE2: 2, 0xFA: 4, 0xFB: 4, 0xFC: 3, 0xFD: 3, 0xFE: 2, 0xFF: 2, } # 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. Widths come from OP_UNITS, the format specification's table -- not from a mnemonic, and not from a run of neighbours. The failure that replaces was quiet in the worst way: a wrong width keeps producing plausible instructions, just shifted, so every offset derived after that point is wrong and nothing in the output says so. The traps worth naming: * Width is not a family property. `const-wide/16` (0x16) is 2 units, `const-wide/32` (0x17) is 3 and `const-wide` (0x18) is 5; `const-class` (0x1C) is 2 while `monitor-exit` (0x1E) is 1; `instance-of` (0x20) is 2 while `array-length` (0x21) is 1. Nine opcodes in 0x16-0x2C break the "same family, same width" reading. * `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, 21c) and `0x1B` (const-string/jumbo, 31c) are different widths; only the jumbo form carries a 32-bit string index. * The `goto` family sits at `0x28` (10t, **1** unit), `0x29` (20t, **2**) and `0x2A` (30t, **3**) -- one slot later than the obvious reading, which puts `throw`, `goto`, `goto/16` at 0x27/0x28/0x29. 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 ` 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 # Unallocated opcodes (0x3E-0x43, 0x73, 0x79-0x7A, 0xE3-0xF9) never appear in # a valid dex; anything unlisted is treated as one unit so a corrupt stream # still terminates rather than running off the end. return OP_UNITS.get(op, 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 "" % idx return self.string(idx) except Exception: return "" % 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) for item in self.methods_at(cd): yield item def methods_at(self, class_def_off): """Same as methods_of(), from a class_def_item offset you already have. methods_of() calls find_class(), which is a linear scan of class_defs, so walking every class in a dex through it costs O(n^2). A caller that is already iterating class_defs should come through here instead. """ p = u32(self.d, class_def_off + 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 == 0x28: # goto (10t, signed byte) off = raw[1] if off > 127: off -= 256 return pos + off * 2 if op == 0x29: # goto/16 (20t, signed int16) return pos + s16(u16(self.d, pos + 2)) * 2 if op == 0x2A: # goto/32 (30t, signed int32) 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 (0x28, 0x29, 0x2A): 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 == 0x1A: # const-string (21c): uint16 string index at byte offset 2. text += ' "%s"' % self.string_safe(u16(self.d, pos + 2)) elif op == 0x1B: # const-string/jumbo (31c): the index is a uint32, so reading it as a # uint16 would silently name a different string. text += ' "%s"' % self.string_safe(u32(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 ""), entry or "" 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, 0x0F, 0x10, 0x11): return raw[1] & 0xF # 12x: two 4-bit registers if op in (0x0A, 0x0B, 0x0C, 0x0D, 0x1C, 0x1D, 0x1E, 0x1F, 0x22): return raw[1] # 11x / 21c: one 8-bit register return 0 if __name__ == "__main__": sys.exit(main(sys.argv))