某论坛类软件的逆向
前言
感慨一下AI的强大,让以前很费时间和和很大难度变得很简单
1.逆向登录(主要逆向登陆的加密算法)
一般来说登陆的密码加密的过程是
1 | loginByPassword |
用jadx打开该软件发现源代码的类名都被混淆成各个字母,但是Android这个类名还是很显眼的,仅是将类名混淆,方法名一般便于阅读不好混淆,直接查找 loginByPassword,看一下那个方法引用这个函数
找到了login方法(这很明显),该方法中检验到空行会有NullPointerException异常报告
1 | PorteH5logUtils.INSTANCE.report("porte", "password_login", o0.M(C17133r0.a("msg", username))); |
这个函数记录登录的日志
1 | loginByPassword(activity, "", username, password, iLoginCallback); |
这个应该就是加密的核心函数,进入这个函数
1 | HashMap<String, Object> mapM = o0.M(C17133r0.a(LoginFragmentsKt.ARG_PARAM_ACCOUNT, rSAUtils.encryptByPublicKey(str2)), C17133r0.a("password", rSAUtils.encryptByPublicKey(str3))); |
核心是rsa加密,要找到公钥
而rsa的公钥是(n,e),其加密为y = xe mod n
但是找到的密钥确是字符串,其由公钥加密的过程如下
1 | n (128字节) + e (65537) |
再根据
1 | byte[] bytes = data.getBytes(C16501g.f587422b) |
这是对于明文的加密就清楚了,明文->字节->rsa加密->base64加密
当然解密需要的私钥是在客户端,我这边肯定拿不到,这里只是简单学习一下登陆的加密和解密算法
1 | `python |
2.frida检验绕过
2.1过frida检验中使用到的工具
| 工具 | 版本 |
|---|---|
| Frida | 17.17.0(高版本纯是因为一次更新将手机从Android13更到Android16只有高版本适配) |
| Capstone | python 版(arm64 反汇编) |
| apksigner / zipalign | |
| java / keytool | java17\jdk-17.0.17+10\bin` |
| apktool | apktool.bat (2.12.1) |
| 设备 | Pixel 6 (oriole), Android 16, rooted |
2.2对于libmsaoaidsec.so中检验函数的总览
| 维度 | 内容 |
|---|---|
| 检测载体 | libmsaoaidsec.so(MSA OAID SDK 安全组件,JNI 类名 NagaLinker v8.83,688KB arm64 stripped) |
| 引入方 | com.xxx.platform.oaidkit.OAIDInitializer → Nd.g.loadLibrary → System.loadLibrary("msaoaidsec") |
| 触发时机 | 启动早期后台线程(Thread-2),linker64 __dl_soinfo::call_constructors 阶段 |
| 检测代码位置 | DT_INIT 构造器 _init @ 0x14400(.init_array 6 项全为 0,构造并入 _init),控制流平坦化状态机 |
| 自杀机制 | _exit(0) 干净自杀 + SIGSEGV 野指针崩溃兜底(双保险) |
| 绕过结论 | 静态 .so 补丁 + 重签名 不可行(运行时签名自校验);root 替换 .so(保原签名)可行且有效 |
2.3调用链与backtrace
调用链
1 | Thread-2 (tid=11100) |
backtrace
1 | #00 pc 0x0020d10 libmsaoaidsec.so ← 崩溃触发点:检测分支/非法指针解引用(兜底崩溃) |
既然检测发生存在native层,那就要要看一下so文件是怎么加载的
System.loadLibrary(“msaoaidsec”) -通过JNI—> JVM_NativeLoad (libopenjdkjvm.so) ->linker64动态链接器
当然在原来的包中并没有直接的System.loadLibrary(“msaoaidsec”) ,msaoaidsec在原来包中进行了字符串混淆(无法通过jadx直接找到),只要在执行程序后才复原
2.4检测机制分析
首先看一下这些检测函数都在

检测函数都在init_process函数中
检测函数清单与顺序
| # | 通道 | 字符串 | 函数 | 判定逻辑 | 证据 |
|---|---|---|---|---|---|
| 1 | 线程 state 反附加 | /proc/%d/task/%s/stat @0x3051b |
sub_1B730 |
逐线程读 stat,state 字符 't'(TASK_TRACED)/'T'(TASK_STOPPED) → 返回 777 |
反编译逐条对应(`(s |
| 2 | maps 特征扫描 | /proc/self/maps、/proc/%u/maps @0x2f6b0 |
0x20bdc(崩溃点)、0x18a0c、0xf438、0x167a8 | 扫 frida/gum/linjector 映射 | stream_1 = fopen(“/proc/self/maps”, “r”) 打开maps并解析每行内容返回给回调函数 |
| 3 | fd 句柄扫描 | /proc/self/fd/%s @0x305e7、fd/%d @0x3098e |
0x1c168、0x25ce4 | 遍历 fd 查 frida socket/pipe | 字符串引用定位 |
| 4 | cmdline 检查 | /proc/%d/cmdline @0x2f838 |
_init 块 0x14788(+0x3CC)、0x11f38、0x1b144、0x23748、0x23b38、0x2703c |
fopen(...,"r") + strchr(':') |
运行时首个 /proc 访问 |
| 5 | 模块枚举 | 导入 dl_iterate_phdr |
— | 遍历已加载 .so 匹配 frida 特征 | 动态符号表 |
| 6 | linker 完整性 | /system/bin/linker64 等 @0x306fd |
0x2082c(被 0x20bdc 调) | 校验当前 linker 路径 | 引用 @0x20850/0x20890 |
| 7 | 自研 ELF 加载器 | LIBVIEW! @0x2f68a、Can't find library file %s 等 |
0xd1f8、0xdf74 等 | 自行加载/校验库,识别伪装 | 字符串+候选库名 opqrstuvwxyz.so/libhdog.so 等 |
| 8 | 系统属性 | ro.build.version.sdk @0x2f8d2、persist.sys.dalvik.vm.lib @0x2f937、security_patch |
0x123f0、0x12550、0x12440 | 环境/运行时识别 | 反汇编 |
| 9 | 模拟器 | /data/dalvik-cache/x86、/system/fake-libs/ |
0x23748、0x18b3c | 模拟器/fake 库特征 | 字符串 |
| 10 | 加密 VM 载荷 | 导入 inflate/crc32 |
全库 | 部分检测逻辑在 zlib 解压的 VM 字节码中动态展开 | **动态符号表 |
线程 state 反附加
1
2if ( ((unsigned __int8)s_[(unsigned int)(v11 + 1)] | 0x20) == 0x74 && s_[(unsigned int)(v11 + 2)] == ' ' )
return 777;6.linker完整性检验
1
2
3
4stream_3 = sub_20B04("/system/bin/linker64");
if ( stream_3
|| (stream_3 = sub_20B04("/bionic/bin/linker64")) != 0
|| (stream = (FILE *)sub_20B04("/apex/com.android.runtime/bin/linker64"), (stream_3 = (__int64)stream) != 0) )8.系统检验
1
2_system_property_get("ro.build.version.sdk", nptr_);
_system_property_get("ro.build.version.release_or_codename", s);检测Android版本号
1
2
3if ( !(unsigned int)_system_property_get("persist.sys.dalvik.vm.lib", haystack) || *off_47FB8 >= 21 )
_system_property_get("persist.sys.dalvik.vm.lib.2", haystack)
这是检测jvm,吗获取 Dalvik/ART 虚拟机的库文件路径
9.模拟器检测
1 | if ( sscanf(s__1, "%lx-%lx %s %lx %s %ld %s", &v24, &v23, &v28, &v21, &v26, &v22, s) == 7 |
检测执行顺序
| 顺序 | 状态值 | 动作 |
|---|---|---|
| 1 | 0xa0ba64ce |
栈上分配 0x7d0 缓冲区 |
| 2 | 0x000e3ba313 |
调 0x123f0(读 ro.build.version.sdk)、0x12550(读 persist.sys.dalvik.vm.lib),存全局 @0x47000+0xfb8 |
| 3 | 0x1627d0c0 |
调 0x12440(属性二次检查) |
| 4 | 0x9f46953a |
cmp #0x17 置 SDK 版本标志 |
| 5 | 0x9c5eb96f |
按标志分支 |
| 6 | 0x76541a4f |
置全局标志 @0x47000+0xed8 |
| 7 | 0xbb1e2113 |
调 0x25a48 早期门控;tst w0,#1 → 奇数则 _init 提前正常返回 |
| 8 | 0x5b4c847c |
缓冲区初始化 |
| 9 | 0x23d89166 |
cmdline 检查(/proc/%d/cmdline,运行时首个 /proc 访问) |
| 10 | 0x65c6d61d |
拷贝 + strchr(':') 解析 |
| 11 | 0x7f89508c |
分支 |
| 12 | 0x398f5683 |
调 0x1bec4 |
| 13 | 0xb945dffa→0x19437fab |
调 0x13728、0x23ad4 |
| 14 | 0xf4587fb0 |
调 0xc830(决定是否进入主检测) |
| 15 | 0x01fb9186 |
cmp #1:=1→进入主检测 0x95c8;否则走 0x9150 出口 |
| 16 | 0x8b2a9a8c |
调 0x95c8 主检测函数(崩溃栈 #03 所在块) |
0x95c8 主检测内部:0x95c8 → 0x11f38 → 0x20bdc(maps+linker+崩溃点 0x20d10);另经函数指针表间接分发调用 0x18a0c(maps)、0x1b730(线程 state)、0x1c168/0x25ce4(fd)。
总的检测顺序:属性预检 → SDK 版本门控 → 早期门控 0x25a48 → cmdline → maps →(0x1bec4/0x13728/0x23ad4/0xc830 门控链)→ 主检测 0x95c8(cmdline 复查 → maps+linker → 线程 state → fd)→ 判定异常 _exit(0) + SIGSEGV 兜底。
自杀机制
| 步骤 | 行为 | 证据 |
|---|---|---|
| 8a | _exit(0) 干净自杀 |
spawn 日志 ### DETECT: _exit(0x0) @ thread=11102 |
| 9a | 野指针解引用 SIGSEGV 兜底 | tombstone fault addr 0x1aa568 = x20(0x1aa3e0)+0x188,0x20d10: ldrb w8,[x20,#0x188](链表遍历中故意解引用) |
崩溃栈(frida spawn 触发):
1 | #00 0x20d10 → #01 0x11f64 → #02 0x95f8 → #03 0x14824(_init) → #04 linker64 call_constructors |
寄存器 x23-x28 与 _init 状态机魔数 0xbb1e2113/0x0e3ba312/0xde366c1c/0x19437faa/0x4f03bfb2 逐一对应,证明崩溃发生在 _init 内。
2.5通过脚本绕过
总的来说多余Frida
1 | frida 足迹(被检测到的现象) |
解决办法是让检测永远输出未检测到
把检测函数本身改成 mov w0, #0; ret——检测逻辑还没算,直接返回”未检测到”:
0x1b730 线程 state 检查 → mov w0,#0; ret 恒返回 0(原来 frida 下会返回 777)
0x1c168 fd 扫描 → mov w0,#0; ret 恒返回 0
0x25ce4 fd 扫描 → mov w0,#0; ret 恒返回 0
hook脚本的是附件 patch_libmsaoaidsec.py
spawn_detect.js主要是定位检测函数的脚本
catch_exit.js是为了、”进程被杀日志丢失”问题
2.6 签名校验(只是简述)
patch_libmsaoaidsec.py会生成补丁后的so文件,而repack_apk.py是将**补丁后的so文件塞入apk中,这就导致了签名发生改变,所以要进行一定操作让签名保持不变
流程:解包 → patch libmsaoaidsec.so → 重打包(zipfile 剔除旧签名)→ zipalign → apksigner 签名(v2+v3)→ adb install。
主要思路:.so 在 APK 中为 deflate 压缩(extractNativeLibs=true),系统安装时解压到 /data/app/.../lib/arm64/。root 直接替换已安装的 .so,签名保持原版(绕过签名自校验),补丁库消除检测。
