安卓逆向技术

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 number 或 Int64 大数需用 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 开始分析,逐步理解寄存器操作和控制流结构。