[md][md]# 🔐 某智能摄像头 APK 逆向分析实战
<h4 align="center">脱壳 · 去签名校验 — 全流程深度拆解</h4>
<p align="center">
<img src="https://img.shields.io/badge/目标-智能摄像头%20IoT-blue?style=flat-square&logo=android" alt="目标">
<img src="https://img.shields.io/badge/壳-爱加密%20v4.6.4.7-red?style=flat-square" alt="壳">
<img src="https://img.shields.io/badge/平台-Android%208.1%20ARM64-green?style=flat-square&logo=android" alt="平台">
<img src="https://img.shields.io/badge/工具-Frida%2017.15.3-orange?style=flat-square" alt="Frida">
<img src="https://img.shields.io/badge/状态-已完成-success?style=flat-square" alt="状态">
</p>
| 🎯 目标 |
📱 平台 |
🛠️ 工具链 |
com.xxx.smartcamera |
Android 8.1 (ARM64) |
Frida 17.15.3 |
| 某智能摄像头 vX.X.X |
真机 (ROOT) |
JDK 11 · Python 3.12 |
📑 目录
<p align="center">
| Step 1 |
Step 2 |
Step 3 |
| 🔍 壳识别与分析 |
🧬 DEX 内存脱壳 |
✍️ 去除签名校验 |
| → 跳转 |
→ 跳转 |
→ 跳转 |
</p>
🔍 Step 1 — 壳识别与分析
1.1 识别壳特征
拿到 APK 后,第一件事就是解剖它的内部结构:
$ unzip -l sample.apk | grep "classes.*\.dex\|libshell\|libshella\|IJm"
🎯 关键发现:一眼就能看出典型的加固痕迹——1 个正常的 classes.dex + 14 个体积异常小的 stub。
| 📄 文件 |
📏 大小 |
💡 说明 |
classes.dex |
195 KB |
壳的 Wrapper 代码 |
classes2.dex ~ classes15.dex |
各 2.4 KB |
⚠️ 虚假 stub DEX(全部相同) |
lib/arm64-v8a/libshell-super.com.xxx.smartcamera.so |
446 KB |
🔴 壳核心 Native 库 |
lib/arm64-v8a/libshella-4.6.4.7.so |
6 KB |
辅助壳模块 |
res/IJm.xml |
496 B |
壳配置文件 |
<div align="center">
┌─────────────────────────────────────────────────────┐
│ 🏷️ 结论:爱加密 (AiJiaMi) Shell-Super v4.6.4.7 │
└─────────────────────────────────────────────────────┘
</div>
14 个 classes2~15.dex 的 DEX 魔数完全相同且仅有 2432 字节,确认为占位空壳:
$ xxd classes2.dex | head -1
00000000: 6465 780a 3033 3700 dex.037 ← DEX 魔数正确但内容为空
1.2 分析壳加载流程
从 classes.dex(壳的 Wrapper)中提取关键类名:
$ strings classes.dex | grep "com/wrapper/proxy"
com/wrapper/proxyapplication/WrapperProxyApplication ← 壳 Application
com/wrapper/proxyapplication/MultiDex ← DEX 动态加载
com/wrapper/proxyapplication/AndroidNClassLoader ← 自定义 ClassLoader
com/xxx/smartcamera/StubWrapperProxyApplication ← Manifest 入口
📌 壳加载链路:从 Native 层到 Java 层,每一步都环环相扣。
<!-- 📸 截图建议:可以用 draw.io 或 Excalidraw 绘制此流程图导出为 03-loading-chain.png -->
flowchart TD
A["📦 System.loadLibrary('shell-super')"] --> B["🔧 libshell-super.so → .init_array"]
B --> B1["🛡️ ELF 完整性检查"]
B1 --> C["🔑 JNI_OnLoad"]
C --> C1["🔓 解密 DEX → prodexdir/"]
C1 --> D["🏗️ WrapperProxyApplication.attachBaseContext()"]
D --> E["📚 MultiDex.install()"]
E --> E1["🔄 自定义 ClassLoader 加载解密 DEX"]
E1 --> F["🎯 反射创建 RealApplication"]
style A fill:#1a1a2e,stroke:#e94560,color:#eee
style B fill:#16213e,stroke:#0f3460,color:#eee
style B1 fill:#16213e,stroke:#e94560,color:#eee
style C fill:#16213e,stroke:#0f3460,color:#eee
style C1 fill:#16213e,stroke:#e94560,color:#eee
style D fill:#1a1a2e,stroke:#533483,color:#eee
style E fill:#1a1a2e,stroke:#533483,color:#eee
style E1 fill:#16213e,stroke:#e94560,color:#eee
style F fill:#0f3460,stroke:#00ff88,color:#eee
1.3 防护系统分析
$ strings libshell-super...so | grep -E "SIGSEGV|hook|LD_PRELOAD|inotify|check"
壳内置了多层防护,防护密度远超普通加固:
| 🛡️ 检测项 |
⚙️ 实现方式 |
🔴 威胁等级 |
| ELF 完整性 |
catch SIGSEGV when check_elfheader |
🔴 高 |
| 注入检测 |
LD_PRELOAD 环境变量扫描 |
🟠 中 |
| 文件监控 |
inotify_add_watch (CLOSE_WRITE/DELETE_SELF/MOVE_SELF) |
🔴 高 |
| Hook 检测 |
libxhook 1.1.12 + hooking %s in %s 日志 |
🔴 高 |
| 反调试 |
catch SIGSEGV when init or hook |
🔴 高 |
| 字节码校验 |
DEX 中部分类字节码被故意破坏 |
🟡 中 |
⚠️ 注意:这套反作弊系统是后续脱壳和 Hook 操作的主要障碍,直接 Frida spawn 会触发 SIGSEGV 崩溃。
🧬 Step 2 — DEX 内存脱壳
2.1 脱壳策略
爱加密的 DEX 保护有两个关键特征:
<div align="center">
| 🔴 挑战 |
💡 策略 |
| 磁盘上的 DEX 字节码被破坏 |
不从磁盘读,从内存中 dump |
| 壳的 ClassLoader 在运行时修复字节码 |
在壳完成修复 之后 再 dump |
</div>
核心思路:让壳自己把 DEX 解密并修复好,然后我们直接从它的工作目录拿走成品。
2.2 部署 Frida Server
# 1️⃣ 下载 ARM64 frida-server
curl -L -o frida-server-arm64.xz \
"https://github.com/frida/frida/releases/download/17.15.3/frida-server-17.15.3-android-arm64.xz"
# 2️⃣ 解压并推送到设备
xz -d frida-server-arm64.xz
adb push frida-server-arm64 /data/local/tmp/fs
adb shell "su -c 'chmod 755 /data/local/tmp/fs'"
# 3️⃣ 以后台模式启动
adb shell "su -c '/data/local/tmp/fs -D &'"
✅ Frida Server 已就绪
2.3 🔑 关键发现:prodexdir 自动解密
启动 APP 后,壳会自动将解密后的 DEX 写入可读目录——这是整个脱壳过程最大的突破口:
$ adb shell "su -c 'ls -la /data/data/com.xxx.smartcamera/files/prodexdir/'"
total 175160
-r--r--r-- 1 u0_a106 14,139,272 00O000ll111l_0.dex
-r--r--r-- 1 u0_a106 11,098,960 00O000ll111l_1.dex
-r--r--r-- 1 u0_a106 11,855,208 00O000ll111l_2.dex
...
-r--r--r-- 1 u0_a106 8,822,496 00O000ll111l_14.dex
<div align="center">
┌──────────────────────────────────────────┐
│ 📦 15 个 DEX · 总计 120 MB · 全部有效 │
└──────────────────────────────────────────┘
</div>
一键拉取所有 DEX:
for i in $(seq 0 14); do
adb shell "su -c 'cp \
/data/user/0/com.xxx.smartcamera/files/prodexdir/firstLoad/00O000ll111l_${i}.dex \
/sdcard/fixed_${i}.dex'"
adb pull "/sdcard/fixed_${i}.dex" "fixed_dex/classes${i}.dex"
done
验证 DEX 有效性:
$ xxd classes0.dex | head -1
00000000: 6465 780a 3033 3700 ← "dex.037" — 标准 DEX 魔数 ✅
<div align="center">
| 文件 |
大小 |
状态 |
classes0.dex |
14,139,272 bytes |
✅ 有效 |
classes1.dex |
11,098,960 bytes |
✅ 有效 |
... |
... |
✅ 有效 |
classes14.dex |
8,822,496 bytes |
✅ 有效 |
</div>
💡 经验之谈:很多时候脱壳不需要写复杂的 Frida dump 脚本。先观察壳的行为——它自己就会把解密产物放在某个目录里,直接 cp 出来即可。
✍️ Step 3 — 去除签名校验
3.1 ❌ 错误尝试:直接 Patch ELF
最直觉的做法是直接修改 libshell-super.so 中的签名字符串——但这条路是死胡同:
java.lang.UnsatisfiedLinkError: dlopen failed:
".dynamic section header was not found"
<div align="center">
🔴 Patch ELF 字符串 → 长度不一致 → .dynamic 段偏移量被破坏 → dlopen 失败
</div>
教训:不要直接修改加固 SO 的字符串常量——ELF 结构对字节偏移极其敏感。
3.2 ✅ 正确方案:Python zipfile 流式重签
🕳️ 核心坑点
APK 中存在 238 对大小写碰撞的文件:
res/3UU.xml ≠ res/3uU.xml ← Windows 上被视为同一文件!
Windows JDK 的 jar 命令因文件系统大小写不敏感,在重新打包时会静默丢失这 238 个资源文件,导致 APP 启动即崩溃。
🔑 解法:绕过 jar 命令,直接用 Python zipfile 流式复制——它对文件名大小写完全敏感。
核心代码
import zipfile
SRC = 'sample.apk'
DST = 'sample_resign.apk'
with zipfile.ZipFile(SRC, 'r') as zf_in:
with zipfile.ZipFile(DST, 'w', zipfile.ZIP_DEFLATED) as zf_out:
for item in zf_in.infolist():
# ⏭️ 跳过旧签名文件
if item.filename.startswith('META-INF/') and (
item.filename.endswith('.RSA') or
item.filename.endswith('.SF')
):
continue
zf_out.writestr(item, zf_in.read(item.filename))
✅ 完整性校验
改完必须验证,否则到安装阶段才发现丢文件就晚了:
orig = set(zipfile.ZipFile(SRC, 'r').namelist())
final = set(zipfile.ZipFile(DST, 'r').namelist())
missing = orig - final - {'META-INF/ORIGKEY.SF', 'META-INF/ORIGKEY.RSA'}
print(f"Missing entries: {len(missing)}") # → 0 ✅
🔏 重新签名
jarsigner -sigalg SHA256withRSA -digestalg SHA-256 \
-keystore ~/.android/debug.keystore \
-storepass android -keypass android \
sample_resign.apk androiddebugkey
3.3 🎉 安装验证
$ adb install sample_resign.apk
Success ✅
<p>
<sub>这是MT直接签名打开的图</sub>
</p>
<div align="center">
┌──────────────────────────────────────────────────────────┐
│ │
│ 🎯 爱加密 v4.6.4.7 签名容忍度较高 │
│ 重签后直接到达登录页,无需额外绕过 │
│ │
│ ⚠️ 主要检测点在运行时注入(Frida spawn),非签名本身 │
│ │
└──────────────────────────────────────────────────────────┘
</div>
🏁 成果总览
📦 输出产物
| 📄 文件 |
💡 说明 |
📏 大小 |
fixed_dex/classes0~14.dex |
运行时修复后的完整 DEX |
120 MB |
sample_final.apk |
去签名校验版本 |
249 MB |
🛠️ 关键工具链
flowchart LR
subgraph 分析阶段
A1["📦 unzip"] --> A2["📝 strings"] --> A3["🔢 xxd"] --> A4["🐍 Python"]
end
subgraph 脱壳阶段
B1["📱 ADB Root"] --> B2["📂 prodexdir 直接拉取"]
end
subgraph 签名阶段
C1["🐍 zipfile 流式复制"] --> C2["🔏 jarsigner"]
end
分析阶段 --> 脱壳阶段 --> 签名阶段
style A1 fill:#1a1a2e,stroke:#e94560,color:#eee
style A2 fill:#1a1a2e,stroke:#e94560,color:#eee
style A3 fill:#1a1a2e,stroke:#e94560,color:#eee
style A4 fill:#1a1a2e,stroke:#e94560,color:#eee
style B1 fill:#16213e,stroke:#533483,color:#eee
style B2 fill:#16213e,stroke:#533483,color:#eee
style C1 fill:#0f3460,stroke:#00ff88,color:#eee
style C2 fill:#0f3460,stroke:#00ff88,color:#eee
⚠️ 踩坑记录
| 🐛 问题 |
🔍 原因 |
✅ 解决 |
| APK 重打包后崩溃 |
Windows 大小写不敏感丢失 238 个资源文件 |
Python zipfile 流式复制 |
| 直接 patch ELF 导致 dlopen 失败 |
字符串长度不一致破坏 .dynamic 段 |
放弃 patch,改用纯重签 |
| Frida spawn 触发 SIGSEGV |
壳检测 frida-agent 注入 |
先启动 APP 再 attach |
| 模拟器缺 drawable 资源 |
x86_64 架构资源不完整 |
换 ARM64 真机 |
🔑 关键经验
<div align="center">
| # |
💎 经验 |
| 1 |
壳的签名校验未必严格 — 爱加密 v4.6.4.7 对签名变更容忍度较高,重签即可通过 |
| 2 |
不要 patch ELF — 字符串长度变化会破坏 ELF 结构,宁可用运行时 Hook 或放弃 patch |
</div>
<br>
<div align="center">
╔══════════════════════════════════════════════════════════╗
║ ⚠️ 本文仅供安全研究和学习交流使用,请勿用于非法用途 ║
╚══════════════════════════════════════════════════════════╝
<p>
<sub>本文全程主要由claude code和deepseek-v4完成,欢迎讨论。</sub>
</p>
</div>