frida和逆向的学习

0-前言

之前用frida只晓得使用已经构建好的js,还是系统的学习一下比较好,遇到一些自定义的类也可以自己解决,可以去看大佬们的frida操作手册某加固新版frida检测绕过-trace某当劳 Frida 检测,哔哩哔哩上也有相关课程吾爱破解安卓逆向入门教程《安卓逆向这档事》_哔哩哔哩_bilibili小肩膀出品安卓逆向课程_哔哩哔哩_bilibiliPython入门/Frida逆向 哔哩哔哩_bilibili

写在前言:单纯绕过root的话直接拖入白名单让它检测不到su环境即可

体会就是:只要有执行权限的都避免不了被hook

ai编写unidbg进行分析的化,如果是标准版的话,推荐用两个ai,一个专门处理报错,因为ai给的方法很多都不是标准的

app里的so是我随便找的一个有加壳的,如有不合适的地方请联系我删除,水平有限,记录的东西存在错误请多指教

工具:frida+jadx+ida.pro+unidbg+SoFixer

注:unidbg有很多魔改版

可以先熟悉前端的js逆向再深入app的逆向,有很多相通的地方前端游走

1-Java层hook(py)

js是弱参数类型,java是强参数类型,注意转换

普通方法

1>常规hook:

var 类名 = java.use(包名.类名);

类名.方法名.implementation = 具体function函数

构造方法

1
2
#特征--构造方法
new xxx();

1>常规hook更改$init

var 类名 = java.use(包名.类名);

类名.$init.implementation = 具体function函数

重载方法

1
2
3
4
#特征--多个相同方法不同传入参数类型
public static String test(){xxx};
public static String test(int num){xxx};
public static String test(String num){xxx};

1>常规hook+overload()

var 类名 = java.use(包名.类名);

类名.方法名.overload("int").implementation = 具体function函数

注意Stringjava系统类

String是单独一个在lang包里面的类,可以在jre里面的librt.jar压包里面找到String.class,示例:
类名.方法名.overload("java.lang.String").implementation = 具体function函数

2>传入对象参数

1
2
#传参为自定义的对象
类名1.test(new 对象("xx"))

#重载hook+类路径

类名.方法名.overload("java.lang.String").implementation = 具体function函数

更改入参

注:
1
2
send()比console.log输出多,可以直接输出object的结构
console.log输出稳定,就算输出不了也给你输出一点属性类型
1
2
3
4
5
6
7
8
9
10
var 类名1 = java.use('com.xxx.xxx.类名');
var 对象1 = java.use('com.xxx.xxx.对象');
类名1.方法名.overload("com.xxx.xxx.对象").implementation = function(obj){
#展示
send(obj.get());
#实例化对象
var test = 对象1.$new('xxx')
send(test.get());
return 本身的类型
}

更改属性

1
2
3
4
5
6
7
8
9
send(实例化对象test.name);
#返回的是objec的json结构
console.log(实例化对象test.name);
#返回的是object
console.log(实例化对象test.name.value);
#返回的是值
#直接赋值
object.name.value='test';
#更改为字符
反射

可以反射给其他语言例如C++来操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#类的构造器
var clazz = Java.use('java.lang.Class');
类名1.方法名.overload("com.xxx.xxx.对象").implementation = function(obj){
#利用java.cast得到类id
#getDeclaredField('num');得到字段id
var 类的id==>test1 = Java.cast(对象.getClass(),clazz).getDeclaredField('num');
#参数设置,为可更改
test1.setAccessible(true);
var value = test1.get(实例化对象test);
console.log(value);
#更改属性类型的值
test1.setInt(实例化对象test,更改的int类型的值);
return this.test(实例化对象test);
}

2-实际生产环境

逆向思路与web的js逆向相似,但存在难点如下

①反调试(Process terminated)

app自杀逻辑Native 层(C/C++)

③架构不匹配(直接用真机)

常见知识学习

1>>常见调用栈

android.* 开头的类是 Android 系统自带的 SDK 标准库(系统 UI/框架库)
android 开头的类(例如 com.netease.nis.\*): 这些是 App 自身的代码第三方加固/安全 SDK 的代码

当 App 被“加壳”或集成了安全检测时,你会看到这些特定公司的包名。

  • com.netease.nis.\* / com.netease.secure.\*: 网易易盾
  • com.tencent.StubShell.\* / com.tencent.msdk.\*: 腾讯乐固/MSDK
  • com.secshell.\* / com.secneo.\*: 梆梆加固
  • com.aliyun.nex.\*: 阿里聚安全
  • com.qihoo.util.\* / com.qihoo360.\*: 360 加固
java.lang.*是java的公开接口,有些脚本可以打印出app调用了哪些jdk的类
dalvik.system.VMStack.getThreadStackTrace: Java 必须请求 Android 虚拟机(Dalvik 或 ART) 帮忙;虚拟机底层逻辑 (dalvik.* / libcore.*)

这些属于 Android 运行环境(ART/Dalvik)的最底层。

  • dalvik.system.VMStack: 负责处理虚拟机内部的堆栈,通常出现在调用栈的最顶部(因为你在获取堆栈)。
  • dalvik.system.DexClassLoader: 说明 App 正在动态加载插件或 .dex 文件(很多加固包会用到)。
  • libcore.io.\*: 处理底层的底层文件读写、网络连接。
okhttp3.*常见的第三方功能库

如果调用栈里出现这些,说明正在执行某些具体功能(如网络请求、图片加载):

  • okhttp3.\* / retrofit2.\*: 正在进行网络请求。
  • com.google.gson.\* / com.alibaba.fastjson.\*: 正在解析 JSON 数据。
  • com.bumptech.glide.\*: 正在加载图片。
  • kotlinx.coroutines.\*: 正在执行 Kotlin 协程(异步任务)。
com.miui.*, com.samsung.* 等厂商自定义框架

2>>因果链条

①调用栈的顺序是“后进先出”:最上面的是当前正在执行的方法、最下面的是最初启动的方法

3>>实用手法

根据”1>>”:可以忽略所有 android.*java.* 开头的行

根据”2>>”:可以优先断定在上面的是执行单位,直接hook执行方法

遇到的可能

1>找调用栈

找输出点,再找判断点,实在没办法了再枚举

①从 Java 到 Native

在动代码之前,先观察 App 的静态特征。

  • 查看包名和组件:使用 dumpsys 或相关工具确定当前 Activity。
  • 解压 APK:查看 lib/ 目录下有哪些 .so 文件(判定 Native 逻辑多不多)。
  • 查壳:如果是加固壳(如腾讯、爱加密),直接枚举类是无效的,需要先脱壳(或者找薄弱点顺藤摸瓜)。

java加载.so逻辑

1>>关键词 System.loadLibrary

这是 Java 加载 .so 库的标准 API

2>>在 Android 应用启动时,执行顺序
  • attachBaseContext(最早,甚至此时 Application 对象还没完全初始化好)

  • onCreate

工具

1>>Hook/枚举 Java 类与方法

frida-android-hook: 这是一个综合工具箱,支持追踪类、枚举方法。

Frida-Scripts-Android: 包含 hook_all_methods_classloaderfix.js,可以直接 Hook 指定类下的所有方法。

2>>Hook/枚举 .so 库 (Native) 方法

frida-trace (官方自带)

②从 Native到 Syscall

1>>常用工具

bcc / bpftrace / eCapture

sktrace功能:

  • 类似 ida 指令 trace 功能

  • 统计寄存器变化,辅助分析,并且可能会有字符串产生

层级 技术内容 难度工具 对抗重点
Java 层 业务逻辑、界面、SDK 调用 Dex2Jar, jadx 代码混淆、动态加载
Native 层 (.so) C/C++ 逻辑、加密算法、JNI IDA Pro, Frida 代码膨胀、控制流平坦化 (OLLVM)
系统调用层 (Syscall) read, write, openat, kill strace, seccomp 绕过 libc.so 直接调用内核接口
内核层 (Kernel) 驱动程序、内存管理、进程调度 GDB, KGDB Root 检测、反调试驱动、内存混淆
硬件/固件层 (TEE/TrustZone) 指纹支付、密钥管理、安全启动 硬件协议分析 受信任执行环境 (隔离空间)

2>找关键代码

找到关键点后,直接hook对应的方法输出测试,看是否是关键的判断点

①常用工具

jadx、[DIE][https://github.com/horsicq/DIE-engine/releases]

举例

1>冈x的常见解密逻辑

根据你看到的薄弱点(弹窗/日志/标题/……)找到对应的解密逻辑,解密逻辑示例如下:

这是一种基于循环异或(XOR)算法的对称加密

密钥是:byte[] bArr = {78, 101, 116, ??, ??, ??, ??};

ASCII 码解码为:"Net****" (冈yi)

2>打印技巧

1
2
3
// 打印调用栈,看看是谁在调这个解密函数
console.log("[调用来源]: \n" +
Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));

3-so层简单的动态逆向

代码

工具:

unidbg、ida.pro、SoFixer

1
2
3
4
5
sofixer  -s soruce.so -o fix.so -m 0x0 -d 
-s 待修復的so路徑
-o 修復後的so路徑
-m 內存dump的基地址(16位) 0xABC
-d 輸出debug信息

步骤

1>常见问题和知识

1>>常见知识

DT_SONAME 代表库名
  • 在 Android 中,系统加载器(Linker)识别一个库到底叫什么,不是看它的文件名,而是看它 ELF 头部里 DT_SONAME 这个字段。

tracejs逆向里相似
  • 可以使用Trace进行汇编的导出,但请注意指令级的 Trace (traceCode) 颗粒度太细了,分分钟会爆炸。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
emulator.getMemory().addModuleListener(new ModuleListener() {
@Override
public void onLoaded(Emulator<?> emulator, Module module) {
// 只要加载的库名字包含你的壳文件名
if (module.name.contains("secexe") || module.name.contains("nesec")) {
try {
// 立即重定向 Trace
PrintStream traceStream = new PrintStream(new FileOutputStream("trace_output.txt"), true);
emulator.traceCode(module.base, module.base + module.size).setRedirect(traceStream);
System.out.println(">>> [Success] 已成功为 " + module.name + " 挂载 Trace 指令追踪");
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

奖池还在叠加

③常用调用

自己写的汇编(可以替代很多调用)

fopenopen 打开文件

strstr(查找子串)、strcmp字符串对比

memcmp、内存对比

pthread_create(创建检测线程)

SVC #0exit (退出)

2>>常见问题

①配环境
One.配置启动环境(如 libc.solibdl.so

so层的实现方法也可以问ai,但是可能要改的方法名有很多,unidbg实现方法的接口如下:
unidbg-android/src/main/java/com/github/unidbg/Jni.java

Two.配置缺乏的java环境

下面是问的ai给出的实现方法

模拟项目 Java 实现方式 解决的问题
上下文 (Context) 继承 AbstractJni 并重写 callObjectMethodV getContext(), getPackageName(), getAssets()
签名校验 模拟 getPackageInfo 返回 Signature 绕过 native 层对 App 签名的哈希校验。
设备 ID 模拟 Settings.Secure.getString 获取 android_id, imei 等设备唯一标识。
网络状态 模拟 ConnectivityManager 检测是否连网、是否使用代理(反调试)。
多线程/Handler 实现 LooperThread 相关 JNI 解决部分加固混淆库对异步任务的依赖。

2>记录难点

是应该“欺骗过程”让他自以为正常,还是“修正结果”强行抹平伤痕

1>>检查正常运行的同时检查环境

so层可能会效验的对象

校验对象 校验方式 目的
代码段 (.text) 计算特定内存区域的 CRC32 或 MD5 防止你修改汇编指令(比如把 BNE 改成 BEQ
导入表 (GOT) 检查偏移量是否指向非标准的地址 防止你使用 Inline Hook(Hook 之后地址会跳到你的代码区)
符号表 检查特定的导出符号是否被抹除 防止被静态分析工具轻易识别
文件头 (ELF Header) 检查 e_entry 等字段 防止文件被重新脱壳或重打包
Ⅰ.自校验与反调试逻辑
①检查文件强制一致
1
[File Fake] 拦截到 maps 读取: /proc/self/maps
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private String generateMockMaps() {
StringBuilder sb = new StringBuilder();
for (Module module : emulator.getMemory().getLoadedModules()) {
// 抹除调试器和 Hook 框架痕迹
if (module.name.contains("dobby") || module.name.contains("unidbg")) {
continue;
}
// 模拟代码段 (RX)
sb.append(String.format("%08x-%08x r-xp 00000000 00:00 0 %s\n",
module.base, module.base + module.size, module.getPath()));
// 模拟数据段 (RW) - 很多壳会检查这个是否存在
sb.append(String.format("%08x-%08x rw-p %08x 00:00 0 %s\n",
module.base + module.size, module.base + module.size + 0x1000, module.size, module.getPath()));
}
sb.append("bffff000-c0000000 rw-p 00000000 00:00 0 [stack]\n");
return sb.toString();
}
②分析伪代码
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
// 假设 sub_E30B0 是底层处理函数
extern void sub_E30B0(int a1, void* a2, int a3, ...);

void sub_E2DC8(void* arg0, int arg1, ...) {
// 1. 这里的寄存器保存是由编译器在进入变长参数函数时自动生成的
// 它会将 X2-X7, Q0-Q7 全部压栈,以便后续 va_start 使用

int flags = arg1;
void* context_ptr = arg0;

// 2. 对应 TBZ W2, #6, loc_E2E40
// 检查 arg1 的第 6 位 (0x40)
if (flags & 0x40) {
// 这部分代码在汇编中是在手动初始化类似 va_list 的结构
// 比如设置 gr_offs = -48, vr_offs = -128 等
// 通常见于需要手动转发变长参数的情况
}

loc_E2E40:
// 3. 对应 ORR W2, W2, #0x20000
flags |= 0x20000;

// 4. 对应 MOV W0, #0xFFFFFF9C (即 -100)
// 5. 调用子函数并传递参数
sub_E30B0(-100, context_ptr, flags);

// 6. 函数返回
return;
}
③补全环境
Ⅱ.尝试更改退出逻辑
①内存强制一致

导入java代码里面找到的关键的libxxx.so,强制对其.ELF 魔数校验(memcmp 返回 0 表示两段内存完全一致,也报错)

1
2
3
[libc Monitor] memcmp 调用 | 长度: 4 | LR: libsecexe.so!0xe4410
-> [Data] P1: 7f 45 4c 46 | .ELF
-> [Data] P2: 7f 45 4c 46 | .ELF

报错如下:

ida.pro查找对应的地址分析汇编得知如下:

“内存指纹”:

做一个循环,次数是 64次CMP X19, #0x40,0x40 等于 64)。

  • **BLR X3**:这里的 X3 存放的就是 memcmp 的地址。它在循环调用 memcmp
  • 两次关键比对
    1. **第一处 CBNZ W0, loc_E4430**:如果 memcmp 返回非 0(不相等),就跳走。
    2. **第二处 CBZ W0, loc_E4414**:如果 memcmp 返回 0(相等),就跳到 loc_E4414
  • 返回值意义
    • 跳转到 loc_E4414:你会看到 MOV W0, #1。在 C 语言里,返回 1 通常代表“校验失败/发现异常”。
    • 末尾的 MOV W0, #0:只有完整跑完 64 次循环且没有触发跳转,才会执行到这里,代表“校验通过/安全”。
②退出强制拦截
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 5. 监听 exit (反调试暴毙点) - 强行续命版
Symbol exit = libc.findSymbolByName("exit");
if (exit != null) {
dobby.replace(exit, new ReplaceCallback() {
@Override
public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {
int code = context.getIntArg(0);
long lr = context.getLR();
System.out.printf("\n[Anti-Exit] >>> 拦截到自毁退出请求!Code: %d, LR: %s <<<\n",
code, getLrInfo(lr));

System.out.println("[Anti-Exit] 正在强行拦截 exit,不准退出,尝试让程序继续向下执行...");

// 【关键修改】:返回 HookStatus.RET(emulator, 0)
// 并且【不要】调用 originFunction。
// 这样易盾调用 exit() 后,会直接返回到它下一条指令,就像什么都没发生一样。
return HookStatus.RET(emulator, 0);
}
}, true);
}

会遇到”寄存器污染”技术,就是上述的内存检查*64没有正常通过,就会返回一个异常值,虽然routineAddr == 0 拦截,但会导致非法内存读取

3>实际操作

特性 resolve (IOResolver 接口) addModuleListener (监听器)
触发时机 当 SO 运行中执行 open 系统调用时触发。 .so 文件被加载进内存、还没运行前触发。
层级 系统级 (System Call Level)。 加载级 (Linker Level)。
主要功能 欺骗:告诉 SO 文件在本地哪里(文件重定向)。 修改:在代码执行前进行 Hook、Patch(改汇编)。
生效范围 全局,只要是打开文件的操作都会走这里。 仅限指定的模块加载时触发。

①直接重定向,给我一种透明代理的感觉

1
2
3
4
5
6
var dummy_func = Memory.alloc(Process.pageSize);
Memory.patchCode(dummy_func, Process.pageSize, function (code) {
var cw = new Arm64Writer(code);
cw.putRet(); // 写入 ret 指令
cw.flush();
});

hookjava会报hook异常,但直接hook系统调用无异常

1
frida -Uf xxx.app.com -l do_so_hook.js

③启动前hook异常,完全启动后hook正常

操作:

1
2
// 延迟 5 秒(或者你认为 App 完全加载后的时间)
setTimeout(do_java_hook, 5000);
注意:

如果一个空的js进行hook还会被退出的话需要伪装一下手机的环境了,大概率可能被壳找到了frida的特征

3>>构建脚本

问文本对话型的ai即可