191 lines
8.5 KiB
Python
191 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Small Dalvik disassembler for a selected locally defined method.
|
|
|
|
It builds on dex_probe.py and is intentionally limited to textual forensic
|
|
inspection. It does not execute or modify the APK.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import re
|
|
import struct
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
from dex_probe import Dex, iter_instructions, u16, u32
|
|
|
|
|
|
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",
|
|
}
|
|
|
|
for i, name in enumerate(("aget", "aget-wide", "aget-object", "aget-boolean", "aget-byte", "aget-char", "aget-short",
|
|
"aput", "aput-wide", "aput-object", "aput-boolean", "aput-byte", "aput-char", "aput-short"), 0x44):
|
|
NAMES[i] = name
|
|
for i, name in enumerate(("iget", "iget-wide", "iget-object", "iget-boolean", "iget-byte", "iget-char", "iget-short",
|
|
"iput", "iput-wide", "iput-object", "iput-boolean", "iput-byte", "iput-char", "iput-short"), 0x52):
|
|
NAMES[i] = name
|
|
for i, name in enumerate(("sget", "sget-wide", "sget-object", "sget-boolean", "sget-byte", "sget-char", "sget-short",
|
|
"sput", "sput-wide", "sput-object", "sput-boolean", "sput-byte", "sput-char", "sput-short"), 0x60):
|
|
NAMES[i] = name
|
|
for i, name in enumerate(("invoke-virtual", "invoke-super", "invoke-direct", "invoke-static", "invoke-interface"), 0x6E):
|
|
NAMES[i] = name
|
|
for i, name in enumerate(("invoke-virtual/range", "invoke-super/range", "invoke-direct/range", "invoke-static/range", "invoke-interface/range"), 0x74):
|
|
NAMES[i] = name
|
|
|
|
BIN = [
|
|
"add-int", "sub-int", "mul-int", "div-int", "rem-int", "and-int", "or-int", "xor-int", "shl-int", "shr-int", "ushr-int",
|
|
"add-long", "sub-long", "mul-long", "div-long", "rem-long", "and-long", "or-long", "xor-long", "shl-long", "shr-long", "ushr-long",
|
|
"add-float", "sub-float", "mul-float", "div-float", "rem-float", "add-double", "sub-double", "mul-double", "div-double", "rem-double",
|
|
]
|
|
for i, name in enumerate(BIN, 0x90):
|
|
NAMES[i] = name
|
|
|
|
ADDR = [
|
|
"add-int/2addr", "sub-int/2addr", "mul-int/2addr", "div-int/2addr", "rem-int/2addr", "and-int/2addr", "or-int/2addr", "xor-int/2addr", "shl-int/2addr", "shr-int/2addr", "ushr-int/2addr",
|
|
"add-long/2addr", "sub-long/2addr", "mul-long/2addr", "div-long/2addr", "rem-long/2addr", "and-long/2addr", "or-long/2addr", "xor-long/2addr", "shl-long/2addr", "shr-long/2addr", "ushr-long/2addr",
|
|
"add-float/2addr", "sub-float/2addr", "mul-float/2addr", "div-float/2addr", "rem-float/2addr", "add-double/2addr", "sub-double/2addr", "mul-double/2addr", "div-double/2addr", "rem-double/2addr",
|
|
]
|
|
for i, name in enumerate(ADDR, 0xB0):
|
|
NAMES[i] = name
|
|
|
|
for i, name in enumerate(("add-int/lit16", "rsub-int", "mul-int/lit16", "div-int/lit16", "rem-int/lit16", "and-int/lit16", "or-int/lit16", "xor-int/lit16"), 0xD0):
|
|
NAMES[i] = name
|
|
for i, name in enumerate(("add-int/lit8", "rsub-int/lit8", "mul-int/lit8", "div-int/lit8", "rem-int/lit8", "and-int/lit8", "or-int/lit8", "xor-int/lit8", "shl-int/lit8", "shr-int/lit8", "ushr-int/lit8"), 0xD8):
|
|
NAMES[i] = name
|
|
|
|
|
|
def s8(v: int) -> int:
|
|
return struct.unpack("b", bytes([v & 0xFF]))[0]
|
|
|
|
|
|
def s16(v: int) -> int:
|
|
return struct.unpack("<h", struct.pack("<H", v & 0xFFFF))[0]
|
|
|
|
|
|
def s32(v: int) -> int:
|
|
return struct.unpack("<i", struct.pack("<I", v & 0xFFFFFFFF))[0]
|
|
|
|
|
|
def ab(unit: int) -> tuple[int, int]:
|
|
return (unit >> 8) & 0xF, (unit >> 12) & 0xF
|
|
|
|
|
|
def fmt(d: Dex, pc: int, op: int, u: list[int]) -> str:
|
|
name = NAMES.get(op, f"op_{op:02x}")
|
|
a4, b4 = ab(u[0])
|
|
aa = u[0] >> 8
|
|
|
|
if op in (0x01, 0x04, 0x07) or 0xB0 <= op <= 0xCF:
|
|
return f"{name} v{a4}, v{b4}"
|
|
if op in (0x02, 0x05, 0x08):
|
|
return f"{name} v{aa}, v{u[1]}"
|
|
if op in (0x03, 0x06, 0x09):
|
|
return f"{name} v{u[1]}, v{u[2]}"
|
|
if op in (0x0A, 0x0B, 0x0C, 0x0D, 0x0F, 0x10, 0x11, 0x1D, 0x1E, 0x27):
|
|
return f"{name} v{aa}"
|
|
if op in (0x0E,):
|
|
return name
|
|
if op == 0x12:
|
|
return f"{name} v{a4}, #{s8(b4 if b4 < 8 else b4 - 16)}"
|
|
if op in (0x13, 0x16):
|
|
return f"{name} v{aa}, #{s16(u[1])}"
|
|
if op in (0x14, 0x17):
|
|
return f"{name} v{aa}, #{s32(u[1] | u[2] << 16)}"
|
|
if op in (0x15, 0x19):
|
|
return f"{name} v{aa}, #0x{u[1]:04x}0000"
|
|
if op == 0x18:
|
|
val = u[1] | u[2] << 16 | u[3] << 32 | u[4] << 48
|
|
return f"{name} v{aa}, #0x{val:016x}"
|
|
if op == 0x1A:
|
|
return f"{name} v{aa}, {d.strings[u[1]]!r}"
|
|
if op == 0x1B:
|
|
idx = u[1] | u[2] << 16
|
|
return f"{name} v{aa}, {d.strings[idx]!r}"
|
|
if op in (0x1C, 0x1F, 0x22):
|
|
return f"{name} v{aa}, {d.types[u[1]]}"
|
|
if op == 0x20:
|
|
return f"{name} v{a4}, v{b4}, {d.types[u[1]]}"
|
|
if op == 0x21:
|
|
return f"{name} v{a4}, v{b4}"
|
|
if op == 0x23:
|
|
return f"{name} v{a4}, v{b4}, {d.types[u[1]]}"
|
|
if op in (0x24, 0x6E, 0x6F, 0x70, 0x71, 0x72):
|
|
count = b4
|
|
g = a4
|
|
regs = [u[2] & 0xF, (u[2] >> 4) & 0xF, (u[2] >> 8) & 0xF, (u[2] >> 12) & 0xF, g][:count]
|
|
target = d.types[u[1]] if op == 0x24 else d.methods[u[1]]
|
|
return f"{name} {{{', '.join('v'+str(x) for x in regs)}}}, {target}"
|
|
if op in (0x25, 0x74, 0x75, 0x76, 0x77, 0x78):
|
|
count = aa
|
|
regs = list(range(u[2], u[2] + count))
|
|
target = d.types[u[1]] if op == 0x25 else d.methods[u[1]]
|
|
return f"{name} {{{', '.join('v'+str(x) for x in regs)}}}, {target}"
|
|
if op in (0x26, 0x2B, 0x2C):
|
|
off = s32(u[1] | u[2] << 16)
|
|
return f"{name} v{aa}, :{pc + off:04x}"
|
|
if op == 0x28:
|
|
return f"{name} :{pc + s8(aa):04x}"
|
|
if op == 0x29:
|
|
return f"{name} :{pc + s16(u[1]):04x}"
|
|
if op == 0x2A:
|
|
return f"{name} :{pc + s32(u[1] | u[2] << 16):04x}"
|
|
if 0x2D <= op <= 0x31 or 0x44 <= op <= 0x51 or 0x90 <= op <= 0xAF:
|
|
return f"{name} v{aa}, v{u[1] & 0xFF}, v{u[1] >> 8}"
|
|
if 0x32 <= op <= 0x37:
|
|
return f"{name} v{a4}, v{b4}, :{pc + s16(u[1]):04x}"
|
|
if 0x38 <= op <= 0x3D:
|
|
return f"{name} v{aa}, :{pc + s16(u[1]):04x}"
|
|
if 0x52 <= op <= 0x5F:
|
|
return f"{name} v{a4}, v{b4}, {d.fields[u[1]]}"
|
|
if 0x60 <= op <= 0x6D:
|
|
return f"{name} v{aa}, {d.fields[u[1]]}"
|
|
if 0xD0 <= op <= 0xD7:
|
|
return f"{name} v{a4}, v{b4}, #{s16(u[1])}"
|
|
if 0xD8 <= op <= 0xE2:
|
|
return f"{name} v{aa}, v{u[1] & 0xFF}, #{s8(u[1] >> 8)}"
|
|
return name + " " + " ".join(f"0x{x:04x}" for x in u)
|
|
|
|
|
|
def main() -> None:
|
|
ap = argparse.ArgumentParser()
|
|
ap.add_argument("apk", type=Path)
|
|
ap.add_argument("--method", required=True, help="regex matched against full DEX method name")
|
|
args = ap.parse_args()
|
|
rx = re.compile(args.method, re.I)
|
|
with zipfile.ZipFile(args.apk) as zf:
|
|
for name in sorted(n for n in zf.namelist() if re.fullmatch(r"classes\d*\.dex", n)):
|
|
d = Dex(zf.read(name), f"{args.apk}!/{name}")
|
|
for em in d.encoded_methods:
|
|
method = d.methods[em.method_idx]
|
|
if not rx.search(method):
|
|
continue
|
|
print(f"\n### {d.source}\n{method}")
|
|
print(f"registers={u16(d.data, em.code_off)} ins={u16(d.data, em.code_off+2)} outs={u16(d.data, em.code_off+4)}")
|
|
for pc, op, units in iter_instructions(d.data, em.code_off):
|
|
print(f"{pc:04x}: {fmt(d, pc, op, units)}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|