Skip to content

Guide

Task-oriented recipes for the DEX engine. They assume apk = Apk.open("app.apk") and dex = apk.dex[0], and that you are working on an app you are authorized to test.

Round-trip a method

Disassemble then assemble to confirm the toolchain reproduces a method exactly.

import struct
from reapk.dex import disassemble, assemble, Assembler

m = dex.find_method("Lp/C;", "m")
ci, lines = disassemble(dex, m["code_off"])
original = [struct.unpack_from("<H", dex.d, ci["insns_off"] + 2 * i)[0]
           for i in range(ci["insns_size"])]
words = assemble(Assembler(dex), lines)
assert words == original

Assembler(dex) resolves operand references against the dex's pools.

Force a method to return a fixed value

build_dex(dex, replacements={code_off: new_code_item}) re-emits a whole, valid DEX with method bodies swapped. Build the replacement body with build_return_insns and build_code_item.

from reapk.dex import build_return_insns, build_code_item, build_dex, DexFile

m = dex.find_method("Lp/C;", "isRooted")        # ()Z
ci = dex.code_insns(m["code_off"])
words = build_return_insns("Z", "false", ci["regs"])
new_code = build_code_item(ci["regs"], ci["ins"], 0, words)

out = build_dex(dex, replacements={m["code_off"]: new_code})
DexFile(out)                                     # re-parses, so it is valid

build_return_insns(ret_desc, value, regs) accepts value in {"true", "false", "null", "void"}.

Bypass SSL pinning, then repackage and sign

Pinning and hostname verification done in code block an intercepting proxy. Neutralize the usual sinks across every dex, then rebuild and re-sign the APK.

import re
import zipfile
from reapk import Signer
from reapk.dex import build_return_insns, build_code_item, build_dex
from reapk.zipalign import read_zip_entries, stored_entry, write_aligned_zip

def find_pin_sinks(dex):
    repl = {}
    for c in dex.classes():
        cname = c["name"]
        for m in dex.class_methods(c["cdata"]):
            if not m["code_off"]:
                continue
            nm, proto = m["name"], m["proto"]
            ci = dex.code_insns(m["code_off"])
            words = None
            if nm == "check" and "CertificatePinner" in cname and proto.endswith(")V"):
                words = build_return_insns("V", "void", ci["regs"])
            elif nm in ("checkServerTrusted", "checkClientTrusted") and proto.endswith(")V"):
                words = build_return_insns("V", "void", ci["regs"])
            elif (nm == "verify" and proto.endswith(")Z") and ci["regs"] >= 1
                  and ("HostnameVerifier" in cname or "Ljavax/net/ssl/SSLSession;" in proto)):
                words = build_return_insns("Z", "true", ci["regs"])
            if words is not None:
                repl[m["code_off"]] = build_code_item(ci["regs"], ci["ins"], 0, words)
    return repl

names = sorted(n for n in zipfile.ZipFile("app.apk").namelist()
               if re.fullmatch(r"classes\d*\.dex", n))
patched = {}
for name, d in zip(names, apk.dex):
    repl = find_pin_sinks(d)
    if repl:
        patched[name] = build_dex(d, replacements=repl)

entries = read_zip_entries(open("app.apk", "rb").read())
rebuilt = [stored_entry(e["name"], patched[e["name"].decode()])
           if e["name"].decode() in patched else e
           for e in entries]
out = Signer().sign(write_aligned_zip(rebuilt))
open("patched.apk", "wb").write(out)

The verify rule is kept narrow (hostname verifiers, or methods that take an SSLSession) so unrelated signature checks are left alone.

Intern a new string

Adding a pool entry shifts indices, so build_dex remaps every reference.

from reapk.dex import Interner, build_dex, DexFile

it = Interner(dex)
it.add_string("patched-by-reapk")
it.finalize()
dex2 = DexFile(build_dex(dex, interner=it))
assert "patched-by-reapk" in [dex2.string(i) for i in range(dex2.n_str)]

Interner also has add_type, add_field, and add_method, each of which interns its dependencies. After finalize, the remaps are on sr, tr, fr, mr, and pr.

Make an app debuggable

Manifest edits go through the Manifest view and save repackages and signs.

apk.manifest.set_debuggable().set_cleartext()
apk.manifest.add_permission("android.permission.INTERNET")
apk.save("debuggable.apk")
from reapk.playground import dump_dex
print(dump_dex(apk.dex[0], max_classes=3, max_methods_per_class=5))

The caps keep the output readable on a real app, where a single dex can hold thousands of classes.