frida 检测
本帖最后由 2016976438 于 2023-5-9 19:37 编辑# frida 检测
**(本章内容需要用到真机)**
frida 特征检测仿造自**qtfreet00** 和 **darvincisec**:
- https://github.com/qtfreet00/AntiFrida
- https://github.com/darvincisec/DetectFrida
看样子还是好几年前的,看来大佬几年前就摸透了
## 项目创建
- 首先我们创建 **cpp** 的项目
- 创建完的结构如下
- 我并不擅长写 android,后续我全部都写 android 日志进行交互。
关于android 日志 请参考文档:
https://developer.android.google.cn/ndk/reference/group/logging
## proc self maps 说明
通过下面指令可以看到内存映射段
```
cat /proc/self/maps
```
```
platina:/ # ps -ef|grep antifrida_demo1
u0_a214 9608 31372 1 17:26:01 ? 00:00:01 com.luckfollow.antifrida_demo1
root 98719826 2 17:27:24 pts/5 00:00:00 grep antifrida_demo1
platina:/ # cat /proc/9608/maps
12c00000-13200000 rw-p 00000000 00:00 0
13200000-140c0000 ---p 00000000 00:00 0
140c0000-14100000 ---p 00000000 00:00 0
14100000-14140000 rw-p 00000000 00:00 0
14140000-14180000 ---p 00000000 00:00 0
14180000-14240000 rw-p 00000000 00:00 0
14240000-16b80000 ---p 00000000 00:00 0
16b80000-32c00000 rw-p 00000000 00:00 0
70ab7000-70d61000 rw-p 00000000 103:2d 1989 /system/framework/arm64/boot.art
70d61000-70e76000 rw-p 00000000 103:2d 1953 /system/framework/arm64/boot-core-libart.art
70e76000-70eb1000 rw-p 00000000 103:2d 1971 /system/framework/arm64/boot-okhttp.art
70eb1000-70f0b000 rw-p 00000000 103:2d 1947 /system/framework/arm64/boot-bouncycastle.art
70f0b000-70f54000 rw-p 00000000 103:2d 1944 /system/framework/arm64/boot-apache-xml.art
70f54000-70f57000 rw-p 00000000 103:2d 1932 /system/framework/arm64/boot-QPerformance.art
70f57000-70f59000 rw-p 00000000 103:2d 1935 /system/framework/arm64/boot-UxPerformance.art
70f59000-718c6000 rw-p 00000000 103:2d 1959 /system/framework/arm64/boot-framework.art
718c6000-7190b000 rw-p 00000000 103:2d 1956 /system/framework/arm64/boot-ext.art
7190b000-71a29000 rw-p 00000000 103:2d 1980 /system/framework/arm64/boot-telephony-common.art
71a29000-71a3a000 rw-p 00000000 103:2d 1986 /system/framework/arm64/boot-voip-common.art
71a3a000-71a53000 rw-p 00000000 103:2d 1962 /system/framework/arm64/boot-ims-common.art
71a53000-71aab000 rw-p 00000000 103:2d 1965 /system/framework/arm64/boot-miuisdk@boot.art
71aab000-71ad1000 rw-p 00000000 103:2d 1968 /system/framework/arm64/boot-miuisystemsdk@boot.art
```
就以某一段为例
```
70ab7000-70d61000 rw-p 00000000 103:2d 1989 /system/framework/arm64/boot.art
```
分别含义如下:
```
70ab7000-70d61000 本段内存映射的虚拟地址空间范围,对应vm_area_struct中的vm_start和vm_end
rw-p 此段虚拟地址空间的属性。每种属性用一个字段表示,r表示可读,w表示可写,x表示可执行,p和s共用一个字段,互斥关系,p表示私有段,s表示共享段,如果没有相应权限,则用’-’代替
00000000 针对有名映射,指本段映射地址在文件中的偏移
103:2d 所映射的文件所属设备的设备号,
1989 映射文件所属节点号
/system/framework/arm64/boot.art映射的文件
```
但我们用 frida 使用 spwan 附加上去后
```
frida -U -f com.luckfollow.antifrida_demo1
```
其 maps 中多处了这一段
```
platina:/ # cat /proc/12186/maps|grep frida
7ea7841000-7ea8231000 r--p 00000000 fc:00 1114131 /data/local/tmp/re.frida.server/frida-agent-64.so
7ea8232000-7ea8f4e000 r-xp 009f0000 fc:00 1114131 /data/local/tmp/re.frida.server/frida-agent-64.so
7ea8f4e000-7ea901d000 r--p 0170b000 fc:00 1114131 /data/local/tmp/re.frida.server/frida-agent-64.so
7ea901e000-7ea903a000 rw-p 017da000 fc:00 1114131 /data/local/tmp/re.frida.server/frida-agent-64.so
```
这一段应该是 **frida** 附加上去的。
我们借助 **ida pro** 看一下
可以看到在内存中 每个 **segments** 的具体情况。
地址跟
```
platina:/ # cat /proc/12186/maps|grep frida
7ea7841000-7ea8231000 r--p 00000000 fc:00 1114131 /data/local/tmp/re.frida.server/frida-agent-64.so
7ea8232000-7ea8f4e000 r-xp 009f0000 fc:00 1114131 /data/local/tmp/re.frida.server/frida-agent-64.so
7ea8f4e000-7ea901d000 r--p 0170b000 fc:00 1114131 /data/local/tmp/re.frida.server/frida-agent-64.so
7ea901e000-7ea903a000 rw-p 017da000 fc:00 1114131 /data/local/tmp/re.frida.server/frida-agent-64.so
```
完美对应
## frida 检测思路
以下只是个人结合开源 **antifrida**的一些列开源项目 ,并没有看 **frida 源码** 并没有深入追究,肯定会有漏的情况。
上诉按道理直接 看 **maps** 中映射的文件是否包含 **/tmp** 目录就可以了。但可能有些**改目录**的情况。所以检测会根据 **内存特征** 或者 **elf**中描述信息对比。
**elf** 目前我还不**了解**。 先看看基于 **内存** 和 **线程**的
### 1.基于线程
我们可以看到线程中多了 **gmain** 和 **pool-frida**
我们可以通过
```
/proc/self/task/thread_id/status
/proc/self/task/thread_id/stat
```
获取线程名
```
platina:/proc/14270/task # cat 14294/status
Name: pool-frida
State:t (tracing stop)
Tgid: 14270
Pid: 14294
PPid: 31372
.....
platina:/proc/14270/task # cat 14294/stat
14294 (pool-frida) t 31372 31372 0 0 -1 1077952576 14 0 0 0 0 0 0 0 20 0 19 0 196500473 5490044928 19924 18446744073709551615 424577748992 424577773808 549642079824 545349434336 547774104380 0 4612 1 1073775864 1 0 0 -1 1 0 0 0 0 0 424577777664 424577779096 425002627072 549642081909 549642082008 549642082008 549642084318 0
```
### 2.打开的文件
```
ls /pro/self/fd -l
```
```
platina:/proc/14270/task/14294/fd # ls -l /proc/14270/fd|grep /tmp
l-wx------ 1 u0_a214 u0_a214 64 2023-05-06 20:39 43 -> /data/local/tmp/re.frida.server/linjector-45
```
可以看到**fd**软链接到文件 **linjector**
### 3.内存特征
通过 ida pro **segments** 中 ``CODE``段是代码段。
我们双击点进去下面看
找到了 **frida_agent_main**方法。
我们可以通过这个方法一些**代码特征码** 来寻找是否被 **frida** 注入了。
当然个人觉得 直接解析 **elf** 更快点,看特征符号是否包含 **frida_agent_main**方法。
内存搜索需要带上算法 (BM 或 Sunday) ,那就不好说了。
### 4.trace 检测
当**调试工具** 进行附加程序的时候,会产生**TracerPid**。
如下图所示:
有些程序会 **自己附加自己** 达到 **frida 无法附加**的功能
不过只用 **frida 附加** 不会出现 **PtracerPid**. 原因不知,愿大佬解答
### 5.总结检测
上述所说,除了 ``/proc/self/fd`` 只是用到目录函数外。
其余的都需要用到 **openat** 函数。
总结一下
- **openat**
- **/proc/self/maps**
- 通过判断 elf 是否是执行段扫描代码特征码
- 解析 elf 中导出符号名称
- 直接解析链接名称是否出现非系统目录
- **/proc/self/task/thread_id/stat**
- 判断线程名称是否存在 **frida** **gmain**关键字
- **/proc/self/status**
- 判断是否有调试工具附加
- **opendir->open**
- **/proc/self/fd**
- 查找被打开的文件描述符的文件
由于 elf 需要了解elf格式扫内存需要用到算法。这对我来说还是有点挑战性的。
所以我只演示三个:
- 直接解析内存链接名称是否带/tmp
- 判断线程名称是否存在 **frida** **gmain**关键字
- 查找被打开的文件描述符的文件是否是/tmp路径
## 代码演示
### 1.判断 maps linker的文件 是否存在 tmp 目录
```cpp
static const char *CHECK_FEATURE = "/tmp";
static const char *TAG = "ANTI_FRIDA";
static const char *PROC_MAPS = "/proc/self/maps";
void check_path()
{
char buffer;
int fd = 0;
// 64 位地址
unsigned long long base;
unsigned long long end;
unsigned long offset;
char path;
char perm;
if ((fd = openat(AT_FDCWD, PROC_MAPS, O_RDONLY)) > 0)
{
while (read_line(fd, buffer, BUFFER_LEN) > 0)
{
// sscanf 函数用于 格式化输入 到参数
// x 十六进制 l长整型 * 可要可不要
if (sscanf(buffer, "%x-%lx %4s %lx %*s %*s %s", &base, &end, perm, &offset, path) < 5)
continue;
if (strlen(path) == 0)
continue;
// check tmp path
if (strstr(path, CHECK_FEATURE) != NULL)
{
__android_log_print(ANDROID_LOG_DEBUG, TAG, "maps 不通过:%s", path);
break;
}
}
close(fd);
}
}
```
### 2.检查task 中 stat 线程名称
```cpp
void check_thread_name()
{
static const char *PROC_TASK = "/proc/self/task";
static const char *PROC_STATUS = "/proc/self/task/%s/stat";
static const char *THREAD_NAME1 = "gmain";
static const char *THREAD_NAME2 = "pool-frida";
// 打开目录
DIR *dir = opendir(PROC_TASK);
if (dir != NULL)
{
struct dirent *entry = NULL;
// 遍历子目录
while ((entry = readdir(dir)) != NULL)
{
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..")==0)
{
continue;
}
char filePath = "";
snprintf(filePath, sizeof(filePath), PROC_STATUS, entry->d_name);
int fd = openat(AT_FDCWD, filePath, O_RDONLY | O_CLOEXEC, 0);
if (fd > 0)
{
char buf = "";
read_line(fd, buf, BUFFER_LEN);
if (strstr(buf, THREAD_NAME1) != NULL || strstr(buf, THREAD_NAME2) != NULL)
{
__android_log_print(ANDROID_LOG_DEBUG, TAG, "thread 不通过: %s",buf);
break;
}
}
}
closedir(dir);
}
}
```
### 3.检查使用的文件描述符
```cpp
void check_fd()
{
static const char *PROC_FD = "/proc/self/fd";
DIR *dir = opendir(PROC_FD);
if (dir != NULL)
{
struct dirent *entry = readdir(dir);
struct stat filestat;
while ((entry = readdir(dir)) != nullptr)
{
char filepath = "";
char buf = "";
snprintf(filepath, sizeof(filepath), "/proc/self/fd/%s", entry->d_name);
// linker 文件状态
lstat(filepath, &filestat);
// st_mode 包含 文件权限 和 文件类型
// (__buf.st_mode & S_IFMT) 代表只取 高位4位 文件类型
// S_IFLNK 文件类型是 linker 链接文件
if ((filestat.st_mode & S_IFMT) == S_IFLNK)
{
// 取linker的实际路径
readlinkat(AT_FDCWD, filepath, buf, BUFFER_LEN);
if (strstr(buf, CHECK_FEATURE) != NULL)
{
__android_log_print(ANDROID_LOG_DEBUG, TAG, "FD 未通过: %s",buf);
}
}
}
closedir(dir);
}
}
```
### frida 演示
我们使用frida 进行附加
```
frida -U -f com.luckfollow.antifrida_demo1
```
在 **logcat** 可以看到下面的信息
```
D/ANTI_FRIDA: maps 不通过:/data/local/tmp/re.frida.server/frida-agent-64.so
D/ANTI_FRIDA: thread 不通过: 25187 (gmain) S 31372 31372 0 0 .....
D/ANTI_FRIDA: FD 未通过: /data/local/tmp/re.frida.server/linjector-4
```
## 基于frida hook __openat 完成过检测
是不是 这样就安全了呢?答案肯定是否的
上诉所有检测中,几乎都离不开 **openat** 函数。
哪怕是 **opendir** 底层也是用到 **open** 函数打开目录文件描述符
而 **open** 和 **openat** 最终都使用到了 **__openat** 的 **svc**调用。
所以说我们可以 hook **__openat** 有几个方案处理:
- 判断 proc
- 判断 maps
- 返回值改 **-1**
- 重定向修正文件
- 判断 fd
- 返回值改**-1**
- 判断 task
- 返回值改**-1**
**__openat** 我们可以直接**hook**为了以防万一也 **hook** **syscall**
### 1.通过 __openat
```js
function anti_open() {
//prepared fun
let openatPtr: NativePointer | null = NativeUtil.open_io.find_real_openat();
let openat_fun = NativeUtil.open_io.openat_fun(openatPtr!);
Interceptor.replace(openatPtr!, new NativeCallback(function (fd, pathname, flags) {
const pathnamestr = pathname.readCString();
if (pathnamestr != null) {
if (pathnamestr.indexOf("proc") != -1) {
if (pathnamestr.indexOf("maps") > 0) return maps_handle(pathnamestr);
if (pathnamestr.indexOf("task") > 0 && pathnamestr.indexOf("status") > 0) return thread_handle(pathnamestr);
if (pathnamestr.indexOf("fd") > 0) return fd_handle(pathnamestr);
}
}
return openat_fun(fd, pathname, flags);
}, "int", ["int", "pointer", "int"]));
}
function maps_handle(pathnamestr: string) {
DebugUtil.LOGD("anti_maps:" + pathnamestr);
return -1;
}
function thread_handle(pathnamestr: string) {
DebugUtil.LOGD("anti_thread:" + pathnamestr);
return -1;
}
function fd_handle(pathnamestr: string) {
DebugUtil.LOGD("anti_fd:" + pathnamestr);
return -1;
}
function status_handle(pathnamestr: string) {
DebugUtil.LOGD("anti_status:" + pathnamestr);
return -1;
}
```
### 2.通过syscall
**syscall** 比较麻烦。 需要判断 **arm64** 和 **arm32**。
当 **openat** 使用 **syscall** 函数调用的时候,如下:
```cpp
int pick_openat(int fd, const char *pathname, int flags,...)
{
// 0 系统 call
// 1 原始openat
// 2 自定义系统 call
static int SYSCALL_INVOKE = 0;
switch (SYSCALL_INVOKE)
{
case 0:
return syscall(__NR_openat,fd,pathname,flags);
default:
return openat(fd,pathname,flags);
}
}
```
我们虽然不能 **hook svc 的内核调用**
但是可以hook 到外部使用 svc 的 **syscall**
```js
function anti_syscall_openat() {
let syscallPtr = NativeUtil.unistd.get_syscall_call_ptr();
let syscallFun = NativeUtil.unistd.get_syscall_call_function()!;
let openatPtr: NativePointer | null = NativeUtil.open_io.find_real_openat();
let openat_fun = NativeUtil.open_io.openat_fun(openatPtr!);
function handle_openat(args: NativePointer[], sysFun: NativeFunction<any, any>): number {
const pathnamestr = args.readCString();
if (pathnamestr != null && pathnamestr.indexOf("proc") != -1) {
if (pathnamestr.indexOf("maps") > 0) return maps_handle(pathnamestr);
if (pathnamestr.indexOf("task") > 0 && pathnamestr.indexOf("status") > 0) return thread_handle(pathnamestr);
if (pathnamestr.indexOf("fd") > 0) return fd_handle(pathnamestr);
}
return sysFun.apply(null, args);
}
if (Process.arch === "arm64") {
DebugUtil.LOGD("anti_syscall_openat start arm64...")
Interceptor.replace(syscallPtr, new NativeCallback(function (sysSign, arg1, arg2, arg3, arg4, arg5, arg6) {
if (sysSign === NativeUtil.unistd.syscall_asm.__NR_openat) {
DebugUtil.LOGW("syscall openat arm64");
return handle_openat([...arguments], syscallFun);
}
return syscallFun(sysSign, arg1, arg2, arg3, arg4, arg5, arg6);
}, "int", ["int", "pointer", "pointer", "pointer", "pointer", "pointer", "pointer"]))
} else {
DebugUtil.LOGD("anti_syscall_openat start arm32...")
Interceptor.replace(syscallPtr, new NativeCallback(function (sysSign, arg1, arg2, arg3) {
if (sysSign === NativeUtil.unistd.syscall_asm.__NR_openat) {
DebugUtil.LOGW("syscall openat arm32");
return handle_openat([...arguments], syscallFun);
}
return syscallFun(sysSign, arg1, arg2, arg3);
}, "int", ["int", "pointer", "pointer", "pointer"]))
}
}
```
### 3.重定向maps
当然,我们还可以生成一个处理过的 **maps** 文件 重定向上面去。
这也可以防止 **误伤** 或者 **检测内容** 的问题
操作可以留给大家尝试,
## 自定义syscall 防止被hook
为了防止被 **hook** 寻常的 **__openat** 以及 **syscall**
我们自定义 **syscall** 完成防止hook
**(syscall 使用我会在 arm学习篇 写几篇教程)**
为了方便,我只实现 **arm64**
```asm
// syscall.S
#include "bionic_asm.h"
#if defined(__aarch64__)
ENTRY(my_syscall)
// x8 系统调用号
mov x8, x0
// x0 - x5 系统函数传参
mov x0, x1
mov x1, x2
mov x2, x3
mov x3, x4
mov x4, x5
mov x5, x6
// 系统调用
svc #0
// 当 CF = 1代表无符号 溢出 则 x0 是负数 有错误码
cmn x0, #(MAX_ERRNO + 1)
// hi 条件为 CF = 1 一般用于 无符号比较大小
cneg x0, x0, hi
// 调用 __set_errno_internal传入错误码
b.hi __set_errno_internal
ret
END(my_syscall)
#endif
```
```cpp
extern "C" int my_syscall(int sys_no, ...);
int pick_openat(int fd, const char *pathname, int flags, ...)
{
// 0 系统 call
// 1 原始openat
// 2 自定义系统 call
static int SYSCALL_INVOKE = 2;
switch (SYSCALL_INVOKE)
{
case 0:
return syscall(__NR_openat, fd, pathname, flags);
case 2:
return my_syscall(__NR_openat, fd, pathname, flags);
default:
return openat(fd, pathname, flags);
}
}
```
## 总结
实际上有很多问题。
比没有采用**elf执行段** 中的 **内存特征** 去检测 **frida**。
因为 **elf结构** 我还不太了解
总归来说, **大多数**都用 **openat** 来打开 **/proc/self/maps** 获取内存映射信息,方便扫描内存。
不过应该还有其他方式,目前我只能通过开源的方案去寻找答案
**svc** 搜过一些资料还是可以被观察到的。比如一些指令跟踪。 或者 一些基于 linux 限制强制跳转到``__set_errno_internal``函数进行转发处理。目前我还看不懂。能做到这些的大佬指定是个大佬
不过还有一种方案我也测试了的。
**debug_cat大佬**发了我一个他改版的**frida** 去掉了内存特征并将残留文件变成随机名,奈何我不会改机,原可以将 frida生成的残留文件 放在**系统目录**下,由于权限设置不了 不然就可以解决了 debug_cat大佬发了我一个他改版的frida 去掉了内存特征并将残留文件变成随机名,奈何我不会改机,原可以将 frida生成的残留文件 放在系统目录下,由于权限设置不了 不然就可以解决了。——我想要这个可以吗? 编程帮助法 不错不错,感谢分享 {:1_918:}进度太快了,我晕车~ debug_cat 发表于 2023-5-9 22:55
进度太快了,我晕车~
大佬停下,我想上车~ 看的有点晕,内容太深了 认真学习 感谢大佬 这东西太复杂了,俺只能慢慢学习了! 太难了,慢慢学习