前言:整个 APP 比较幽默的是直接把开发者包拿来当正式包发布了,一定程度上降低了这次逆向的难度。本次用的 codebuddy 国际版的免费 deepseek v4.1 flash 进行的逆向工作,整体来说 deepseek 的输出速度还是很 nice 的。大模型语言毕竟还是擅长处理语言,所以当你没法提供一个 ARM 的 root 环境时最好还是先让大模型先做充分的静态逆向工作,为避免不必要的风险,下文已将 APP 相关信息以打码代替。
1. 结论摘要
- 本 APK 受网易易盾加固保护,入口 Application 被替换为
com.netease.nis.wrapper.MyApplication,加固壳版本7.6.1_817。 - APK 内不存在任何明文应用代码。
classes.dex头部合法(dex\n035)且壳自身的 31 个类是真实可读的(com.netease.nis.wrapper.*),但约 13.76 MB 的真实业务代码以全熵密文形式躺在同一文件的"填充区"里,只能由libnesec.so在运行时解密。 - 尽管如此,加固壳本身(
classes.dex+ 31 个 smali)被完整逆向,包括其字符串加密算法,因此壳的行为、加载链、反调试策略完全透明。 - 加固不影响以下内容的静态读取,均已在本文档中取出:
AndroidManifest.xml、resources.arsc、res/xml/network_security_config.xml、assets/第三方 SDK 凭据。 - 最严重的配置缺陷:网络安全配置全局信任用户 CA、允许明文 HTTP 且
overridePins=true。任何能安装用户证书的人都可以中间人解密该 App 的全部 HTTPS 流量,且该配置会覆盖证书固定。这正是逆向配合抓包场景成立的原因。 - 已提取到硬编码第三方凭据(微信 / QQ / 新浪微博),部分与 manifest 中的真实 AppID 交叉印证,属于生产环境有效凭据。
libnesec.so的内部结构已完全还原:伪造节表被识破、JNI_OnLoad导出名由 ELF hash 桶反推确证、四套字符串密码全部破解、加载链逐条对应到指令;并发现专门对抗 unidbg 模拟脱壳的检测器。
2. 加固 / 保护机制
2.1 壳的加载链
从解密后的壳字符串可完整还原:
对应 manifest 关键项:android:appComponentFactory="androidx.core.app.CoreComponentFactory"(被壳用作代理点)、android:name="com.netease.nis.wrapper.MyApplication"。
2.2 密文载荷定位
| 区域 | 偏移 | 大小 | 熵 |
|---|---|---|---|
| 伪 DEX 头部(字符串/类型/方法表) | 0 .. ~69,804 | ~68 KB | 5.47 |
| 加密载荷(全熵密文) | 69,804 .. 13,831,792 | ≈13.76 MB | 7.90 – 8.00 |
| ZIP 中央目录 | 77,967,360 .. 78,261,461 | 294 KB | — |
| EOCD(文件尾) | 78,261,461 .. 78,261,483 | 22 B | — |
- 全文件无隐藏尾部数据(EOCD 之后仅 22 字节,即 EOCD 本身)。
classes.dex头部声明file_size = 13,831,792,而data_off=15,676 / data_size=54,128, 即有效 DEX 数据只占 0.5%,其余为密文填充。- 密文中不存在明文
dex\n035、PK\x03\x04、zlib 流等特征,载荷为流密码/块密码加密,非简单压缩或单字节 XOR。
2.3 反分析 / 反篡改能力
壳内 NEDialog 携带的完整检测提示语,说明易盾已启用下列检测:
| 类别 | 检测文案(原文) |
|---|---|
| Root | Detected that the app is running in a rooted environment. |
| 模拟器 | Detected that the app is running on an emulator. |
| Xposed | Detected that the app is running in an Xposed environment. |
| Hook | Detected that the app is running in a hooking environment. |
| 调试器 | Detected the presence of a debugger. |
| 二次打包 | Detected that the application has been tampered. |
| 注入 | Detected that the app has been injected. |
| 多开 / 分身 | Detected that the app is running in a dual app environment. |
| VPN | Detected that a VPN is being used. |
| 代理 | Detected that a proxy is being used. |
| USB 调试 | Detected that the USB debugging mode is enabled. |
| 模拟点击 | Detected that the app is running in an environment with simulated clicks. |
| 异常 ROM | Detected an abnormal ROM. |
| 试用版 | Detected that the app is a trial version, please do not release it directly. |
| 渠道 | Please download the official app from the official channel. |
其他已解密的关键串:libnesec.so、libneguard.so、libdexfix.so、
assets/.nesec_patch、/templib/libs.zip、extract_switch_0、provider_switch_1、
shell_limit_0、debug_switch_0、classTable、findLoadedClass、ReLinker、
CrashHandler(Bugly 上报)、WebViewGoogle。
注:
Ls/h/e/l/l/S;类中残留cn.cntvnews/cn.cntvnews.base.App(央视新闻包名),笑嘻了。 与images/icon_max_data_encrypted_xxxxx.png等串,是易盾壳的复用/伪装残留,与本 App 无关。
2.4 DEX 层的反解析陷阱
壳 DEX 的 31 个类是可读的(apktool 可正常反汇编),但其中埋了针对解析器的陷阱:
- 第 31 个 class_def(
Ls/h/e/l/l/S;)被故意破坏:其class_data_off = 775,302,711,远超文件大小 13,831,792。凡是「无条件遍历全部class_defs并求值 class_data」的解析器都会在此越界出错或读到垃圾。 - 前 30 个类的
class_data_item合法;但La/auu/a;的source_file_idx填1025,恰为合法的字符串索引("entry"之类),属语义混用而非损坏。 - 另有一处真实损坏:
proto_ids表中存在短描述符与返回类型不匹配的条目(如I型 proto 的返回类型被填成Ljava/lang/String;),会误导按 shorty 建签名的工具。
实测工具行为:
| 工具 | 结果 |
|---|---|
apktool 2.11.1 | 成功,输出 31 个 smali |
jadx 1.5.6 | 卡死:因它试图将 13.76 MB 密文区当指令解析 |
| 结论:对易盾加固 APK 做 DEX 层分析,优先用 apktool(smali 粒度),不要用 jadx。 |
3. 组件与攻击面
| 类型 | 数量 |
|---|---|
| Activity | 378 |
| Service | 25 |
| BroadcastReceiver | 16 |
| ContentProvider | 13 |
| 申请权限 | 62(3 项重复) |
| 自定义权限 | 8(均 protectionLevel=0x02 = signature) |
3.1 导出组件
| 组件 | 备注 |
|---|---|
…qualitync.SplashActivity | LAUNCHER |
…commonui.SchemeActivity | xxxx:// 深链入口 |
…commonui.ActivityNotificationScheme | 通知跳转深链 |
com.tencent.tauth.AuthActivity | QQ 登录(scheme=tencentxxxxxx) |
com.tencent.connect.common.AssistActivity | QQ 分享 |
…wxapi.WXEntryActivity / WXPayEntryActivity | 微信登录 / 支付回调 |
cn.jpush.android.ui.PushActivity / JNotifyActivity | 极光推送 |
cn.android.service.JTransitActivity | 极光 |
cn.jiguang.share.android.ui.JiguangShellActivity | 极光分享 |
…base.AppRegister | perm=com.tencent.mm.plugin.permission.SEND |
…playmusic.receiver.RemoteControlReceiver / StatusBarReceiver | 无权限保护 |
com.huawei.openalliance.ad.provider.PPSECProvider | 华为广告 |
风险点:RemoteControlReceiver / StatusBarReceiver 导出且未设置 android:permission,任意第三方应用可投递 ACTION_MEDIA_BUTTON 等广播影响播放器状态。
3.2 高危权限清单
QUERY_ALL_PACKAGES、READ_LOGS、SYSTEM_ALERT_WINDOW、REQUEST_INSTALL_PACKAGES、
WRITE_SETTINGS、MOUNT_UNMOUNT_FILESYSTEMS、MANAGE_ACCOUNTS、USE_CREDENTIALS、
GET_TASKS、KILL_BACKGROUND_PROCESSES、SET_DEBUG_APP、CALL_PHONE、
ACCESS_BACKGROUND_LOCATION、READ_PHONE_STATE、CAMERA、
READ/WRITE_EXTERNAL_STORAGE、requestLegacyExternalStorage="true"。
QUERY_ALL_PACKAGES+READ_LOGS+SYSTEM_ALERT_WINDOW组合,配合壳的环境检测能力, 属于典型的“风控型”权限画像。
3.3 Application 级配置
android:debuggable="false" 正确
android:allowBackup="false" 正确
android:usesCleartextTraffic="true" 全局允许明文
android:networkSecurityConfig=@xml/network_security_config 见下文
android:largeHeap="true"
android:allowNativeHeapPointerTagging="false" 关闭 MTE 指针标记
android:requestLegacyExternalStorage="true" 旧存储模型
4. 网络安全配置
4.1 res/xml/network_security_config.xml 原文(已解码)
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors>
<certificates overridePins="true" src="system"/>
<certificates overridePins="true" src="user"/> <!-- 信任用户 CA -->
</trust-anchors>
</base-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">…</domain>
</domain-config>
</network-security-config>
三个叠加问题:
| 问题 | 影响 |
|---|---|
src="user" 在 base-config 中 | 全局信任用户安装的 CA(Android 7+ 默认只信任系统 CA) |
overridePins="true" | 强制覆盖证书固定,即使 SDK 内部做了 pinning 也会被绕过 |
cleartextTrafficPermitted="true" | 允许明文 HTTP,配合 §5 的 http:// 端点可被降级/篡改 |
结论:本 App 对中间人攻击零防护。只要诱导用户安装一张证书(或设备已被 root/植入用户证书),攻击者可完整解密并篡改登录凭据、Token、身份证 OCR 上传数据等全部流量。
4.2 签名方案
APK Signing Block 位于偏移 0x04A5A000,长度 4,088 字节,含 3 个 ID-value 对:
| ID | 方案 | 长度 |
|---|---|---|
0x7109871a | APK Signature Scheme v2 | 1,535 |
0xf05368c0 | APK Signature Scheme v3 | 1,535 |
0x42726577 | verity padding | 958 |
块起始 0x04A5A000: 08 0F 00 00 00 00 00 00 ← u64 块长 = 4088 (0xff8)
块结束 0x04A5AFF8: F8 0F 00 00 00 00 00 00 "APK Sig Block 42"
中央目录 0x04A5B000: 50 4B 01 02 ... ← PK\x01\x02
apksigner verify --verbose:
Verifies
Verified using v1 scheme (JAR signing): true
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
Verified using v3.1 scheme: false
Verified using v4 scheme: false
Verified for SourceStamp: false
Number of signers: 1
- v1 + v2 + v3 三方案齐全,且 v1/v2/v3 证书完全一致(DER 877 字节,SHA-256 相同)。
- 摘要算法
0x0103= RSASSA-PKCS1-v1_5 + SHA-256。Android 7+ 走 v2/v3 校验,重打包会直接安装失败,签名保护是有效的。
签名证书(v1 = v2 = v3):
Subject : CN=xxxxxxx, OU=dev, O=xxxxxxx, L=hangzhou, ST=zhejiang, C=CN
有效期 : 2016-02-01 → 2041-01-25 (25 年)
算法 : SHA256withRSA, 2048 bit
SHA-1 : 27:DC:24:2A:F3:7D:61:08:26:DC:2D:55:9B:3C:F7:64:2B:17:30:B2
SHA-256 : CF:4D:98:CB:05:16:D8:99:B8:59:0C:1E:9F:DB:E5:04:96:98:9C:79:79:14:89:94:D3:62:DC:AF:3E:22:67:5C
OU=dev(开发)证书用于发布包,且有效期 25 年,若该私钥泄露,攻击者可长期伪造该 App 的更新包。
5. 加固壳字符串加密算法
classes.dex 中所有字符串常量形如 "LQoZSw8WESsEBwBPHQw9SwMXAAMVKxdaKBgyFT4JHQYABwwhCw==",
由 La/auu/a;->c(Ljava/lang/String;)Ljava/lang/String; 解密。
算法(从 smali 完整还原):
String decrypt(String s) {
byte[] b = Base64.decode(s); // 标准 Base64 字母表 A-Za-z0-9+/
byte[] k = "Netease".getBytes(); // 7 字节重复密钥
for (int i = 0; i < b.length; i++)
b[i] ^= k[i % 7];
return new String(b, "utf-8");
}
验证:
| 密文 | 明文 |
|---|---|
KgQYEwgYSz0cBxEEHksMBAcAJRYdDQkVFhI/Ci8BERc= | dalvik.system.BaseDexClassLoader |
IycbEA8XJD4VGAwCEhEnCho= | mBoundApplication |
LQoZSw8WESsEBwBPHQw9SwMXAAMVKxdaKBgyFT4JHQYABwwhCw== | com.netease.nis.wrapper.MyApplication |
Kx0AFwAQEREWAwwVEA0RVQ== | extract_switch_0 |
ORcVFREWFw== | wrapper |
- 该密钥
"Netease"硬编码在classes.dex的字符串池中(偏移0x9acc,全文仅出现 1 次, 已断言确认);libnesec.so中不含该明文串。 - 共解密出 353 条壳字符串`
说明:破解的是加固壳自身的字符串表。业务代码的 13.76 MB 载荷是另一套更高的加密 (非 Base64+XOR),静态未破解。
6. 硬编码凭据与敏感信息
6.1 assets/JGShareSDK.xml — 极光分享 SDK 第三方凭据
<SinaWeibo AppKey="xxx" AppSecret="xxxx"
RedirectUrl="https://www.jiguang.cn"/>
<QQ AppId="xxxx" AppKey="xxxx"/>
结论:AndroidManifest.xml 中存在<data android:scheme="xxxx"/>,与 QQ AppId="xxxx" 完全一致;WXEntryActivity / WXPayEntryActivity(…wxapi 包)也已注册。这些不是 Demo 残留,而是该 App 生产环境的真实凭据。
风险:AppSecret 硬编码在 APK 中即等同于公开。任何人可:
- 冒充该 App 调用微信/微博/QQ OpenAPI(以其配额);
- 伪造
OAuth回调 / 分享来源; - 消耗其接口调用额度。
6.2 极光推送
com.tencent.open.config.json、assets/JGShareSDK.xml 与 cn.jpush.android.*组件(cn.jpush.android.service.DataProvider、InitProvider)表明使用 极光推送/分享 (JPush/JShare),但未发现硬编码的 JPUSH_APPKEY(应在业务代码或壳内,静态不可见)。
7. 后端资产测绘(来自 resources.arsc,全部明文)
加固保护不了 resources.arsc,因此全部环境配置暴露:
7.1 API 路径字典(26 个,来自资源表)
login/loginByToken login/searchSchools login/sno
login/telOrSno login/welcome active/sno/verify
active/info/complete active/info/completeEmail
user/loginPwd/appeal user/loginPwd/appeal/check
user/check/pwd user/phone/checkPin user/phone/restPwd
user/emailFind/checkPin user/emailFind/restPwd user/mailbox/modify
user/studentno/relevance user/studentno/not/relevance
common/smCode common/sendEmailCode pic/get/token
activity/cancel activity/edit/detail activity/edit/save
activity/manager/enroll/stop activity/time/check
app/createactivity appPic/homepage outside/app/update
signup/signups
值得注意:login/loginByToken(Token 换登录)、user/phone/restPwd、user/emailFind/restPwd(找回密码)、common/smCode(短信验证码)、outside/app/update(应用更新,配合 REQUEST_INSTALL_PACKAGES)。
8. 第三方 SDK 清单
| SDK / 厂商 | 证据 |
|---|---|
| 网易易盾(加固) | libnesec.so、libnesec-x86.so、libenvsec.so、com.netease.nis.wrapper.* |
| 百度地图 + 定位 | libBaiduMapSDK_base_v7_5_7.so、libBaiduMapSDK_map_v7_5_7.so、liblocSDK8b.so、libindoor.so、assets/cfg/a/** |
| 腾讯 Bugly | libBugly.so、libBugly_Native.so、dump_syms/ |
| 友盟 | libumeng-spy.so |
| 支付宝(安全/图像) | libappc-cv-lib.so |
| 华为 HMS / AGConnect | 8 个 HMSCore-*.properties (6.13.0.303)、agconnect-core.properties (1.9.1.304)、libCtaApiLib.so、assets/base_hms_app_root.cer |
| 极光推送 / 分享 | libjcore420.so、libjutils.so、libquicklogin.so、cn.jpush.*、assets/JGShareSDK.xml |
| 哔哩哔哩 ijkplayer | libijkplayer.so、libijkffmpeg.so、libijksdl.so、librtmp-jni.so |
| 腾讯 X5 / 广告 | assets/mobile.v2.29.2.html、com.tencent.*、com.huawei.openalliance.ad.* |
| OpenCV 类视觉 | libscannative.so、libtiny_magic.so、libyuv-decoder.so |
| 厦门云脉 身份证 OCR | libIDCardengine.so、libocr.so、assets/zocr0.lib(见 §9) |
| 微信 / QQ / 微博 分享 | wxapi、tencent.tauth、JGShareSDK.xml |
| MMKV 存储 | libmmkv.so |
| BouncyCastle、OkHttp、Kotlin、Firebase Crashlytics 构建工具 | org/bouncycastle/*、okhttp3/*、kotlin/*、com/google/firebase/crashlytics/buildtools/* |
打包爆笑环节:dump_syms/ 目录被误打包进发布 APK:
dump_syms/linux/dump_syms.bin 2,396,136
dump_syms/macos/dump_syms.bin 1,493,632
dump_syms/windows/dump_syms.bin 2,723,549
dump_syms/windows/libstdc++-6.dll 26,339,528 ← 26 MB Windows DLL
dump_syms/windows/libgcc_s_seh-1.dll 607,449
dump_syms/windows/libssp-0.dll 137,828
dump_syms/windows/libwinpthread-1.dll 367,931
这些是 Breakpad/Crashlytics 符号提取工具链,与 Android 运行时毫无关系, 纯属 CI 打包脚本把工具目录当资源带出
9. 商业 SDK 授权与外部依赖风险
9.1 OCR 库的联网校验(明文 HTTP)
http://web.ccyunmai.com:81/SrvTimeChk?T=%s&M=%s&S=%s&V=%s&C=%d
- 该 SDK 每次授权校验都走明文 HTTP 访问
web.ccyunmai.com:81,依赖服务器返回时间做试用期控制。 - 攻击面:明文传输 → 可被中间人篡改时间戳以延长/绕过试用期;同时该请求会向第三方泄露设备/时间信息。
10. 数据资产概览
| 文件 | 大小 | 熵 | 说明 |
|---|---|---|---|
classes.dex | 13,831,792 | 头部 5.47 / 主体 ~7.99 | 伪 DEX + 13.76 MB 加密业务代码 |
resources.arsc | 1,189,160 | 5.99 | 明文,含 §7 全部端点 |
assets/zocr0.lib | 2,837,252 | 6.93 | ZOCR 识别模型/字典 |
assets/nedata.db | 405,572 | 7.999 | 易盾加密数据缩) |
assets/nedig.properties | 3,120 | 6.58 | 易盾配置(加密) |
assets/cfg/a/mode_1/map.rs + .sty | 1.43 MB + 864 KB | — | 百度地图渲染样式 |
assets/mobile.v2.29.2.html | 46,068 | — | 腾讯 X5 内核兜底页 |
assets/openmeasure/omsdk-v1.js | 39,855 | — | IAB OM SDK(广告可见性测量) |
assets/angle.ms / corner.ms / detect.ms | 125 KB / 380 KB / 679 KB | — | 图像处理数据 |
| 资源图片 | 1,078 PNG + 11 WebP + … | — | 含 res/drawable-nodpi-v4/*_metal.png、*_vignette.png 等高熵位图 |
LICENSES.txt | 33,853 | — | 开源许可清单 |
11.libnesec.so 内部结构完全还原
该库实际上有 60 KB 明文 AArch64 机器码,且 四套字符串密码全部被破解。
11.1 section header 是伪造的,必须走 program header
绝大多数自动工具都被这份伪造的节表误导:
| 伪造节名 | 实际用途 |
|---|---|
.gnu.fragment | 加密的真实 .text/.rodata(784 KB,熵 7.42) |
.eh_frame_hdr / .eh_frame / .gcc_except_table | 全熵(7.97–7.99),绝非异常表,是加密数据 |
.gnu.draft | 明文引导代码 / 加载器(71,256 字节,熵 6.39) |
.gnu.stub | GOT + 未初始化数据(熵 2.37) |
.eh_intent / .text(仅 16 字节) | 诱饵 |
真实段布局(由 program header 决定):
| 段 | vaddr | file offset | 大小 | 权限 | 性质 |
|---|---|---|---|---|---|
| PT_LOAD#0 | 0x00000 | 0x000000 | 0xc4000 = 784.0 KB | R-X | 加密的真实代码(熵 7.42) |
| PT_LOAD#1 | 0x0d8000 | 0x0c4000 | 0x11698 = 69.6 KB | R-X | 明文引导代码 / loader(熵 6.39) |
| PT_LOAD#2 | 0x0edb18 | 0x0d5b18 | 0x12c90 = 75.1 KB | RW- | dynsym / dynstr / hash / GOT / 掩码字符串 |
明文机器码的精确范围(逐 4 KB 页统计有效指令率得出):
| 文件区间 | 大小 | 有效指令率 | 性质 |
|---|---|---|---|
0xc4000 – 0xd3000 | 60.0 KB | 92.5%(多数页 100%) | 真实 AArch64 代码 |
0xd3000 – 0xd5698 | 9.6 KB | 47% | 只读数据(~/} 填充表等) |
对照:加密区有效率仅 45.0%,纯随机基线 35.8%。 用有效率而非熵来定位代码,是本轮的关键方法。
换算关系:PT_LOAD#1 →
vaddr = file + 0x14000;PT_LOAD#2 →vaddr = file + 0x18000混淆点:vaddr ≠ file offset,两段偏移不同,按节表或按统一偏移都会算错。
注意:对易盾加固的 .so,不要信 section header,一律用 PT_LOAD + DT_SYMTAB / DT_STRTAB / DT_HASH / DT_JMPREL 解析。
另注意:capstone 统计指令有效率时必须 skipdata=False,否则任意字节序列都会被解码,有效率恒为 100%,完全失去判别力。
11.2 符号表脱钩:导出名被抹掉,但 ELF hash 桶泄露真相
DT_HASH:nbucket=37、nchain=58(即 58 个符号),但 37 个桶里只有 bucket[36] 非空。
bucket[36] -> sym[57]
st_value = 0x8ec12 st_size = 1500
st_name = 0x4a6 <- .dynstr 中一段 307 字节的随机数据(不是字符串)
决定性验证:ELF hash 是公开算法,直接反算——
elf_hash("JNI_OnLoad") = 0x0467e784
0x0467e784 % 37 = 36 ← 与唯一非空桶完全吻合
而 .dynstr 里 "JNI_OnLoad" 位于 +0x8c8,却没有任何符号引用它。
结论:真实导出符号就是 JNI_OnLoad,只是名字被故意从符号表脱钩(st_name 指向垃圾),但哈希桶忘了改,等于自证。
PLT / GOT 完全对应(16 个,与 DT_JMPREL 的 16 条 R_AARCH64_JUMP_SLOT 一一对应):
| PLT | GOT | 导入函数 | PLT | GOT | 导入函数 |
|---|---|---|---|---|---|
0xd92f0 | 0xedd58 | sysconf | 0xd9370 | 0xedd98 | sigaction |
0xd9300 | 0xedd60 | munmap | 0xd9380 | 0xedda0 | dlsym |
0xd9310 | 0xedd68 | fgets | 0xd9390 | 0xedda8 | fopen |
0xd9320 | 0xedd70 | __cxa_finalize | 0xd93a0 | 0xeddb0 | memset |
0xd9330 | 0xedd78 | mmap | 0xd93b0 | 0xeddb8 | fclose |
0xd9340 | 0xedd80 | dlclose | 0xd93c0 | 0xeddc0 | atoi |
0xd9350 | 0xedd88 | dlopen | 0xd93d0 | 0xeddc8 | mprotect |
0xd9360 | 0xedd90 | sscanf | 0xd93e0 | 0xeddd0 | raise |
adrp x16, #0xed000+ldr x17,[x16,#0xd58]→0xed000+0xd58 = 0xedd58与DT_JMPREL解析结果逐条吻合,映射 100% 自洽。
11.3 四套字符串密码全部破解
密码 A — 内联栈串(解密函数 @ vaddr 0xdbe68)
0xdbe68 ldrb w1, [x0] ; c = buf[i]
0xdbe6c adrp x5, #0xe9000
0xdbe78 sub w3, w2, w0 ; i = p - buf
0xdbe7c add x4, x5, #0x200 ; x4 = 0xe9200 <- 32 字节密钥表
0xdbe80 and w3, w3, #7 ; i & 7
0xdbe84 ldr w3, [x4, w3, sxtw #2] ; key32[i & 7]
0xdbe88 eor w1, w3, w1
0xdbe8c and w1, w1, #0x7f ; & 0x7F
0xdbe90 strb w1, [x2] ; 原地写回
算法:
byte[] key = {1,2,3,4,5,6,7,8}; // vaddr 0xe9200, 实测 01 00 00 00 02 00 00 00 ...
for (int i = 0; buf[i] != 0; i++)
buf[i] = (buf[i] ^ key[i & 7]) & 0x7F;
验证样本:
| 密文 | 明文 |
|---|---|
f3 ed ad e6 f0 ef eb ec af f4 e6 f6 f6 ef e8 e6 af f1 e7 ef | ro.build.version.sdk |
ed eb e1 e7 ab f5 e8 | libc.so |
de dd f0 fd f6 f2 e2 e5 de f2 f1 eb f5 e3 f5 fc f8 dd e4 e1 f1 | __system_property_get |
密码 B — 数据段掩码串(绝大多数敏感串)
代码形态:
ldr d0, [x2] ; 密文 8 字节
ldr d1, [x3] ; 掩码 8 字节
eor v0.8b, v0.8b, v1.8b ; NEON 向量异或 -> 天然 period-8
str d0, [x19]
strb wN, [x19, #8] ... ; 尾部逐字节
算法(两个变体,base 由各调用点的立即数决定):
// 变体 B1(主流,0xee0xx 表全部使用)—— 周期 8
plain[i] = cipher[i] ^ ((base + (i % 8)) & 0xFF); // base = 0x81
// 变体 B2(部分调用点)—— 线性
plain[i] = cipher[i] ^ ((base + i) & 0xFF); // 如 base = 0x1d
对每个样本反推掩码后逐字节比对:
| vaddr | base | 掩码族 | 明文 |
|---|---|---|---|
0xee040 | 0x81 | period-8 | libc++_shared.so |
0xee060 | 0x81 | period-8¹ | libc.so |
0xee070 | 0x81 | period-8¹ | libdl.so |
0xee080 | 0x81 | period-8¹ | libm.so |
0xee090 | 0x81 | period-8¹ | libz.so |
0xee0c0 | 0x81 | period-8 | libandroid.so |
0xee160 | 0x81 | period-8 | android/os/Build$VERSION |
0xee1b0 | 0x81 | period-8 | currentActivityThread |
0xee250 | 0x81 | period-8 | /system/bin/pm path %s | ... |
0xee310 | 0x81 | period-8 | /AndroidManifest.xml |
0xee330 | 0x81 | period-8 | com/netease/nis/wrapper/MyJni |
0xee400 | 0x81 | period-8¹ | unidbg |
0xee420 | 0x81 | period-8 | /proc/self/maps |
0xee0d0 | 0x1d | linear | %lx-%lx %4s %*x %*x:%*x %*d%n |
¹ 这些串长度 < 8,两种变体在数学上等价(无法区分),按所属表归为 period-8。
一个必须注意的坑:0xee0d0 的 %lx-%lx ... 用的是 linear(base 0x1d),
而不是主流表的 period-8(base 0x81)。两族掩码不能混用—— 只用 period-8 去解全表时,正是这一条解不出来,才反查出存在两族。
正确做法是:按调用点确定 base 与族,或两族都试并按可打印性择优。
存储约定:密文串以 明文的
0x00填充 分隔(密文里看到00需注意可能是plain[i] == key[i]的巧合,故用「连续 2 个0x00」判终止)。 解密后出现的0x88表示同一条目内的多段(原始字节为0x00,0x00 ^ 0x81 = 0x88),例如il2cpp_override_stack_backtrace+libil2cpp.so是同一条记录的两段。
密码 D — 长度异或
格式是 16 字节定长槽:
[密文 N 字节][N 字节][0x00 填充到 16]
↑ 这个 N 既是长度, 也是单字节异或密钥
plain[i] = cipher[i] ^ N; // N = 明文长度, 同时是密钥
样本验证:
| 密文(hex) | N | 明文 |
|---|---|---|
75 6f 61 68 67 6a | 06 | signal |
62 6a 67 62 62 74 | 06 | dladdr |
7a 60 6e 68 6a 7d 60 66 67 | 09 | sigaction |
78 62 6c 7b 79 64 68 66 6a 78 60 | 0b | sigprocmask |
6b 63 50 66 7b 6a 7d 6e 7b 6a 50 7f 67 6b 7d | 0f | dl_iterate_phdr |
这一套密码最有价值——它解出的串构成一个完整的「反注入 / 反调试」API 表:
| vaddr | 明文 | 用途 |
|---|---|---|
0xe9240 | signal | 接管信号(反调试) |
0xe9250 | dladdr | 反查调用者属于哪个模块 |
0xe9260 | LD_PRELOAD(明文) | 检测环境变量注入 |
0xe9280 | sigaction | 同上(更精细的信号接管) |
0xe9290 | sigprocmask | 屏蔽信号 |
0xe92a0 | dl_iterate_phdr | 遍历全部已加载模块 → 查注入/查 hook 框架 |
0xe92d0 | /dev/null(明文) | 丢弃输出 |
0xe92e0 | libandroid.so(明文) | 解析目标 |
0xe92f0 | dl_iterate_phdr(明文) | 与上重复出现,说明被多处引用 |
0xe9300 | libdl.so(明文) | 解析目标 |
结论:这段表就是「检测 Frida / Xposed / LD_PRELOAD 注入」的核心。 若要动态脱壳,必须同时绕过:①
dl_iterate_phdr的模块枚举;②/proc/self/maps解析(§13.7);③/proc/self/exe的unidbg比对(§13.7)。
四种密码的分工总结:
| 密码 | 形态 | 用途 | 破解状态 |
|---|---|---|---|
| A | (c ^ ((i&7)+1)) & 0x7F | 短串、内联在栈上构造 | |
| B | c ^ ((base + i%fam) & 0xFF) | 数据段主体(Java 反射名等) | |
| C | 明文 | dynstr 尾段、立即数 | |
| D | c ^ len | 16 字节定长槽位表(反注入 API) |
11.4 还原出的全部关键字符串
A. 加载器 / 检测器语义串(0xee000 区,密码 B 掩码族)
0x0ee010 il2cpp_override_stack_backtrace | libil2cpp.so
0x0ee030 libil2cpp.so
0x0ee040 libc++_shared.so 0x0ee060 libc.so
0x0ee070 libdl.so 0x0ee080 libm.so
0x0ee090 libz.so 0x0ee0c0 libandroid.so
0x0ee0d0 %lx-%lx %4s %*x %*x:%*x %*d%n ← 线性掩码族 (base 0x1d)
0x0ee0f0 fake-libs
0x0ee100 .3a5505535732c68fab3089f8df24c0dc
0x0ee150 getResource
0x0ee160 android/os/Build$VERSION 0x0ee180 appInfo
0x0ee190 Ljava/lang/String; 0x0ee1b0 currentActivityThread
0x0ee1d0 SDK_INT 0x0ee1e0 toString
0x0ee1f0 Landroid/app/ActivityThread$AppBindData;
0x0ee220 (Ljava/lang/String;)Ljava/net/URL;
0x0ee250 /system/bin/pm path %s | /system/bin/sed 's/package://'
0x0ee290 java/lang/Class 0x0ee2a0 mBoundApplication
0x0ee2c0 packageName
0x0ee2d0 Landroid/content/pm/ApplicationInfo;
0x0ee310 /AndroidManifest.xml
0x0ee330 com/netease/nis/wrapper/MyJni
0x0ee350 android/content/pm/PackageItemInfo
0x0ee380 android/app/ActivityThread
0x0ee3a0 ()Landroid/app/ActivityThread;
0x0ee3c0 currentPackageName 0x0ee3e0 ()Ljava/lang/String;
0x0ee400 unidbg 0x0ee410 /proc/self/exe
0x0ee420 /proc/self/maps
B. 完整的 dlsym 解析表(0xf0c90–0xf0e7e,明文)
这段表直接暴露了壳自身要调用的全部 libc API——等于把它的能力清单交了出来:
| 类别 | 符号 |
|---|---|
| 进程/内存 | mmap mprotect munmap sysconf |
| 动态加载 | dlopen dlclose dlsym dladdr dl_iterate_phdr |
| 信号(反调试) | signal sigaction sigprocmask raise abort |
| 文件/IO | fopen fclose fread fwrite fseek read write |
| 字符串/内存 | memset memcpy memcmp strlen strcmp strncmp strncpy strstr strtok strcspn sprintf sscanf basename |
| 线程 | pthread_mutex_lock pthread_mutex_unlock |
| 其他 | malloc calloc free atoi exit stat fstat uname |
| 目标库 | liblog.so libz.so libdl.so libandroid.so libc.so libm.so libstdc++.so libsecexe.so |
libsecexe.so:它出现在这张解析表里,但不在 APK 中,这坐实了「真正的代码容器在运行时下发」的判断。
dl_iterate_phdr+dladdr同时出现,说明壳会遍历所有已加载模块并反查来源,这是检测 Frida/Xposed/注入框架的标准手法。
注:
0xee140处有一条 15 字节高熵数据(ef 9c cd 17 d2 06 …),四种密码、全部 256×5 种掩码组合均无法解出可打印串,判定为随机填充/密钥材料,非字符串。
11.5 加载链

11.6 反分析实现的真实面貌:MBA 混淆 + 不透明谓词
明文引导区大量使用 MBA(Mixed Boolean-Arithmetic)恒等式把简单运算膨胀成几十条指令:
; 下面这一整段实际只等于一个 eor
0xe38d0 mov w0, w0, ... ; 真正的 eor w0,w0,w1 被拆成:
0xe38b0 eor w0, w0, w1 ; (w0 ^ w1)
0xe38c0 add w0, w1, w0 ; (w1 + w0)
0xe38d0 sub w0, w0, w1 ; (w0 - w1)
典型膨胀模式(w0 = w0 ^ w1 被展开):
lsr w2, w9, #1 ; and w3, w9, #1 ; cmp w3, wzr ; eor w5, w2, K ; csel w5, w2, w5, eq
配合不透明谓词:函数开头总有
0xe0df0 tbnz w0, #0, #0xe1180 ; 条件恒为真 -> 走真实分支
0xe0df4 bl #0xdb0a8 ; 该函数体只有 sub sp / ret(死代码)
静态反编译器会被这两者严重干扰。破解方法:按 bl 目标聚类 + 手工化简 MBA 恒等式,即可恢复语义。
11.7 反注入 / 反模拟器 / 反 unidbg 检测器(对动态脱壳有直接影响)
A. 反注入 API 表(由密码 D 解出,见 §13.3)
dl_iterate_phdr + dladdr + LD_PRELOAD + signal/sigaction/sigprocmask → 遍历已加载模块、反查调用来源、检测环境变量注入,并接管信号反调试。
B. 反模拟器检测器
| vaddr | 行为 |
|---|---|
0xe3460 | fopen("/proc/self/maps") → fgets → sscanf 逐行解析内存映射 |
0xe3580 | 读 /proc/self/exe,与硬编码串 "unidbg" 比较 → 返回布尔 |
0xe35f8 | 读 /proc/self/maps,检查 fake-libs、il2cpp_override_stack_backtrace |
重要提示:
unidbg是目前最常用的「模拟执行脱壳」框架。 易盾在此专门做了对抗——一旦在/proc/self/exe路径或 maps 里发现 unidbg 痕迹即判定为模拟环境。 另"il2cpp_override_stack_backtrace"说明它还覆盖栈回溯以对抗基于调用栈的分析。 因此上文建议的「用 Unicorn/QEMU 模拟执行」路线,必须先 patch 掉这些检测。综上所述,动态脱壳需同时绕过 4 道检测:
dl_iterate_phdr模块枚举 ·/proc/self/maps解析 ·/proc/self/exe的 unidbg 比对 ·LD_PRELOAD环境变量检查。
11.8 .ms 资产闭环
| 项 | 结论 |
|---|---|
| 文件格式 | FlatBuffers(文件头 24 00 00 00 + 标识符 MSL2) |
| 加载方 | libscannative.so(内含 MSL2、angleSession、cornerSession、detectSession) |
| 上游厂商 | 华为 HMS ScanKit(Java_com_huawei_hms_scankit_util_OpencvJNI_QRCornerDetect) |
| 推理引擎 | 华为 MindSpore Lite(libscannative.so 内嵌 mindspore::schema::*、MSLITE_* 环境变量) |
| 用途 | 扫码/识别的三个模型:角度矫正 / 角点检测 / 目标检测 |
这些文件与 App 业务无关,属华为扫码 SDK 的模型文件,非敏感数据。
11.9 仍未攻克的部分
| 目标 | 状态 | 说明 |
|---|---|---|
PT_LOAD#0(file 0x0..0xc4000,熵 7.42,784 KB) | 加密的真实 .text/.rodata。解密算法本身就在这段密文里(自举),纯静态无法解 | |
.dynstr[0x000..0x87d](2173 字节,熵 7.30) | 已试:单字节 XOR、线性密钥、周期密钥(1/2/4/8/16/32/64)、以及本轮破解的 A/B/D 三套密钥族 —— 全部失败。判断:要么是另一套密码,要么该区域本就是随机填充 | |
| 真实 DEX(13.76 MB)的解密密钥 | 密钥派生在 PT_LOAD#0 内,需动态 |
但请注意:即使 PT_LOAD#0 未解密,本轮已拿到加载器/检测器的全部语义。 要拿到业务 DEX,现在最省力的路径是动态:在
mmap+mprotect之后、JNI_OnLoad返回之前 dump 那块匿名可执行内存。