Frida特征检测

frida检测与防护

端口检测

检测 frida-server 默认端口 27042 是否开放。

1
2
3
4
5
6
7
8
9
10
11
12
13
bool detectByDefaultPort(){
struct sockaddr_in sa;
int sock = socket(AF_INET , SOCK_STREAM , 0);
memset(&sa, 0, sizeof(sa));
sa.sin_family = AF_INET;
sa.sin_port = htons(27047);
inet_aton("127.0.0.1", &(sa.sin_addr));
if (connect(sock , (struct sockaddr*)&sa , sizeof sa) != -1) {
LOGD("%s => Found frida-server: running on default port", TAG);
return true;
}
return false;
}

进程名检测

遍历运行的进程列表(/proc目录下),检测进程名是否包含 “frida-server” 。

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 detectByProcessName(){
std::vector<std::string> pids;
std::string target_name = "frida";
DIR* proc_dir = opendir("/proc");
std::string current_pid = std::to_string(getpid());//当前进程pid
//获取所有进程id
if (proc_dir != nullptr){
struct dirent* entry;
while((entry = readdir(proc_dir)) != nullptr){
// 文件是否是目录
if (entry->d_type == DT_DIR){
std::string dir_name(entry->d_name);
//如果目录名是数字,则为进程文件,还需要排除当前进程,因为当前进程的应用名为FridaDetector,包含目标字符串
if (std::all_of(dir_name.begin(), dir_name.end(), ::isdigit) && dir_name != current_pid) {
pids.push_back(dir_name);
}
}
}
closedir(proc_dir);
}
//判断进程名是否包含frida字样
for (int i = 0; i < pids.size(); i++) {
//创建输入文件流对象, 用于读取数据
std::string comm_path = "/proc/" + pids[i] + "/comm";
std::ifstream comm_file(comm_path);
if (comm_file.is_open()) {//是否成功打开对应文件
std::string process_name;
std::getline(comm_file, process_name);//读一行
comm_file.close();
//LOGD("%s => %s: pid is %s", TAG, process_name.c_str(), pids[i].c_str());
if (process_name.find(target_name) != std::string::npos) {
LOGD("%s => Found frida-server: pid is %s", TAG, pids[i].c_str());
return true;
}
}
}
return false;
}

双进程守护

应用程序可通过开启双进程守护,即让子进程附加父进程,从而使得 frida 不能 attach 目标进程,但 frida 可通过 spawn 方式启动,从而让子进程无法附加到父进程(那如果子进程附加不上父进程,导致整个应用进程关闭呢?)。

D-Bus 协议通信

frida 使用 D-Bus 协议通信,这个通信协议并不常见,因此可以遍历 /proc/net/tcp 文件,或者直接从 0-65535 向每个开放的端口发送 D-Bus 认证消息,哪个端口回复了 REJECT,哪个端口上就运行了 frida-server。(为啥一样的代码,而我的却没有回复消息??)

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 detectByDBus(){
int sock;
struct sockaddr_in sa;
char res[7];
//初始化 sockaddr_in 结构体
memset(&sa, 0, sizeof(sa));
sa.sin_family = AF_INET;//AF_INET表示使用 IPv4 地址
inet_aton("127.0.0.1", &sa.sin_addr);//将 IP 地址字符串 "127.0.0.1" 转换为网络字节序的二进制形式, 并将其存储在 sa.sin_addr 中
//遍历端口,发送d-bus认证消息,回复了REJECT的端口就是frida-server
for (int i = 0; i <= 65535; ++i) {
sock = socket(AF_INET, SOCK_STREAM, 0);
sa.sin_port = htons(i);//设置端口号, htons(i) 将主机字节序转换为网络字节序
//尝试使用 connect 函数连接到指定的 IP 地址和端口号
if (connect(sock, (struct sockaddr*)(&sa), sizeof sa) != -1){
LOGD("%s => Connect on port %d", TAG, i);
memset(res, 0, 7);
//发送 d-bus 认证消息
send(sock, "\x00", 1, NULL);
send(sock, "AUTH\r\n", 6, NULL);
usleep(1000); // Give it some time to answer
LOGD("%s => send Message on port %d", TAG, i);
//接收数据到res中
//接收不到消息!!!???
if (recv(sock, res, 6, MSG_DONTWAIT) != -1){
LOGD("%s => recv Message on port %d: %s", TAG, i, res);
if (strcmp(res, "REJECT") == 0){
LOGD("%s => Found frida-server: running on port %d", TAG, i);
close(sock);
return true;
}
}else{
LOGD("%s => can't recv Message on port %d", TAG, i);
}
close(sock);
}
}
return false;
}

扫描 maps 文件

被 frida 附加的进程,在 proc/self/maps 文件中会多出如下文件(很奇怪为什么图中是 deleted ):

因此通过扫描 maps 文件是否存在“frida”字样可以判断进程是否被 frida 附加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
bool detectByMaps(){
std::ifstream maps_file("/proc/self/maps");
if (maps_file.is_open()) {
std::string line;
while (std::getline(maps_file, line)) {
if (line.find("frida") != std::string::npos) {
LOGD(" %s => Found Frida agent in current process by scanf /proc/self/maps", TAG);
maps_file.close();
return true;
}
}
maps_file.close();
}
return false;
}

扫描 task 目录

在 /proc/pid/task 目录下存储的是当前进程下的线程相关信息,文件结构类似 /proc/pid 目录。如果 frida 附加到了当前进程,那么在 task 目录下,就会存在运行 gmain、gdbus、gum-js-loop、pool-frida等的线程。

  • gmain:Frida 使用 Glib 库,其中的主事件循环被称为 GMainLoop。在 Frida 中,gmain 表示 GMainLoop 的线程。
  • gdbus:GDBus 是 Glib 提供的一个用于 D-Bus 通信的库。在 Frida 中,gdbus 表示 GDBus 相关的线程。
  • gum-js-loop:Gum 是 Frida 的运行时引擎,用于执行注入的 JavaScript 代码。gum-js-loop 表示 Gum 引擎执行 JavaScript 代码的线程。
  • pool-frida:Frida 中的某些功能可能会使用线程池来处理任务,pool-frida 表示 Frida 中的线程池。

因此通过扫描 task 目录下的线程的 comm 文件或 status 文件,看是否存在上述字符。

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
bool detectByTask(){
//获取当前线程的task目录
DIR* task_dir = opendir("/proc/self/task");
std::vector<std::string> pids;
std::string current_pid = std::to_string(getpid());//当前进程pid
if (task_dir != nullptr) {
struct dirent* entry;
while((entry = readdir(task_dir)) != nullptr){
if (entry->d_type == DT_DIR){
std::string dir_name(entry->d_name);
//如果目录名是数字,则为进程目录
if (std::all_of(dir_name.begin(), dir_name.end(), ::isdigit)) {
pids.push_back(dir_name);
}
}
}
closedir(task_dir);
}
for (int i = 0; i < pids.size(); i++) {
std::string comm_path = "/proc/self/task/" + pids[i] + "/comm";
std::ifstream comm_file(comm_path);
//打开comm文件,查看进程名
if (comm_file.is_open()) {
std::string process_name;
std::getline(comm_file, process_name);//读一行
comm_file.close();
if (process_name.find("gmain") != std::string::npos || process_name.find("gdbus") != std::string::npos
|| process_name.find("gum-js-loop") != std::string::npos || process_name.find("pool-frida") != std::string::npos) {
LOGD("%s => Found Frida agent in current process by scanf /proc/self/task, process_name: %s", TAG, process_name.c_str());
return true;
}
}
}
return false;
}

内存搜索

借助 /proc/self/maps 文件,在内存中扫描 frida 库特征,例如字符串 “LIBFRIDA”、“rpc”等。

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
bool detectByScanfMemory(){
std::string target_name = "LIBFRIDA";//可指定多个特征字符串
char permission[512];
unsigned long start, end;
std::ifstream maps_file("/proc/self/maps");
if (maps_file.is_open()) {
std::string line;
while (std::getline(maps_file, line)) {
sscanf(line.c_str(), "%lx-%lx %s", &start, &end, permission);
//如果可执行
if (permission[2] == 'x') {
size_t region_size = end - start;
char* buffer = new char[region_size];
// 使用 /proc/self/mem 读取内存区域
std::ifstream mem_file("/proc/self/mem", std::ios::binary);
if (mem_file.is_open()) {
//从start ~ end的内存区域中搜索target_name字符串,如果存在则返回true
mem_file.seekg(start);
mem_file.read(buffer, region_size);
LOGD("%s => search memory: %lx - %lx", TAG, start, end);
// 在内存区域中搜索 target_name
if (std::search(buffer, buffer + region_size, target_name.begin(), target_name.end()) != buffer + region_size) {
delete[] buffer;
mem_file.close();
maps_file.close();
LOGD("%s => Found target string in memory: %s", TAG, target_name.c_str());
return true;
}
mem_file.close();
}
delete[] buffer;
}
}
maps_file.close();
}
return false;
}

inlinehook检测

通过 IDA 分析被 frida 调试的进程(需先frida附加到进程再使用 IDA 调试),会发现被 hook 的函数的开头改成了一串16进制数,例如

我们可以通过检测每个函数的开头是否有0xd61f020058000050这样的一段代码来判断进程是否被frida 附加了。具体参考从inlinehook角度检测frida-Android安全-看雪-安全社区|安全招聘|kanxue.com

自定义 syscall 防 hook

当然,以上检测方法中使用到的函数如果被 frida hook,显然就绕过了这些检测,因此对于最关键的函数,例如open、read函数等,需要自定义相应的 syscall,例如:

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
#include "bionic_asm.h"

.text
.globl my_openat
.type my_openat,function
my_openat:
.cfi_startproc
mov ip, r7
.cfi_register r7, ip
ldr r7, =__NR_openat
swi #0
mov r7, ip
.cfi_restore r7
cmn r0, #(4095 + 1)
bxls lr
neg r0, r0
b __set_errno_internal
.cfi_endproc
.size my_openat, .-my_openat;

.text
.globl my_read
.type my_read,function
my_read:
.cfi_startproc
mov ip, r7
.cfi_register r7, ip
ldr r7, =__NR_read
swi #0
mov r7, ip
.cfi_restore r7
cmn r0, #(MAX_ERRNO + 1)
bxls lr
neg r0, r0
b __set_errno_internal
.cfi_endproc
.size my_read, .-my_read;

具体使用可参考https://github.com/qtfreet00/AntiFrida和https://github.com/muellerberndt/frida-detection

总结

对于 frida 的检测点远远不止这些,想要深入了解就需要阅读 frida 源码。然而对于 frida 反检测,也需要阅读 frida 源码,修改掉其中的特征,从而达到反检测目的。


参考:

[翻译]多种特征检测 Frida-外文翻译-看雪-安全社区|安全招聘|kanxue.com

https://bbs.kanxue.com/thread-278145-1.htm

frida 检测 - 『移动安全区』 - 吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

https://xxr0ss.github.io/post/frida_detection/

https://github.com/qtfreet00/AntiFrida

https://frida.re/slides/osdc-2015-the-engineering-behind-the-reverse-engineering.pdf

从inlinehook角度检测frida-Android安全-看雪-安全社区|安全招聘|kanxue.com


Frida特征检测
http://example.com/2024/07/29/Android安全/Frida特征检测/
作者
gla2xy
发布于
2024年7月29日
许可协议