23. 定制KernelSU绕过frida检测思路介绍

效果非常的棒, 很多人都已经做了, 也是非常有效,可以是啥也不用干,直接, 就过很多很多很大数字的比例的一个frida 检测, 要绕过需要看到 frida的检测 是怎样写的, DetectFrida 是目前,市面上检测frida最为厉害的一个开源项目, 使用了很多的加固方法, 防止检测被轻易的绕过.
“定制” kernelsu绕过Frida检测思路
比如 C函数, 重写 , 系统调用重写, 之类的方法, 防止你, hook 系统C里面的一些常用函数比如 strstr, strcat, 函数直接绕过.
系统调用重写,防止你, 给官方的系统调用加上包装, 自己手动调用一个硬编码, 硬编码一个系统调用,
我们可以通过ebpf 的方式来hook系统调用, 直接观察, 不管你如何重写, 只要你调用了跟系统相关的东西, 我们就可以通过ebpf来hook 一些系统调用. 来观察你输出的结果是什么? 通过系统调用, 把kernelsu如何修改把 hook 持久化到源码中去 , frida在跑时候, app去调用系统调用, 在检测的时候,会被hook绕过.
DetectFrida项目编译运行使用
DetectFrida时间虽然久, 但是项目是非常厉害的, 主要是在一个命名空间, 线程, 看看 so 有没有被修改过,
This project has 3 ways to detect frida hooking
Detect through named pipes used by Frida // 命名空间
Detect through frida specific named thread // 线程名称
Compare text section in memory with text section in disk for both libc and native library // 对比 libc 和 系统库,在内存中看有没有被修改过.
并不是所有的Frida 检测都是可以直接绕过的 , 因为他包含了, 通用的hook 检测的一些东西,不过他是一个通用的hook检测,他跟Frida 是没有关系的. 不是说我做了一个定制rom 就能够 100% 绕过. 对把. 如果他是一个通用的hook 检测, 所以还是需要逆向这个so的, 只能说过一部分以及是非常大的一部分了. 100%很大的一个数值的了.
More details can be found in my blog -> DetectFrida
Also this project has 3 mechanisms to harden the native code // 这里3种机制,加固,
Replace certain libc calls with syscalls # 1, 把 C的函数 替换成 系统调用,
Replace string,memory related operation with custom implementation # 2, 把 C 的一些的其他函数, 自定义实现.防止你直接hook C的lib库, 内存字符串
Apply O-LLVM native obfuscation # 3, 以及做了一些 ollvm 的混淆.
使用 2023不可以[跑起来直接白屏,什么都没有.], r0env2021 可以直接跑起来, 还有就是 adb logcat 里面可以输出很多的内容.

这里就是检测的3个逻辑
void detect_frida_loop(void *pargs) {
struct timespec timereq;
timereq.tv_sec = 5; //Changing to 5 seconds from 1 second
timereq.tv_nsec = 0;
while (1) {
detect_frida_threads(); // Detect through frida specific named thread
detect_frida_namedpipe(); // Detect through named pipes used by Frida
detect_frida_memdiskcompare(); // Compare text section in memory with text section in disk for both libc and native library
my_nanosleep(&timereq, NULL);
}
}
DetectFrida项目加固技术
Replace certain libc calls with syscalls
之 C 函数重写与系统调用重写
给官方 的系统调用加上包装.
从使用开始, detectfrida() 这个函数,开始了检测.
/*
* System calls such as file operations, sleep are converted to syscalls to avoid easy bypass 系统调用(如文件作、睡眠)会转换为 syscall,以避免轻易绕过
* through readymade scripts hooking onto libc calls. 通过挂接到 libc 调用的现成脚本。
*/
__attribute__((always_inline))
//Replace certain libc calls with syscalls // 把 C的函数替换成系统调用,
static inline int my_openat(int __dir_fd, const void* __path, int __flags, int __mode ){ // my_openat() 这里加了一些, 包装实际上还是, 调用了 __NR_openat, 防止你搜索到 // 本身是一个系统调用,但是这里加入了一层包装.变成了 openat,防止某些,被搜到
return (int)__syscall4(__NR_openat, __dir_fd, (long)__path, __flags, __mode);
}
这样就直接避免了打开 openat( ) 直接hook到我(目的检测 Frida.)
eBPF 为什么会依然发生作用,因为我们hook的是,系统调用, 如果ebfp是hook系统调用,那么他的小心机,就无法隐匿了.(int)__syscall4(__NR_openat, __dir_fd, (long)__path, __flags, __mode);
read_one_line(fd/*fd是文件*/, buf/*buf 是文件里面的每一行*/, MAX_LENGTH); // 然后一行一行地 读出, 读完之后,一行一行看有没有,有的话就 杀掉
可以知道文件里面的每一行是怎么写的
这是每一行, 文件存在的这个buff里面.
__attribute__((always_inline))
static inline ssize_t read_one_line(int fd, char *buf, unsigned int max_len) {
char b;
ssize_t ret;
ssize_t bytes_read = 0;
my_memset(buf, 0, max_len); // 内存操作用的 custom implementation, memory related operation with custom implementation
do {
ret = my_read(fd, &b, 1);
if (ret != 1) {
if (bytes_read == 0) {
// error or EOF
return -1;
} else {
return bytes_read;
}
}
if (b == '\n') {
return bytes_read;
}
*(buf++) = b;
bytes_read += 1;
} while (bytes_read < max_len - 1);
return bytes_read;
}
// mylibc.h
// 这里的内存, 全部都是自己手动清空的.防止特征被发现.
void* my_memset(void* dst, int c, size_t n)
{
char* q = (char*)dst;
char* end = q + n;
for (;;) {
if (q >= end) break; *q++ = (char) c;
if (q >= end) break; *q++ = (char) c;
if (q >= end) break; *q++ = (char) c;
if (q >= end) break; *q++ = (char) c;
}
return dst;
}
// mylibc.h
// 他自己做了很多, my_strcmp( ) , 这是自己实现的 strstr(() 基础的libc 函数.方式被搜索到.
my_strcmp(const char *s1, const char *s2)
{
while (*s1 == *s2++)
if (*s1++ == 0)
return (0);
return (*(unsigned char *)s1 - *(unsigned char *)--s2);
}
__attribute__((always_inline)) // 强制编译器内联该函数,提高执行效率
static inline int my_atoi(const char *s) // 自定义 atoi 实现,将字符串转换为整数
{
int n=0, neg=0; // n 存储转换后的整数,neg 标记是否为负数
while (isspace(*s)) s++; // 跳过前导空白字符
switch (*s) {
case '-': neg=1; // 负号时标记 neg=1
case '+': s++; // 负号或正号都跳过
}
/* Compute n as a negative number to avoid overflow on INT_MIN */
while (isdigit(*s)) // 遇到数字字符时转换
n = 10*n - (*s++ - '0'); // 计算负数,避免 INT_MIN 溢出
return neg ? n : -n; // 如果是负数直接返回 n,否则返回 -n
}
__attribute__((always_inline))
static inline
size_t my_strlen(const char *s) // 这是长度计算
{
size_t len = 0;
while(*s++) len++;
return len;
}
// 这是拷贝函数
__attribute__((always_inline))
static inline size_t
my_strlcpy(char *dst, const char *src, size_t siz)
{
char *d = dst;
const char *s = src;
size_t n = siz;
/* Copy as many bytes as will fit */
if (n != 0) {
while (--n != 0) {
if ((*d++ = *s++) == '\0')
break;
}
}
/* Not enough room in dst, add NUL and traverse rest of src */
if (n == 0) {
if (siz != 0)
*d = '\0'; /* NUL-terminate dst */
while (*s++)
;
}
return(s - src - 1); /* count does not include NUL */
}
这里就可以看到很多的关键的监测点 , linjector, gum-js-loop, gmain

在buff 的位置打上断点, 点击 debug 左边的那个, 继续按钮. 直到运行到断点结束.
buff = "Name:\tMetrics Backgro"
"Name:\tSignal Catcher"
"Name:\tperfetto_hprof_"
"Name:\tADB-JDWP Connec"
"Name:\tJit thread pool"
"Name:\tHeapTaskDaemon"
检测 Frida 的线程,
if (my_strstr(buf, FRIDA_THREAD_GUM_JS_LOOP) || my_strstr(buf, FRIDA_THREAD_GMAIN)) {
检测 Frida 的 detect_frida_namedpipe
"/proc/self/fd/%s *FRIDA_NAMEDPIPE_LINJECTOR = "linjector";

Apply O-LLVM native obfuscation
这里的 ollvm的混淆,不是过于健壮, 陈老师并没有编译通过.

CMakeLists.txt 编译 ollvm 和 符号的开关
一个超级节点, length 最大的 一个函数, ida9 打开后.
ida '/home/calleng/AndroidStudioProjects/kanxue_2w_Chapter2_darvincisec_DetectFrida/obfuscated-app/obfuscated-app-release/lib/arm64-v8a/libnative-lib.so'

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html
# Sets the minimum version of CMake required to build the native library.
cmake_minimum_required(VERSION 3.4.1)
set(can_use_assembler TRUE)
enable_language(ASM)
#Start - Comment this block for generating non-obfuscated builds # --> 开始混淆
#set(OLLVM_PATH ${CMAKE_HOME_DIRECTORY}/../../../../../o-llvm-binary/ollvm-tll/build/bin_Darwin) # --> 开始混淆
#set(OLLVM_C_COMPILER ${OLLVM_PATH}/clang) # --> 开始混淆
#set(OLLVM_CXX_COMPILER ${OLLVM_PATH}/clang++) # --> 开始混淆
#
#set(OLLVM_C_FLAGS "-mllvm -sub -mllvm -bcf -mllvm -fla") # --> 开始混淆
#
#set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OLLVM_C_FLAGS}") # --> 开始混淆
#set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OLLVM_C_FLAGS}") # --> 开始混淆
#set(CMAKE_C_COMPILER ${OLLVM_C_COMPILER}) # --> 开始混淆
#set(CMAKE_CXX_COMPILER ${OLLVM_CXX_COMPILER}) # --> 开始混淆
#End - Comment this block for generating non-obfuscated builds # --> 开始混淆
#Set flags to detect arm32 bit or arm64 bit for switching between elf structures
if(${ANDROID_ABI} STREQUAL "armeabi-v7a" OR ${ANDROID_ABI} STREQUAL "x86")
add_definitions("-D_32_BIT")
elseif(${ANDROID_ABI} STREQUAL "arm64-v8a" OR ${ANDROID_ABI} STREQUAL "x86_64")
add_definitions("-D_64_BIT")
endif()
add_library( # Sets the name of the library.
native-lib
# Sets the library as a shared library.
SHARED
# Provides a relative path to your source file(s).
native-lib.c)
target_include_directories(native-lib PRIVATE arch/${ANDROID_ABI})
# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.
find_library( # Sets the name of the path variable.
log-lib
# Specifies the name of the NDK library that
# you want CMake to locate.
log )
# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.
target_link_libraries( # Specifies the target library.
native-lib
# Links the target library to the log library
# included in the NDK.
${log-lib} )
#add_custom_command( TARGET native-lib # --> 这里做了 --strip-unneeded 的操作, 把所有的符号拿掉 . 让你找不到!
# POST_BUILD
# COMMAND "${ANDROID_TOOLCHAIN_PREFIX}strip" -R .comment -g -S -d --strip-unneeded ${CMAKE_HOME_DIRECTORY}/../../../build/intermediates/cmake/${CMAKE_BUILD_TYPE}/obj/${ANDROID_ABI}/libnative-lib.so
# COMMENT "Stripped native library")
当混淆ollvm 打开后 加上 符号拿掉, 编译出来后的结果, 效果是非常吓人的. 核心的代码如果看不懂, 那么就调用 gpt, 去问一下, 死记硬背, 一个读一个个看,都没问题.
那么我们就直接hook 这个系统调用
./stackplz -n com.darvin.security --syscall openat -o 20250329.txt
做了 strstr的 混淆的, 加上 , 下面的编译参数去掉符号.
那么, 你是基本没有办法去做对比的. ========> 去掉符号的编译选项
#add_custom_command( TARGET native-lib # --> 这里做了 --strip-unneeded 的操作, 把所有的符号拿掉 . 让你找不到!
# POST_BUILD
# COMMAND "${ANDROID_TOOLCHAIN_PREFIX}strip" -R .comment -g -S -d --strip-unneeded ${CMAKE_HOME_DIRECTORY}/../../../build/intermediates/cmake/${CMAKE_BUILD_TYPE}/obj/${ANDROID_ABI}/libnative-lib.so
# COMMENT "Stripped native library")
my_strstr(const char *s, const char *find) 这个函数 ,被混淆+ 抹去符号, 没有任何办法,将他找出来 ,
我们可以通过,
__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);
// __android_log_print(ANDROID_LOG_WARN, APPNAME,buf,
// "Frida specific thread found. Act now!!!");
if (my_strstr(buf, FRIDA_THREAD_GUM_JS_LOOP) ||
my_strstr(buf, FRIDA_THREAD_GMAIN)) { // my_strstr 已经没有办法寻找到他了. 只能通过.
//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);
}
}
__attribute__((always_inline))
static inline ssize_t my_read(int __fd, void* __buf, size_t __count){
return __syscall3(__NR_read, __fd, (long)__buf, (long)__count);
}
// 这个是一个,byte字节,他是一个个的读. 的.东西.
// 那么通过 eBPF 来过滤 read 作为关键字来 过滤进行调用.

这里的
__attribute__((always_inline)) // ssize_t 就是 字节的 单位.
static inline ssize_t read_one_line(int fd, char *buf, unsigned int max_len) {
char b;
ssize_t ret; // 以字节的形式来读取的.
ssize_t bytes_read = 0; // 以每个字节, 字节, 方式过来读取. my_read ( ) // 001–> 是读的炒作, 是 每个字节,
ret = my_read(fd, &b, 1); // 002–> 读的操作.
__syscall3(__NR_read, __fd, (long)__buf, (long)__count); // 003–> 读的操作 ,3个底层的逻辑函数. 直接系统调用 , __syscall3 级别的函数
__attribute__((always_inline))
// 以下是系统调用的 3个 syscall 底层直接调用 内核处理的地方.
static inline long __syscall3(long n, long a, long b, long c) // 004–> 读的底层逻辑 –> 明白的话,直接看到代码即可–> kanxue_2w_chapt2_DetectFrida_by_r0yuseChanged/app/src/main/c/arch/arm64-v8a/syscall_arch.h
{
register long x8 __asm__(“x8”) = n;
register long x0 __asm__(“x0”) = a;
register long x1 __asm__(“x1”) = b;
register long x2 __asm__(“x2”) = c;
__asm_syscall(“r”(x8), “0”(x0), “r”(x1), “r”(x2));
}
通过以上的探索,就可以知道, 你 hook C的库,
/system/lib/libc.so
/system/lib/libm.so
/system/lib/libdl.so
/system/lib/libart.so
/system/lib/libandroid_runtime.so
32 位系统:/system/lib/
64 位系统:/system/lib64/
这些库, 你是没有机会去hook的, 别人(用户层的so)直接走(内核层)的调用, 交互的 syscall 就直接走了. 别人绕过上述的库.
那么底层遇到的这些案例:
_syscall4(__NR_openat, __dir_fd, (long)__path, __flags, __mode);
__syscall3(__NR_read, __fd, (long)__buf, (long)__count);
__syscall1(__NR_close, __fd);
__syscall2(__NR_nanosleep, (long)__request, (long)__remainder);
__syscall4(__NR_readlinkat, __dir_fd, (long)__path, (long)__buf,(long)__buf_size);
__syscall3(__NR_tgkill, __tgid, __tid, __signal);
__syscall1(__NR_exit, __status);
直接hook 他们的关键字, 就可以了,
stackplz系统调用参数输出观测
这里的 estrace 已经被 github.com/SeeFlowerX/estrace 被 作者 seeflowerX 给弃用了.use stackplz now –> https://github.com/SeeFlowerX/stackplz
我们要看的目的就是, 从源码中看, __NR_openat 和 __NR_read
把多余的选项关闭
void detect_frida_loop(void *pargs) {
struct timespec timereq;
timereq.tv_sec = 5; //Changing to 5 seconds from 1 second
timereq.tv_nsec = 0;
while (1) {
detect_frida_threads();
// detect_frida_namedpipe(); // 关掉1
// detect_frida_memdiskcompare(); // 关掉2
my_nanosleep(&timereq, NULL);
}
}
追踪syscall
# 定位
adb -s 1A041FDF6S00EP shell dumpsys window | grep mCurrentFocus # 即可过滤出,想要的顶部的位置的 app包名.
./stackplz -n com.darvin.security --syscall openat -o tmp.log 2025y3m30.txt
[18045|18103|darvin.security] openat(dirfd=-100, *pathname=0x73ae945070, flags=0x80000(O_CLOEXEC), mode=0o000, ret=88)
[18045|18103|darvin.security] openat(dirfd=-100, *pathname=0x73ae945070(/proc/self/task/18108/status), flags=0x80000(O_CLOEXEC), mode=0o000) LR:0x7417c7adf0 PC:0x7417c7ae84 SP:0x73ae944ed0
[18045|18103|darvin.security] openat(dirfd=-100, *pathname=0x73ae945070, flags=0x80000(O_CLOEXEC), mode=0o000, ret=88)
[18045|18103|darvin.security] openat(dirfd=-100, *pathname=0x73ae945070(/proc/self/task/18110/status), flags=0x80000(O_CLOEXEC), mode=0o000) LR:0x7417c7adf0 PC:0x7417c7ae84 SP:0x73ae944ed0
[18045|18103|darvin.security] openat(dirfd=-100, *pathname=0x73ae945070, flags=0x80000(O_CLOEXEC), mode=0o000, ret=88)
[18045|18103|darvin.security] openat(dirfd=-100, *pathname=0x7417c7c7a1(/proc/self/fd), flags=0x84000(O_CLOEXEC|O_DIRECTORY), mode=0o000) LR:0x76d44fac60 PC:0x76d4546ed8 SP:0x73ae944e50
[18045|18103|darvin.security] openat(dirfd=-100, *pathname=0x7417c7c7a1, flags=0x84000(O_CLOEXEC|O_DIRECTORY), mode=0o000, ret=5)
[18045|18103|darvin.security] openat(dirfd=-100, *pathname=0x7417c7c6ac(/proc/self/maps), flags=0x80000(O_CLOEXEC), mode=0o000) LR:0x7417c7b868 PC:0x7417c7b904 SP:0x73ae944ed0
[18045|18103|darvin.security] openat(dirfd=-100, *pathname=0x7417c7c6ac, flags=0x80000(O_CLOEXEC), mode=0o000, ret=5)
# 通过上面可以看到 很多的 maps , task , fd , 等等路径, 都进行了打开的检测 .
print 打印调用栈, 查找他是哪里发出来的. 从哪里hook出来,调用栈打印出来.
0000000000001e84 这个位置发出.
./stackplz -n com.darvin.security --syscall openat --stack
把调用栈打印出来, 看出都是同一个位置 ,发起了调用.
[18045|18103|darvin.security] openat(dirfd=-100, *pathname=0x73ae945070, flags=0x80000(O_CLOEXEC), mode=0o000, ret=88)
[18045|18103|darvin.security] openat(dirfd=-100, *pathname=0x73ae945070(/proc/self/task/18068/status), flags=0x80000(O_CLOEXEC), mode=0o000) LR:0x7417c7adf0 PC:0x7417c7ae84 SP:0x73ae944ed0, Backtrace:
#00 pc 0000000000001e84 /data/app/~~F1sa1VJorfEVu8T3hGd9vQ==/com.darvin.security-3TvzKOKijSa0q3x2UexEsA==/lib/arm64/libnative-lib.so
#01 pc 00000000000c9ccc /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+204)
#02 pc 000000000005db00 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64)
[18045|18103|darvin.security] openat(dirfd=-100, *pathname=0x73ae945070, flags=0x80000(O_CLOEXEC), mode=0o000, ret=88)
[18045|18103|darvin.security] openat(dirfd=-100, *pathname=0x73ae945070(/proc/self/task/18070/status), flags=0x80000(O_CLOEXEC), mode=0o000) LR:0x7417c7adf0 PC:0x7417c7ae84 SP:0x73ae944ed0, Backtrace:
#00 pc 0000000000001e84 /data/app/~~F1sa1VJorfEVu8T3hGd9vQ==/com.darvin.security-3TvzKOKijSa0q3x2UexEsA==/lib/arm64/libnative-lib.so
#01 pc 00000000000c9ccc /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+204)
#02 pc 000000000005db00 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64)
[18045|18103|darvin.security] openat(dirfd=-100, *pathname=0x73ae945070, flags=0x80000(O_CLOEXEC), mode=0o000, ret=88)
[18045|18103|darvin.security] openat(dirfd=-100, *pathname=0x73ae945070(/proc/self/task/18071/status), flags=0x80000(O_CLOEXEC), mode=0o000) LR:0x7417c7adf0 PC:0x7417c7ae84 SP:0x73ae944ed0, Backtrace:
#00 pc 0000000000001e84 /data/app/~~F1sa1VJorfEVu8T3hGd9vQ==/com.darvin.security-3TvzKOKijSa0q3x2UexEsA==/lib/arm64/libnative-lib.so
#01 pc 00000000000c9ccc /apex/com.android.runtime/lib64/bionic/libc.so (__pthread_start(void*)+204)
#02 pc 000000000005db00 /apex/com.android.runtime/lib64/bionic/libc.so (__start_thread+64)
#00 pc 0000000000001e84 /data/app/~~F1sa1VJorfEVu8T3hGd9vQ==/com.darvin.security-3TvzKOKijSa0q3x2UexEsA==/lib/arm64/libnative-lib.so 看起来, 是一个地方 的 #00 的地方,这个偏移,发起的调用.
通过上面的结论,我们可以知道, 我们的对他这个 Replace certain libc calls with syscalls # 1, 把 C的函数 替换成 系统调用, ===> 对他这个进行了绕过.
那么对于他自己设计的.
my_strlcpy( ) // 自实现str 字符串函数 1
my_strlen( ) // 自实现str 字符串函数 2
my_strncmp( ) // 自实现str 字符串函数 3
my_strstr( ) // 自实现str 字符串函数 4
my_memset( ) // 自实现str 字符串函数 5
my_atoi( ) // 自实现str 字符串函数 6
他用了 my_strstr( ) 加上符号抽取, 我们从导出表,已经找不到 strstr 的符号了. 再加上 做了 ollvm 混淆, 其实这下 ,已经没有办法, 函数已经完全不见了, 哪怕ebpf 再强, 把这个函数找出来, 没有任何办法, 就是把 so 拿出来, 找到 myopen这个函数. 通过

再通过这个 detect_frida_threads() 这个函数,定位到

那么read 的话,需要使用 byte 来读,

定位到 gmain 或者 gum_js_loop , 关键字, 我们现在搞一个 frida 进去.
oriole:/data/local/tmp # ./f1613 # 把 frida server 跑起来, 然后再 attach 上去.
(f1613) calleng@hw:~/p9/Mikrom2.0/kanxue/custome2025$ frida -D 1A041FDF6S00EP -F
通过 adb logcat | grep -i frida
通过这样来过滤frida 的相关联的日志!!!

那么就可找到 gmain 或者 loop,
./stackplz -n com.darvin.security --syscall read
Name: darvin.security
Name: hwuiTask0
Name: hwuiTask1
Name: SurfaceSyncGrou
Name: binder:18045_4
Name: binder:18045_2
Name: darvin.security
Name: gmain
Frida specific thread found. Act now!!!
Name: gdbus
Name: Thread-2
Name: queued-work-loo
stackplz 系统调用参数输出观测.
oriole:/data/local/tmp # ./stackplz -n com.darvin.security –syscall read -o 20250330_Third.txt
哪些检测可否用系统调用hook绕过
我要去修改, 你要做一个 status 的, 打开这个 status 的时候, 有哪些思路, 你要参考这个文件, 看下那个思路可以, 可以的思路就是可以把 /proc/self/task/%pid/staus

这个地址, 换掉, 针对这个进程换掉, 不要把所有的都换掉, 否则, 系统是启动不起来了,
KernelSU 源码修改参数位置与思路
将hook 持久化到源码中去. 绕过商业检测. app系统调用的时候, 检测就会被hook.
kernelsu 还可以针对单个进程,而且已经做好了进程的选项,打勾的地方, 针对哪些进程白名单, 超级用户白名单, 哪些app能够开超级用户, 那么不能开, 已经帮我们做好了,进程过滤, 直接在kernelsu 源码上,进行修改是, 最为方便的 ,
解读KernelSu的源码.
上节课, 我们解读的关键的 源码的地方,核心的地方,hook了4个系统调用 , execve. hook 什么 face_art ,
下面是2025年3月的版本, 修改,最新的修改.
// https://github.com/tiann/KernelSU/blob/650c3006c37dc0a8996754ef387d4dcfd7b13462/kernel/sucompat.c#L216 的关键代码的位置.
// sucompat: permited process can execute 'su' to gain root access.
void ksu_sucompat_init()
{
#ifdef CONFIG_KPROBES
su_kps[0] = init_kprobe(SYS_EXECVE_SYMBOL, execve_handler_pre);
su_kps[1] = init_kprobe(SYS_FACCESSAT_SYMBOL, faccessat_handler_pre);
su_kps[2] = init_kprobe(SYS_NEWFSTATAT_SYMBOL, newfstatat_handler_pre);
su_kps[3] = init_kprobe("pts_unix98_lookup", pts_unix98_lookup_pre);
#endif
}
// hook
execve_kp
newf stat at_kp
f access at_kp
pts_unix98_lookup_kp
这么4个函数. 这是他的核心原理.
下面这是 2024年的核心原理
https://github.com/tiann/KernelSU/blob/e4e34df9cae9ab87519d622e3b5dcdf5a91d0914/kernel/sucompat.c
#include <linux/dcache.h>
#include <linux/security.h>
#include <asm/current.h>
#include <linux/cred.h>
#include <linux/err.h>
#include <linux/fs.h>
#include <linux/kprobes.h>
#include <linux/types.h>
#include <linux/uaccess.h>
#include <linux/version.h>
#include <linux/sched/task_stack.h>
#include "objsec.h"
#include "allowlist.h"
#include "arch.h"
#include "klog.h" // IWYU pragma: keep
#include "ksud.h"
#include "kernel_compat.h"
#define SU_PATH "/system/bin/su"
#define SH_PATH "/system/bin/sh"
extern void escape_to_root();
static void __user *userspace_stack_buffer(const void *d, size_t len)
{
/* To avoid having to mmap a page in userspace, just write below the stack
* pointer. */
char __user *p = (void __user *)current_user_stack_pointer() - len;
return copy_to_user(p, d, len) ? NULL : p;
}
static char __user *sh_user_path(void)
{
static const char sh_path[] = "/system/bin/sh";
return userspace_stack_buffer(sh_path, sizeof(sh_path));
}
static char __user *ksud_user_path(void)
{
static const char ksud_path[] = KSUD_PATH;
return userspace_stack_buffer(ksud_path, sizeof(ksud_path));
}
int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode, // 这里的具体的处理的地方.
int *__unused_flags)
{
const char su[] = SU_PATH;
if (!ksu_is_allow_uid(current_uid().val)) { // current_uid 是什么意思, 就是ksu在进程管理的时候有开一个白名单--->uuid ,管理的时候, 点开🕛,那个位置, 才会让你用这个东西. ksu_is_allow_uid 只对当前进程生效.这样就提高了我的稳定性, 对其他进程不生效,直接排除.
return 0;
}
char path[sizeof(su) + 1];
memset(path, 0, sizeof(path));
ksu_strncpy_from_user_nofault(path, *filename_user, sizeof(path)); // ksu_strncpy_from_user_nofault --> 他从用户态取了一个字符串过来.如果他要检测什么, 你可以 写死也行, 不可以也行,自定义一个字符串, 就像上节课一样 K(A)Patch 一样,改一个字符串写在这里.
if (unlikely(!memcmp(path, su, sizeof(su)))) { // 写完后, 他会让你拷贝出来到这儿, 去生效(睡觉??).
pr_info("faccessat su->sh!\n");
*filename_user = sh_user_path(); // 你修改的话, 加也是这样加上.一模一样的加上, 没有多大的区别. 打开文件的时候,他是--> /proc/self/task/%s/status 就变成了, /proc/aaaaaaaaaaaaaaa/task/%s/status, 那么他这样 ,就打不开了, 打不开之后,他的检测就不会再往下进行了.
}
return 0;
}
int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags)
{
// const char sh[] = SH_PATH;
const char su[] = SU_PATH;
if (!ksu_is_allow_uid(current_uid().val)) {
return 0;
}
if (unlikely(!filename_user)) {
return 0;
}
char path[sizeof(su) + 1];
memset(path, 0, sizeof(path));
// Remove this later!! we use syscall hook, so this will never happen!!!!!
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 18, 0) && 0
// it becomes a `struct filename *` after 5.18
// https://elixir.bootlin.com/linux/v5.18/source/fs/stat.c#L216
const char sh[] = SH_PATH;
struct filename *filename = *((struct filename **)filename_user);
if (IS_ERR(filename)) {
return 0;
}
if (likely(memcmp(filename->name, su, sizeof(su))))
return 0;
pr_info("vfs_statx su->sh!\n");
memcpy((void *)filename->name, sh, sizeof(sh));
#else
ksu_strncpy_from_user_nofault(path, *filename_user, sizeof(path));
if (unlikely(!memcmp(path, su, sizeof(su)))) {
pr_info("newfstatat su->sh!\n");
*filename_user = sh_user_path();
}
#endif
return 0;
}
// the call from execve_handler_pre won't provided correct value for __never_use_argument, use them after fix execve_handler_pre, keeping them for consistence for manually patched code
int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr,
void *__never_use_argv, void *__never_use_envp,
int *__never_use_flags)
{
struct filename *filename;
const char sh[] = KSUD_PATH;
const char su[] = SU_PATH;
if (unlikely(!filename_ptr))
return 0;
filename = *filename_ptr;
if (IS_ERR(filename)) {
return 0;
}
if (likely(memcmp(filename->name, su, sizeof(su))))
return 0;
if (!ksu_is_allow_uid(current_uid().val))
return 0;
pr_info("do_execveat_common su found\n");
memcpy((void *)filename->name, sh, sizeof(sh));
escape_to_root();
return 0;
}
int ksu_handle_execve_sucompat(int *fd, const char __user **filename_user,
void *__never_use_argv, void *__never_use_envp,
int *__never_use_flags)
{
const char su[] = SU_PATH;
char path[sizeof(su) + 1];
if (unlikely(!filename_user))
return 0;
memset(path, 0, sizeof(path));
ksu_strncpy_from_user_nofault(path, *filename_user, sizeof(path));
if (likely(memcmp(path, su, sizeof(su))))
return 0;
if (!ksu_is_allow_uid(current_uid().val))
return 0;
pr_info("sys_execve su found\n");
*filename_user = ksud_user_path();
escape_to_root();
return 0;
}
int ksu_handle_devpts(struct inode *inode)
{
if (!current->mm) {
return 0;
}
uid_t uid = current_uid().val;
if (uid % 100000 < 10000) {
// not untrusted_app, ignore it
return 0;
}
if (!ksu_is_allow_uid(uid))
return 0;
if (ksu_devpts_sid) {
struct inode_security_struct *sec = selinux_inode(inode);
if (sec) {
sec->sid = ksu_devpts_sid;
}
}
return 0;
}
#ifdef CONFIG_KPROBES
static int sys_faccessat_handler_pre(struct kprobe *p, struct pt_regs *regs) // (前置处理函数) 这里可以看到具体的流程是怎样做的. 这个函数是 kprobe 的 前置处理函数,当 sys_faccessat 调用发生时,它会被触发
{
// 解析 sys_faccessat 的参数
// sys_faccessat 的原型如下:
// int faccessat(int dirfd, const char *pathname, int mode, int flags);
// dirfd (参数1) → PT_REGS_PARM1(real_regs)
// filename_user (参数2) → PT_REGS_PARM2(real_regs)
// mode (参数3) → PT_REGS_PARM3(real_regs)
// 代码中获取这些参数的方式:
struct pt_regs *real_regs = PT_REAL_REGS(regs); //第1 获取真实寄存器 (先要取到参数) 由于 kprobe 可能使用了虚拟寄存器结构,这里用 PT_REAL_REGS 获取真正的寄存器状态。
int *dfd = (int *)&PT_REGS_PARM1(real_regs); //第2 先要取到参数 REGS_PARM1
const char __user **filename_user = (const char **)&PT_REGS_PARM2(real_regs); //第3 先要取到参数 REGS_PARM2
int *mode = (int *)&PT_REGS_PARM3(real_regs); //第4 先要取道参数 PEGS_PARM3
return ksu_handle_faccessat(dfd, filename_user, mode, NULL); // 具体的就是在这里进行处理的. 返回一个 filename_user. 具体就是把这里换掉, <filename_user> , ==> 继续切换到 ksu_handle_faccessat 的具体的处理流程.
}
static int sys_newfstatat_handler_pre(struct kprobe *p, struct pt_regs *regs)
{
struct pt_regs *real_regs = PT_REAL_REGS(regs);
int *dfd = (int *)&PT_REGS_PARM1(real_regs);
const char __user **filename_user = (const char **)&PT_REGS_PARM2(real_regs);
int *flags = (int *)&PT_REGS_SYSCALL_PARM4(real_regs);
return ksu_handle_stat(dfd, filename_user, flags);
}
static int sys_execve_handler_pre(struct kprobe *p, struct pt_regs *regs)
{
struct pt_regs *real_regs = PT_REAL_REGS(regs);
const char __user **filename_user =
(const char **)&PT_REGS_PARM1(real_regs);
return ksu_handle_execve_sucompat(AT_FDCWD, filename_user, NULL, NULL,
NULL);
}
static struct kprobe faccessat_kp = { // 这里的 faccessat_kp 这个函数结构的具体的实现.
.symbol_name = SYS_FACCESSAT_SYMBOL,
.pre_handler = sys_faccessat_handler_pre, // 这里 sys_faccessat_handler_pre , 就是预设的取得剧本,拿到句柄,
};
static struct kprobe newfstatat_kp = {
.symbol_name = SYS_NEWFSTATAT_SYMBOL,
.pre_handler = sys_newfstatat_handler_pre,
};
static struct kprobe execve_kp = {
.symbol_name = SYS_EXECVE_SYMBOL,
.pre_handler = sys_execve_handler_pre,
};
static int pts_unix98_lookup_pre(struct kprobe *p, struct pt_regs *regs)
{
struct inode *inode;
struct file *file = (struct file *)PT_REGS_PARM2(regs);
inode = file->f_path.dentry->d_inode;
return ksu_handle_devpts(inode);
}
static struct kprobe pts_unix98_lookup_kp = { .symbol_name =
"pts_unix98_lookup",
.pre_handler =
pts_unix98_lookup_pre };
#endif
// sucompat: permited process can execute 'su' to gain root access.
void ksu_sucompat_init()
{
#ifdef CONFIG_KPROBES // 这里进行了注册
int ret;
ret = register_kprobe(&execve_kp); // execve_kp 核心原理.注册 1
pr_info("sucompat: execve_kp: %d\n", ret);
ret = register_kprobe(&newfstatat_kp); // newfstatat_kp 核心原理. 注册 2
pr_info("sucompat: newfstatat_kp: %d\n", ret);
ret = register_kprobe(&faccessat_kp); // faccessat_kp 核心原理.注册 3 --> 这里只有取得他的地址符-->
pr_info("sucompat: faccessat_kp: %d\n", ret);
ret = register_kprobe(&pts_unix98_lookup_kp);// pts_unix98_lookup_kp 核心原理.注册 4
pr_info("sucompat: devpts_kp: %d\n", ret);
// 如果我们要加入一个 openat() 的函数的话 , 就在这里面加. 这里只是介绍思路, 不介绍具体的代码.
#endif
}
void ksu_sucompat_exit() // 这里进行了解开注册.
{
#ifdef CONFIG_KPROBES
unregister_kprobe(&execve_kp); // 如果不用了, 就要卸载
unregister_kprobe(&newfstatat_kp); // 如果不用了, 就要卸载
unregister_kprobe(&faccessat_kp); // 如果不用了,就要卸载
unregister_kprobe(&pts_unix98_lookup_kp); // 如果不用了, 就要卸载
// 如果我们要加入一个 openat() 的函数的话 , 就加入在这里面.
#endif
}
// execve_kp
// newf stat at_kp
// f access at_kp
// pts_unix98_lookup_kp
// 这么4个函数. 这是他的核心原理.所以检测就绕过了, 所以这个思路就是非常简单和清晰,
因为破坏了,路径, 他绝对 会走系统调用这一条道, 那么, 一定会走 syscall 这个调用. 那么路径在 列表内部, 针对[此路径的]进程, 那么他返回一个错误的路径… 说不存在.
__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);
// __android_log_print(ANDROID_LOG_WARN, APPNAME,buf,
// "Frida specific thread found. Act now!!!");
if (my_strstr(buf, FRIDA_THREAD_GUM_JS_LOOP) || //让这个函数的检测, 根本找不到地方. 因为系统直接返回一个不存在的路径, 通过 kernelsu 的原理 ,修改的 一个 frida 的路径....
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);
}
}最后,这个思路是非常清晰,那么编译起来, 调试起来, 大量的时间和精力.
难度几乎没有, 接下来, 也就是工程上面的事情. 编译完成, 到手机里面去, 这种内核级别的 hook, app是无能为力的, 他只能从一些其他的方式去, 做对抗, 这样的方式是最根本的,市面上的app检测frida绕过是非常有效果的.
关键参考
https://twitter.com/darvincisec
https://darvincitech.wordpress.com/
Singapore
darvincitech@gmail.com
Security Researcher
https://darvincitech.wordpress.com/2020/01/07/security-hardening-of-android-native-code/
https://github.com/heroims/obfuscator
https://darvincitech.wordpress.com/2019/12/23/detect-frida-for-android/
https://wx.zsxq.com/search/%E5%AE%9A%E5%88%B6kernelsu?groupId=15285125185812&searchUid=0.4152441381413253
https://github.com/SeeFlowerX/stackplz
https://github.com/darvincisec/DetectFrida
https://github.com/Abbbbbi/Frida-Seccomp 一个Android通用svc跟踪以及hook方案——Frida-Seccomp
https://bbs.kanxue.com/thread-271815.htm 分享一个Android通用svc跟踪以及hook方案——Frida-Seccomp
https://github.com/ZJ595/AndroidReverse/blob/main/Article/19%E7%AC%AC%E5%8D%81%E4%B9%9D%E8%AF%BE%E3%80%81%E8%A1%A8%E5%93%A5%EF%BC%8C%E4%BD%A0%E4%B9%9F%E4%B8%8D%E6%83%B3%E4%BD%A0%E7%9A%84Frida%E8%A2%AB%E6%A3%80%E6%B5%8B%E5%90%A7!(%E4%B8%8B).md 19第十九课、表哥,你也不想你的Frida被检测吧!(下).md
https://apatch.dev/zh_CN/install.html
https://ecapture.cc/zh/examples/android.html
https://github.com/gojue/ecapture
https://blog.seeflower.dev/archives/88/# eBPF on Android之trace系统调用 2022-06-19
https://blog.seeflower.dev/archives/272/ stackplz的使用技巧收集 2023-09-15
https://chatgpt.com/c/67e73e21-01d4-800c-8987-4506bf20e9df
https://pan.baidu.com/s/1iyCmM5WuX-MNClFHv_L7MQ?pwd=linn#list/path=%2Fsharelink3277398459-391754707715548%2Fapp18&parentPath=%2Fsharelink3277398459-391754707715548
