Android逆向14-Frida检测

来自吾爱破解-正己

https://www.52pojie.cn/thread-1701353-1-1.html

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

检测/data/local/tmp路径下的是否有frida特征文件,server端改名,例如:fr

指定端口转发

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

spawn启动过双进程保护

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

PS:学会看注入报错的日志,比如说当app主动附加自身进程时,这时候再注入就会提示run frida as root(以spawn的方式启动进程即可)

借助脚本定位检测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);
}
}
}
);
}

随着firda的版本迭代,以前诸多检测点以失效

例如检测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
43
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
}

检测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
32
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;
}

检测文件

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

检测map

image-20250508200413047

1
2
adb shell ps | findstr com.zj.wuaipojie
cat /proc/12186/maps|grep frida

image-20250508200429619

字段 描述
u0_a504 用户ID和应用ID:在Android系统中,u0代表系统用户(user 0),而a504是该应用在用户0下的唯一标识符。
28082 PID(进程ID):该进程在操作系统中的标识符。
1935 PPID(父进程ID):该进程的父进程的PID。
6511212 虚拟内存:进程使用的虚拟内存大小,通常以字节为单位。
125728 共享内存:进程使用的共享内存大小,同样以字节为单位。
0 CPU时间/线程数:这通常表示进程的CPU时间或者是线程数,具体含义取决于ps命令的输出格式。
S 状态:其中S代表睡眠状态(Sleeping),即进程没有在执行,而是在等待某些事件或资源。

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

  • 起始地址(Start Address)
  • 结束地址(End Address)
  • 权限(如可读、可写、可执行)
  • 共享/私有标志(Shared or Private)
  • 关联的文件或设备(如果内存区域是文件映射的)
  • 内存区域的偏移量
  • 内存区域的类型(如匿名映射、文件映射、设备映射等)
    当注入frida后,在maps文件中就会存在 frida-agent-64.sofrida-agent-32.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
bool check_maps() {
// 定义一个足够大的字符数组line,用于存储读取的行
char line[512];
// 打开当前进程的内存映射文件/proc/self/maps进行读取
FILE* fp = fopen("/proc/self/maps", "r");
if (fp) {
// 如果文件成功打开,循环读取每一行
while (fgets(line, sizeof(line), fp)) {
// 使用strstr函数检查当前行是否包含"frida"字符串
if (strstr(line, "frida") || strstr(line, "gadget")) {
// 如果找到了"frida",关闭文件并返回true,表示检测到了恶意库
fclose(fp);
return true; // Evil library is loaded.
}
}
// 遍历完文件后,关闭文件
fclose(fp);
} else {
// 如果无法打开文件,记录错误。这可能意味着系统状态异常
// 注意:这里的代码没有处理错误,只是注释说明了可能的情况
}
// 如果没有在内存映射文件中找到"frida",返回false,表示没有检测到恶意库
return false; // No evil library detected.
}

方法1

anti脚本

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);
}
}
});
}

image-20250508201306008

方法2

重定向maps

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
// 定义一个函数,用于重定向并修改maps文件内容,以隐藏特定的库和路径信息
function mapsRedirect() {
// 定义伪造的maps文件路径
var FakeMaps = "/data/data/com.zj.wuaipojie/maps";
// 获取libc.so库中'open'函数的地址
const openPtr = Module.getExportByName('libc.so', 'open');
// 根据地址创建一个新的NativeFunction对象,表示原生的'open'函数
const open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
// 查找并获取libc.so库中'read'函数的地址
var readPtr = Module.findExportByName("libc.so", "read");
// 创建新的NativeFunction对象表示原生的'read'函数
var read = new NativeFunction(readPtr, 'int', ['int', 'pointer', "int"]);
// 分配512字节的内存空间,用于临时存储从maps文件读取的内容
var MapsBuffer = Memory.alloc(512);
// 创建一个伪造的maps文件,用于写入修改后的内容,模式为"w"(写入)
var MapsFile = new File(FakeMaps, "w");
// 使用Interceptor替换原有的'open'函数,注入自定义逻辑
Interceptor.replace(openPtr, new NativeCallback(function(pathname, flag) {
// 调用原始的'open'函数,并获取文件描述符(FD)
var FD = open(pathname, flag);
// 读取并打印尝试打开的文件路径
var ch = pathname.readCString();
if (ch.indexOf("/proc/") >= 0 && ch.indexOf("maps") >= 0) {
console.log("open : ", pathname.readCString());
// 循环读取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");
// 将修改后的内容写入伪造的maps文件
MapsFile.write(MBuffer);
}
// 为返回伪造maps文件的打开操作,分配UTF8编码的文件名字符串
var filename = Memory.allocUtf8String(FakeMaps);
// 返回打开伪造maps文件的文件描述符
return open(filename, flag);
}
// 如果不是目标maps文件,则直接返回原open调用的结果
return FD;
}, 'int', ['pointer', 'int']));
}

方法3

用eBPF来hook系统调用并修改参数实现目的,使用bpf_probe_write_user向用户态函数地址写内容直接修改参数

1
2
char placeholder[] = "/data/data/com.zj.wuaipojie/maps";
bpf_probe_write_user((void*)addr, placeholder, sizeof(placeholder));

检测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的全周期,或者至少是敏感函数执行前
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
bool check_status() {
DIR *dir = opendir("/proc/self/task/");
struct dirent *entry;
char status_path[MAX_PATH];
char buffer[MAX_BUFFER];
int found = false;

if (dir) {
while ((entry = readdir(dir)) != NULL) {
if (entry->d_type == DT_DIR) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
snprintf(status_path, sizeof(status_path), "/proc/self/task/%s/status", entry->d_name);
if (read_file(status_path, buffer, sizeof(buffer)) == -1) {
continue;
}
if (strcmp(buffer, "null") == 0) {
continue;
}
char *line = strtok(buffer, "\n");
while (line) {
if (strstr(line, "Name:") != NULL) {
const char *frida_name = strstr(line, "gmain");
if (frida_name || strstr(line, "gum-js-loop") || strstr(line, "pool-frida") || strstr(line, "gdbus")) {
found = true;
break;
}
}
line = strtok(NULL, "\n");
}
if (found) break;
}
}
closedir(dir);
}
return found;
}

anti脚本

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 replace_str() {
var pt_strstr = Module.findExportByName("libc.so", 'strstr');
var pt_strcmp = Module.findExportByName("libc.so", 'strcmp');

Interceptor.attach(pt_strstr, {
onEnter: function (args) {
var str1 = args[0].readCString();
var str2 = args[1].readCString();
if (str2.indexOf("tmp") !== -1 ||
str2.indexOf("frida") !== -1 ||
str2.indexOf("gum-js-loop") !== -1 ||
str2.indexOf("gmain") !== -1 ||
str2.indexOf("gdbus") !== -1 ||
str2.indexOf("pool-frida") !== -1||
str2.indexOf("linjector") !== -1) {
//console.log("strcmp-->", str1, str2);
this.hook = true;
}
}, onLeave: function (retval) {
if (this.hook) {
retval.replace(0);
}
}
});

Interceptor.attach(pt_strcmp, {
onEnter: function (args) {
var str1 = args[0].readCString();
var str2 = args[1].readCString();
if (str2.indexOf("tmp") !== -1 ||
str2.indexOf("frida") !== -1 ||
str2.indexOf("gum-js-loop") !== -1 ||
str2.indexOf("gmain") !== -1 ||
str2.indexOf("gdbus") !== -1 ||
str2.indexOf("pool-frida") !== -1||
str2.indexOf("linjector") !== -1) {
//console.log("strcmp-->", str1, str2);
this.hook = true;
}
}, onLeave: function (retval) {
if (this.hook) {
retval.replace(0);
}
}
})

}

检测inlinehook

通过Frida查看一个函数hook之前和之后的机器码,以此来判断是否被Frida的inlinehook注入。

image-20250508201928352

下面的方案以内存中字节和本地对应的字节进行比较,如果不一致,那么可以认为内存中的字节被修改了,即被inlinehook了

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
#include <jni.h>
#include <string>
#include <dlfcn.h>
#include "dlfcn/local_dlfcn.h"

bool check_inlinehook() {
// 根据系统架构选择对应的libc.so库路径
const char *lib_path;
#ifdef __LP64__
lib_path = "/system/lib64/libc.so";
#else
lib_path = "/system/lib/libc.so";
#endif

// 定义要比较的字节数
const int CMP_COUNT = 8;
// 指定要查找的符号名,这里是"open"函数
const char *sym_name = "open";

// 使用local_dlopen函数打开指定的共享库,并获取操作句柄
struct local_dlfcn_handle *handle = static_cast<local_dlfcn_handle *>(local_dlopen(lib_path));
if (!handle) {
return JNI_FALSE; // 如果无法打开共享库,返回false
}

// 获取"open"函数在libc.so中的偏移量
off_t offset = local_dlsym(handle, sym_name);

// 关闭handle,因为我们接下来使用标准的dlopen/dlsy来获取函数地址
local_dlclose(handle);

// 打开libc.so文件,准备读取数据
FILE *fp = fopen(lib_path, "rb");
if (!fp) {
return JNI_FALSE; // 如果无法打开文件,返回false
}

// 定义一个缓冲区,用于存储读取的文件内容
char file_bytes[CMP_COUNT] = {0};
// 读取指定偏移量处的CMP_COUNT个字节
fseek(fp, offset, SEEK_SET);
fread(file_bytes, 1, CMP_COUNT, fp);
fclose(fp);

// 使用dlopen函数打开libc.so共享库,并获取操作句柄
void *dl_handle = dlopen(lib_path, RTLD_NOW);
if (!dl_handle) {
return JNI_FALSE; // 如果无法打开共享库,返回false
}

// 使用dlsym函数获取"open"函数的地址
void *sym = dlsym(dl_handle, sym_name);
if (!sym) {
dlclose(dl_handle);
return JNI_FALSE; // 如果无法找到符号,返回false
}

// 比较原libc.so中的"open"函数内容与通过dlsym获取的"open"函数内容是否一致
int is_hook = memcmp(file_bytes, sym, CMP_COUNT) != 0;

// 关闭dlopen打开的共享库句柄
dlclose(dl_handle);

// 返回比较结果,如果函数被hook则返回JNI_TRUE,否则返回JNI_FALSE
return is_hook ? JNI_TRUE : JNI_FALSE;
}

获取hook前字节码的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
let bytes_count = 8
let address = Module.getExportByName("libc.so","open")

let before = ptr(address)
console.log("")
console.log(" before hook: ")
console.log(hexdump(before, {
offset: 0,
length: bytes_count,
header: true,
ansi: true
}));

anti脚本

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

function hook_memcmp_addr(){
//hook反调试
var memcmp_addr = Module.findExportByName("libc.so", "fread");
if (memcmp_addr !== null) {
console.log("fread address: ", memcmp_addr);
Interceptor.attach(memcmp_addr, {
onEnter: function (args) {
this.buffer = args[0]; // 保存 buffer 参数
this.size = args[1]; // 保存 size 参数
this.count = args[2]; // 保存 count 参数
this.stream = args[3]; // 保存 FILE* 参数
},
onLeave: function (retval) {
// 这里可以修改 buffer 的内容,假设我们知道何时 fread 被用于敏感操作
console.log(this.count.toInt32());
if (this.count.toInt32() == 8) {
// 模拟 fread 读取了预期数据,伪造返回值
Memory.writeByteArray(this.buffer, [0x50, 0x00, 0x00, 0x58, 0x00, 0x02, 0x1f, 0xd6]);
retval.replace(8); // 填充前8字节
console.log(hexdump(this.buffer));
}
}
});
} else {
console.log("Error: memcmp function not found in libc.so");
}
}

刷入魔改的frida-server端

注意版本!!!

strongR-frida-android

Syscall&SVC&自定义strstr

在上面的检测对抗中,我们hook了libc.so中的fread、strstr、open等系统函数,但是如果app不讲武德,自实现这些函数,阁下又该如何应对?

在用户空间和内核空间之间,有一个叫做Syscall(系统调用, system call)的中间层,是连接用户态和内核态的桥梁。这样即提高了内核的安全型,也便于移植,只需实现同一套接口即可。Linux系统,用户空间通过向内核空间发出Syscall,产生软中断,从而让程序陷入内核态,执行相应的操作。

SVC(软件中断指令)指令:在ARM架构的系统中,svc是一条特殊的指令,它允许用户态的程序发起一个系统调用。当这条指令被执行时,CPU会从用户态切换到内核态,从而允许内核处理这个请求。

Linux操作系统是一个巨大的图书馆,而syscall就是这个图书馆的前台服务窗口。当一个应用程序(比如一个读者)需要借阅书籍(获取系统资源或服务)时,它不能直接进入图书馆的内部书架去拿书,因为那样可能会造成混乱和损坏。所以,读者需要通过前台服务窗口,也就是syscall,来请求它想要的书籍。

svc就像是图书馆前台服务窗口的内部电话。当读者通过前台窗口提出请求时,前台工作人员会通过内部电话(svc)来联系图书馆的内部工作人员,请求他们找到并提供所需的书籍。在Linux系统中,当一个程序通过syscall请求服务时,实际上是通过svc这条指令通知内核,然后由内核来处理这些请求。

Frida-Sigaction-Seccomp实现对Android APP系统调用的拦截

分享一个Android通用svc跟踪以及hook方案——Frida-Seccomp

基于seccomp+sigaction的Android通用svc hook方案

原创]SVC的TraceHook沙箱的实现&无痕Hook实现思路

原创]Seccomp技术在Android应用中的滥用与防护

原创]批量检测android app的so中是否有svc调用

图片

  1. 首先当我们长按开机键(电源按钮)开机,此时会引导芯片开始从固化到ROM中的预设代码处执行,然后加载引导程序到RAM。然后启动加载的引导程序,引导程序主要做一些基本的检查,包括RAM的检查,初始化硬件的参数。
  2. 到达内核层的流程后,这里初始化一些进程管理、内存管理、加载各种Driver等相关操作,如Camera Driver、Binder Driver 等。下一步就是内核线程,如软中断线程、内核守护线程。下面一层就是Native层,这里额外提一点知识,层于层之间是不可以直接通信的,所以需要一种中间状态来通信。Native层和Kernel层之间通信用的是syscall,Native层和Java层之间的通信是JNI。
  3. 在Native层会初始化init进程,也就是用户组进程的祖先进程。init中加载配置文件init.rc,init.rc中孵化出ueventd、logd、healthd、installd、lmkd等用户守护进程。开机动画启动等操作。核心的一步是孵化出Zygote进程,此进程是所有APP的父进程,这也是Xposed注入的核心,同时也是Android的第一个Java进程(虚拟机进程)。
  4. 进入框架层后,加载zygote init类,注册zygote socket套接字,通过此套接字来做进程通信,并加载虚拟机、类、系统资源等。zygote第一个孵化的进程是system_server进程,负责启动和管理整个Java Framework,包含ActivityManager、PowerManager等服务。
  5. 应用层的所有APP都是从zygote孵化而来
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
bool anti_anti_maps() {
// 定义一个足够大的字符数组line,用于存储读取的行
const int buf_size = 512;
char buf[buf_size];
int fd; // 文件描述符
// 使用 my_openat 打开当前进程的内存映射文件 /proc/self/maps 进行读取
// AT_FDCWD 表示当前工作目录,"r" 表示只读方式打开
fd = my_openat(AT_FDCWD, "/proc/self/maps", O_RDONLY | O_CLOEXEC, 0);
if (fd != -1) {
// 如果文件成功打开,循环读取每一行
while ((read_line(fd, buf, buf_size)) > 0) {
// 使用strstr函数检查当前行是否包含"frida"字符串
if (strstr(buf, "frida") || strstr(buf, "gadget")) {
// 如果找到了"frida",关闭文件并返回true,表示检测到了恶意库
close(fd);
return true; // Evil library is loaded.
}
}
// 遍历完文件后,关闭文件
close(fd);
} else {
// 如果无法打开文件,记录错误。这可能意味着系统状态异常
// 注意:这里的代码没有处理错误,只是注释说明了可能的情况
}
// 如果没有在内存映射文件中找到"frida",返回false,表示没有检测到恶意库
return false; // No evil library detected.
}

ENTRY(my_openat) // 定义函数入口,标签my_openat
mov x8, __NR_openat // 将openat系统调用号(__NR_openat)移动到x8寄存器,x8用于存储系统调用号
svc #0 // 触发系统调用异常,进入操作系统执行系统调用
cmn x0, #(MAX_ERRNO + 1) // 将函数返回值(存储在x0寄存器)与MAX_ERRNO + 1进行无符号比较
cneg x0, x0, hi // 如果上面的比较结果大于或等于零(即没有错误),则将x0的符号位取反(如果原来是负则变正)
b.hi __set_errno_internal // 如果上面的比较结果大于或等于零(即发生了错误),则跳转到__set_errno_internal进行错误处理
ret // 从函数返回,继续执行调用者代码
END(my_openat) // 标记函数结束

anti脚本

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
function anti_svc(){
let target_code_hex; // 用于搜索特定汇编指令序列的十六进制字符串
let call_number_openat; // 系统调用号对应的数值,openat
let arch = Process.arch; // 获取当前进程的架构

if ("arm" === arch){ // 如果架构是ARM
target_code_hex = "00 00 00 EF"; // ARM架构下svc指令的十六进制表示
call_number_openat = 322; // openat在ARM架构中的系统调用号
}else if("arm64" === arch){ // 如果架构是ARM64
target_code_hex = "01 00 00 D4"; // ARM64架构下svc指令的十六进制表示
call_number_openat = 56; // openat在ARM64架构中的系统调用号
}else {
console.log("arch not support!"); // 如果架构不支持,打印错误信息
}

if (arch){ // 如果成功获取了架构信息
console.log("\nthe_arch = " + arch); // 打印当前架构
// 枚举进程的内存范围,寻找只读内存段
Process.enumerateRanges('r--').forEach(function (range) {
if(!range.file || !range.file.path){ // 如果内存段没有文件路径,跳过
return;
}
let path = range.file.path; // 获取内存段的文件路径
// 如果文件路径不是以"/data/app/"开头或不以".so"结尾,跳过
if ((!path.startsWith("/data/app/")) || (!path.endsWith(".so"))){
return;
}
let baseAddress = Module.getBaseAddress(path); // 获取so库的基址
let soNameList = path.split("/"); // 通过路径分割获取so库的名称
let soName = soNameList[soNameList.length - 1]; // 获取so库的名称
console.log("\npath = " + path + " , baseAddress = " + baseAddress +
" , rangeAddress = " + range.base + " , size = " + range.size);
// 在so库的内存范围内搜索target_code_hex对应的指令序列
Memory.scan(range.base, range.size, target_code_hex, {
onMatch: function (match){
let code_address = match; // 获取匹配到的指令地址
let code_address_str = code_address.toString(); // 转换为字符串
// 如果地址的最低位是0, 4, 8, c中的任意一个,说明可能是svc指令
if (code_address_str.endsWith("0") || code_address_str.endsWith("4") ||
code_address_str.endsWith("8") || code_address_str.endsWith("c")){
console.log("--------------------------");
let call_number = 0; // 初始化系统调用号
if ("arm" === arch){
// 获取svc指令后面的立即数,作为系统调用号
call_number = (code_address.sub(0x4).readS32()) & 0xFFF;
}else if("arm64" === arch){
call_number = (code_address.sub(0x4).readS32() >> 5) & 0xFFFF;
}else {
console.log("the arch get call_number not support!"); // 如果架构不支持,打印错误信息
}
console.log("find svc : so_name = " + soName + " , address = " + code_address +
" , call_number = " + call_number + " , offset = " + code_address.sub(baseAddress));
// 如果匹配到的系统调用号是openat,挂钩该地址
if (call_number_openat === call_number){
let target_hook_addr = code_address;
let target_hook_addr_offset = target_hook_addr.sub(baseAddress);
console.log("find svc openat , start inlinehook by frida!");
Interceptor.attach(target_hook_addr, {
onEnter: function (args){ // 当进入挂钩函数时
console.log("\nonEnter_" + target_hook_addr_offset + " , __NR_openat , args[1] = " +
args[1].readCString());
// 修改openat的第一个参数为指定路径
this.new_addr = Memory.allocUtf8String("/data/user/0/com.zj.wuaipojie/maps");
args[1] = this.new_addr;
console.log("onEnter_" + target_hook_addr_offset + " , __NR_openat , args[1] = " +
args[1].readCString());
},
onLeave: function (retval){ // 当离开挂钩函数时
console.log("onLeave_" + target_hook_addr_offset + " , __NR_openat , retval = " + retval)
}
});
}
}
},
onComplete: function () {} // 搜索完成后的回调函数
});
});
}
}

自定义strstr

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
bool anti_str_maps() {
// 定义一个足够大的字符数组line,用于存储读取的行
char line[512];
// 打开当前进程的内存映射文件/proc/self/maps进行读取
FILE* fp = fopen("/proc/self/maps", "r");
if (fp) {
// 如果文件成功打开,循环读取每一行
while (fgets(line, sizeof(line), fp)) {
// 使用自定义strstr函数检查当前行是否包含"frida"指纹
if (my_strstr(line, "frida") || my_strstr(line, "gadget")) {
// 如果找到了,关闭文件并返回true,表示检测到了恶意库
fclose(fp);
return true;
}
}
// 遍历完文件后,关闭文件
fclose(fp);
} else {
// 如果无法打开文件,记录错误。这可能意味着系统状态异常
// 注意:这里的代码没有处理错误,只是注释说明了可能的情况
}
// 如果没有在内存映射文件中找到"frida",返回false,表示没有检测到恶意库
return false;
}

//自实现了libc里的几个系统函数
__attribute__((always_inline))
static inline char *
my_strstr(const char *s, const char *find)
{
char c, sc;
size_t len;

if ((c = *find++) != '\0') {
len = my_strlen(find);
do {
do {
if ((sc = *s++) == '\0')
return (NULL);
} while (sc != c);
} while (my_strncmp(s, find, len) != 0);
s--;
}
return ((char *)s);
}

frida持久化方案

免root方案

Frida的Gadget是一个共享库,用于免root注入hook脚本。

官方文档

思路:将APK解包后,通过修改smali代码或patch so文件的方式植入frida-gadget,然后重新打包安装。

优点:免ROOT、能过掉一部分检测机制

缺点:重打包可能会遇到解决不了的签名校验、hook时机需要把握

基于obejction的patchapk功能

官方文档命令:

1
2
3
4
objection patchapk -V 14.2.18 -c config.txt -s demo.apk(注意路径不要有中文)
-V 指定gadget版本
-c 加载脚本配置信息
-s 要注入的apk

注意的问题:
objection patchapk命令基本上是其他几个系统命令的补充,可尽可能地自动化修补过程。当然,需要先安装并启用这些命令。它们是:

ps:这几个环境工具,aapt、jarsigner都是Android Studio自带的,所以在配置好as的环境即可,abd的环境配置网上搜一下就行,apktool则需要额外配置

另外会遇到的问题,patchapk的功能在patch的时候会下载对应版本的gadget的so,但是网络问题异常慢,所以建议根据链接去下载好,然后放到这个路径下并重命名

1
C:\Users\用户名\.objection\android\arm64-v8a\libfrida-gadget.so

root方案

方法一:

思路:可以patch /data/app/pkgname/lib/arm64(or arm)目录下的so文件,apk安装后会将so文件解压到该目录并在运行时加载,修改该目录下的文件不会触发签名校验。

Patch SO的原理可以参考Android平台感染ELF文件实现模块注入

优点:绕过签名校验、root检测和部分ptrace保护。

缺点:需要root、高版本系统下,当manifest中的android:extractNativeLibs为false时,lib目录文件可能不会被加载,而是直接映射apk中的so文件、可能会有so完整性校验

使用方法

1
2
3
python LIEFInjectFrida.py test.apk ./ lib52pojie.so -apksign -persistence
test.apk要注入的apk名称
lib52pojie.so要注入的so名称

然后提取patch后是so文件放到对应的so目录下

方法二:

思路:基于magisk模块方案注入frida-gadget,实现加载和hook。寒冰师傅的FridaManager

优点:无需重打包、灵活性较强

缺点:需要过root检测,magsik检测

方法三:

思路:基于jshook封装好的fridainject框架实现hook

JsHook

源码定制方案

原理:修改aosp源代码,在fork子进程的时候注入frida-gadget

ubuntu 20.04系统AOSP(Android 11)集成Frida

AOSP Android 10内置FridaGadget实践01

AOSP Android 10内置FridaGadget实践02(完)|

其他检测思路与反思

1.检测方法签名信息,frida在hook方法的时候会把java方法转为native方法

2.Frida在attach进程注入SO时会显式地校验ELF_magic字段,不对则直接报错退出进程,可以手动在内存中抹掉SO的magic,达到反调试的效果

1
2
3
4
somain = api->solist_get_somain ();
gum_init_soinfo_details (&details, somain, api, &ranges);
api->solist_get_head ()
gum_init_soinfo_details (&details, si, api, &ranges);
1
2
int getsomainoff = findsym("/system/bin/linker64","__dl__ZL6somain");
*(long*)((char*)start+getsomainoff)=0;

3.通常inline hook第一条指令是mov 常数到寄存器,然后第二条是一个br 寄存器指令。检查第二条指令高16位是不是0xd61f,就可以判断目标函数是否被inline hook了!

4.还可以去hook加固壳,现在很多加固厂商都antifrida了,从壳中的代码去分析检测思路

反调试现状 详细说明
检测方式多样 从通用检测、hook检测到源码检测,方式层出不穷。源码检测可以针对每行代码都能开发出不同检测方式,Frida指纹过多。
检测位置不确定 一般是单独开线程跑,也可以在关键函数执行前判断
强混淆加大定位难度 反调试通常埋几行代码,但结合混淆可达万行代码,不考虑效率可膨胀更多,定位极难