聊聊frida 检测

常见检测点

检测 gmain 和 gum-js-loop

frida 在注入时, 会在目标进程下创建两个新线程,名为 gmain 和 gum-js-loop.
通过检测线程名,可以判断当前进程是否被frida注入. 具体实现方法 可以通过判断 /proc/self/task/tid/status 文件, status文件的第一行是线程名.
遍历所有的线程名, 如果发现 gmain 和 gum-js-loop, 则可认定该进程被注入.
下面给出检测代码

检测 gmain 和 gum-js-loop
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
static const char *APPNAME = "DetectFrida";
static const char *FRIDA_THREAD_GUM_JS_LOOP = "gum-js-loop";
static const char *FRIDA_THREAD_GMAIN = "gmain";
static const char *FRIDA_NAMEDPIPE_LINJECTOR = "linjector";
static const char *PROC_MAPS = "/proc/self/maps";
static const char *PROC_STATUS = "/proc/self/task/%s/status";
static const char *PROC_FD = "/proc/self/fd";
static const char *PROC_TASK = "/proc/self/task";

__attribute__((always_inline))
static inline void detect_frida_threads() {

DIR *dir = opendir(PROC_TASK);

if (dir != NULL) {
struct dirent *entry = NULL;
while ((entry = readdir(dir)) != NULL) {
char filePath[MAX_LENGTH] = "";

if (0 == my_strcmp(entry->d_name, ".") || 0 == my_strcmp(entry->d_name, "..")) {
continue;
}
snprintf(filePath, sizeof(filePath), PROC_STATUS, entry->d_name);

int fd = my_openat(AT_FDCWD, filePath, O_RDONLY | O_CLOEXEC, 0);
if (fd != 0) {
char buf[MAX_LENGTH] = "";
read_one_line(fd, buf, MAX_LENGTH);
if (my_strstr(buf, FRIDA_THREAD_GUM_JS_LOOP) ||
my_strstr(buf, FRIDA_THREAD_GMAIN)) {
//Kill the thread. This freezes the app. Check if it is an anticpated behaviour
//int tid = my_atoi(entry->d_name);
//int ret = my_tgkill(getpid(), tid, SIGSTOP);
__android_log_print(ANDROID_LOG_WARN, APPNAME,
"Frida specific thread found. Act now!!!");
}
my_close(fd);
}

}
closedir(dir);

}

}

检测 frida 特征命名管道 “linjector”

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

static const char *APPNAME = "DetectFrida";
static const char *FRIDA_THREAD_GUM_JS_LOOP = "gum-js-loop";
static const char *FRIDA_THREAD_GMAIN = "gmain";
static const char *FRIDA_NAMEDPIPE_LINJECTOR = "linjector";
static const char *PROC_MAPS = "/proc/self/maps";
static const char *PROC_STATUS = "/proc/self/task/%s/status";
static const char *PROC_FD = "/proc/self/fd";
static const char *PROC_TASK = "/proc/self/task";

__attribute__((always_inline))
static inline void detect_frida_namedpipe() {

DIR *dir = opendir(PROC_FD);
if (dir != NULL) {
struct dirent *entry = NULL;
while ((entry = readdir(dir)) != NULL) {
struct stat filestat;
char buf[MAX_LENGTH] = "";
char filePath[MAX_LENGTH] = "";
snprintf(filePath, sizeof(filePath), "/proc/self/fd/%s", entry->d_name);

lstat(filePath, &filestat);

if ((filestat.st_mode & S_IFMT) == S_IFLNK) {
//TODO: Another way is to check if filepath belongs to a path not related to system or the app
my_readlinkat(AT_FDCWD, filePath, buf, MAX_LENGTH);
if (NULL != my_strstr(buf, FRIDA_NAMEDPIPE_LINJECTOR)) {
__android_log_print(ANDROID_LOG_WARN, APPNAME,
"Frida specific named pipe found. Act now!!!");
}
}

}
}
closedir(dir);
}

对比内存中的可执行段(.text 段等)与磁盘上的 ELF 文件内容,识别是否被 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
//Structure to hold the details of executable section of library
typedef struct stExecSection {
int execSectionCount;
unsigned long offset[2];
unsigned long memsize[2];
unsigned long checksum[2];
unsigned long startAddrinMem;
} execSection;


#define NUM_LIBS 2

//Include more libs as per your need, but beware of the performance bottleneck especially
//when the size of the libraries are > few MBs
static const char *libstocheck[NUM_LIBS] = {"libnative-lib.so", LIBC};
static execSection *elfSectionArr[NUM_LIBS] = {NULL};

__attribute__((always_inline))
static inline bool
scan_executable_segments(char *map, execSection *pElfSectArr, const char *libraryName) {
unsigned long start, end;
char buf[MAX_LINE] = "";
char path[MAX_LENGTH] = "";
char tmp[100] = "";

sscanf(map, "%lx-%lx %s %s %s %s %s", &start, &end, buf, tmp, tmp, tmp, path);
//__android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "Map [%s]", map);

if (buf[2] == 'x') {
if (buf[0] == 'r') {
uint8_t *buffer = NULL;

buffer = (uint8_t *) start;
for (int i = 0; i < pElfSectArr->execSectionCount; i++) {
if (start + pElfSectArr->offset[i] + pElfSectArr->memsize[i] > end) {
if (pElfSectArr->startAddrinMem != 0) {
buffer = (uint8_t *) pElfSectArr->startAddrinMem;
pElfSectArr->startAddrinMem = 0;
break;
}
}
}
for (int i = 0; i < pElfSectArr->execSectionCount; i++) {
unsigned long output = checksum(buffer + pElfSectArr->offset[i],
pElfSectArr->memsize[i]);
// __android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "Checksum:[%ld][%ld]", output,
// pElfSectArr->checksum[i]);

if (output != pElfSectArr->checksum[i]) {
__android_log_print(ANDROID_LOG_VERBOSE, APPNAME,
"Executable Section Manipulated, "
"maybe due to Frida or other hooking framework."
"Act Now!!!");
}
}

} else {

char ch[10] = "", ch1[10] = "";
__system_property_get("ro.build.version.release", ch);
__system_property_get("ro.system.build.version.release", ch1);
int version = my_atoi(ch);
int version1 = my_atoi(ch1);
if (version < 10 || version1 < 10) {
__android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "Suspicious to get XOM in "
"version < Android10");
} else {
if (0 == my_strncmp(libraryName, LIBC, my_strlen(LIBC))) {
//If it is not readable, then most likely it is not manipulated by Frida
__android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "LIBC Executable Section"
" not readable! ");

} else {
__android_log_print(ANDROID_LOG_VERBOSE, APPNAME, "Suspicious to get XOM "
"for non-system library on "
"Android 10 and above");
}
}
}
return true;
} else {
if (buf[0] == 'r') {
pElfSectArr->startAddrinMem = start;
}
}
return false;
}


__attribute__((always_inline))
static inline void detect_frida_memdiskcompare() {
int fd = 0;
char map[MAX_LINE];

if ((fd = my_openat(AT_FDCWD, PROC_MAPS, O_RDONLY | O_CLOEXEC, 0)) != 0) {

while ((read_one_line(fd, map, MAX_LINE)) > 0) {
for (int i = 0; i < NUM_LIBS; i++) {
if (my_strstr(map, libstocheck[i]) != NULL) {
if (true == scan_executable_segments(map, elfSectionArr[i], libstocheck[i])) {
break;
}
}
}
}
} else {
__android_log_print(ANDROID_LOG_WARN, APPNAME,
"Error opening /proc/self/maps. That's usually a bad sign.");

}
my_close(fd);

}

来看看一些绕过 frida检测的项目 strong-frida

strong-frida

这里只patch 了针对 gmain 和 loop-js 还有命名管道的检测.

这里放一个 frida check 的apk, 可自行验证

strong-frida

点我下载 Frida_check.apk

这里延申一种定位加密位置的办法, 很好用

试试看 hook StringBuilder StringBuffer 的 toString , 然后打印调用堆栈.

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
Java.perform(function x() {
//定位StringBuilder,StringBuffer类
const stringbuilder = Java.use("java.lang.StringBuilder");
const stringbuffer = Java.use("java.lang.StringBuffer");

//定位方法
const toString = "toString";


// 使用log类和Exception类产生堆栈
var jAndroidLog = Java.use("android.util.Log");
var jException = Java.use("java.lang.Exception");

stringbuilder[toString].implementation = function () {
//执行原逻辑
const result = this[toString]();

if (result.indexOf("aaaaa") != -1) {
// 打印返回的字符串内容
console.log(result);
console.log(jAndroidLog.getStackTraceString(jException.$new()));
}
return result;
};

stringbuffer[toString].implementation = function () {
//执行原逻辑
const result = this[toString]();
if (result.indexOf("aaaaa") != -1) {
// 打印返回的字符串内容
console.log(result);
console.log(jAndroidLog.getStackTraceString(jException.$new()));
}
return result;
}

});

样例分析

如果一个样本存在 anti-frida, 要怎么定位关键监测点呢
先试试看 hook 库的加载函数(在 Android 较低版本比如 6.0 上是dlopen,较高版本上是android_dlopen_ext)

1
2
3
4
5
6
7
8
9
10
11
12
var android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
if(android_dlopen_ext != null){
Interceptor.attach(android_dlopen_ext,{
onEnter: function(args){
var soName = args[0].readCString();
console.log(soName);
},
onLeave: function(retval){
Thread.sleep(5);
}
});
}

每次加载完一个so文件后, 就休眠一段时间, 如果程序退出了, 那么凶手可能就是最后一个加载的so文件.

针对 libmsaoaidsec.so 的分析, 可以看看之前发过的文章, 使用 stackplz 分析 libmsaoaidsec.so, 很方便.

其实也可以简单绕过

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function hookDlopen() {
var p_android_dlopen_ext = Module.findExportByName(null, "android_dlopen_ext");
if (p_android_dlopen_ext != null) {
// void* android_dlopen_ext(const char* filename, int flags, const android_dlextinfo* extinfo)
var android_dlopen_ext = new NativeFunction(p_android_dlopen_ext, "pointer", ["pointer", "int", "pointer"]);

Interceptor.replace(p_android_dlopen_ext, new NativeCallback(function (filename, flag, extinfo) {
if (filename.readCString().indexOf("libmsaoaidsec.so") != -1) {
return ptr(-1);
} else {
return android_dlopen_ext(filename, flag, extinfo);
}

}, "pointer", ["pointer", "int", "pointer"]));
}
}

hookDlopen();

或者 明明要加载 libmsaoaidsec.so,让程序再加载一个空 或者加载一个已经加载过的so文件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function hook_dlopen(soName = '') {
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();
if(path.indexOf('libmsaoaidsec.so') >= 0){
ptr(pathptr).writeUtf8String("");
}
console.log('path: ',path)
}
}
});
}
hook_dlopen()