跟着正己大跌学安卓

环境安装

———这一段不过多解释,可以去网上搜,也可以看正己爹的视频

一、初识APK

1.apk的文件结构

静态资源文件(assets) 存放静态资源,比如视频、音频、图片等
库文件(lib) armeabi-v7a –> android设备基本通用;arm64-v8a –>64位android设备;x86 –> 常用于安卓模拟器,目录下的so文件时c or c++ 编译的动态链接库
签名文件(META-INF) 签名信息,相当于身份证
编译资源文件(res) 存放资源文件,包括图片,字符串等
配置清单文件(AndroidManifest.xml) 描述应用名字、版本、权限、引用的库文件等
核心代码文件(classes.dex) java源码编译后的字节码文件,Apk运行的主要逻辑
资源映射文件(resources.arsc) 编译后的二进制资源文件,映射资源和id

2.apk双开

目前只知道修改包名

3.apk汉化

利用MT和NP管理器

利用mt的搜索功能

mt dex 里面有单独的搜素功能

image-20250911231436963

4.AndroidManifest.xml

  • *package*:指定应用包名。
  • versionCode*versionName*:定义版本号和版本名称。
  • mainfest* 中添加 uses-permission*:应用需要访问系统功能(如网络、位置)
1
2
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
  • 调试模式:android:debuggable

二、smali代码

简单学习一下smali代码

dex里面修改即可

三、安卓组件、弹窗去除、广告分析

四、动态调试与Log插桩

——–9.11,9.21前写完

五、签名校验

签名校验、dexcrc校验、apk完整性校验、路径文件校验

六、Xposed Hook

七、密码与算法

———9.28前学完

八、ida与so文件

九、so分析(加载、混淆、动调、反调)

———国庆吗???

十、Frida

JNI静态注册,可以使用java的api进行hook也可以使用so的一些api进行hook

// 整数型、布尔值类型、char类型

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
function hookTest1(){
let SecurityUtil = Java.use("com.zj.wuaipojie.util.SecurityUtil");
SecurityUtil["diamondNum"].implementation = function () {
console.log(`SecurityUtil.diamondNum is called`);
let result = this["diamondNum"]();
console.log(`SecurityUtil.diamondNum result=${result}`);
return 9999;
};
}



function hookTest2(){
Java.perform(function(){
// 根据导出函数获取打印地址
var HelloAddr = Module.findExportByName("lib52pojie.so", "Java_com_zj_wuaipojie_util_SecurityUtil_checkVip");
console.log("HelloAddr: " + HelloAddr);
// 拦截函数
if (HelloAddr){
Interceptor.attach(HelloAddr, {
// onEnter函数在目标函数被调用时执行,args参数包含了传递给目标函数的所有参数
onEnter: function(args){
// console.log("onEnter");
// console.log("args[0]: " + args[0]); // 第一个参数,通常是JNIEnv指针
// console.log("args[1]: " + args[1]); // 第二个参数,通常是jclass指针
// console.log("args[2]: " + args[2]); // 第三个参数,通常是jstring指针 // 也是我们真正传入的参数
// console.log(this.context.x1); // 打印寄存器内容
// console.log(args[1].toInt32()); //toInt32()转十进制
// console.log(args[2].readCString()); //读取字符串 char类型
// console.log(hexdump(args[2])); //内存dump
},
// onLeave函数在目标函数返回时执行,retval参数包含了目标函数的返回值
onLeave: function(retval){
console.log("onLeave");
console.log("retval: " + retval); // 打印返回值
retval.replace(0x1); // 修改返回值
}
})
}
})
}
function main(){
Java.perform(function(){
hookTest1();
});
}
setImmediate(main);

// 字符串类型

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
// 字符串类型
function hookTest3(){
Java.perform(function(){
// 根据导出函数获取打印地址
var HelloAddr = Module.findExportByName("lib52pojie.so", "Java_com_zj_wuaipojie_util_SecurityUtil_vipLevel");
// console.log("HelloAddr: " + HelloAddr);
// 拦截函数
if (HelloAddr){
Interceptor.attach(HelloAddr, {
// onEnter函数在目标函数被调用时执行,args参数包含了传递给目标函数的所有参数
onEnter: function(args){
console.log("onEnter");
// 方法一
// var jString = Java.cast(args[2], Java.use('java.lang.String'));
// console.log("参数:", jString.toString());

// // 方法二
var JNIEnv = Java.vm.getEnv();
var originalStrPtr = JNIEnv.getStringUtfChars(args[2], null).readCString();
console.log("参数:", originalStrPtr);
},
// onLeave函数在目标函数返回时执行,retval参数包含了目标函数的返回值
onLeave: function(retval){
console.log("onLeave");
var returnedJString = Java.cast(retval, Java.use('java.lang.String'));
console.log("返回值:", returnedJString.toString());
}
})
}
})
}

整数修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function hookTest4(){
Java.perform(function(){
//根据导出函数名打印地址
var helloAddr = Module.findExportByName("lib52pojie.so","Java_com_zj_wuaipojie_util_SecurityUtil_checkVip");
console.log(helloAddr);
if(helloAddr != null){
Interceptor.attach(helloAddr,{
onEnter: function(args){ //args参数
args[0] = ptr(1000); //第一个参数修改为整数 1000,先转为指针再赋值
console.log(args[0]);

},
onLeave: function(retval){ //retval返回值
retval.replace(20000); //返回值修改
console.log("retval",retval.toInt32());
}
})
}
})
}

字符串修改

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
function hookTest5(){
Java.perform(function(){
// 根据导出函数获取打印地址
var HelloAddr = Module.findExportByName("lib52pojie.so", "Java_com_zj_wuaipojie_util_SecurityUtil_vipLevel");

if (HelloAddr){
Interceptor.attach(HelloAddr, {
onEnter: function(args){
console.log("onEnter");
var JNIEnv = Java.vm.getEnv();
var originalStrPtr = JNIEnv.getStringUtfChars(args[2], null).readCString();
console.log("参数:", originalStrPtr);
var modifiedContent = "至尊";
var newJString = JNIEnv.newStringUtf(modifiedContent);
args[2] = newJString;
},

onLeave: function(retval){
console.log("onLeave");
var JNIEnv = Java.vm.getEnv();
var originalStrPtr = JNIEnv.getStringUtfChars(retval, null).readCString();
console.log("参数:", originalStrPtr);
var modifiedContent = "至尊" + originalStrPtr; //在原字符串前添加"至尊";
var newJString = JNIEnv.newStringUtf(modifiedContent);
retval.replace(newJString);
}

})
}
});
}

导入导出表,通过ida查看即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function hookTest6(){
Java.perform(function(){
//打印导入表
var imports = Module.enumerateImports("lib52pojie.so");
console.log("imports:");
for(var i =0; i < imports.length;i++){
if(imports[i].name == "vip"){
console.log(JSON.stringify(imports[i])); //通过JSON.stringify打印object数据
console.log(imports[i].address);
}
}
//打印导出表
var exports = Module.enumerateExports("lib52pojie.so");
console.log("exports:");
for(var i =0; i < exports.length;i++){
console.log(JSON.stringify(exports[i]));
}

})
}

函数地址计算

  1. 安卓里一般32 位的 so 中都是thumb指令,64 位的 so 中都是arm指令
  2. 通过IDA里的opcode bytes来判断,arm 指令为 4 个字节(options -> general -> Number of opcode bytes (non-graph) 输入4)
  3. thumb 指令,函数地址计算方式: so 基址 + 函数在 so 中的偏移 + 1 arm 指令,函数地址计算方式: so 基址 + 函数在 so 中的偏移
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
// Hook 未导出函数与函数地址的计算
function hookTest2(){

Java.perform(function(){
// 获取so的基地址
var moduleAddr = Module.findBaseAddress("lib52pojie.so");
console.log("Module base address: " + moduleAddr);

// 获取函数地址
var funAddr = moduleAddr.add(0x1071C);
console.log("Function address: " + funAddr);

// Hook函数
if (funAddr){
Interceptor.attach(funAddr, {
onEnter: function(args){

},

onLeave: function(retval){

}
})
}
})
}

frida API

01 write:

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
function hookTest01(){
Java.perform(function(){
// 根据导出函数获取打印地址
var HelloAddr = Module.findExportByName("lib52pojie.so", "Java_com_zj_wuaipojie_util_SecurityUtil_vipLevel");
// console.log("HelloAddr: " + HelloAddr);
// 拦截函数
if (HelloAddr){
Interceptor.attach(HelloAddr, {
// onEnter函数在目标函数被调用时执行,args参数包含了传递给目标函数的所有参数
onEnter: function(args){
console.log("onEnter");

},
// onLeave函数在目标函数返回时执行,retval参数包含了目标函数的返回值
onLeave: function(retval){
console.log("onLeave");
var returnedJstring = Java.cast(retval, Java.use('java.lang.String'));
console.log("returnedJstring: " + returnedJstring);
//一般写在app的私有目录里,不然会报错:failed to open file (Permission denied)(实际上就是权限不足)
var file_path = "/data/user/0/com.zj.wuaipojie/test.txt";
var file_handle = new File(file_path, "wb");
if (file_handle && file_handle != null) {
file_handle.write(returnedJstring.toString()); //写入数据
file_handle.flush(); //刷新
file_handle.close(); //关闭
}
}
})
}
})
}

02 Inline 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
function hexToBytes(str) {
var pos = 0;
var len = str.length;
if (len % 2 != 0) {
return null;
}
len /= 2;
var hexA = new Array();
for (var i = 0; i < len; i++) {
var s = str.substr(pos, 2);
var v = parseInt(s, 16);
hexA.push(v);
pos += 2;
}
return hexA;
}

function hookTest02(){
Java.perform(function(){
var soAddr = Module.findBaseAddress("lib52pojie.so");
var funAddr = soAddr.add(0x103FC);
// Instruction.parse可以解析相应地址对应的汇编指令
// var codeAddr = Instruction.parse(soAddr.add(0x10428));
// console.log(codeAddr.toString());
//PatchCode()方法可以直接修改内存中的指令,达到hook的目的
Memory.patchCode(codeAddr, 4, function(code) {
const writer = new Arm64Writer(code, { pc: funAddr });
writer.putBytes(hexToBytes("20008052"));
writer.flush();
});

if (funAddr){
Interceptor.attach(funAddr, {
onEnter: function(args){
console.log("funAddr onEnter");
// console.log(JSON.stringify(this.context)); // 打印寄存器信息
console.log(this.context.x22);
var arg01 = this.context.x0;
// 读取字符串内容
console.log("x0: " + arg01.readCString());
var arg02 = this.context.x1;
console.log("x1: " + arg02.readCString());
// 修改寄存器的值
this.context.x22 = ptr(1);
console.log("修改后寄存器x22的值: " + this.context.x22);
},
onLeave: function(retval){
}
})
}
})
}

03 主动调用:

数据类型 描述
void 无返回值
pointer 指针
int 整数
long 长整数
char 字符
float 浮点数
double 双精度浮点数
bool 布尔值
1
2
3
4
5
6
7
8
9
var funcAddr = Module.findBaseAddress("lib52pojie.so").add(0x1054C);
// var funcAddr = Module.findExportByName("lib52pojie.so", "AES_ECB_PKCS7_Decrypt");

//声明函数指针
//NativeFunction的第一个参数是地址,第二个参数是返回值类型,第三个[]里的是传入的参数类型(有几个就填几个)
var aesAddr = new NativeFunction(funcAddr , 'pointer', ['pointer', 'pointer']);
var encry_text = Memory.allocUtf8String("OOmGYpk6s0qPSXEPp4X31g=="); //开辟一个指针存放字符串
var key = Memory.allocUtf8String('wuaipojie0123456');
console.log(aesAddr(encry_text ,key).readCString());

jni的主动调用 参考java的主动调用,简单快捷

trace工具:

frida-trace

官方文档 frida-trace 可以一次性监控一堆函数地址。还能打印出比较漂亮的树状图,不仅可以显示调用流程,还能显示调用层次。并且贴心的把不同线程调用结果用不同的颜色区分开了。

我的frida版本是16,websockets==13.1,可以跑通

jnitrace: jnitrace -m attach -l lib52pojie.so com.zj.wuaipojie -o trace.json //attach模式附加52pojie.so并输出日志

十一、Frida检测

1.检测文件名、端口名、双进程保护、失效的检测点

1.检测/data/local/tmp路径下的是否有frida特征文件,可以在server端改名,例如:fr 2.指定端口转发

1
2
3
/fs1 -l 0.0.0.0:6666
adb forward tcp:6666 tcp:6666
frida -H 127.0.0.1:6666 wuaipojie -l hook.js

3.spawn启动过双进程保护

1
frida -U -f 进程名 -l hook.js

看注入报错的日志,比如说当app主动附加自身进程时,这时候再注入就会提示run frida as root(以spawn的方式启动进程即可) 4.借助脚本定位检测frida的so(之前写过)

1
2
3
4
5
6
7
8
9
10
11
12
13
 复制代码 隐藏代码function hook_dlopen() {
Interceptor.attach(Module.findExportByName(null, "android_dlopen_ext"),
{
onEnter: function (args) {
var pathptr = args[0];
if (pathptr !== undefined && pathptr != null) {
var path = ptr(pathptr).readCString();
console.log("load " + path);
}
}
}
);
}

5检测点的失效 (1.)例如检测D-Bus D-Bus是一种进程间通信(IPC)和远程过程调用(RPC)机制,最初是为Linux开发的,目的是用一个统一的协议替代现有的和竞争的IPC解决方案。

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
 复制代码 隐藏代码bool check_dbus() {
// 定义一个socket地址结构体变量sa
struct sockaddr_in sa;
// 创建一个socket文件描述符
int sock;
// 定义一个字符数组res,用于存储接收到的数据
char res[7];

// 循环遍历所有可能的端口号,从0到65535
for(int i = 0; i <= 65535; i++) {
// 创建一个新的socket连接
sock = socket(AF_INET, SOCK_STREAM, 0);
// 设置socket地址结构体的端口号
sa.sin_port = htons(i);
// 尝试连接到当前端口
if (connect(sock, (struct sockaddr*)&sa, sizeof(sa)) != -1) {
// 如果连接成功,记录日志信息,表示发现了一个开放的端口
__android_log_print(ANDROID_LOG_VERBOSE, "ZJ595", "FRIDA DETECTION [1]: Open Port: %d", i);
// 初始化res数组,清零
memset(res, 0, 7);
// 向socket发送一个空字节
send(sock, "\x00", 1, 0); // 注意这里的NULL被替换为0
// 发送AUTH请求
send(sock, "AUTH\r\n", 6, 0);
// 等待100微秒
usleep(100);
// 尝试接收响应
if (recv(sock, res, 6, MSG_DONTWAIT) != -1) {
// 如果接收到响应,检查响应内容是否为"REJECT"
if (strcmp(res, "REJECT") == 0) {
// 如果是,关闭socket并返回true,表示检测到了Frida服务器
close(sock);
return true; // Frida server detected
}
}
}
// 如果当前端口连接失败或没有检测到Frida服务器,关闭socket
close(sock);
}
// 如果遍历完所有端口都没有检测到Frida服务器,返回false
return false; // No Frida server detected
}

(2)检测fd /proc/pid/fd 目录的作用在于提供了一种方便的方式来查看进程的文件描述符信息,这对于调试和监控进程非常有用。通过查看文件描述符信息,可以了解进程打开了哪些文件、网络连接等,帮助开发者和系统管理员进行问题排查和分析工作。

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
 复制代码 隐藏代码bool check_fd() {
DIR *dir = NULL;
struct dirent *entry;
char link_name[100];
char buf[100];
bool ret = false;
if ((dir = opendir("/proc/self/fd/")) == NULL) {
LOGI(" %s - %d error:%s", __FILE__, __LINE__, strerror(errno));
} else {
entry = readdir(dir);
while (entry) {
switch (entry->d_type) {
case DT_LNK:
sprintf(link_name, "%s/%s", "/proc/self/fd/", entry->d_name);
readlink(link_name, buf, sizeof(buf));
if (strstr(buf, "frida") || strstr(buf, "gum-js-loop") ||
strstr(buf, "gmain") ||
strstr(buf, "-gadget") || strstr(buf, "linjector")) {
LOGI("check_fd -> find frida:%s", buf);
ret = true;
}
break;
default:
break;
}
entry = readdir(dir);
}
}
closedir(dir);
return ret;
}

(3)检测文件 众所周知frida我们一般都会放在data/local/tmp目录下,旧版fridaserver端运行时都会释放到re.frida.server,所以这里在旧版也会被当做一个检测点,而新版已不再释放

2、检测map

/proc/self/maps 是一个特殊的文件,它包含了当前进程的内存映射信息。当你打开这个文件时,它会显示一个列表,其中包含了进程中每个内存区域的详细信息。这些信息通常包括:

  • 起始地址(Start Address)
  • 结束地址(End Address)
  • 权限(如可读、可写、可执行)
  • 共享/私有标志(Shared or Private)
  • 关联的文件或设备(如果内存区域是文件映射的)
  • 内存区域的偏移量
  • 内存区域的类型(如匿名映射、文件映射、设备映射等) 当注入frida后,在maps文件中就会存在 frida-agent-64.sofrida-agent-32.so 等文件。
1
2
3
4
5
OP591BL1:/ # cat /proc/22503/maps | grep frida
7de3c4f000-7de4679000 r--p 00000000 00:01 9930 /memfd:frida-agent-64.so (deleted)
7de467a000-7de53b2000 r-xp 00a2a000 00:01 9930 /memfd:frida-agent-64.so (deleted)
7de53b2000-7de5483000 r--p 01761000 00:01 9930 /memfd:frida-agent-64.so (deleted)
7de5484000-7de549f000 rw-p 01832000 00:01 9930 /memfd:frida-agent-64.so (deleted)

检查无非就是检查字符串,想过掉直接hook字符串或者其他的一些东西都可以

anti1:(hook了strstr以及strcmp)

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
// 定义一个函数anti_maps,用于阻止特定字符串的搜索匹配,避免检测到敏感内容如"Frida"或"REJECT"
function anti_maps() {
// 查找libc.so库中strstr函数的地址,strstr用于查找字符串中首次出现指定字符序列的位置
var pt_strstr = Module.findExportByName("libc.so", 'strstr');
// 查找libc.so库中strcmp函数的地址,strcmp用于比较两个字符串
var pt_strcmp = Module.findExportByName("libc.so", 'strcmp');
// 使用Interceptor模块附加到strstr函数上,拦截并修改其行为
Interceptor.attach(pt_strstr, {
// 在strstr函数调用前执行的回调
onEnter: function (args) {
// 读取strstr的第一个参数(源字符串)和第二个参数(要查找的子字符串)
var str1 = args[0].readCString();
var str2 = args[1].readCString();
// 检查子字符串是否包含"REJECT"或"frida",如果包含则设置hook标志为true
if (str2.indexOf("REJECT") !== -1 || str2.indexOf("frida") !== -1) {
this.hook = true;
}
},
// 在strstr函数调用后执行的回调
onLeave: function (retval) {
// 如果之前设置了hook标志,则将strstr的结果替换为0(表示未找到),从而隐藏敏感信息
if (this.hook) {
retval.replace(0);
}
}
});

// 对strcmp函数做类似的处理,防止通过字符串比较检测敏感信息
Interceptor.attach(pt_strcmp, {
onEnter: function (args) {
var str1 = args[0].readCString();
var str2 = args[1].readCString();
if (str2.indexOf("REJECT") !== -1 || str2.indexOf("frida") !== -1) {
this.hook = true;
}
},
onLeave: function (retval) {
if (this.hook) {
// strcmp返回值为0表示两个字符串相等,这里同样替换为0以避免匹配成功
retval.replace(0);
}
}
});
}

anti2:重定向我们修改之后的maps文件

1
2
3
4
5
6
7
8
9
// 循环读取maps内容,并写入伪造的maps文件中,同时进行字符串替换以隐藏特定信息
while (parseInt(read(FD, MapsBuffer, 512)) !== 0) {
var MBuffer = MapsBuffer.readCString();
MBuffer = MBuffer.replaceAll("/data/local/tmp/re.frida.server/frida-agent-64.so", "FakingMaps");
MBuffer = MBuffer.replaceAll("re.frida.server", "FakingMaps");
MBuffer = MBuffer.replaceAll("frida-agent-64.so", "FakingMaps");
MBuffer = MBuffer.replaceAll("frida-agent-32.so", "FakingMaps");
MBuffer = MBuffer.replaceAll("frida", "FakingMaps");
MBuffer = MBuffer.replaceAll("/data/local/tmp", "/data");

3.检测status(线程名)

1
2
ls /proc/pid/task 列出线程id
cat /proc/pid/task/线程id/status
  • /proc/pid/task 目录下,可以通过查看不同的线程子目录,来获取进程中每个线程的运行时信息。这些信息包括线程的状态、线程的寄存器内容、线程占用的CPU时间、线程的堆栈信息等。通过这些信息,可以实时观察和监控进程中每个线程的运行状态,帮助进行调试、性能优化和问题排查等工作。
  • 在某些app中就会去读取 /proc/stask/线程ID/status 文件,如果是运行frida产生的,则进行反调试。例如:gmain/gdbus/gum-js-loop/pool-frida
  1. gmain:Frida 使用 Glib 库,其中的主事件循环被称为 GMainLoop。在 Frida 中,gmain 表示 GMainLoop 的线程。
  2. gdbus:GDBus 是 Glib 提供的一个用于 D-Bus 通信的库。在 Frida 中,gdbus 表示 GDBus 相关的线程。
  3. gum-js-loop:Gum 是 Frida 的运行时引擎,用于执行注入的 JavaScript 代码。gum-js-loop 表示 Gum 引擎执行 JavaScript 代码的线程。
  4. pool-frida:Frida 中的某些功能可能会使用线程池来处理任务,pool-frida 表示 Frida 中的线程池。
  5. linjector 是一种用于 Android 设备的开源工具,它允许用户在运行时向 Android 应用程序注入动态链接库(DLL)文件。通过注入 DLL 文件,用户可以修改应用程序的行为、调试应用程序、监视函数调用等,这在逆向工程、安全研究和动态分析中是非常有用的。 PS:由于frida可以随时附加到进程,所以写的检测必须覆盖APP的全周期,或者至少是敏感函数执行前

——–10.26

十二、RPC

hook_RegisterNatives.js:hook打印动态注册的函数

frida -U -f com.zj.wuaipojie -l hook_RegisterNatives.js

hook_GetStringUTFChars.js: 不知道为什么我使用的不太正确

十三、抓包

——–11.2

十四、Flutter逆向

十五、Unidbg