4. 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 目录到调用上下文报告的完整数据流。
可运行的定位脚本
下面脚本使用 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 和结尾的 ;,否则会匹配到字符串常量或注释中的同名文本,产生误报。