GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

应用安全 --- 安卓加固 之 绑企加固

简介:

https://dev.bangclexxx.com/home/introduction

 

加固样本:

雅玛逊购物  万年理 7.3.7  油正懒投  某数TV ,需要LD过签,而且不支持Android15 7.2.2.0.apk_123云盘免登录下载不限速  辛巴克  智已汽车  卖档佬  中果英航  易懂哀家  某雷  森空岛在某一个版本使用了绑企,但是之后自己去除了,是最好的研究样本

  

特征:

这个软件有多个版本,不同版本加密的方法不一样

202411旧版本特征文件

  libDexHelper-x86.so  这是解密dex的x86版本
  libDexHelper.so    这是解密dex的arm版本
  libdatajar.so      这是加密后的dex

2025新版特征

image

 壳入口

image

 

对抗:

1.https://56.al/ 上传apk脱dex

2.将脱ke后的dex批量修复dex并重命名classes{2}.dex

3.将脱ke后的dex加入apk中,注意不要全程不要勾选自动签名!!!!!!!

4.打开第一个dex,找到H类,将app名称和acf名称复制出来,打开AndroidManifest.xml替换掉

例如

<application
android:name="com.youloft.core.CApp"
android:appComponentFactory="androidx.core.app.CoreComponentFactory"

删除

        <provider

            android:name="com.secneo.apkwrapper.CP"

            android:exported="false"

            android:authorities="cn.amazon.mShop.android.CP"

            android:initOrder="2147483647" />

5.删除lib下的两个特征  libDexHelper.so   libDexHelper-x86.so  和assets下的meta-data目录

6.去签名校验并勾选签名

 

原理分析:

libDexHelper.so原理

 Java层入口 → init函数 → JNI_OnLoad → DEX解密 → DEX加载 → 反调试处理 

步骤1:入口定位

1. 检查 AndroidManifest.xml 中的 Application 类
2. 找到 attachBaseContext 方法
3. 发现加载了 libDexHelper.so

步骤 2:init 函数分析(SO 初始化)

遇到的问题与解决

问题解决方法
F5 反编译失败 修正函数参数声明
sp-analysis failed Options → General → Stack pointer,找到破坏堆栈的指令并 NOP

init 函数核心逻辑

void init_proc() {
    // 步骤1: mmap 内存,拷贝 seg000 段内容
    sub_D0F50("");
    
    // 步骤2: 解密代码段和数据段
    sub_D0C30("", &dword_103D4, v0);
}

Dump 解密代码的方法

在 sub_D0C30 函数末尾的两个 mprotect 处下断点:
├── 第1个 mprotect: 修改代码段权限 → dump 解密后的代码段
└── 第2个 mprotect: 修改数据段权限(含GOT表) → dump 解密后的数据段

步骤 3:JNI_OnLoad 分析(绕过 OLLVM)

OLLVM 混淆处理流程

1. 使用 trace 脚本记录所有执行的指令
2. 过滤出真实执行的基本块
3. 按执行顺序分析每个真实块
4. 在关键块下断点进行动态分析

RegisterNatives 注册的方法

Native方法              对应函数
────────────────────────────────
"attach"        →      sub_2775C
"b"             →      sub_17210
"c"             →      sub_18B10
"d"             →      sub_20384
"e"             →      sub_224A0
"f"             →      sub_1CE90  (关键: makeDexElements)
... (共18个方法)

步骤 4:DEX 解密

解密流程图

┌─────────────────────────────────────────────────────────────┐
│                    DEX 解密过程                              │
├─────────────────────────────────────────────────────────────┤
│  1. 反射获取 java/lang/Class                                │
│           ↓                                                 │
│  2. 获取 dexCache 对象                                      │
│           ↓                                                 │
│  3. 反射获取 java/lang/DexCache 的 dexFile                  │
│           ↓                                                 │
│  4. GetIntField 转换为 native 指针                          │
│           ↓                                                 │
│  5. 指针+4 → 壳 DEX 在内存中的位置                           │
│           ↓                                                 │
│  6. 匹配特征找到 injected_data                              │
│           ↓                                                 │
│  7. 调用解密函数解密 DEX                                    │
└─────────────────────────────────────────────────────────────┘

额外资源解密:
• v1filter.jar: 从 assets/resthird.data 解密释放到 .cache 目录

步骤5:DEX 加载(Hook 技术)

Hook 点与作用

┌────────────────────────────────────────────────────────────────────┐
│  Hook 目标                              │  作用                    │
├────────────────────────────────────────────────────────────────────┤
│  art::DexFileVerifier::verify          │  恒返回 true,跳过验证    │
│  art::ClassLinker::OpenDexFilesFromOat │  拦截 DEX 加载,注入逻辑  │
└────────────────────────────────────────────────────────────────────┘

Hook 特征识别:
    BX PC           ; 保证 LDR 指令 4 字节对齐
    LDR PC, [PC-4]  ; 跳转到 fake 函数
    DCD fake_addr   ; fake 函数地址

DEX 加载流程

1. 在 .cache 目录创建 10 个 DEX 文件 (仅写 4 字节 magic)
2. 调用 native 方法 "f" (sub_1CE90)
3. 调用 makeDexElements 加载 DEX
4. OpenDexFilesFromOat 被 Hook,进入 fake 函数 sub_433E8:
   ├── v1Filter.jar → 调用原 OpenDexFilesFromOat (文件加载)
   └── classes.dex  → 调用 OpenMemory (内存加载)
5. 替换原 ClassLoader 的 dexPathList.dexElements

Dump DEX 的方法

Python
# 在 OpenMemory 处下断点,dump 所有 DEX
# 使用 dex2apk 工具合并 DEX 为 APK

步骤6:反调试与检测机制

检测机制汇总

┌─────────────────────────────────────────────────────────────────────┐
│                        检测类型                                      │
├─────────────────────────────────────────────────────────────────────┤
│  【Magisk 检测】                                                    │
│   • 检查进程名是否为 ":bbs:com.secneo.apkwrapper.r.S"               │
├─────────────────────────────────────────────────────────────────────┤
│  【Frida 检测】                                                     │
│   • /proc/self/task/<tid>/status 的 name 字段:                      │
│     - "gum-js-loop"                                                │
│     - "gmain"                                                      │
│   • /proc/self/fd/<fd-id> 链接内容:                                 │
│     - "linjector"                                                  │
├─────────────────────────────────────────────────────────────────────┤
│  【反调试】                                                         │
│   • Hook ptrace → fake 函数调用 svc kill                           │
│   • inotify_add_watch 监控 /proc/self/mem                          │
│   • 检测 /proc/self/status 和 /proc/self/task/<tid>/status:        │
│     - "T (stopped)"                                                │
│     - "(zombie)"                                                   │
│     - "t (tracing stop)"                                           │
│   • 父子进程管道通信验证 (写入/读取 0xFF)                            │
└─────────────────────────────────────────────────────────────────────┘

反调试架构

            ┌─────────────┐
            │   父进程     │
            ├─────────────┤
    fork    │ • Hook ptrace
    ─────→  │ • 监控 /proc/self/mem
            │ • 检测进程状态
            │ • 管道通信验证
            └──────┬──────┘
                   │ pipe 通信
            ┌─────────────┐
            │   子进程     │
            ├─────────────┤
            │ • 监控 /proc/self/mem
            │ • 检测进程状态
            │ • 管道通信验证
            │ (不 Hook ptrace)
            └─────────────┘

步骤 7:后续处理

JNI_OnLoad 完成后:
Java 层 AW.attachBaseContext
调用 H.attach (sub_2775C)
• 资源替换
• Application 替换

 绕过方法总结

保护机制绕过方法
OLLVM 混淆 Trace 脚本 + 真实块分析
代码加密 mprotect 断点 dump
DEX 加密 OpenMemory 断点 dump
Frida 检测 使用 hluda 的 spawn 模式
ptrace 反调试 Frida attach 会崩,需用其他方法
状态检测反调试 编译修改内核绕过

 关键断点位置

┌──────────────────────────────────────────────────────────────┐
│  地址/函数                    │  用途                        │
├──────────────────────────────────────────────────────────────┤
│  sub_D0C30 的 mprotect       │  dump 解密后的代码/数据段     │
│  0x27366                     │  DEX 解密函数调用处           │
│  OpenMemory                  │  dump 解密后的 DEX            │
│  0x2A024 RegisterNatives     │  获取 native 方法映射         │
└──────────────────────────────────────────────────────────────┘

这个分析流程适用于类似的 VMP 壳,核心思路是:找加密点 → dump 解密数据 → 绕过反调试 → 分析业务逻辑。

 

 

 

 

 

 

 

 

 

1.找到java入口,检查Application类的attachBaseContext方法,或者其他方法也可能出现,方法内部会反射调用so文件。

2.找到c的入口,我们发现入口是init_proc方法,

void init_proc()
{
    sub_D0F50("");    // ① mmap内存,把seg000段内容拷贝过去
    sub_D0C30(...);   // ② 解密代码段和数据段
}

3.解密so方法

sub_D0C30函数结尾两个mprotect调用处
    ├─ 第1个mprotect → 修改代码段权限 → 可dump解密后代码段
    └─ 第2个mprotect → 修改数据段权限(含GOT表) → 可dump解密后数据段

4.注册jni函数

0x2A024 调用 RegisterNatives,注册com/secneo/apkwrapper/H类的所有native方法(attach, b, c, d等13个方法)  ,建立 Java 方法 ↔ Native 函数映射

Java方法Native函数
attach sub_2775C
b sub_17210
c sub_18B10
d sub_20384
e sub_224A0
f sub_1CE90

5.找到关键解密dex方法

解密DEX内存数据

0x2A030: 调用sub_265E8
0x2721C: 调用sub_49604, 通过反射获取当前壳DEX在内存中的位置
    ├─ 反射获取 java/lang/Class 的 dexCache
    ├─ 反射获取 java/lang/DexCache 的 dexFile
    ├─ GetIntField 转为native指针
    ├─ 指针+4字节 → 壳DEX内存位置
    └─ 匹配特征找到 injected_data(这是壳厂商在DEX中注入的加密数据区)
0x27366: 调用解密函数,传入 injected_data 中某个位置的加密数据,解密算法将加密数据原地解密成真正的DEX文件 → 【可dump DEX】
0x26E6A: access检查v1filter.jar是否存在
0x26D26: 解密assets/resthird.data → 释放v1filter.jar(这是一个工具类DEX,包含壳需要用到的辅助类和方法,在后续加载真实DEX时会用到)

解密DEX是获取真实APP代码,释放v1filter.jar是获取壳的辅助工具,两者配合完成完整的脱壳加载流程。

6.加载dex

0x273C6: 调用sub_3FA6D
    ├─ 0x44A52: Hook art::DexFileVerifier::verify
    │           → 恒返回true(跳过验证)
    ├─ 0x426BA: Hook art::ClassLinker::OpenDexFilesFromOat
    │           → fake函数: sub_433E8
    └─ dlsym获取 art::DexFile::OpenMemory 地址
    
    
0x26756: 调用sub_1A7C4
    └─ 对每个classes.dex在.cache创建文件(仅写4字节magic)
    
    
0x1AE44: 调用native方法f
    ├─ 调用makeDexElements加载dex
    └─ 替换classloader的dexElements

sub_433E8(fake函数)逻辑:

判断加载目标
    ├─ v1Filter.jar → 0x435EE调用原函数(文件加载)
    └─ classes.dex → 0x4352E找到dex数据
                   → 0x43568调用OpenMemory(内存加载)
                   → 【在此处下断点可dump所有DEX】

 

 

需要处理的可能错误情况

1.ollvm控制流混淆

步骤1: 识别平坦化混淆
步骤2: 使用trace脚本记录指令日志
步骤3: 过滤虚假块,提取真实块
步骤4: 逐块分析真实逻辑

Trace输出示例:

index: 0, start ea: 0x28868
index: 2, start ea: 0x288ae
...
index: 118, start ea: 0x2ac46

2.堆栈不平衡,开启Stack pointer视图,找到破坏堆栈的指令并NOP掉

3.参数不一致,F5失败(0xD0688) 修正函数参数声明为 void __fastcall sub_D0C30(_DWORD *a1, int a2, int a3)

4.4 框架检测和反调试

4.4.1 Magisk检测

text
位置: 0x2AB2C
检测方式: 检查进程名是否为 ":bbs:com.secneo.apkwrapper.r.S"

4.4.2 Frida检测

text
位置: 0x5fd12 → sub_5A088

检测点1: /proc/self/task/<tid>/status
         └─ Name字段匹配 "gum-js-loop"

检测点2: /proc/self/task/<tid>/status  
         └─ 匹配 "gmain"

检测点3: readlink /proc/self/fd/<fd-id>
         └─ 匹配 "linjector"

4.4.3 反调试机制

text
┌─────────────────────────────────────────────────────────────┐
│                      反调试架构                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   0x5fd00/0x5fd06: 创建两个pipe用于父子进程通信              │
│                          ↓                                  │
│   0x5f8ee: fork()                                          │
│         ┌──────────┴──────────┐                            │
│      父进程                  子进程                          │
│         │                      │                            │
│   ┌─────┴─────┐          ┌─────┴─────┐                     │
│   │           │          │           │                     │
│   ▼           ▼          ▼           ▼                     │
│ Hook       开线程      同父进程    管道通信                   │
│ ptrace     ×2         (不hook     验证                      │
│   │         │         ptrace)                               │
│   ▼         ▼                                              │
│ fake函数  ┌──┴──┐                                          │
│ kill进程  │     │                                          │
│           ▼     ▼                                          │
│    inotify    读取State字段                                  │
│    监控mem    匹配:                                         │
│               - "T (stopped)"                              │
│               - "(zombie)"                                 │
│               - "t (tracing stop)"                         │
│                                                             │
│   管道通信: 写入0xFF,读取验证,非0xFF则kill                   │
└─────────────────────────────────────────────────────────────┘

 

加固技术总结

技术类型实现方式
代码保护 OLLVM平坦化混淆
DEX保护 加密存储 + 内存加载
完整性校验 Hook DexFileVerifier::verify
反调试 双进程守护 + ptrace检测 + 状态监控
反Frida 进程名/线程名/fd特征检测

绕过方案

保护类型绕过方法
DEX加密 在OpenMemory处下断点dump
反Frida 使用hluda的spawn模式
反调试 编译修改内核绕过

 

 

 

 

libdexjni.so原理

libdexjni.so也是一个特征文件。实际上 libdexjni.so在不同的APP中体积会不一样,应该是硬编码写入key和自定义指令导致的

这个加固的 VMP 属于 DEX-VMP ,原理大致是

1.将每个java中的activity类的onCreate方法的字节码转义为自定义字节码保存在so文件内部(630加固会将java 函数为 native,邦加固会反射)。

2.执行时从so中获取 自定义字节码数据 传入内部 vmpEntry 解析器执行。

3.最终调用  jni 方式实现c语言调用java代码执行。

 

so使用了vmp、ollvm和加密壳。虽然使用了最强加密方法,但是还是有迹可循,

问题1,这个vmp没有完美实现虚拟化,只是利用了jni简化执行代码。我们可以使用黑盒方法猜测还原代码而不关心内部是如何实现的。一个自定义操作字节码对应一个jni函数,jni函数可以猜测这个smali指令。

问题2,同一个版本的指令映射表应该是一样的

我们的目的就是找到这个自定义指令和jni码表

 

流程图

┌─────────────────────────────────────────────────────────────┐
│                     VMP 解密执行流程                         │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 步骤1: 读取加密的opcode                                      │
│        LDRH R6, [R6]        ; R6 = 0x1000 (加密值)                │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 步骤2: 计算key地址                                           │
│        R2 = 当前insn地址 - insn基址                          │
│        R7 = key表基址                                        │
│        key = [R7 + R2]      ; key = 0x89                        │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 步骤3: XOR解密                                               │
│        EOR.W R1, R2, R6    ; R1 = 0x89 XOR 0x1000 = 0x1089    │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 步骤4: 计算handler跳转地址                                   │
│        R1 = R1 & 0xFF       ; 取低8位 = 0x89                     │
│        handler_addr = handler_table + (R1 << 2)             │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 步骤5: 跳转到对应handler执行                                  │
│        MOV PC, R0          ; 跳转到 invoke-super 的handler          │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 步骤6: Handler内部调用JNI函数                                │
│        FindClass("java/lang/Object")                        │
│        GetMethodID(class, "<init>", "()V")                  │
│        CallNonvirtualVoidMethodA(...)                       │
└─────────────────────────────────────────────────────────────┘

 

分析步骤

1. 环境准备与反调试绕过

  • 编写Python脚本辅助调试
  • 在libDexHelper.so加载时绕过反调试(patch地址0x2A142处的指令)
  • 等待JNI_OnLoad解密完成

2. 定位VMP入口

  • 找到不同返回类型的VMP入口方法(cV等函数)
  • 这些方法都注册到libdexjni.so中

3. 找到CodeItem

通过回溯分析发现:

  • Java层传入vmpId参数索引vmpInfo结构体
  • vmpInfo结构体包含:vmpId、codeItem等字段
  • codeItem偏移在结构体+0x8位置

4. 指令分析

通过trace日志和断点JNI函数(如FindClass、GetMethodID)分析出:

指令格式:

opcode(2字节) | operand(2字节) | 参数(2字节) | opcode(2字节)

示例指令:

  • 00 10 CB 28 00 00 47 00
    • 00 10:invoke-super的opcode(需要XOR解密)
    • CB 28:字符串索引操作数
    • 00 00:参数(寄存器)
    • 47 00:return-void的opcode

5. 指令解密机制

  • opcode会进行XOR解密
  • 解密key的获取公式:[[[GOT + 0xD8C50 + 0x8] + vmpId * 4] + addr_cur_insn - addr_insn_base]
  • 每条指令的opcode部分都需要解密

6. 识别的指令类型

通过JNI函数调用推断出对应的smali指令:

  • invoke-super:调用父类方法
  • return-void:空返回
  • const-string:字符串常量
  • iput-object:对象字段赋值

技术要点

  1. trace脚本的使用:记录执行流程和寄存器状态,便于回溯分析
  2. 回溯法:从JNI函数断点向上追踪参数来源
  3. 结构体推断:通过内存访问模式推断数据结构定义
  4. 指令映射:将VMP指令映射到原始Dalvik指令

难点

  • OLLVM混淆增加了分析难度
  • 地址随机化(ASLR)
  • IDA无法下内存断点
  • 完整的指令映射表需要大量时间建立
  • 内存中的VMP指令是加密的,增加了patch难度

 

也就是还原出字节码需要加密的自定义操作码,key和自定义操作码与smali的映射关系 

 

 具体步骤

1.一切的开端,找java入口的MainActivity类

我们脱dex后打开,搜索oncreate找到加密位置

image

image

 2.解密so,找到c入口的init_proc方法

跟 libDexHelper 中一样有 init_proc 函数,位于 ELF 文件的 .init_proc 段,负责在库加载时执行必要的解密so,包括 JNI_OnLoad 。 dump   so后完成解密。

__int64 __fastcall init_proc(__int64 a1)
{
  __int64 v1; // x0

  v1 = sub_176734((char *)&elf_hash_chain[285] + (_QWORD)&off_176078);  //内存分配与数据复制
  ((void (__fastcall *)(char *, __int64 (*)(), __int64))loc_17650C)(    //解密so
    (char *)&elf_hash_chain[285] + (_QWORD)&off_176078,
    sub_95A0,   
    v1);
  return a1;
}

 3.动态注册,找到解密后的jni_onload方法

jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
  jint v2; // w19
  __int64 v3; // x0
  int v5[2]; // [xsp+8h] [xbp-18h] BYREF

  *(_QWORD *)v5 = 0;
  v2 = 65540;
  if ( (*vm)->GetEnv(vm, (void **)v5, 65540) )
    return -1;
  j_j__lS0lIllO_l_0lSll_5l__IIllSIlI05SlISS0I_5Ol_lO0llS5_();      //这里是thub函数只用于强制调转执行,跳转两次到达最终执行函数,j_j_就是跳转两次,属于混淆的一种
v3 = j_pE42128F1981DEFB860D59DE036EF9F09(); j_j__lI5_SOlIO5I_ll0_SllOlO__lOISSO5SllOO_l0Ill5lO5IIS5_(v3); // 前三个j_j__函数解密 vmp 执行所需的指令及解密指令的 key  
j_j__l5I0l_l5lSIIl_ISlIl_Sl5Il0OOI_OSI55l__IS_SSSO5SSS5_(v5[
0], "com/fort/andjni/JniLib"); //动态注册本地方法并将反射获取 java 类及 methodId ,并把它们保存到全局变量上

return v2; }
j_j__l5I0l_l5lSIIl_ISlIl_Sl5Il0OOI_OSI55l__IS_SSSO5SSS5_的最终执行函数
__int64 (__fastcall *__fastcall j__l5I0l_l5lSIIl_ISlIl_Sl5Il0OOI_OSI55l__IS_SSSO5SSS5_(
        __int64 a1,
        char *a2,
        __int64 (__fastcall *a3)()))()
{
  char v3; // w25
  size_t v7; // x0
  int v8; // w22
  size_t v9; // x23
  void *v10; // x24
  __int64 (__fastcall *result)(); // x0
  int v12; // w28
  unsigned int v13; // w8
  const char *v14; // x22
  unsigned int v15; // w8

  if ( a2 )
  {
    v7 = strlen(a2);
    v8 = v7;
    v9 = (__int64)((v7 << 32) + 0x100000000LL) >> 32;
    v10 = malloc(v9);
    qword_175398 = (__int64)v10;
    memset(v10, 0, v9);
    strncpy((char *)v10, a2, v8);
  }
  ((void (__fastcall *)(__int64))loc_1061C)(a1);   //这里面首先是调用 反射获取 java 类及 methodId ,并把它们保存到全局变量上
  qword_175A68 = (__int64)sub_17AC4;                //接着注册 com.fort.andjni.JniLib 的所有 native 方法,后面以 cV 函数为例
  qword_175A58 = (__int64)byte_847A0;          //我们发现这里没有识别出字符串cV但是不要紧,我们在目标字符串定义的位置中选中字符串按下a重新定义为字符串即可。
  qword_175A60 = (__int64)"([Ljava/lang/Object;)V";   
  qword_175A80 = (__int64)sub_17AEC;
  qword_175A70 = (__int64)"cI";
  qword_175A78 = (__int64)"([Ljava/lang/Object;)I";
  qword_175A98 = (__int64)sub_17B18;
  qword_175A88 = (__int64)"cL";
  qword_175A90 = (__int64)"([Ljava/lang/Object;)Ljava/lang/Object;";
  qword_175AB0 = (__int64)sub_17B44;
  qword_175AA0 = (__int64)"cS";
  result = sub_17C4C;
  qword_175AA8 = (__int64)"([Ljava/lang/Object;)S";
  qword_175AC8 = (__int64)sub_17B70;
  qword_175AB8 = (__int64)"cC";
  qword_175AC0 = (__int64)"([Ljava/lang/Object;)C";
  v12 = 30745;
  qword_175AE0 = (__int64)sub_17B9C;
  v13 = 9;
  qword_175AD0 = (__int64)"cB";
  qword_175AD8 = (__int64)"([Ljava/lang/Object;)B";
  qword_175AF8 = (__int64)sub_17BC8;
  qword_175AE8 = (__int64)"cJ";
  qword_175AF0 = (__int64)"([Ljava/lang/Object;)J";
  qword_175B10 = (__int64)sub_17BF4;
  qword_175B00 = (__int64)"cZ";
  qword_175B08 = (__int64)"([Ljava/lang/Object;)Z";
  qword_175B28 = (__int64)sub_17C20;
  qword_175B18 = (__int64)"cF";
  qword_175B20 = (__int64)"([Ljava/lang/Object;)F";
  qword_175B40 = (__int64)sub_17C4C;
  qword_175B30 = (__int64)"cD";
  qword_175B38 = (__int64)"([Ljava/lang/Object;)D";
  if ( qword_175398 )
    v14 = (const char *)qword_175398;
  else
    v14 = "com/fort/andjni/JniLib";
  while ( 1 )
  {
    switch ( v13 )
    {
      case 0u:
      case 3u:
      case 4u:
      case 0xCu:
        return result;
      case 1u:
        if ( a3 )
          v13 = 5;
        else
          v13 = 4;
        break;
      case 2u:
        result = (__int64 (__fastcall *)())(*(__int64 (__fastcall **)(__int64, __int64 (__fastcall *)()))(*(_QWORD *)a1 + 184LL))(
                                             a1,
                                             a3);
        v13 = 3;
        break;
      case 5u:
        result = (__int64 (__fastcall *)())(*(__int64 (__fastcall **)(__int64, __int64 (__fastcall *)(), __int64 *, __int64))(*(_QWORD *)a1 + 1720LL))(
                                             a1,
                                             a3,
                                             &qword_175A58,
                                             10);
        if ( (_DWORD)result )
          v13 = 10;
        else
          v13 = 11;
        v12 = 20516;
        break;
      case 6u:
        result = (__int64 (__fastcall *)())(*(__int64 (__fastcall **)(__int64, __int64 (__fastcall *)()))(*(_QWORD *)a1 + 184LL))(
                                             a1,
                                             a3);
        v13 = 0;
        break;
      case 7u:
        result = (__int64 (__fastcall *)())(*(__int64 (__fastcall **)(__int64, const char *))(*(_QWORD *)a1 + 48LL))(
                                             a1,
                                             v14);
        a3 = result;
        v13 = 1;
        v3 = 1;
        break;
      case 8u:
        result = (__int64 (__fastcall *)())(*(__int64 (__fastcall **)(__int64, const char *))(*(_QWORD *)a1 + 48LL))(
                                             a1,
                                             v14);
        a3 = result;
        v13 = 40 - v12 + 83 * ((unsigned int)(50534 * v12) >> 22);
        v3 = 1;
        break;
      case 9u:
        v3 = 0;
        if ( a3 )
          v13 = 1;
        else
          v13 = 7;
        break;
      case 0xAu:
        v15 = v12 - 157 * ((unsigned int)(53431 * v12) >> 23);
        if ( (v3 & 1) != 0 )
        {
          v13 = 112 - v15;
          v3 = 1;
        }
        else
        {
          v3 = 0;
          v13 = 106 - v15;
        }
        break;
      case 0xBu:
        if ( (v3 & 1) != 0 )
          v13 = 2;
        else
          v13 = 3;
        break;
      default:
        continue;
    }
  }
}

 4.java调用注册的c的参数解析方法,cV函数

 这是一个"万能方法调用器" —— 它可以根据传入的参数描述,动态调用任意 Java 方法。

简化后的流程
C

void parseAndInvokeJniMethod(JNIEnv* env, jobjectArray args, int64_t* result) {
    
    // 第1步:读取方法描述
    jobject methodInfo = args[0];
    MethodDesc desc = parseMethodInfo(methodInfo);
    
    // 第2步:遍历所有参数,拆箱
    for (int i = 1; i < args.length; i++) {
        jobject param = args[i];
        char type = desc.paramTypes[i-1];
        
        switch (type) {
            case 'I': nativeArgs[i] = env->CallIntMethod(param, intValue); break;
            case 'J': nativeArgs[i] = env->CallLongMethod(param, longValue); break;
            case 'D': nativeArgs[i] = env->CallDoubleMethod(param, doubleValue); break;
            case 'Z': nativeArgs[i] = env->CallBooleanMethod(param, booleanValue); break;
            // ... 其他类型
        }
    }
    
    // 第3步:调用实际方法
    *result = invokeRealMethod(desc, nativeArgs);
}

4.分析vmp解密原理

VMP 指令分析

搞清楚 VMP 保护的指令格式,以及如何解密和还原原始 smali 指令


 分析流程图

┌─────────────────────────────────────────────────────────────────┐
│                     VMP 指令分析完整流程                         │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 第1步:准备工作 - 修改 trace 脚本打印寄存器值                    │
│                                                                 │
│   格式:指令  D:[目的寄存器值]  S:[源寄存器值]                    │
│   目的:能看到数据的变化过程                                     │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 第2步:找到指令解密过程                                          │
│                                                                 │
│   搜索 insn 的值(如 0x1000),找到异或操作:                     │
│   EOR.W R1, R2, R6   ; R2=0x89(key), R6=0x1000(加密opcode)      │
│   结果:0x1000 ^ 0x89 = 0x1089(解密后的opcode)                 │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 第3步:观察跳转,确定这是 handler 分发                           │
│                                                                 │
│   UXTB R1, R1              ; 取低8位 = 0x89                     │
│   ADD.W R0, R0, R1,LSL#2   ; 计算跳转表地址                      │
│   LDR R0, [R0]             ; 加载 handler 地址                   │
│   MOV PC, R0               ; 跳转到对应 handler                  │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 第4步:观察 handler 调用了什么 JNI 函数                          │
│                                                                 │
│   FindClass("java/lang/Object")                                 │
│   GetMethodID(..., "<init>", ...)                               │
│   CallNonvirtualVoidMethodA(...)                                │
│                                                                 │
│   → 推断:这是 invoke-super 指令,调用父类构造函数               │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ 第5步:分析完整指令格式                                          │
└─────────────────────────────────────────────────────────────────┘

 指令解密机制

发现过程

第一次执行 cV 函数:
加密指令:00 10  CB 28  00 00  47 00
解密key: 89           ←      87
解密结果:89 10  CB 28  00 00  C0 00
          ↑                   ↑
       0x1000^0x89=0x1089  0x47^0x87=0xC0

第二次执行 cV 函数:
加密指令:69 10  CB 28  00 00  8B 00
解密key: 同上
解密结果:89 10  CB 28  00 00  C0 00  ← 结果相同!

关键发现

 结论:只有 opcode 需要异或解密,操作数(operand)不需要解密
 每次执行时 opcode 的加密值不同,但解密后结果相同 。
 这说明 key 是动态变化的,增加了静态分析的难度

 VMP 指令格式

┌─────────────────────────────────────────────────────────────────┐
  VMP 指令格式(与 Dalvik 相同)├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   00 10       CB 28         00 00         47 00                          │
│├───┤     ├───┤      ├───┤     ├───┤                          │
│   opcode      operand      参数        opcode                          │
│   (加密)      (明文)       (明文)      (加密)                          │
│     ↓                                   ↓                              │
│   解密后                               解密后                           │
│   0x1089                               0x00C0                           │
│     ↓                                   ↓                              │
│   invoke-super                        return-void                      │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

通过 JNI 函数推断 smali 指令

推断表


JNI 函数调用对应的 smali 指令
FindClass + GetMethodID + CallNonvirtualVoidMethodA invoke-super
FindClass + GetMethodID + CallStaticXxxMethod invoke-static
GetFieldID + SetObjectField iput-object
GetFieldID + GetObjectField iget-object
无 JNI 调用,直接返回 return-void

实际分析案例

观察到的 JNI 调用序列:
┌─────────────────────────────────────────────────────────────────┐
│ 1. GetFieldID("cn/.../i", "a", "Ljava/lang/String;")            │
│ 2. SetObjectField(obj, fieldId, stringValue)                    │
├─────────────────────────────────────────────────────────────────┤
│ 推断:这是给对象的字符串字段赋值                                  │
│ 对应 smali:iput-object v0, v1, Lcn/.../i;->a:Ljava/lang/String;│
└─────────────────────────────────────────────────────────────────┘

完整指令还原示例

加密的 VMP 指令

5B 10 CB 28 01 00 41 00  00 00 DA 10 38 02 9D 00

解密过程

5B 10 → 5B^?? = 89 → invoke-super
CB 28 → 操作数(类/方法索引)
01 00 → 参数(使用 v1 寄存器)
41 00 → 41^?? = 75 → const-string  
00 00 → 操作数
DA 10 → DA^?? = F5 → iput-object
38 02 → 操作数(字段索引)
9D 00 → 9D^?? = C0 → return-void

还原的 smali

invoke-super {v1}, Ljava/lang/Object;-><init>()V
const-string v0, "xxx"
iput-object v0, v1, Lcn/.../i;->a:Ljava/lang/String;
return-void
┌─────────────────────────────────────────────────────────────────┐
│                    VMP 指令分析方法论                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  1. Trace 记录:打印完整执行流程 + 寄存器值                       │
│                                                                 │
│  2. 找解密点:搜索 EOR/XOR 指令,找到 opcode 解密位置            │
│                                                                 │
│  3. 观察跳转:解密后的 opcode 用于计算 handler 地址并跳转        │
│                                                                 │
│  4. 分析 handler:                                               │
│     • 记录调用了哪些 JNI 函数                                    │
│     • 根据 JNI 函数组合推断 smali 指令                           │
│     • 参考 dex2c 项目了解 JNI 实现细节                           │
│                                                                 │
│  5. 多次执行对比:                                               │
│     • 确认哪些部分被加密(opcode)                               │
│     • 哪些部分是明文(operand)                                  │
│                                                                 │
│  6. 建立映射表:                                                 │
│     • 解密后 opcode → smali 指令类型                             │
│     • 有了映射表就能批量还原  !!!!!!!                                   │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

 核心要点

1. VMP 指令格式与 Dalvik 相同,只是 opcode 被加密了
2. 解密方式:opcode ^ key = 真实opcode  
3. key 是动态的,每次执行可能不同
4. 通过观察 JNI 函数调用序列,可以反推 smali 指令
5. 操作数(operand)不加密,可以直接用于索引字符串/类/方法

5.找到vmpInfo(哪一个oncreate方法的加密指令集合)、codeItem(某个加密指令集合)、insns(指令)、opcode(指令的一个加密操作码) 和key(解密指令的关键)在内存中的位置

vmpInfo结构体关系图

┌─────────────────────────────────────────────────────────────┐
│                     全局 vmpInfo 数组                        │
├─────────────────────────────────────────────────────────────┤
│  vmpInfo[0]  │  vmpInfo[1]  │  vmpInfo[2]  │  ...           │
└──────┬───────┴──────┬───────┴──────┬───────┴────────────────┘
       │              │              │
       ▼              ▼              ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│  vmpInfo           │ │ vmpInfo            │ │  vmpInfo           │
├─────────────┤ ├─────────────┤ ├─────────────┤
│ +0x0: vmpid=0       │ │ +0x0: vmpid=1        │ │ +0x0: vmpid=2        │
│ +0x4: ???          │ │ +0x4: ???          │ │ +0x4: ???         │
│ +0x8:(*)codeItem      ┼─┤ +0x8: (*)codeItem     ┼─┤ +0x8: (*)codeItem      ┼──┐
└─────────────┘ └─────────────┘ └─────────────┘  │
       │                                                               │
       ▼                                                               ▼
┌──────────────────────────┐            ┌──────────────────────────┐
│       CodeItem                         │            │       CodeItem                       │
├──────────────────────────┤            ├──────────────────────────┤
│ +0x00: registers_size    方法中寄存器总数    │            │ +0x00: registers_size                │
│ +0x02: ins_size           传入参数占用的寄存器数│              │ +0x02: ins_size                      │
│ +0x04: outs_size          调用其他方法时需要的寄存器数│          │ +0x04: outs_size                     │
│ +0x06: tries_size        try-catch 块的数量   │            │ +0x06: tries_size                    │
│ +0x08: debug_info_off     调试信息偏移      │            │ +0x08: debug_info_off                │
│ +0x0C: insns_size        指令数量(以2字节为单位)│            │ +0x0C: insns_size                    │
// 头部固定 16 字节,之后是变长的指令数据 ├──────────────────────────┤    ├──────────────────────────┤ │ +0x10: insns[0]   指令数组(变长)             │ │ +0x10: insns[0]              │ │ +0x12: insns[1]                │ │ +0x12: insns[1]                │ │ +0x14: insns[2]                 │ │ +0x14: insns[2]            │ │ ...                 │ │ ...            │ └──────────────────────────┘ └──────────────────────────┘

实际内存示例

地址        内容              含义
─────────────────────────────────────────────────
+0x00      01 00             registers_size = 1
+0x02      01 00             ins_size = 1
+0x04      01 00             outs_size = 1
+0x06      00 00             tries_size = 0
+0x08      16 38 09 00       debug_info_off = 0x93816
+0x0C      04 00 00 00       insns_size = 4(共4条指令)
─────────────────────────────────────────────────
+0x10      00 10 CB 28       insns[0]: invoke-super 操作码+操作数
+0x14      00 00             insns[1]: 参数
+0x16      47 00             insns[2]: return-void

  获取 vmpInfo 的计算方式

// 根据分析,获取 vmpInfo 的公式:
// vmpInfo地址 = 全局数组基址 + vmpId * sizeof(VmpInfo)

// 获取 codeItem 的公式:
CodeItem* codeItem = vmpInfo->code_item;  // vmpInfo + 0x8

// 获取 insns 的公式:
uint16_t* insns = codeItem->insns;        // codeItem + 0x10
// 或者
uint16_t* insns = (uint16_t*)((uint8_t*)codeItem + 0x10);

推断依据(来自 trace 日志)

; 根据索引获取 vmpInfo
0x000142C8: BLX j_153f0           ; 返回 vmpInfo 地址
0x000142CC: STR R0, [SP]          ; 保存 vmpInfo 到栈

; 从 vmpInfo 中取出 codeItem
0x0001433A: LDR R1, [SP]          ; 取出 vmpInfo
0x00014352: LDR R0, [R1,#8]       ; vmpInfo + 0x8 = codeItem 指针 ✓
0x00014354: STR R0, [SP,#0x13C]   ; 保存 codeItem

; 计算 insns 地址
0x0001462E: LDR R0, [SP,#0x13C]   ; 取出 codeItem
0x00014634: ADDS R0, #0x10        ; codeItem + 0x10 = insns 地址 ✓
0x00014636: STR R0, [R5]          ; 保存 insns 地址给 vmpEntry 使用

找到 codeItem?

  因为原始的vmp引擎十分复杂,我们要找到codeItem的位置,这里采用的是倒推法

分析一个VMP加固的程序,遇到了一块未知的内存(vmpInfo结构体)。他不知道这块内存里各个字段的含义,特别是不知道codeItem(存放原始指令的地方)在哪里。


核心思路:倒推法

用一个生活比喻来解释

想象你在找一个秘密仓库的位置:

你不知道仓库在哪,但你知道一件事:
→ 每天有个快递员会从仓库取货,然后送到一个固定的收货点

你的策略:
1. 在收货点蹲守,等快递员送货时拦住他
2. 问他:"这货从哪取的?"
3. 他说:"从A点拿的"
4. 你去A点调监控,发现货是从B点转运来的
5. 你再去B点查,发现货是从C点发出的
6. 一步步往回追,最终找到秘密仓库!

 实际分析过程

第1步:蹲点

VMP执行Java代码时,必然要调用JNI函数
比如 FindClass —— 用来查找Java类
选择在 FindClass 处下断点
这就是"蹲守点"

第2步:断点触发,开始追踪

断点触发了!发现VMP在调用:
FindClass("java/lang/Object")

此时查看LR寄存器(返回地址)
发现调用者是 vmpEntry 函数(偏移0x1D0B0)

第3步:追踪参数来源

问题:FindClass的参数"java/lang/Object"这个字符串从哪来的?

使用trace脚本记录执行过程,然后往回看:

0x0002163C: LDR.W R1, [SP,#0x950]  ← 字符串地址从栈上取
0x00021642: LDR   R1, [R1]          ← 再解引用一次
0x00021646: BLX   R2               ← 调用FindClass

所以:字符串地址 = [[SP, #0x950]]

第4步:继续往上追

问题:[SP,#0x950] 这个位置的值是谁存进去的?

在trace日志中搜索 "SP,#0x950",找到:

0x0003A93C: LDR.W R1, [SP,#0x9A4]  ← 取出一个"字符串索引"
0x0003A940: MOV   R0, R4         
0x0003A942: BLX   sub_xxx         ← 调用函数,用索引获取字符串
0x0003A946: STR.W R0, [SP,#0x950] ← 返回值存到这里!

发现:有个"字符串索引"被用来获取字符串

第5步:追踪字符串索引的来源

问题:这个"字符串索引"又是从哪来的?

继续往上追踪 [SP,#0x9A4]...

最终发现:
字符串索引 = [[[R0]] + 2]

而 R0 是 vmpEntry 的第一个参数!

第6步:关键发现

既然字符串索引在 vmpEntry 的第一个参数指向的内存里,
而字符串索引是指令的一部分(操作数),
那么:

vmpEntry 的第一个参数 = 指令(insn)的地址!

第7步:找到 codeItem

读取内存,发现指令内容是:
00 10 CB 28 00 00 47 00

已知 codeItem 结构:头部16字节 + 指令数据
所以往前偏移16字节,就是 codeItem:
01 00 01 00 01 00 00 00 16 38 09 00 04 00 00 00

验证:最后4字节 = 04 00 00 00 = insnsSize = 4条指令 ✓

完整的倒推链

FindClass("java/lang/Object")
         ↑ 参数从哪来?
[[SP,#0x950]]
         ↑ 这个值谁存的?
函数返回值(用字符串索引获取字符串)
         ↑ 字符串索引从哪来?
[[[R0]] + 2]
         ↑ R0是什么?
vmpEntry的第一个参数 = insn地址
         ↑ insn和codeItem什么关系?
insn = codeItem + 16
所以:codeItem = insn地址 - 16

额外收获:推断 vmpInfo 结构

继续回溯,发现 insn 地址是这样来的:

0x000142CC: STR R0, [SP]         ; 保存vmpInfo到栈
...
0x0001433A: LDR R1, [SP]         ; 取出vmpInfo
...
0x00014352: LDR R0, [R1,#8]      ; 从vmpInfo+8处取出codeItem!
0x00014354: STR R0, [SP,#0x13C]  ; 保存codeItem
...
0x00014634: ADDS R0, #0x10       ; codeItem + 16 = insn地址
0x00014636: STR R0, [R5]         ; 保存insn地址

由此推断出:vmpInfo 偏移0x8处 = codeItem 字段

核心方法论

┌─────────────────────────────────────────────────────┐
│                    倒推分析法                        │
├─────────────────────────────────────────────────────┤
│                                                     │
│  1. 找锚点:选一个VMP必然调用的函数(如FindClass)   │
│                                                     │
│  2. 下断点:等它触发                                │
│                                                     │
│  3. 记录trace:用脚本记录完整执行过程                │
│                                                     │
│  4. 逆向追踪:从函数参数开始,往回追溯数据来源       │
│     - 看到 LDR R1, [SP,#xxx] → 搜索谁写了这个地址   │
│     - 一层层往回追                                  │
│                                                     │
│  5. 推断结构:根据偏移量,反推结构体字段含义         │
│                                                     │
│  原理:VMP再怎么保护,执行时一定要读取原始指令       │
│        找到读指令的地方,就能找到指令存放的位置!    │
│                                                     │
└─────────────────────────────────────────────────────┘

简单说就是:顺藤摸瓜,从结果倒推原因,从使用者追溯到数据源头。

 


找key

在so中的java字节码opcode 是加密存储的,我们要找到解密 opcode 的 key 存放在哪里?

我们采用回溯法 ,以为key是异或解密的,从异或指令开始倒推

EOR.W R1, R2, R6    ; R2=0x89(key), R6=0x1000(加密opcode)
                    这个 key 从哪来的?
LDR R2, [R7, R2]    ; 从 R7 指向的表中取出 key
                    R7 是什么?
                    R7 是 key 表的基地址

key表内存布局图

GOT + 0xD8C50 + 0x8
┌───────────────────┐
│    0x9C100000               │  ← 存放 key表指针数组 的基地址
└─────────┬─────────┘
                │
                ▼
┌─────────────────────────────────────────────────────────────────┐
│                     key表指针数组                                                                │
│                  (每个元素4字节,是一个指针)                                                      │
├───────────┬───────────┬───────────┬───────────┬─────────────────┤
│        [0]      │  [1]            │  [2]            │  [3]          │  ...                    │
│     0x9C0FD000  │ 0x9C0FD100      │ 0x9C0FD1C0      │ 0x9C0FD280    │                         │
│              │              │              │            │                    │      │    │      │                 │  
└────┼──────┴────┼──────┴────┼──────┴────┼──────┴─────────────────┘
       │                 │                 │                 │
       │                 │                 │                 │
       ▼                 ▼                 ▼                 ▼
┌─────────┐   ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 方法0的      │   │ 方法1的      │ │ 方法2的      │ │ 方法3的      │
│ key表        │   │ key表        │ │ key表        │ │ key表        │
│              │   │             │ │             │ │             │
│ 89 A2 ..    │     │ 45 F3 ..    │ │ 87 C1 ..      │ │ 91 B4 ..    │
└─────────┘   └─────────┘ └─────────┘ └─────────┘
     ↑                   ↑                 ↑                 ↑
   vmpId=0               vmpId=1           vmpId=2           vmpId=3

 实际计算示例

# 假设:
GOT = 0x9BF00000
vmpId = 5
当前insn地址 = 0xB399E3BC
insn基址 = 0xB399E3B8

# 第1步:找到 key表数组 基地址
key_array_ptr = 读取内存(GOT + 0xD8C50 + 0x8)
# 假设得到 0x9C100000

# 第2步:找到当前方法的 key表
key_table = 读取内存(key_array_ptr + vmpId * 4)
# key_table = 读取内存(0x9C100000 + 5 * 4) = 0x9C0FD1C0

# 第3步:计算指令偏移,获取 key
offset = 当前insn地址 - insn基址
# offset = 0xB399E3BC - 0xB399E3B8 = 4

key = 读取内存(key_table + offset)
# key = 读取内存(0x9C0FD1C0 + 4) = 0x89

# 第4步:解密 opcode
加密opcode = 0x1000
真实opcode = 加密opcode ^ key
# 真实opcode = 0x1000 ^ 0x89 = 0x1089

我再用通俗的语言表述这一个过程,

先找到key表数组,也就是key的起始地址,在找到某个函数对应的key表,函数编号对应数组角标,最后找到加密操作码对应的key表的值。

也就是说,key表数组角标=函数索引,key表索引=指令操作码位置

这样设计的原因是:

1. 每个方法有独立的 key表 ,破解一个方法不影响其他方法
2. 每条指令有独立的 key,不能用同一个 key 解密所有指令
3. key 和指令分开存储, 增加静态分析难度
4. 通过多层指针间接寻址,不容易直接定位 key 的位置

一句话总结:

key 的存放采用了"三级索引"结构:
全局基址 → vmpId索引方法 → 偏移量索引指令 → 得到key

这样每个被保护的方法、每条指令都有独立的 key,增加了破解难度。

6.恢复完整的opcode和smali映射表
构建 解密后 VMP opcode → 标准 smali 指令 的完整映射表。这个表是通用的,但是在内存中的 vmp 指令是被动态的key加密过的,无法批量修复vmp指令。
也就是说我们的码表获取后,key的值的获取就成为了关键。


7.自动修复 VMP 指令脚本

 

 

 

 

 

 

企业级加固VMP解释执行与指令还原详解

这是一篇关于Android VMP(虚拟机保护)壳的逆向分析文章。我将按步骤详细解释整个分析过程。


📚 第一部分:DEX指令格式基础

1.1 理解DEX字节码格式

在分析VMP之前,必须先理解标准的DEX指令格式。

text
┌─────────────────────────────────────────────────────────────┐
│                    DEX 指令格式 (35c)                        │
├─────────────────────────────────────────────────────────────┤
│  格式: A|G|op BBBB F|E|D|C                                  │
│                                                             │
│  字节布局:                                                   │
│  ┌────┬────┬────────┬────┬────┬────┬────┐                  │
│  │ A  │ G  │   op   │  BBBB   │ F  │E│D│C│                  │
│  └────┴────┴────────┴─────────┴────┴──┴──┘                  │
│   4bit 4bit  8bit    16bit     各4bit                       │
└─────────────────────────────────────────────────────────────┘

1.2 实例解析

Java源码:

Java
public class BaseActivity extends FragmentActivity {
    public Resources getResources() {
        return super.getResources();
    }
}

对应DEX指令:

text
6F 10 1C 33 01 00

解析过程:

text
6F 10 → 拆分为 A|G|op = 1|0|6F
       A = 1 (参数数量)
       G = 0 
       op = 0x6F (invoke-super)

1C 33 → BBBB = 0x331C (方法索引)

01 00 → F|E|D|C = 0|0|0|1
        C = 1 表示使用 v1 寄存器

还原结果:

smali
invoke-super {p0}, Landroidx/fragment/app/FragmentActivity;->getResources()Landroid/content/res/Resources;

🛡️ 第二部分:反调试处理

2.1 发现的反调试点

text
┌─────────────────────────────────────────────────────────────┐
│                    反调试检测点                              │
├──────────────┬──────────────────────────────────────────────┤
│   地址       │   反调试类型                                  │
├──────────────┼──────────────────────────────────────────────┤
│   0x47CAC    │   创建线程检测运行时间                        │
│              │   getpid → syscall(__NR_kill) 杀死进程        │
├──────────────┼──────────────────────────────────────────────┤
│   0x047C70   │   cmdline反调试                              │
│              │   检测 /proc/[pid]/cmdline                    │
├──────────────┼──────────────────────────────────────────────┤
│   0x489EC    │   /proc/status 检测                          │
│              │   检测 TracerPid 字段                         │
└──────────────┴──────────────────────────────────────────────┘

2.2 绕过方法

在 JNI_OnLoad 处找到反调试的总入口,直接NOP掉:

text
ARM64 NOP指令: mov w1, w1
对应HEX: E1 03 01 2A

2.3 Dump DEX脚本

Python
import struct

start = 0x75172191ec          # DEX起始地址(内存中搜索 "dex\n035")
length = 0x6ee27c             # DEX大小
dump_so = "/Users/beita/tmp/bangbang/dump_vmp.dex"

fn = AskStr(dump_so, "save as:")
with open(fn, "wb+") as f:
    for addr in range(start, start + length):
        f.write(struct.pack("B", Byte(addr)))
    print("success to save as")

🔍 第三部分:VMP核心分析

3.1 VMP调用特征识别

dump出的DEX反编译后,发现VMP特征:

Java
public void o() {
    // 索引17标识要执行的函数
    JniLib.cV(new Object[] { this, Integer.valueOf(17) });
    throw new VerifyError("bad dex opcode");  // 垃圾代码,阻止静态分析
}

protected void onCreate(Bundle paramBundle) {
    // 索引18标识onCreate函数
    JniLib.cV(new Object[] { this, paramBundle, Integer.valueOf(18) });
    throw new VerifyError("bad dex opcode");
}

3.2 VMP数据结构分析

JavaInfo结构体(根据索引获取):

C
struct JavaInfo {
    uint32_t index;      // 0x12 - Java层传递的函数索引
    uint32_t unknow2;    // 0x2E - 未知字段
    uint64_t dexcode;    // DexCode指针 → 指向VMP加密指令
    uint32_t unknow4;    // 0x03
    uint32_t unknow5;    // 0x02
    uint32_t unknow6;    // 0x02
};

DexCode结构体:

C
struct DexCode {
    u2 registersSize;    // 寄存器数量 = 3
    u2 insSize;          // 输入参数数量 = 2
    u2 outsSize;         // 输出参数数量
    u2 triesSize;        // try块数量
    u4 debugInfoOff;     // 调试信息偏移
    u4 insnsSize;        // 指令数量 = 0x0F (15条)
    u2 insns[1];         // VMP加密指令数组 ↓
};

// VMP加密后的指令 (非标准DEX opcode)
insns[] = {
    A3 20 5C 00 21 00    // 第1条
    6B 10                // 第2条  
    CC 20 13 02 01 00    // 第3条
    55 11 6D 00          // 第4条
    53 10 6D 00          // 第5条
    72 10 60 01 00 00    // 第6条
    69 00                // 第7条
};

3.3 VMP执行流程图

text
┌─────────────────────────────────────────────────────────────────────────┐
│                         VMP 执行流程                                     │
└─────────────────────────────────────────────────────────────────────────┘

     Java层调用
┌─────────────────────┐
│ JniLib.cV(Object[]) │  传入: this, paramBundle, Integer.valueOf(18)
└──────────┬──────────┘
┌─────────────────────┐
│  解析Object数组      │  获取最后一个Integer作为函数索引
│  提取函数索引=18     │
└──────────┬──────────┘
┌─────────────────────┐
│ 根据索引获取JavaInfo │  JavaInfo结构体包含DexCode指针
└──────────┬──────────┘
┌─────────────────────┐
│  获取DexCode        │  包含VMP加密后的insns[]数组
│  栈地址: SP+var_1460│  ← 指令指针存放位置
└──────────┬──────────┘
┌─────────────────────────────────────────────────────────────────────────┐
│                        vm_parse (地址: 0x29b70)                          │
│  ┌─────────────────────────────────────────────────────────────────┐   │
│  │                    超大switch dispatcher                         │   │
│  │                   (被OLLVM混淆, 几千个case)                       │   │
│  └───────────┬───────────┬───────────┬───────────┬─────────────────┘   │
│              │           │           │           │                      │
│              ▼           ▼           ▼           ▼                      │
│         ┌────────┐  ┌────────┐  ┌────────┐  ┌────────┐                 │
│         │invoke  │  │ const  │  │ iget   │  │ iput   │  ...            │
│         │-super  │  │        │  │-object │  │-object │                 │
│         └────────┘  └────────┘  └────────┘  └────────┘                 │
│              │           │           │           │                      │
│              └───────────┴───────────┴───────────┘                      │
│                                  │                                      │
│                                  ▼                                      │
│                         JNI函数调用实现                                   │
│                    (FindClass, GetMethodID,                             │
│                     CallNonVirtualMethod等)                             │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────┐
│     函数执行完毕     │
└─────────────────────┘

🔬 第四部分:指令还原详解

4.1 分析方法:逆向追踪

由于vm_parse被OLLVM严重混淆(FLA+BCF),F5无法使用,采用以下策略:

text
┌───────────────────────────────────────────────────────────────┐
│                    逆向追踪分析策略                            │
├───────────────────────────────────────────────────────────────┤
│                                                               │
│  1. 在GetMethodID/FindClass等JNI函数下断点                    │
│                    ↓                                          │
│  2. 观察参数值(类名、方法名、签名)                             │
│                    ↓                                          │
│  3. 反向追踪这些参数从哪里来                                   │
│                    ↓                                          │
│  4. 找到读取VMP指令的位置                                      │
│                    ↓                                          │
│  5. 建立 VMP opcode ↔ 真实opcode 映射关系                     │
│                                                               │
└───────────────────────────────────────────────────────────────┘

4.2 关键断点追踪

在GetMethodID断点观察:

asm
.text:7517CF9988  LDR  X8, [X8, #0x30]    ; FindClass
.text:7517CF999C  LDR  X8, [X8, #0x108]   ; GetMethodID

; 寄存器内容:
; X2 = "onCreate"                         ; 方法名
; X3 = "(Landroid/os/Bundle;)V"           ; 方法签名

方法信息结构体:

C
struct MethodInfo {
    char* class_name;    // "android/support/v4/app/FragmentActivity"
    char* method_sig;    // "(Landroid/os/Bundle;)V"  
    char* method_name;   // "onCreate"
};

IDAPython提取信息:

Python
base = 0x7517D666A0
index = 0x5C

# 获取类名
idc.GetString(idc.Qword(idc.Qword(idc.Qword(base) + index * 8) + 8 * 0))
# → "android/support/v4/app/FragmentActivity"

# 获取方法签名  
idc.GetString(idc.Qword(idc.Qword(idc.Qword(base) + index * 8) + 8 * 1))
# → "(Landroid/os/Bundle;)V"

# 获取方法名
idc.GetString(idc.Qword(idc.Qword(idc.Qword(base) + index * 8) + 8 * 2))
# → "onCreate"

4.3 指令指针追踪脚本

Python
import re

def fn_f8():
    idaapi.step_over()
    GetDebuggerEvent(WFNE_SUSP | WFNE_CONT, -1)

def run_next():
    fn_f8()
    pc = idc.GetRegValue('pc')
    asm_str = idc.GetDisasm(pc)
    
    # 查找写入指令指针的位置
    match = re.match(r'STR\s+(\S+),\s\[SP,#0x15A0\+var_1460\]', asm_str, re.I)
    if match:
        reg = match.group(1)
        print(f'Found instruction pointer store at {hex(pc)}: {asm_str}')
        return
    
    run_next()

run_next()

4.4 逐条指令解析

第一条指令: A3 20 5C 00 21 00

text
┌────────────────────────────────────────────────────────────────┐
│  VMP指令: A3 20 5C 00 21 00                                    │
├────────────────────────────────────────────────────────────────┤
│  解析过程:                                                      │
│  ┌──────┬──────┬──────────┬──────────┐                        │
│  │ A3   │ 20   │  5C 00   │  21 00   │                        │
│  ├──────┴──────┼──────────┼──────────┤                        │
│  │ A|G|op      │  BBBB    │  F|E|D|C │                        │
│  │ 2|0|A3      │  0x5C    │  0|0|2|1 │                        │
│  └─────────────┴──────────┴──────────┘                        │
│                                                                │
│  A = 2 (2个参数)                                               │
│  op = 0xA3 → 映射到真实opcode 0x6F (invoke-super)             │
│  BBBB = 0x5C → 方法索引(从MethodInfo表查找)                    │
│  21 & 0xF = 1 → 参数寄存器                                     │
├────────────────────────────────────────────────────────────────┤
│  真实指令:                                                      │
│  invoke-super {p0, p1},                                        │
│    Landroidx/fragment/app/FragmentActivity;->                  │
│    onCreate(Landroid/os/Bundle;)V                              │
├────────────────────────────────────────────────────────────────┤
│  汇编执行流程:                                                  │
│  LDR  X8, [SP,#0x15A0+var_1460]  ; 取指令指针                  │
│  LDRH W26, [X8,#4]               ; 取0x0021                    │
│  LDRH W8, [X8,#2]                ; 取0x005C(方法索引)          │
│  STR  W8, [SP,#0x15A0+var_7F4]   ; 保存方法索引                │
│  AND  X8, X26, #0xF              ; 解密参数: 0x21→0x01         │
└────────────────────────────────────────────────────────────────┘

第二条指令: 6B 10

text
┌────────────────────────────────────────────────────────────────┐
│  VMP指令: 6B 10                                                │
├────────────────────────────────────────────────────────────────┤
│  6B → 映射到 0x12 (const)                                      │
│  10 → v0, 0x1                                                  │
├────────────────────────────────────────────────────────────────┤
│  真实指令: const v0, 0x1                                       │
└────────────────────────────────────────────────────────────────┘

第三条指令: CC 20 13 02 01 00

text
┌────────────────────────────────────────────────────────────────┐
│  VMP指令: CC 20 13 02 01 00                                    │
├────────────────────────────────────────────────────────────────┤
│  CC → 映射到 0x6E (invoke-virtual)                             │
│  0213 → 方法索引 (requestWindowFeature)                        │
│  0001 → 参数                                                   │
├────────────────────────────────────────────────────────────────┤
│  真实指令:                                                      │
│  invoke-virtual {p0, v0},                                      │
│    Lcom/abing/appvmp/BaseActivity;->requestWindowFeature(I)Z   │
└────────────────────────────────────────────────────────────────┘

完整映射表

text
┌─────────────────────────────────────────────────────────────────────────┐
│                     VMP Opcode ↔ 真实DEX Opcode 映射表                  │
├──────────────────────────┬──────────────────────────┬───────────────────┤
│      VMP 指令            │      真实 DEX 指令        │      操作类型     │
├──────────────────────────┼──────────────────────────┼───────────────────┤
│  A3 20 5C 00 01 00       │  6F 20 02 15 21 00       │  invoke-super     │
├──────────────────────────┼──────────────────────────┼───────────────────┤
│  6B 10                   │  12 10                   │  const            │
├──────────────────────────┼──────────────────────────┼───────────────────┤
│  CC 20 13 02 01 00       │  6E 20 96 B1 01 00       │  invoke-virtual   │
├──────────────────────────┼──────────────────────────┼───────────────────┤
│  55 11 6D 00             │  5B 11 80 69             │  iput-object      │
├──────────────────────────┼──────────────────────────┼───────────────────┤
│  53 10 6D 00             │  54 10 80 69             │  iget-object      │
├──────────────────────────┼──────────────────────────┼───────────────────┤
│  72 10 60 01 00 00       │  71 10 AD AD 00 00       │  invoke-static    │
├──────────────────────────┼──────────────────────────┼───────────────────┤
│  69 00                   │  0E 00                   │  return-void      │
└──────────────────────────┴──────────────────────────┴───────────────────┘

🔧 第五部分:DEX修复

5.1 修复前后对比

修复前(VMP保护):

Java
protected void onCreate(Bundle paramBundle) {
    JniLib.cV(new Object[] { this, paramBundle, Integer.valueOf(18) });
    throw new VerifyError("bad dex opcode");
}

修复后(还原真实逻辑):

Java
protected void onCreate(Bundle paramBundle) {
    super.onCreate(paramBundle);
    requestWindowFeature(1);
    this.a = this;
    c.a(this.a);
}

5.2 修复注意事项

text
┌─────────────────────────────────────────────────────────────────────────┐
│                          DEX修复注意事项                                 │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  1. 寄存器数量必须正确                                                   │
│     - 修改 registersSize 字段                                           │
│     - 否则 dex2jar 转换失败                                             │
│                                                                         │
│  2. 指令长度对齐                                                         │
│     - 多余位置用 NOP (0x0000) 填充                                      │
│     - 最后确保有 return-void (0x0E00)                                   │
│                                                                         │
│  3. 方法索引需要重新计算                                                 │
│     - VMP的0x5C要转换为真实DEX的方法索引                                 │
│     - 需要在DEX的method_ids表中查找                                     │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

📊 第六部分:总结

6.1 VMP技术特点

text
┌─────────────────────────────────────────────────────────────────────────┐
│                         VMP技术特点分析                                  │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ✓ 使用JNI解释执行opcode                                                │
│    └─ 最终通过 FindClass, GetMethodID, CallXxxMethod 实现               │
│                                                                         │
│  ✓ Opcode被替换                                                         │
│    └─ 0xA3 → 0x6F (invoke-super)                                       │
│    └─ 0x6B → 0x12 (const)                                              │
│    └─ 0xCC → 0x6E (invoke-virtual)                                     │
│                                                                         │
│  ✓ 参数寄存器编号不变                                                    │
│    └─ A|G字段保持原样,便于VMP解析                                       │
│                                                                         │
│  ✓ 方法信息预存储                                                        │
│    └─ 类名、方法名、签名存储在全局表中                                    │
│    └─ 用索引快速查找,提高效率                                           │
│                                                                         │
│  ✓ OLLVM混淆保护                                                         │
│    └─ 控制流平坦化 (FLA)                                                │
│    └─ 虚假控制流 (BCF)                                                  │
│    └─ 导致IDA F5失效                                                    │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

6.2 分析方法论

text
   分析VMP的关键步骤
   
   ① 绕过反调试
   ② Dump内存中的DEX
   ③ 识别VMP调用特征 (JniLib.cV)
   ④ 分析数据结构 (JavaInfo, DexCode)
   ⑤ 在JNI函数下断点逆向追踪
   ⑥ 建立opcode映射表
   ⑦ 还原真实指令并修复DEX

6.3 防护建议

text
┌─────────────────────────────────────────────────────────────────────────┐
│  当前VMP的弱点:                                                          │
│  • 仍依赖JNI调用,可以hook JNI函数获取信息                                │
│  • opcode只是简单替换,存在固定映射关系                                   │
│  • 效率较低(每条指令都需JNI调用)                                        │
│                                                                         │
│  更安全的方案:                                                           │
│  • Java2C + ARM指令虚拟化                                                │
│  • 自定义完整解释器(不依赖JNI)                                          │
│  • 动态opcode映射(每次加载不同)                                         │
└─────────────────────────────────────────────────────────────────────────┘
 






 

 

 

 

 

一、问题根源分析

这段代码无法被IDA自动识别为函数的主要原因有:

1. 代码位于非标准段 (.text&ARM.extab)

 
.text&ARM.extab:0000000000018F94
  • 这是 ARM Exception Table 段,通常存放异常处理信息
  • IDA默认不会在此段进行函数识别
  • 这是一种反调试/反分析技术:将可执行代码伪装成异常表数据

2. 缺少标准函数序言

 
asm
STP X28, X27, [SP,#-0x60]!  ; 直接开始保存寄存器
STP X26, X25, [SP,#0x10]
  • 没有标准的 STP X29, X30, [SP, #-XX]! 开头
  • IDA的启发式函数识别算法无法匹配

3. 嵌入了伪造的数据

 
asm
.text&ARM.extab:0000000000019398  DCQ 0xE04FC5930BA00336, ...
.text&ARM.extab:00000000000193B0  DCQ 0x129041F3DF695441, ...
  • 在代码中间插入了64位数据 (DCQ = Define Quad word)
  • 这些数据会破坏代码流分析
  • 这是典型的代码/数据混淆技术

4. 使用间接跳转表

 
asm
LDRSW X8, [X20,X9,LSL#2]  ; 从跳转表加载偏移
ADD   X8, X8, X20         ; 计算目标地址
BR    X8                   ; 间接跳转
```
- Switch语句通过计算跳转,IDA难以追踪所有分支
- 跳转表地址动态计算

### 5. **混淆的函数名**
```
j__lS0lIllO_l$0lSll_5l$$IIllSIlI05SlISS0I_5Ol$lO0llS5$
  • 超长且无意义的名称
  • 包含特殊字符 $,增加分析难度

二、解决方案(多种方法)

方法1: 强制定义函数(最快速)

操作步骤:

  1. 定位到函数起始地址 0x18F94
  2. P(Create Function)强制创建函数
  3. 如果失败,先执行以下操作:
 
python
# IDA Python 脚本
import idaapi
import idc

# 函数起始地址
func_start = 0x18F94
func_end = 0x19454  # RET指令地址 + 4

# 1. 取消现有定义
idc.del_items(func_start, idc.DELIT_SIMPLE, func_end - func_start)

# 2. 将区域标记为代码
idc.create_insn(func_start)

# 3. 强制创建函数
idc.add_func(func_start, func_end)

print(f"Function created at 0x{func_start:X}")

方法2: 处理嵌入的数据块

问题代码段:

 
asm
0x19398: DCQ 0xE04FC5930BA00336  ; 这些不是代码!
0x193A0: DCQ 0x76D66DD5BB77DF45
...
0x193C8: DCQ 0x8CB0EE2B2636D202
0x193D0: MOV W8, #3               ; 代码恢复

IDA Python 修复脚本:

 
python
import idc
import idaapi

# 数据块地址范围
data_start = 0x19398
data_end = 0x193D0

# 1. 删除错误的指令定义
idc.del_items(data_start, idc.DELIT_SIMPLE, data_end - data_start)

# 2. 定义为数据
for addr in range(data_start, data_end, 8):
    idc.create_qword(addr)  # 定义为64位数据
    idc.set_cmt(addr, "Fake data for anti-analysis", 0)

# 3. 从数据块后重新创建代码
idc.create_insn(data_end)

# 4. 重新分析函数
idc.plan_and_wait(0x18F94, 0x19454)

方法3: 修复跳转表引用

IDA Python 脚本:

 
python
import idc
import idaapi

# 跳转表地址
jump_table_addr = 0x19458

# Switch语句地址
switch_addr = 0x1901C

# 1. 定义跳转表
for i in range(17):  # 17个case
    offset_addr = jump_table_addr + i * 4
    idc.create_dword(offset_addr)
    
    # 读取偏移值
    offset = idc.get_wide_dword(offset_addr)
    # 计算目标地址(相对于跳转表基址)
    target = jump_table_addr + offset
    
    # 添加代码交叉引用
    idc.add_cref(switch_addr, target, idc.fl_JN)
    
    print(f"Case {i}: offset=0x{offset:X}, target=0x{target:X}")

# 2. 在switch地址添加注释
idc.set_cmt(switch_addr, f"Switch jump table at 0x{jump_table_addr:X}", 1)

方法4: 完整自动化修复脚本

 
python
"""
IDA Pro 自动修复脚本
修复 .text&ARM.extab 段中的混淆函数
"""

import idc
import idaapi
import ida_segment
import ida_funcs

def fix_obfuscated_function():
    """修复混淆的函数"""
    
    # ========== 配置 ==========
    func_start = 0x18F94
    func_end = 0x19454
    fake_data_start = 0x19398
    fake_data_end = 0x193D0
    jump_table = 0x19458
    num_cases = 17
    
    print("[+] Starting function recovery...")
    
    # ========== 步骤1: 清理现有定义 ==========
    print("[*] Step 1: Cleaning existing definitions...")
    idc.del_items(func_start, idc.DELIT_EXPAND, func_end - func_start)
    
    # ========== 步骤2: 处理嵌入的假数据 ==========
    print("[*] Step 2: Handling fake embedded data...")
    for addr in range(fake_data_start, fake_data_end, 8):
        idc.del_items(addr, idc.DELIT_SIMPLE, 8)
        idc.create_qword(addr)
        idc.set_cmt(addr, "Anti-analysis fake data", 0)
    
    # ========== 步骤3: 重新创建代码 ==========
    print("[*] Step 3: Recreating code...")
    
    # 从函数开始处创建指令
    current_addr = func_start
    while current_addr < fake_data_start:
        if idc.create_insn(current_addr) == 0:
            print(f"[!] Failed to create instruction at 0x{current_addr:X}")
            current_addr += 4
        else:
            current_addr = idc.next_head(current_addr)
    
    # 跳过假数据区域
    current_addr = fake_data_end
    while current_addr < func_end:
        if idc.create_insn(current_addr) == 0:
            current_addr += 4
        else:
            current_addr = idc.next_head(current_addr)
    
    # ========== 步骤4: 修复跳转表 ==========
    print("[*] Step 4: Fixing jump table...")
    
    switch_insn = 0x1901C  # BR X8 的地址
    
    for i in range(num_cases):
        offset_addr = jump_table + i * 4
        idc.create_dword(offset_addr)
        
        # 读取偏移(有符号)
        offset = idc.get_wide_dword(offset_addr)
        if offset & 0x80000000:  # 负数
            offset = offset - 0x100000000
        
        target = jump_table + offset
        
        # 确保目标是代码
        idc.create_insn(target)
        
        # 添加交叉引用
        idc.add_cref(switch_insn, target, idc.fl_JN)
        
        # 添加注释
        idc.set_cmt(offset_addr, f"Case {i} -> 0x{target:X}", 0)
    
    # ========== 步骤5: 创建函数 ==========
    print("[*] Step 5: Creating function...")
    
    if idc.add_func(func_start, func_end):
        print(f"[+] Function successfully created at 0x{func_start:X}")
        
        # 设置函数名
        new_name = "parse_and_allocate_data_structures"
        idc.set_name(func_start, new_name, idc.SN_FORCE)
        print(f"[+] Function renamed to: {new_name}")
        
        # 添加函数注释
        idc.set_func_cmt(func_start, 
            "Deobfuscated function\n"
            "Original name: j__lS0lIllO_l$0lSll_5l$$IIllSIlI05SlISS0I_5Ol$lO0llS5$\n"
            "Function parses input data and allocates multiple data structures\n"
            "Uses state machine with 17 cases for control flow obfuscation", 
            1)
        
    else:
        print("[!] Failed to create function, trying manual analysis...")
        ida_funcs.add_func(func_start)
    
    # ========== 步骤6: 标记段属性 ==========
    print("[*] Step 6: Updating segment permissions...")
    
    seg = ida_segment.getseg(func_start)
    if seg:
        # 确保段有执行权限
        seg.perm |= ida_segment.SEGPERM_EXEC
        ida_segment.update_segm(seg)
        print(f"[+] Segment permissions updated: {seg.name}")
    
    # ========== 完成 ==========
    print("[+] Function recovery completed!")
    print(f"[+] You can now press F5 to decompile at 0x{func_start:X}")
    
    # 自动跳转到函数
    idc.jumpto(func_start)

# 执行修复
if __name__ == "__main__":
    fix_obfuscated_function()

方法5: 使用IDC脚本(简化版)

如果不熟悉Python,可以用IDA自带的IDC:

 
c
#include <idc.idc>

static main() {
    auto func_start = 0x18F94;
    auto func_end = 0x19454;
    auto fake_data_start = 0x19398;
    auto fake_data_end = 0x193D0;
    
    Message("Starting function fix...\n");
    
    // 1. 删除现有定义
    del_items(func_start, DELIT_EXPAND, func_end - func_start);
    
    // 2. 处理假数据
    auto addr;
    for (addr = fake_data_start; addr < fake_data_end; addr = addr + 8) {
        MakeQword(addr);
        MakeComm(addr, "Fake data");
    }
    
    // 3. 创建代码
    auto ea;
    for (ea = func_start; ea < fake_data_start; ea = NextAddr(ea)) {
        MakeCode(ea);
    }
    for (ea = fake_data_end; ea < func_end; ea = NextAddr(ea)) {
        MakeCode(ea);
    }
    
    // 4. 创建函数
    if (MakeFunction(func_start, func_end)) {
        Message("Function created successfully!\n");
        MakeName(func_start, "recovered_function");
    } else {
        Message("Failed to create function!\n");
    }
    
    // 跳转到函数
    Jump(func_start);
}

使用方法

  1. 在IDA中按 Alt+F7 打开脚本窗口
  2. 粘贴上述代码
  3. 点击 "Run" 执行

三、手动修复步骤(无需脚本)

如果不想用脚本,可以手动操作:

步骤A: 取消伪造数据

  1. 定位到 0x19398
  2. 选中到 0x193C8(按住Shift点击)
  3. U 键取消定义
  4. D 键重新定义为数据

步骤B: 创建代码

  1. 回到 0x18F94
  2. C 键创建代码
  3. 如果失败,按 U 再按 C

步骤C: 创建函数

  1. 光标在 0x18F94
  2. P 键(Create Function)
  3. 如果提示错误,选择 "Yes" 强制创建

步骤D: 修复跳转表

  1. 定位到 0x1901C(BR X8 指令)
  2. 右键 → "Specify switch idiom"
  3. 填写参数:
    • Number of cases: 17
    • Jump table address: 0x19458
    • Default case: 0x19008

四、根本原因总结

这段代码采用了以下反分析技术:

混淆技术目的对抗方法
代码放入异常表段 隐藏真实代码 手动分析或修改段属性
嵌入假数据 破坏线性扫描 标记数据区域
间接跳转表 隐藏控制流 重建跳转表
非标准函数序言 避免模式匹配 手动创建函数
状态机混淆 复杂化逻辑 符号执行或动态调试

五、推荐的完整操作流程

 
bash
# 1. 在IDA中打开文件
# 2. File → Script File → 选择上面的Python脚本
# 3. 等待执行完成
# 4. 在 0x18F94 按 F5 反编译
# 5. 如果反编译失败,使用方法1-4的组合

最终效果

  • 函数被正确识别
  • 可以按 F5 查看伪代码
  • 交叉引用正常工作
  • 函数调用图正确

六、验证修复是否成功

修复后检查以下几点:

 
python
# 验证脚本
import idc

func_addr = 0x18F94

# 1. 检查是否是函数
if idc.get_func_attr(func_addr, idc.FUNCATTR_START) == func_addr:
    print("[+] Function exists!")
else:
    print("[-] Not a function")

# 2. 检查函数边界
func_end = idc.get_func_attr(func_addr, idc.FUNCATTR_END)
print(f"[*] Function range: 0x{func_addr:X} - 0x{func_end:X}")

# 3. 检查交叉引用数量
xrefs = list(idautils.XrefsTo(func_addr))
print(f"[*] Number of xrefs: {len(xrefs)}")

# 4. 尝试反编译
import ida_hexrays
try:
    cfunc = ida_hexrays.decompile(func_addr)
    print("[+] Decompilation successful!")
except:
    print("[-] Decompilation failed")

使用上述任一方法后,这个函数应该可以被正确识别和分析了!

 

 

 

 

待完成:

 

 脱ke后不一定保证没有其他地方验证,需要进一步手动测试app的正确性,油正懒投app登录时出现服务器繁忙,请稍后重试的错误

 

 

 

 

 

 

 

 

 

 

 

 

 

https://d0nuts33.github.io/2022/11/07/vmp%E5%8A%A0%E5%9B%BA%E5%88%9D%E6%8E%A2%EF%BC%88%E4%B8%8A%EF%BC%89/

image

 

 

https://baijiahao.baidu.com/s?id=1586743591867534431&wfr=spider&for=pc

https://bbs.binmt.cc/thread-135925-1-1.html

https://cloud.tencent.com.cn/developer/article/1803293

https://www.anquanke.com/post/id/263197

https://www.cnblogs.com/uestcbutcher/p/7000929.html

https://zhuanlan.zhihu.com/p/631341327

https://zhuanlan.zhihu.com/p/1895176369614323793

https://blog.csdn.net/feibabeibei_beibei/article/details/78986987

https://blog.csdn.net/weixin_39738152/article/details/111000036

https://bbs.kanxue.com/thread-257061.htm

https://bbs.kanxue.com/thread-273614-1.htm

 https://d0nuts33.github.io/2022/11/24/vmp%E5%8A%A0%E5%9B%BA%E5%88%9D%E6%8E%A2%EF%BC%88%E4%B8%8B%EF%BC%89/

image

 [1] Android安全-app加固分析狗尾续貂之dex vmp还原. http://www.yxfzedu.com/article/12204
[2] [原创]android so文件攻防实战-libDexHelper.so反混淆. https://bbs.kanxue.com/thread-273614.htm
[3] [原创]vmp入门(一):android dex vmp还原和安全性论述. https://bbs.kanxue.com/thread-281427.htm
[4] [原创]梆梆企业版加固技术之防篡改剖析. https://bbs.kanxue.com/thread-223141.htm

 https://bbs.kanxue.com/thread-257061.htm
https://bbs.kanxue.com/thread-280513.htm
https://bbs.kanxue.com/thread-277006.htm
https://bbs.kanxue.com/thread-273614.htm
https://bbs.kanxue.com/thread-257061.htm
 
 

https://bbs.kanxue.com/thread-257061-1.htm
[原创]某企业级加固[四代壳]VMP解释执行+指令还原


现在的VMP的比较常见了,应该也是稳定性满足要求了,今天来分析一波,如有不当还请各位大佬指正
实际上 libdexjni.so在不同的APP中体积会不一样,应该是硬编码写入字符串和指令导致的

1-VMP还是先看下opcode部分知识,DEX指令格式
代码转换成DEX指令先看代码
public class BaseActivity extends FragmentActivity {

public Resources getResources() {
return super.getResources();
}
}

对应的第一条指令是
6F 10 1C 33 01 00 0C 00 11 00
每条指令是2字节,所以先看第一条 6f 20,根据官方文档 6F的解释是 invoke-super 格式为35c

A|G|op BBBB 35c [A=5] op {vC, vD, vE, vF, vG},
F|E|D|C meth@BBBB

[A=5] op {vC, vD, vE, vF, vG}, site@BBBB

[A=5] op {vC, vD, vE, vF, vG}, type@BBBB

[A=4] op {vC, vD, vE, vF}, kind@BBBB

[A=3] op {vC, vD, vE}, kind@BBBB

[A=2] op {vC, vD}, kind@BBBB

[A=1] op {vC}, kind@BBBB

[A=0] op {}, kind@BBBB

 

 

 

A | G | op BBBB F|E|D|C

根据opcode克制总共6个字节,对应的就是

A=1 G=0 op=6f BBBB就是 331c,然后是C=1 D=0 E=0 F=1

所以这里转换过来就是

invoke-super {p0}, Landroidx/fragment/app/FragmentActivity;->getResources()Landroid/content/res/Resources;

2-反调试
通过常规手段,在关键的open函数观察,然后逆向查找
发现几处反调试
0x47CAC 处是创建线程,检测运行时间,getpid 然后 linux_eabi_syscall(__NR_kill, a1, a2)来杀死进程
0x047C70 处是cmdline反调试,https://bbs.pediy.com/thread-223460.htm 这位大佬提到过
0x489EC 处是 /proc/status检测反调试
实际可能还有,但是在找到这三处之后,我发现特殊的地方是刚好在JNI_OnLoad处有个总的入口,所以直接
nop指令反调试就gg了
我用 arm64调试的 mov w1,w1 对应的的hex是E103012A
然后dump出dex,先内存找到dex.035
右侧蓝色和高亮部分显示的字符如下:

第一行:asses.jar....... (推测为 assets.jar)

第二行:.h...h....H.dex. (该部分被黄色高亮,并有红色箭头指向)

第三行:035.P.y\.....Ic.

第四行:....F.i.7..2|...

第五行:p...xV4.........

第六行:........p.......

import struct


start = 0x75172191ec

dump_so = "/Users/beita/tmp/bangbang/dump_vmp.dex"
length = 0x6ee27c
file = open(dump_so,'w')
file.close()
fn = AskStr(dump_so ,"save as:")
with open(fn,"wb+") as f:
for addr in range(start , start+length):
f.write(struct.pack("B" , Byte(addr)))
print "success to save as "

3-VMP的具体分析
得到dex之后,转成jar,看了下,大部分函数是 JniLib.cV等来做的,但是有一个Integer.valueof,是一个函数索引,用来查找指令的
public void o()
{
JniLib.cV(new Object[] { this, Integer.valueOf(17) });
throw new VerifyError("bad dex opcode");
}

protected void onCreate(Bundle paramBundle)
{
JniLib.cV(new Object[] { this, paramBundle, Integer.valueOf(18) });
throw new VerifyError("bad dex opcode");
}

protected void onDestroy()
附加调试发现实际在这里解开这个java数组也就是 new Object的这个数组
if ( !v6 )
{
v17 = (unsigned __int64)((__int64 (__fastcall *)(JNIEnv *, __int64))(*v5)->GetArrayLength)(v5, v4) - 1;
v12 = ((__int64 (__fastcall *)(JNIEnv *, __int64))(*v5)->GetObjectArrayElement)(v5, v4);
v13 = _JNIEnv::CallIntMethod(v5, v12, qword_7516104A00);
result = j___sl_OS__II0__IIIll_O_sls_5000___IOilsllIII_OIS_OS5_(v13);
v14 = result;
}
这里用onCreate来分析 索引是18=0x12
JniLib.cV(new Object[] { this, paramBundle, Integer.valueOf(18) });
调试往下走,根据这个索引,会取出一个结构体信息,结合上下文信息
F0 4E A3 17 75 00 00 00 00 00 00 00 00 00 00 00
12 00 00 00 2E 00 00 00 50 6B A9 17 75 00 00 00 <-- 红色箭头指向处
03 00 00 00 02 00 00 00 02 00 00 00 00 00 00 00
F8 4E A3 17 75 00 00 00 00 00 00 00 00 00 00 00
03 00 02 00 02 00 00 00 FB 30 01 00 0F 00 00 00
A3 20 5C 00 21 00 6B 10 CC 20 13 02 01 00 55 11
6D 00 53 10 6D 00 72 10 60 01 00 00 69 00 00 00
12 00 00 00 56 00 00 00 00 8D A9 17 75 00 00 00

这里取出 0x7517a96b50的值 是 0x12
case 1u:
v31 = *(_DWORD *)(v14 + 16);
v32 = *(_DWORD *)(v14 + 20);
v33 = *(_DWORD *)(v14 + 24);
v34 = *(_QWORD *)(v14 + 32);
v35 = *(_QWORD *)(v14 + 8);
v36 = *(_DWORD *)v14;
v37 = *(_DWORD *)(v14 + 4);

strut JavaInfo {
uint32_t index; // 0x12 这是java层传递的
uint32_t unknow2; // 0x2e 未知
uint64_t dexcode; // dexcode指针
uint32_t unknow4; // 0x03
uint32_t unknow5; // 0x02
uint32_t unknow6; // 0x02 这里看起来没有用到 但是貌似是DexCode的内容
};
跳转到dexcode的位置看下内容
内存地址,00 01 02 03 04 05 06 07,08 09 0A 0B 0C 0D 0E 0F
...E6730,03 00 02 00 02 00 00 00,FB 30 01 00 0F 00 00 00
...E6740,A3 20 5C 00 21 00 6B 10,CC 20 13 02 01 00 55 11
...E6750,6D 00 53 10 6D 00 72 10,60 01 00 00 69 00 00 00
...E6760,13 00 00 00 56 00 00 00,00 3A 2E 17 75 00 00 00
...E6770,03 00 00 00 01 00 00 00,02 00 00 00 00 00 00 00

struct DexCode {
u2 registersSize; // 3
u2 insSize; 、、 2
u2 outsSize;
u2 triesSize;
u4 debugInfoOff; /* file offset to debug info stream */
u4 insnsSize; /* size of the insns array, in u2 units */
u2 insns[1];
}

registerSize = 3
insSize = 2
outsSize = 0
.....
主要看
insnsSize = 0xf
共15条指令 ,但是这个指令不是 标准的dex指令 opcode被改过,且字符串信息也是被改过,就是是说他不是系统来解析的,而且会有一个对应关系

A3 20 5C 00 21 00 6B 10 CC 20 13 02 01 00 55 11
6D 00 53 10 6D 00 72 10 60 01 00 00 69 00

进入到vm_parse函数之前的代码还能F5看下逻辑,但是到 vm_parse地址是29b70位置处,F5不好用了,貌似是刻意把这个函数写的非常大,
有点像dalvik里边的HANDLE那种搞到一起, 这样在加固过程中OLLVM混淆之后,更加复杂

在解析opcode之前会进行数据保存
信息看起来是保存到一组结构中
1
2
3
4
5
6
7
8
9
10
struct Infos1{
uint64_t data1;
uint64_t *data2; // data2 = malloc(32) 是根据JavaInfo的dexCode来的
uint64_t data3;
uint64_t data4;
uint64_t data5;
uint64_t data6;
uint64_t data7;
uint64_t data8; // JavaInfo的data3的值
};


调试继续往下走,来到 j___Sl_I5_lO000_0SSIO_I0_O__OI_5I___lSSl0_lO5_0I5I5S5_ 这个函数,这个函数不能F5了,要根据汇编来分析具体的vm是如何
解析opcde来实现代码运行的

最终的 入口是 29b70这个函数
调用获取GetMethodID的过程是
vm_parse 29b70 - 29bb0 - 4ae80 - 4aeb4 - 4e78c - 3f92c 开始获取名称和GetMethodID
第一个参数 结合全局变量可以获得这些内容Class MethodSig MethodName

前面提到,vmp可能会借助jni来实现,所以现在GetMethodID下段点,查看数据,方法名称和签名
内存地址,指令,操作数,注释 (Comments)
.text:0000007517CF997C,MOV,"X19, X0",
.text:0000007517CF9980,LDR,"X8, [X21]",
.text:0000007517CF9984,LDR,"X1, [X19]",
.text:0000007517CF9988,LDR,"X8, [X8, #0x30]",; FindClass 参数是 android/xxx/FragmentActivity
.text:0000007517CF998C,MOV,"X0, X21",
.text:0000007517CF9990,BLR,X8,
.text:0000007517CF9994,LDR,"X8, [X21]",
.text:0000007517CF9998,LDP,"X3, X2, [X19, #8]",
.text:0000007517CF999C,LDR,"X8, [X8, #0x108]",; GetMethodID onCreate
.text:0000007517CF99A0,MOV,"X9, X0",
.text:0000007517CF99A4,STR,"X9, [SP, #0x15A0+var_7D8]",
.text:0000007517CF99A8,MOV,"X0, X21",
寄存器x2
十六进制数据:显示了如 6F 6E 43 72 65 61 74 65 等数值。

文本内容:右侧的 ASCII 解码显示了 onCreate 字符串。
寄存器x3
十六进制数据:包含了文件路径和类名的编码。

文本内容:右侧可以看到 (Landroid/os/Bundle;) 等字样。

但是因为被ollvm混淆过,体积非常大,可能是fla和bcf都加上去了
这个函数IDA识别基本上是卡死状态,所以只能是找关键点切入
看一下OLLVM的图,被混的 单个switch有几千个,而且F5卡死了

所以用快速定位关键汇编位置


分析ollvm个人觉得一点技巧是找到关键的block,下好断点,走一遍,逆向查找,基本上如果不是很大的代码块都能梳理清楚逻辑
大致如下图
流程图标题 :VMP解析流程
流程步骤 :

1. 函数开始运行
2. vm_parse
- step1 找到dex指令对应关系
- step2 查找栈上的存储位置
- step3 假设这里用到字符串
3. 函数运行结束

现在反方向去找到是从哪里获取到的字符串,这个字符串是如何从DexCode取出来的,那么这个vm解释执行的逻辑差不多就清楚了

倒推代码来了解逻辑

上面的onCreate是根据在函数j__$S$0l0$SOOII$0lIll$SI_O0$S0ll__Il_S5lIl5lOlI5SO0S5$ 这里,根据一个输入值返回的结构体来得到的
计算处一个全局变量的偏移值
return *(_QWORD *)(qword_7517D666A0 + 8LL * a1);
其实是个结构体
用IDA直接取字符串看一下
idc.GetString(idc.Qword(idc.Qword(idc.Qword(0x7517D666A0) + 0x5C * 1) + 8 *n))
n=0是类名 android/support/v4/app/FragmentActivity
n=1是方法的参数签名 (Landroid/os/Bundle;)V
n=2是方法名称 onCreate
看起来是个如下的结构结构
1
2
3
4
5
struct {
void *class_name;
void *method_sig;
void *method_name;
}
所以JNI调用的onCreate来自这个结构体,实际上如果做过java2c的一看就知道是调用super.onCreate在
然后再网上查看汇编,找到这个结构体是从哪里来的
地址 (Address),指令 (Mnemonic),操作数 (Operands)
.text:0000007517CF9940,ADD,"X1, X1, #0x78"
.text:0000007517CF9944,MOV,"X24, X26"
.text:0000007517CF9948,MOV,"W23, W21"
.text:0000007517CF994C,LDR,"X21, [SP,#0x15A0+var_778]"
.text:0000007517CF9950,STR,"W22, [SP,#0x15A0+var_7BC]"
.text:0000007517CF9954,MOV,"W22, W6"
.text:0000007517CF9958,MOV,"X0, X21"
.text:0000007517CF995C,LDR,"X2, [SP,#0x15A0+var_780]"
.text:0000007517CF9960,LDR,"W3, [SP,#0x15A0+var_814]"
.text:0000007517CF9964,MOV,"W26, W25"
.text:0000007517CF9968,MOV,"W25, W30"
.text:0000007517CF996C,BL,sub_7517D17CF4
.text:0000007517CF9970,MOV,"X0, X21"
.text:0000007517CF9974,LDR,"W1, [SP,#0x15A0+var_7F4] (红色箭头所指)"
.text:0000007517CF9978,BL,.j_$$S$010$S00II$01I11$SI_00$S011__I1_S51I151olI5S00$S$ ...
.text:0000007517CF997C,MOV,"X19, X0"
.text:0000007517CF9980,LDR,"X8, [X21]"

函数入参存放在x1寄存器 就是w1,而且是在站栈上
LDR W1, [SP,#0x15A0+var_7F4]
根据这局汇编反向推一下 LDR取值必然有一个STR赋值
STR X1, [SP,#0x15A0+var_7F4]
借助IDAPython来查找一下,之所以不用快捷键x去直接找,是因为需要找到调用顺序,所以在2b970的位置开始用脚本
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
last_insns = ''

def fn_f8():
idaapi.step_over()
GetDebuggerEvent(WFNE_SUSP | WFNE_CONT, -1)

def fn_f9():
idaapi.continue_process()
GetDebuggerEvent(WFNE_SUSP | WFNE_CONT, -1)


last_ins = ''
def run_next():
fn_f8()

asm_str = idc.GetDisasm(idc.GetRegValue('pc'))

cur_match = re.match(r'STR\s+(\S+),\s\[SP,#0x15A0\+var_1460\]', asm_str, re.M | re.I)
if cur_match :
reg1 = cur_match.group(1)

value = hex(idc.Word(idc.Qword(reg1) + 2))
print('nop addr', hex(cur_addr), asm_str)
return
else:
last_ins = asm_str

run_next()


run_next()

最终找到这个上面输入的5C是从最开始的 结构体里面的DexCode取出来的
如下
A3 20 5C 00 21 00

然后利用这个思路,找到指向指令insns的指针,实际就是在29b70处判断,当前的STR放入的指针是否是前边解出来的insns地址
找到指令指针是在存放在栈上的一个地址 [SP,#0x15A0+var_1460]


1. 汇编指令区 (Disassembly View)
这是左侧的主视图,显示了 ARM64 指令。
地址,指令,操作数,备注
00000075298368D8,LDR,"X8, [SP,#0x15A0+var_1470]",
00000075298368DC,LDR,"X11, [SP,#0x15A0+var_1460]",红色箭头指向行
00000075298368E0,LDRH,"W9, [X11]",
00000075298368E4,LDR,"X10, [SP,#0x15A0+var_7A0]",
00000075298368E8,SUB,"X10, X11, #1",
00000075298368EC,LDR,"X10, [X8, X10, LSL#3]",
00000075298368F0,EOR,"W8, W8, W9",
00000075298368F4,LDR,"W9, [SP,#0x15A0+var_790]",
2. 寄存器状态区 (Register View)
这是右上角的面板,显示了当前的寄存器数值及指向的内存区域。

X11: 0000007517AEA460 (红色箭头所指,对应内存地址)

X8: 0000007529B5BA4C (.text:jpt_2A6C0)

PC: 00000075298368E0 (当前执行位置)

SP: 00000075FBA774D0

其他寄存器涉及 dalvik_main_space 和 libc_malloc 等内存段。

3. 十六进制内存视图 (Hex View-1)
这是底部显示的内存数据,对应 X11 所指向的地址 0x7517AEA460。
地址偏移,00 01 02 03 04 05 06 07,08 09 0A 0B 0C 0D 0E 0F,文本表示
...A450,03 00 00 02 00 00 00 00,FB 30 01 00 0F 00 00 00,..0.....
...A460,A3 20 5C 00 21 00 6B 10,CC 20 13 02 01 00 55 11,..\.!.k... ...U.
...A470,6D 00 53 10 6D 00 72 10,60 01 00 00 69 00 00 00,m.S.m.r.

 

看上图所示,从栈 SP,#0x15A0+var_1460的地址 07517AEA460 得到的正是insns的地址

A3 20 5C 00 21 00
---------------------------------------------------------------------------------------------------------------------------------------------------------
此时联系到onCreate的地方,用到了5c 而我们根据1460推导出了5c的来源
地址,指令,操作数,中文注释内容
.text:00000075164AA78C,LDR,"X8, [SP,#0x15A0+var_1460]",; jumptable 000000000047F0C case 2123
.text:00000075164AA790,LDRH,"W26, [X8,#4]",; 指令指针向前加4,取高位,也就是取出 0021
.text:00000075164AA794,LDRH,"W8, [X8,#2]",; 第一组指令向前2个字节,取出0x5c
.text:00000075164AA798,STR,"W8, [SP,#0x15A0+var_7F4]",; 结果是0x5c再存放到7f4处
.text:00000075164AA79C,AND,"X8, X26, #0xF",; 与运算一下 解密依一下
.text:00000075164AA7A0,LDR,"X9, [SP,#0x15A0+var_780]",
这里很清楚了,实际上指令的格式还是没变 则,解释执行OPCODE

还是6字节
A3 20 5C 00 21 00
A | G | op BBBB F|E|D|C
对应一下就是 A=2 G=0 op=A3 BBBB是0x5C C=0 D=0 E=0 F=1
这里的第三组也就是 0021解密一下 0021 & 0xf = 0001
0x5c在Android自带虚拟机里变解释执行为 MethodID,这里vmp使用的是自定义存放的一个结构体,估计是为了快速查找,因为按照逻辑,是要从DEX里边取查找,可能是为了提高效率,所以保存起来
并且我看到vmp虚拟化的java函数越多,libdexjni.so的体积越大
.text:000000751649B96C BL sub_75164B9CF4
.text:000000751649B970 MOV X0, X21
.text:000000751649B974 LDR W1, [SP,#0x15A0+var_7F4] ; 指令里边取出来的0x5c 结合全局变量来取函数名称信息结构体
.text:000000751649B978 BL .j__$S$010$S00II$01I11$SI_O0$S011__I1_S51I151O1I5S00S5$
.text:000000751649B97C MOV X19, X0
.text:000000751649B980 LDR X8, [X21]
.text:000000751649B984 LDR X1, [X19]
.text:000000751649B988 LDR X8, [X8,#0x30] ; findclass
.text:000000751649B98C MOV X0, X21
.text:000000751649B990 BLR X8
.text:000000751649B994 LDR X8, [X21]
.text:000000751649B998 LDP X3, X2, [X19,#8]
.text:000000751649B99C LDR X8, [X8,#0x108] ; Getmethodid oncreate
.text:000000751649B9A0 MOV X0, X21
继续调试往下走,你会看到 CallNonVirtualMethod 正是 super.onCreate
很熟悉的格式 [A=2] op {vC, vD}, kind@BBBB
--------------------------------------------------------------------------------------------------------------------------------
a320 是invoke-super
005c是取MethodID
0021 解密0001 实际上是参数v0 但是我觉得这个解密多余的 因为取前边2位的
则这条指令是 invoke-super {p0, p1}, Landroidx/fragment/app/FragmentActivity;->onCreate(Landroid/os/Bundle;)V
--------------------------------------------------------------------------------------------------------------------------------
同样往下走 用脚本跑,到这里停下,刚好是前边的6个字节的指令执行完了的地方 下一组指令
16494D88 LDR X8, [SP,#0x15A0+var_1460] ; jumptable 000000000002A6C0 case 674
16494D8C ADD X8, X8, #6 ; 往后6个字节,就是invoke-super解析完了的位置
16494D90 STR X8, [SP,#0x15A0+var_1460]
16494D94 MOV W8, #0x456
16494D98 B loc_75164B49C0
这里取出来是
6B 10
调试发现实际就是 定义了一个数值
--------------------------------------------------------------------------------------------------------------------------------
6b const
10 v0,0x1
结果就是 const v0,0x1
--------------------------------------------------------------------------------------------------------------------------------

next
--------------------------------------------------------------------------------------------------------------------------------
CC 20 13 02 01 00[A=2]op{vC, vD},kind@BBBB
CC 20 invoke-virtual
0213 取MethodiD requestWindowFeature (I)Z
0001 参数
invoke-virtual {p0, v0}, Lcom/abing/appvmp/BaseActivity;->requestWindowFeature(I)Z
--------------------------------------------------------------------------------------------------------------------------------

next

--------------------------------------------------------------------------------------------------------------------------------

5511 6d00[A=2]op{vC, vD},kind@BBBB

1155 invoke-virtual

006d 取MethodiD requestWindowFeature (I)Z

0001 参数

iput-object p0, p0,Lcom/wangzhong/fortune/ui/activity/BaseActivity;->a:Lcom/wangzhong/fortune/ui/activity/BaseActivity;;

--------------------------------------------------------------------------------------------------------------------------------

 

next 继续往下走,

在5feac处找到 这三句代码,运气不错,这里刻意F5,可以看到是 取出一个对象的值,根据分析得知是 BaseActivity的属性a
parseStringInfo(v8, v6[2]); // 解密一下字符串 BaseActivity
v10 = 15 - v11;
v9 = 28501 - v11;
break;
case 1:
v12 = v9 - 151 * ((unsigned int)(55554 * v9) >> 23);
v3 = ((__int64 (__fastcall *)(JNIEnv *, __int64, __int64))(*v8)->GetObjectField)(v8, v7, v4);// 获取BaseActivity的a对象
v10 = 102 - v12;
if ( !v5 )
{
v5 = 0LL;
v10 = 103 - v12;
}
break;

合起来就是执行了 5310 6d00 这条指令

case 2:
v13 = v9 - 11 * ((unsigned int)(47663 * v9) >> 19);
v5 = ((__int64 (__fastcall *)(JNIEnv *, _QWORD))(*v8)->FindClass)(v8, *v6);// FieldID获取
v4 = ((__int64 (__fastcall *)(JNIEnv *, __int64, _QWORD, _QWORD))(*v8)->GetFieldID)(v8, v5, v6[2], v6[1]);
if ( v4 )
--------------------------------------------------------------------------------------------------------------------------------

53 10 6D 00 [A=2]op{vC, vD},kind@BBBB

1053 invoke-virtual

006d 取MethodiD requestWindowFeature (I)Z

0001 参数

iget-object p0, p0,Lcom/wangzhong/fortune/ui/activity/BaseActivity;->a:Lcom/wangzhong/fortune/ui/activity/BaseActivity;

--------------------------------------------------------------------------------------------------------------------------------

 

 

next 脚本执行结果如下

75165086A0: using guessed type __int64 qword_75165086A0;
Python>idc.GetString(idc.Qword(idc.Qword(idc.Qword(0x75165086A0) + 0x160 * 8) + 8 *1))
(Landroid/app/Activity;)V
Python>idc.GetString(idc.Qword(idc.Qword(idc.Qword(0x75165086A0) + 0x160 * 8) + 8 *0))
com/wangzhong/fortune/f/c
Python>idc.GetString(idc.Qword(idc.Qword(idc.Qword(0x75165086A0) + 0x160 * 8) + 8 *2))
b

--------------------------------------------------------------------------------------------------------------------------------

72 10 60 01 00 00 [A=2]op{vC, vD},kind@BBBB

10 72 invoke-virtual

0160 取MethodiD requestWindowFeature (I)Z

0000 参数编号

invoke-static {v0}, Lcom/wangzhong/fortune/f/c;->a(Landroid/app/Activity;)V

--------------------------------------------------------------------------------------------------------------------------------

.text:000000751648CDD0 ; DATA XREF: .text:j__$S1$I5$10000$0S$S10$SI0$0$$OI 5I$$ 1S$10 105$0I5I58S$+30124↑o
.text:000000751648CDD0 LDR X8, [SP,#0x15A0+var_1470] ; jumptable 000000000002A6C0 case 1170
.text:000000751648CDD4 LDR X8, [X8,#0x10]
.text:000000751648CDD8 LDR X11, [SP,#0x15A0+var_1460]
.text:000000751648CDDC LDRH W9, [X11]
.text:000000751648CDE0 LDR X10, [SP,#0x15A0+var_7A0]
.text:000000751648CDE4 SUB X10, X11, X10
.text:000000751648CDE8 LSR X10, X10, #1
.text:000000751648CDEC LDR W8, [X8,X10,SXTW#2]
.text:000000751648CDF0 EOR W8, W8, W9
.text:000000751648CDF4 STR X8, [SP,#0x15A0+var_790]
.text:000000751648CDF8 AND W8, W8, #0xFF
.text:000000751648CDFC ADRP X9, #unk_75164D91F8@PAGE
.text:000000751648CE00 ADD X9, X9, #unk_75164D91F8@PAGEOFF
.text:000000751648CE04 ADD X8, X9, W8,UXTW#3
.text:000000751648CE08 LDR X13, [X8]
.text:000000751648CE0C MOV W8, #0x64A
.text:000000751648CE10 MOV W9, #0x121
.text:000000751648CE14 MOV W4, W22
.text:000000751648CE18 MOV W0, W28
.text:000000751648CE1C MOV W11, W30
.text:000000751648CE20 MOV W12, W20
.text:000000751648CE24 MOV W27, W30
图片 5 标注内容: 指令地址是 15a0 - 1460 的栈地址 所以 idapy 搜索 LDR STR 指令,就能跟进读取和使用指令的流程

next

 

--------------------------------------------------------------------------------------------------------------------------------

69 00 这个指令比较简单就是

return-void

--------------------------------------------------------------------------------------------------------------------------------

对应到dex指令 ,0x5c这些部分需要自己取dex里边查找MethodID和ClassName对应起来,就是算出MethodID的索引就行
这里的5c最终是要到dex里取查找的

 

把下面这部分指令的根据分析经过转换

A3 20 5C 00 01 00 6B 10 CC 20 13 02 01 00 55 11
6D 00 53 10 6D 00 72 10 60 01 00 00 69 00 00 00


VMP 的 opcode,映射关系,真实的 Dex 指令
A3 20 5C 00 01 00,------>,6f 20 02 15 21 00
6B 10,------>,12 10
CC 20 13 02 01 00,------>,6e 20 96 b1 01 00
55 11 6D 00,------>,5b 11 80 69
53 10 6D 00,------>,54 10 80 69
72 10 60 01 00 00,------>,71 10 ad ad 00 00
69 00,------>,0e 00


用流程图来说明下


┌───────────────────────────┐ ┌──────────────────────────────────┐
│ JNILib.cV(Integer类型的索引) │─────────>│ 读取下一条指令 进入分发器 超大的switch │
└──────────────┬────────────┘ └──────────────────────┬───────────┘
│ │
┌──────────────▼────────────┐ ┌──────────────────────▼───────────┐
│ 根据索引获取并且DexCode │ │ 示例 │
└──────────────┬────────────┘ │ a3 20 是指令和格式的值 │
│ │ 5c 00 是对应的class │
┌──────────────▼────────────┐ │ 01 00 是寄存器和变量值 │
│ 栈位置[SP,#0x15A0+var_1460]│─────────>│ 栈位置[SP,#0x15A0+var_1460] │
│ 是指令指针 │ │ 执行完指令指针移到 0100之后 │
└──────────────┬────────────┘ └──────────────────────┬───────────┘

┌──────────────────────▼───────────┐
│ dispatcher │
└───────────┬──────────┬───────────┘
│ │
┌───────────▼─┐ ┌─────▼────┐ ┌─────▼────┐ ┌─────▼────┐
│ mov │ │ invoke │ │ iget │ │ ...... │
└─────────────┘ └──────────┘ └──────────┘ └──────────┘

┌──────────────▼────────────┐
│ Function END │
└───────────────────────────┘

 

 

 


得到


修复前的指令 实际上 JNILib.cv这部分代码是填充的 只有一个索引有用,所以直接覆盖
字段名称,数值
字段名称,数值
uint debug_info_off,0h
uint insns_size,16h
▼ ushort insns[22],
  ushort insns[0],3012h
  ushort insns[1],23h
  ushort insns[2],1D9Ch
  ushort insns[3],112h
  ushort insns[4],44Dh
  ushort insns[5],100h
  ushort insns[6],1112h
  ushort insns[7],54Dh
  ushort insns[8],100h
  ushort insns[9],2112h
  ushort insns[10],313h
  ushort insns[11],12h
  ushort insns[12],1071h
  ushort insns[13],BD59h
  ushort insns[14],3h
  ushort insns[15],20Ch
  ushort insns[16],24Dh
  ushort insns[17],100h
  ushort insns[18],1071h
  ushort insns[19],6099h
  ushort insns[20],0h
  ushort insns[21],73h

修复后的指令
字段名称,数值
ushort outs_size,1h
ushort tries_size,0h
uint debug_info_off,0h
uint insns_size,16h
▼ ushort insns[22],
  ushort insns[0],206Fh
  ushort insns[1],1502h
  ushort insns[2],21h
  ushort insns[3],1012h
  ushort insns[4],206Eh
  ushort insns[5],B196h
  ushort insns[6],1h
  ushort insns[7],115Bh
  ushort insns[8],6980h
  ushort insns[9],1054h
  ushort insns[10],6980h
  ushort insns[11],1071h
  ushort insns[12],ADADh
  ushort insns[13],0h
  ushort insns[14],0h
  ushort insns[15],0h
  ushort insns[16],0h
  ushort insns[17],0h
  ushort insns[18],0h
  ushort insns[19],0h
  ushort insns[20],0h
  ushort insns[21],Eh
实际指令是 0xF所以其他的nop掉 最后给一个return void 就可以了

这里比较坑的一点是寄存器的数量一定要改,不然的话dex2jar转不了
修复前
protected void onCreate(Bundle paramBundle)
{
// 调用 VMP 解释器,传入索引 18
JniLib.cV(new Object[] { this, paramBundle, Integer.valueOf(18) });

// 垃圾指令,防止静态分析正常通过
throw new VerifyError("bad dex opcode");
}


修复后
protected void onCreate(Bundle paramBundle) {
super.onCreate(paramBundle);
requestWindowFeature(1);
this.a = this;
c.a(this.a);
}

 

总结 :
1-是用JNI来解释执行opcde的
2-op被替换了,但是 A G 那部分参数寄存器数字是不会变的,因为vmp也需要指定是几个参数,来使用
3-做过java2c的都比较熟悉,对dex的opcode比较熟悉的情况下,联系上下文很容易得到结果
4-这里的op可能被加密了,个人愚见人为这个Op加密不加密无所谓,因为最终实际上是个对应关系 0xff个opcode对应0xff个opcode


hookjni 可以看到很多输出信息 就是说vmp实际采用的还是 jni来实现

如果要全部都替换掉,需要挨个分析指令,做一个映射表岀来
--------------------------------------------------------------------------------------------------
目前来看还是java2c + arm指令虚拟化应该是比较保险的操作,因为自己写一个解释器,纯自己实现指令,肯定问题非常多,所以指令还是通过Jni来实现的,
但是效率貌似低了些,如果这种方式加上ARM指令虚拟化,分析起来可就难受很多了
------------------------------------------------------------------------------------
样本是以前的版本,目的是为了分析和学习,这里只提供so文件,交流经验,需要样本私聊我

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

金融密码杯2024年第一道安卓题目,公众号有介绍,是分析邦企的好例子,但是只能分析libdexhelper,没有libdexjni

样本:

https://wwbln.lanzouw.com/iR13B3hrm01g

https://gitee.com/null_465_7266/cn_pbcdci_fincryptography_appetizer

Bangcle (梆梆加固) 核心解壳逆向分析报告

1. 核心调用链可视化 (Arrow Flowchart)

以下图表展示了从 .init_proc 到最终代码解密还原的完整执行流。箭头表示调用或跳转关系,层级表示包含关系。

Plaintext
 
[.init_proc] (0x10C098: SO加载初始化入口)
     │
     │ 1. 启动解壳流程
     ▼
[sub_10C7DC] (准备解压缓冲区)
     │ ├──> 动作: sys_mmap (申请 RWX 内存)
     │ ├──> 动作: memcpy (拷贝压缩数据)
     │ └──> 调用主逻辑
     │
     ▼
[sub_10C50C] (CORE: 主解壳引擎 / 混淆控制流)
     │
     ├──> [完整性校验逻辑]
     │       ├──> 获取嵌入签名 (BL技巧: "__b_a_n_g_...")
     │       └──> 校验失败则跳转至反调试
     │
     ├──> [sub_10C2A8] (反调试检测)
     │       │ 1. 打开文件
     │       ├──> [sub_10C1C4] (sys_openat: "/proc/self/maps")
     │       │ 2. 读取内容
     │       ├──> [sub_10C1B8] (sys_read)
     │       │ 3. 匹配特征
     │       ├──> [sub_10C1E8] (memcmp: 搜索加固特征/Hook特征)
     │       │ 4. 发现异常
     │       └──> [sub_10C1DC] (sys_exit_group: 终止进程)
     │
     ├──> [sub_10C1A0] (sys_mprotect: 修改内存权限)
     │
     ├──> [sub_10C8F0] (解压包装函数)
     │       │
     │       └──> [sub_10C914] (aPLib 解压算法实现)
     │               ├──> 模式: Literal (直接拷贝)
     │               ├──> 模式: Short Match (短回溯)
     │               └──> 模式: Long Match (长回溯)
     │
     ├──> [XOR Decryption] (内联简单的异或解密循环)
     │
     ├──> [sub_10C224] (Cache Flush: 刷新指令缓存)
     │       ├──> dc cvau (清理数据缓存)
     │       ├──> dsb / isb (内存屏障)
     │       └──> ic ivau (无效化指令缓存)
     │
     ├──> [sub_10C3AC] (ELF重定位: 修复符号地址)
     │       ├──> 混淆特征: 控制流平坦化 (State Machine)
     │       └──> [sub_10C18C] (sys_mmap: 申请临时计算内存)
     │
     └──> [sub_10C180] (sys_munmap: 清理压缩数据和临时内存)
             │
             ▼
      (跳转至还原后的 OEP / Payload 执行)

2. 关键模块深度解析

2.1 基础架构:系统调用内联 (Inline Syscall)

Bangcle 为了躲避基于 PLT/GOT 表的 Hook 工具(如 Frida 的常规拦截),放弃了 libc 函数,直接使用汇编指令 svc #0 进内核。

  • 实现方式: 见图中 sub_10C1xx 系列函数。

  • 寄存器规则: X8 存系统调用号,X0-X5 存参数。

  • 对抗意义: open, read, mmap 等关键行为无法被常规 API Monitor 监控。

2.2 核心逻辑:主解壳函数 (sub_10C50C)

这是整个加固壳的大脑,其内部采用了复杂的逻辑保护:

  1. 自校验 (Signature Check): 程序利用 BL 指令获取当前 PC 地址,读取尾部嵌入的 __b_a_n_g_c_l_e_... 字符串,防止 Shellcode 被篡改或剥离。

  2. 反调试 (sub_10C2A8):

    • 原理: 扫描 /proc/self/maps

    • 目的: 检查内存映射中是否存在调试器特征(如 TracerPid 变化,或者内存中加载了异常的 SO 文件)。

    • 后果: 一旦检测到,立即调用 sys_exit_group 自杀。

  3. 压缩算法 (sub_10C914 - aPLib): 代码分析确认这是 aPLib 算法。这是一种非对称压缩算法,解压代码极小(适合嵌入),但压缩率很高。

    • 逆向特征: 看到处理 bit 流,且有复杂的 if (bitbuf & 0x100) 判断以及 gamma 编码长度计算。

  4. 缓存一致性 (sub_10C224): 因为解压后的代码是动态写入内存的(自修改代码),CPU 的指令缓存(I-Cache)可能仍保留着旧数据。必须显式调用 flush_cache 才能保证程序不崩溃。这是 ARM 架构下 Shellcode 的标准动作。

  5. 重定位与混淆 (sub_10C3AC): 解压出的 SO 文件基址变化了,必须修正全局变量和函数指针的绝对地址。

    • 混淆技术: 此函数内部使用了 O-LLVM 控制流平坦化。你会看到一个巨大的 while(1) 循环和 switch(state) 结构,用于打乱代码执行顺序,阻碍静态分析。


3. 逆向总结与建议

3.1 样本特征总结

  • 加密策略: aPLib 压缩 + 动态 XOR 密钥。

  • 反分析: Inline Syscall + 字符串加密 + O-LLVM 混淆 + 完整性校验。

  • 运行环境: 强依赖 /proc/ 文件系统,在部分定制 ROM 或沙箱中可能无法运行。

3.2 动态调试建议 (脱壳思路)

如果您需要提取解密后的 Payload:

  1. 断点位置:sub_10C50C 函数的末尾,或者紧跟着 sub_10C224 (Flush Cache) 之后。

  2. 绕过反调试: 需要 Hook 内核的 sys_openat,当它试图打开 /proc/self/maps 时,重定向到一个伪造的、干净的文件,或者直接 Patch 掉 sub_10C2A8 的入口让其直接返回。

  3. Dump 内存: 当执行流到达 Flush Cache 之后,内存中 unpack_dst 指向的区域就是完整的、已解密且修复了重定位的 DEX/SO 文件。

 

第一步:init函数开始去壳so,这一步是所有加密的开端,也是第一个坎


整体架构概述

这是 bangcle(梆梆加固) Android 加固壳的核心解壳模块,包含以下关键组件:

text
┌─────────────────────────────────────────────────────────────────────┐
│                         .init_proc (入口)                            │
│                              ↓                                       │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │  sub_10C7DC: 准备解压缓冲区 (mmap + memcpy + cache_flush)     │   │
│  └──────────────────────────────────────────────────────────────┘   │
│                              ↓                                       │
│  ┌──────────────────────────────────────────────────────────────┐   │
│  │  sub_10C50C: 主解壳函数 (带控制流平坦化混淆)                   │   │
│  │    ├─ 完整性校验 (BL技巧获取嵌入签名)                          │   │
│  │    ├─ sub_10C2A8: 反调试检测 (/proc/self/maps)                │   │
│  │    ├─ sub_10C1A0: mprotect 修改内存权限                       │   │
│  │    ├─ sub_10C8F0 → sub_10C914: aPLib解压                      │   │
│  │    ├─ XOR解密循环                                              │   │
│  │    ├─ sub_10C224: 缓存刷新                                     │   │
│  │    ├─ ELF程序头解析,定位PT_LOAD段                             │   │
│  │    ├─ sub_10C3AC: 执行重定位                                   │   │
│  │    └─ sub_10C180: munmap 释放资源                              │   │
│  └──────────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

一、系统调用包装函数

C
/*
 * ============================================================================
 * 文件: bangcle_syscall.c
 * 描述: Linux ARM64 系统调用包装函数
 * ============================================================================
 */

#include <stdint.h>
#include <stddef.h>

/* 系统调用号定义 (ARM64) */
#define __NR_close      57      // 0x39
#define __NR_openat     56      // 0x38
#define __NR_read       63      // 0x3F
#define __NR_write      64      // 0x40
#define __NR_exit_group 94      // 0x5E
#define __NR_mprotect   226     // 0xE2
#define __NR_munmap     215     // 0xD7
#define __NR_mmap       222     // 0xDE

/* 内存保护标志 */
#define PROT_READ   1
#define PROT_WRITE  2
#define PROT_EXEC   4

/* mmap 标志 */
#define MAP_PRIVATE   0x02
#define MAP_ANONYMOUS 0x20

/*
 * 函数名: sys_write (sub_10C174 / sub_10C1AC)
 * 功能: write系统调用包装
 * 参数:
 *   fd   - 文件描述符
 *   buf  - 数据缓冲区
 *   count - 写入字节数
 * 返回: 写入的字节数,失败返回负数
 */
static inline ssize_t sys_write(int fd, const void *buf, size_t count)
{
    register long x8 __asm__("x8") = __NR_write;
    register long x0 __asm__("x0") = fd;
    register long x1 __asm__("x1") = (long)buf;
    register long x2 __asm__("x2") = count;
    
    __asm__ volatile (
        "svc #0"
        : "+r"(x0)
        : "r"(x8), "r"(x1), "r"(x2)
        : "memory"
    );
    
    return x0;
}

/*
 * 函数名: sys_read (sub_10C1B8)
 * 功能: read系统调用包装
 */
static inline ssize_t sys_read(int fd, void *buf, size_t count)
{
    register long x8 __asm__("x8") = __NR_read;
    register long x0 __asm__("x0") = fd;
    register long x1 __asm__("x1") = (long)buf;
    register long x2 __asm__("x2") = count;
    
    __asm__ volatile (
        "svc #0"
        : "+r"(x0)
        : "r"(x8), "r"(x1), "r"(x2)
        : "memory"
    );
    
    return x0;
}

/*
 * 函数名: sys_openat (sub_10C1C4)
 * 功能: openat系统调用包装
 */
static inline int sys_openat(int dirfd, const char *pathname, int flags)
{
    register long x8 __asm__("x8") = __NR_openat;
    register long x0 __asm__("x0") = dirfd;
    register long x1 __asm__("x1") = (long)pathname;
    register long x2 __asm__("x2") = flags;
    
    __asm__ volatile (
        "svc #0"
        : "+r"(x0)
        : "r"(x8), "r"(x1), "r"(x2)
        : "memory"
    );
    
    return (int)x0;
}

/*
 * 函数名: sys_close (sub_10C1D0)
 * 功能: close系统调用包装
 */
static inline int sys_close(int fd)
{
    register long x8 __asm__("x8") = __NR_close;
    register long x0 __asm__("x0") = fd;
    
    __asm__ volatile (
        "svc #0"
        : "+r"(x0)
        : "r"(x8)
        : "memory"
    );
    
    return (int)x0;
}

/*
 * 函数名: sys_exit_group (sub_10C1DC)
 * 功能: exit_group系统调用包装,终止进程
 */
static inline void __attribute__((noreturn)) sys_exit_group(int status)
{
    register long x8 __asm__("x8") = __NR_exit_group;
    register long x0 __asm__("x0") = status;
    
    __asm__ volatile (
        "svc #0"
        :
        : "r"(x8), "r"(x0)
        : "memory"
    );
    
    __builtin_unreachable();
}

/*
 * 函数名: sys_munmap (sub_10C180)
 * 功能: munmap系统调用包装
 */
static inline int sys_munmap(void *addr, size_t length)
{
    register long x8 __asm__("x8") = __NR_munmap;
    register long x0 __asm__("x0") = (long)addr;
    register long x1 __asm__("x1") = length;
    
    __asm__ volatile (
        "svc #0"
        : "+r"(x0)
        : "r"(x8), "r"(x1)
        : "memory"
    );
    
    return (int)x0;
}

/*
 * 函数名: sys_mmap (sub_10C18C)
 * 功能: mmap系统调用包装
 */
static inline void *sys_mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset)
{
    register long x8 __asm__("x8") = __NR_mmap;
    register long x0 __asm__("x0") = (long)addr;
    register long x1 __asm__("x1") = length;
    register long x2 __asm__("x2") = prot;
    register long x3 __asm__("x3") = flags;
    register long x4 __asm__("x4") = fd;
    register long x5 __asm__("x5") = offset;
    
    __asm__ volatile (
        "svc #0"
        : "+r"(x0)
        : "r"(x8), "r"(x1), "r"(x2), "r"(x3), "r"(x4), "r"(x5)
        : "memory"
    );
    
    return (void *)x0;
}

/*
 * 函数名: sys_mprotect (sub_10C1A0)
 * 功能: mprotect系统调用包装
 */
static inline int sys_mprotect(void *addr, size_t len, int prot)
{
    register long x8 __asm__("x8") = __NR_mprotect;
    register long x0 __asm__("x0") = (long)addr;
    register long x1 __asm__("x1") = len;
    register long x2 __asm__("x2") = prot;
    
    __asm__ volatile (
        "svc #0"
        : "+r"(x0)
        : "r"(x8), "r"(x1), "r"(x2)
        : "memory"
    );
    
    return (int)x0;
}

二、基础工具函数

C
/*
 * ============================================================================
 * 文件: bangcle_utils.c
 * 描述: 基础工具函数
 * ============================================================================
 */

/*
 * 函数名: bangcle_strlen (sub_10C28C)
 * 功能: 计算字符串长度
 * 参数:
 *   str - 输入字符串
 * 返回: 字符串长度(不含终止符)
 * 第三方库猜测: 标准库 strlen 的简化实现
 */
static unsigned int bangcle_strlen(const char *str)
{
    const char *p = str;
    
    while (*p != '\0') {
        p++;
    }
    
    return (unsigned int)(p - str);
}

/*
 * 函数名: bangcle_memcmp (sub_10C1E8)
 * 功能: 内存比较
 * 参数:
 *   s1  - 第一个内存块
 *   s2  - 第二个内存块
 *   n   - 比较字节数
 * 返回: 0表示相等,否则返回差值
 * 第三方库猜测: 标准库 memcmp/strncmp 的混合实现
 */
static int bangcle_memcmp(const void *s1, const void *s2, unsigned int n)
{
    const unsigned char *p1 = (const unsigned char *)s1;
    const unsigned char *p2 = (const unsigned char *)s2;
    unsigned int i = 0;
    
    while (1) {
        unsigned char c1 = p1[i];
        unsigned char c2 = p2[i];
        
        if (c1 != c2) {
            return (int)c1 - (int)c2;
        }
        
        /* 遇到字符串结束符也停止 */
        if (c1 == 0) {
            return 0;
        }
        
        i++;
        if (i == n) {
            return 0;
        }
    }
}

三、缓存维护函数

C
/*
 * ============================================================================
 * 函数名: bangcle_flush_cache (sub_10C224)
 * ============================================================================
 * 功能描述:
 *   ARM64 缓存维护函数,确保修改后的代码能正确执行
 *   用于自修改代码场景(解密/解压后的代码)
 * 
 * 参数描述:
 *   start - 需要刷新的内存起始地址
 *   end   - 需要刷新的内存结束地址
 * 
 * 算法描述:
 *   1. 读取 CTR_EL0 获取缓存行大小
 *   2. 对数据缓存执行 DC CVAU(清理到PoU点)
 *   3. 执行 DSB 确保数据缓存操作完成
 *   4. 对指令缓存执行 IC IVAU(使无效)
 *   5. 执行 DSB + ISB 确保指令缓存更新
 * 
 * 第三方库猜测:
 *   类似于 __clear_cache() 或 Android libutils 中的缓存刷新
 */
static void bangcle_flush_cache(void *start, void *end)
{
    unsigned long ctr_el0;
    unsigned int dcache_line_size;
    unsigned int icache_line_size;
    unsigned char *addr;
    
    /* 读取缓存类型寄存器 */
    __asm__ volatile ("mrs %0, ctr_el0" : "=r"(ctr_el0));
    
    /* 计算数据缓存行大小: 4 << CTR_EL0[19:16] */
    dcache_line_size = 4 << ((ctr_el0 >> 16) & 0xF);
    
    /* 计算指令缓存行大小: 4 << CTR_EL0[3:0] */
    icache_line_size = 4 << (ctr_el0 & 0xF);
    
    /* 清理数据缓存 */
    addr = (unsigned char *)((unsigned long)start & ~(dcache_line_size - 1));
    while (addr < (unsigned char *)end) {
        __asm__ volatile ("dc cvau, %0" : : "r"(addr) : "memory");
        addr += dcache_line_size;
    }
    
    /* 数据同步屏障 */
    __asm__ volatile ("dsb ish" : : : "memory");
    
    /* 使指令缓存无效 */
    addr = (unsigned char *)((unsigned long)start & ~(icache_line_size - 1));
    while (addr < (unsigned char *)end) {
        __asm__ volatile ("ic ivau, %0" : : "r"(addr) : "memory");
        addr += icache_line_size;
    }
    
    /* 数据同步屏障 + 指令同步屏障 */
    __asm__ volatile ("dsb ish" : : : "memory");
    __asm__ volatile ("isb" : : : "memory");
}

四、aPLib 解压算法

C
/*
 * ============================================================================
 * 函数名: aplib_decompress (sub_10C914)
 * ============================================================================
 * 功能描述:
 *   aPLib 压缩算法的解压实现,这是一种高效的LZ77变种
 *   广泛用于壳程序和恶意软件中
 * 
 * 参数描述:
 *   src      - 压缩数据源地址
 *   src_size - 压缩数据大小
 *   dst      - 解压目标缓冲区
 *   out_size - 输出解压后的数据大小
 * 
 * 返回值:
 *   0        - 成功
 *   -205     - 源数据不足 (src_idx < src_size)
 *   -201     - 源数据过多 (src_idx > src_size)
 * 
 * 算法描述:
 *   使用位流控制,包含以下模式:
 *   - Literal: 直接拷贝单字节
 *   - Short match: 短距离回溯拷贝
 *   - Long match: 长距离回溯拷贝
 *   - 结束标记: offset = -1
 * 
 * 第三方库猜测:
 *   确认是 aPLib 解压算法 (http://ibsensoftware.com/products_aPLib.html)
 * 
 * 数据流:
 *   src[src_idx] --读取位流--> 控制逻辑 --literal/match--> dst[dst_idx]
 * 
 * 控制流:
 *   主循环 -> 判断literal/match -> literal直接拷贝 / match回溯拷贝 -> 循环
 */

#define APLIB_SUCCESS       0
#define APLIB_ERROR_UNDERFLOW  0xFFFFFF33  /* -205 */
#define APLIB_ERROR_OVERFLOW   0xFFFFFF37  /* -201 */

static int aplib_decompress(
    const unsigned char *src,
    unsigned int src_size,
    unsigned char *dst,
    unsigned int *out_size)
{
    unsigned int dst_idx = 0;      /* 输出索引 */
    unsigned int src_idx = 0;      /* 输入索引 */
    unsigned int offset = 1;       /* 回溯偏移量 */
    unsigned int bitbuf = 0;       /* 位缓冲区 */
    unsigned int bitcount;         /* 位计数 */
    unsigned int length;           /* 匹配长度 */
    unsigned int tmp;
    unsigned int i;

    while (1) {
        /* ==================== 读取控制位 ==================== */
        bitcount = bitbuf & 0x7F;
        bitbuf <<= 1;
        
        if (bitcount == 0) {
            /* 位缓冲区耗尽,加载新字节 */
            bitbuf = src[src_idx++];
            bitbuf = (bitbuf << 1) | 1;  /* 加标记位 */
        }
        
        /* ==================== 检查是否为 Literal ==================== */
        if (bitbuf & 0x100) {
            /* Literal: 直接拷贝一个字节 */
            dst[dst_idx++] = src[src_idx++];
            continue;
        }
        
        /* ==================== 解码偏移量长度 ==================== */
        length = 1;
        
        do {
            /* 读取一位 */
            bitcount = bitbuf & 0x7F;
            length <<= 1;
            bitbuf <<= 1;
            
            if (bitcount == 0) {
                bitbuf = src[src_idx++];
                bitbuf = (bitbuf << 1) | 1;
            }
            
            /* 提取高位 */
            length += (bitbuf >> 8) & 1;
            
            /* 再读取一位判断是否继续 */
            bitcount = bitbuf & 0x7F;
            bitbuf <<= 1;
            
            if (bitcount == 0) {
                bitbuf = src[src_idx++];
                bitbuf = (bitbuf << 1) | 1;
            }
            
            if (bitbuf & 0x100) {
                break;  /* 结束偏移量解码 */
            }
            
            /* 继续扩展 */
            bitcount = bitbuf & 0x7F;
            length = (length + 0x7FFFFFFF) << 1;  /* length = (length - 1) * 2 */
            bitbuf <<= 1;
            
            if (bitcount == 0) {
                bitbuf = src[src_idx++];
                bitbuf = (bitbuf << 1) | 1;
            }
            
            length += (bitbuf >> 8) & 1;
            
        } while (1);
        
        /* ==================== 处理偏移量 ==================== */
        tmp = src_idx;
        
        if (length == 2) {
            /* 短匹配: 使用上一个偏移量 */
            bitcount = bitbuf & 0x7F;
            bitbuf <<= 1;
            
            if (bitcount == 0) {
                src_idx++;
                bitbuf = (src[tmp] << 1) | 1;
            }
            
            length = (bitbuf >> 8) & 1;  /* 长度的低位 */
        }
        else {
            /* 长匹配: 读取新偏移量 */
            offset = (length << 8) + src[tmp] - 0x300;
            src_idx++;
            
            /* 检查结束标记 */
            if (offset == 0xFFFFFFFF) {
                /* 解压完成 */
                *out_size = dst_idx;
                
                if (src_idx == src_size) {
                    return APLIB_SUCCESS;
                }
                else if (src_idx < src_size) {
                    return APLIB_ERROR_UNDERFLOW;
                }
                else {
                    return APLIB_ERROR_OVERFLOW;
                }
            }
            
            /* 解码实际偏移量 */
            length = ~offset & 1;  /* 取反后的最低位 */
            offset = (offset >> 1) + 1;
        }
        
        /* ==================== 解码匹配长度 ==================== */
        bitcount = bitbuf & 0x7F;
        length <<= 1;
        bitbuf <<= 1;
        
        if (bitcount == 0) {
            bitbuf = src[src_idx++];
            bitbuf = (bitbuf << 1) | 1;
        }
        
        length += (bitbuf >> 8) & 1;
        
        if (length == 0) {
            /* 扩展长度编码 */
            unsigned int gamma = 1;
            
            do {
                bitcount = bitbuf & 0x7F;
                gamma <<= 1;
                bitbuf <<= 1;
                
                if (bitcount == 0) {
                    bitbuf = src[src_idx++];
                    bitbuf = (bitbuf << 1) | 1;
                }
                
                gamma += (bitbuf >> 8) & 1;
                
                bitcount = bitbuf & 0x7F;
                bitbuf <<= 1;
                
                if (bitcount == 0) {
                    bitbuf = src[src_idx++];
                    bitbuf = (bitbuf << 1) | 1;
                }
                
            } while ((bitbuf & 0x100) == 0);
            
            length = gamma + 2;
        }
        
        /* ==================== 执行回溯拷贝 ==================== */
        /* 大偏移量时长度+1 */
        if (offset > 0x500) {
            length++;
        }
        
        /* 拷贝第一个字节 */
        dst[dst_idx] = dst[dst_idx - offset];
        dst_idx++;
        
        /* 拷贝剩余字节 */
        for (i = 0; i < length; i++) {
            dst[dst_idx + i] = dst[dst_idx - offset + i];
        }
        
        dst_idx += length;
    }
}

/*
 * 函数名: bangcle_decompress (sub_10C8F0)
 * 功能: 解压包装函数
 */
static int bangcle_decompress(void *dst, const void *src, unsigned int packed_size, unsigned int param)
{
    unsigned int out_size;
    return aplib_decompress((const unsigned char *)src, param, (unsigned char *)dst, &out_size);
}

五、反调试检测函数

C
/*
 * ============================================================================
 * 函数名: bangcle_check_maps (sub_10C2A8)
 * ============================================================================
 * 功能描述:
 *   通过读取 /proc/self/maps 检测内存映射中的特定标识
 *   用于完整性校验或反调试检测
 * 
 * 参数描述:
 *   pattern - 要搜索的模式字符串(第一个字符为分隔符)
 * 
 * 控制流:
 *   1. 打开 /proc/self/maps
 *   2. 逐行读取
 *   3. 在每行中搜索 pattern
 *   4. 找到则关闭文件返回
 *   5. 找不到则调用 exit_group 终止进程
 * 
 * 反调试机制:
 *   检测加固特征是否存在,如果被移除则自毁
 */
static void bangcle_check_maps(const char *pattern)
{
    int fd;
    char line_buffer[512];
    char char_buffer;
    char *line_ptr;
    char *search_ptr;
    char separator;
    unsigned int pattern_len;
    int bytes_read;
    
    /* 打开 /proc/self/maps */
    fd = sys_openat(-100, "/proc/self/maps", 0, 0);  /* AT_FDCWD = -100 */
    
    if (fd < 0) {
        return;
    }
    
    while (1) {
        /* 逐字符读取一行 */
        line_ptr = line_buffer;
        
        do {
            bytes_read = sys_read(fd, &char_buffer, 1);
            
            if (bytes_read != 1) {
                break;
            }
            
            *line_ptr = char_buffer;
            line_ptr++;
            
            if (char_buffer == '\n') {
                break;
            }
            
        } while (line_ptr < line_buffer + 1023);
        
        *line_ptr = '\0';
        
        /* 检查是否读取到内容 */
        if (line_ptr == line_buffer) {
            /* 文件结束,未找到标识,终止进程 */
            sys_exit_group(0);
        }
        
        /* 获取分隔符和模式长度 */
        separator = pattern[0];
        
        if (separator == '\0') {
            sys_close(fd);
            return;
        }
        
        pattern_len = bangcle_strlen(pattern + 1);
        search_ptr = line_buffer;
        
        /* 在行中搜索模式 */
        while (1) {
            char current_char = *search_ptr;
            search_ptr++;
            
            if (current_char == '\0') {
                break;  /* 本行未找到,继续下一行 */
            }
            
            if (current_char == separator) {
                /* 找到分隔符,检查后续是否匹配 */
                if (pattern_len == 0 || 
                    bangcle_memcmp(search_ptr, pattern + 1, pattern_len) == 0) {
                    /* 找到匹配 */
                    sys_close(fd);
                    return;
                }
            }
        }
    }
}

六、重定位处理函数

C
/*
 * ============================================================================
 * 函数名: bangcle_do_relocations (sub_10C3AC)
 * ============================================================================
 * 功能描述:
 *   处理ELF重定位,将符号地址写入对应位置
 *   使用临时缓冲区避免原地修改问题
 * 
 * 参数描述:
 *   reloc_offsets - 重定位偏移量数组
 *   symbol_values - 符号值数组
 *   count         - 重定位条目数量
 *   base_addr     - 基地址
 * 
 * 算法描述:
 *   1. 分配临时缓冲区
 *   2. 拷贝重定位偏移量到临时缓冲区
 *   3. 清空原偏移量数组
 *   4. 应用重定位: *(base + offset[i]) = value[i]
 *   5. 释放临时缓冲区
 *   6. 清空符号值数组
 * 
 * 混淆特征:
 *   包含控制流平坦化混淆(状态机循环)
 */

/* ==================== 带混淆的精确版本 ==================== */
static void bangcle_do_relocations_obfuscated(
    unsigned int *reloc_offsets,
    unsigned int *symbol_values,
    unsigned int count,
    unsigned int base_addr)
{
    /* 混淆变量 */
    volatile unsigned int obf_unused = 0;
    volatile unsigned int obf_param = 0x1E;
    volatile unsigned int obf_state = 0x111;
    
    unsigned int const_0x14 = 0x14;
    unsigned int const_0x06 = 6;
    unsigned int const_0x1E = 0x1E;
    unsigned int const_0x46 = 0x46;
    unsigned int const_0xD6 = 0xD6;
    
    unsigned int *temp_buffer;
    size_t buffer_size;
    unsigned int i;
    unsigned int *end_ptr;
    
    /* ========== 控制流平坦化状态机 ========== */
    while (1) {
        unsigned int state = obf_state;
        
        if (state == 0x46) {
            break;  /* 退出混淆,执行真正逻辑 */
        }
        
        if (state == 0x105) {
            /* 死代码分支 */
            continue;
        }
        
        if (state == 0x111) {
            /* 初始状态 -> 0x46 */
            obf_param = const_0x1E;
            obf_state = const_0x46;
            continue;
        }
        
        if (state == 0x15) {
            /* 死代码分支 */
            unsigned int tmp = state * const_0x14 + 0x74;
            obf_state = tmp;
            if (tmp > 3) {
                obf_state = const_0xD6;
            } else {
                obf_param = const_0x06;
            }
            continue;
        }
    }
    
    /* ========== 真正的重定位逻辑 ========== */
    
    /* 计算缓冲区大小(页对齐) */
    buffer_size = (count * 4 + 0xFFF) & 0xFFFFF000;
    
    /* 分配临时缓冲区 */
    temp_buffer = (unsigned int *)sys_mmap(
        NULL,
        buffer_size,
        PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS,
        -1,
        0
    );
    
    /* 拷贝偏移量并清空原数组 */
    for (i = 0; i < count; i++) {
        temp_buffer[i] = reloc_offsets[i];
        reloc_offsets[i] = 0;
    }
    
    /* 应用重定位 */
    end_ptr = reloc_offsets + count;
    
    for (i = 0; i < count; i++) {
        unsigned int offset = temp_buffer[i];
        unsigned int value = symbol_values[i];
        *(unsigned int *)(base_addr + offset) = value;
    }
    
    /* 释放临时缓冲区 */
    sys_munmap(temp_buffer, buffer_size);
    
    /* 清空符号值数组 */
    for (i = 0; i < count; i++) {
        end_ptr[i] = 0;
    }
}

/* ==================== 去混淆的简化版本 ==================== */
static void bangcle_do_relocations_clean(
    unsigned int *reloc_offsets,
    unsigned int *symbol_values,
    unsigned int count,
    unsigned int base_addr)
{
    size_t buffer_size = (count * 4 + 0xFFF) & 0xFFFFF000;
    
    /* 分配临时缓冲区 */
    unsigned int *temp = (unsigned int *)sys_mmap(
        NULL, buffer_size, PROT_READ | PROT_WRITE,
        MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    
    /* 备份偏移量并清空 */
    for (unsigned int i = 0; i < count; i++) {
        temp[i] = reloc_offsets[i];
        reloc_offsets[i] = 0;
    }
    
    /* 应用重定位 */
    for (unsigned int i = 0; i < count; i++) {
        *(unsigned int *)(base_addr + temp[i]) = symbol_values[i];
    }
    
    /* 清理 */
    sys_munmap(temp, buffer_size);
    
    unsigned int *values_end = reloc_offsets + count;
    for (unsigned int i = 0; i < count; i++) {
        values_end[i] = 0;
    }
}

七、准备解压缓冲区

C
/*
 * ============================================================================
 * 函数名: bangcle_prepare_buffer (sub_10C7DC)
 * ============================================================================
 * 功能描述:
 *   为解压分配可执行内存,拷贝压缩数据,并刷新缓存
 * 
 * 参数描述:
 *   loader_info - 加载器信息结构体
 *   packed_data - 压缩数据源
 * 
 * 返回值:
 *   分配的内存指针(可执行)
 */
static void *bangcle_prepare_buffer(const void *loader_info, const void *packed_data)
{
    /* 混淆状态机(同上,省略) */
    volatile unsigned int obf_state = 0x111;
    while (obf_state != 0x46) {
        obf_state = 0x46;
    }
    
    /* 获取压缩数据大小 */
    unsigned int packed_size = *(unsigned int *)((char *)loader_info + 0x28);
    
    /* 分配RWX内存 */
    size_t alloc_size = (packed_size + 0xFFF) & 0xFFFFF000;
    unsigned char *buffer = (unsigned char *)sys_mmap(
        NULL,
        alloc_size,
        PROT_READ | PROT_WRITE | PROT_EXEC,
        MAP_PRIVATE | MAP_ANONYMOUS,
        -1,
        0
    );
    
    /* 拷贝压缩数据 */
    const unsigned char *src = (const unsigned char *)packed_data;
    for (unsigned int i = 0; i < packed_size; i++) {
        buffer[i] = src[i];
    }
    
    /* 刷新缓存 */
    bangcle_flush_cache(buffer, buffer + packed_size);
    
    return buffer;
}

八、主解壳函数

C
/*
 * ============================================================================
 * 函数名: bangcle_unpack_main (sub_10C50C + loc_10C5EC)
 * ============================================================================
 * 功能描述:
 *   梆梆加固的主解壳入口函数
 *   包含完整性校验、解压、解密、重定位等全部流程
 * 
 * 参数描述:
 *   loader_info    - 加载器控制结构体
 *   elf_base       - ELF映射基地址
 *   compressed_src - 压缩数据源
 * 
 * 外部引用:
 *   - 嵌入字符串: "__b_a_n_g_c_l_e__check1234567_"
 *   - 校验字符串: "__b_a_n_g_", "c_l_e__che", "ck1234567_"
 * 
 * 混淆特征:
 *   1. 控制流平坦化(状态机)
 *   2. BL技巧获取嵌入数据地址
 *   3. 混淆的条件判断
 */

/* 加载器信息结构体 */
typedef struct {
    unsigned int reserved[3];      /* +0x00: 保留 */
    unsigned int base_offset;      /* +0x0C: 基地址偏移 */
    unsigned int reloc_offset;     /* +0x10: 重定位表偏移 */
    unsigned int reloc_size;       /* +0x14: 重定位表大小 */
    unsigned int dynsym_offset;    /* +0x18: 动态符号表偏移 */
    unsigned int packed_size;      /* +0x1C: 压缩数据大小 */
    unsigned int xor_start;        /* +0x20: XOR解密起始偏移 */
    unsigned int xor_length;       /* +0x24: XOR解密长度(也用作密钥) */
    unsigned int unpack_offset;    /* +0x28: 解压目标偏移 */
    unsigned int decompress_param; /* +0x2C: 解压参数 */
} bangcle_loader_t;

/* ELF64 头部 */
typedef struct {
    unsigned char  e_ident[16];
    unsigned short e_type;
    unsigned short e_machine;
    unsigned int   e_version;
    unsigned long  e_entry;
    unsigned long  e_phoff;        /* +0x20: 程序头偏移 */
    unsigned long  e_shoff;
    unsigned int   e_flags;
    unsigned short e_ehsize;
    unsigned short e_phentsize;    /* +0x36: 程序头条目大小 */
    unsigned short e_phnum;        /* +0x38: 程序头数量 */
    unsigned short e_shentsize;
    unsigned short e_shnum;
    unsigned short e_shstrndx;
} Elf64_Ehdr;

/* ELF64 程序头 */
typedef struct {
    unsigned int   p_type;         /* +0x00 */
    unsigned int   p_flags;
    unsigned long  p_offset;
    unsigned long  p_vaddr;        /* +0x10 */
    unsigned long  p_paddr;
    unsigned long  p_filesz;       /* +0x20 */
    unsigned long  p_memsz;        /* +0x28 */
    unsigned long  p_align;
} Elf64_Phdr;

#define PT_LOAD 1

/* ==================== 带混淆的精确版本 ==================== */
static int bangcle_unpack_main_obfuscated(
    bangcle_loader_t *loader_info,
    void *elf_base,
    void *compressed_src)
{
    /* 混淆变量 */
    volatile unsigned int obf_unused = 0;
    volatile unsigned int obf_param = 0x1E;
    volatile unsigned int obf_state = 0x111;
    
    unsigned int const_0x14 = 0x14;
    unsigned int const_0x06 = 6;
    unsigned int const_0x1E = 0x1E;
    unsigned int const_0x46 = 0x46;
    unsigned int const_0xD6 = 0xD6;
    
    const char *embedded_signature;
    int cmp_result1, cmp_result2, cmp_result3;
    unsigned int flag1, flag2;
    
    unsigned char *real_base;
    unsigned char *unpack_dst;
    Elf64_Ehdr *ehdr;
    Elf64_Phdr *phdr;
    unsigned long page_base, page_end1, page_end2;
    size_t protect_size1, protect_size2;
    unsigned int decompress_param;
    unsigned int i;
    
    /* ========== 控制流平坦化状态机 ========== */
    while (1) {
        unsigned int state = obf_state;
        
        if (state == 0x46) {
            break;
        }
        
        if (state == 0x105) {
            continue;
        }
        
        if (state == 0x111) {
            obf_param = const_0x1E;
            obf_state = const_0x46;
            continue;
        }
        
        if (state == 0x15) {
            unsigned int tmp = state * const_0x14 + 0x74;
            obf_state = tmp;
            if (tmp > 3) {
                obf_state = const_0xD6;
            } else {
                obf_param = const_0x06;
            }
            continue;
        }
    }
    
    /* ========== BL技巧获取嵌入字符串地址 ========== */
    /* 
     * 原始汇编:
     *   BL loc_10C5EC           ; LR = 0x10C5CC (字符串地址)
     *   DCB "__b_a_n_g_c_l_e__check1234567_",0
     * loc_10C5EC:
     *   MOV X20, X30            ; 保存字符串地址
     */
    __asm__ volatile (
        "bl 1f\n"
        ".ascii \"__b_a_n_g_c_l_e__check1234567_\"\n"
        ".byte 0\n"
        ".align 2\n"
        "1: mov %0, x30"
        : "=r"(embedded_signature)
        :
        : "x30"
    );
    
    /* ========== 完整性校验 ========== */
    cmp_result1 = bangcle_memcmp(embedded_signature + 0,  "__b_a_n_g_", 10);
    cmp_result2 = bangcle_memcmp(embedded_signature + 10, "c_l_e__che", 10);
    cmp_result3 = bangcle_memcmp(embedded_signature + 20, "ck1234567_", 10);
    
    /* 混淆的条件判断 */
    flag1 = (cmp_result1 == 0) ? 1 : 0;  /* CSET W2, EQ */
    flag2 = (cmp_result2 != 0) ? 1 : 0;  /* CSET W0, NE */
    
    if (flag1 <= flag2 || cmp_result3 != 0) {
        /* 校验失败,调用反调试检测 */
tamper_detected:
        bangcle_check_maps((const char *)embedded_signature);
        /* 继续执行解壳(可能是迷惑策略) */
    }
    
process_unpacking:
    /* ========== 计算地址 ========== */
    real_base = (unsigned char *)elf_base - loader_info->base_offset;
    unpack_dst = real_base + loader_info->unpack_offset;
    decompress_param = loader_info->decompress_param;
    
    /* ========== 修改内存权限 ========== */
    {
        size_t total = loader_info->packed_size + loader_info->xor_length;
        total = ((unsigned long)unpack_dst + total + 0xFFF) & ~0xFFFUL;
        protect_size1 = total - (unsigned long)real_base;
        
        sys_mprotect(real_base, protect_size1, PROT_READ | PROT_WRITE | PROT_EXEC);
    }
    
    /* ========== 解压数据 ========== */
    bangcle_decompress(unpack_dst, compressed_src, loader_info->packed_size, decompress_param);
    
    /* ========== XOR解密 ========== */
    if (loader_info->xor_length != 0) {
        unsigned int xor_key = loader_info->xor_length;
        unsigned int xor_start = loader_info->xor_start;
        
        for (i = 0; i < xor_key; i += 4) {
            unsigned int offset = xor_start + i;
            unsigned int *target = (unsigned int *)(unpack_dst + offset);
            *target ^= xor_key;
        }
    }
    
    /* ========== 缓存刷新 ========== */
    bangcle_flush_cache(unpack_dst, unpack_dst + loader_info->packed_size);
    
    /* ========== 解析ELF程序头 ========== */
    ehdr = (Elf64_Ehdr *)real_base;
    phdr = (Elf64_Phdr *)(real_base + ehdr->e_phoff);
    
    /* 计算搜索范围 */
    unsigned int search_limit = ehdr->e_phnum * ehdr->e_phentsize + 0x34;
    unsigned short phentsize = ehdr->e_phentsize;
    
    /* 查找第2个PT_LOAD段 */
    int pt_load_count = (phdr->p_type == PT_LOAD) ? 1 : 0;
    
    while (pt_load_count < 2) {
        phdr = (Elf64_Phdr *)((char *)phdr + phentsize);
        if (phdr->p_type == PT_LOAD) {
            pt_load_count++;
        }
    }
    
    /* ========== 处理动态段内存权限 ========== */
    {
        unsigned long dyn_vaddr = phdr->p_vaddr;
        unsigned long dyn_filesz = phdr->p_filesz;
        unsigned long dyn_memsz = phdr->p_memsz;
        
        unsigned char *dyn_addr = real_base + dyn_vaddr;
        
        page_base = (unsigned long)dyn_addr & ~0xFFFUL;
        page_end1 = ((unsigned long)dyn_addr + dyn_filesz + 0xFFF) & ~0xFFFUL;
        page_end2 = ((unsigned long)dyn_addr + dyn_memsz + 0xFFF) & ~0xFFFUL;
        
        protect_size1 = page_end1 - page_base;
        protect_size2 = page_end2 - page_end1;
        
        sys_mprotect((void *)page_base, protect_size1, PROT_READ | PROT_WRITE);
        sys_mprotect((void *)page_end1, protect_size2, PROT_READ | PROT_WRITE);
    }
    
    /* ========== 执行重定位 ========== */
    bangcle_do_relocations_clean(
        (unsigned int *)(real_base + loader_info->dynsym_offset),
        (unsigned int *)(real_base + loader_info->reloc_offset),
        loader_info->reloc_size,
        (unsigned int)(unsigned long)real_base
    );
    
    /* ========== 释放压缩数据 ========== */
    sys_munmap(compressed_src, decompress_param);
    
    return 0;
}

/* ==================== 去混淆的简化版本 ==================== */
static int bangcle_unpack_main_clean(
    bangcle_loader_t *loader,
    void *elf_base,
    void *packed_data)
{
    /* 完整性校验(实际实现中通过BL技巧) */
    static const char embedded_sig[] = "__b_a_n_g_c_l_e__check1234567_";
    
    if (bangcle_memcmp(embedded_sig + 0,  "__b_a_n_g_", 10) != 0 ||
        bangcle_memcmp(embedded_sig + 10, "c_l_e__che", 10) != 0 ||
        bangcle_memcmp(embedded_sig + 20, "ck1234567_", 10) != 0) {
        bangcle_check_maps(embedded_sig);
    }
    
    /* 计算地址 */
    unsigned char *base = (unsigned char *)elf_base - loader->base_offset;
    unsigned char *dst = base + loader->unpack_offset;
    
    /* 修改内存权限 */
    size_t total_size = loader->packed_size + loader->xor_length;
    size_t prot_size = (((unsigned long)dst + total_size + 0xFFF) & ~0xFFFUL) - (unsigned long)base;
    sys_mprotect(base, prot_size, PROT_READ | PROT_WRITE | PROT_EXEC);
    
    /* 解压 */
    bangcle_decompress(dst, packed_data, loader->packed_size, loader->decompress_param);
    
    /* XOR解密 */
    if (loader->xor_length > 0) {
        unsigned int *ptr = (unsigned int *)(dst + loader->xor_start);
        for (unsigned int i = 0; i < loader->xor_length; i += 4) {
            *ptr++ ^= loader->xor_length;
        }
    }
    
    /* 缓存刷新 */
    bangcle_flush_cache(dst, dst + loader->packed_size);
    
    /* 解析ELF找第2个PT_LOAD */
    Elf64_Ehdr *ehdr = (Elf64_Ehdr *)base;
    Elf64_Phdr *phdr = (Elf64_Phdr *)(base + ehdr->e_phoff);
    
    int count = 0;
    while (count < 2) {
        if (phdr->p_type == PT_LOAD) count++;
        if (count < 2) phdr = (Elf64_Phdr *)((char *)phdr + ehdr->e_phentsize);
    }
    
    /* 设置动态段权限 */
    unsigned long page_start = ((unsigned long)base + phdr->p_vaddr) & ~0xFFFUL;
    unsigned long page_end = ((unsigned long)base + phdr->p_vaddr + phdr->p_memsz + 0xFFF) & ~0xFFFUL;
    sys_mprotect((void *)page_start, page_end - page_start, PROT_READ | PROT_WRITE);
    
    /* 重定位 */
    bangcle_do_relocations_clean(
        (unsigned int *)(base + loader->dynsym_offset),
        (unsigned int *)(base + loader->reloc_offset),
        loader->reloc_size,
        (unsigned int)(unsigned long)base);
    
    /* 释放 */
    sys_munmap(packed_data, loader->decompress_param);
    
    return 0;
}

九、初始化入口

C
/*
 * ============================================================================
 * 函数名: .init_proc (0x10C098)
 * ============================================================================
 * 功能描述:
 *   ELF初始化函数,在库加载时自动执行
 *   保存所有寄存器,调用解壳流程,恢复寄存器
 * 
 * 调用链:
 *   linker -> .init_proc -> sub_10C7DC -> sub_10C50C -> 解壳完成
 */
void __attribute__((constructor)) init_proc(void)
{
    /* 计算加载器信息地址 */
    extern char qword_10CB48[];
    extern char off_10C078[];
    extern char qword_2AE10[];
    
    /* 通过PC相对寻址计算运行时地址 */
    char *current_pc = /* ADR获取的当前PC */;
    char *loader_info = qword_10CB48 + (current_pc - off_10C078);
    char *packed_data = qword_2AE10 + (current_pc - off_10C078);
    
    /* 准备解压缓冲区 */
    void *buffer = bangcle_prepare_buffer(loader_info, packed_data);
    
    /* 执行解壳 */
    bangcle_unpack_main_clean(
        (bangcle_loader_t *)loader_info,
        packed_data,
        buffer
    );
    
    /* 检查是否有后续初始化函数 */
    void (*next_init)(void) = /* 从偏移量计算 */;
    if (next_init != NULL) {
        next_init();
    }
}

十、混淆特征总结

混淆技术位置描述
控制流平坦化 多处 状态机循环 0x111 → 0x46,包含永不执行的死代码分支
BL地址获取 0x10C5C8 利用 BL 指令将下一条地址存入 LR,获取嵌入数据地址
嵌入式数据 0x10C5CC 代码中嵌入字符串,破坏反汇编连续性
条件混淆 0x10C638-0x10C650 使用 CSET+CMP 构造复杂条件
栈指针操作 0x10C590 无意义的 ADD SP, SP, #0x40
寄存器复用 多处 同一寄存器在不同上下文中含义不同

十一、第三方库识别

函数识别结果置信度
sub_10C914 aPLib 解压算法
sub_10C1E8 memcmp/strncmp 混合
sub_10C28C strlen
sub_10C224 __clear_cache 变种
整体 Bangcle(梆梆加固) 确认

十二、简明伪代码总结

C
void bangcle_init(void) 
{
    // 步骤1:准备战场
    buffer = mmap(RWX内存);
    memcpy(buffer, 压缩数据);
    flush_cache(buffer);
    
    // 步骤2:我是不是还是原装的?
    if (嵌入签名被篡改) {
        check_maps("/proc/self/maps");  // 可能终止进程
    }
    
    // 步骤3:解开保护
    mprotect(代码区, RWX);
    aplib_decompress(目标, 源);
    
    // 步骤4:解密
    for (每4字节) {
        *ptr ^= xor_key;
    }
    flush_cache(解密区域);
    
    // 步骤5:重建世界
    找到第2个PT_LOAD段;
    mprotect(动态段, RW);
    apply_relocations();
    
    // 步骤6:清理痕迹
    munmap(压缩数据);
    
    // 解壳完成,程序可以正常运行
}

 

 

 

 

 

动调libdexhelper.so

1. 手机端调试服务部署

手机开启usb调试并root

手机连接电脑,输入  adb devices

adb shell su
cd /data/local/tmp
adb push "D:\Program Files (x86)\IDA Pro_v7.5 Portable\dbgsrv\android_server" /data/local/tmp #因为我们这次调试的app的so是32为的所以我们选择android_server文件
chmod 777 android_server # 赋予执行权限
./android_server # 启动调试服务

adb forward tcp:23946 tcp:23946 这条命令是将手机监听的端口通过usb转发到电脑的这个端口上来 默认监听端口:23946
至此手机调试环境搭建完成

我们调试启动我们的app
adb shell am start -D -n cn.pbcdci.fincryptography.appetizer/cn.pbcdci.fincryptography.appetizer.MainActivity 注意这里的app中有个文件androidmanifest.xml必须有可调式的配置例如 <application android:debuggable="true"> 。然后ddms里对应app前会出现红色昆虫。此时手机app启动调试模式等待被调试。

4. IDA Pro调试环境配置
用IDA打开目标.so文件,在关键函数(如JNI_OnLoad/目标函数)按F2下断点

调试器设置
在IDA里面选择
Debugge---Debugger options
选择第一列的3、4、5 (进程/线程/DLL事件暂停,这里我只选择入口暂停) 再Edit exceptions 全选删除异常。
Debugger → Process options:
填上hostname 127.0.0.1 Port: 23946 点OK即可
至此电脑调试环境搭建完成

附加进程
最后Debugger → Attach to process
选择目标包名进程

5. 恢复APP执行

打开ddms查看调试端口号

image

jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8889  调试端口号。

在IDA里面点运行。 此时红色变为绿色,同时手机上的提示框会消失。 此时手机app启动调试模式正在被调试。同时命令行出现提示,如果没有出现需要重新执行监听端口的第五步。

image

 此时一个app会黑屏,同时手机上的提示框会消失。

 

6. 触发调试断点

image

 在模块处搜索apk

需要一直按下f9三次,直到模块中出现apk后,

我们再开个ida打开这个so静态分析,导出函数搜索init,找到偏移地址LOAD:000000000010C098,记下偏移10C098

依据apk的基地址上图红框,加你的方法偏移10C098,计算绝对地址,

按下g,输入绝对地址调转到目标地址,数据是ff开头,下断点

如果不是ff而是0就是有错误需要重新来过,原因未知

image

 

在按下f9运行到断点处,

按下p识别为代码

我们发现执行到最后一句  return a1;函数就会异常终止,因此反调试在上一句的函数调用中

测试发现 右键运行到光标位置,也就是如下图红框的位置

image

运行到这个断点这里,我们发现x14里放的是elf解压后的文件,

f6单步步过调试,发现返回到了linker中,

执行导出so的脚本

import idc
import idaapi

# 配置
SO_BASE = 0x6FF2A3E000
OUTPUT_PATH = "/tmp/dumped_real.so"

def get_so_size(base):
    """通过 ELF 头计算 SO 大小"""
    # Section Header 表的结束位置通常就是文件大小
    e_shoff = idc.get_qword(base + 0x28)    # Section Header 偏移
    e_shentsize = idc.get_wide_word(base + 0x3A)  # Section Header 条目大小
    e_shnum = idc.get_wide_word(base + 0x3C)      # Section Header 数量
    
    if e_shoff > 0 and e_shnum > 0:
        size = e_shoff + (e_shentsize * e_shnum)
        return size
    
    # 备选:通过 Program Header 计算
    e_phoff = idc.get_qword(base + 0x20)
    e_phentsize = idc.get_wide_word(base + 0x36)
    e_phnum = idc.get_wide_word(base + 0x38)
    
    max_end = 0
    for i in range(e_phnum):
        ph_addr = base + e_phoff + (i * e_phentsize)
        p_offset = idc.get_qword(ph_addr + 0x08)
        p_filesz = idc.get_qword(ph_addr + 0x20)
        end = p_offset + p_filesz
        if end > max_end:
            max_end = end
    
    return max_end

def dump_so(base, output_path):
    """Dump SO 到文件"""
    # 验证 ELF 魔术字节
    magic = idc.get_bytes(base, 4)
    if magic != b'\x7fELF':
        print(f"[-] Invalid ELF magic at 0x{base:X}: {magic}")
        return False
    
    print(f"[+] Valid ELF found at 0x{base:X}")
    
    # 获取大小
    size = get_so_size(base)
    print(f"[+] Calculated SO size: 0x{size:X} ({size} bytes)")
    
    # 读取数据
    data = idc.get_bytes(base, size)
    if data is None:
        print("[-] Failed to read memory")
        return False
    
    # 写入文件
    with open(output_path, 'wb') as f:
        f.write(data)
    
    print(f"[+] SO dumped to: {output_path}")
    return True

# 执行 Dump
dump_so(SO_BASE, OUTPUT_PATH)

 

 执行下面脚本,注意修改入口地址,执行脚本查找jni_onload,在控制栏中双击地址跳转过去

"""
IDA Python 脚本 - 查找 JNI_OnLoad (IDA 9.x 兼容版本)
"""
import idc
import idaapi
import ida_bytes

SO_BASE = 0x6FF2A3E000

def read_dword(addr):
    """读取 4 字节"""
    return ida_bytes.get_dword(addr)

def read_qword(addr):
    """读取 8 字节"""
    return ida_bytes.get_qword(addr)

def read_word(addr):
    """读取 2 字节"""
    return ida_bytes.get_word(addr)

def read_bytes(addr, size):
    """读取字节数组"""
    return ida_bytes.get_bytes(addr, size)

def read_string(addr):
    """读取 C 字符串"""
    result = b""
    while True:
        b = ida_bytes.get_byte(addr)
        if b == 0:
            break
        result += bytes([b])
        addr += 1
    return result.decode('utf-8', errors='ignore')

def find_jni_onload():
    """通过解析 ELF 动态符号表找到 JNI_OnLoad"""
    
    # 验证 ELF
    magic = read_bytes(SO_BASE, 4)
    if magic != b'\x7fELF':
        print(f"[-] Invalid ELF at 0x{SO_BASE:X}")
        return None
    
    print(f"[+] Valid ELF at 0x{SO_BASE:X}")
    
    # 读取 ELF 头
    e_phoff = read_qword(SO_BASE + 0x20)
    e_phnum = read_word(SO_BASE + 0x38)
    
    print(f"[+] Program Headers: offset=0x{e_phoff:X}, count={e_phnum}")
    
    # 找 PT_DYNAMIC 段
    dynamic_addr = 0
    for i in range(e_phnum):
        ph = SO_BASE + e_phoff + (i * 0x38)  # sizeof(Elf64_Phdr) = 0x38
        p_type = read_dword(ph)
        
        if p_type == 2:  # PT_DYNAMIC
            p_vaddr = read_qword(ph + 0x10)
            dynamic_addr = SO_BASE + p_vaddr
            print(f"[+] PT_DYNAMIC found at: 0x{dynamic_addr:X}")
            break
    
    if dynamic_addr == 0:
        print("[-] PT_DYNAMIC not found")
        return None
    
    # 解析 dynamic 段找符号表和字符串表
    symtab = 0
    strtab = 0
    hash_addr = 0
    gnu_hash = 0
    
    offset = 0
    while True:
        d_tag = read_qword(dynamic_addr + offset)
        d_val = read_qword(dynamic_addr + offset + 8)
        
        if d_tag == 0:  # DT_NULL
            break
        elif d_tag == 5:  # DT_STRTAB
            strtab = d_val
        elif d_tag == 6:  # DT_SYMTAB
            symtab = d_val
        elif d_tag == 4:  # DT_HASH
            hash_addr = d_val
        elif d_tag == 0x6ffffef5:  # DT_GNU_HASH
            gnu_hash = d_val
        
        offset += 16
    
    print(f"[+] SYMTAB: 0x{symtab:X}")
    print(f"[+] STRTAB: 0x{strtab:X}")
    
    if symtab == 0 or strtab == 0:
        print("[-] Symbol table or string table not found")
        return None
    
    # 遍历符号表找 JNI_OnLoad
    syment = 0x18  # sizeof(Elf64_Sym)
    sym_idx = 0
    max_syms = 2000
    
    print("[*] Searching for JNI_OnLoad in symbol table...")
    
    while sym_idx < max_syms:
        sym_addr = symtab + (sym_idx * syment)
        st_name = read_dword(sym_addr)
        st_value = read_qword(sym_addr + 8)
        
        if st_name != 0:
            # 读取符号名
            name = read_string(strtab + st_name)
            
            if "JNI_OnLoad" in name:
                jni_onload = SO_BASE + st_value
                print(f"[+] Found: {name} at 0x{jni_onload:X}")
                return jni_onload
        
        sym_idx += 1
    
    print("[-] JNI_OnLoad not found in symbol table")
    return None

def search_jni_onload_string():
    """备选方案:搜索 JNI_OnLoad 字符串"""
    
    print("\n[*] Fallback: Searching for JNI_OnLoad string...")
    
    SO_SIZE = 0x300000  # 3MB
    search_pattern = b"JNI_OnLoad"
    
    addr = SO_BASE
    while addr < SO_BASE + SO_SIZE:
        data = read_bytes(addr, len(search_pattern) + 1)
        if data and data[:len(search_pattern)] == search_pattern:
            print(f"[+] Found 'JNI_OnLoad' string at: 0x{addr:X}")
            return addr
        addr += 0x1000  # 每页搜索
    
    # 更细致的搜索
    addr = SO_BASE
    step = 1
    while addr < SO_BASE + SO_SIZE:
        data = read_bytes(addr, len(search_pattern))
        if data == search_pattern:
            print(f"[+] Found 'JNI_OnLoad' string at: 0x{addr:X}")
            return addr
        addr += step
    
    print("[-] JNI_OnLoad string not found")
    return None

def set_common_breakpoints():
    """设置常见的关键断点"""
    
    print("\n[*] Setting common breakpoints...")
    
    # 入口点
    e_entry = read_qword(SO_BASE + 0x18)
    if e_entry and e_entry < 0x1000000:
        entry = SO_BASE + e_entry
        idc.add_bpt(entry)
        print(f"[+] Entry point: 0x{entry:X}")
    
    # init_array
    INIT_ARRAY = 0x6FF2A68E10
    for i in range(10):
        ptr = read_qword(INIT_ARRAY + i * 8)
        if ptr and ptr > SO_BASE and ptr < SO_BASE + 0x1000000:
            idc.add_bpt(ptr)
            print(f"[+] Init array[{i}]: 0x{ptr:X}")
        elif ptr == 0:
            break

# ============ 主执行 ============
print("=" * 60)
print("   JNI_OnLoad Finder (IDA 9.x Compatible)")
print("=" * 60)

# 方法1: 解析符号表
addr = find_jni_onload()

# 方法2: 字符串搜索
if not addr:
    str_addr = search_jni_onload_string()

# 方法3: 设置通用断点
set_common_breakpoints()

if addr:
    idc.add_bpt(addr)
    print(f"\n[+] Breakpoint set at JNI_OnLoad: 0x{addr:X}")
    print("[*] Press F9 to continue execution")
else:
    print("\n[-] JNI_OnLoad not found automatically")
    print("[*] Common breakpoints have been set")
    print("[*] Press F9 to continue, or manually search")

print("=" * 60)

按p按f5

找到 两个核心函数

  我们进入jni后一直执行发现调用函数DA4后退出,我们ctlr+f输入DA4

右键运行到光标所在处

 f7进入函数DA4

因此关键反调试代码就在这个函数内,在附近代码报错,

((__int64 (__fastcall *)(_BYTE *, _QWORD, __int64 (__fastcall *)(), _BYTE *))unk_6FFC17FFE0)(
    v941,           // pthread_t*
    0,              // attr
    sub_6FFC1A1B54, // 线程函数
    v164)           // 参数 (v160 的值)

 1.我们要修复的第一个报错位置在DA4函数,为什么我要将函数sub_6FF2A6FDA4重命名为DA4,以为因为这个地址是绝对地址,DA4是后三位的相对地址。

image

对应的汇编

image

 

 

  1. 右键同步定位到地址 0x6FF2A71788

  2. 菜单:Edit → Patch program → Change byte...

  3. 修改前4个字节:

    • 把 16 改成 00
    • 把 DE 改成 00
    • 把 FF 改成 80
    • 把 97 改成 52
  4. 按 C 重新分析代码,应该显示:

base.apk:0000006FF2A71788 MOV             W0, #0
base.apk:0000006FF2A7178C CBZ             W0, loc_6FF2A717C4  ; 会跳转

2.在2148行

if ( *(_BYTE *)(v149 + 65) )
{
v181 = ((__int64 (*)(void))unk_6FFC181940)();
((void (__fastcall *)(_BYTE *, _QWORD))unk_6FFC1AB918)(v180, v181);
goto LABEL_158;
}

 这里报错unk_6FFC181940

 

Patch BL 调用为 MOV W0, #1

找到地址 0x6FFC187F68 附近的 BL unk_6FFC181940

assembly
; 原始
BL      unk_6FFC181940    ; 返回 PID

; 修改为
MOV     W0, #1            ; 假装返回 PID = 1

Patch 字节:

text
原始: XX XX XX 97 (BL 指令)
修改: 20 00 80 52 (MOV W0, #1)


3.
base.apk:0000006FFC1AB9B0 BL              unk_6FFC180FE0



"""
SecNeo/Bangcle Anti-Debug Comprehensive Patcher v2.0
=====================================================
在 DA4 函数内扫描并修复 所有 反调试相关调用:

  Patch 1: BL pthread_create (后缀 0xFE0) → MOV W0, #0
           跳过所有反调试/监控线程创建 (约10处)

  Patch 2: BL getpid (后缀 0x940)         → MOV W0, #1
           返回假 PID,使监控系统失效 (约4-5处)
           自动排除 /proc/%d/maps 合法调用

用法: IDA → File → Script file → 选择此脚本
"""

import idaapi
import idautils
import idc
import ida_bytes
import ida_funcs

# ═══════════════════════════════════════════════════════
#  常量
# ═══════════════════════════════════════════════════════

# ARM64 补丁指令 (小端序)
MOV_W0_0 = b'\x00\x00\x80\x52'   # MOV W0, #0
MOV_W0_1 = b'\x20\x00\x80\x52'   # MOV W0, #1

# 函数地址后缀 (& 0xFFF)
SUFFIX_MASK      = 0xFFF
S_PTHREAD_CREATE = 0xFE0    # unk_XXX0FE0 = pthread_create wrapper
S_GETPID         = 0x940    # unk_XXX9940 = getpid
S_SNPRINTF       = 0x8AC    # unk_XXXC8AC = snprintf / sprintf
S_MONITOR_REG    = 0x918    # sub_XXX3918 = 反调试监控注册
S_MONITOR_868    = 0x868    # sub_XXX7868 = 进程信号/监控

# 上下文扫描窗口 (向后扫描多少条指令)
CONTEXT_WINDOW = 10


# ═══════════════════════════════════════════════════════
#  辅助函数
# ═══════════════════════════════════════════════════════

def is_bl_instruction(ea):
    """检查 ea 处是否为 BL 指令 (未被 Patch)"""
    b = ida_bytes.get_bytes(ea, 4)
    if not b or len(b) < 4:
        return False
    # ARM64 BL: bits[31:26] = 100101
    # byte[3] & 0xFC == 0x94 → 匹配 0x94/0x95/0x96/0x97
    return (b[3] & 0xFC) == 0x94


def is_already_patched(ea):
    """检查是否已经被 Patch 过"""
    b = ida_bytes.get_bytes(ea, 4)
    return b in (MOV_W0_0, MOV_W0_1)


def get_bl_target(ea):
    """获取 BL 指令的目标地址"""
    refs = list(idautils.CodeRefsFrom(ea, 0))
    if refs:
        return refs[0]
    return idc.get_operand_value(ea, 0)


def suffix_match(addr, suffix):
    """地址后缀匹配"""
    return addr is not None and (addr & SUFFIX_MASK) == suffix


def scan_for_suffix_after(ea, func_end, suffix, window=CONTEXT_WINDOW):
    """
    从 ea 之后扫描 window 条指令,
    检查是否存在 BL 到指定后缀的函数
    """
    cur = ea + 4
    for _ in range(window):
        if cur >= func_end:
            break
        if idc.print_insn_mnem(cur) == "BL":
            t = get_bl_target(cur)
            if suffix_match(t, suffix):
                return cur  # 返回找到的地址
        cur = idc.next_head(cur, func_end)
    return None


def classify_getpid_context(ea, func_end):
    """
    分析 getpid 调用的上下文,判断是否为反调试用途

    返回: (should_patch: bool, reason: str)
    """
    # 检查1: 后面紧跟 snprintf → 合法的 /proc/%d/maps
    snprintf_addr = scan_for_suffix_after(ea, func_end, S_SNPRINTF, 8)
    if snprintf_addr:
        return False, f"→ snprintf @ +{snprintf_addr - ea} (合法: /proc/maps)"

    # 检查2: 后面紧跟 sub_918 (监控注册) → 反调试
    monitor_addr = scan_for_suffix_after(ea, func_end, S_MONITOR_REG, 10)
    if monitor_addr:
        return True, f"→ monitor_reg @ +{monitor_addr - ea} (反调试注册)"

    # 检查3: 后面紧跟 sub_868 (信号/监控) → 反调试
    signal_addr = scan_for_suffix_after(ea, func_end, S_MONITOR_868, 10)
    if signal_addr:
        return True, f"→ monitor_868 @ +{signal_addr - ea} (反调试信号)"

    # 检查4: 不符合以上模式,但也没有 snprintf → 大概率反调试
    # (存储 PID 到监控结构体的情况)
    return True, "→ 无 snprintf (推断: 监控结构体)"


def find_da4_function():
    """
    自动定位 DA4 主初始化函数
    策略1: 函数名以 DA4 结尾
    策略2: 地址后缀 0xDA4 且函数体 > 0x3000
    策略3: 地址后缀 0xDA4 且函数体 > 0x1000
    """
    candidates = []

    for fea in idautils.Functions():
        name = (idc.get_func_name(fea) or "").upper()
        func = ida_funcs.get_func(fea)
        if not func:
            continue
        size = func.end_ea - func.start_ea

        # 名称匹配
        if name.endswith("DA4") or name.endswith("FDA4"):
            if size > 0x3000:
                return fea
            candidates.append((fea, size, "name"))

        # 地址匹配
        if (fea & 0xFFF) == 0xDA4:
            if size > 0x3000:
                return fea
            if size > 0x1000:
                candidates.append((fea, size, "addr"))

    # 返回最大的候选
    if candidates:
        candidates.sort(key=lambda x: x[1], reverse=True)
        return candidates[0][0]

    return None


def read_hex(ea, n=4):
    """读取字节的十六进制表示"""
    b = ida_bytes.get_bytes(ea, n)
    return b.hex().upper() if b else "?" * (n * 2)


# ═══════════════════════════════════════════════════════
#  主逻辑
# ═══════════════════════════════════════════════════════

def run_patcher():
    print("\n" + "=" * 72)
    print("  SecNeo/Bangcle Anti-Debug Comprehensive Patcher v2.0")
    print("  修复范围: DA4 函数内所有 pthread_create + getpid 反调试调用")
    print("=" * 72)

    # ── 1. 定位函数 ──
    func_ea = find_da4_function()

    if not func_ea:
        print("[!] 自动检测失败,请手动输入")
        func_ea = idaapi.ask_addr(
            idc.here(),
            "输入 DA4 函数地址 (如 sub_XXXXXXDA4):"
        )
        if not func_ea or func_ea == idc.BADADDR:
            print("[!] 已取消")
            return 0

    func = ida_funcs.get_func(func_ea)
    func_end = func.end_ea if func else func_ea + 0x12000
    func_name = idc.get_func_name(func_ea) or f"0x{func_ea:X}"

    print(f"\n[*] 函数名称 : {func_name}")
    print(f"[*] 地址范围 : 0x{func_ea:X} ~ 0x{func_end:X}")
    print(f"[*] 函数大小 : 0x{func_end - func_ea:X} ({func_end - func_ea} bytes)")

    # ── 2. 扫描所有 BL 指令 ──
    patch1_targets = []    # pthread_create → MOV W0, #0
    patch2_targets = []    # getpid         → MOV W0, #1
    skipped = []           # 跳过的合法调用
    already_patched = []   # 已经被 Patch 过的

    ea = func_ea
    while ea < func_end:
        if ea == idc.BADADDR:
            break

        mnem = idc.print_insn_mnem(ea)

        if mnem == "BL":
            offset = ea - func_ea
            target = get_bl_target(ea)

            if is_already_patched(ea):
                # 已经 Patch 过
                if suffix_match(target, S_PTHREAD_CREATE):
                    already_patched.append((ea, offset, "Patch1 (pthread_create)"))
                elif suffix_match(target, S_GETPID):
                    already_patched.append((ea, offset, "Patch2 (getpid)"))

            elif is_bl_instruction(ea):
                orig_bytes = ida_bytes.get_bytes(ea, 4)

                # ── Patch 1: pthread_create ──
                if suffix_match(target, S_PTHREAD_CREATE):
                    # 确认下一条是 CBZ/CBNZ/CMP 等条件检查
                    next_ea = idc.next_head(ea, func_end)
                    next_mnem = idc.print_insn_mnem(next_ea) if next_ea else ""
                    patch1_targets.append({
                        'addr': ea,
                        'target': target,
                        'orig': orig_bytes,
                        'offset': offset,
                        'next_mnem': next_mnem,
                    })

                # ── Patch 2: getpid ──
                elif suffix_match(target, S_GETPID):
                    should_patch, reason = classify_getpid_context(ea, func_end)
                    if should_patch:
                        patch2_targets.append({
                            'addr': ea,
                            'target': target,
                            'orig': orig_bytes,
                            'offset': offset,
                            'reason': reason,
                        })
                    else:
                        skipped.append({
                            'addr': ea,
                            'offset': offset,
                            'reason': reason,
                        })

        ea = idc.next_head(ea, func_end)

    # ── 3. 应用 Patch 1 ──
    print(f"\n{'─' * 72}")
    print(f"  PATCH 1: pthread_create → MOV W0, #0")
    print(f"  效果: 返回成功(0)但不创建线程,后续 CBZ 跳过重试循环")
    print(f"  发现: {len(patch1_targets)} 处")
    print(f"{'─' * 72}")

    if not patch1_targets:
        print(f"  (无目标)")
    else:
        for i, p in enumerate(patch1_targets):
            ida_bytes.patch_bytes(p['addr'], MOV_W0_0)
            print(
                f"  [{i+1:2d}] 0x{p['addr']:X}  "
                f"(+0x{p['offset']:04X})  "
                f"[{p['orig'].hex().upper()}] → [00008052]  "
                f"BL 0x{p['target']:X}"
                f"  next={p['next_mnem']}"
            )

    # ── 4. 应用 Patch 2 ──
    print(f"\n{'─' * 72}")
    print(f"  PATCH 2: getpid → MOV W0, #1")
    print(f"  效果: 返回假 PID=1,监控系统跟踪 init 而非自身")
    print(f"  发现: {len(patch2_targets)} 处反调试, {len(skipped)} 处合法(已跳过)")
    print(f"{'─' * 72}")

    if not patch2_targets:
        print(f"  (无目标)")
    else:
        for i, p in enumerate(patch2_targets):
            ida_bytes.patch_bytes(p['addr'], MOV_W0_1)
            print(
                f"  [{i+1:2d}] 0x{p['addr']:X}  "
                f"(+0x{p['offset']:04X})  "
                f"[{p['orig'].hex().upper()}] → [20008052]  "
                f"BL 0x{p['target']:X}"
            )
            print(f"       上下文: {p['reason']}")

    # ── 5. 跳过的合法调用 ──
    if skipped:
        print(f"\n  ── 已跳过 (合法调用) ──")
        for s in skipped:
            print(
                f"   ✓ 0x{s['addr']:X}  "
                f"(+0x{s['offset']:04X})  "
                f"{s['reason']}"
            )

    # ── 6. 已 Patch 过的 ──
    if already_patched:
        print(f"\n  ── 已存在的 Patch (跳过) ──")
        for addr, offset, desc in already_patched:
            print(f"   • 0x{addr:X}  (+0x{offset:04X})  {desc}")

    # ── 7. 验证 ──
    print(f"\n{'─' * 72}")
    print(f"  验证修复结果...")
    print(f"{'─' * 72}")

    errors = 0
    for p in patch1_targets:
        actual = ida_bytes.get_bytes(p['addr'], 4)
        status = "" if actual == MOV_W0_0 else ""
        if actual != MOV_W0_0:
            errors += 1
            print(f"  {status} 0x{p['addr']:X}  "
                  f"期望 [00008052] 实际 [{actual.hex().upper()}]")

    for p in patch2_targets:
        actual = ida_bytes.get_bytes(p['addr'], 4)
        status = "" if actual == MOV_W0_1 else ""
        if actual != MOV_W0_1:
            errors += 1
            print(f"  {status} 0x{p['addr']:X}  "
                  f"期望 [20008052] 实际 [{actual.hex().upper()}]")

    if errors == 0:
        print(f"  ✓ 全部验证通过")
    else:
        print(f"  ✗ {errors} 处验证失败!")

    # ── 8. 汇总 ──
    total = len(patch1_targets) + len(patch2_targets)

    print(f"\n{'=' * 72}")
    print(f"  ┌────────────────────────────────────────────┐")
    print(f"  │  修复汇总                                  │")
    print(f"  ├────────────────────────────────────────────┤")
    print(f"  │  Patch 1 (pthread_create): {len(patch1_targets):2d} 处            │")
    print(f"  │  Patch 2 (getpid)        : {len(patch2_targets):2d} 处            │")
    print(f"  │  合法调用 (已跳过)       : {len(skipped):2d} 处            │")
    print(f"  │  已存在 Patch (跳过)     : {len(already_patched):2d} 处            │")
    print(f"  │  验证错误               :  {errors} 处            │")
    print(f"  ├────────────────────────────────────────────┤")
    print(f"  │  总计修复               : {total:2d} 处            │")
    print(f"  └────────────────────────────────────────────┘")
    print(f"{'=' * 72}")

    if total > 0:
        print(f"""
  后续操作:
  ────────
  1. 在每个修复地址按 'C' 重新分析为代码
  2. 按 F5 重新反编译,确认 BL 已变为 MOV W0, #N
  3. Edit → Patch program → Apply patches to input file
  4. 重新签名 APK 后测试

  预期反编译效果:
  ──────────────
  修复前: if (pthread_create(&tid, NULL, anti_debug, arg)) ...
  修复后: if (0)  ← 永远不进入重试循环

  修复前: pid = getpid();  monitor_register(ptr, pid);
  修复后: pid = 1;         monitor_register(ptr, 1);  ← 监控init进程
""")

    return total


# ═══════════════════════════════════════════════════════
#  入口
# ═══════════════════════════════════════════════════════
if __name__ == "__main__":
    run_patcher()

 

 

 

如果直接运行到指定的调试位置会直接报错调转到libc处,必须一步一步执行,原因暂时未知

f6直到第一个循环处

右键运行到指向到1235行的

image

右键运行到指向到1367行,继续f6

image

 右键运行到1667

image

 右键1739

image

 右键1749

image

 

奔溃位置

image







1848处奔溃,需要调试
2249
4879

1921
2315奔溃

 

2291奔溃 if ( v214 )
2831 ((void(*)(_OWORDint64,constchar3...))unk_71B39838AC)(v896,256,%s"aLibdatajarSo);
2839位置奔溃 v290 = dl_dlopen(v896, 2);

memset(v896, 0, sizeof(v896));
if ( v215 > 18 )
((void (*)(_OWORD *, __int64, const char , ...))unk_71B39838AC)(v896, 256, "%s", aLibdatajarSo);
else
((void (
)(_OWORD *, __int64, const char *, ...))unk_71B39838AC)(
v896,
256,
"%s/%s",
*(const char **)(qword_71B3A4A3F8 + 600),
aLibdatajarSo);
v290 = dl_dlopen(v896, 2);

__int64 sub_71B39838AC(__int64 a1, __int64 a2, __int64 a3, ...)
{
unsigned __int64 StatusReg; // x19
__int64 result; // x0
__int64 v5; // x1
int v6; // w2
gcc_va_list va1; // [xsp+B0h] [xbp-60h] BYREF
gcc_va_list va; // [xsp+D8h] [xbp-38h] BYREF
__int64 v9; // [xsp+F8h] [xbp-18h]

va_start(va, a3);
StatusReg = _ReadStatusReg(TPIDR_EL0);
v9 = *(_QWORD *)(StatusReg + 40);
va_copy(va1, va);
result = ((__int64 (__fastcall *)(__int64, _QWORD, __int64, __int64, __va_list_tag *))unk_71B3981A50)(
a1,
0,
a2,
a3,
va1);
if ( *(_QWORD *)(StatusReg + 40) != v9 )
{
result = ((__int64 (__fastcall *)(__int64))unk_71B3980F60)(result);
*(_QWORD *)result = v5;
*(_DWORD *)(result + 8) = v6;
*(_QWORD *)(result + 16) = 0;
}
return result;
}

base.apk:00000071B39893AC
base.apk:00000071B39893AC loc_71B39893AC ; CODE XREF: sub_71B3986DA4+23C0↑j
base.apk:00000071B39893AC ADRP X2, #aS@PAGE ; "%s"
base.apk:00000071B39893B0 ADRP X3, #aLibdatajarSo@PAGE ; "libdatajar.so"
base.apk:00000071B39893B4 ADD X2, X2, #aS@PAGEOFF ; "%s"
base.apk:00000071B39893B8 ADD X3, X3, #aLibdatajarSo@PAGEOFF ; "libdatajar.so"
base.apk:00000071B39893BC ADD X0, SP, #0x9F0+var_770
base.apk:00000071B39893C0 MOV W1, #0x100
执行到这里 base.apk:00000071B39893C4 BL unk_71B39838AC
base.apk:00000071B39893C8
base.apk:00000071B39893C8 loc_71B39893C8 ; CODE XREF: sub_71B3986DA4+23EC↑j
base.apk:00000071B39893C8 ADD X0, SP, #0x9F0+var_770
base.apk:00000071B39893CC MOV W1, #2
base.apk:00000071B39893D0 BL _dl_dlopen

base.apk:00000071B39838AC unk_71B39838AC DCB 0xFF ; CODE XREF: sub_71B3984930+548↓p
base.apk:00000071B39838AC ; sub_71B3984930+564↓p ...

X0 0000007FF6FD4930 [stack] → 0000000000000000 seg000 → 00010102464C457F
X1 0000000000000100 seg000 → 00000000000D64A8 seg000 → 0000000000000000 seg000 → 00010102464C457F
X2 00000071B3A27485 base.apk → 0000000028007325
X3 00000071B3A1DC81 base.apk → "libdatajar.so"
X4 00000071A9FFF460 [stack:24567] → 00005F8700005FF7
X5 00000071B8A00ECC __pthread_start(void *) → STP X26, X25, [SP,#-0x50]!
X6 00000071A9FFF450 [stack:24567] → 00000071AA333450 [stack:24565] → 00000071B3BCE450 [stack:24491] → 00000071B3E0B450 [stack:24474] → 00000071B441A450 [stack:24473]
X7 0000000000000020 seg000 → 0000000000000040 seg000 → 0000000500000001
X8 0000007FF6FD4830 [stack] → "/data/user/0/cn.pbcdci.fincryptography.appetizer/.cache/oat/arm6"
X9 0000000000000000 seg000 → 00010102464C457F
X10 0000007FF6FD4768 [stack] → 0000000000000000 seg000 → 00010102464C457F
X11 00000000AAAAAAAB
X12 000000000000006B seg000 → 0010000000000000
X13 000000000000006D seg000 → 0000001000000000
X14 000000000000000C seg000 → 00B7000300000000
X15 2E8BA2E8BA2E8BA3
X16 00000071B8A5A458 stats_arenas_i_hchunks_j_node+818 → 00000071B8A01C88 pthread_mutex_lock+3A0 → STP X22, X21, [SP,#0x80+var_B0]!
X17 00000071B8A01C88 pthread_mutex_lock+3A0 → STP X22, X21, [SP,#0x80+var_B0]!
X18 00000071B3A1C10F base.apk → DCB 0
X19 0000007FF6FD4C30 [stack] → 00000071AA333450 [stack:24565] → 00000071B3BCE450 [stack:24491] → 00000071B3E0B450 [stack:24474] → 00000071B441A450 [stack:24473]
X20 0000000000000000 seg000 → 00010102464C457F
X21 00000071AE6A8D80 [anon:libc_malloc] → 0000000100000019
X22 0000000000000001 seg000 → 0000010102464C45
X23 00000071AE6A8D80 [anon:libc_malloc] → 0000000100000019
X24 00000071B3A4A420 [anon:.bss] → 0000100800000001
X25 0000000000000019 seg000 → 40000000000002D3
X26 0000000000000000 seg000 → 00010102464C457F
X27 00000071B52420C0 [anon:libc_malloc] → 00000071B4FEC028 art::gCheckNativeInterface → 0000000000000000 seg000 → 00010102464C457F
X28 0000000000000021 seg000 → 4000000000000000
X29 0000007FF6FD50A0 [stack] → 0000007FF6FD5AD0 [stack] → 0000007FF6FD5C20 [stack] → 0000007FF6FD5CC0 [stack] → 0000007FF6FD5D00 [stack]
X30 00000071B3988AA4 sub_71B3986DA4+1D00 → LDRB W8, [X20]
SP 0000007FF6FD46B0 [stack] → 00000071AF4AC6B4 boot.oat → 0000000200020002
PC 00000071B39893C4 sub_71B3986DA4+2620 → BL unk_71B39838AC
PSR 20200000

M 0 T 0 F 0 I 0 A 0 E 0 IT 0 GE 0 IL 0 IT2 0
Q 0 V 0 C 1 Z 0 N 0

base.apk:00000071B39893C8 ADD X0, SP, #0x9F0+var_770

X0 000000000000000D seg000 → 0100B70003000000
X1 0000000000000000 seg000 → 00010102464C457F
X2 0000000000000004 seg000 → 0000000000010102 seg000 → "ISl5I5I5$Sl5_S$SI5S5I5I5IS05_5I5$S0SlS_SlSlSIS$Sl"
X3 0000007FF6FD3C80 [stack] → 0000000000000000 seg000 → 00010102464C457F
X4 76006F732E7261FF
X5 0000000000000000 seg000 → 00010102464C457F
X6 0000007FF6FD493C [stack] → 000000000000006F seg000 → 0000000000100000 LOAD → FFFFFFFFFFFFFFFF
X7 6073606361686BFE
X8 ABA8E9D8AD274316
X9 ABA8E9D8AD274316
X10 0000000000000001 seg000 → 0000010102464C45
X11 0000000000000010 seg000 → 0000000100B70003
X12 000000000CCCCCCC
X13 000000008000002F
X14 00000071B8A3E9E8 __vfprintf.xdigs_upper → "0123456789ABCDEF"
X15 00000071B8A3E68C inet_pton6.xdigits_u+14 → DCB 0
X16 00000071B8A5A468 stats_arenas_i_hchunks_j_node+828 → 00000071B89B2D14 free → ADRL X8, __libc_globals
X17 00000071B89B2D14 free → ADRL X8, __libc_globals
X18 00000071B3A1C10F base.apk → DCB 0
X19 0000007FF6FD4C30 [stack] → 00000071AA333450 [stack:24565] → 00000071B3BCE450 [stack:24491] → 00000071B3E0B450 [stack:24474] → 00000071B441A450 [stack:24473]
X20 0000000000000000 seg000 → 00010102464C457F
X21 00000071AE6A8D80 [anon:libc_malloc] → 0000000100000019
X22 0000000000000001 seg000 → 0000010102464C45
X23 00000071AE6A8D80 [anon:libc_malloc] → 0000000100000019
X24 00000071B3A4A420 [anon:.bss] → 0000100800000001
X25 0000000000000019 seg000 → 40000000000002D3
X26 0000000000000000 seg000 → 00010102464C457F
X27 00000071B52420C0 [anon:libc_malloc] → 00000071B4FEC028 art::gCheckNativeInterface → 0000000000000000 seg000 → 00010102464C457F
X28 0000000000000021 seg000 → 4000000000000000
X29 0000007FF6FD50A0 [stack] → 0000007FF6FD5AD0 [stack] → 0000007FF6FD5C20 [stack] → 0000007FF6FD5CC0 [stack] → 0000007FF6FD5D00 [stack]
X30 00000071B39893C8 sub_71B3986DA4+2624 → ADD X0, SP, #0x9F0+var_770
SP 0000007FF6FD46B0 [stack] → 00000071AF4AC6B4 boot.oat → 0000000200020002
PC 00000071B39893C8 sub_71B3986DA4+2624 → ADD X0, SP, #0x9F0+var_770
PSR 60000000

M 0 T 0 F 0 I 0 A 0 E 0 IT 0 GE 0 IL 0 IT2 0
Q 0 V 0 C 1 Z 1 N 0

[stack]:0000007FF6FD4930 DCB 0x6C ; l
[stack]:0000007FF6FD4931 DCB 0x69 ; i
[stack]:0000007FF6FD4932 DCB 0x62 ; b
[stack]:0000007FF6FD4933 DCB 0x64 ; d
[stack]:0000007FF6FD4934 DCB 0x61 ; a
[stack]:0000007FF6FD4935 DCB 0x74 ; t
[stack]:0000007FF6FD4936 DCB 0x61 ; a
[stack]:0000007FF6FD4937 DCB 0x6A ; j
[stack]:0000007FF6FD4938 DCB 0x61 ; a
[stack]:0000007FF6FD4939 DCB 0x72 ; r
[stack]:0000007FF6FD493A DCB 0x2E ; .
[stack]:0000007FF6FD493B DCB 0x73 ; s
[stack]:0000007FF6FD493C DCB 0x6F ; o

base.apk:00000071B39893CC MOV W1, #2
seg000:0000000000000000 dword_0 DCD 0x464C457F ; DATA XREF: seg000:00000000000052E8↓o
seg000:0000000000000000 ; seg000:0000000000005300↓o ...

base.apk:00000071B39893D0 BL _dl_dlopen
X0 0000007FF6FD4930 [stack] → "libdatajar.so"
X1 0000000000000002 seg000 → 000000010102464C
X2 0000000000000004 seg000 → 0000000000010102 seg000 → "ISl5I5I5$Sl5_S$SI5S5I5I5IS05_5I5$S0SlS_SlSlSIS$Sl"
X3 0000007FF6FD3C80 [stack] → 0000000000000000 seg000 → 00010102464C457F
X4 76006F732E7261FF
X5 0000000000000000 seg000 → 00010102464C457F
X6 0000007FF6FD493C [stack] → 000000000000006F seg000 → 0000000000100000 LOAD → FFFFFFFFFFFFFFFF
X7 6073606361686BFE
X8 ABA8E9D8AD274316
X9 ABA8E9D8AD274316
X10 0000000000000001 seg000 → 0000010102464C45
X11 0000000000000010 seg000 → 0000000100B70003
X12 000000000CCCCCCC
X13 000000008000002F
X14 00000071B8A3E9E8 __vfprintf.xdigs_upper → "0123456789ABCDEF"
X15 00000071B8A3E68C inet_pton6.xdigits_u+14 → DCB 0
X16 00000071B8A5A468 stats_arenas_i_hchunks_j_node+828 → 00000071B89B2D14 free → ADRL X8, __libc_globals
X17 00000071B89B2D14 free → ADRL X8, __libc_globals
X18 00000071B3A1C10F base.apk → DCB 0
X19 0000007FF6FD4C30 [stack] → 00000071AA333450 [stack:24565] → 00000071B3BCE450 [stack:24491] → 00000071B3E0B450 [stack:24474] → 00000071B441A450 [stack:24473]
X20 0000000000000000 seg000 → 00010102464C457F
X21 00000071AE6A8D80 [anon:libc_malloc] → 0000000100000019
X22 0000000000000001 seg000 → 0000010102464C45
X23 00000071AE6A8D80 [anon:libc_malloc] → 0000000100000019
X24 00000071B3A4A420 [anon:.bss] → 0000100800000001
X25 0000000000000019 seg000 → 40000000000002D3
X26 0000000000000000 seg000 → 00010102464C457F
X27 00000071B52420C0 [anon:libc_malloc] → 00000071B4FEC028 art::gCheckNativeInterface → 0000000000000000 seg000 → 00010102464C457F
X28 0000000000000021 seg000 → 4000000000000000
X29 0000007FF6FD50A0 [stack] → 0000007FF6FD5AD0 [stack] → 0000007FF6FD5C20 [stack] → 0000007FF6FD5CC0 [stack] → 0000007FF6FD5D00 [stack]
X30 00000071B39893C8 sub_71B3986DA4+2624 → ADD X0, SP, #0x9F0+var_770
SP 0000007FF6FD46B0 [stack] → 00000071AF4AC6B4 boot.oat → 0000000200020002
PC 00000071B39893D0 sub_71B3986DA4+262C → BL _dl_dlopen
PSR 60200000

M 0 T 0 F 0 I 0 A 0 E 0 IT 0 GE 0 IL 0 IT2 0
Q 0 V 0 C 1 Z 1 N 0

6AC: got unknown signal #11 (exc.code b, tid 24567) 请分析

 

崩溃分析:Signal 11 (SIGSEGV)

问题诊断

根据你提供的调试信息,我来分析崩溃原因:

关键观察

  1. snprintf正常执行:

    • 调用前:X0 = 0x7FF6FD4930(栈缓冲区)
    • 调用后:栈上确实写入了 "libdatajar.so" ✓
  2. dlopen参数正确:

    • X0 = 0x7FF6FD4930 → "libdatajar.so" ✓
    • X1 = 2 (RTLD_NOW) ✓
  3. 崩溃信号:

    • Signal #11 = SIGSEGV(段错误)
    • exc.code b = 11 = 访问无效内存

根本原因

dlopen("libdatajar.so", 2) 返回 NULL,但后续代码没有检查就直接使用了返回值!

C
v290 = dl_dlopen(v896, 2);  // 返回 NULL(0x0)

// 后续代码可能是:
if (v290) {
    // ... 正常处理
    memset(ptr, 0, 44);
    ptr[0] = 95;
    // ... 解密符号名
    v280 = dlsym(v290, s);  // v290=NULL,这里崩溃!
    v237 = v280 + 4;        // 或者这里崩溃
}

为什么dlopen失败?

  1. libdatajar.so不存在:APK中可能没有这个库
  2. 路径错误:只传文件名时,系统找不到库
  3. 架构不匹配:库是32位但运行在64位环境

验证脚本

Python
"""
验证dlopen失败并绕过
"""
import idc
import ida_dbg

# dlopen调用地址
DLOPEN_CALL = 0x71B39893D0

def on_dlopen_return():
    """dlopen返回后检查"""
    ret = idc.get_reg_value("X0")
    print(f"[*] dlopen returned: 0x{ret:X}")
    
    if ret == 0:
        print("[!] dlopen FAILED - libdatajar.so not found!")
        print("[*] This will cause crash when code tries to use NULL handle")
        
        # 获取调用后的下一条指令地址
        next_pc = idc.get_reg_value("PC")
        print(f"[*] Next instruction: 0x{next_pc:X}")

# 设置断点在dlopen返回后
ida_dbg.add_bpt(DLOPEN_CALL + 4, 0, idc.BPT_SOFT)
print(f"[+] Breakpoint set at 0x{DLOPEN_CALL + 4:X}")

解决方案

方案1:跳过整个libdatajar.so加载逻辑(推荐)

查看后续代码结构:

C
v290 = dl_dlopen(v896, 2);
if ( v290 )
{
    // 处理libdatajar.so
    // ...
}
else
{
    // 备用路径或abort
}

绕过方法:让代码走 else 分支或备用路径

Python
"""
绕过libdatajar.so加载
"""
import ida_bytes
import idc
import struct

def bypass_libdatajar():
    """
    在dlopen返回后,如果返回NULL,跳过后续处理
    或者直接patch让整个加载块被跳过
    """
    
    # 方法1: 在dlopen调用处设置断点,返回时修改控制流
    DLOPEN_RET_ADDR = 0x71B39893D4  # dlopen返回后的地址
    
    # 需要找到 if(v290) 的CBZ/CBNZ指令位置
    # 查看dlopen后的代码:
    # 0x71B39893D4: 检查X0是否为0
    # 如果是CBZ X0, skip_addr 就跳过
    # 如果是CBNZ X0, process_addr 就处理
    
    print("[*] Analyzing code after dlopen...")
    
    ea = DLOPEN_RET_ADDR
    for i in range(20):  # 检查接下来20条指令
        mnem = idc.print_insn_mnem(ea)
        disasm = idc.generate_disasm_line(ea, 0)
        print(f"  0x{ea:X}: {disasm}")
        
        if mnem in ["CBZ", "CBNZ"]:
            op = idc.print_operand(ea, 0)
            if "X0" in op or "W0" in op:
                print(f"[!] Found NULL check at 0x{ea:X}")
                break
        ea = idc.next_head(ea)

bypass_libdatajar()

方案2:强制跳过加载块

根据反编译代码,找到整个 if(v290) 块的结束位置:

Python
"""
直接patch跳过libdatajar加载
"""
import ida_bytes
import struct

def patch_skip_libdatajar():
    """
    修改dlopen调用,让其直接返回0并跳到else分支
    """
    
    # dlopen调用位置
    DLOPEN_CALL = 0x71B39893D0
    
    # 方法:将BL dlopen改为 MOV X0, #0 (让返回值为0)
    # 这样后续的if(v290)检查会失败,走else分支
    
    # MOV X0, #0 的机器码: 0xD2800000
    mov_x0_0 = struct.pack("<I", 0xD2800000)
    
    # 保存原始指令
    original = ida_bytes.get_bytes(DLOPEN_CALL, 4)
    print(f"[*] Original instruction at 0x{DLOPEN_CALL:X}: {original.hex()}")
    
    # Patch
    ida_bytes.patch_bytes(DLOPEN_CALL, mov_x0_0)
    print(f"[+] Patched to: MOV X0, #0")
    print("[*] dlopen will be skipped, code will take else branch")

patch_skip_libdatajar()

方案3:运行时动态绕过

Python
"""
运行时绕过 - 在dlopen返回后修改寄存器
"""
import idc
import ida_dbg

class DlopenBypass(ida_dbg.DBG_Hooks):
    def __init__(self):
        ida_dbg.DBG_Hooks.__init__(self)
        self.dlopen_ret = 0x71B39893D4  # dlopen返回后地址
        
    def dbg_bpt(self, tid, ea):
        if ea == self.dlopen_ret:
            x0 = idc.get_reg_value("X0")
            print(f"[*] dlopen returned: 0x{x0:X}")
            
            if x0 == 0:
                print("[!] dlopen failed, applying bypass...")
                
                # 选项1: 跳过整个if块
                # 需要找到else分支或if块结束的地址
                # 假设找到了跳转目标 SKIP_ADDR
                SKIP_ADDR = 0x71B398XXXX  # 需要分析确定
                
                # 修改PC跳过处理代码
                # idc.set_reg_value(SKIP_ADDR, "PC")
                
                # 选项2: 让程序继续,但在后续dlsym处再拦截
                pass
                
            return 0
        return 0

# 安装hook
hook = DlopenBypass()
hook.hook()

# 设置断点
ida_dbg.add_bpt(0x71B39893D4, 0, idc.BPT_SOFT)
print("[+] Bypass hook installed")

方案4:分析完整控制流并patch

让我帮你分析dlopen后的代码流程:

Python
"""
分析dlopen后的控制流
"""
import idc
import idautils

def analyze_dlopen_aftermath():
    """分析dlopen返回后的代码结构"""
    
    DLOPEN_RET = 0x71B39893D4
    
    print("=" * 60)
    print("Analyzing code flow after dlopen...")
    print("=" * 60)
    
    ea = DLOPEN_RET
    indent = 0
    branch_targets = []
    
    for i in range(100):
        mnem = idc.print_insn_mnem(ea)
        disasm = idc.generate_disasm_line(ea, 0)
        
        # 标记重要指令
        marker = "  "
        if mnem in ["CBZ", "CBNZ", "TBZ", "TBNZ"]:
            marker = "→ "
            target = idc.get_operand_value(ea, 1)
            branch_targets.append((ea, target, mnem))
        elif mnem.startswith("B."):
            marker = "→ "
            target = idc.get_operand_value(ea, 0)
            branch_targets.append((ea, target, mnem))
        elif mnem == "BL":
            marker = "⊙ "
            # 检查是否是dlsym调用
            target = idc.get_operand_value(ea, 0)
            target_name = idc.get_func_name(target)
            if target_name and "dlsym" in target_name.lower():
                print(f"\n[!] CRITICAL: dlsym call at 0x{ea:X}")
                print(f"    If dlopen returned NULL, this will crash!\n")
        elif mnem == "RET":
            marker = "← "
        elif mnem in ["STP", "LDP"] and "X29" in disasm:
            marker = "▪ "
            
        print(f"{marker}0x{ea:X}: {disasm}")
        
        if mnem == "RET":
            break
            
        ea = idc.next_head(ea)
    
    print("\n" + "=" * 60)
    print("Branch instructions found:")
    print("=" * 60)
    for src, dst, mnem in branch_targets:
        print(f"  0x{src:X} [{mnem}] → 0x{dst:X}")
        
    return branch_targets

branches = analyze_dlopen_aftermath()

最终解决方案脚本

Python
"""
完整的libdatajar.so崩溃绕过脚本
"""
import ida_bytes
import ida_dbg
import idc
import struct

class LibDataJarCrashFix:
    def __init__(self):
        # 关键地址 - 需要根据实际运行时地址调整
        self.base = 0x71B3980000  # SO加载基址
        self.dlopen_call = 0x71B39893D0
        self.dlopen_ret = 0x71B39893D4
        
    def log(self, msg):
        print(f"[LibDataJarFix] {msg}")
        
    def method1_patch_dlopen_to_null(self):
        """
        方法1: 将dlopen调用改为直接返回0
        让程序走"加载失败"的分支
        """
        # BL _dl_dlopen → MOV X0, #0
        mov_x0_0 = struct.pack("<I", 0xD2800000)
        
        original = ida_bytes.get_bytes(self.dlopen_call, 4)
        self.log(f"Original at 0x{self.dlopen_call:X}: {original.hex()}")
        
        ida_bytes.patch_bytes(self.dlopen_call, mov_x0_0)
        self.log(f"Patched to MOV X0, #0")
        
    def method2_nop_entire_block(self):
        """
        方法2: NOP掉整个libdatajar处理块
        需要先分析块的范围
        """
        # 假设块从 dlopen_call 到某个位置
        # 这需要具体分析
        pass
        
    def method3_runtime_bypass(self):
        """
        方法3: 运行时断点绕过
        """
        class Hook(ida_dbg.DBG_Hooks):
            def __init__(self, parent):
                ida_dbg.DBG_Hooks.__init__(self)
                self.parent = parent
                
            def dbg_bpt(self, tid, ea):
                if ea == self.parent.dlopen_ret:
                    x0 = idc.get_reg_value("X0")
                    self.parent.log(f"dlopen returned: 0x{x0:X}")
                    
                    if x0 == 0:
                        self.parent.log("dlopen FAILED!")
                        self.parent.log("Need to skip subsequent code...")
                        
                        # 这里需要分析后续代码确定跳转目标
                        # 临时方案:单步执行观察
                        
                    ida_dbg.continue_process()
                    return 0
                return 0
                
        hook = Hook(self)
        hook.hook()
        ida_dbg.add_bpt(self.dlopen_ret, 0, idc.BPT_SOFT)
        self.log("Runtime bypass hook installed")
        
    def analyze_and_fix(self):
        """分析并自动修复"""
        self.log("=" * 50)
        self.log("LibDataJar Crash Fix")
        self.log("=" * 50)
        
        # 首先尝试最简单的方法
        self.log("Applying Method 1: Patch dlopen to return 0")
        self.method1_patch_dlopen_to_null()
        
        self.log("=" * 50)
        self.log("Fix applied! The code will now skip libdatajar.so loading")
        self.log("=" * 50)

# 执行修复
fix = LibDataJarCrashFix()
fix.analyze_and_fix()

使用说明

  1. 首先运行分析脚本:确定dlopen后的控制流
  2. 选择绕过方法:
    • 方法1(推荐):直接patch dlopen调用
    • 方法3:如果需要动态分析,使用运行时绕过
  3. 继续调试:绕过后程序应该会走备用路径或正常退出该函数

执行后,dlopen 不会被调用,X0 直接为0,后续的 if(v290) 检查会失败,跳过整个 libdatajar.so 处理逻辑。

 

于是我看了lib目录下是不是有libdatajar,但是什么文件都没有,其他未加固的就会有文件,我将文件考入后,再次调试发现出现了same提示,需要在so中你提前下端。

 

 

"""
Android加固反调试绕过脚本
适用于IDA Pro 7.x/8.x
针对secneo/bangcle类型加固
"""

import ida_dbg
import ida_bytes
import ida_funcs
import ida_name
import ida_kernwin
import ida_idd
import idautils
import idc
import struct

class AntiAntiDebug:
    def __init__(self):
        self.hooks = []
        self.breakpoints = {}
        self.original_bytes = {}
        
        # 关键函数地址(需要根据实际分析调整)
        self.target_functions = {
            'fork': None,
            'ptrace': None,
            'fopen': None,
            'pthread_create': None,
            'getpid': None,
            'prctl': None,
            'waitpid': None,
            'kill': None,
        }
        
        # 反调试函数地址
        self.anti_debug_funcs = []
        
    def log(self, msg):
        print(f"[AntiDebug] {msg}")
        
    # ==================== 1. ptrace绕过 ====================
    def bypass_ptrace(self):
        """
        绕过ptrace检测
        方法:hook ptrace返回0
        """
        ptrace_addr = self.find_plt_function("ptrace")
        if not ptrace_addr:
            self.log("ptrace not found, trying alternative...")
            ptrace_addr = self.find_function_by_name("ptrace")
            
        if ptrace_addr:
            self.log(f"Found ptrace at 0x{ptrace_addr:X}")
            
            # 在ptrace入口设置断点
            ida_dbg.add_bpt(ptrace_addr, 0, idc.BPT_SOFT)
            self.breakpoints['ptrace'] = ptrace_addr
            
            # 注册断点回调
            self.hooks.append(('ptrace', self.ptrace_handler))
            self.log("ptrace bypass installed")
            return True
        return False
    
    def ptrace_handler(self, ea):
        """ptrace断点处理:直接返回0"""
        # 读取第一个参数(request)
        # ARM64: X0 = request
        request = idc.get_reg_value("X0")
        
        self.log(f"ptrace called with request={request}")
        
        # PTRACE_ATTACH = 16, PTRACE_TRACEME = 0
        if request in [0, 16, 17, 24]:  # TRACEME, ATTACH, DETACH, CONT
            # 修改返回值为0(成功)
            # 跳过函数执行,直接返回
            lr = idc.get_reg_value("LR")
            idc.set_reg_value(0, "X0")  # 返回值 = 0
            idc.set_reg_value(lr, "PC")  # 跳转到返回地址
            self.log(f"ptrace bypassed, returning 0")
            return True
        return False
    
    # ==================== 2. fork绕过 ====================
    def bypass_fork(self):
        """
        绕过fork反调试
        方法:让fork返回0(模拟在子进程中),但不执行子进程代码
        或者:让fork返回-1(失败)
        """
        fork_addr = self.find_plt_function("fork")
        if not fork_addr:
            fork_addr = self.find_function_by_name("fork")
            
        if fork_addr:
            self.log(f"Found fork at 0x{fork_addr:X}")
            ida_dbg.add_bpt(fork_addr, 0, idc.BPT_SOFT)
            self.breakpoints['fork'] = fork_addr
            self.hooks.append(('fork', self.fork_handler))
            self.log("fork bypass installed")
            return True
        return False
    
    def fork_handler(self, ea):
        """fork断点处理:返回一个大的PID(模拟父进程)"""
        lr = idc.get_reg_value("LR")
        
        # 返回一个假的子进程PID,让程序认为自己是父进程
        # 这样会跳过子进程的ptrace代码
        idc.set_reg_value(99999, "X0")  # 返回假PID
        idc.set_reg_value(lr, "PC")
        
        self.log("fork bypassed, returning fake child PID")
        return True
    
    # ==================== 3. /proc文件检测绕过 ====================
    def bypass_proc_detection(self):
        """
        绕过/proc/self/maps和/proc/self/status检测
        方法:hook fopen,对敏感路径返回NULL或修改内容
        """
        fopen_addr = self.find_plt_function("fopen")
        if not fopen_addr:
            fopen_addr = self.find_function_by_name("fopen")
            
        if fopen_addr:
            self.log(f"Found fopen at 0x{fopen_addr:X}")
            ida_dbg.add_bpt(fopen_addr, 0, idc.BPT_SOFT)
            self.breakpoints['fopen'] = fopen_addr
            self.hooks.append(('fopen', self.fopen_handler))
            self.log("fopen bypass installed")
            return True
        return False
    
    def fopen_handler(self, ea):
        """fopen断点处理"""
        # X0 = filename, X1 = mode
        filename_ptr = idc.get_reg_value("X0")
        
        try:
            filename = idc.get_strlit_contents(filename_ptr, -1, idc.STRTYPE_C)
            if filename:
                filename = filename.decode('utf-8') if isinstance(filename, bytes) else filename
                
                sensitive_paths = [
                    "/proc/self/status",
                    "/proc/self/maps", 
                    "/proc/self/cmdline",
                    "/proc/self/wchan",
                    "/proc/self/task",
                ]
                
                for path in sensitive_paths:
                    if path in filename:
                        self.log(f"Blocking fopen({filename})")
                        # 返回NULL
                        lr = idc.get_reg_value("LR")
                        idc.set_reg_value(0, "X0")
                        idc.set_reg_value(lr, "PC")
                        return True
        except:
            pass
        return False
    
    # ==================== 4. 直接patch关键函数 ====================
    def patch_anti_debug_function(self, func_addr):
        """
        直接patch反调试函数,让其直接返回
        ARM64: RET = 0xD65F03C0
        """
        if func_addr:
            # 保存原始字节
            original = ida_bytes.get_bytes(func_addr, 4)
            self.original_bytes[func_addr] = original
            
            # 写入RET指令
            ret_instruction = struct.pack("<I", 0xD65F03C0)
            ida_bytes.patch_bytes(func_addr, ret_instruction)
            
            self.log(f"Patched function at 0x{func_addr:X}")
            return True
        return False
    
    def patch_fork_check(self):
        """
        查找并patch fork后的子进程检查
        通常是: if (fork() == 0) { ... ptrace ... }
        """
        # 搜索CBZ X0, xxx 或 CMP X0, #0; B.EQ xxx 模式
        self.log("Searching for fork result checks...")
        
        # 在sub_31DA4函数范围内搜索
        func_start = 0x31DA4  # 需要根据实际地址调整
        func_end = func_start + 0x10000
        
        ea = func_start
        while ea < func_end:
            insn = idc.print_insn_mnem(ea)
            if insn == "CBZ" or insn == "CBNZ":
                operand = idc.print_operand(ea, 0)
                if "X0" in operand or "W0" in operand:
                    self.log(f"Found fork check at 0x{ea:X}: {idc.generate_disasm_line(ea, 0)}")
                    # 可以选择NOP掉或修改跳转
            ea = idc.next_head(ea)
    
    # ==================== 5. pthread_create hook检测绕过 ====================
    def bypass_hook_detection(self):
        """
        绕过sub_4C820函数(检测libc函数是否被hook)
        直接patch让其返回0
        """
        # sub_4C820的地址
        check_func = 0x4C820  # 需要根据实际调整
        
        if idc.get_func_attr(check_func, idc.FUNCATTR_START) != idc.BADADDR:
            self.patch_anti_debug_function(check_func)
            self.log("Hook detection bypassed")
            return True
        return False
    
    # ==================== 6. prctl绕过 ====================
    def bypass_prctl(self):
        """
        绕过prctl(PR_SET_DUMPABLE, 1)
        这会影响调试能力
        """
        prctl_addr = self.find_plt_function("prctl")
        if prctl_addr:
            self.log(f"Found prctl at 0x{prctl_addr:X}")
            ida_dbg.add_bpt(prctl_addr, 0, idc.BPT_SOFT)
            self.breakpoints['prctl'] = prctl_addr
            self.hooks.append(('prctl', self.prctl_handler))
            return True
        return False
    
    def prctl_handler(self, ea):
        """prctl断点处理"""
        option = idc.get_reg_value("X0")
        
        # PR_SET_DUMPABLE = 4
        if option == 4:
            self.log("Bypassing prctl(PR_SET_DUMPABLE)")
            lr = idc.get_reg_value("LR")
            idc.set_reg_value(0, "X0")
            idc.set_reg_value(lr, "PC")
            return True
        return False
    
    # ==================== 辅助函数 ====================
    def find_plt_function(self, name):
        """在PLT段查找函数"""
        for seg in idautils.Segments():
            seg_name = idc.get_segm_name(seg)
            if ".plt" in seg_name:
                for ea in idautils.Functions(seg, idc.get_segm_end(seg)):
                    func_name = idc.get_func_name(ea)
                    if name in func_name:
                        return ea
        return None
    
    def find_function_by_name(self, name):
        """按名称查找函数"""
        ea = ida_name.get_name_ea(idc.BADADDR, name)
        if ea != idc.BADADDR:
            return ea
            
        # 尝试带前缀
        for prefix in [".", "_", "__", "j_"]:
            ea = ida_name.get_name_ea(idc.BADADDR, prefix + name)
            if ea != idc.BADADDR:
                return ea
        return None
    
    def find_references_to_string(self, target_str):
        """查找字符串引用"""
        results = []
        for seg in idautils.Segments():
            seg_start = seg
            seg_end = idc.get_segm_end(seg)
            
            ea = seg_start
            while ea < seg_end:
                s = idc.get_strlit_contents(ea, -1, idc.STRTYPE_C)
                if s and target_str.encode() in s:
                    # 查找引用
                    for xref in idautils.XrefsTo(ea):
                        results.append(xref.frm)
                ea = idc.next_head(ea)
        return results
    
    # ==================== 主安装函数 ====================
    def install_all_bypasses(self):
        """安装所有绕过"""
        self.log("=" * 50)
        self.log("Installing anti-anti-debug bypasses...")
        self.log("=" * 50)
        
        # 1. ptrace绕过(最重要)
        self.bypass_ptrace()
        
        # 2. fork绕过
        self.bypass_fork()
        
        # 3. /proc检测绕过
        self.bypass_proc_detection()
        
        # 4. prctl绕过
        self.bypass_prctl()
        
        # 5. hook检测绕过
        self.bypass_hook_detection()
        
        self.log("=" * 50)
        self.log("All bypasses installed!")
        self.log(f"Active breakpoints: {list(self.breakpoints.keys())}")
        self.log("=" * 50)
        
    def restore_all(self):
        """恢复所有修改"""
        # 恢复patched字节
        for addr, original in self.original_bytes.items():
            ida_bytes.patch_bytes(addr, original)
            self.log(f"Restored bytes at 0x{addr:X}")
            
        # 删除断点
        for name, addr in self.breakpoints.items():
            ida_dbg.del_bpt(addr)
            self.log(f"Removed breakpoint: {name}")
            
        self.log("All modifications restored")


# ==================== 调试事件处理器 ====================
class DebugHook(ida_dbg.DBG_Hooks):
    def __init__(self, anti_debug):
        ida_dbg.DBG_Hooks.__init__(self)
        self.anti = anti_debug
        
    def dbg_bpt(self, tid, ea):
        """断点命中处理"""
        for name, handler in self.anti.hooks:
            if ea == self.anti.breakpoints.get(name):
                if handler(ea):
                    # 继续执行
                    ida_dbg.continue_process()
                    return 0
        return 0
    
    def dbg_process_attach(self, pid, tid, ea, name, base, size):
        """进程附加时安装所有绕过"""
        self.anti.log(f"Attached to process {pid}")
        self.anti.install_all_bypasses()
        return 0


# ==================== 高级绕过:直接patch二进制 ====================
class BinaryPatcher:
    """直接修改二进制的反调试代码"""
    
    def __init__(self):
        self.patches = []
        
    def log(self, msg):
        print(f"[Patcher] {msg}")
        
    def nop_range(self, start, end):
        """NOP掉一段代码"""
        # ARM64 NOP = 0xD503201F
        nop = struct.pack("<I", 0xD503201F)
        
        ea = start
        while ea < end:
            ida_bytes.patch_bytes(ea, nop)
            ea += 4
            
        self.patches.append((start, end))
        self.log(f"NOPed range 0x{start:X} - 0x{end:X}")
        
    def patch_conditional_branch(self, ea, always_take=True):
        """
        修改条件跳转为无条件跳转或NOP
        """
        insn = idc.print_insn_mnem(ea)
        
        if insn.startswith("B."):
            # 条件跳转,获取目标地址
            target = idc.get_operand_value(ea, 0)
            
            if always_take:
                # 改为无条件跳转 B target
                offset = (target - ea) >> 2
                b_insn = 0x14000000 | (offset & 0x3FFFFFF)
                ida_bytes.patch_bytes(ea, struct.pack("<I", b_insn))
                self.log(f"Changed conditional branch at 0x{ea:X} to unconditional")
            else:
                # NOP掉
                ida_bytes.patch_bytes(ea, struct.pack("<I", 0xD503201F))
                self.log(f"NOPed conditional branch at 0x{ea:X}")
                
    def find_and_patch_fork_ptrace_pattern(self):
        """
        查找fork + ptrace模式并patch
        
        典型模式:
        BL fork
        CBZ X0, child_branch  ; if (fork() == 0)
        ...
        child_branch:
        BL ptrace             ; ptrace(PTRACE_ATTACH, ...)
        """
        self.log("Searching for fork+ptrace anti-debug pattern...")
        
        func_addr = 0x31DA4  # sub_31DA4的地址
        func = ida_funcs.get_func(func_addr)
        
        if not func:
            self.log("Function not found!")
            return
            
        ea = func.start_ea
        while ea < func.end_ea:
            mnem = idc.print_insn_mnem(ea)
            
            # 查找BL fork
            if mnem == "BL":
                target_name = idc.get_func_name(idc.get_operand_value(ea, 0))
                if target_name and "fork" in target_name:
                    self.log(f"Found fork call at 0x{ea:X}")
                    
                    # 查找后续的CBZ/CBNZ X0
                    next_ea = idc.next_head(ea)
                    for _ in range(5):  # 检查接下来的5条指令
                        next_mnem = idc.print_insn_mnem(next_ea)
                        if next_mnem in ["CBZ", "CBNZ"]:
                            op = idc.print_operand(next_ea, 0)
                            if "X0" in op or "W0" in op:
                                self.log(f"Found fork result check at 0x{next_ea:X}")
                                
                                # 方案1: NOP掉CBZ使其不跳转(留在父进程路径)
                                if next_mnem == "CBZ":
                                    self.nop_range(next_ea, next_ea + 4)
                                    self.log("Patched: child process code will be skipped")
                                    break
                        next_ea = idc.next_head(next_ea)
                        
            ea = idc.next_head(ea)
    
    def patch_sub_42F88(self):
        """Patch sub_42F88 反调试函数"""
        func_addr = 0x42F88
        if idc.get_func_attr(func_addr, idc.FUNCATTR_START) != idc.BADADDR:
            # 直接让函数返回
            ret = struct.pack("<I", 0xD65F03C0)
            ida_bytes.patch_bytes(func_addr, ret)
            self.log(f"Patched sub_42F88 at 0x{func_addr:X}")
            
    def patch_sub_4C820(self):
        """Patch hook检测函数"""
        func_addr = 0x4C820
        if idc.get_func_attr(func_addr, idc.FUNCATTR_START) != idc.BADADDR:
            # MOV X0, #0; RET
            patch = struct.pack("<II", 0xD2800000, 0xD65F03C0)
            ida_bytes.patch_bytes(func_addr, patch)
            self.log(f"Patched sub_4C820 at 0x{func_addr:X}")


# ==================== 使用方法 ====================
def main():
    print("=" * 60)
    print("Anti-Anti-Debug Script for Android Packer")
    print("=" * 60)
    
    # 创建实例
    anti = AntiAntiDebug()
    patcher = BinaryPatcher()
    
    # 选择操作
    choice = ida_kernwin.ask_buttons(
        "Hook方式", "Patch方式", "取消",
        0,
        "选择绕过方式:\n\n"
        "Hook方式: 使用断点动态绕过(推荐调试时使用)\n"
        "Patch方式: 直接修改二进制(永久修改)"
    )
    
    if choice == 1:  # Hook方式
        # 安装调试钩子
        hook = DebugHook(anti)
        hook.hook()
        
        # 如果已经在调试,立即安装绕过
        if ida_dbg.is_debugger_on():
            anti.install_all_bypasses()
        else:
            print("Hooks installed. Start debugging to activate bypasses.")
            
    elif choice == 0:  # Patch方式
        patcher.find_and_patch_fork_ptrace_pattern()
        patcher.patch_sub_42F88()
        patcher.patch_sub_4C820()
        print("Binary patched. Save the database to keep changes.")
        
    else:
        print("Cancelled.")


if __name__ == "__main__":
    main()

 

 

 执行hook脚本后

2839位置处下断点,运行到这里 v290 = dl_dlopen(v896, 2);

右键下一条指令运行到这里

f6

直到3451处,base.apk:0000007ABF9298A4 BLR             X8下u但

 

 

启动ida这里不说了

按下三个f9后,发现模块绝对地址,加上int函数的相对地址计算出绝对地址,

按下g,输入绝对地址,按下p,右键下断点,运行到光标处

在最后结束的跳转指令前一个add的地方下端运行到光标处

f7一直按下直到跳出函数,进入linker区域

执行脚本定位到jnionload函数,双击地址进入,

在开头下断运行到光标处,

ctrl+f搜索DA4函数双击进入调用处,这里要在调用DA4函数执行三次右键运行到光标才能能真正到达

image

 f7进入,这里要等好久,函数很大

执行绕过脚本

运行到

2839行 dlopen位置发现运行奔溃,将文件libdatajar.so 放入后又不奔溃了 v290 = dl_dlopen(v896, 2);,下断运行到光标处,这里执行三次就回到了这里

在下一行执行两次右键运行到光标,同时禁用两个进程

image

 

2849处奔溃,我们右键运行到光标在2849,继续f7进入

image

 


3451奔溃 这里一定要f7进入 (3130奔溃 ZN3art8CheckJNI19GetMethodIDInte: 3160奔溃)

 

image

 接下来一直f6直到3450行停下,对应的报错位置sub_5A4B4_jni_invoke_dexfile_loadDex,我们f7步入4B4函数,在一步一步f6

image

 

 报错位置

image

 

 

 

 

 

image

 右键运行到报错位置,报错位置

image

 

 先找到I I的绝对地址 然后在入口处下段,执行到最后一条语句的附近有个add. D, 然后再次下段运行到光标所在处,然后按F7 单步到宁可段中,在执行脚本查找GNIOUNT,找到地址之后,双击G,ILO, 进入地址之后,再次在入口下端执行在光标所在处,然后按F5 版调试反编译成源码,找到D,A0, D4 函数,右键下段运行在光标所在处,然后F7 进入该函数进入后执行脚本进行去反混淆,再次运行到D,L, open下段运行在光标所在处,执行两次在下一个语句中右键执行 在光标所在处运行两次单步调试 一直到下一个崩溃点



计算init函数地址,在开始和结束的add位置下端运行,
f6运行到loader区域
执行jni_onload脚本计算地址,双击进入
搜索DA4运行三次,f7进入
搜索dlopen下端运行三次,
暂停两个可疑线程
继续运行即可


大约运行到这里出现了dex,

image

执行dex导出脚本

import idaapi
import idc
import struct
import os


def read_mem(addr, size):
    """Read memory in chunks."""
    data = b''
    chunk_size = 0x10000
    offset = 0
    while offset < size:
        read_size = min(chunk_size, size - offset)
        chunk = idaapi.dbg_read_memory(addr + offset, read_size)
        if chunk is None:
            print(f"  [ERROR] Read failed at 0x{addr+offset:X}")
            return None
        data += chunk
        offset += read_size
    return data


def read_u8(addr):
    d = idaapi.dbg_read_memory(addr, 1)
    return d[0] if d else None

def read_u32(addr):
    d = idaapi.dbg_read_memory(addr, 4)
    return struct.unpack('<I', d)[0] if d and len(d)==4 else None

def read_u64(addr):
    d = idaapi.dbg_read_memory(addr, 8)
    return struct.unpack('<Q', d)[0] if d and len(d)==8 else None


def print_dex_header(data, prefix=""):
    """Print DEX header details."""
    if len(data) < 0x70:
        print(f"{prefix}[!] Data too short for DEX header")
        return
    magic = data[:8]
    checksum = struct.unpack('<I', data[8:12])[0]
    sig = data[12:32].hex()
    file_size = struct.unpack('<I', data[0x20:0x24])[0]
    hdr_size = struct.unpack('<I', data[0x24:0x28])[0]
    endian = struct.unpack('<I', data[0x28:0x2C])[0]
    str_ids_sz = struct.unpack('<I', data[0x38:0x3C])[0]
    str_ids_off = struct.unpack('<I', data[0x3C:0x40])[0]
    type_ids_sz = struct.unpack('<I', data[0x40:0x44])[0]
    method_ids_sz = struct.unpack('<I', data[0x48:0x4C])[0]
    class_defs_sz = struct.unpack('<I', data[0x60:0x64])[0]
    data_size = struct.unpack('<I', data[0x68:0x6C])[0]
    data_off = struct.unpack('<I', data[0x6C:0x70])[0]

    print(f"{prefix}Magic:       {magic}")
    print(f"{prefix}Checksum:    0x{checksum:08X}")
    print(f"{prefix}Signature:   {sig}")
    print(f"{prefix}File Size:   {file_size} (0x{file_size:X})")
    print(f"{prefix}Header Size: {hdr_size}")
    print(f"{prefix}Endian Tag:  0x{endian:08X}")
    print(f"{prefix}String IDs:  {str_ids_sz} @ 0x{str_ids_off:X}")
    print(f"{prefix}Type IDs:    {type_ids_sz}")
    print(f"{prefix}Method IDs:  {method_ids_sz}")
    print(f"{prefix}Class Defs:  {class_defs_sz}")
    print(f"{prefix}Data Size:   {data_size} @ 0x{data_off:X}")


def save_file(data, filepath):
    """Save data, avoid overwriting."""
    path = filepath
    counter = 1
    while os.path.exists(path):
        name, ext = os.path.splitext(filepath)
        path = f"{name}_{counter}{ext}"
        counter += 1
    with open(path, 'wb') as f:
        f.write(data)
    return path


def dump_dex_now():
    """
    Dump decrypted DEX at current execution point.
    
    We're inside the LABEL_410 loop at v469 = v380.
    
    Register state shows:
      X22 = 0x7E5A14300C"dex\n037" (decrypted DEX!)
      
    From the code at LABEL_410:
      v472 = v467 + big_endian_offset  (DEX address)
      *(_QWORD *)(v381 + 8*v468) = v472  (stored in ptr array)
      v474 = bswap32(size)               (DEX size)
      *(_DWORD *)(v382 + 4*v468) = v474  (stored in size array)
      
    Where:
      v467 = v816 + 4096
      v381 = qword_7E64050F28 (pointer array)
      v382 = qword_7E64050F20 (size array)
      v380 = dword_7E64050F18 (total count)
    """

    output_dir = os.path.expanduser("~/Desktop")
    if not os.path.exists(output_dir):
        output_dir = "/tmp"

    print(f"[*] Output dir: {output_dir}")
    print()

    # ── Current register state ──
    x22 = idc.get_reg_value("X22")  # DEX pointer (v472)
    sp = idc.get_reg_value("SP")

    print(f"[*] X22 (current DEX ptr): 0x{x22:X}")

    # ── Verify X22 is DEX ──
    magic = idaapi.dbg_read_memory(x22, 8)
    if magic and magic[:3] == b'dex':
        print(f"[+] X22 confirmed as DEX: {magic}")
    else:
        print(f"[!] X22 magic: {magic.hex() if magic else 'unreadable'}")

    # ── Read globals for full picture ──
    print(f"\n{'─'*50}")
    print("[*] Reading global arrays...")

    # dword_7E64050F18 = number of DEX entries
    num_entries = read_u32(0x7E64050F18)
    print(f"  dword_7E64050F18 (count):     {num_entries}")

    # qword_7E64050F28 = pointer array
    ptr_array_addr = read_u64(0x7E64050F28)
    print(f"  qword_7E64050F28 (ptr array): "
          f"0x{ptr_array_addr:X}" if ptr_array_addr else "  NULL")

    # qword_7E64050F20 = size array
    size_array_addr = read_u64(0x7E64050F20)
    print(f"  qword_7E64050F20 (sz array):  "
          f"0x{size_array_addr:X}" if size_array_addr else "  NULL")

    # v816 from stack [SP+0x18]
    v816_data = idaapi.dbg_read_memory(sp + 0x18, 8)
    v816 = struct.unpack('<Q', v816_data)[0] if v816_data else 0
    print(f"  v816 [SP+0x18]:               0x{v816:X}")

    if v816:
        base_data = v816 + 4096
        print(f"  base_data (v816+0x1000):      0x{base_data:X}")

    if not num_entries or num_entries > 50:
        print(f"\n[!] Invalid entry count ({num_entries}), "
              f"trying to dump X22 directly")
        num_entries = 0

    dumped = 0

    # ═══════════════════════════════════════
    # Strategy 1: Dump from global arrays
    # ═══════════════════════════════════════
    if num_entries and ptr_array_addr and size_array_addr:
        print(f"\n{'═'*50}")
        print(f"[*] Dumping {num_entries} DEX entries from arrays")
        print(f"{'═'*50}")

        for i in range(num_entries):
            ptr = read_u64(ptr_array_addr + 8 * i)
            sz = read_u32(size_array_addr + 4 * i)

            print(f"\n[*] Entry {i}:")
            print(f"  Pointer: 0x{ptr:X}" if ptr else "  Pointer: NULL")
            print(f"  Size:    0x{sz:X} ({sz})" if sz else "  Size: 0")

            if not ptr:
                # Entry not yet populated (loop hasn't 
                # reached this index yet)
                print(f"  [*] Not yet populated, skipping")
                continue

            # Read and verify magic
            entry_magic = idaapi.dbg_read_memory(ptr, 8)
            if entry_magic:
                print(f"  Magic:   {entry_magic[:8]}")
                is_dex = entry_magic[:3] == b'dex'
                print(f"  Valid:   {'✓ YES' if is_dex else '✗ NO'}")
            else:
                print(f"  [ERROR] Cannot read memory")
                continue

            # Get actual size from DEX header
            actual_size = sz
            if is_dex:
                hdr = idaapi.dbg_read_memory(ptr, 0x70)
                if hdr:
                    hdr_file_size = struct.unpack('<I',
                                                  hdr[0x20:0x24])[0]
                    print(f"  Header file_size: {hdr_file_size} "
                          f"(0x{hdr_file_size:X})")

                    if 0 < hdr_file_size < 200*1024*1024:
                        if hdr_file_size != sz:
                            print(f"  [*] Using header size "
                                  f"instead of array size")
                        actual_size = hdr_file_size

            if actual_size == 0 or actual_size > 200*1024*1024:
                print(f"  [SKIP] Invalid size: {actual_size}")
                continue

            # Read DEX data
            print(f"  [*] Reading {actual_size} bytes...")
            dex_data = read_mem(ptr, actual_size)
            if dex_data is None:
                continue

            if len(dex_data) != actual_size:
                print(f"  [WARN] Read {len(dex_data)}, "
                      f"expected {actual_size}")

            # Save
            filename = f"classes{i}.dex" if is_dex else \
                       f"classes{i}.bin"
            path = save_file(dex_data,
                             os.path.join(output_dir, filename))
            print(f"  [+] SAVED: {path} ({len(dex_data)} bytes)")

            if is_dex:
                print_dex_header(dex_data, prefix="      ")

            dumped += 1

    # ═══════════════════════════════════════
    # Strategy 2: Dump X22 directly
    # ═══════════════════════════════════════
    if x22 and magic and magic[:3] == b'dex':
        # Check if we already dumped this address
        already_dumped = False
        if ptr_array_addr and num_entries:
            for i in range(num_entries):
                ptr = read_u64(ptr_array_addr + 8 * i)
                if ptr == x22:
                    already_dumped = True
                    break

        if not already_dumped:
            print(f"\n{'═'*50}")
            print(f"[*] Dumping X22 DEX directly (0x{x22:X})")
            print(f"{'═'*50}")

            hdr = idaapi.dbg_read_memory(x22, 0x70)
            if hdr:
                dex_size = struct.unpack('<I',
                                         hdr[0x20:0x24])[0]
                print(f"  File size: {dex_size} (0x{dex_size:X})")

                if 0 < dex_size < 200*1024*1024:
                    dex_data = read_mem(x22, dex_size)
                    if dex_data:
                        path = save_file(
                            dex_data,
                            os.path.join(output_dir,
                                         "classes_x22.dex"))
                        print(f"  [+] SAVED: {path} "
                              f"({len(dex_data)} bytes)")
                        print_dex_header(dex_data, "      ")
                        dumped += 1

    # ═══════════════════════════════════════
    # Strategy 3: Scan v816+4096 area for DEX
    # ═══════════════════════════════════════
    if v816 and dumped == 0:
        print(f"\n{'═'*50}")
        print(f"[*] Scanning v816 region for DEX files")
        print(f"{'═'*50}")

        # Scan from v816 forward
        scan_addr = v816
        scan_end = v816 + 50 * 1024 * 1024  # 50MB max

        offset = 0
        while scan_addr + offset < scan_end:
            chunk = idaapi.dbg_read_memory(
                scan_addr + offset, 0x10000)
            if chunk is None:
                break

            pos = 0
            while True:
                idx = chunk.find(b'dex\n', pos)
                if idx == -1:
                    break

                found_addr = scan_addr + offset + idx
                test_hdr = idaapi.dbg_read_memory(
                    found_addr, 0x70)
                if test_hdr:
                    fs = struct.unpack('<I',
                                      test_hdr[0x20:0x24])[0]
                    hs = struct.unpack('<I',
                                      test_hdr[0x24:0x28])[0]

                    if hs == 0x70 and 0x70 < fs < 200*1024*1024:
                        print(f"\n  [+] DEX at 0x{found_addr:X} "
                              f"size={fs}")

                        dex_data = read_mem(found_addr, fs)
                        if dex_data:
                            path = save_file(
                                dex_data,
                                os.path.join(
                                    output_dir,
                                    f"scan_classes_{dumped}.dex"))
                            print(f"  [+] SAVED: {path}")
                            print_dex_header(dex_data, "      ")
                            dumped += 1

                pos = idx + 4

            offset += 0x10000 - 8  # overlap

    # ═══════════════════════════════════════
    # Summary
    # ═══════════════════════════════════════
    print(f"\n{'═'*60}")
    if dumped > 0:
        print(f"[+] SUCCESS: Dumped {dumped} DEX file(s)")
        print(f"[+] Output:  {output_dir}")
    else:
        print("[!] No DEX files dumped")
        print("[*] The loop may not have finished yet")
        print("[*] Try continuing execution and breaking later")
        print("[*] Or break after the AES decryption at:")
        print(f"    0x7E63F86F00 (memcpy after decrypt)")
    print(f"{'═'*60}")

    return dumped > 0


# ═════════════════════════════════════════
print("=" * 60)
print("  SecNeo DEX Dumper v4")
print("  At LABEL_410 loop (v469 = v380)")
print("  X22 = DEX pointer")
print("=" * 60)
print()
dump_dex_now()

 


















 

 

 

 

 

代办:

动调

过反调试

代码追踪

自动下断点找打奔溃点

编写一个脚本用于在is o加载之后用于执行F九一直调试在APK段下,然后给入口所有函数进行下段下断点,然后自动运行。当运行到报错点之后,记录报错位置和报错函数

 

 

通过用ai分析每个函数并重命名,然后导出所有调用链分析

全部调用链逐条分析

由于调用链数量极大(634条),我将按功能分组逐条分析,标注每条是系统/运行时调用链还是用户业务调用链。


第一大组:C++ 名称反修饰(Demangle)链

这一组全部以 sub_B1A10___cxa_demangle 开头,共约80+条。

分类:系统运行时调用链

通俗解释:

这是C++编译器自带的"名称翻译器"。C++编译时会把函数名变成一串乱码(如 _ZN5Utils13getMethodInfoE),这组函数负责把乱码翻译回人能看懂的名字(如 Utils::getMethodInfo)。

逐条说明(按深度分组):


[12] cxa_demangle → cpp_abi_parser → cpp_translator → 解析CXX精化类型 → parse_nested_name → template_param_list_parse → parse_special_expression → mangled_to_readable → parseUnresolvedName → parse_type → parse_substitution → parseBackReference

系统运行时链。 翻译一个极其复杂的C++模板函数名:从最外层的demangle入口进入,先解析ABI名称,再翻译成可读文本,遇到嵌套名称空间就递归进去,处理模板参数列表,碰到特殊表达式再深入解析,遇到未解析的名称继续拆分,解析类型时发现是个替换引用(之前出现过的类型的缩写),最后回溯查找之前记录的类型。就像翻译一个有12层嵌套括号的数学公式。


[12] ...同上路径... → parse_type → parse_substitution → sub_2C840

系统运行时链。 跟上一条几乎一样的路径,但最终调用的是一个辅助内存操作函数(sub_2C840),用于在解析替换引用时分配或调整内部数据结构。


[12] ...同上路径... → parse_type → parse_decltype → make_function_call

系统运行时链。 同样的翻译路径,但这次遇到的类型是C++11的decltype(编译器自动推断类型),需要进一步解析它里面包含的函数调用表达式。


[11] ...同上路径... → parse_type → parseTemplateParam

系统运行时链。 翻译路径遇到的类型是一个模板参数(如TU),需要根据上下文查找这个参数代表什么具体类型。


[11] ...同上路径... → parse_type → sub_2C840

系统运行时链。 解析类型后进行内存辅助操作。


[11] ...同上路径... → parse_expression → parse_source_name

系统运行时链。 翻译路径进入表达式解析分支,遇到一个源码名称(就是程序员写的原始函数名/变量名),从编码的名称中提取出原始文本。


[11] ...同上路径... → parse_expression → sha1_core_compression_function

系统运行时链。 解析表达式时,内部走到了一个被IDA误命名为"sha1压缩函数"的代码段。实际上这很可能是demangle过程中的某个数值计算或查表操作,IDA的自动命名搞错了。


[11] ...同上路径... → parse_expression → parse_type

系统运行时链。 表达式中嵌套了类型信息,所以又递归回去解析类型。这种互相调用在C++复杂模板中非常常见。


[10] ...同上路径... → parseUnresolvedName → parse_source_name

系统运行时链。 在解析未解析名称时直接提取源码名称,比上面那几条少了一层递归。


[10] ...同上路径... → parseExprPrimary → parse_sizeof_pack_or_fold

系统运行时链。 遇到字面量编码(如数字常量),然后发现是sizeof...运算或fold表达式(C++17的参数包展开),需要特殊处理。


[10] ...同上路径... → parse_d_expression → parse_source_name

系统运行时链。 解析另一种表达式格式(d_expression代表依赖表达式),从中提取源码名称。


[9] 系列(约15条)

全部是系统运行时链。这些是上面12/11/10层路径的"较短版本",在到达第8-9层的不同分支时终止:

  • → parse_function_param — 解析函数参数
  • → parseTemplateParam — 解析模板参数
  • → sub_2C840 — 内存辅助操作
  • → cpp_itanium_abi_name_demangler — 另一个ABI名称反修饰器
  • → parser_pool_copy — 解析器内存池拷贝
  • → make_function_call — 构造函数调用表达式节点
  • → 解析器内存池分配分组括号AST节点 — 在语法树中创建括号分组节点
  • → make_ternary_expr — 构造三元运算符(?:)表达式
  • → parse_sizeof_pack_or_fold — 处理sizeof包
  • → sub_C454C带校验的格式化输出 — 输出翻译结果时带校验

[8] cxa_demangle → parser → translator → 解析CXX精化类型 → parse_nested_name → parseUnnamedTypeName → parse_source_name → append_string

系统运行时链。 翻译一个匿名类型的名称(lambda表达式或匿名结构体),提取其源码名,然后把翻译结果追加到输出缓冲区。


[8] ...parseUnnamedTypeName → sha1_core_compression → parse_source_name

系统运行时链。 同上,但中间经过了一个被误识别的函数。


[8] ...parseUnnamedTypeName → sha1_core_compression → 带校验格式化输出

系统运行时链。 最终输出翻译结果。


[8] ...template_param_list_parse → parse_special_expression → sub_2C840
[8] ...→ parseExprPrimary
[8] ...→ parser_pool_copy

系统运行时链。 模板参数列表解析过程中的内存操作、字面量解析、内存池操作。


[8] ...parse_nested_name → parse_decltype → mangled_to_readable → parse_special_expression

系统运行时链。 嵌套名称中包含decltype,需要翻译成可读名后再解析其中的特殊表达式。


[7] 系列demangle链

系统运行时链。 更短的路径变体:

  • → parseUnnamedTypeName → sub_2C840 — 匿名类型解析的内存操作
  • → parseUnnamedTypeName → parser_pool_copy — 内存池拷贝
  • → parseUnnamedTypeName → parseBackReference — 回溯引用查找
  • → parseUnnamedTypeName → 带校验格式化输出 — 输出结果
  • → template_param_list_parse → sub_2C840 / parser_pool_copy — 模板参数解析

[6] 系列demangle链

系统运行时链。

  • → parse_nested_name → sub_2C840 — 嵌套名称的内存操作
  • → parse_substitution — 解析替换缩写
  • → parseTemplateParam — 解析模板参数
  • → parseQualifiersAndRef — 解析const/volatile/&/&&限定符
  • → parseBackReference — 回溯引用

[5] cxa_demangle → parser → translator → function_type_parser → mangled_to_readable / sub_2C840 / parser_pool_copy

系统运行时链。 翻译函数类型签名(函数指针、成员函数指针等),路径更短。


[4] cxa_demangle → parser → translator → 各种终端

系统运行时链。 最短的翻译路径:

  • → vendor_extended_qualifier — 解析厂商自定义限定符
  • → parseTemplateParam — 直接解析模板参数
  • → sub_2C840 — 内存操作
  • → mangled_to_readable — 翻译成可读名
  • → parse_substitution — 替换缩写
  • → template_param_list_parse — 模板参数列表
  • → parse_decltype — decltype解析
  • → parse_special_expression → translator — 特殊表达式回调翻译器

[3] cxa_demangle → parser → parse_nested_name / sub_2C840 / huf_decompress_table / parser_pool_copy

系统运行时链。 最简单的入口分支。huf_decompress_table在这里出现是因为IDA可能对同一块代码的功能识别有误,或者demangle内部的查找表操作被误认为Huffman表。


[2] 系列demangle输出链(大量 → append_string)

系统运行时链。 约50+条,全部是把翻译好的名称片段追加到输出字符串:

  • demangle_print_qualified_name → append_string — 输出限定名(如std::vector
  • demangle_print_ctor_vtable → append_string — 输出构造虚表
  • demangle_print_destructor → append_string — 输出析构函数名
  • demangle_print_operator → append_string — 输出运算符重载名
  • demangle_print_nested_name → append_string — 输出嵌套名
  • demangle_template_specialization → append_string — 输出模板特化
  • demangle_function_call → append_string — 输出函数调用
  • demangle_print_delete_expr → append_string — 输出delete表达式
  • demangle_conditional_expression → append_string — 输出三元表达式
  • demangle_pointer_type → append_string — 输出指针类型
  • demangle_reference_qualifier → append_string — 输出引用限定
  • demangle_print_param_list → append_string — 输出参数列表
  • demangle_lambda_expression → append_string — 输出lambda表达式
  • demangle_print_std_namespace → append_string — 输出std::
  • demangle_print_cv_qualified_type → append_string — 输出const/volatile类型
  • demangle_enable_if → append_string — 输出SFINAE enable_if
  • demangle_print_throw → append_string — 输出throw规格
  • 等等...

每一条都是同一个模式:某个特定的C++语法元素翻译完后,把文本追加到结果缓冲区。


[2] 系列demangle互调链

系统运行时链。 demangle内部各解析器之间的互相调用:

  • parse_nested_name → parser — 嵌套名称需要回到主解析器
  • parse_special_expression → parser — 特殊表达式回主解析器
  • parseQualifiersAndRef → parse_nested_name — 限定符中包含嵌套名
  • parseUnnamedTypeName → translator — 匿名类型需要翻译
  • parseExprPrimary → parser / translator — 字面量需要回主解析/翻译
  • mangled_to_readable → translator — 可读名翻译器
  • parse_d_expression → mangled_to_readable — 依赖表达式翻译
  • demangle_parse_expression → template_param_list_parse — 表达式中有模板参数
  • sha1_core_compression → translator — 误识别函数回调翻译器
  • function_type_parser → translator — 函数类型翻译
  • vendor_extended_qualifier → translator — 厂商限定翻译

[2] demangle工具链

系统运行时链。

  • demangle_float_literal → yr_snprintf_checked_v3 / append_string — 把浮点常量格式化为字符串
  • 十六进制转double字符串 → yr_snprintf_checked_v3 / append_string — 把十六进制编码的浮点数转为文本
  • 十六进制转long_double字符串 → yr_snprintf_checked_v3 / append_string — 同上但是long double
  • JSON对象序列化为字符串 → append_string — 序列化JSON输出
  • 类型限定符序列化 → append_string — 序列化类型限定符
  • 数组范围初始化器序列化 → append_string — 序列化数组初始化

第二大组:DWARF 栈展开 / 异常处理链

以 _ZN5Utils13getMethodInfoE 或异常处理函数开头。

分类:系统运行时调用链(带部分用户桥接)

通俗解释:

当C++程序抛出异常或崩溃时,系统需要"倒带"——沿着函数调用栈一层一层往回找,看哪个函数能处理这个异常。DWARF是一种调试信息格式,告诉系统每个函数的栈帧长什么样。


[10] Utils::getMethodInfo → 带校验格式化输出 → init_unwind_context → DWARF帧描述解析 → findFDEForAddress → ac_automaton_compile → eh_frame_count_fdes → parse_cfi_return_type → dwarf_read_encoded_value → decode_uleb128_v2

系统运行时链(用户入口)。 用户代码调用Utils::getMethodInfo查找JNI方法信息,这个函数内部需要做栈展开(可能是为了获取调用者信息或处理异常)。栈展开时:先初始化上下文,然后解析DWARF帧描述条目(FDE),在eh_frame段中搜索对应地址的FDE,解析调用帧信息(CFI),读取DWARF编码的值,最终用ULEB128解码(一种可变长整数编码)。就像在一本加密的电话簿里查号码,每一层都是不同的编码方式。


[10] ...同上... → decode_sleb128_2

系统运行时链。 同上但最后用的是有符号的LEB128解码(SLEB128),处理负数偏移。


[9] ...→ parse_cfi_return_type → decode_uleb128_v2 / decode_sleb128_2

系统运行时链。 少了一层,直接从CFI返回类型解析到LEB128解码。


[8] ...→ eh_frame_count_fdes → get_register_value / dwarf_read_encoded_value / dwarf_get_encoding_size

系统运行时链。 在计数FDE条目时需要:

  • 读取寄存器值(确定当前栈帧位置)
  • 读取DWARF编码值(理解FDE中的数据)
  • 获取编码大小(知道每个字段占多少字节)

[8] ...→ ac_automaton_compile → heap_sort → heap_sift_down

系统运行时链。 在编译AC自动机(多模式匹配用)时对数据进行堆排序,堆排序中需要下沉操作。这里AC自动机被复用于FDE搜索的索引构建。


[8] ...→ 虚基类偏移调整thunk → parse_cfi_return_type

系统运行时链。 C++虚继承中,偏移调整后继续解析CFI返回类型。


[8] ...→ dwarf_find_fde_for_pc → get_register_value / parse_cfi_return_type / dwarf_read_encoded_value / dwarf_get_encoding_size

系统运行时链。 根据程序计数器(PC)值查找对应的FDE,过程中需要读取寄存器、解析CFI、读编码值、获取编码大小。


[8] ...→ dwarf_iterate_fde_entries → get_register_value / parse_cfi_return_type / dwarf_read_encoded_value / dwarf_get_encoding_size

系统运行时链。 遍历所有FDE条目来找到匹配的那个。


[8] ...→ bignum_compare → get_register_value / dwarf_read_encoded_value

系统运行时链。 在FDE搜索的二分查找中,比较地址时使用了大整数比较(可能因为地址是64位的)。


[8] ...→ bignum_compare_generic → 虚基类偏移调整 / get_register_value / dwarf_read_encoded_value

系统运行时链。 通用版的大整数比较。


[7] Utils::getMethodInfo → ... → ac_automaton_compile → get_register_value / dwarf_read_encoded_value / compare_uint64_values

系统运行时链。 AC自动机编译过程中的底层操作。


[6] 系列DWARF链

系统运行时链。

  • → findFDEForAddress → 虚基类偏移调整 — FDE搜索中的偏移调整
  • → findFDEForAddress → get_register_value / dwarf_read_encoded_value — 直接读取寄存器和编码值
  • → dwarf_get_base_address → 获取上下文偏移824字段 — 获取DWARF基地址
  • → read_encoded_pointer → decode_uleb128 / decode_sleb128 — 读取编码指针
  • → dwarf_line_number_program → dwarf_get_base_address / read_encoded_pointer / decode_uleb128 / decode_sleb128 — 执行DWARF行号程序状态机(用于调试信息定位)
  • → libunwind_apply_cfi_register_rules → bytecode_evaluator → decode_uleb128 / decode_sleb128 / vm_get_register / get_base_address / read_encoded_pointer — 应用CFI寄存器规则:用一个小虚拟机执行DWARF字节码来确定每个寄存器在栈展开后的值

[5] 系列DWARF链(更短路径)

系统运行时链。

  • → DWARF帧描述解析 → decode_uleb128 / decode_sleb128 — 直接解码
  • → libunwind_apply_cfi → vm_set_register / decode_uleb128 / vm_get_register — 设置/获取虚拟机寄存器
  • → exception_scan_loop → dwarf_set_cfa → apply_cfi / vm_get_register — 异常扫描循环中设置调用帧地址(CFA)

[4] 系列DWARF/异常链

系统运行时链。

  • → init_unwind → locale_ctype初始化表 / vm_set_register — 栈展开初始化
  • → exception_scan_loop → DWARF帧描述解析 / 获取上下文偏移784字段 — 异常扫描
  • → regex_match_loop → DWARF帧描述解析 / dwarf_set_cfa — 正则匹配循环中的栈展开
  • → unwind_context_copy_registers → vm_set_register / vm_get_register — 复制栈展开上下文的寄存器

[3] cxa_throw → regex_find_all_matches → init_unwind / DWARF帧描述解析 / dwarf_set_cfa / 获取上下文偏移784字段 / exception_scan_loop / unwind_context_copy_registers

系统运行时链。 抛出C++异常时触发栈展开:__cxa_throw被调用,内部需要查找匹配的catch块,这涉及完整的DWARF栈展开流程。


[2] 系列栈展开链

系统运行时链。

  • dwarf_find_fde → findFDEForAddress — FDE查找入口
  • regex_compile_pattern → DWARF帧描述解析 — 正则编译中的栈展开
  • demangle_binary_operands → init_unwind / regex_match_loop / unwind_context_copy — 二元运算解析中的栈展开
  • setup_exception_handler → regex_find_all_matches / init_unwind / regex_match_loop / unwind_context_copy — 设置异常处理器
  • scan_exception_handlers → init_unwind / DWARF帧描述解析 / dwarf_set_cfa — 扫描异常处理器
  • dl_find_unwind_info → get_register_value_2 / dwarf_read_encoded_value / 虚基类偏移调整 / dwarf_get_encoding_size / dwarf_find_fde_for_pc — 从动态链接器查找栈展开信息

第三大组:JNI_OnLoad 初始化主链

以 JNI_OnLoad_0002F930 开头,这是SO库的入口点。

分类:用户业务调用链

通俗解释:

当Android加载这个SO库时,JNI_OnLoad是第一个被调用的函数,它负责初始化整个加固保护系统。


[7] JNI_OnLoad → initialization_function → file_level_transparent_encryption_engine → integrity_monitor_procmaps_code_section_rbtree_checker → debug_data_lookup_by_function_name → hook_write_memory_patch_with_permission_check → flush_icache

用户业务链。 启动时的代码完整性保护流程:加载→初始化→启动文件透明加密引擎→检查代码段完整性(读取/proc/maps获取内存映射,用红黑树管理)→按函数名查找需要保护的代码位置→如果发现被修改就用内存写入来修复(需要先改权限)→刷新指令缓存让修复生效。通俗说:检查自己的代码有没有被别人改过,如果被改了就改回来。


[6] JNI_OnLoad → init → file_encryption → check_file_paths → file_encrypt_decrypt → rc4_variant_decrypt_with_xor_fallback

用户业务链。 启动→初始化→透明加密引擎→检查文件路径是否在保护列表中→对文件进行加解密→使用RC4变种加XOR混合的加密算法。通俗说:对特定文件做加密保护,用的是RC4+XOR这种加密方式。


[6] JNI_OnLoad → init → eight_item_security_check → APK签名块V2V3解析 → sha256_hash_compute → MD5_Transform

用户业务链。 启动→初始化→八项安全检查→验证APK的V2/V3签名→用SHA256算法计算哈希→内部用到了MD5变换。通俗说:检查这个APP的安装包有没有被篡改,通过验证数字签名来判断。


[6] JNI_OnLoad → init → eight_item_security_check → APK签名V2V3解析 → sha256 → MD5最后阶段

用户业务链。 同上,但走到的是MD5计算的收尾阶段(padding和最终输出)。


[5] JNI_OnLoad → init → file_encryption → fd_hashtable_register_entry → inotify_hash_table_lookup_by_fd_wd

用户业务链。 透明加密引擎注册一个文件描述符到哈希表→查找该fd/wd对应的加密配置。通俗说:记住哪些打开的文件需要加密,用哈希表快速查找。


[5] JNI_OnLoad → init → file_encryption → scan_directory_recursive → is_class_in_target_list

用户业务链。 递归扫描目录→判断找到的文件是否在需要保护的目标列表中。通俗说:翻遍所有文件夹,找出哪些文件需要保护。


[5] JNI_OnLoad → init → file_encryption → scan_directory_recursive → file_encrypt_decrypt

用户业务链。 递归扫描目录→对找到的目标文件执行加解密。通俗说:扫到需要保护的文件就加密它。


[5] JNI_OnLoad → init → file_encryption → recursive_dir_scan_and_process → is_class_in_target_list / file_encrypt_decrypt

用户业务链。 另一个递归目录扫描函数,功能同上。可能是不同路径或不同时机的扫描。


[5] JNI_OnLoad → init → file_encryption → integrity_monitor → validate_elf_aarch64

用户业务链。 完整性监控时验证ELF文件是否为合法的AArch64格式。通俗说:检查内存中的SO库文件格式是否正常,防止被恶意替换。


[5] JNI_OnLoad → init → file_encryption → integrity_monitor → rbtree_insert_string_key

用户业务链。 把内存映射信息插入红黑树。通俗说:用红黑树(一种快速查找的数据结构)管理所有需要监控的内存区域。


[5] JNI_OnLoad → init → file_encryption → integrity_monitor → rbtree_delete_node_with_fixup

用户业务链。 从红黑树中删除不需要的节点。通俗说:清理过期的监控记录。


[5] JNI_OnLoad → init → file_encryption → cleanup_ac_scanner → rbtree_delete_node_with_fixup

用户业务链。 清理YARA AC自动机扫描器时,释放红黑树资源。通俗说:扫描引擎用完了,清理它的数据结构。


[4] JNI_OnLoad → init → MD5最后阶段 → MD5_Transform

用户业务链。 初始化过程中计算MD5哈希的核心变换。通俗说:算指纹用的MD5。


[4] JNI_OnLoad → init → generate_or_load_device_AID_MD5_fingerprint → MD5_Transform / MD5最后阶段

用户业务链。 生成或加载设备的Android ID的MD5指纹。通俗说:给这台手机算一个唯一的身份证号。


[4] JNI_OnLoad → init → file_encryption → parse_exclude_patterns

用户业务链。 解析文件排除规则。通俗说:读取配置,搞清楚哪些文件不需要加密。


[4] JNI_OnLoad → init → file_encryption → file_encrypt_decrypt

用户业务链。 直接调用加解密。


[4] JNI_OnLoad → init → file_encryption → yara_expandScanPaths

用户业务链。 展开需要扫描的路径列表。通俗说:把配置中的扫描路径展开成具体的文件夹列表。


[4] JNI_OnLoad → init → file_encryption → 获取共享库基地址 / ELF符号查找

用户业务链。 加密引擎启动时需要找到相关库的内存位置。通俗说:找到自己和其他库在内存中的位置。


[4] JNI_OnLoad → init → check_library_integrity → proc_maps_library_base_address_lookup / ELF符号查找

用户业务链。 检查SO库完整性:通过/proc/maps找到库的基地址,然后解析ELF符号来验证。通俗说:检查自己这个SO文件有没有被别人动手脚。


[4] JNI_OnLoad → init → 让别人查不到你加载了哪些动态库 → ELF符号查找

用户业务链。 隐藏动态库加载信息:找到linker的内部函数来操纵加载记录。通俗说:抹掉自己加载过哪些库的记录,防止逆向分析者发现。


[4] JNI_OnLoad → init → 抹除某个内存区域 → protectionShell_findLinkerBaseAddress

用户业务链。 清除敏感内存内容,需要先找到linker的基地址。通俗说:擦掉内存中的敏感信息。


[4] JNI_OnLoad → init → 子程序的自我监控 → 杀进程

用户业务链。 看门狗自我监控,发现异常就杀掉进程。通俗说:自己检查自己是否正常,不正常就自杀。


[4] JNI_OnLoad → init → eight_item_security_check → 获取基地址 / ELF符号查找 / rc4_encrypt_32bytes / MD5_Transform / MD5最后阶段 / jni_getApplicationContext

用户业务链。 八项安全检查的各个子步骤:获取库基地址、查找符号、RC4加密32字节(可能是验证token)、计算MD5、获取Android的Application上下文。通俗说:从八个不同角度检查这个APP是否安全。


[4] JNI_OnLoad → init → DEX文件加载 → 从APK读取/解析FDEX格式/数据包拆箱

用户业务链(核心壳功能)。 加载加密的DEX文件:从APK中读取加密的DEX数据→解析梆梆安全自定义的FDEX容器格式→解密解包数据。通俗说:从安装包里取出被加密的程序代码,解开密码,准备运行。


[4] JNI_OnLoad → init → anti_debug_watchdog → 格式化字符串 / 杀进程

用户业务链(反调试)。 反调试看门狗子进程:格式化调试信息→如果检测到调试就杀掉主进程。通俗说:开一个哨兵进程,发现有人在调试就直接干掉程序。


[4] JNI_OnLoad → init → 监控看门狗管道 → 杀进程

用户业务链(反调试)。 通过管道通信的另一个看门狗。通俗说:另一个哨兵,用管道跟主进程保持联络,联络断了就知道出事了。


[3] JNI_OnLoad → init → 各种短链

用户业务链。

  • → MD5_Transform — 初始化时直接用MD5
  • → 递归删除目录 — 清理临时文件
  • → 从APK读取条目 — 读取APK资源
  • → 杀进程 — 安全终止
  • → start_monitor_thread — 启动监控线程
  • → 数据包拆箱员 — 解密数据包
  • → 解析FDEX容器格式 — 解析加密DEX格式
  • → SM4密钥扩展 — 准备国密SM4加密密钥
  • → SM4_decrypt_block — 用SM4解密一个数据块
  • → 获取共享库基地址 / ELF符号查找 — 定位库
  • → 格式化字符串 — 日志输出
  • → jni_loadDex的克隆体 / jni_loadDex — 加载解密后的DEX文件到虚拟机

[3] JNI_OnLoad → parse_linker_elf_find_internal_symbols → find_library_mapping

用户业务链。 解析linker的ELF找到其内部符号→通过/proc/maps获取库的内存映射范围。通俗说:黑进系统的链接器,拿到它的内部函数地址。


[2] JNI_OnLoad → find_elf_base_in_maps / 格式化字符串 / find_library_mapping

用户业务链。 启动时的基础操作:找自己的内存基址、格式化字符串、解析内存映射。


[3] JNI_OnLoad → get_package_info_jni → jni_binder_get_package_info

用户业务链。 通过JNI调用Java层获取包信息(包名、版本号等)。通俗说:问Android系统"这个APP叫什么名字、什么版本"。


第四大组:SO自身壳保护链


[3] init_proc_SO加载时代码段运行时解密 → alloc_exec_and_copy_code → mmap匿名映射系统调用包装器

用户业务链(壳保护)。 SO加载时自动运行的解密初始化:分配可执行内存(用mmap系统调用)并复制代码。通俗说:SO文件一加载就开始自我解密,先申请一块可以运行代码的内存。


[3] init_proc → alloc_exec_and_copy_code → config_set_double_by_path

用户业务链。 代码解密时设置配置参数。


[3] elf_loader_bootstrap → 跳转aplib_decompress → aplib_decompress

用户业务链(壳保护)。 ELF引导加载器解压代码段:先跳转到APLib解压入口,然后执行APLib解压缩算法。通俗说:把压缩过的代码解压出来。


[3] elf_loader_bootstrap → anti_debug_check_maps → openat/read/strlen/strncmp/exit_group/close 系统调用包装器

用户业务链(壳保护+反调试)。 在引导加载阶段就检查反调试:

  • → openat — 打开/proc/self/maps
  • → read — 读取内容
  • → strlen/strncmp — 检查有没有可疑字符串(如"frida"、"xposed")
  • → exit_group — 发现了就立即退出
  • → close — 关闭文件
    通俗说:在最早期就检查有没有人在监视,发现就立即关门走人。注意这里用的都是直接系统调用,绕过了libc,防止Hook。

[3] elf_loader_bootstrap → secure_memory_shuffle → mmap/munmap 系统调用包装器

用户业务链。 安全内存洗牌:通过mmap和munmap来打乱内存布局。通俗说:打乱自己在内存里的位置,让逆向分析更困难。


[2] elf_loader_bootstrap → safe_strncmp / mprotect系统调用 / config_set_double_by_path / munmap系统调用

用户业务链。 引导加载的其他操作:字符串比较(反调试检测用)、修改内存权限(让解密后的代码可执行)、设置配置、释放临时内存。


第五大组:文件IO Hook链

Hook系统调用实现透明加解密。


[2] hook_read_RC4_XOR解密 → inotify_hash_table_lookup / rc4_variant_decrypt

用户业务链(核心Hook)。 Hook了read系统调用:先查哈希表看这个fd是不是受保护的文件→如果是,用RC4+XOR解密读到的数据。通俗说:拦截了read函数,偷偷把加密的文件内容解密后再给APP。


[2] hook_open_拦截trace_marker → is_class_in_target_list / fd_hashtable_register_entry

用户业务链(反调试+文件保护)。 Hook了open系统调用:检查要打开的文件是不是调试相关的(如trace_marker)→如果是就拦截;如果是受保护的文件就注册到哈希表。通俗说:拦截了文件打开操作,阻止调试跟踪,同时记录哪些文件被打开了。


[2] hook_open64 → 同上

用户业务链。 64位版本的open Hook,功能相同。


[2] hook_mmap_映射后原地RC4_XOR解密 → inotify_hash_table_lookup / rc4_variant_decrypt

用户业务链。 Hook了mmap:文件被内存映射后,原地解密映射的内容。通俗说:拦截内存映射操作,把映射进来的加密数据就地解密。


[2] hook_close_注销fd加密配置 → inotify_hash_table_lookup

用户业务链。 Hook了close:文件关闭时从哈希表中删除其加密配置。通俗说:文件关了就把它的加密记录也清掉。


[2] hooked_write透明文件加密写入 → inotify_hash_table_lookup / rc4_variant_decrypt

用户业务链。 Hook了write:写入受保护文件时先加密。通俗说:写文件时偷偷加密再写,保证存储的都是密文。


[2] 内存区域混合RC4XOR加解密Hook → inotify_hash_table_lookup / rc4_variant_decrypt

用户业务链。 另一个内存区域的加解密Hook。


[2] hook写入系统调用并对特定fd数据加密 → inotify_hash_table_lookup / rc4_variant_decrypt

用户业务链(流量加密)。 Hook写入系统调用,对特定文件描述符的数据进行加密。通俗说:加密写出去的数据。


[2] hookFunAddr_read_arm64_inline_hook → flush_icache

用户业务链。 在ARM64上安装inline hook到read函数后刷新指令缓存。通俗说:在read函数上安装跳转指令,然后告诉CPU缓存已经变了。


[3] hookFunAddr_read → find_libc_base_and_make_writable → safe_sprintf

用户业务链。 安装Hook前:找到libc的基地址并把它变成可写的(需要先改内存权限才能修改代码)。通俗说:要改别人家的门牌(代码),得先拿到别人家的地址并弄开门锁。


[2] arm64_inline_hook_installer → decode_aarch64_branch_insn / flush_icache

用户业务链。 ARM64 inline hook安装器:解码被Hook位置的分支指令(需要理解原来的代码)→安装完后刷新缓存。通俗说:Hook安装工具,需要先看懂原来的指令,改完后刷新。


[2] wrapHook → flush_icache

用户业务链。 另一个Hook包装函数,安装后刷新缓存。


第六大组:反调试/安全检查链


[3] protectionShell_registerAntiDebugCallbacks → 找到所有非系统库的内存区域 → check_library_path_is_restricted_so

用户业务链(反调试)。 注册反调试回调:扫描内存找到非系统库→检查这些库路径是否可疑。通俗说:登记反调试检查点,找出可能被注入的可疑库。


[3] check_data_cache_directories → security_check_fd_symlink_integrity → concatVarargStrings / check_path_contains_data_directory_prefix

用户业务链。 检查数据缓存目录:验证文件描述符对应的符号链接是否完整→拼接路径字符串→检查路径是否在/data目录下。通俗说:检查APP的数据目录有没有被人动手脚。


[3] android_emulator_vm_environment_detection → security_check_debugger_via_shellcode → flush_icache

用户业务链(反调试)。 检测模拟器/虚拟机环境→用shellcode方式检测调试器→刷新缓存。通俗说:检查是不是在模拟器上运行,顺便检查有没有调试器附着。


[3] assets_decrypt_and_periodic_config_loader → scan_executable_mappings → check_library_path_is_restricted_so / yr_module_lookup

用户业务链。 解密assets配置并定期加载→扫描可执行内存映射→检查是否是受限库 / 用YARA模块查找可疑特征。通俗说:定期检查有没有可疑的代码被加载到内存里。


[2] secneo_launch_verification_process_double_fork → 找非系统库内存区域 / 格式化字符串 / sys_getpid / 杀进程

用户业务链(反调试)。 双fork反调试:启动验证子进程→扫描内存→获取进程ID→如果发现异常就杀掉进程。通俗说:生两个孩子进程互相监视,被调试了就大家一起死。


[2] dlopen_intercept → check_restricted_vendor_data_path / SecNeo_ELFSymbolParser

用户业务链。 拦截dlopen(动态库加载):检查加载的路径是否可疑→解析ELF符号表验证。通俗说:有人加载新的库时,先检查这个库是不是合法的。


[2] anti_debug_tracer_pid_monitor → 格式化字符串

用户业务链(反调试)。 监控TracerPid字段,格式化输出日志。通俗说:不停地检查有没有调试器attach过来。


[2] detect_art_hook_frameworks → check_library_integrity

用户业务链(反Hook)。 检测ART虚拟机中的Hook框架(如Frida、Xposed)→验证libart库完整性。通俗说:检查Java虚拟机有没有被人安装了Hook框架。


[2] resolve_linker_internal_symbols → findLinkerBaseAddress / SecNeo_ELFSymbolParser

用户业务链。 获取linker的内部符号:找到linker基地址→解析其符号表。通俗说:深入系统链接器内部,拿到它的私有函数地址。


[3] yr_rules_scan_wrapper → hook_openat → is_class_in_target_list / fd_hashtable_register_entry

用户业务链。 YARA扫描时触发openat Hook:检查目标文件→注册fd。通俗说:扫描规则执行时,Hook的openat拦截器同时在工作。


第七大组:APK完整性验证链


[5] sub_5C508延迟执行并释放 → apk_integrity_verification → compute_sha1_hash → hash_update → cpp_demangler_operator_name_translator

用户业务链。 延迟执行APK完整性验证:计算SHA1哈希→更新哈希状态→(最后的函数可能是IDA误识别)。通俗说:算APK文件的指纹(SHA1),跟预存的对比。


[5] ...→ bignum_long_division → bignum_multiply → array_resize / bn_mul_add_words_AArch64Optimized

用户业务链。 APK验证中的大整数运算:长除法→乘法→数组扩容 / ARM64优化的乘加运算。通俗说:验证数字签名需要做大数运算(RSA算法的核心),这里是用ARM64优化的大整数乘法。


[4] ...→ mpi_read_hex_string → array_resize

用户业务链。 从十六进制字符串读取多精度整数→扩容数组。通俗说:把签名中的大数字从文本格式转成数字格式。


[4] ...→ bignum_long_division → array_resize

用户业务链。 大整数除法时需要扩容。


[4] ...→ bignum_montgomery_multiply_reduction → bn_mul_add_words_AArch64Optimized

用户业务链。 蒙哥马利乘法约简→ARM64优化的乘加。通俗说:RSA签名验证中最关键的数学运算,用了专门优化的快速算法。


[3] ...→ apk_integrity_verification → 各种终端

用户业务链。

  • → read_obfuscated_zip_entry — 读取混淆过的APK条目
  • → MD5_Transform / MD5最后阶段 — 计算MD5哈希
  • → base64_encode — Base64编码
  • → reset_dynamic_arrays — 批量释放动态数组
  • → destroy_list_and_header — 释放链表
  • → array_resize — 调整数组大小
  • → check_data_cache_directories — 检查数据目录
  • → 格式化字符串 — 日志输出

第八大组:DEX壳加载/Application替换链


[2] dex_loader_exception_cleanup → 加固壳的应用替换函数

用户业务链(核心壳功能)。 DEX加载异常清理时调用壳的应用替换函数。通俗说:即使DEX加载出错了,也要把壳的Application替换成真实的Application。


[2] dex_loader_exception_cleanup_variant2 → fixProviderMapApplicationReference_secneo_bangcle_replace_shell_application_with_real

用户业务链(核心壳功能)。 另一个变体的异常清理→修复ContentProvider中对Application的引用,把壳替换成真实的。通俗说:Android的ContentProvider里引用了壳的Application对象,需要换成真正的。


[2] DexFileLoader_LoadV34_DP2/BETA1/V26 → ELF符号查找

用户业务链。 不同Android版本的DEX加载器:V34(Android 14)的两个预览版、V26(Android 8.0),都需要查找ELF符号来调用系统的DEX加载API。通俗说:适配不同Android版本的DEX加载方式。


[2] aes_ecb_decrypt → SM4密钥扩展 / SM4_decrypt_block

用户业务链。 AES ECB解密函数内部实际调用的是SM4加密算法。通俗说:函数名叫AES,但实际是国密SM4算法,可能是故意迷惑逆向分析者。


[2] sm4_encrypt_ecb_secneo_tbox → SM4密钥扩展

用户业务链。 梆梆安全的SM4加密(T-Box优化实现),先扩展密钥。


第九大组:C++异常处理运行时链


[4] cxa_throw → noreturn异常终止调用回调 → call_terminate_handler → cxxabi_fatal

系统运行时链。 抛出C++异常→异常无法被捕获时调用终止回调→调用terminate处理器→致命错误退出。通俗说:C++异常没人处理,程序直接崩溃。


[4] personality_routine → gxx_personality_scan_lsda → 异常清理与恢复 → call_terminate_handler

系统运行时链。 GCC的personality函数(异常处理的核心)→扫描语言特定数据区(LSDA)找catch块→找不到就清理→调用terminate。通俗说:异常处理引擎在查找哪个catch能接住这个异常,找不到就终止。


[3] personality_routine → gxx_personality_scan_lsda → 获取上下文各偏移字段 / dwarf_readEncodedValue

系统运行时链。 personality函数读取异常处理的上下文信息(各种偏移字段)和DWARF编码值。


[3] cxa_allocate_exception → aligned_alloc → slab_allocator_alloc

系统运行时链。 分配异常对象内存:对齐分配→从slab分配器获取。通俗说:为C++异常对象申请内存。


[3] cxa_get_globals → calloc_with_fallback → slab_allocator_alloc

系统运行时链。 获取线程级别的C++异常全局数据:用calloc分配→slab分配器兜底。


[3] cxa_globals_cleanup → 智能释放 → pool_free_block_release_merge

系统运行时链。 清理异常全局数据:智能判断是栈内存还是堆内存→释放回内存池。


[3] noreturn原子加载终止处理函数后调用 → call_unexpected_handler → cxxabi_fatal

系统运行时链。 调用unexpected处理器→致命错误。


[2] 大量cxa系列链

系统运行时链。 C++异常处理运行时的各种操作:

  • cxa_throw → 设置异常类标识 — 标记异常类型
  • cxa_exception_cleanup → pool_free / call_terminate — 异常清理
  • cxa_decrement_exception_refcount → pool_free / 引用计数释放 — 异常对象引用计数递减
  • cxa_end_catch → regex_find_all_matches / call_terminate — 结束catch块
  • cxa_begin_catch → aligned_alloc / regex_find_all_matches — 开始catch块
  • cxa_nested_exception_cleanup → pool_free / call_terminate — 嵌套异常清理
  • cxa_get_globals/_fast/init_globals_key/globals_cleanup → cxxabi_fatal — 获取/初始化/清理全局数据
  • std_terminate → call_terminate_handler — std::terminate
  • cxa_pure_virtual/deleted_virtual → cxxabi_fatal — 纯虚函数调用错误
  • cxa_call_unexpected → 异常清理 / unexpected_handler / dwarf_readEncodedValue / terminate — 调用unexpected处理器
  • type_info_can_catch → type_compatibility_check — 类型匹配检查
  • cxxabi_terminate_handler → cxxabi_fatal / try_dynamic_cast — 终止处理器

第十大组:JNI桥接/SafeJNI链


[2] 大量JNI清理/调用链

用户业务链。 这些是安全的JNI调用包装:

  • jni_static_call_wrapper → 获取类型信息 / 带校验格式化输出 — 安全地调用Java静态方法
  • jni_cleanup_invoke_static_3args → 带校验格式化输出 — 3参数静态调用的清理
  • cleanup_and_abort_v1/v2 → 带校验格式化输出 — 出错时清理并终止
  • jni_cleanup_and_invoke_java_method → 带校验格式化输出 / 获取类型信息 — 清理后调Java方法
  • jni_invoke_cleanup → 带校验格式化输出 / 获取类型信息 — 调用后清理
  • jni_cleanup_set_object_field → 带校验格式化输出 — 设置对象字段
  • 释放资源后异常处理 / 创建异常对象 / 释放资源后析构 → 带校验格式化输出 — 各种异常和资源处理
  • safejni_catch_and_cleanup / safejni_get_api_version / safejni_new_byte_array → 带校验格式化输出 — SafeJNI框架的各种安全操作
  • 安全JNI_AllocObject → 获取类型信息 — 安全创建Java对象
    通俗说:所有JNI调用都包了一层安全外衣,调用前后检查异常、记录日志、清理资源。

[2] Utils::getMethodInfo → 获取类型信息字符串
[2] Utils_getStaticMethodInfo → 带校验格式化输出 / 获取类型信息字符串

用户业务链。 工具类查找JNI方法信息,获取Java类型的字符串表示。


[2] 大量vector_push_back_ptr → 抛出带消息的异常

系统运行时链(用户相关)。 各种vector插入操作在空间不足时抛出异常:

  • vector ClassLoader / String / DexFile / 通用 → 抛出异常 — 不同类型的vector都可能因内存不足抛异常
  • vector_unique_ptr_push_back → 抛出异常 — unique_ptr的vector
  • 编译器生成_异常处理_抛出长度错误 → 抛出异常 — 长度错误
    通俗说:往动态数组里添加元素时如果内存不够了就报错。

[3] basic_string异常 → 抛出带消息的异常 → 初始化异常对象设置虚表

系统运行时链。 string操作出错→创建异常对象→初始化虚表指针(C++多态需要)。


[3] next_prime → 抛出系统错误 → 初始化异常对象设置虚表2

系统运行时链。 哈希表扩容时查找下一个质数→失败抛系统错误。


[2] hashtable_rehash → 抛出带消息的异常

系统运行时链。 哈希表重新散列失败时抛异常。


第十一大组:YARA扫描引擎链


[3] yr_compiler_compile_full → zstd帧解压 → scanner_prepare_scan

第三方库调用链。 YARA规则编译完成→解压规则数据(用zstd压缩格式)→准备扫描。通俗说:编译好扫描规则后解压并准备开始扫描。


[3] yr_scan_verify_match → yr_scan_record_match → layout_update_depth

第三方库调用链。 验证YARA匹配→记录匹配结果→更新布局深度。通俗说:找到匹配项后记录下来并更新扫描进度。


[3] regex_character_class_parser → scanner_read_next_char → yy_get_next_buffer

第三方库调用链。 正则表达式字符类解析器→读取下一个字符→获取词法分析器的下一个缓冲区。通俗说:解析正则表达式中的[a-z]这种字符类。


[2] YARA系列短链

第三方库调用链。

  • yr_scanner_scan → AC自动机多模式匹配 — 启动扫描→用AC自动机做多模式字符串匹配
  • yr_rules_deserialize → segment_alloc — 反序列化规则→分配内存段
  • yr_arena_write_data/write_string/allocate/allocate_aligned → segment_alloc — 各种arena内存操作
  • yr_compiler_compile_hex_string → zstd帧解压 — 编译十六进制模式串
  • YARA规则LALR语法解析器 → 语法错误报告 / option_value_free / yr_vsnprintf_256 — 语法解析
  • yr_hash_data/hmac_string/hmac_data_flagged/hmac_string_flagged → hashtable_lookup — 各种哈希查找
  • 编译器错误消息格式化 → yr_snprintf_checked — 格式化错误信息
  • compile_string_base64_variants → base64_encode_variants — 编译Base64变体模式
  • compile_rule → elf_declarations / elf_load_module — 编译规则时加载ELF模块
  • yr_module_dispatch_by_name → elf_declarations — 按名称分发模块
  • set_namespace_and_compile → compiler_lookup_or_add_identifier — 设置命名空间
  • yr_scan_debug_dump → debug_print_ast_node — 调试输出AST节点
  • AC自动机原子提取 → generate_case_variants — 提取原子时生成大小写变体
  • pattern_match_rule_compile_to_linked_list → free_tree / ac_node_match / generate_case_variants — 编译匹配规则链表
  • yr_iterator_get_current → regex_ast_dump — 获取当前迭代器→打印正则AST
  • open_file_and_process / session_add_entry → 正则AST编译为字节码 — 处理文件/添加会话条目
  • 比较运算符解析 → yr_snprintf_checked_v2 — 解析比较运算符
  • yara_regex_engine → NFA正则引擎epsilon闭包 — 正则引擎NFA执行
  • 词法分析器核心 → buffer_stack扩容 / yr_vsnprintf_512 — 词法分析
  • compiler_add_rule_reference → compiler_lookup_or_add_identifier — 添加规则引用
  • hex_yyparse → yr_scan_handle_special_type / 语法错误报告 — 十六进制模式解析
  • hex_yy_scan_buffer/switch_to_buffer/lex/push_buffer/re → demangle_cv_qualifiers — 各种词法分析器操作
  • LALR1语法分析器 → hex语法错误报告 / sqlite_value_cleanup — LALR(1)语法分析
  • locked_execute/v2 → TLS写入后调用回调 — 加锁执行→回调
  • yr_indirect_dispatch/call_1/call_2 → TLS写入后调用回调 / 匿名函数 — 间接调用分发

第十二大组:压缩/解压缩链


[3] apple_lzfse_core_decode → LZ序列解码复制(两种路径)

第三方库调用链。 Apple LZFSE解压引擎→LZ序列解码和内存复制。通俗说:用Apple的LZFSE算法解压数据。


[2] 大量zstd/huffman/fse链

第三方库调用链。 Zstandard压缩/解压的各种操作:

  • ZSTD_decompressStream → codec_convert_thunk / scanner_prepare / zstd帧解压 — 流式解压
  • decompress_dispatch/buffer/with_dict/with_copy → zstd_four_way_parallel_huffman / huffman_decode / zstd帧解压 — 各种解压入口
  • Huffman解码分发 → four_way_parallel / single_stream — 4路或1路Huffman解码
  • FSE熵解码器 → fse_build_decode_table — 有限状态熵解码表构建
  • Zstd序列头解码 → decode_frame_params — 解码帧参数
  • LZ解压栈状态两阶段 → huffman_decode — LZ解压的两阶段处理
  • 压缩调度 → four_way_parallel / huffman_decode — 根据压缩率选择策略
  • FSE解码调度器 → huffman_fast_decode / huf_decompress_table — FSE解码分发
  • zstd_decompress_continue → scanner_prepare_scan — 继续解压
    通俗说:这个库支持zstd、LZFSE等多种压缩算法,用来压缩/解压资源文件。

第十三大组:ZIP/Minizip操作链


[3] sqlite3_bind_data → minizip_zip_entry_read_cd_header → stream_readUint32LE

第三方库调用链。 绑定数据→读取ZIP中央目录头→读取小端32位整数。通俗说:处理ZIP文件中的索引信息。


[2] 各种ZIP操作

第三方库调用链。

  • ZIP条目打开与解密初始化 → stream_readUint32LE — 打开加密的ZIP条目
  • unzOpenInternal → stream_read_uint32_le / outBuffer_appendData — 打开ZIP读取
  • zip_write_next_entry → cd_header_parse — 写入ZIP条目
  • zip_close_file_in_zip → outBuffer_appendData — 关闭ZIP中的文件
  • FAT文件系统目录条目查找 → cd_header_parse — 目录查找

第十四大组:数据库/配置操作链


[2] 各种config/sqlite链

第三方库/用户调用链。

  • config_set_double/int64/string → query_data_node_by_path — 按路径设置配置值
  • sqlite_bind_double/int/text/blob → handle_file_operation — SQLite参数绑定
  • sqlite_create_node/printf_va/config_get_int64/config_get_double → query_data_node_by_path — 创建节点/查询配置
  • config_is_null → query_data_node_by_path — 判断配置是否为空
  • node_set_blob_data → query_data_node_by_path — 设置二进制数据
    通俗说:读写配置和数据库的各种操作。

第十五大组:红黑树/哈希表数据结构链


[2] rbdestroy → free_list_node

第三方库调用链。 销毁红黑树→释放链表节点。


[2] rbsearch → rb_tree_find_or_insert

第三方库调用链。 在红黑树中搜索→找到或插入。


[2] rbwalk → rbtree_inorder_traverse

第三方库调用链。 遍历红黑树→中序遍历。


[2] string_search/binary_search → hashtable_lookup

用户/第三方调用链。 字符串/二分查找→哈希表查找。


第十六大组:加密算法链


[2] get_hash_md5_file_hash_with_header_skip → MD5_Transform_second_instance

用户业务链。 计算文件MD5哈希(跳过文件头部分)→MD5变换的第二个实例。通俗说:给文件算指纹,跳过文件头(可能因为头部会变化)。


[2] sub_38278_free_and_decode → 带校验格式化输出

用户业务链。 释放资源后解码→格式化输出结果。


第十七大组:系统初始化/注册链


[3] locale_facet创建 → locale_facet初始化 → dwarf_register_frame_info

系统运行时链。 创建区域设置facet→初始化→注册DWARF帧信息。通俗说:C++运行时初始化区域设置时顺便注册异常处理信息。


[3] streambuf创建并初始化 → streambuf初始化 → 初始化处理器节点并注册到全局链表

系统运行时链。 创建IO流缓冲区→初始化→注册到全局处理器链表。


[3] fse_build_decode_table → strlen_chk → append_string

第三方库调用链。 FSE解码表构建中的字符串操作。


第十八大组:其他短链


[2] wildmatch → fnmatch_wildcard_pattern_match

第三方库调用链。 通配符匹配→fnmatch风格的模式匹配。通俗说:支持*?的文件名匹配。


[2] read_escaped_char → scanner_read_next_char

第三方库调用链。 读取转义字符→扫描器读下一个字符。


[2] decompress_init_window → yr_exec_dispatch

第三方库调用链。 初始化解压窗口→YARA执行分发。


[2] dwarf_find_exception_handler → decode_aarch64_branch_insn

系统运行时/用户链。 查找DWARF异常处理器→解码ARM64分支指令(可能需要理解跳转指令来定位处理器)。


[2] validate_block_chain → yr_codec_convert_thunk_0

第三方库调用链。 验证块链→编解码转换。


[2] full_write → yr_codec_convert_thunk_0
[2] yr_codec_convert_thunk → yr_codec_convert_thunk_0

第三方库调用链。 写入/编解码转换链。


[2] pSlSI5IS$_ZSTD_decompressStream → yr_codec_convert_thunk_0 / scanner_prepare_scan / zstd帧解压

第三方库调用链。 ZSTD流式解压的三条出边。


[2] exception_safe_vcall_N → 带校验格式化输出 (×6条)

系统运行时链。 异常安全的虚函数调用(6个不同的虚函数),每个都调用格式化输出做日志。


[2] 各种清理/资源释放链

混合链。

  • cleanup_loop_multi_resource / cleanup_and_finalize / dex_load_cleanup_loop / cleanup_handler → 带校验格式化输出 — 各种资源清理后记录日志
  • release_ref_and_invoke_jni / release_and_convert → 带校验格式化输出 — 释放引用后调用/转换
  • double_convert_and_process/free → 带校验格式化输出 — 双重转换和处理
  • context_convert_and_free → 带校验格式化输出 — 上下文转换和释放
  • demangle_template_cleanup → 带校验格式化输出 / append_string — demangle清理

[2] assets_decrypt → 从APK读取条目

用户业务链。 解密assets文件→从APK中读取数据。


[2] sub_4FA74 → findLinkerBaseAddress / SecNeo_ELFSymbolParser

用户业务链。 解析linker内部符号。


孤立节点总结([1]类)

约500+个孤立节点,分为以下大类:

1. YARA引擎API(~150个)

yr_scanner_*yr_rules_*yr_arena_*yr_module_*等。这是完整YARA引擎的公开API,提供规则编译、扫描、结果获取等功能。

2. inotifytools文件监控(~40个)

inotifytools_watch_*inotifytools_event_*等。用于监控文件系统变化,是反调试和文件保护的基础设施。

3. JNI接口函数(~30个)

addLocalRefdeleteLocalRefsafejni::invoke*等。JNI引用管理和安全调用包装。

4. Hook回调函数(~15个)

read函数的Hook回调write的Hook回调mmap的Hook回调等。这些是各个系统调用被Hook后的处理函数。

5. 反调试/安全检查(~15个)

detect_hooking_frameworksdetect_debug_servicesdetect_memory_injection等。各种独立的安全检测函数。

6. 加固壳函数(~10个)

hook_openDexFilehook_open_dex_files_v1/v2/v3等。拦截DEX加载的不同版本适配。

7. 压缩/哈希工具(~30个)

xxh64_hashsha256_*crc32_updatecompressBound等。

8. C++运行时基础设施(~100个)

obj_destructor_freeruntime_error_*typeinfo_*vtable_*等。

9. 数据结构操作(~50个)

rbtree_*hashtable_*list_*array_*等。

10. 线程/同步原语(~20个)

mutex_*semaphore_*yr_thread_*等。

11. IO/流操作(~20个)

stream_*read_*_endianwrite_*等。

12. 配置/数据库(~20个)

config_*sqlite_*db_*等。

这些孤立节点要么是被其他函数通过函数指针间接调用的回调函数,要么是导出API供外部使用,或者是编译器生成的辅助代码。


总结统计

类别调用链数类型
C++ demangle(名称反修饰) ~180条 系统运行时
DWARF栈展开/异常处理 ~80条 系统运行时
JNI_OnLoad初始化主流程 ~50条 用户业务
文件IO Hook ~20条 用户业务
APK完整性验证 ~15条 用户业务
反调试/安全检查 ~15条 用户业务
SO自身壳保护 ~12条 用户业务
C++异常运行时 ~40条 系统运行时
YARA扫描引擎 ~60条 第三方库
压缩/解压缩 ~40条 第三方库
JNI桥接/SafeJNI ~40条 用户业务
ZIP/数据库/配置 ~30条 第三方库/用户
DEX壳加载 ~10条 用户业务
数据结构/工具 ~15条 混合
孤立节点 ~500个 混合

核心用户业务逻辑约占总调用链的25%(~160条),主要集中在DEX加固壳、透明加解密、反调试和完整性校验四大功能。其余75%是系统运行时(C++ ABI/异常处理)和第三方库(YARA/zstd/minizip)的内部实现。

 

 通过分析得出

这个SO库是一个Android应用的"保险箱管家"——它在应用启动时先给自己的代码解密穿上防弹衣,然后派出多个"保安"(反调试看门狗、端口巡逻、内存巡查)四处巡逻确保没人偷窥,接着用SM4密钥打开保险箱把真正的应用代码(加密的DEX文件)解密出来交给Android系统运行,同时在所有文件读写的"门口"安插"安检员"(Hook系统调用),让受保护的文件在硬盘上始终是加密的、只有经过它的手才能读到明文,并且全程不断检查应用的签名有没有被篡改、运行环境有没有被Root或装了Frida/Xposed等黑客工具,一旦发现异常就立刻"关门大吉"终止进程。

 

 

 

 

 

 

 https://bbs.kanxue.com/thread-257061-1.htm

 

 

 

 

 
 
 
 

        <provider            android:name="com.secneo.apkwrapper.CP"            android:exported="false"            android:authorities="cn.amazon.mShop.android.CP"            android:initOrder="2147483647" />

 

 

 

原文链接总结:

VMP(虚拟机保护)逆向分析总结

整体架构概览

text
┌─────────────────────────────────────────────────────┐
│                    Java Layer                        │
│  JniLib.cV(new Object[]{this, param, Integer(18)})  │
│                       │                              │
│              传入对象数组 + 索引                       │
└───────────────────────┬─────────────────────────────┘
                        │ JNI调用
┌───────────────────────▼─────────────────────────────┐
│                 libdexjni.so                         │
│  ┌─────────────┐  ┌──────────┐  ┌───────────────┐  │
│  │  反调试模块   │  │ 索引解析  │  │  VM解释执行器  │  │
│  │  (3处检测)   │  │  模块    │  │  (OLLVM混淆)  │  │
│  └─────────────┘  └────┬─────┘  └───────┬───────┘  │
│                        │                 │          │
│                   ┌────▼─────────────────▼────┐     │
│                   │   自定义Opcode → JNI调用   │     │
│                   └───────────────────────────┘     │
└─────────────────────────────────────────────────────┘

1. DEX指令基础

标准DEX指令格式(35c格式)

text
A | G | op BBBB F|E|D|C     共6字节

示例: 6F 10 1C 33 01 00
┌─────┬─────┬──────┬──────────┬─────────────────┐
│  A  │  G  │  op  │   BBBB   │   F|E|D|C       │
│  1  │  0  │  6F  │  331C    │   C=1,D=0,E=0   │
└─────┴─────┴──────┴──────────┴─────────────────┘
invoke-super {p0}, FragmentActivity->getResources()

2. 反调试机制

text
JNI_OnLoad (总入口)
    ├── 0x47CAC  线程检测
    │   └── 检测运行时间 → getpid → syscall(__NR_kill) 杀进程
    ├── 0x47C70  cmdline反调试
    │   └── 读取/proc/self/cmdline 检测调试器特征
    └── 0x489EC  status反调试
        └── 读取/proc/self/status 检测TracerPid

绕过方法: NOP掉JNI_OnLoad中的反调试入口
ARM64 NOP: MOV W1,W1 → E1 03 01 2A

3. VMP核心分析

3.1 索引到DexCode的解析流程

text
Java层: Integer.valueOf(18)  →  索引 0x12
┌──────────────────────────────────────────────────┐
│            JavaInfo 结构体 (从索引获取)             │
├──────────────────────────────────────────────────┤
│  +0x00  指针1                                     │
│  +0x08  指针2 → DexCode                          │
│  +0x10  index = 0x12 (java层传递的索引)            │
│  +0x14  unknow2 = 0x2E                           │
│  +0x18  dexcode指针                               │
│  +0x20  unknow4 = 0x03                           │
│  +0x24  unknow5 = 0x02                           │
│  +0x28  unknow6 = 0x02                           │
└──────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────┐
│               DexCode 结构体                      │
├──────────────────────────────────────────────────┤
│  registersSize = 3                               │
│  insSize       = 2                               │
│  outsSize      = 0                               │
│  triesSize     = 0                               │
│  debugInfoOff  = ...                             │
│  insnsSize     = 0x0F  (15条指令)                 │
│  insns[] = 自定义opcode序列                        │
└──────────────────────────────────────────────────┘

3.2 VM解释执行器架构

text
vm_parse (地址: 0x29B70)
    │  ┌─────────────────────────────────┐
    │  │  超大switch分发器 (OLLVM混淆)    │
    │  │  - 数千个case分支               │
    │  │  - FLA + BCF混淆               │
    │  │  - IDA F5完全卡死               │
    │  └──────────┬──────────────────────┘
    │             │
    ▼             ▼
┌─────────────────────────────────────────────┐
│           指令执行循环                        │
│                                             │
│  1. 从 [SP,#0x15A0+var_1460] 读取指令指针    │
│  2. 解析当前指令的opcode                     │
│  3. 进入对应case执行                         │
│  4. 指令指针 += 指令长度(如+6字节)            │
│  5. 跳回分发器继续下一条                      │
└─────────────────────────────────────────────┘

3.3 方法信息获取流程

text
vm_parse (0x29B70)
0x29BB0 → 0x4AE80 → 0x4AEB4 → 0x4E78C → 0x3F92C
┌─────────────────────────────────────────────────────┐
│  全局方法信息表                                       │
│  base = qword_7517D666A0                            │
│                                                     │
│  entry = *(base + 0x5C * 8)   // 0x5C来自指令操作数  │
│  ┌─────────────────────────────────────────────┐    │
│  │  +0x00  class_name  → "android/.../Fragment" │    │
│  │  +0x08  method_sig  → "(Landroid/os/Bundle;)V"│   │
│  │  +0x10  method_name → "onCreate"             │    │
│  └─────────────────────────────────────────────┘    │
│                                                     │
│  IDAPython查询:                                      │
│  idc.GetString(idc.Qword(                           │
│    idc.Qword(idc.Qword(0x7517D666A0)+0x5C*8)+8*n)) │
│    n=0: 类名  n=1: 签名  n=2: 方法名                │
└─────────────────────────────────────────────────────┘
┌──────────────────────────────────────┐
│  JNI调用链                            │
│  FindClass("android/.../Fragment")   │
│  GetMethodID(cls, "onCreate", sig)   │
│  CallNonVirtualMethod(...)           │
│  → 等价于 super.onCreate()           │
└──────────────────────────────────────┘

4. Opcode映射表(onCreate方法完整解析)

自定义指令 → 标准DEX指令

text
VMP Opcode                          标准DEX
┌──────────────────┐    ┌──────────────────────────────────────────┐
│ A3 20 5C 00 01 00│ →  │ 6F 20 02 15 21 00                        │
│ [invoke-super]   │    │ invoke-super {p0,p1},                    │
│                  │    │   FragmentActivity->onCreate(Bundle)V    │
├──────────────────┤    ├──────────────────────────────────────────┤
│ 6B 10            │ →  │ 12 10                                    │
│ [const]          │    │ const v0, 0x1                            │
├──────────────────┤    ├──────────────────────────────────────────┤
│ CC 20 13 02 01 00│ →  │ 6E 20 96 B1 01 00                        │
│ [invoke-virtual] │    │ invoke-virtual {p0,v0},                  │
│                  │    │   BaseActivity->requestWindowFeature(I)Z │
├──────────────────┤    ├──────────────────────────────────────────┤
│ 55 11 6D 00      │ →  │ 5B 11 80 69                              │
│ [iput-object]    │    │ iput-object p0, p0,                      │
│                  │    │   BaseActivity->a:BaseActivity           │
├──────────────────┤    ├──────────────────────────────────────────┤
│ 53 10 6D 00      │ →  │ 54 10 80 69                              │
│ [iget-object]    │    │ iget-object p0, p0,                      │
│                  │    │   BaseActivity->a:BaseActivity           │
├──────────────────┤    ├──────────────────────────────────────────┤
│ 72 10 60 01 00 00│ →  │ 71 10 AD AD 00 00                        │
│ [invoke-static]  │    │ invoke-static {v0}, c->a(Activity)V     │
├──────────────────┤    ├──────────────────────────────────────────┤
│ 69 00            │ →  │ 0E 00                                    │
│ [return-void]    │    │ return-void                              │
└──────────────────┘    └──────────────────────────────────────────┘

指令解析示例(A3 20 5C 00 01 00)

text
字节:  A3  20  5C  00  01  00
       ──  ──  ──────  ──────
       │   │     │       │
       │   │     │       └── F|E|D|C 参数寄存器
       │   │     │           0001 & 0xF = 0001 → {p0, p1}
       │   │     │
       │   │     └── BBBB = 0x005C
       │   │         全局方法表索引 → 查找到:
       │   │         class: "FragmentActivity"
       │   │         name:  "onCreate"  
       │   │         sig:   "(Landroid/os/Bundle;)V"
       │   │
       │   └── A=2 (参数个数), G=0
       └── op=A3 → 映射到 0x6F (invoke-super)

5. 修复结果

text
修复前:
┌──────────────────────────────────────────────┐
│ protected void onCreate(Bundle paramBundle) {│
│   JniLib.cV(new Object[] {                   │
│     this, paramBundle, Integer.valueOf(18)    │
│   });                                        │
│   throw new VerifyError("bad dex opcode");   │
│ }                                            │
└──────────────────────────────────────────────┘

修复后:
┌──────────────────────────────────────────────┐
│ protected void onCreate(Bundle paramBundle) {│
│   super.onCreate(paramBundle);               │
│   requestWindowFeature(1);                   │
│   this.a = this;                             │
│   c.a(this.a);                               │
│ }                                            │
└──────────────────────────────────────────────┘

6. 关键发现总结

text
┌─────────────────────────────────────────────────────────┐
│                    VMP特征总结                            │
├─────────────────────────────────────────────────────────┤
│                                                         │
│  1. 执行方式: JNI解释执行自定义opcode                     │
│     └── 不是自己重新实现指令,而是通过JNI桥接调用           │
│                                                         │
│  2. 混淆方式: OLLVM (FLA + BCF)                          │
│     └── 核心vm_parse函数F5完全失效                        │
│                                                         │
│  3. Opcode替换: op字节被替换,但格式不变                   │
│     └── A|G|op|BBBB|F|E|D|C 结构保持                    │
│     └── 参数寄存器编号不变 (VMP也需要知道参数个数)          │
│                                                         │
│  4. 字符串硬编码: 方法名/签名写入SO                        │
│     └── 导致不同APP的libdexjni.so体积不同                 │
│                                                         │
│  5. 全局方法表: 预构建的方法信息结构体                      │
│     └── 通过索引快速查找,避免运行时从DEX搜索               │
│                                                         │
│  6. 0xFF个opcode → 0xFF个opcode                         │
│     └── 本质是一一映射关系,加密与否影响不大                │
│                                                         │
│  分析方法论:                                              │
│  ┌───────┐   ┌───────────┐   ┌──────────┐              │
│  │找关键点│ → │下断跑一遍  │ → │逆向倒推  │              │
│  │(JNI调用)│  │(记录执行流)│   │(还原逻辑)│              │
│  └───────┘   └───────────┘   └──────────┘              │
└─────────────────────────────────────────────────────────┘


原文如下:



现在的VMP的比较常见了,应该也是稳定性满足要求了,今天来分析一波,如有不当还请各位大佬指正
实际上 libdexjni.so在不同的APP中体积会不一样,应该是硬编码写入字符串和指令导致的

1-VMP还是先看下opcode部分知识,DEX指令格式
代码转换成DEX指令先看代码
public class BaseActivity extends FragmentActivity {
    public Resources getResources() {
        return super.getResources();
    }
}

对应的第一条指令是
6F 10 1C 33 01 00 0C 00 11 00
每条指令是2字节,所以先看第一条 6f 20,根据官方文档  6F的解释是 invoke-super    格式为35c
A | G | op BBBB F|E|D|C  35c    [A=5] op (vC, vD, vE, vF, vG),meth@BBBB
    A[=5] op (vC, vD, vE, vF, vG), site@BBBB
    A[=5] op (vC, vD, vE, vF, vG), type@BBBB
    A[=4] op (vC, vD, vE, vF), kind@BBBB
    A[=3] op (vC, vD, vE), kind@BBBB
    A[=2] op (vC, vD), kind@BBBB
    A[=1] op (vC), kind@BBBB
     A[=0] op (), kind@BBBB


A | G | op BBBB F|E|D|C
根据opcode克制总共6个字节,对应的就是
A=1  G=0 op=6f  BBBB就是 331c,然后是C=1 D=0 E=0 F=1
    所以这里转换过来就是
invoke-super {p0}, Landroidx/fragment/app/FragmentActivity;->getResources()Landroid/content/res/Resources;

2-反调试
通过常规手段,在关键的open函数观察,然后逆向查找
发现几处反调试
0x47CAC 处是创建线程,检测运行时间,getpid 然后  linux_eabi_syscall(__NR_kill, a1, a2)来杀死进程
0x047C70  处是cmdline反调试,https://bbs.pediy.com/thread-223460.htm  这位大佬提到过
0x489EC    处是 /proc/status检测反调试  
实际可能还有,但是在找到这三处之后,我发现特殊的地方是刚好在JNI_OnLoad处有个总的入口,所以直接
nop指令反调试就gg了
我用 arm64调试的    mov w1,w1 对应的的hex是E103012A
然后dump出dex,先内存找到dex.035


import struct
 
 
start = 0x75172191ec
 
dump_so = "/Users/beita/tmp/bangbang/dump_vmp.dex"
length = 0x6ee27c
file = open(dump_so,'w')
file.close()
fn = AskStr(dump_so ,"save as:")
with open(fn,"wb+") as f:
    for addr in range(start , start+length):
        f.write(struct.pack("B" , Byte(addr)))
    print "success to save as "

3-VMP的具体分析
得到dex之后,转成jar,看了下,大部分函数是 JniLib.cV等来做的,但是有一个Integer.valueof,是一个函数索引,用来查找指令的
public void o()
{
    JniLib.cV(new Object[] { this, Integer.valueOf(17) });
    throw new VerifyError("bad dex opcode");
}
protected void onCreate(Bundle paramBundle)
{
    JniLib.cV(new Object[] { this, paramBundle, Integer.valueOf(18) });
    throw new VerifyError("bad dex opcode");
}

附加调试发现实际在这里解开这个java数组也就是 new Object的这个数组

if ( !v6 )
{
    v17 = (unsigned _int64)((_int64( _fastcall *)(JNIEnv *, _int64)(*v5)->GetArrayLength)(v5, v4) - 1;
    v12 = ((_int64( _fastcall *)(JNIEnv *,__int64))(*v5)->GetObjectArrayElement)(v5, v4);
    v13 = JNIEnv::CallInMethod(v5, v12, qword_7516104A00);
    result = j_010_I1111_081_0800_001_001111111_0801_0805_(v13)
    v14=result;
}


这里用onCreate来分析  索引是18=0x12
 JniLib.cV(new Object[] { this, paramBundle, Integer.valueOf(18) });
调试往下走,根据这个索引,会取出一个结构体信息,结合上下文信息
F0 4E A3 17 75 00 00 00 50 6B A9 17 75 00 00 00
12 00 00 00 2E 00 00 00 00 00 00 00 00 00 00 00
03 00 00 00 02 00 00 02 00 00 00 00 00 00 00 00
F8 4E A3 17 75 00 00 00 00 00 00 00 00 00 00 00
03 00 02 00 02 00 00 FB 30 01 00 0F 00 00 00 00
00 00 00 00 00 00 00 FC 20 13 02 01 00 55 11 00
A3 20 5C 01 21 00 6B 10 CC 20 13 02 01 00 55 11
6D 05 53 10 60 72 10 60 01 00 69 00 00 00 00 00


这里取出  0x7517a96b50的值 是  0x12

v31 = *(_DWORD *)(v14 + 16);
v32 = *(_DWORD *)(v14 + 20);
v33 = *(_DWORD *)(v14 + 24);
v34 = *(_DWORD *)(v14 + 32);
v35 = *(_DWORD *)(v14 + 8);
v36 = *(_DWORD *)v14;
v37 = *(_DWORD *)(v14 + 4);



strut JavaInfo {
    uint32_t index;     // 0x12   这是java层传递的
    uint32_t unknow2;   // 0x2e   未知
    uint64_t dexcode;   // dexcode指针
    uint32_t unknow4;   // 0x03
    uint32_t unknow5;   // 0x02
    uint32_t unknow6;   // 0x02  这里看起来没有用到  但是貌似是DexCode的内容
};
跳转到dexcode的位置看下内容
03 00 02 00 02 00 00 00      FB 30 01 00 0F 00 00 00
A3 20 5C 00 21 00 6B 10      CC 20 13 02 01 00 55 11
6D 00 53 10 6D 00 72 10        60 01 00 00 69 00 00 00
13 00 00 00 56 00 00 00     00 3A 2E 17 75 00 00 00
03 00 00 00 01 00 00 00      02 00 00 00 00 00 00 00

struct DexCode {
    u2  registersSize;   // 3
    u2  insSize;           、、 2
    u2  outsSize;
    u2  triesSize;
    u4  debugInfoOff;       /* file offset to debug info stream */
    u4  insnsSize;          /* size of the insns array, in u2 units */
    u2  insns[1];
    }

registerSize = 3
insSize = 2
outsSize = 0
.....
主要看
insnsSize = 0xf
共15条指令   ,但是这个指令不是 标准的dex指令 opcode被改过,且字符串信息也是被改过,就是是说他不是系统来解析的,而且会有一个对应关系

A3 20 5C 00 21 00 6B 10  CC 20 13 02 01 00 55 11
6D 00 53 10 6D 00 72 10  60 01 00 00 69 00

进入到vm_parse函数之前的代码还能F5看下逻辑,但是到  vm_parse地址是29b70位置处,F5不好用了,貌似是刻意把这个函数写的非常大,
有点像dalvik里边的HANDLE那种搞到一起, 这样在加固过程中OLLVM混淆之后,更加复杂

在解析opcode之前会进行数据保存
信息看起来是保存到一组结构中
struct Infos1{
    uint64_t data1;
    uint64_t *data2;  // data2 = malloc(32)   是根据JavaInfo的dexCode来的
    uint64_t data3;
    uint64_t data4;
    uint64_t data5;
    uint64_t data6;
    uint64_t data7;
    uint64_t data8;    // JavaInfo的data3的值
};


调试继续往下走,来到 j___Sl_I5_lO000_0SSIO_I0_O__OI_5I___lSSl0_lO5_0I5I5S5_  这个函数,这个函数不能F5了,要根据汇编来分析具体的vm是如何
解析opcde来实现代码运行的

最终的 入口是  29b70这个函数
调用获取GetMethodID的过程是
vm_parse   29b70  -  29bb0 -  4ae80 - 4aeb4  -  4e78c - 3f92c 开始获取名称和GetMethodID
第一个参数 结合全局变量可以获得这些内容Class MethodSig MethodName  

前面提到,vmp可能会借助jni来实现,所以现在GetMethodID下段点,查看数据,方法名称和签名


.text:0000007979980 LDR          X8, [X21]
.text:0000007979988 LDR          X1, [X19]
.text:0000007979988 LDR          X8, [X8, #0x30]       ; FindClass  参数是 android/xxx/FragmentActivity
.text:000000797998C MOV          X0, X21
.text:000000797998E MOV          X8
.text:000000797998E MOV          X8, [X21]
.text:0000007979994 LDR          X3, X2, [X19, #8]
.text:000000797999C LDR          X8, [X8, #0x108]       ; GetMethodID onCreate
.text:000000797999A MOV          X9,x0
.text:000000797999A STR          X9, [sp, #0x15A0+var_7D8]
.text:000000797999A MOV          X0, x21


这段代码是ARM架构的汇编指令,涉及Android的FragmentActivity类查找和onCreate方法ID获取,属于Android应用逆向分析场景的内容。如果需要对这段代码的逻辑进行解析、定位相关功能模块等,我可以帮你进一步处理哦~
寄存器x2
0000007517809A50  6F 6E 43 72 65 61 74 65 00 00 00 00 00 00 00 00  |onCreate........|
0000007517809A60  50 9A 80 17 75 00 00 00 0B 00 00 00 00 00 00 00  |P...u.........|

寄存器x3
0000007515E0E6C0  28 4C 61 6E 64 72 6F 69 64 2F 6F 73 2F 42 75 6E  |Landroid/os/Bun|
0000007515E0E6C0  64 6C 65 3B 29 56 00 00 00 00 00 00 00 00 00 00  |dle;)V........|


但是因为被ollvm混淆过,体积非常大,可能是fla和bcf都加上去了
这个函数IDA识别基本上是卡死状态,所以只能是找关键点切入
看一下OLLVM的图,被混的 单个switch有几千个,而且F5卡死了

所以用快速定位关键汇编位置


分析ollvm个人觉得一点技巧是找到关键的block,下好断点,走一遍,逆向查找,基本上如果不是很大的代码块都能梳理清楚逻辑
大致如下图

函数开始运行
vm_parse
step1 找到dex指令对应关系
step2 查找栈上的存储位置
step3 假设这里用到字符串
函数运行结束

现在反方向去找到是从哪里获取到的字符串,这个字符串是如何从DexCode取出来的,那么这个vm解释执行的逻辑差不多就清楚了

倒推代码来了解逻辑

上面的onCreate是根据在函数j__$S$0l0$SOOII$0lIll$SI_O0$S0ll__Il_S5lIl5lOlI5SO0S5$ 这里,根据一个输入值返回的结构体来得到的
计算处一个全局变量的偏移值
return *(_QWORD *)(qword_7517D666A0 + 8LL * a1);
其实是个结构体
用IDA直接取字符串看一下
idc.GetString(idc.Qword(idc.Qword(idc.Qword(0x7517D666A0) + 0x5C * 1) + 8 *n))
n=0是类名   android/support/v4/app/FragmentActivity
n=1是方法的参数签名 (Landroid/os/Bundle;)V
n=2是方法名称 onCreate
看起来是个如下的结构结构

struct {
    void *class_name;
    void *method_sig;
    void *method_name;
}
所以JNI调用的onCreate来自这个结构体,实际上如果做过java2c的一看就知道是调用super.onCreate在
然后再网上查看汇编,找到这个结构体是从哪里来的

.text:0000007517CF9940 ADD     X1,X1,#0x78
.text:0000007517CF9944 MOV     X24,x26
.text:0000007517CF9948 MOV     W23,W21
.text:0000007517CF994C LDR     X21, [SP,#0x15A0+var_778]
.text:0000007517CF9950 STR     W22, [SP, #0x15A0+var_7BC]
.text:0000007517CF9954 MOV     W22,W6
.text:0000007517CF9958 MOV     x0,x21
.text:0000007517CF995C LDR     X2,[SP,#0x15A0+var_780]
.text:0000007517CF9960 LDR     W3, [SP,#0x15A0+var_814]
.text:0000007517CF9964 MOV     W26,W25
.text:0000007517CF9968 MOV     W25,W30
.text:0000007517CF996C BL      sub_7517D17CF4
.text:0000007517CF9970 MOV     x0,x21
.text:0000007517CF9974 LDR     W1, [SP, #0x15A0+var_7F4]
text:0000007517CF9978 BL    $8$010$800IIS0lIl1$8I
.text:0000007517CF997C MOV     x19,x0
.text:0000007517CF9980 LDR     x8,[x21]

函数入参存放在x1寄存器 就是w1,而且是在站栈上
LDR             W1, [SP,#0x15A0+var_7F4]
根据这局汇编反向推一下            LDR取值必然有一个STR赋值
STR             X1, [SP,#0x15A0+var_7F4]
借助IDAPython来查找一下,之所以不用快捷键x去直接找,是因为需要找到调用顺序,所以在2b970的位置开始用脚本

last_insns = ''
 
def fn_f8():
    idaapi.step_over()
    GetDebuggerEvent(WFNE_SUSP | WFNE_CONT, -1)
 
def fn_f9():
    idaapi.continue_process()
    GetDebuggerEvent(WFNE_SUSP | WFNE_CONT, -1)
 
 
last_ins = ''
def run_next():
    fn_f8()
 
    asm_str = idc.GetDisasm(idc.GetRegValue('pc'))
 
    cur_match = re.match(r'STR\s+(\S+),\s\[SP,#0x15A0\+var_1460\]', asm_str,  re.M | re.I)
    if cur_match  :
        reg1 = cur_match.group(1)
 
        value = hex(idc.Word(idc.Qword(reg1) + 2))
        print('nop addr', hex(cur_addr), asm_str)
        return
    else:
        last_ins = asm_str
 
    run_next()
 
 
run_next()
 
 

最终找到这个上面输入的5C是从最开始的 结构体里面的DexCode取出来的
如下
A3 20 5C 00 21 00

然后利用这个思路,找到指向指令insns的指针,实际就是在29b70处判断,当前的STR放入的指针是否是前边解出来的insns地址
找到指令指针是在存放在栈上的一个地址     [SP,#0x15A0+var_1460]
![alt text](image.png)
看上图所示,从栈 SP,#0x15A0+var_1460的地址  07517AEA460 得到的正是insns的地址

A3 20 5C 00 21 00
---------------------------------------------------------------------------------------------------------------------------------------------------------
此时联系到onCreate的地方,用到了5c  而我们根据1460推导出了5c的来源
.text:00000075164A7A9C LDR     X8, [SP, #0x15A0+var_1460]
.text:00000075164A7A9C LDR     W26, [X8, #4]
.text:00000075164A7A9C LDR     X9, [X8, #0x14]
.text:00000075164A7A9C STR     W8, [SP, #0x15A0+var_7F4]
.text:00000075164A7A9C LDR     X9, [SP, #0x15A0+var_780]
.text:00000075164A7A9C LDR     X9, [X9, #0x20]

这里很清楚了,实际上指令的格式还是没变  则,解释执行OPCODE

还是6字节
A3 20 5C 00 21 00
A | G | op BBBB F|E|D|C  
对应一下就是   A=2 G=0 op=A3     BBBB是0x5C    C=0  D=0  E=0  F=1
这里的第三组也就是 0021解密一下     0021 & 0xf  = 0001  
0x5c在Android自带虚拟机里变解释执行为  MethodID,这里vmp使用的是自定义存放的一个结构体,估计是为了快速查找,因为按照逻辑,是要从DEX里边取查找,可能是为了提高效率,所以保存起来
并且我看到vmp虚拟化的java函数越多,libdexjni.so的体积越大
.text:000000751649896C BL      sub_75164B9CF4
.text:0000007516498970 MOV     x0,x21
.text:0000007516498974 LDR     W1,[SP,0x15A0+var_7F4]  ;指令里边取出来的0x5c结合全局变量来取函数名称信息结构体
.text:0000007516498978 NL      .55001058001100111165100580111165111510115800558
.text:000000751649897C HOV     x19,x0
.text:0000007516498980 LDR     x0,[x21]
.text:0000007516498984 LDR     x1,[x19]
.text:0000007516498988 LDR     xB,[x8,00x30]    findelass
.text:000000751649898C HOV     x0,x21
.text:0000007516498994 LDH     x0,[x21]
.text:0000007516498998 LDF     x3,x2,[x19,08]
.text:000000751649899C LDH     xB,[x0,00108]    Getmathodid oncreate

继续调试往下走,你会看到  CallNonVirtualMethod   正是 super.onCreate
很熟悉的格式
--------------------------------------------------------------------------------------------------------------------------------
a320 是invoke-super
005c是取MethodID
0021 解密0001 实际上是参数v0  但是我觉得这个解密多余的 因为取前边2位的
则这条指令是   invoke-super {p0, p1}, Landroidx/fragment/app/FragmentActivity;->onCreate(Landroid/os/Bundle;)V
--------------------------------------------------------------------------------------------------------------------------------
同样往下走   用脚本跑,到这里停下,刚好是前边的6个字节的指令执行完了的地方    下一组指令
.text:0000007516494D88 LDR     X8, [SP,#0x15A0+var_1460]  ; jumptable 0000000000246C0 case 674
.text:0000007516494DBC ADD     X8, X8, #6  ; 往后6个字节,就是invoke-super解析完了的位置
.text:0000007516494D90 STR     X8, [SP,#0x15A0+var_1460]
.text:0000007516494D94 MOV     W8, #0x456
.text:0000007516494D98 B       loc_7514B94C0

这里取出来是
6B 10
调试发现实际就是  定义了一个数值
--------------------------------------------------------------------------------------------------------------------------------
6b  const
10 v0,0x1
结果就是   const v0,0x1
--------------------------------------------------------------------------------------------------------------------------------

next
--------------------------------------------------------------------------------------------------------------------------------
CC 20 13 02 01 00[A=2]op{vC, vD},kind@BBBB
CC 20  invoke-virtual  
0213   取MethodiD  requestWindowFeature     (I)Z
0001  参数
 invoke-virtual {p0, v0}, Lcom/abing/appvmp/BaseActivity;->requestWindowFeature(I)Z
--------------------------------------------------------------------------------------------------------------------------------

next

--------------------------------------------------------------------------------------------------------------------------------

5511   6d00[A=2]op{vC, vD},kind@BBBB

1155 invoke-virtual  

006d  取MethodiD  requestWindowFeature     (I)Z

0001  参数

iput-object p0, p0,Lcom/wangzhong/fortune/ui/activity/BaseActivity;->a:Lcom/wangzhong/fortune/ui/activity/BaseActivity;;

--------------------------------------------------------------------------------------------------------------------------------



next  继续往下走,

在5feac处找到 这三句代码,运气不错,这里刻意F5,可以看到是 取出一个对象的值,根据分析得知是 BaseActivity的属性a

parsestringInfo(v8, 6(2]);  //解密一下字Baseactivity
v10=15-v11;
928501-v11
break;
ase 1:
v12 *v9 -151* ((unaigned int)(55554* v)>> 23);
v((int64(fastcall+)(JNIn*,int64,int64))(*)->etobjectrield)(,v7,v)1//获取BasActivity的a对象
v10=102-v12
(1)
合起来就是执行了53106d00这条指令
5OLL
v10=103- v12;
break;
ase 2:
v1)v- 11((unsigned int)(47663·v) >>19);
v((int4(fantcall*)(JNInv*,ORD))(*v)->Pindclass)(v,*);/ PieldiD获取
((int64 (faatcall *)(JNIv*,int4, QORD, QORD))(*)->trieldID)(, v5, v6,v)
(4)


--------------------------------------------------------------------------------------------------------------------------------

53 10 6D 00         [A=2]op{vC, vD},kind@BBBB

1053 invoke-virtual  

006d  取MethodiD  requestWindowFeature     (I)Z

0001  参数

iget-object p0, p0,Lcom/wangzhong/fortune/ui/activity/BaseActivity;->a:Lcom/wangzhong/fortune/ui/activity/BaseActivity;

--------------------------------------------------------------------------------------------------------------------------------


Type: `idc.Qword(idc.Qword(idc.Qword(0x751505806A0) + 0x160 * 8) + 8 * 1)` (Landroid/app/Activity)V
Python>idc.GetString(idc.Qword(idc.Qword(0x751505806A0) + 0x160 * 8) + 8 * 1)
(com/wangzhong/fortune/f/c)
Python>idc.GetString(idc.Qword(idc.Qword(idc.Qword(0x751505806A0) + 0x160 * 8) + 8 * 2))
b



next  脚本执行结果如下



--------------------------------------------------------------------------------------------------------------------------------

  72 10  60 01 00 00    [A=2]op{vC, vD},kind@BBBB

10 72  invoke-virtual  

0160   取MethodiD  requestWindowFeature     (I)Z

0000  参数编号

invoke-static {v0}, Lcom/wangzhong/fortune/f/c;->a(Landroid/app/Activity;)V

--------------------------------------------------------------------------------------------------------------------------------

000000751648CDD0 LDR     X0, [SP,#0x15A0+var_1470]  ; jumptable 0000000000031170
000000751648CDD4 LDR     X8, [X8,#0x10]
000000751648CDD8 LDR     X11, [SP,#0x15A0+var_1460]
000000751648CDDC LDRH    W9, [X11]
000000751648CDE0 LDR     X10, [SP,#0x15A0+var_7A0]
000000751648CDE4 SUB     X10, X11, X10
000000751648CDE8 LSL     X10, X10, #1
000000751648CDEC STR     W8, [X8,W10,LSL #2]
000000751648CDF0 ROR     W8, W8, W9
000000751648CDF4 STR     X8, [SP,#0x15A0+var_790]
000000751648CDF8 ADRP    X9, #func_75164091F8@PAGE
000000751648CDFC ADD     X9, X9, #func_75164D91P8@PAGEOFF
000000751648CE04 ADD     X8, X9, W0,UXTW #3
000000751648CE08 LDR     X13, [X8]
000000751648CE0C MOV     W9, #0x131
000000751648CE10 MOV     W8, #0x4A
000000751648CE14 MOV     W4, W22
000000751648CE18 MOV     W0, W28


next



--------------------------------------------------------------------------------------------------------------------------------

69 00   这个指令比较简单就是

return-void

--------------------------------------------------------------------------------------------------------------------------------

对应到dex指令  ,0x5c这些部分需要自己取dex里边查找MethodID和ClassName对应起来,就是算出MethodID的索引就行
这里的5c最终是要到dex里取查找的



把下面这部分指令的根据分析经过转换

A3 20 5C 00 01 00 6B 10  CC 20 13 02 01 00 55 11
6D 00 53 10 6D 00 72 10  60 01 00 00 69 00 00 00


VMP的opcode                真实的Dex指令
A3 20 5C 00 01 00  ------>  6f 20 02 15 21 00
6B 10                  ------>  12 10
CC 20 13 02 01 00  ------>  6e 20 96 b1 01 00
55 11 6D 00            ------>  5b 11 80 69
53 10 6D 00            ------>  54 10 80 69
72 10 60 01 00 00  ------>  71 10 ad ad 00 00
69 00                  ------>  0e 00



用流程图来说明下


‌指令获取与解析‌
调用JNILib.cV(Integer类型的索引),传入Integer类型索引
根据索引获取对应DexCode
从栈位置[SP,#0x15A0+var_1460]读取指令指针,该位置存储着指令地址
‌指令分发执行‌
读取下一条指令,进入超大switch结构的分发器(dispatcher)
分发器会根据指令类型执行对应操作,示例中包含mov、invoke、iget等指令
指令执行完成后,指令指针会移动到0100之后的位置
‌流程结束‌
执行Function END,结束当前指令分发流程
另外补充示例里的指令字段说明:

a3 20:代表指令和格式的值
5c 00:对应指令所属的class
01 00:代表寄存器和变量值








得到


修复前的指令  实际上 JNILib.cv这部分代码是填充的 只有一个索引有用,所以直接覆盖
uint insns_size: 16h
ushort insns:
    insns: 3012h
    insns: 23h
    insns: 1D9Ch
    insns: 112h
    insns: 44Dh
    insns: 100h
    insns: 1112h
    insns: 54Dh
    insns: 100h
    insns: 2112h
    insns: 313h
    insns: 12h
    insns: 1071h
    insns: BD59h
    insns: 3h
    insns: 20Ch
    insns: 24Dh
    insns: 100h
    insns: 1071h
    insns: 6099h
    insns: 0h
    insns: 73h

修复后的指令
ushort tries_size
uint debug_info_off: 0h
uint insns_size: 16h
ushort insns:
    insns: 206Fh
    insns: 1502h
    insns: 21h
    insns: 1012h
    insns: 206Eh
    insns: B196h
    insns: 1h
    insns: 115Bh
    insns: 6980h
    insns: 1054h
    insns: 6980h
    insns: 1071h
    insns: ADADh
    insns: 0h
    insns: 0h
    insns: 0h
    insns: 0h
    insns: 0h
    insns: 0h
    insns: 0h
    insns: 0h
    insns: Eh

实际指令是 0xF所以其他的nop掉 最后给一个return void 就可以了

这里比较坑的一点是寄存器的数量一定要改,不然的话dex2jar转不了
修复前


throw new VerifyError("bad dex opcode");

protected void onCreate(Bundle paramBundle) {
    JniLib.cV(new Object[] { this, paramBundle, Integer.valueOf(18) });
    throw new VerifyError("bad dex opcode");
}


修复后

public void o() {
    JniLib.cV(new Object[] { this, Integer.valueOf(17) });
    throw new VerifyError("bad dex opcode");
}

protected void onCreate(Bundle paramBundle) {
    super.onCreate(paramBundle);
    requestWindowFeature(1);
    this.a = this;
    c.a(this.a);
}

protected void onDestroy() {
    JniLib.cV(new Object[] { this, Integer.valueOf(19) });
    throw new VerifyError("bad dex opcode");
}



总结 :
1-是用JNI来解释执行opcde的
2-op被替换了,但是 A G 那部分参数寄存器数字是不会变的,因为vmp也需要指定是几个参数,来使用
3-做过java2c的都比较熟悉,对dex的opcode比较熟悉的情况下,联系上下文很容易得到结果
4-这里的op可能被加密了,个人愚见人为这个Op加密不加密无所谓,因为最终实际上是个对应关系  0xff个opcode对应0xff个opcode


 hookjni 可以看到很多输出信息 就是说vmp实际采用的还是 jni来实现

如果要全部都替换掉,需要挨个分析指令,做一个映射表岀来
--------------------------------------------------------------------------------------------------
目前来看还是java2c + arm指令虚拟化应该是比较保险的操作,因为自己写一个解释器,纯自己实现指令,肯定问题非常多,所以指令还是通过Jni来实现的,
但是效率貌似低了些,如果这种方式加上ARM指令虚拟化,分析起来可就难受很多了
------------------------------------------------------------------------------------
样本是以前的版本,目的是为了分析和学习,这里只提供so文件,交流经验,需要样本私聊我


[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!

 

posted on 2026-01-24 16:30  GKLBB  阅读(197)  评论(0)    收藏  举报