4. Smali 代码中的敏感 API 调用定位

Smali 代码中的敏感 API 调用定位 学习卡片

为什么要在 Smali 层做敏感 API 定位

反编译 APK 后,Java 源码往往因混淆、内联或反编译失败而不可读,但 Smali 是 Dalvik 字节码的稳定文本表示,方法调用指令 invoke-* 会完整保留目标方法签名。因此直接在 Smali 上做模式匹配,可以绕过源码可读性问题,稳定定位 Runtime.exec、Cipher、TelephonyManager 等敏感调用点,并回溯其所在类、方法与上下文。

匹配对象与 Smali 调用形态

Smali 中一次方法调用由 invoke-kind {参数寄存器}, L类;->方法(参数类型)返回类型 构成。敏感 API 定位的关键是构造正确的目标签名正则,而不是简单匹配类名,否则会漏掉重载方法或误报同名字段。

敏感类别目标类典型 Smali 调用片段风险含义
命令执行Ljava/lang/Runtime;invoke-virtual {v0, v1}, Ljava/lang/Runtime;->exec([Ljava/lang/String;)Ljava/lang/Process;可能执行外部命令
加密操作Ljavax/crypto/Cipher;invoke-virtual {v0, v1}, Ljavax/crypto/Cipher;->getInstance(Ljava/lang/String;)Ljavax/crypto/Cipher;硬编码密钥/弱算法线索
设备信息Landroid/telephony/TelephonyManager;invoke-virtual {v0}, Landroid/telephony/TelephonyManager;->getDeviceId()Ljava/lang/String;读取 IMEI 等标识
反射调用Ljava/lang/reflect/Method;invoke-virtual {v0, v1}, Ljava/lang/reflect/Method;->invoke(...)Ljava/lang/Object;隐藏调用链

定位流程与上下文回溯

扫描时以 .method 为方法边界维护当前方法上下文,逐行匹配 invoke- 指令;命中后记录类名、方法名、行号、调用指令与目标签名,并向前回溯最近的 const-string 作为参数线索。下图给出从 Smali 目录到调用上下文报告的完整数据流。

Smali 敏感 API 调用定位数据流 Smali 目录 smali/**.smali 按包路径组织 逐行扫描器 .method 边界跟踪 invoke- 指令匹配 敏感签名规则 Runtime.exec Cipher / TelephonyManager 签名匹配? 是 / 否 跳过 继续下一行 上下文回溯 记录类名 / 方法名 / 行号 回溯最近 const-string 参数 调用上下文报告 (JSON/CSV) 报告字段示例 class: Lcom/example/NetTask; method: run()V line: 87 api: Runtime->exec arg: "sh -c ..." risk: HIGH

可运行的定位脚本

下面脚本使用 Python 标准库,无需额外依赖。它遍历 Smali 目录,按方法边界跟踪上下文,用正则匹配敏感签名,并输出 JSON 报告。规则表可自行扩展。

import os, re, json, sys

# 敏感 API 规则:类名 + 方法名正则
RULES = [
    {"cat": "命令执行", "cls": r"Ljava/lang/Runtime;", "mtd": r"exec", "risk": "HIGH"},
    {"cat": "加密操作", "cls": r"Ljavax/crypto/Cipher;", "mtd": r"getInstance|doFinal|init", "risk": "MEDIUM"},
    {"cat": "设备信息", "cls": r"Landroid/telephony/TelephonyManager;", "mtd": r"getDeviceId|getSubscriberId|getSimSerialNumber", "risk": "HIGH"},
    {"cat": "反射调用", "cls": r"Ljava/lang/reflect/Method;", "mtd": r"invoke", "risk": "MEDIUM"},
]

INVOKE_RE = re.compile(r"invoke-\w+\s+\{[^}]*\},\s*(L[^;]+;)->([\w$<>]+)\(([^)]*)\)(\S+)")
METHOD_RE = re.compile(r"^\.method\s+(.*)$")
CONST_RE  = re.compile(r'const-string\s+[vp]\d+,\s*"(.*)"')

def scan(smali_root):
    findings = []
    for dirpath, _, files in os.walk(smali_root):
        for fn in files:
            if not fn.endswith(".smali"):
                continue
            path = os.path.join(dirpath, fn)
            cur_method = "<unknown>"
            last_const = None
            with open(path, "r", encoding="utf-8", errors="ignore") as f:
                for lineno, line in enumerate(f, 1):
                    line = line.strip()
                    m = METHOD_RE.match(line)
                    if m:
                        cur_method = m.group(1)
                        last_const = None
                        continue
                    c = CONST_RE.search(line)
                    if c:
                        last_const = c.group(1)
                    inv = INVOKE_RE.search(line)
                    if not inv:
                        continue
                    cls, mtd, args, ret = inv.groups()
                    for r in RULES:
                        if re.search(r["cls"], cls) and re.search(r["mtd"], mtd):
                            findings.append({
                                "file": os.path.relpath(path, smali_root),
                                "line": lineno,
                                "class": cls,
                                "method": cur_method,
                                "api": f"{cls}->{mtd}",
                                "args": args,
                                "ret": ret,
                                "category": r["cat"],
                                "risk": r["risk"],
                                "nearby_string": last_const,
                            })
                            break
    return findings

if __name__ == "__main__":
    root = sys.argv[1] if len(sys.argv) > 1 else "smali"
    result = scan(root)
    print(json.dumps(result, ensure_ascii=False, indent=2))

运行与结果判读

将反编译产物中的 smali 目录作为参数传入即可:

python locate_sensitive.py ./smali > report.json

报告每条记录包含文件、行号、类、方法、API 签名、参数类型、风险等级和最近的字符串常量。判读时重点关注:

  • Runtime->exec 且 nearby_string 含 sh -c、su、chmod 等,属于高危命令执行。
  • Cipher->getInstance 参数为 "AES/ECB/..." 或 "DES",提示弱加密模式。
  • TelephonyManager->getDeviceId 出现在非系统类中,提示设备标识采集。
  • 同一方法内多个敏感调用聚集,应作为重点人工复核对象。
若 Smali 被拆分为多个 dex(smali、smali_classes2…),需对每个目录分别运行脚本后合并报告,避免漏检。
正则匹配类名时不要省略开头的 L 和结尾的 ;,否则会匹配到字符串常量或注释中的同名文本,产生误报。