引
言
- 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
9871
9826
2
17
:
27
:
24
pts/
5
00
:
00
:
00
grep antifrida_demo1
platina:/ # cat /proc/
9608
/maps
12
c00000
-13200000
rw-p
00000000
00
:
00
0
[anon:dalvik-main space (region space)]
13200000
-140
c0000 ---p
00000000
00
:
00
0
[anon:dalvik-main space (region space)]
140
c0000
-14100000
---p
00000000
00
:
00
0
[anon:dalvik-main space (region space)]
14100000
-14140000
rw-p
00000000
00
:
00
0
[anon:dalvik-main space (region space)]
14140000
-14180000
---p
00000000
00
:
00
0
[anon:dalvik-main space (region space)]
14180000
-14240000
rw-p
00000000
00
:
00
0
[anon:dalvik-main space (region space)]
14240000
-16
b80000 ---p
00000000
00
:
00
0
[anon:dalvik-main space (region space)]
16
b80000
-32
c00000 rw-p
00000000
00
:
00
0
[anon:dalvik-main space (region space)]
70
ab7000
-70d
61000 rw-p
00000000
103
:
2d
1989
/system/framework/arm64/boot.art
70d
61000
-70e76000
rw-p
00000000
103
:
2d
1953
/system/framework/arm64/boot-core-libart.art
70e76000
-70
eb1000 rw-p
00000000
103
:
2d
1971
/system/framework/arm64/boot-okhttp.art
70
eb1000
-70f
0b000 rw-p
00000000
103
:
2d
1947
/system/framework/arm64/boot-bouncycastle.art
70f
0b000
-70f
54000 rw-p
00000000
103
:
2d
1944
/system/framework/arm64/boot-apache-xml.art
70f
54000
-70f
57000 rw-p
00000000
103
:
2d
1932
/system/framework/arm64/boot-QPerformance.art
70f
57000
-70f
59000 rw-p
00000000
103
:
2d
1935
/system/framework/arm64/boot-UxPerformance.art
70f
59000
-718
c6000 rw-p
00000000
103
:
2d
1959
/system/framework/arm64/boot-framework.art
718
c6000
-7190
b000 rw-p
00000000
103
:
2d
1956
/system/framework/arm64/boot-ext.art
7190
b000
-71
a29000 rw-p
00000000
103
:
2d
1980
/system/framework/arm64/boot-telephony-common.art
71
a29000
-71
a3a000 rw-p
00000000
103
:
2d
1986
/system/framework/arm64/boot-voip-common.art
71
a3a000
-71
a53000 rw-p
00000000
103
:
2d
1962
/system/framework/arm64/boot-ims-common.art
71
a53000
-71
aab000 rw-p
00000000
103
:
2d
1965
/system/framework/arm64/[email protected]
71
aab000
-71
ad1000 rw-p
00000000
103
:
2d
1968
/system/framework/arm64/[email protected]
某一列为例子
复制代码 隐藏代码
70
ab7000
-70d
61000 rw-p
00000000
103
:
2d
1989
/system/framework/arm64/boot.art
分别含义如下:
复制代码 隐藏代码
本段内存映射的虚拟地址空间范围,对应vm_area_struct中的vm_start和vm_end
此段虚拟地址空间的属性。每种属性用一个字段表示,r表示可读,w表示可写,x表示可执行,p和s共用一个字段,互斥关系,p表示私有段,s表示共享段,如果没有相应权限,则用’-’代替
00000000
针对有名映射,指本段映射地址在文件中的偏移
103
:
2d 所映射的文件所属设备的设备号,
1989
映射文件所属节点号
映射的文件
但我们用 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 函数。
总结一下
由于 elf 需要了解elf格式 扫内存需要用到算法。这对我来说还是有点挑战性的。
所以我只演示三个:
代码演示
1.判断 maps linker的文件 是否存在 tmp 目录
复制代码 隐藏代码
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[BUFFER_LEN];
int
fd =
0
;
// 64 位地址
unsigned
long
long
base;
unsigned
long
long
end;
unsigned
long
offset;
char
path[
256
];
char
perm[
5
];
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 线程名称
复制代码 隐藏代码
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[BUFFER_LEN] =
""
;
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[BUFFER_LEN] =
""
;
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.检查使用的文件描述符
复制代码 隐藏代码
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[BUFFER_LEN] =
""
;
char
buf[BUFFER_LEN] =
""
;
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 有几个方案处理:
__openat 我们可以直接hook 为了以防万一也 hook syscall
1.通过 __openat
复制代码 隐藏代码
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 函数调用的时候,如下:
复制代码 隐藏代码
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
复制代码 隐藏代码
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[
2
].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
复制代码 隐藏代码
// syscall.S
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)
复制代码 隐藏代码
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生成的残留文件 放在系统目录下,由于权限设置不了 不然就可以解决了
原文始发于微信公众号(Rot5pider安全团队):frida 检测思路
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论