Files
reverse-skills-max/vendor-new/apk-reverse/docs/tool-verification/fixtures/make_fixture_dex.py

263 lines
8.9 KiB
Python
Executable File

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Build a minimal, structurally valid dex whose reference counts are known.
WHY THIS EXISTS
---------------
Verifying a dex reader against a real APK only shows that two readers agree.
Verifying it against a program whose call graph was written down in advance shows
that the *number* is right. This fixture encodes exactly two method bodies:
Lcom/example/Caller;->go()V invoke-static Lcom/example/Target;->run()V x2
Lcom/example/Target;->run()V invoke-virtual Ljava/lang/String;->length()I x3
so the answers a blast-radius check must return are 3, 2 and 2. The same call
graph is also written out as a smali tree, because the point of the fix this
fixture verifies is that a `.dex` and a smali tree answer a needle identically.
USAGE
-----
python make_fixture_dex.py out.dex [--smali DIR]
# then, from the skill's scripts directory:
python find_refs.py out.dex 'Ljava/lang/String;->length' # expect 3
python find_refs.py out.dex 'Lcom/example/Target;->run' # expect 2
python find_refs.py out.dex 'Lcom/example/Target;' # expect 2
python find_refs.py DIR 'Ljava/lang/String;->length' # expect 3
Dependency-free (stdlib only) on purpose: the fixture must be reproducible on a
machine with no Android toolchain, which is where the defect it verifies was found.
Nothing here is target data. The dex is synthesised from the constants below.
"""
import argparse
import hashlib
import os
import struct
import zlib
CALLER = 'Lcom/example/Caller;'
TARGET = 'Lcom/example/Target;'
OBJECT = 'Ljava/lang/Object;'
STRING = 'Ljava/lang/String;'
SMALI_CALLER = """.class public Lcom/example/Caller;
.super Ljava/lang/Object;
.method public static go()V
.registers 0
invoke-static {}, Lcom/example/Target;->run()V
invoke-static {}, Lcom/example/Target;->run()V
return-void
.end method
"""
SMALI_TARGET = """.class public Lcom/example/Target;
.super Ljava/lang/Object;
.method public static run()V
.registers 1
invoke-virtual {v0}, Ljava/lang/String;->length()I
invoke-virtual {v0}, Ljava/lang/String;->length()I
invoke-virtual {v0}, Ljava/lang/String;->length()I
return-void
.end method
"""
EXPECTED = (('Ljava/lang/String;->length', 3),
('Lcom/example/Target;->run', 2),
('Lcom/example/Target;', 2))
def uleb(n):
out = bytearray()
while True:
b = n & 0x7F
n >>= 7
out.append(b | (0x80 if n else 0))
if not n:
return bytes(out)
def p16(v):
return struct.pack('<H', v)
def p32(v):
return struct.pack('<I', v)
def string_data(s):
raw = s.encode('utf-8')
return uleb(len(raw)) + raw + b'\x00'
def code_item(registers, outs, insns):
body = b''.join(p16(u) for u in insns)
return (p16(registers) + p16(0) + p16(outs) + p16(0)
+ p32(0) + p32(len(insns)) + body)
def invoke(op, method_idx, count=0, c=0, g=0):
"""35c layout: op(G<<8)(count<<12), index, then the register nibbles."""
return [op | (g << 8) | (count << 12), method_idx, c & 0xF]
def class_data(entries):
"""entries: [(method_idx, access_flags, code_off)], ascending by method_idx."""
out = uleb(0) + uleb(0) + uleb(len(entries)) + uleb(0)
prev = 0
for mi, acc, co in entries:
out += uleb(mi - prev) + uleb(acc) + uleb(co)
prev = mi
return out
def build():
strings = sorted({CALLER, TARGET, OBJECT, STRING, 'I', 'V', 'go', 'run', 'length'})
sidx = {s: i for i, s in enumerate(strings)}
types = sorted({CALLER, TARGET, OBJECT, STRING, 'I', 'V'}, key=lambda t: sidx[t])
tidx = {t: i for i, t in enumerate(types)}
protos = [('I', 'I'), ('V', 'V')] # (shorty, return type)
protos.sort(key=lambda p: tidx[p[1]])
pidx = {shorty: i for i, (shorty, _r) in enumerate(protos)}
methods = [(CALLER, 'go', 'V'), (TARGET, 'run', 'V'), (STRING, 'length', 'I')]
methods.sort(key=lambda m: (tidx[m[0]], sidx[m[1]], pidx[m[2]]))
midx = {(c, n): i for i, (c, n, _r) in enumerate(methods)}
insns_go = invoke(0x71, midx[(TARGET, 'run')]) * 2 + [0x000E]
insns_run = invoke(0x6E, midx[(STRING, 'length')], count=1) * 3 + [0x000E]
off = 0x70
string_ids_off = off
off += 4 * len(strings)
type_ids_off = off
off += 4 * len(types)
proto_ids_off = off
off += 12 * len(protos)
method_ids_off = off
off += 8 * len(methods)
class_defs_off = off
off += 32 * 2
data_off = off
data = bytearray()
def align(n):
while len(data) % n:
data.append(0)
str_offs = []
for s in strings:
str_offs.append(data_off + len(data))
data += string_data(s)
align(4)
code_go_off = data_off + len(data)
data += code_item(0, 0, insns_go)
align(4)
code_run_off = data_off + len(data)
data += code_item(1, 1, insns_run)
align(4)
cd_go_off = data_off + len(data)
data += class_data([(midx[(CALLER, 'go')], 0x0009, code_go_off)])
cd_run_off = data_off + len(data)
data += class_data([(midx[(TARGET, 'run')], 0x0009, code_run_off)])
align(4)
map_off = data_off + len(data)
def map_item(kind, size, offset):
return p16(kind) + p16(0) + p32(size) + p32(offset)
items = [
(0x0000, 1, 0), # header_item
(0x0001, len(strings), string_ids_off),
(0x0002, len(types), type_ids_off),
(0x0003, len(protos), proto_ids_off),
(0x0005, len(methods), method_ids_off),
(0x0006, 2, class_defs_off),
(0x2002, len(strings), str_offs[0]), # string_data_item
(0x2001, 2, code_go_off), # code_item
(0x2000, 2, cd_go_off), # class_data_item
(0x1000, 1, map_off), # map_list
]
items.sort(key=lambda it: it[2])
data += p32(len(items)) + b''.join(map_item(*it) for it in items)
string_ids = b''.join(p32(str_offs[i]) for i in range(len(strings)))
type_ids = b''.join(p32(sidx[t]) for t in types)
proto_ids = b''.join(p32(sidx[shorty]) + p32(tidx[ret]) + p32(0)
for shorty, ret in protos)
method_ids = b''.join(p16(tidx[c]) + p16(pidx[r]) + p32(sidx[n])
for c, n, r in methods)
def class_def(class_idx, super_idx, class_data_off):
return (p32(class_idx) + p32(0x0001) + p32(super_idx) + p32(0)
+ p32(0xFFFFFFFF) + p32(0) + p32(class_data_off) + p32(0))
classes = sorted([(CALLER, cd_go_off), (TARGET, cd_run_off)], key=lambda c: tidx[c[0]])
class_defs = b''.join(class_def(tidx[c], tidx[OBJECT], cdo) for c, cdo in classes)
h = bytearray(0x70)
h[0:8] = b'dex\n035\x00'
h[32:36] = p32(data_off + len(data)) # file_size
h[36:40] = p32(0x70) # header_size
h[40:44] = p32(0x12345678) # endian_tag
h[52:56] = p32(map_off)
h[56:60] = p32(len(strings))
h[60:64] = p32(string_ids_off)
h[64:68] = p32(len(types))
h[68:72] = p32(type_ids_off)
h[72:76] = p32(len(protos))
h[76:80] = p32(proto_ids_off)
h[80:84] = p32(0) # field_ids_size
# There are no fields. Size 0 is correct; the offset points at the next
# section rather than 0 only because a zero offset reads as "out of range"
# to a structural check that treats every offset as present.
h[84:88] = p32(method_ids_off)
h[88:92] = p32(len(methods))
h[92:96] = p32(method_ids_off)
h[96:100] = p32(2) # class_defs_size
h[100:104] = p32(class_defs_off)
h[104:108] = p32(len(data))
h[108:112] = p32(data_off)
blob = bytearray(bytes(h) + string_ids + type_ids + proto_ids + method_ids
+ class_defs + bytes(data))
blob[12:32] = hashlib.sha1(bytes(blob[32:])).digest() # signature first
blob[8:12] = (zlib.adler32(bytes(blob[12:])) & 0xFFFFFFFF).to_bytes(4, 'little')
return bytes(blob), len(strings), len(types), len(methods)
def main():
ap = argparse.ArgumentParser(description=__doc__.split('\n')[0])
ap.add_argument('out', nargs='?', default='fixture.dex')
ap.add_argument('--smali', metavar='DIR',
help='also write the same call graph as a smali tree')
a = ap.parse_args()
blob, n_str, n_ty, n_m = build()
with open(a.out, 'wb') as fh:
fh.write(blob)
print('wrote %s (%d bytes, %d strings, %d types, %d methods, 2 classes)'
% (a.out, len(blob), n_str, n_ty, n_m))
if a.smali:
d = os.path.join(a.smali, 'com', 'example')
os.makedirs(d, exist_ok=True)
with open(os.path.join(d, 'Caller.smali'), 'w', newline='\n') as fh:
fh.write(SMALI_CALLER)
with open(os.path.join(d, 'Target.smali'), 'w', newline='\n') as fh:
fh.write(SMALI_TARGET)
print('wrote smali tree under %s' % a.smali)
print('expected: ' + ' '.join('%s=%d' % kv for kv in EXPECTED))
if __name__ == '__main__':
main()