首页
社区
课程
招聘
5
[原创]Frida检测思路
发表于: 4天前 2446

[原创]Frida检测思路

4天前
2446

1、frida-core

Frida 注入目标进程后会使用Interceptor.attach对退出进程的方法进行inline hook。此处可以定位三个关键函数:exit、_exit、abort
GitHub源码链接:
https://github.com/frida/frida-core/blob/1018aca28d49ace21cbb5d54d2328c0283d327fe/lib/payload/exit-monitor.vala#L34

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#if WINDOWS
            interceptor.attach ((void *) Gum.Process.find_module_by_name ("kernel32.dll").find_export_by_name ("ExitProcess"),
                listener);
#else
            var libc = Gum.Process.get_libc_module ();
            const string[] apis = {
                "exit",
                "_exit",
                "abort",
            };
            foreach (var symbol in apis) {
                interceptor.attach ((void *) libc.find_export_by_name (symbol), listener);
            }
#endif

2、frida-gum

frida 调用了gum_interceptor_replace函数对libc库的signal和sigaction函数进行了inline hook。
GitHub链接:https://github.com/frida/frida-gum/blob/0671c27d941d77490fff4ca6dbb9ca10a0f872bb/gum/backend-posix/gumexceptor-posix.c#L228

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
static void
gum_exceptor_backend_attach (GumExceptorBackend * self)
{
  GumInterceptor * interceptor = self->interceptor;
  const gint handled_signals[] = {
    SIGABRT,
    SIGSEGV,
    SIGBUS,
    SIGILL,
    SIGFPE,
    SIGTRAP,
    SIGSYS,
  };
  gint highest, i;
  struct sigaction action;
 
  highest = 0;
  for (i = 0; i != G_N_ELEMENTS (handled_signals); i++)
    highest = MAX (handled_signals[i], highest);
  g_assert (highest > 0);
  self->num_old_handlers = highest + 1;
  self->old_handlers = g_new0 (struct sigaction *, self->num_old_handlers);
 
  action.sa_sigaction = gum_exceptor_backend_on_signal;
  sigemptyset (&action.sa_mask);
  action.sa_flags = SA_SIGINFO | SA_NODEFER;
#ifdef SA_ONSTACK
  action.sa_flags |= SA_ONSTACK;
#endif
  for (i = 0; i != G_N_ELEMENTS (handled_signals); i++)
  {
    gint sig = handled_signals[i];
    struct sigaction * old_handler;
 
    old_handler = g_slice_new0 (struct sigaction);
    self->old_handlers[sig] = old_handler;
    gum_original_sigaction (sig, &action, old_handler);
  }
 
  gum_interceptor_begin_transaction (interceptor);
 
  gum_interceptor_replace (interceptor, gum_original_signal,
      gum_exceptor_backend_replacement_signal, self, NULL);
  gum_interceptor_replace (interceptor, gum_original_sigaction,
      gum_exceptor_backend_replacement_sigaction, self, NULL);
 
  gum_interceptor_end_transaction (interceptor);
}

3、检测思路

前提:已知frida注入目标进程会hook libc库以下目标函数:
{"sigaction", "signal", "exit", "abort", "_exit"}

a.获取目标函数在libc库的地址,并copy这些函数的入口机器码。

b.获取目标函数的偏移。

c.获取这些函数入口的原始机器码,读取磁盘文件/system/lib64/libc.so(此处为arm64-v8架构),通过获取的偏移地址定位目标函数在libc.so文件的位置;copy这些函数的入口机器码。

d.比较从磁盘文件读取的机器码和从内存中读取的机器码,不同则可以确定frida已经注入。

实现代码如下:

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
#include <dlfcn.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <elf.h>
#include <android/log.h>
#include <stdbool.h>
#include <errno.h>
#include <stdio.h>
#include <jni.h>
 
#define LOG_TAG "HookDetection"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define BYTE_BUFFER_SIZE 16
 
// 获取函数地址
void* get_function_address(void* handle, const char* func_name) {
    void* func_addr = dlsym(handle, func_name);
    if (!func_addr) {
        LOGD("[-] Function %s not found in global symbol table", func_name);
    } else {
        LOGD("[+] Function: %s, addr: 0x%lx", func_name, (uintptr_t)func_addr);
    }
    return func_addr;
}
 
// 获取函数偏移
uintptr_t get_function_offset(void* func_addr) {
    Dl_info info;
    if (dladdr(func_addr, &info) == 0) {
        LOGD("[-] Unable to get function info");
        return 0;
    }
    return (uintptr_t)func_addr - (uintptr_t)info.dli_fbase;
}
 
// 读取库文件中的字节
bool read_bytes_from_libso(const char* libpath, uintptr_t offset, uint8_t* buffer, size_t size) {
    int fd = open(libpath, O_RDONLY);
    if (fd == -1) {
        LOGD("[-] Failed to open %s: %s", libpath, strerror(errno));
        return false;
    }
 
    if (lseek(fd, offset, SEEK_SET) == -1) {
        LOGD("[-] Seek failed at %lx in %s: %s", (unsigned long)offset, libpath, strerror(errno));
        close(fd);
        return false;
    }
 
    ssize_t bytes_read = read(fd, buffer, size);
    close(fd);
 
    if (bytes_read != (ssize_t)size) {
        LOGD("[-] Read %zd bytes, expected %zu from %s at offset %lx", bytes_read, size, libpath, (unsigned long)offset);
        return false;
    }
 
    return true;
}
 
// 字节数组转十六进制字符串
void bytes_to_hex_string(const uint8_t* bytes, size_t size, char* hex_string) {
    for (size_t i = 0; i < size; i++) {
        sprintf(hex_string + i * 2, "%02x", bytes[i]);
    }
    hex_string[size * 2] = '\0';
}
 
// 比较内存中的字节和库文件中的字节
bool compare_function_bytes(void* func_addr, uint8_t* file_bytes, size_t size) {
    uint8_t mem_bytes[BYTE_BUFFER_SIZE];
    memcpy(mem_bytes, func_addr, size);
 
    char mem_hex_string[BYTE_BUFFER_SIZE * 2 + 1];
    char file_hex_string[BYTE_BUFFER_SIZE * 2 + 1];
 
    bytes_to_hex_string(mem_bytes, size, mem_hex_string);
    bytes_to_hex_string(file_bytes, size, file_hex_string);
 
    LOGD("[*] Memory: %s | File: %s", mem_hex_string, file_hex_string);
 
    return memcmp(mem_bytes, file_bytes, size) == 0;
}
 
// Hook 检测函数
bool detect_hook(void* handle, const char* lib_path, const char* func_name) {
    void* func_addr = get_function_address(handle, func_name);
    if (!func_addr) return false;
 
    uintptr_t offset = get_function_offset(func_addr);
    if (offset == 0) {
        LOGD("[-] Failed to get offset for %s", func_name);
        return false;
    }
 
    uint8_t file_bytes[BYTE_BUFFER_SIZE];
    if (!read_bytes_from_libso(lib_path, offset, file_bytes, sizeof(file_bytes))) {
        LOGD("[-] Failed to read bytes for %s", func_name);
        return false;
    }
 
    bool is_hooked = !compare_function_bytes(func_addr, file_bytes, sizeof(file_bytes));
    LOGD("[+] %s in %s is %s", func_name, lib_path, is_hooked ? "HOOKED" : "NOT HOOKED");
    return is_hooked;
}
 
// 执行 Hook 检测
int do_hook_check() {
    const char* libpath = "/system/lib64/libc.so";
    void* handle = dlopen(libpath, RTLD_LAZY);
    if (!handle) {
        LOGD("[-] Failed to load %s", libpath);
        return -1;
    }
 
    const char* funcs[] = {"sigaction", "signal", "exit", "abort", "_exit"};
    bool hooked = false;
 
    for (size_t i = 0; i < sizeof(funcs) / sizeof(funcs[0]); i++) {
        if (detect_hook(handle, libpath, funcs[i])) {
            hooked = true;
        }
    }
 
    dlclose(handle);
    return hooked;
}

4、总结

分析源码中frida注入逻辑进行检测,这种方法类似于开卷考试,不过优点明显:针对性很强,检测逻辑复杂度很低,性能开销小。
不知道后续frida会不会修改相关逻辑。


[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!

收藏
免费 5
支持
分享
赞赏记录
参与人
雪币
留言
时间
安卓逆向test
期待更多优质内容的分享,论坛有你更精彩!
3天前
你瞒我瞒
非常支持你的观点!
3天前
残月_374878
感谢你的贡献,论坛因你而更加精彩!
4天前
NSky99
谢谢你的细致分析,受益匪浅!
4天前
令狐双
感谢你分享这么好的资源!
4天前
最新回复 (3)
雪    币: 5
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
攻击方可以hook memcmp 和 open 函数,使其对应返回一致的结果。
守方可以选择自实现memcmp和open函数,这样不调用系统的库函数,不至于被轻易hook
3天前
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
3
mb_yvuzbfvo 攻击方可以hook memcmp 和 open 函数,使其对应返回一致的结果。 守方可以选择自实现memcmp和open函数,这样不调用系统的库函数,不至于被轻易hook
本贴主旨是,frida在注入Android进程后会自动hook五个固定的native函数,这个特征有助于对frida进行有针对性的检测。
至于对检测方法本身的hook并非主旨,所以使用了最一般的方法来实现,但在app中完全可以用更隐蔽的方法实现该检测方案。
2天前
0
雪    币: 15478
活跃值: (6768)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
frida是hook了这5个函数,但是不代表凡是hook了这5个函数就是frida?以前有人写过类似文章。
2天前
0
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册