709 lines
32 KiB
Python
709 lines
32 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/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 <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
|
|
# 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 "<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)
|
|
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 "<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, 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))
|