前言

感慨一下AI的强大,让以前很费时间和和很大难度变得很简单

1.逆向登录(主要逆向登陆的加密算法)

一般来说登陆的密码加密的过程是

1
2
3
4
5
6
7
8
9
loginByPassword
getRSAKey
geetest ← 极验验证码相关
rsa_public_key
password
MIGf ← RSA 公钥 DER 的 Base64 前缀(1024位)
MIIBIj ← RSA 公钥前缀(2048位)
Cipher
PKCS1

用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
2
3
4
5
6
7
8
9
10
11
12
13
14
n (128字节)  +  e (65537)
│ ASN.1 DER 逐层包裹
▼
30 81 9f ─ SEQUENCE(外层)
├─ 30 0d ─ SEQUENCE(算法标识)
│ ├─ 06 09 2a864886f70d010101 ─ OID
│ └─ 05 00 ─ NULL
└─ 03 81 8d 00 ─ BIT STRING
└─ 30 81 89 ─ SEQUENCE(RSAPublicKey)
├─ 02 81 81 00 + n ─ INTEGER n
└─ 02 03 01 00 01 ─ INTEGER e (=65537)
│ Base64
▼
"M************************************B"

再根据

1
2
byte[] bytes = data.getBytes(C16501g.f587422b)
CryptoExtendKt.toBase64String(encryptByPublicKey(bytes, CryptoExtendKt.fromBase64("公钥")

这是对于明文的加密就清楚了,明文->字节->rsa加密->base64加密

当然解密需要的私钥是在客户端,我这边肯定拿不到,这里只是简单学习一下登陆的加密和解密算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
`python
from cryptography.hazmat.primitives.asymmetric import rsa, padding
import base64

# ===== 密钥占位:公钥 / 私钥 =====
私钥 = rsa.generate_private_key(public_exponent=65537, key_size=2048)
公钥 = 私钥.public_key()

# ===== 1. 加密(客户端,持有【公钥】)=====
def encrypt_by_public_key(明文, 公钥):
密文 = 公钥.encrypt(明文.encode('utf-8'), padding.PKCS1v15())
return base64.b64encode(密文).decode()

# ===== 2. 解密(服务端,持有【私钥】)=====
def decrypt_by_private_key(密文, 私钥):
明文 = 私钥.decrypt(base64.b64decode(密文), padding.PKCS1v15())
return 明文.decode('utf-8')

# ===== 3. 往返 =====
结果 = decrypt_by_private_key(encrypt_by_public_key('测试密码', 公钥), 私钥)
print(结果) # 测试密码

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
2
3
4
5
6
7
8
9
10
11
12
13
Thread-2 (tid=11100)
└─ com.xxx.platform.oaidkit.OAIDInitializer.initOAID$lambda-1
└─ com.xxx.platform.oaidkit.OAIDInitializer.a
└─ com.xxx.platform.oaidkit.c.run
└─ Nd.f.f → Nd.f.i → Nd.f.j
└─ Nd.g.loadLibrary
└─ java.lang.System.loadLibrary("msaoaidsec")
└─ Runtime.loadLibrary0
└─ JVM_NativeLoad
└─ android_dlopen_ext
└─ linker64 __dl_do_dlopen
└─ __dl_soinfo::call_constructors
└─ libmsaoaidsec.so (.init_array 构造器) ← 检测在此执行

backtrace

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#00 pc 0x0020d10  libmsaoaidsec.so                    ← 崩溃触发点:检测分支/非法指针解引用(兜底崩溃)
#01 pc 0x0011f64 libmsaoaidsec.so ← 检测逻辑内部调用
#02 pc 0x00095f8 libmsaoaidsec.so ← 检测逻辑内部调用(VM/混淆块)
#03 pc 0x0014824 libmsaoaidsec.so ← _init 状态机入口(构造器)
#04 linker64 (__dl_soinfo::call_constructors+556) ← 链接器执行该 so 的构造器
#05 linker64 (__dl_do_dlopen) ← dlopen 主流程
#06 linker64 (__dl_dlopen_ext) ← dlopen 扩展入口
#07 libdl.so (android_dlopen_ext+16) ← libdl 转发给 linker64
#08 libnativeloader.so (NativeLoaderNamespace::Load) ← 系统 native loader:选择命名空间
#09 libnativeloader.so (OpenNativeLibrary) ← 打开 native 库
#10 libart.so (JavaVMExt::LoadNativeLibrary+832) ← ART 虚拟机加载 native 库入口
#11 libopenjdkjvm.so (JVM_NativeLoad+368) ← JNI 层调用系统加载
#12-14 boot.oat (Runtime.loadLibrary0) ← Java 层 ART 运行时
#15 boot.oat (java.lang.System.loadLibrary+84) ← ★ Java API:加载库
#16 libart.so (nterp_helper) ← ART 解释器(由解释执行进入)
#17 classes9.dex (Nd.g.loadLibrary+6) ← ★ 混淆加载器:构造 "msaoaidsec" 并调用 loadLibrary
#18 libart.so (nterp_helper) ← ART 解释器
#19 classes9.dex (Nd.f.j+48) ← 混淆类 Nd.f 内部(分派链)
#20 libart.so (nterp_helper)
#21 classes9.dex (Nd.f.i+38) ← 混淆类 Nd.f 内部
#22 libart.so (nterp_helper)
#23 classes9.dex (Nd.f.f+2) ← 混淆类 Nd.f 内部
#24 libart.so (nterp_helper)
#25 classes4.dex (OAIDInitializer.initOAID$lambda-1+150) ← OAID 初始化 lambda
#26 libart.so (nterp_helper)
#27 classes4.dex (OAIDInitializer.a+6) ← OAID 初始化辅助方法
#28 libart.so (nterp_helper)
#29 classes4.dex (com.mihoyo.platform.oaidkit.c.run+26) ← 后台线程 run()
#30 boot.oat (java.lang.Thread.run+64) ← 线程入口
#31 libart.so (art_quick_invoke_stub) ← ART 方法调用桩
#32 libart.so (art::ArtMethod::Invoke) ← ART 反射/调用方法
#33 libart.so (art::Thread::CreateCallback) ← 线程创建回调
#34 libart.so (art::Thread::CreateCallbackWithUffdGc) ← 线程创建(带 GC)
#35 libc.so (__pthread_start) ← pthread 线程入口
#36 libc.so (__start_thread) ← 线程启动

既然检测发生存在native层,那就要要看一下so文件是怎么加载的

System.loadLibrary(“msaoaidsec”) -通过JNI—> JVM_NativeLoad (libopenjdkjvm.so) ->linker64动态链接器

当然在原来的包中并没有直接的System.loadLibrary(“msaoaidsec”) ,msaoaidsec在原来包中进行了字符串混淆(无法通过jadx直接找到),只要在执行程序后才复原

2.4检测机制分析

首先看一下这些检测函数都在

1

检测函数都在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 字节码中动态展开 **动态符号表
  1. 线程 state 反附加

    1
    2
    if ( ((unsigned __int8)s_[(unsigned int)(v11 + 1)] | 0x20) == 0x74 && s_[(unsigned int)(v11 + 2)] == ' ' )
    return 777;

    6.linker完整性检验

    1
    2
    3
    4
    stream_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
    3
    if ( !(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
2
if ( sscanf(s__1, "%lx-%lx %s %lx %s %ld %s", &v24, &v23, &v28, &v21, &v26, &v22, s) == 7
&& strstr(s, "/data/dalvik-cache/x86") )

检测执行顺序

顺序 状态值 动作
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
2
3
4
5
6
7
8
9
10
11
frida 足迹(被检测到的现象)
├─ 线程被 ptrace 停止 → 0x1b730 读 /proc/<pid>/task/<tid>/stat → state='t'/'T' → 返回 777
├─ frida 的 fd/socket → 0x1c168/0x25ce4 读 /proc/self/fd/* → 发现 frida 句柄
├─ frida 的内存映射 → maps 扫描回调(0x18a0c 的 a1+24) → 命中 rwxp/路径
└─ 模块枚举 dl_iterate_phdr → 找到 frida-agent 模块
│ 任一命中
▼
置全局"检测到"标志
│
▼
_exit(0) 干净自杀 + SIGSEGV 野指针兜底(0x20d10 链表遍历故意解引用)

解决办法是让检测永远输出未检测到

把检测函数本身改成 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,签名保持原版(绕过签名自校验),补丁库消除检测。