目标 App:com.sichuanol.cbgc(v12.2.5)
加固方案:某棒企业版(SO 加密 + Java VMP + 反调试 + 反 Frida)
设备:Pixel 6 (oriole), Android 15, kernel 5.10.209, KernelPatch 0.13.1
分析工具链:
ecapture(eBPF TLS 明文抓包,零注入)
定制系统(自动脱壳)
MemDumper(内存 dump+修复 加密的 SO)
IDA Pro(静态分析)
xiaojianbang_hook(内核级无痕 Hook,硬件断点,零代码修改)
本次分析全程使用内核级工具,绕过所有用户态检测:
●eBPF 在内核态 hook SSL_write/SSL_read,不修改目标进程任何内存
●定制系统在 ART 虚拟机层面自动 dump dex(能检测,后续改进)
●硬件断点在 CPU 调试寄存器层面工作,不修改 .text 段,CRC 校验无效
●ptrace_spoof 返回假的调试寄存器状态,反调试看不到我们的断点
1. ecapture eBPF 抓包(TLS 明文,零注入)
传统抓包需要安装 CA 证书 + 绕过 Certificate Pinning,容易被检测。ecapture 利用 eBPF uprobe 直接在内核态 hook libssl.so 的 SSL_write/SSL_read 函数,在加密前/解密后截获明文。不修改目标进程任何内存,不注入任何代码,目标进程完全无感知。
# 找到目标进程PID=$(pidof com.sichuanol.cbgc)# eBPF 抓包(指定 boringssl 版本和 libssl 路径)ecapture tls --libssl=/apex/com.android.conscrypt/lib64/libssl.so \--ssl_version="boringssl 1.1.1" --pid=$PID -l ecapture.log
触发登录(手机号 18888888888,验证码 888888),抓到完整 HTTP 请求明文:
POST //user/vCodeLogin HTTP/1.1Host: cbgcapi.scol.com.cnUser-Agent: covermedia-androidtenantId: 0fmio_token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...fmio_appid: rmt_cbgcContent-Type: application/x-www-form-urlencodedswitch_suggest=1data={"mobile":"18888888888","step":2,"vcode":"888888"}vno=12.2.5sign=ADEF6EFED6B2E3C89A2FC1A98BBC8EC9← 目标:还原这个 signchannel=myapp_169client=androidteen_mode=0app_vno=12.2.5deviceid=500254ce-8216-38d9-ae5d-500d587704a4-ZCEUaccount=0da46ac5-23d1-4ad2-97f4-28d2432536detimestamp=1780060621606token=
关键观察:
●sign 为 32 位大写 hex → MD5 输出格式
●请求中有 account(UUID)、timestamp(13 位毫秒)、switch_suggest("1")
●这三个字段很可能是 sign 的输入参数
2. 定制系统自动脱壳
# dex 自动 dump 到以下目录(10 个 dex 文件)ls /data/data/com.sichuanol.cbgc/xiaojianbang/# 2373879_dexfile_execute.dex# 19314748_dexfile_execute.dex# ... (共 10 个)# jadx 反编译(棒棒修改了 dex checksum,必须关闭校验)jadx -Pdex-input.verify-checksum=no --show-bad-code --no-res \-d jadx_out *_dexfile_execute.dex# 成功反编译 16266 个类
定位 sign 生成的调用链
通过搜索 "sign" 关键字,逐步追踪到 native 层:
// HttpRequestEntity.java — 网络请求构造(VMP 保护)public HttpRequestEntity(Context context, HashMap map, String str) {JniLib.cV(HttpRequestEntity.class, this, context, map, str, 23);// VMP}// SignFactory.java — sign 工厂public static String getSign(String str, String str2, String str3) {return sManager.getSign_J(str, str2, str3);}// SignManager.java — 桥接 nativestatic { System.loadLibrary("wtf"); }public static native String getSign(String str, String str2, String str3);public String getSign_J(String str, String str2, String str3) {return (String) JniLib.cL(SignManager.class, this, str, str2, str3, 16);}// JniLib.java — 棒棒 VMP 引擎(libdexjni.so)public static native Object cL(Object... objArr);// VMP dispatch
完整调用链:
HttpRequestEntity 构造 → SignFactory.getSign(str1, str2, str3)→ SignManager.getSign_J() → JniLib.cL() (VMP dispatch)→ native getSign() in libwtf.so
sign 的核心计算在 libwtf.so 的 native 函数中,Java 层只是传参。
3. MemDumper dump 加密 SO
libwtf.so 在磁盘上是加密的(棒棒 SO 加密),运行时解密到内存。/proc/pid/maps 中映射为 rwxp(可读可写可执行),说明壳在运行时解密并修改了权限。
直接从磁盘拿到的 SO 是密文,IDA 无法分析。必须从内存中 dump 已解密的版本:
# 编译 MemDumper(arm64 版本)cd ~/bin/MemDumper-master && ndk-build APP_ABI=arm64-v8a# 推送到设备adb push libs/arm64-v8a/memdumper /data/local/tmp/adb shell "su -c 'chmod 755 /data/local/tmp/memdumper'"# dump libwtf.so(自动修复 ELF 头)adb shell "su -c '/data/local/tmp/memdumper -p com.sichuanol.cbgc -l -n libwtf.so'"# 输出: /sdcard/libwtf.so (963KB,已修复 ELF section headers)
dump 后用 readelf 确认 JNI 导出符号:
0x4161cJava_cn_thecover_lib_common_manager_SignManager_getSign (3056 bytes)0x42aa4Java_cn_thecover_lib_common_manager_SignManager_getFinalDeviceId (1460 bytes)
SO 内部还包含自实现的 SHA1 和 MD5(不依赖系统库,增加逆向难度):
0x3F914MD5::MD5()— 构造函数0x3F93CMD5::init()— 初始化0x422A8SHA1::update(std::string const&)0x4256CSHA1::final()
4. IDA 静态分析 getSign (0x4161c)
4.1 函数签名
jstring Java_cn_thecover_lib_common_manager_SignManager_getSign(JNIEnv *env, jclass clazz, jstring str1, jstring str2, jstring str3)
ARM64 调用约定:X0=env, X1=clazz, X2=str1, X3=str2, X4=str3。
4.2 反编译逻辑梳理
IDA 反编译后,手动标注函数名和变量,还原出完整逻辑流程:
1. FindClass("cn/thecover/lib/common/utils/LogShutDown")2. GetStaticMethodID("getAppSign", "()Ljava/lang/String;")3. CallStaticObjectMethod → 获取 appSign jstring4. GetStringUTFChars(appSign) → appSign char*5. sub_3E5B0(appSign) → MD5(appSign).toUpperCase() → KEY(32 字符固定值)6. GetStringUTFChars(str1) → account char*7. GetStringUTFChars(str2) → switch_suggest char*8. GetStringUTFChars(str3) → timestamp char*9. SHA1 IV 常量加载 (0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0)10. sub_3EDE0() → SHA1 context 初始化11. sub_3E6A0(ctx, switch_suggest) → std::string append(注:实测不影响 SHA1 输入,见第 6 节误区)12. sub_3EE70(buf, timestamp, len) → std::string assign13. sub_3EE70(buf, account, len) → std::string assign14. sub_3E090(output, ...) → 字符串拼接(实际 SHA1 输入 = timestamp + account)15. sub_3F440(sha1_ctx, ...) → SHA1 计算(update + final)16. toUpperCase(sha1_result) → 40 字符大写 hex17. sub_3EE70(buf, KEY, len) → 拼接 KEY(32 字符)18. sub_3E5B0(sha1_upper + KEY) → MD5(72 字符).toUpperCase() → 最终 sign19. NewStringUTF(sign) → 返回 jstring
4.3 PLT thunk 对应关系
libwtf.so 使用 PLT thunk 间接调用内部函数(增加静态分析难度)。通过 GOT 表运行时解析确认:
PLT thunk 地址 | 实际函数 | 确认方式 |
sub_3E6A0 | std::string::append | IDA 交叉引用 |
sub_3EE70 | std::string::assign | IDA 交叉引用 |
sub_3E420 | strlen | 符号名 |
sub_3EDE0 | SHA1 context 初始化 | IV 常量特征 |
sub_3E5B0 | MD5 + hex + toUpperCase | hook 验证 |
sub_3F440 | SHA1::update + final | GOT 运行时解析 → 0x422A8/0x4256C |
sub_3E090 | std::string 拼接 | hook 验证 |
sub_3DC50 | memcpy | 符号名 |
4.4 关键汇编(getSign 内部 0x419e4 ~ 0x41d7c)
; SHA1 初始化419e4: movk w9, #0xc3d2, lsl #16; SHA1 第 5 个 IV 常量 0xC3D2E1F0419fc: adrp x1, 0xb1000; 常量表地址41a00: addx1, x1, #0x72c41a08: bl0x3ede0; SHA1 init; 字符串拼接41a20: movx1, x25; x25 = switch_suggest41a24: bl0x3e6a0; append(buf, switch_suggest)41a38: movx1, x28; x28 = timestamp41a3c: bl0x3ee70; assign(buf, timestamp)41a68: movx1, x26; x26 = account41a6c: bl0x3ee70; assign(buf, account); SHA1 计算41a90: bl0x3e090; combine → 最终 SHA1 输入41ac0: bl0x3f440; SHA1(input) → 20 字节 hash; toUpperCase (NEON 向量化,高效批量转换)41ac4-41c2c: ...; 20 字节 → 40 字符大写 hex; 拼接 KEY + 最终 MD541c6c: bl0x3ee70; 拼接 KEY(32 字符)41d7c: bl0x3dc50; memcpy 最终 72 字符到 MD5 输入 buffer
IDA 分析的局限:静态分析只能看到代码结构,无法确定运行时的实际数据流。例如步骤 11 的 append(ctx, switch_suggest) 看起来像是把 "1" 加入 SHA1 输入,但实际运行时 SHA1 的输入只有 timestamp+account。必须通过动态 hook 验证。
5. 无痕 Hook 动态验证(xiaojianbang_hook)
为什么必须用无痕 Hook
棒棒加固的反调试体系:
●ptrace 检测:多线程循环读取 /proc/self/status 的 TracerPid
●硬件断点检测:通过 ptrace(PTRACE_GETREGSET) 读取调试寄存器
●内存完整性校验:CRC 扫描 .text 段
●Frida 检测:扫描 /proc/maps 中的 frida-agent、检测 D-Bus 端口
●线程检测:检查是否有非预期线程(如 Frida 的 gmain 线程)
xiaojianbang_hook 的应对:
●硬件断点工作在 CPU 调试寄存器层面,不修改任何内存 → CRC 校验无效
●ptrace_spoof 模块返回假的调试寄存器状态 → 反调试看到全零
●不注入任何 SO、不创建新线程 → maps 扫描和线程检测无效
●内核态 perf_event_create_kernel_counter 注册断点 → 不占用户态 perf_event 配额
5.1 确认 getSign 三个 Java 参数
IDA 显示 getSign 接收 3 个 jstring 参数,但不确定哪个是 account、哪个是 timestamp。通过 hook GetStringUTFChars 返回后的位置,直接读取转换后的 char* 明文:
# 0x41998: 第一个 GetStringUTFChars 返回后,X0 = char* 结果xiaojianbang_hook --pid $PID --so libwtf.so --offset 0x41998 --dump-size 64# X0 dump: "6eab9142-de08-4944-888e-657068203196" → UUID 格式 = account# 0x419b8: 第二个 GetStringUTFChars 返回后xiaojianbang_hook --pid $PID --so libwtf.so --offset 0x419b8 --dump-size 64# X0 dump: "1" (1 字节) → switch_suggest# 0x419e0: 第三个 GetStringUTFChars 返回后xiaojianbang_hook --pid $PID --so libwtf.so --offset 0x419e0 --dump-size 64# X0 dump: "1780084979049" (13 字节) → timestamp
结论:getSign(account, switch_suggest, timestamp)
注意:这里 hook 的是 getSign 内部的 0x41998/0x419b8/0x419e0 地址,不是 GetStringUTFChars 函数本身。因为 GetStringUTFChars 是通用 JNI 函数,被所有 native 方法调用,直接 hook 它会导致大量无关触发甚至崩溃。hook 内部地址只在 getSign 执行时触发,安全可控。
5.2 确认 appSign 原文和 KEY
getSign 内部先调用 Java 层 LogShutDown.getAppSign() 获取一个字符串,然后对它做 MD5 得到 KEY。
# hook sub_3E5B0(MD5 函数)的第一次调用,X1 = 输入字符串xiaojianbang_hook --pid $PID --so libwtf.so --offset 0x3e5b0 --dump-size 64# X1 dump: "0093CB6721DAF15D31CFBC9BBE3A2B79" → appSign 原文(32 字符)
验证 KEY:
echo -n "0093CB6721DAF15D31CFBC9BBE3A2B79" | md5sum | awk '{print toupper($1)}'# = 85D3917DB1677696A982D3FD090D4C66
KEY = MD5("0093CB6721DAF15D31CFBC9BBE3A2B79").toUpperCase() = "85D3917DB1677696A982D3FD090D4C66"
这是一个固定值(appSign 不会变),后续所有请求都用同一个 KEY。
5.3 确认最终 MD5 输入(72 字符)
hook memcpy 调用点(0x41d7c),X1 指向即将被 MD5 处理的 72 字符明文:
xiaojianbang_hook --pid $PID --so libwtf.so --offset 0x41d7c --dump-size 96
抓到的 X1 内容:
EA2CED6D231E0DD2DEA468A785697E497FE8F99085D3917DB1677696A982D3FD090D4C66|<---------- SHA1 输出 40 字符 ---------->||<-------- KEY 32 字符 -------->|
与 ecapture 同步抓包交叉验证:
hook 捕获 72 字符: "EA2CED6D231E0DD2DEA468A785697E497FE8F99085D3917DB1677696A982D3FD090D4C66"MD5(上述 72 字符) = E35CDA00560673E28196663258526A17ecapture 抓到的 sign = E35CDA00560673E28196663258526A17完全一致
结论:sign = MD5(SHA1_40字符 + KEY_32字符).toUpperCase()
5.4 确认 SHA1 输入(关键突破)
已知 SHA1 输出 40 字符,但 SHA1 的输入是什么?IDA 静态分析显示可能是 switch_suggest + timestamp + account,但简单拼接计算不匹配。
不猜测,直接 hook 读取实际数据。
hook SHA1 计算入口(0x41ac0,即 getSign 内部调用 sub_3F440 的位置),dump X2 寄存器(SHA1 数据输入指针):
xiaojianbang_hook --pid $PID --so libwtf.so --offset 0x41ac0 --dump-size 96
X2 内存 dump:
0000: 31 37 38 30 30 38 35 3237 39 31 35 36 63 39 65|1780085279156c9e|0010: 62 37 38 34 31 2d 39 3831 66 2d 34 61 39 66 2d|b7841-981f-4a9f-|0020: 62 33 63 63 2d 66 32 3438 37 35 62 38 64 32 38|b3cc-f24875b8d28|0030: 36 00|6.|
明文:1780085279156c9eb7841-981f-4a9f-b3cc-f24875b8d286(49 字符)
●前 13 字符 = timestamp 1780085279156
●后 36 字符 = account c9eb7841-981f-4a9f-b3cc-f24875b8d286
●直接拼接,无分隔符,无 switch_suggest
5.5 同步抓取 SHA1 输入和输出(最终验证)
利用 xiaojianbang_hook 的多地址 hook 能力,同时 hook 两个点,确保抓到的是同一次函数调用的数据:
xiaojianbang_hook --pid $PID --so libwtf.so --offset 0x41ac0,0x41d7c --dump-size 96
同一次调用的配对输出:
[0x41ac0 #1] tid=5852pc=0x7574482ac0X2=0x771610cdb0 →"1780085279156c9eb7841-981f-4a9f-b3cc-f24875b8d286"(SHA1 输入)[0x41d7c #1] tid=5852pc=0x7574482d7cX1=0x7826118890 →"DB9EC3C629CCE75163AA2C78C93B82A89EFE6CF085D3917DB1677696A982D3FD090D4C66"(SHA1 输出 + KEY)
Python 验证:
import hashlibsha1_input = "1780085279156c9eb7841-981f-4a9f-b3cc-f24875b8d286"sha1_hex = hashlib.sha1(sha1_input.encode()).hexdigest().upper()# = "DB9EC3C629CCE75163AA2C78C93B82A89EFE6CF0"与 hook 抓取完全一致
算法 100% 确认。
6. 最终算法
import hashlibdef generate_sign(timestamp: str, account: str) -> str:"""川观新闻登录 sign 生成算法参数:timestamp: 13 位毫秒时间戳,如 "1780085279156"account:36 位 UUID 设备标识,如 "c9eb7841-981f-4a9f-b3cc-f24875b8d286"返回:32 位大写 MD5 hex 字符串"""# 固定 KEY(来自 Java 层 LogShutDown.getAppSign() 的 MD5)KEY = "85D3917DB1677696A982D3FD090D4C66"# Step 1: SHA1(timestamp + account),直接拼接,无分隔符sha1_input = timestamp + account# 49 字符sha1_hex = hashlib.sha1(sha1_input.encode()).hexdigest().upper()# 40 字符# Step 2: MD5(SHA1_HEX + KEY)md5_input = sha1_hex + KEY# 72 字符sign = hashlib.md5(md5_input.encode()).hexdigest().upper()# 32 字符return sign# 验证assert generate_sign("1780085279156", "c9eb7841-981f-4a9f-b3cc-f24875b8d286") \== "9C716E213830E77FFB6B314AEFF66F5B"
关键常量
名称 | 值 | 来源 |
appSign | 0093CB6721DAF15D31CFBC9BBE3A2B79 | Java 层 LogShutDown.getAppSign() 返回 |
KEY | 85D3917DB1677696A982D3FD090D4C66 | MD5(appSign).toUpperCase(),固定不变 |
分析中的误区(踩坑记录)
1. switch_suggest "1" 不参与 SHA1。IDA 反编译显示 append(ctx, switch_suggest),看起来像是把 "1" 加入 SHA1 输入。但实际 hook 验证 SHA1 输入只有 timestamp + account。原因:那个 append 是对 std::string buffer 的操作(用于后续拼接),不是对 SHA1 context 的 update。静态分析容易误判数据流方向。
2. SHA1 没有魔改。标准 SHA1(timestamp+account) 直接匹配。
3. account 不是手机号。account 是 UUID 格式的设备/会话标识符(每次安装生成),不是用户输入的手机号。手机号在 data JSON 字段中,不参与 sign 计算。
7. 无痕 Hook (xiaojianbang_hook) 的优势
能力 | Frida/Xposed | xiaojianbang_hook |
代码修改 | 修改 .text 段(被 CRC 检测) | 零修改(硬件断点) |
内存注入 | 注入 agent SO(被 maps 检测) | 零注入 |
线程创建 | 创建 gmain 等线程(被检测) | 零线程 |
调试器痕迹 | TracerPid 非零 | ptrace_spoof 返回零 |
断点检测 | — | 假账本欺骗 GETREGSET |
多地址 hook | 无限制 | 硬件上限 6 个 |
性能开销 | 较高(解释执行) | 极低(CPU 硬件比较器) |
xiaojianbang_hook 的核心优势:在最高对抗强度下仍然有效。 当 Frida/Xposed 全部被检测封杀时,xiaojianbang_hook 是唯一可用的动态分析手段。
8. 完整工作流
ecapture 抓包 → 确定目标字段(sign)↓定制系统脱壳 → jadx 反编译 → 定位 native 函数↓MemDumper dump 加密 SO → IDA 静态分析 → 梳理算法结构↓xiaojianbang_hook 逐步验证 → 确认每一步的实际输入输出↓算法还原 + ecapture 交叉验证 → 完成
每一步都在内核态或系统态完成,目标 App 全程无感知。
GitHub: https://github.com/xiaojianbang8888/xiaojianbang-stealth-hook
内核无痕hook,完整实现过程的文章会放在知识星球。
星球主要发高质量文章,爬虫、逆向、安卓系统定制、指纹浏览器相关内容。立足AI时代,持续输出可直接落地的高质量技术方案、实操教程和行业干货。可以喂给AI,省token的绝佳方案。
Outlook微软邮箱注册机、DomTrace工具、小肩膀安卓定制系统也都会放在星球。星球文章的md格式也更好看些。
知识星球限时八折优惠券链接(仅限前100人噢):
https://t.zsxq.com/FPcMZ



官网:https://xjbedu.site/https://xjbedu.site/proxy介绍网址:https://xjbedu.site/tokentoken站网址(claude和openai):https://xjbtoken.site/欢迎重度AI用户来体验尝鲜。

小肩膀本人联系方式

夜雨聆风