前言:整个 APP 比较幽默的是直接把开发者包拿来当正式包发布了,一定程度上降低了这次逆向的难度。本次用的 codebuddy 国际版的免费 deepseek v4.1 flash 进行的逆向工作,整体来说 deepseek 的输出速度还是很 nice 的。大模型语言毕竟还是擅长处理语言,所以当你没法提供一个 ARM 的 root 环境时最好还是先让大模型先做充分的静态逆向工作,为避免不必要的风险,下文已将 APP 相关信息以打码代替。

1. 结论摘要

  1. 本 APK 受网易易盾加固保护,入口 Application 被替换为com.netease.nis.wrapper.MyApplication,加固壳版本 7.6.1_817。
  2. APK 内不存在任何明文应用代码。 classes.dex 头部合法(dex\n035)且壳自身的 31 个类是真实可读的(com.netease.nis.wrapper.*),但约 13.76 MB 的真实业务代码以全熵密文形式躺在同一文件的"填充区"里,只能由 libnesec.so 在运行时解密。
  3. 尽管如此,加固壳本身(classes.dex + 31 个 smali)被完整逆向,包括其字符串加密算法,因此壳的行为、加载链、反调试策略完全透明。
  4. 加固不影响以下内容的静态读取,均已在本文档中取出:AndroidManifest.xml、resources.arsc、res/xml/network_security_config.xml、assets/ 第三方 SDK 凭据。
  5. 最严重的配置缺陷:网络安全配置全局信任用户 CA、允许明文 HTTP 且 overridePins=true。任何能安装用户证书的人都可以中间人解密该 App 的全部 HTTPS 流量,且该配置会覆盖证书固定。这正是逆向配合抓包场景成立的原因。
  6. 已提取到硬编码第三方凭据(微信 / QQ / 新浪微博),部分与 manifest 中的真实 AppID 交叉印证,属于生产环境有效凭据。
  7. libnesec.so 的内部结构已完全还原:伪造节表被识破、JNI_OnLoad 导出名由 ELF hash 桶反推确证、四套字符串密码全部破解、加载链逐条对应到指令;并发现专门对抗 unidbg 模拟脱壳的检测器。

2. 加固 / 保护机制

2.1 壳的加载链

从解密后的壳字符串可完整还原: alt text 对应 manifest 关键项:android:appComponentFactory="androidx.core.app.CoreComponentFactory"(被壳用作代理点)、android:name="com.netease.nis.wrapper.MyApplication"。

2.2 密文载荷定位

区域偏移大小熵
伪 DEX 头部(字符串/类型/方法表)0 .. ~69,804~68 KB5.47
加密载荷(全熵密文)69,804 .. 13,831,792≈13.76 MB7.90 – 8.00
ZIP 中央目录77,967,360 .. 78,261,461294 KB—
EOCD(文件尾)78,261,461 .. 78,261,48322 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 携带的完整检测提示语,说明易盾已启用下列检测:

类别检测文案(原文)
RootDetected that the app is running in a rooted environment.
模拟器Detected that the app is running on an emulator.
XposedDetected that the app is running in an Xposed environment.
HookDetected 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.
VPNDetected 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.
异常 ROMDetected 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. 组件与攻击面

类型数量
Activity378
Service25
BroadcastReceiver16
ContentProvider13
申请权限62(3 项重复)
自定义权限8(均 protectionLevel=0x02 = signature)

3.1 导出组件

组件备注
…qualitync.SplashActivityLAUNCHER
…commonui.SchemeActivityxxxx:// 深链入口
…commonui.ActivityNotificationScheme通知跳转深链
com.tencent.tauth.AuthActivityQQ 登录(scheme=tencentxxxxxx)
com.tencent.connect.common.AssistActivityQQ 分享
…wxapi.WXEntryActivity / WXPayEntryActivity微信登录 / 支付回调
cn.jpush.android.ui.PushActivity / JNotifyActivity极光推送
cn.android.service.JTransitActivity极光
cn.jiguang.share.android.ui.JiguangShellActivity极光分享
…base.AppRegisterperm=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方案长度
0x7109871aAPK Signature Scheme v21,535
0xf05368c0APK Signature Scheme v31,535
0x42726577verity padding958
块起始 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/**
腾讯 BuglylibBugly.so、libBugly_Native.so、dump_syms/
友盟libumeng-spy.so
支付宝(安全/图像)libappc-cv-lib.so
华为 HMS / AGConnect8 个 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
哔哩哔哩 ijkplayerlibijkplayer.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
厦门云脉 身份证 OCRlibIDCardengine.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.dex13,831,792头部 5.47 / 主体 ~7.99伪 DEX + 13.76 MB 加密业务代码
resources.arsc1,189,1605.99明文,含 §7 全部端点
assets/zocr0.lib2,837,2526.93ZOCR 识别模型/字典
assets/nedata.db405,5727.999易盾加密数据缩)
assets/nedig.properties3,1206.58易盾配置(加密)
assets/cfg/a/mode_1/map.rs + .sty1.43 MB + 864 KB—百度地图渲染样式
assets/mobile.v2.29.2.html46,068—腾讯 X5 内核兜底页
assets/openmeasure/omsdk-v1.js39,855—IAB OM SDK(广告可见性测量)
assets/angle.ms / corner.ms / detect.ms125 KB / 380 KB / 679 KB—图像处理数据
资源图片1,078 PNG + 11 WebP + …—含 res/drawable-nodpi-v4/*_metal.png、*_vignette.png 等高熵位图
LICENSES.txt33,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.stubGOT + 未初始化数据(熵 2.37)
.eh_intent / .text(仅 16 字节)诱饵

真实段布局(由 program header 决定):

段vaddrfile offset大小权限性质
PT_LOAD#00x000000x0000000xc4000 = 784.0 KBR-X加密的真实代码(熵 7.42)
PT_LOAD#10x0d80000x0c40000x11698 = 69.6 KBR-X明文引导代码 / loader(熵 6.39)
PT_LOAD#20x0edb180x0d5b180x12c90 = 75.1 KBRW-dynsym / dynstr / hash / GOT / 掩码字符串

明文机器码的精确范围(逐 4 KB 页统计有效指令率得出):

文件区间大小有效指令率性质
0xc4000 – 0xd300060.0 KB92.5%(多数页 100%)真实 AArch64 代码
0xd3000 – 0xd56989.6 KB47%只读数据(~/} 填充表等)

对照:加密区有效率仅 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 一一对应):

PLTGOT导入函数PLTGOT导入函数
0xd92f00xedd58sysconf0xd93700xedd98sigaction
0xd93000xedd60munmap0xd93800xedda0dlsym
0xd93100xedd68fgets0xd93900xedda8fopen
0xd93200xedd70__cxa_finalize0xd93a00xeddb0memset
0xd93300xedd78mmap0xd93b00xeddb8fclose
0xd93400xedd80dlclose0xd93c00xeddc0atoi
0xd93500xedd88dlopen0xd93d00xeddc8mprotect
0xd93600xedd90sscanf0xd93e00xeddd0raise

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 efro.build.version.sdk
ed eb e1 e7 ab f5 e8libc.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

对每个样本反推掩码后逐字节比对:

vaddrbase掩码族明文
0xee0400x81period-8libc++_shared.so
0xee0600x81period-8¹libc.so
0xee0700x81period-8¹libdl.so
0xee0800x81period-8¹libm.so
0xee0900x81period-8¹libz.so
0xee0c00x81period-8libandroid.so
0xee1600x81period-8android/os/Build$VERSION
0xee1b00x81period-8currentActivityThread
0xee2500x81period-8/system/bin/pm path %s | ...
0xee3100x81period-8/AndroidManifest.xml
0xee3300x81period-8com/netease/nis/wrapper/MyJni
0xee4000x81period-8¹unidbg
0xee4200x81period-8/proc/self/maps
0xee0d00x1dlinear%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 6a06signal
62 6a 67 62 62 7406dladdr
7a 60 6e 68 6a 7d 60 66 6709sigaction
78 62 6c 7b 79 64 68 66 6a 78 600bsigprocmask
6b 63 50 66 7b 6a 7d 6e 7b 6a 50 7f 67 6b 7d0fdl_iterate_phdr

这一套密码最有价值——它解出的串构成一个完整的「反注入 / 反调试」API 表:

vaddr明文用途
0xe9240signal接管信号(反调试)
0xe9250dladdr反查调用者属于哪个模块
0xe9260LD_PRELOAD(明文)检测环境变量注入
0xe9280sigaction同上(更精细的信号接管)
0xe9290sigprocmask屏蔽信号
0xe92a0dl_iterate_phdr遍历全部已加载模块 → 查注入/查 hook 框架
0xe92d0/dev/null(明文)丢弃输出
0xe92e0libandroid.so(明文)解析目标
0xe92f0dl_iterate_phdr(明文)与上重复出现,说明被多处引用
0xe9300libdl.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短串、内联在栈上构造
Bc ^ ((base + i%fam) & 0xFF)数据段主体(Java 反射名等)
C明文dynstr 尾段、立即数
Dc ^ len16 字节定长槽位表(反注入 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
文件/IOfopen 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 加载链

alt text

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行为
0xe3460fopen("/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 那块匿名可执行内存。