安卓逆向技术

1.frida

核心概念:动态二进制插桩

  • 插桩: 在程序执行流的关键位置插入额外的代码。
  • 动态: 在程序运行时进行插桩,无需修改源代码或重新编译目标程序。
  • 二进制: 直接作用于编译后的可执行文件(二进制文件)。
  • Frida 的实现: Frida 通过将一个小型引擎(frida-core)注入到目标进程(如 Android App, iOS App, Windows 桌面程序, macOS 应用等)中实现。这个引擎在目标进程内部运行一个 JavaScript 运行时环境(V8 引擎),允许你执行 JS 脚本并与目标进程的地址空间交互。

1.1.安装

  • frida是开源工具,直接在github下载即可,版本要求,

  • 在选择安装的版本之前,需要先确定手机的系统和CPU的架构

  • 可以通过以下命令获取CPU架构

    1
    2
    PS C:\Users\GaoMu> adb shell getprop ro.product.cpu.abi
    arm64-v8a
  • 根据不同的安装系统版本适配不同的frida版本

  • 同时我的系统是安卓11,因此我选择下载frida16的这个版本

  • 使用adb push 推送解压后的文件到手机目录

1
2
3
4
5
6
7
8
9
10
PS F:\Temp> adb push .\frida-server-16.7.19-android-arm64 /data/local/tmp/workplace
.\frida-server-16.7.19-android-arm64: 1 file pushed, 0 skipped. 108.7 MB/s (53702368 bytes in 0.471s)

# 在手机目录中执行
blueline:/data/local/tmp/workplace # chmod +x frida-server-16.7.19-android-arm64
blueline:/data/local/tmp/workplace # ls -lh
total 26M
-rwxrwx--x 1 root everybody 51M 2025-06-23 22:12 frida-server-16.7.19-android-arm64
# 启动frida服务端
blueline:/data/local/tmp/workplace # ./frida-server-16.7.19-android-arm64
  • Windows客户端安装

  • 使用pip 安装frida客户端,需要与在手机上安装的服务端一致。

1
2
pip install frida==16.7.19 -i https://mirrors.aliyun.com/pypi/simple/
pip install frida-tools==13.7.1 -i https://mirrors.aliyun.com/pypi/simple/
  • Windwos客户端连接测试 以下显示为安装好的App
1
2
3
4
5
6
7
8
9
10
PS C:\Users\GaoMu> frida-ps -Ua
PID Name Identifier
---- ------------ ------------------------------------
2904 AudioFX org.lineageos.audiofx
5172 Aurora Store com.aurora.store
5183 F-Droid org.fdroid.fdroid
3472 Gboard com.google.android.inputmethod.latin
5553 Magisk com.topjohnwu.magisk
5471 日历 org.lineageos.etar
3652 设置 com.android.settings
  • 安卓代码提示插件
1
npm install @types/frida-gum --save

安装证书

Android10以上安装证书需要使用openssl计算hash,然后命名为hash.0 移动到android的/system/etc/security/cacerts目录下,同时添加对等权限。如果遇到只读文件系统问题,可以使用MT文件管理工具进行移动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 生成哈希并重命名hash.0
┌──(kali㉿kali)-[~/Desktop/workplace]
└─$ openssl x509 -subject_hash_old -in Charles.pem
3b5566cf
-----BEGIN CERTIFICATE-----
....
mv Charles.pem 3b5566cf.0
# 或者 openssl x509 -inform PEM -subject_hash_old -in myCA.crt

# 推送证书到设备
adb root
adb remount
adb push 3b5566cf.0 /system/etc/security/cacerts/
adb shell chmod 644 /system/etc/security/cacerts/3b5566cf.0
adb reboot

1.2.基础命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
用法: frida [选项] 目标

位置参数:
args 额外参数和/或目标

选项:
-h, --help 显示此帮助信息并退出
-D ID, --device ID 连接到指定 ID 的设备
-U, --usb 连接到 USB 设备
-R, --remote 连接到远程 frida-server
-H HOST, --host HOST 连接到 HOST 上的远程 frida-server
--certificate CERTIFICATE
与 HOST 使用 TLS 通信,并验证提供的 CERTIFICATE(证书)
--origin ORIGIN 连接到远程服务器时,设置“Origin”请求头为 ORIGIN
--token TOKEN 使用 TOKEN 向 HOST 进行身份验证
--keepalive-interval INTERVAL
设置保活间隔(秒),0 表示禁用(默认为 -1,根据传输方式自动选择)
--p2p 与目标建立点对点(peer-to-peer)连接
--stun-server ADDRESS
使用 --p2p 时,设置要使用的 STUN 服务器地址 ADDRESS
--relay address,username,password,turn-{udp,tcp,tls}
为 --p2p 添加中继(relay)服务器
-f TARGET, --file TARGET
生成(spawn)文件 TARGET(通常是可执行文件)
-F, --attach-frontmost
附加到最前端应用程序
-n NAME, --attach-name NAME
附加到名称为 NAME 的进程
-N IDENTIFIER, --attach-identifier IDENTIFIER
附加到标识符为 IDENTIFIER 的进程(如 bundle ID)
-p PID, --attach-pid PID
附加到进程 ID 为 PID 的进程
-W PATTERN, --await PATTERN
等待匹配 PATTERN 的进程生成
--stdio {inherit,pipe}
生成进程时的标准输入/输出行为(默认为“inherit”继承)
--aux option 设置生成进程时的辅助选项(aux option),例如 “uid=(int)42”(支持的类型:string, bool, int)
--realm {native,emulated}
要附加到的执行域(原生域 / 模拟域)
--runtime {qjs,v8} 要使用的脚本运行时引擎
--debug 启用 Node.js 兼容的脚本调试器
--squelch-crash 如果启用,不会将崩溃报告输出到控制台
-O FILE, --options-file FILE
包含额外命令行选项的文本文件
--version 显示程序版本号并退出
-l SCRIPT, --load SCRIPT
加载脚本文件 SCRIPT
-P PARAMETERS_JSON, --parameters PARAMETERS_JSON
以 JSON 格式提供参数(与 Gadget 相同)
-C USER_CMODULE, --cmodule USER_CMODULE
加载 C 模块(CModule) USER_CMODULE
--toolchain {any,internal,external}
从源代码编译时使用的 CModule 工具链
-c CODESHARE_URI, --codeshare CODESHARE_URI
加载代码共享 URI CODESHARE_URI
-e CODE, --eval CODE 执行(evaluate)代码片段 CODE
-q 安静模式(无提示符),并在执行完 -l 和 -e 后退出
-t TIMEOUT, --timeout TIMEOUT
在安静模式下,等待 TIMEOUT 秒后终止
--pause 在生成程序后,保持主线程暂停状态
-o LOGFILE, --output LOGFILE
输出到日志文件 LOGFILE
--eternalize 在退出前持久化脚本(eternalize the script)
--exit-on-error 在 SCRIPT 中遇到任何异常后以代码 1 退出
--kill-on-exit Frida 退出时杀死生成的程序
--auto-perform 自动将输入的代码包装在 `Java.perform` 中(用于 Java 交互)
--auto-reload 启用对提供的脚本和 C 模块的自动重载(默认开启,未来版本将变为必需)
--no-auto-reload 禁用对提供的脚本和 C 模块的自动重载

1.3.操作模式

Frida 提供了两种核心操作模式来注入目标进程:Attach(附加)和 Spawn(生成)。这两种模式各有特点,适用于不同场景。

1.3.1.Attach模式

  • 附加到已经运行的进程
1
2
3
4
graph LR
A[运行中的目标进程] --> B[Frida 注入]
B --> C[附加脚本]
C --> D[操作进程]
  • 命令操作
1
2
3
4
# 基本语法
frida -U -p <PID> -l script.js
# 或
frida -U -n "Process Name" -l script.js

1.3.2.Spawn模式

  • Frida 启动并暂停目标进程,注入脚本后继续执行
1
2
3
4
graph LR
A[Frida 启动进程] --> B[暂停进程]
B --> C[注入脚本]
C --> D[恢复执行]

1.3.3.两者对比

特性 Attach 模式 Spawn 模式
目标状态 已运行 未启动
执行时机 运行时注入 启动前注入
资源占用 中等
初始化Hook 可能错过 完整捕获
反调试绕过 有限 有效
适用场景 动态分析 静态分析
启动命令 -p PID-n "Name" -f package
热重载 支持 不支持

1.3.4.自定义端口启动

  • frida默认端口号为27043,由于一些应该可能会检查frida的默认端口,导致hook失败,因此可以使用自定义端口来进行hook调试。

    具体操作步骤如下:

  • 自定义端口启动server

1
blueline:/data/local/tmp/workplace # ./frida-server-16.7.19-android-arm64  -l 0.0.0.0:9999
  • 使用adb端口转发将Android的9999端口映射到本地客户端主机。
1
2
PS C:\Users\GaoMu\Desktop\workplace> adb forward tcp:9999 tcp:9999
9999
  • 指定本地端口进行hook
1
frida -H 127.0.0.1:8888 -f com.example.app

1.4.frida常用关键词解析

Frida 脚本常用关键词深度解析

Frida 脚本使用 JavaScript API 与目标进程交互,下面是核心关键词的详细解析和使用示例:

1. Java 对象 (Android Java 交互)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 关键方法
Java.perform(() => { /* 安全执行Java操作 */ });
Java.choose("com.example.Class", { /* 查找实例 */ });
Java.use("com.example.Class") // 获取类引用 等同于JAVA中的Class.forName() 反射调用
//如果是需要加载内部类的化需要使用'$'符引用例如
java.use("com.example.Class$InnerClass")
Java.enumerateLoadedClasses() // 枚举已加载类

// 示例:Hook Activity.onCreate
Java.perform(() => {
const Activity = Java.use("android.app.Activity");
Activity.onCreate.implementation = function(bundle) {
console.log("Activity created!");
this.onCreate(bundle);
};
});

2. Interceptor (函数拦截)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 关键方法
Interceptor.attach(targetAddress, { /* 回调 */ });
Interceptor.replace(targetAddress, replacementFunc);
Interceptor.detachAll();

// 示例:拦截 libc 的 open 函数
const openPtr = Module.getExportByName(null, "open");
Interceptor.attach(openPtr, {
onEnter: function(args) {
this.path = args[0].readUtf8String();
console.log(`Opening file: ${this.path}`);
},
onLeave: function(retval) {
console.log(`Returned fd: ${retval}`);
}
});

3. Module (模块操作)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 关键方法
Module.load("libtarget.so"); // 动态加载
Module.findBaseAddress("libtarget.so"); // 查找基址
Module.enumerateImports("libtarget.so"); // 枚举导入
Module.findExportByName("libc.so", "printf"); // 查找导出

// 示例:扫描模块导出
Module.enumerateExports("libart.so", {
onMatch: function(exp) {
if (exp.name.indexOf("JNI") !== -1) {
console.log(`Found JNI export: ${exp.name}`);
}
},
onComplete: function() {}
});

4. Process (进程控制)

1
2
3
4
5
6
7
8
9
10
11
// 关键方法
Process.id; // 当前进程ID
Process.arch; // 架构 arm/arm64/x64
Process.enumerateModules(); // 枚举模块
Process.enumerateThreads(); // 枚举线程
Process.getCurrentThreadId(); // 获取当前线程ID

// 示例:枚举所有模块
Process.enumerateModules().forEach(mod => {
console.log(`Module: ${mod.name} Base: ${mod.base}`);
});

5. Memory (内存操作)

1
2
3
4
5
6
7
8
9
10
11
12
// 关键方法
Memory.alloc(size); // 分配内存
Memory.protect(address, size, protection); // 修改保护
Memory.scan(address, size, pattern, callbacks); // 内存扫描
Memory.readByteArray(address, length); // 读取字节数组

// 示例:读取字符串
const strPtr = ptr(0x1234);
console.log(`String value: ${strPtr.readUtf8String()}`);

// 示例:修改内存保护
Memory.protect(ptr(0x4000), 4096, 'rwx');

6. ptr (指针操作)

1
2
3
4
5
6
7
8
9
// 关键方法
ptr("0x1234"); // 创建指针
ptr.add(offset); // 指针加法
ptr.sub(offset); // 指针减法

// 示例:指针运算
const base = Module.findBaseAddress("libtarget.so");
const targetFunc = base.add(0x1234);
console.log(`Target function at: ${targetFunc}`);

7. NativeFunction (调用原生函数)

1
2
3
4
5
6
7
8
// 创建原生函数指针
const openPtr = Module.getExportByName(null, "open");
const open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);

// 调用原生函数
const path = Memory.allocUtf8String("/etc/passwd");
const fd = open(path, 0);
console.log(`File descriptor: ${fd}`);

8. NativeCallback (创建回调函数)

1
2
3
4
5
6
7
8
9
// 创建回调函数
const callback = new NativeCallback(function(arg1, arg2) {
console.log(`Callback called with: ${arg1}, ${arg2}`);
return 0;
}, 'int', ['int', 'int']);

// 设置回调
const setCallback = Module.getExportByName("libtarget.so", "set_callback");
setCallback(callback);

9. Thread (线程操作)

1
2
3
4
5
6
7
8
9
10
11
// 关键方法
Thread.backtrace(context); // 获取调用栈
Thread.sleep(seconds); // 线程休眠

// 示例:获取当前线程调用栈
Interceptor.attach(targetFunc, {
onEnter: function(args) {
console.log("Backtrace:\n" +
Thread.backtrace(this.context).map(DebugSymbol.fromAddress).join("\n"));
}
});

10. console (日志输出)

1
2
3
4
console.log("Info message"); // 信息日志
console.warn("Warning!"); // 警告日志
console.error("Error!"); // 错误日志
console.debug("Debug info"); // 调试日志

11. setImmediate / setTimeout (定时操作)

1
2
3
4
5
6
7
8
9
// 立即执行
setImmediate(() => {
console.log("This runs immediately after script load");
});

// 延迟执行
setTimeout(() => {
console.log("This runs after 2 seconds");
}, 2000);

12. recv / send (进程通信)

1
2
3
4
5
6
7
8
// 脚本中接收消息
recv("command", (data) => {
console.log("Received command:", data.payload);
send({ result: "success" });
});

// Python 端发送消息
script.post({"type": "command", "payload": "start_monitoring"})

13. Frida 核心对象

1
2
3
Frida.heapSize; // 获取堆大小
Frida.arch; // 当前架构
Frida.ptrSize; // 指针大小

14. Stalker (指令级跟踪)

1
2
3
4
5
6
7
8
9
10
11
12
13
// 跟踪指令流
Stalker.follow(threadId, {
events: {
call: true, // 跟踪调用指令
ret: false // 不跟踪返回指令
},
onReceive: function(events) {
// 处理事件
}
});

// 停止跟踪
Stalker.unfollow(threadId);

15. File (文件操作)

1
2
3
// 读写文件
const data = File.readAllBytes("/path/to/file");
File.writeAllBytes("/path/to/output", new Uint8Array([0x01, 0x02]));

实战模式:Hook 链式调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Java.perform(() => {
// Hook 类方法
const StringBuilder = Java.use("java.lang.StringBuilder");
StringBuilder.toString.implementation = function() {
const result = this.toString();
console.log("StringBuilder content:", result);
return result;
};

// Hook JNI 函数
const env = Java.vm.getEnv();
const getStringUTFChars = env.getStringUTFChars;
env.getStringUTFChars.implementation = function(str, isCopy) {
const result = getStringUTFChars.call(this, str, isCopy);
console.log("JNI String:", result.readUtf8String());
return result;
};
});

调试技巧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1. 调试模式启用
console.log("Debug mode enabled");
DebugSymbol.enabled = true;

// 2. 异常捕获
Process.setExceptionHandler(function(exception) {
console.log("Caught exception:", exception);
return true; // 阻止崩溃
});

// 3. 内存访问检查
Memory.accessWatch([ptr(0x1234)], {
onAccess: function(details) {
console.log("Memory accessed at", details.address);
}
});

常用代码片段

获取当前 Activity
1
2
3
4
5
6
Java.perform(() => {
const ActivityThread = Java.use("android.app.ActivityThread");
const activity = ActivityThread.currentActivityThread()
.getActivities().values().toArray()[0];
console.log("Current activity:", activity);
});
修改返回值
1
2
3
4
5
Java.use("com.example.Crypto").encrypt.implementation = function(data) {
const result = this.encrypt(data);
// 修改返回值为固定值
return "modified_result";
};
主动调用方法
1
2
3
4
5
Java.perform(() => {
const System = Java.use("java.lang.System");
const currentTime = System.currentTimeMillis();
console.log("Current time:", currentTime.toInt32());
});

1.5.数据类型对比

1.5.1.基本数据类型对比

Java 类型 Frida JavaScript 表示 转换方法/说明
boolean boolean 直接对应 true/false
byte number 自动转换
char number (Unicode 值) 使用 String.fromCharCode() 转字符
short number 自动转换
int number 自动转换
long numberInt64 大数需用 new Int64(value)
float number 自动转换
double number 自动转换
void undefined 无返回值

1.5.2.引用数据类型对比

Java 类型 Frida JavaScript 表示 转换方法/说明
String string 自动转换
Object Java.Object 包装器 通过 Java.cast() 处理
数组 (如 int[]) Java.Array 对象 使用 Java.array() 创建
自定义类实例 Java.Wrapper 对象 通过 Java.use() 获取类引用
List/Map Java.Objects 包装器 特殊方法处理
null null 直接对应

1.6.frida hook

1.6.1.hook示例

  • jdx反编译代码片段
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
package com.gaomu.fridatest.utils;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/* loaded from: classes.dex */
public class AES {
public static String aesEncrypt(String str, String str2, String str3, String str4) throws Exception {
validateHex("密钥", str3, 32);
SecretKeySpec secretKeySpec = new SecretKeySpec(hexToBytes(str3), "AES");
Cipher cipher = Cipher.getInstance(getTransformation(str));
if (str.equalsIgnoreCase("ECB")) {
cipher.init(1, secretKeySpec);
} else {
validateHex("IV", str4, 32);
byte[] bArrHexToBytes = hexToBytes(str4);
if (str.equalsIgnoreCase("GCM")) {
byte[] bArr = new byte[12];
System.arraycopy(bArrHexToBytes, 0, bArr, 0, 12);
cipher.init(1, secretKeySpec, new GCMParameterSpec(128, bArr));
} else {
cipher.init(1, secretKeySpec, new IvParameterSpec(bArrHexToBytes));
}
}
return Base64.getEncoder().encodeToString(cipher.doFinal(str2.getBytes(StandardCharsets.UTF_8)));
}
.....
}
  • hook示例
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
// hookAes函数用于hook目标应用中的AES加密方法
function hookAes(){
// 获取目标类com.gaomu.fridatest.utils.AES的Java对象
var AES = Java.use("com.gaomu.fridatest.utils.AES")
// 重写aesEncrypt方法的实现
AES.aesEncrypt.implementation = function (mode, plainText, keyHex, ivHex) {
// 打印传入的参数,便于分析加密过程
console.log("mode:" + mode);
console.log("plainText:" + plainText);
console.log("keyHex:" + keyHex);
console.log("ivHex:" + ivHex);
// 调用原始的aesEncrypt方法,获取加密结果
var res = this.aesEncrypt(mode, plainText, keyHex, ivHex)
// 打印加密结果
console.log(res)
// 返回加密结果
return res;
}
}

// main函数作为入口,确保在Java环境准备好后执行hook
function main() {
Java.perform(function () {
hookAes()
})
}

// 立即执行main函数,开始hook
setImmediate(main)

//服务端frida启动后,客户端使用frida -UF -l ./hook1.js 进行hook,执行方法后,就可以在控制台打印出参数信息。

1.6.2.重载方法调用示例

  • 需要调用同名函数不同得方法需要使用overload,来指定方法的参数类型
  • 例如调用四字参数均为字符串类型的aesEncrypt()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function hookAes(){
// 获取目标类com.gaomu.fridatest.utils.AES的Java对象
var AES = Java.use("com.gaomu.fridatest.utils.AES")
// 重写aesEncrypt方法的实现
AES.aesEncrypt.overload('java.lang.String','java.lang.String','java.lang.String','java.lang.String').implementation = function (mode, plainText, keyHex, ivHex) {
// 打印传入的参数,便于分析加密过程
console.log("tip:" + "use have four args of reload method");
var res = this.aesEncrypt(mode, plainText, keyHex, ivHex)
// 打印加密结果
console.log(res)
// 返回加密结果
return res;
}
}
  • hook所有同名方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取getOver方法的所有重载方法的数量
var len = utils.getOver.overloads.length;
// 循环遍历所有重载方法
for (var i=0; i<len; i++) {
utils.getOver.overloads[i].implementation = function(){
//获取所有参数并输出
for (var a = 0; a < arguments.length; a++) {
console.log(a + '---' + arguments[a])
}

//调用原始方法
return this.getOver.apply(this, arguments)
}
}

1.6.3.主动调用方法

  • 每次改一次文件都会主动执行一次
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
// 主动调用md5方法
function initMD5() {
var MD5 = Java.use("com.gaomu.fridatest.utils.MD5");
var md5hash = MD5.md5Hash("adminx");
console.log("md5: " + md5hash);
}

// main函数作为入口,确保在Java环境准备好后执行hook
function main() {
Java.perform(function () {
initMD5()
})
}

// 立即执行main函数,开始hook
setImmediate(main)

//主动调用获取实例对象
function useDesSecurityConstructor() function() {
const DesSecurity = Java.use("com.dodonew.online.util.DesSecurity");
//获取参数为两个字符串类型的构造方法
const ctorOverload = DesSecurity.$overload('java.lang.String', 'java.lang.String');
//使用构造方法new一个对象
const ds = ctorOverload.$new(b, c);//获取一个DesSecurity实例对象
}

1.6.4.hook调用实例方法

  • 自定义实例调用
1
2
3
4
5
6
7
8
// 获取class对象
var moneyClass = Java.use("com.gaomu.Money");
// 新建一个实例对象
var money = moneyClass.$new();
// 直接调用其方法
var res = money.getInfo();
// 获取实例中的静态变量值 .value获取
var value = money.falg.value;
  • RPC全局实例调用
    • onMatch方法是frida脚本用于匹配特定条件时触发的回调函数。当满足特定的条件时,比如某个函数被调用或者某个字符串被传递等,onMatch方法可以执行相应的操作。
    • onComplete方法是在Frida脚本执行完毕时触发的回调函数。通常用于在所有操作完成后做一些清理工作或输出总结信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function demo(){

Java.choose("com.gaomu.Money", {
//找到所有为com.gaomu.Money类的对象,每当调用这个对象时都会执行一次onMatch函数
onMatch: function(obj){
//obj 即为匹配到的Money对象
var name = obj.name.value //获取非静态变量 如果字段名和方法名相同时需要在前面加上_
var num = obj.num.value //获非取静态变量
console.log(obj.getInfo())
},
onComplete: function(){
console.log("内存中Money操作完毕")
}
});
}

1.6.5.hook构造方法

1
2
3
4
5
6
7
8
9
10
// 获取class对象
var money = Java.use("com.gaomu.Money")
// hook构造参数类型为java.lang.String和int的构造方法
money.$init.overload('java.lang.String', 'int').implementation = function(str, num) {
console.log(str, num)
// 执行原本逻辑
this.$init(str, num)
}


1.6.6.hook内部类

1
var innerClass = Java.use('com.gaomu.Money$InnerClass')

1.6.7.枚举所有的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
console.log(Java.enumerateLoadedClassesSync().join('\n'));

var utils = Java.use("com.gaomu.Utils");
var methods = utils.class.getDeclaredMethods();
for (var i=0; i<methods.length; i++){
console.log(methods[i].getName());
}

//获取所有构造方法
var constructors = utils.class.getDeclaredConstructors();
//遍历构造方法
console.log(constructors);
for (var i = 0; i < constructors.length; i++) {
var constructor = constructors[i];
console.log(constructor.toString());
console.log(constructor.getName());
}

//获取所有字段
var fields = money.class.getDeclaredFields();

1.6.8.hook map对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//hashMap 
//直接打印,如果配置的hashMap键值对是String类型的话,可以输入字符串内容
console.log(hashMap);
//迭代遍历获取键值对
var key = hashMap.keySet();
var it = key.iterator();
while(it.hasNext()){
var keyStr = it.next();
var valueStr = hashMap.get(keyStr);
console.log("key->" + keystr + " | val->" + valSuetr);
}

//修改hashMap中的值
hashMap.put("name", "gaomu");

1.6.9.hook okhttp

  • 如果app使用了okhttp的依赖进行发包,那么就可以使用这个方法进行hook
1
2
3
4
5
6
7
8
9
10
//hook 请求地址
function hookOkhttpURL() {
var Builder = Java.use('okhttp3.Request$Builder');
//url有三种重载方法,分别是java.lang.String/java.net.URL/okhttp3.HttpUrl,大多数请求都是使用的HttpUrl类型参数
Builder.url.overload('okhttp3.HttpUrl').implementation = function(a) {
console.log('a: ' + a);
var res = this.url(a);
return res;
}
}
  • hook 请求和响应
1
2
3
4
5
6
7
8
var okHttpClinet = Java.use("okhttp3.OkHttpClient");
OkHttpClient.newCall.overload("okhttp3.Request").implementation = function (request) {
console.log("HTTP Reqeust -> " + request.url().toString());
var call = this.newCall(request); //获取新的call对象
var response = call.execute(); //调用call对象的execute发送请求 并获取响应
console.log("HTTP Response -> " + response.body().string());
return call;
}
  • hook 请求头
1
2
3
4
5
6
7
8
9
var okHttpClinet = Java.use("okhttp3.Request$Builder");
//hook Builder类的addHeader方法
Builder["addHeader"].implementation = function(str, str2) {
console.log("key: " + str);
console.log("val: " + str2);
//showStacks()
var result = this["addHeader"](str, str2);
return result;
}

1.6.10.输出当前堆栈信息

1
2
3
4
5
6
7
function showStacks() {
Java.perform(function() {
console.log(Java.use("android.util.Log").getStackTraceString(
Java.use("java.lang.Throwable").$new()
))
})
}

1.7.实战hook 嘟嘟牛

1.7.1.反编译的APK包

  • 根据反编译的APK源码包,发现大多数代码都是经过混淆的,不具有可读性。
  • 仅部分具有可读性。

image-20250628003501716

  • 因此我们接下来的直接进行盲 hook。

1.7.2.hook 记录

  • 首先我们通过对HashMap类进行hook。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
setImmediate(main)

function main() {
Java.perform(function () {
hookHashMap();
})
}

function hookHashMap() {
var hashMap = Java.use("java.util.HashMap");
hashMap.put.implementation = function (a, b){
console.log("print out ---------->",a,b);
return this.put(a,b);
}
}
  • 触发登陆请求后发现关键信息。

image-20250628003951425

  • 因此我们通过在hashMap类里当key等于Encrypt时,输出其堆栈信息。

  • 修改hashMap逻辑,输出堆栈信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hookHashMap() {
var hashMap = Java.use("java.util.HashMap");
hashMap.put.implementation = function (a, b){
if (a == "Encrypt") {
console.log("print out ---------->",a,b);
showStacks();
}
return this.put(a,b);
}
}

function showStacks() {
Java.perform(function() {
console.log(Java.use("android.util.Log").getStackTraceString(
Java.use("java.lang.Throwable").$new()
));
})
}
  • 在输出的堆栈信息中我们并没用发现我们感兴趣的加密函数。

image-20250628005247626

  • 于是,我换了一个思路,获取这个包下所有的类,看有什么我感兴趣的类。
1
2
3
4
5
6
7
8
9
10
11
12
13
function hookClass() {
// 方法1:使用同步过滤
const targetPackage = "com.dodonew.online";
const loadedClasses = Java.enumerateLoadedClassesSync();

// 过滤目标包下的类
const filteredClasses = loadedClasses.filter(className =>
className.startsWith(targetPackage + ".")
);

console.log(`[+] 找到 ${filteredClasses.length} 个类:`);
console.log(filteredClasses.join('\n'));
}

image-20250628005506712

  • 我们再尝试获取该类的所有方法和字段。
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
function hookMethod() {
var DesSecurity = Java.use("com.dodonew.online.util.DesSecurity");

// 获取类的 Class 对象
var clazz = DesSecurity.class;

// 正确获取所有声明的方法
var methods = clazz.getDeclaredMethods();
console.log("\n[+] DesSecurity 类方法列表:");
for (var i = 0; i < methods.length; i++) {
// 获取方法名和参数类型
var method = methods[i];
var methodName = method.getName();
var paramTypes = method.getParameterTypes().map(t => t.getSimpleName());
console.log(` - ${methodName}(${paramTypes.join(", ")})`);
}

// 正确获取所有声明的字段
var fields = clazz.getDeclaredFields();
console.log("\n[+] DesSecurity 类字段列表:");
for (var i = 0; i < fields.length; i++) {
// 获取字段名和类型
var field = fields[i];
var fieldName = field.getName();
var fieldType = field.getType().getSimpleName();
console.log(` - ${fieldType} ${fieldName}`);
}
}
  • 发现了一个加密函数,这个加密函数没有密钥,那么我们完全可以想象一个有密钥的加密函数调用了这个函数。

image-20250628010848082

  • 因此我们hook这个方法,并且输出调用这个方法时的堆栈信息,查找是否有其他加密函数。
1
2
3
4
5
6
7
8
function hookDesSecurity() {
var DesSecurity = Java.use("com.dodonew.online.util.DesSecurity");
DesSecurity.encrypt64.implementation = function (byte){
showStacks();
console.log("encrypt64 execute!!!!!");
return this.encrypt64(byte);
}
}

image-20250628011138394

  • 接下来我们hook一下encodeDesMap方法。

  • 通过deepseek编写以下hook获取该方法所有的重载方法和对应参数类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
function hookEncodeDesMap() {
try {
const RequestUtil = Java.use("com.dodonew.online.http.RequestUtil");

// 获取 encodeDesMap 方法的所有重载
const encodeDesMapOverloads = RequestUtil.encodeDesMap.overloads;

console.log("\n===== encodeDesMap 方法重载分析 =====");
console.log(`[+] 共找到 ${encodeDesMapOverloads.length} 个重载方法`);

if (encodeDesMapOverloads.length === 0) {
console.log("[!] 未找到 encodeDesMap 方法,可能是方法名错误或类未加载");
return;
}

// 遍历所有重载
encodeDesMapOverloads.forEach((overload, index) => {
console.log(`\n[重载 #${index + 1}]`);

// 获取参数类型
const paramTypes = overload.argumentTypes;
console.log(` 参数数量: ${paramTypes.length}`);

// 打印每个参数类型
paramTypes.forEach((paramType, paramIndex) => {
try {
const className = paramType.className;
const simpleName = className.split('.').pop();
console.log(` 参数 ${paramIndex + 1}: ${simpleName} (${className})`);
} catch (e) {
console.log(` 参数 ${paramIndex + 1}: <无法获取类型信息>`);
}
});

// 获取返回类型
try {
const returnType = overload.returnType.className;
const simpleReturnType = returnType.split('.').pop();
console.log(` 返回类型: ${simpleReturnType} (${returnType})`);
} catch (e) {
console.log(" 返回类型: <未知>");
}

// 实际 Hook 该重载
overload.implementation = function() {
console.log(`\n[encodeDesMap 重载 #${index + 1} 被调用]`);

// 打印参数值
if (arguments.length > 0) {
console.log(" 调用参数:");
for (let i = 0; i < arguments.length; i++) {
const arg = arguments[i];

// 特殊处理常见类型
if (arg === null) {
console.log(` 参数 ${i + 1}: null`);
} else if (typeof arg === 'object' && arg.getClass) {
try {
// 尝试获取对象的类名
const className = arg.getClass().getName();
console.log(` 参数 ${i + 1}: ${className} 对象`);

// 如果是 Map 类型,尝试打印内容
if (className.endsWith('Map')) {
try {
const mapSize = arg.size();
console.log(` Map 大小: ${mapSize}`);

if (mapSize > 0) {
const entrySet = arg.entrySet();
const iterator = entrySet.iterator();

console.log(" Map 内容:");
while (iterator.hasNext()) {
const entry = iterator.next();
const key = entry.getKey();
const value = entry.getValue();
console.log(` ${key} = ${value}`);
}
}
} catch (e) {
console.log(" 无法解析 Map 内容");
}
}
} catch (e) {
console.log(` 参数 ${i + 1}: 对象 (无法获取类信息)`);
}
} else {
console.log(` 参数 ${i + 1}: ${arg} (${typeof arg})`);
}
}
} else {
console.log(" 无参数");
}

// 调用原方法
const result = this.encodeDesMap.apply(this, arguments);

// 打印返回值
if (result !== null && result !== undefined) {
console.log(" 返回结果:", result.toString());
} else {
console.log(" 返回结果: null");
}

return result;
};
});

console.log("\n[+] Hook 已设置,等待方法调用...");

} catch (e) {
console.error("[!] 分析失败:", e.message);
}
}
  • 控制台输出如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
===== encodeDesMap 方法重载分析 =====
[+] 共找到 2 个重载方法

[重载 #1]
参数数量: 3
参数 1: String (java.lang.String)
参数 2: String (java.lang.String)
参数 3: String (java.lang.String)
返回类型: String (java.lang.String)

[重载 #2]
参数数量: 4
参数 1: String (java.lang.String)
参数 2: String (java.lang.String)
参数 3: String (java.lang.String)
参数 4: String (java.lang.String)
返回类型: Map (java.util.Map)

[+] Hook 已设置,等待方法调用...

  • 有两个重载方法,具体调用的哪个重载方法我们不得而知,但是我们可以直接hook尝试。
  • 先hook第一个重载方法,也就是3个String参数的方法。
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
setImmediate(main)

function main() {
Java.perform(function () {
hookEncodeDesMap();
hookHashMap();
})
}

function hookEncodeDesMap() {
const RequestUtil = Java.use("com.dodonew.online.http.RequestUtil");
RequestUtil.encodeDesMap.overload("java.lang.String","java.lang.String","java.lang.String").implementation = function (a,b,c) {
console.log("a--------->", a);
console.log("b--------->", b);
console.log("c--------->", c);
return this.encodeDesMap(a, b, c);
}
}


function hookHashMap() {
var hashMap = Java.use("java.util.HashMap");
hashMap.put.implementation = function (a, b){
if (a == "Encrypt") {
console.log("print out ---------->",a,b);
//showStacks();
}
return this.put(a,b);
}
}
  • 通过以上代码hook我们成功获得以下输出
1
2
3
4
5
6
7
8
a---------> {"equtype":"ANDROID","loginImei":"Androidb40a5c0f41fef63f","sign":"C30222B6DF6FF2BCFF34A6E412BE0CA6","timeStamp":"1751045128588","userPwd":"1314520","username":"114614"}
b---------> 65102933
c---------> 32028092
print out ----------> Encrypt NIszaqFPos1vd0pFqKlB42Np5itPxaNH//FDsRnlBfgL4lcVxjXii+gDPsXiVF3VBzVAsMjQKraf
7KdDY8QjXCrDyjbdep0QyDb+fluS62QBfyio5TUMRlhfetK0JJUAEaNIwv7SujX6+4EN/CAunlQe
cDNagp872sabGkggHRlKAcNuPr0wcSqiY9K9GHk/IrHEw6rClCrfZjYolrq0MXo5I9mSpJA3NEQH
Rp5KRms=

  • 那么我们不难猜测a字符串是明文,b、c其中一个是密钥,一个是iv。

  • 那么我们复现其加密逻辑,代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
function hookDesSecurityContructor() {
const RequestUtil = Java.use("com.dodonew.online.http.RequestUtil");
//反射获取指定参数类型的方法
const encodeDesMapMethod = RequestUtil.encodeDesMap.overload("java.lang.String","java.lang.String","java.lang.String");
//反射调用RequestUtil类中的encodeDesMapMethod方法。
var cipher = encodeDesMapMethod.call(RequestUtil, "{\"equtype\":\"ANDROID\",\"loginImei\":\"Androidb40a5c0f41fef63f\",\"sign\":\"C30222B6DF6FF2BCFF34A6E412BE0CA6\",\"timeStamp\":\"1751045128588\",\"userPwd\":\"1314520\",\"username\":\"114614\"}","65102933", "32028092");
console.log("cipher: ", cipher);
}
//输出cipher: NIszaqFPos1vd0pFqKlB42Np5itPxaNH//FDsRnlBfgL4lcVxjXii+gDPsXiVF3VBzVAsMjQKraf
//7KdDY8QjXCrDyjbdep0QyDb+fluS62QBfyio5TUMRlhfetK0JJUAEaNIwv7SujX6+4EN/CAunlQe
//cDNagp872sabGkggHRlKAcNuPr0wcSqiY9K9GHk/IrHEw6rClCrfZjYolrq0MXo5I9mSpJA3NEQH
//Rp5KRms=

  • 我们最后留下一个问题,通过对加密算法的复现,无法复现成功?这是为什么?

image-20250628104409562

  • 其实在处理密钥时做了一次md5算法,并将前64位,十六进制表示的前16个字符作为密钥,如下图所示。

image-20250628141226048

image-20250628141237502

1.8.hook 技巧

1.8.1.hook 用户输入组件

  • Android开发通常回从EditText组件获取用户的输入信息,需要判断是否为空,通常使用的时TextUtils.isEmpty()方法,来判断用户输入的参数是否为空,那么我们就可以通过hook该方法,在方法内部获取堆栈信息。
1
2
3
4
5
6
String myString = "hello, world";
if (TextUtils.isEmpty(myString)) {
// 字符串为空
} else{
// 字符串不为空
}
  • 针对这个特性进行hook
1
2
3
4
5
6
7
Java.perform(function () {
var TextUtils = java.use("android.text,TextUtils");
TextUtils.isEmpty.implementation = function (a) {
console.log("TextUtils-->" , aa);
retrun this.isEmpty(aa);
}
})

1.8.2.hook JSON对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// Hook JSONObject 构造方法
var JSONObject = Java.use('org.json.JSONObject');

// Hook 所有重载的构造函数
JSONObject.$init.overloads.forEach(function(overload) {
overload.implementation = function() {
try {
// 打印调用堆栈和参数
console.log('\n[JSONObject] Constructor called:');
console.log('Arguments: ' + JSON.stringify(arguments));
console.log('Stack trace: ' + Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new()));

// 调用原始构造函数
var result = this.$init.apply(this, arguments);

// 打印构造后的JSON内容
console.log('Created JSON: ' + result.toString(2));
return result;
} catch (e) {
console.error('Error in JSONObject hook: ' + e);
return this.$init.apply(this, arguments);
}
};
});

// Hook toString() 方法
JSONObject.toString.overload().implementation = function() {
try {
var original = this.toString();
console.log('\n[JSONObject.toString()]');
console.log('Original: ' + original);

// 修改示例:给所有JSON对象添加新字段
var modified = original.replace(/\}$/, ', "hooked":true}');
console.log('Modified: ' + modified);
return modified;
} catch (e) {
console.error(e);
return this.toString();
}
};

// Hook JSONArray 的 put 方法
var JSONArray = Java.use('org.json.JSONArray');
JSONArray.put.overload('java.lang.Object').implementation = function(value) {
console.log('\n[JSONArray.put]');
console.log('Inserting value: ' + value);
return this.put(value);
};

// Hook JSON 解析入口 (示例)
var JSONTokener = Java.use('org.json.JSONTokener');
JSONTokener.$init.overload('java.lang.String').implementation = function(jsonStr) {
console.log('\n[PARSING JSON]');
console.log('Raw JSON: ' + jsonStr);
return this.$init(jsonStr);
};

1.8.3.hook 算法

  • 很多Android开发通常都是使用JAVA自带的包实现的加密或者摘要算法,因此我们可以直接通过hook java原生算法包的方法来获取摘要和加密的明文和密文内容。

1.8.3.1.hook md5

  • 以下代码中就使用了MD5原生算法包。因此我们可以直接hook digest方法,这也是一种通用的方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import kotlin.UByte;

/* loaded from: classes.dex */
public class MD5 {
public static String md5Hash(String str) throws NoSuchAlgorithmException {
byte[] bArrDigest = MessageDigest.getInstance("MD5").digest(str.getBytes());
StringBuilder sb = new StringBuilder();
for (byte b : bArrDigest) {
String hexString = Integer.toHexString(b & UByte.MAX_VALUE);
if (hexString.length() == 1) {
sb.append('0');
}
sb.append(hexString);
}
return sb.toString();
}
}
  • hook 代码如下。
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
// hook java md5
function javaMD5() {
// Hook MessageDigest 的 digest 方法
const MessageDigest = Java.use('java.security.MessageDigest');
MessageDigest.digest.overload('[B').implementation = function(input) {

// 将字节数组转换为 UTF-8 字符串
const inputString = Java.use('java.lang.String').$new(input, "UTF-8");
console.log("\nInput: " + inputString);
console.log("\n=== MD5 Hook Called ===");

// 原始调用
const result = this.digest(input);

// 转换结果为十六进制字符串
const hex = Array.from(result, byte =>
('0' + (byte & 0xFF).toString(16)).slice(-2)).join('');

console.log("MD5 Result: " + hex);
console.log("=========================");

return result;
}
}

1.8.3.2.hook AES

  • Android中AES加密代码示例
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
package com.gaomu.fridatest.utils;

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/* loaded from: classes.dex */
public class AES {
public static String aesEncrypt(String str, String str2, String str3, String str4) throws Exception {
validateHex("密钥", str3, 32);
SecretKeySpec secretKeySpec = new SecretKeySpec(hexToBytes(str3), "AES");
Cipher cipher = Cipher.getInstance(getTransformation(str));
if (str.equalsIgnoreCase("ECB")) {
cipher.init(1, secretKeySpec);
} else {
validateHex("IV", str4, 32);
byte[] bArrHexToBytes = hexToBytes(str4);
if (str.equalsIgnoreCase("GCM")) {
byte[] bArr = new byte[12];
System.arraycopy(bArrHexToBytes, 0, bArr, 0, 12);
cipher.init(1, secretKeySpec, new GCMParameterSpec(128, bArr));
} else {
cipher.init(1, secretKeySpec, new IvParameterSpec(bArrHexToBytes));
}
}
return Base64.getEncoder().encodeToString(cipher.doFinal(str2.getBytes(StandardCharsets.UTF_8)));
}
}
  • hook 示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// hookJavaAes
function hookJavaAes() {
// 首先hookAES加密的明文部分
const Cipher = Java.use("javax.crypto.Cipher");
Cipher.doFinal.overload('[B').implementation = function(input) {
var plainText = Java.use('java.lang.String').$new(input, "UTF-8");
console.log("\nAES Encrypt plainText: ->>>> ", plainText);
return this.doFinal(input);
}

// 再hook密钥
const SecretKeySpec = Java.use("javax.crypto.spec.SecretKeySpec");
SecretKeySpec.$init.overload('[B', 'java.lang.String').implementation = function (keyBytes, alg) {
var keyHex = bytesToHex(keyBytes);
console.log("\nAES Encrypt keyHex: ->>>>", keyHex);
console.log("\nAES Encrypt ALG: ->>>>", alg);
return this.$init(keyBytes, alg);
}

// hook IV
const IvParameterSpec = Java.use("javax.crypto.spec.IvParameterSpec");
IvParameterSpec.$init.overload('[B').implementation = function (keyBytes) {
var ivHex = bytesToHex(keyBytes);
console.log("\nAES Encrypt ivHex: ->>>>", ivHex);
return this.$init(keyBytes);
}
}


// 将hook算法内部参数Byte数组转换为hex字符串
function bytesToHex(bytes) {
return Array.from(bytes, byte =>
('0' + (byte & 0xFF).toString(16)).slice(-2)
).join('');
}

2.抓包检测

几种证书验证的机制

1.常规HTTPS

  • HTTPS认证为客户端获取服务端证书,加密请求服务端。
  • 因此只需要利用中间人攻击,让客户端使用中间人伪造的证书,加密请求,中间人再截获请求解密,再使用服务端端证书加密请求服务器,就实现了中间人攻击,窃取或者伪造传输数据。具体可以查看这篇文章【HTTPS加密流程以及抓包原理解析】
  • 常见的中间人攻击的抓包软件有burpsutie、Charles、yakit。
  • 针对这种常规的SSL证书加密只需要安装普通用户证书即可

2.系统证书验证

  • Android7以上安装系统证书,因此需要使用openssl计算hash,并且移动到系统证书目录,然后命名为hash.0 移动到android的/system/etc/security/cacerts目录下,同时添加对等权限。如果遇到只读文件系统问题,可以使用MT文件管理工具进行移动。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 生成哈希并重命名hash.0
┌──(kali㉿kali)-[~/Desktop/workplace]
└─$ openssl x509 -subject_hash_old -in Charles.pem
3b5566cf
-----BEGIN CERTIFICATE-----
....
mv Charles.pem 3b5566cf.0
# 或者 openssl x509 -inform PEM -subject_hash_old -in myCA.crt

# 推送证书到设备
adb root
adb remount
adb push 3b5566cf.0 /system/etc/security/cacerts/
adb shell chmod 644 /system/etc/security/cacerts/3b5566cf.0

3.SSL pinning单向证书认证

  • 即客户端会强制验证服务端证书是否为对应证书,否则不接受该证书。

  • 对于SSL pinning单向证书认证问题,可以使用利用SSL Unpinning 脚本 hook绕过过代码检测,或者一些其他的SSL检测。

  • 还可以通过frida hook URL 将HTTPS协议转换为HTTP

4.双向认证

  • 通常我们SSL传输加密都是服务端返回证书客户端使用证书中的公钥进行加密,但是在一些高安全要求的情况下,可能会使用双向证书校验,也就是说不仅服务端返回公钥证书,同时客户端APK中也存在一个证书文件,用于对服务端的证书进行证书验证。
  • 一般APK中的证书后缀名为.p12
  • 双向认证如何绕过也可以看这个【双向证书绕过】

5.自定义协议或者TCP

  • 直接使用frida hook请求参数和响应

6.VPN检测

  • 这个没啥用,就算不能不能用VPN也可以使用代理去抓也是一样。

  • 可以直接使用frida hook VPN检测代码,或者直接hook请求参数和响应。

3.1.如何判断一个APK是否加壳?

3.1.1.dex完整性校验

  • 使用dexdump解析Dex头信息,若显示code_offset=0或指令段全零,表明代码被加密。

3.1.2.AndroidManifest.xml

  • 启动类替换:主Activity指向壳的代理类(如com.stub.StubApp),而非业务逻辑类。
  • 权限声明异常:声明android:extractNativeLibs="false"(防止so解密到磁盘)。

3.2.查壳方式

3.2.1.凭借so文件特征

​ 如果加壳会在apk的lib目录下生成.so文件,根据文件名称进行判断。

加固厂商 特有标识
腾讯乐固 libshellx-*.so
360加固 libjiagu*.so/assets/jiami
百度加固 libbaiduprotect.so
阿里聚安全 libmobisec.so

3.2.2.查壳工具

  • 使用查壳工具快速检测

  • 查壳工具

    • GDA:直接拖入APK,自动识别加固类型及版本(如”Tencent Legu 3.9.0”)。

    • PKiD:基于机器学习的查壳工具,检测未知壳变种。

    • APKDeepLens:扫描OWASP漏洞同时输出加壳信息。

例如PKID查壳工具:

image-20250629102645853

3.3.脱壳方式

3.3.1.都有哪些壳

  • 壳的发展
  • 壳的种类非常多,根据其种类不同,使用的技术也不同,这里稍微简单分个类
    1. 一代整体型壳,采用Dex整体加密,动态加载运行的机制。
    2. 二代函数抽取型壳,粒度更细,将方法单独抽取出来,加密保存,解密执行。
    3. 三代 VMP、Dex2C壳:独立虚拟机解释执行、语义等价语浙迁移,强度最高。

3.3.2.frida-dexdump

  • 我们可以使用开源工具frida-dexdump做一些脱一些简单的壳

  • 可以在github下载也可以直接通过pip下载pip install frida-dexdump

  • 使用方法

CLI arguments base on frida-tools, you can quickly dump the foreground application like this:

1
frida-dexdump -FU

Or specify and spawn app like this:

1
frida-dexdump -U -f com.app.pkgname

Additionally, you can see in -h that the new options provided by frida-dexdump are:

1
2
3
-o OUTPUT, --output OUTPUT  Output folder path, default is './<appname>/'.
-d, --deep-search Enable deep search mode.
--sleep SLEEP Waiting times for start, spawn mode default is 5s.

When using, I suggest using the -d, --deep-search option, which may take more time, but the results will be more complete.

使用示例

  • 之后通过运行app脱壳,生成大量的dex文件。
  • 这些dex文件都可以通过GDA/JDX-GUI直接阅读。
  • 但是可能生成的文件较多,因此需要过滤,通过检索存在主类的dex文件,之后只需要重点阅读存在主类的dex文件即可,无需再阅读其他的dex文件。
  • windows使用findstr /s /i /m "主类全类名" *搜索所有存在主类的dex文件。

3.3.3.加固对抗 Frida 的常见手段

加固厂商会采用多种技术阻止 Hook:

  1. 反调试检测:扫描 /proc/self/maps 查找 frida-agent 特征
  2. 端口检测:检查 27042/27043 等 Frida 默认端口
  3. 线程监控:检测异常线程(如 Frida 的 gum-js-loop
  4. 代码混淆:关键逻辑转移到 Native 层(SO 文件)
  5. 双进程守护:监控进程状态,崩溃即重启

3.4.so文件处理

  • so文件和dll文件一样是属于二进制动态链接库。

  • 如果在分析代码时发现System.loadLibrary(“”)方法,其实就算在加载so文件。

  • 并且使用Native修饰的方法都是在so文件中实现的,如果需要分析so文件我们就需要使用IDA工具进行逆向分析。

3.5.Smali 基础语法详解

Smali 是 Android 平台上 Dalvik 虚拟机的汇编语言,用于表示 .dex 文件中的字节码指令。掌握 Smali 是 Android 逆向工程的基础,下面详细介绍其核心语法:


一、文件结构与类定义

1. 类声明

1
2
3
.class <访问修饰符> <类名>;       # 类定义
.super <父类>; # 继承关系
.source "SourceFile" # 源文件名(可选)

示例

1
2
3
.class public Lcom/example/MyClass;  # 公共类
.super Ljava/lang/Object; # 继承 Object
.source "MyClass.java" # 源文件

2. 接口实现

1
.implements <接口名>;              # 实现接口

3. 注解

1
2
3
.annotation <注解类型>             # 注解声明
key = value # 键值对
.end annotation

二、字段定义 (Fields)

1
.field <访问修饰符> [static] [final] <字段名>:<类型>

示例

1
2
.field private count:I               # 私有整型字段
.field public static final TAG:Ljava/lang/String; # 公共静态常量

三、方法定义 (Methods)

1. 方法声明

1
2
3
4
5
6
.method <访问修饰符> [static] <方法名>(<参数类型>)<返回类型>
.registers <N> # 使用的寄存器总数
.locals <M> # 本地寄存器数量(不包括参数)
[.param annotations] # 参数注解(可选)
[代码指令]
.end method

示例

1
2
3
4
5
6
7
8
9
10
11
.method public static main([Ljava/lang/String;)V
.registers 5 # 使用 5 个寄存器
.locals 2 # 本地寄存器:2 个
.param p0, "args" # 参数注解

const-string v0, "Hello"
sget-object v1, Ljava/lang/System;->out:Ljava/io/PrintStream;
invoke-virtual {v1, v0}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V

return-void
.end method

四、数据类型

1. 基本类型

类型 符号 示例
void V ()V
boolean Z Z
byte B B
short S S
int I I
long J J
float F F
double D D
char C C

2. 对象类型

1
L<包名>/<类名>;    # 完整类名

示例

1
2
Ljava/lang/String;   # java.lang.String
Landroid/app/Activity; # android.app.Activity

3. 数组类型

1
2
[<类型>            # 一维数组
[[<类型> # 二维数组

示例

1
2
[I                 # int[]
[[Ljava/lang/String; # String[][]

五、寄存器操作

1. 寄存器命名

  • v0, v1, v2… - 本地寄存器
  • p0, p1, p2… - 参数寄存器(p0 在非静态方法中是 this)

2. 常用指令

指令 功能 示例
move vA, vB 寄存器间移动值 move v1, v0
const vA, value 加载常量 const v0, 0x1
const-string vA, "str" 加载字符串常量 const-string v0, "OK"
const-class vA, Lcls; 加载类引用 const-class v0, Ljava/lang/Object;

六、方法调用

1. 调用类型

1
invoke-<类型> {参数}, <方法签名>
类型 用途
static 静态方法
virtual 虚方法(非私有)
direct 直接方法(私有/构造方法)
interface 接口方法
super 调用父类方法

2. 方法签名格式

1
Lpackage/name/ObjectName;->methodName(LparamTypes;)ReturnType

示例

1
2
3
4
# 调用 System.out.println("Hello")
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
const-string v1, "Hello"
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V

七、控制流指令

1. 条件分支

1
if-<cond> vA, vB, :label   # 条件跳转
条件 含义
eq 等于 (==)
ne 不等于 (!=)
lt 小于 (<)
le 小于等于 (<=)
gt 大于 (>)
ge 大于等于 (>=)
eqz 等于零 (==0)
nez 不等于零 (!=0)

2. 无条件跳转

1
goto :label           # 无条件跳转

3. Switch 语句

1
2
packed-switch vA, :switch_table   # 紧凑switch
sparse-switch vA, :switch_table # 稀疏switch

4. 标签定义

1
:label_name            # 跳转标签

八、异常处理

1. 异常捕获

1
.catch <异常类型> {from_label} {to_label} {target_label}

示例

1
2
3
4
5
6
7
:try_start
# 可能抛出异常的代码
:try_end
.catch Ljava/lang/Exception; {:try_start .. :try_end} :catch_block

:catch_block
# 异常处理代码

2. 抛出异常

1
throw vA               # 抛出vA寄存器中的异常

九、调试信息

1
2
3
.line <行号>           # 源代码行号
.local vA, "name":<type> # 局部变量名和类型
.param pA, "name" # 参数名

十、完整方法示例

1
2
3
4
5
6
7
8
9
10
.method public sum(II)I
.registers 5 # 使用2个寄存器 (v0, v1)
.param p1, "a" # 参数a (在p1)
.param p2, "b" # 参数b (在p2)

.line 10 # 来自第10行代码
add-int v0, p1, p2 # v0 = a + b

return v0 # 返回结果
.end method

学习建议

  1. 实践操作:使用 apktool 反编译 APK,查看生成的 smali 代码

    1
    apktool d app.apk -o output_dir
  2. 工具辅助

    • Jadx:尝试将 smali 转回 Java
    • Bytecode Viewer:可视化分析
    • Android Studio + smalidea 插件:调试 smali 代码
  3. 参考资源

掌握 Smali 语法需要实践积累,建议从简单的 APK 开始分析,逐步理解寄存器操作和控制流结构。