
一、课程目标
1.了解常见frida检测
二、工具
1.教程Demo(更新)
2.jadx-gui
3.VS Code
4.jeb
三、课程内容
1.检测文件名、端口名、双进程保护、失效的检测点
1.检测/data/local/tmp路径下的是否有frida特征文件,server端改名,例如:fr
2.指定端口转发
./fs1 -l 0.0.0.0:6666
adb forward tcp:6666 tcp:6666
frida -H 127.0.0.1:6666 wuaipojie -l hook.js
3.spawn启动过双进程保护
frida -U -f 进程名 -l hook.js
PS:学会看注入报错的日志,比如说当app主动附加自身进程时,这时候再注入就会提示run frida as root
(以spawn的方式启动进程即可)
4.借助脚本定位检测frida的so
function hook_dlopen() {
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();
console.log("load " + path);
}
}
}
);
}
5.随着firda的版本迭代,以前诸多检测点以失效
(1.)例如检测D-Bus
D-Bus是一种进程间通信(IPC)和远程过程调用(RPC)机制,最初是为Linux开发的,目的是用一个统一的协议替代现有的和竞争的IPC解决方案。
bool check_dbus() {
struct sockaddr_in sa;
int sock;
char res[7];
for(int i = 0; i <= 65535; i++) {
sock = socket(AF_INET, SOCK_STREAM, 0);
sa.sin_port = htons(i);
if (connect(sock, (struct sockaddr*)&sa, sizeof(sa)) != -1) {
__android_log_print(ANDROID_LOG_VERBOSE, "ZJ595", "FRIDA DETECTION [1]: Open Port: %d", i);
memset(res, 0, 7);
send(sock, "\x00", 1, 0);
send(sock, "AUTH\r\n", 6, 0);
usleep(100);
if (recv(sock, res, 6, MSG_DONTWAIT) != -1) {
if (strcmp(res, "REJECT") == 0) {
close(sock);
return true;
}
}
}
close(sock);
}
return false;
}
(2)检测fd
/proc/pid/fd 目录的作用在于提供了一种方便的方式来查看进程的文件描述符信息,这对于调试和监控进程非常有用。通过查看文件描述符信息,可以了解进程打开了哪些文件、网络连接等,帮助开发者和系统管理员进行问题排查和分析工作。
bool check_fd() {
DIR *dir = NULL;
struct dirent *entry;
char link_name[100];
char buf[100];
bool ret = false;
if ((dir = opendir("/proc/self/fd/")) == NULL) {
LOGI(" %s - %d error:%s", __FILE__, __LINE__, strerror(errno));
} else {
entry = readdir(dir);
while (entry) {
switch (entry->d_type) {
case DT_LNK:
sprintf(link_name, "%s/%s", "/proc/self/fd/", entry->d_name);
readlink(link_name, buf, sizeof(buf));
if (strstr(buf, "frida") || strstr(buf, "gum-js-loop") ||
strstr(buf, "gmain") ||
strstr(buf, "-gadget") || strstr(buf, "linjector")) {
LOGI("check_fd -> find frida:%s", buf);
ret = true;
}
break;
default:
break;
}
entry = readdir(dir);
}
}
closedir(dir);
return ret;
}
(3)检测文件
众所周知frida我们一般都会放在data/local/tmp目录下,旧版fridaserver端运行时都会释放到re.frida.server,所以这里在旧版也会被当做一个检测点,而新版已不再释放
2检测map
adb shell ps | findstr com.zj.wuaipojie
cat /proc/12186/maps|grep frida

字段 |
描述 |
u0_a504 |
用户ID和应用ID:在Android系统中,u0 代表系统用户(user 0),而a504 是该应用在用户0下的唯一标识符。 |
28082 |
PID(进程ID):该进程在操作系统中的标识符。 |
1935 |
PPID(父进程ID):该进程的父进程的PID。 |
6511212 |
虚拟内存:进程使用的虚拟内存大小,通常以字节为单位。 |
125728 |
共享内存:进程使用的共享内存大小,同样以字节为单位。 |
0 |
CPU时间/线程数:这通常表示进程的CPU时间或者是线程数,具体含义取决于ps 命令的输出格式。 |
S |
状态:其中S 代表睡眠状态(Sleeping),即进程没有在执行,而是在等待某些事件或资源。 |
/proc/self/maps
是一个特殊的文件,它包含了当前进程的内存映射信息。当你打开这个文件时,它会显示一个列表,其中包含了进程中每个内存区域的详细信息。这些信息通常包括:
- 起始地址(Start Address)
- 结束地址(End Address)
- 权限(如可读、可写、可执行)
- 共享/私有标志(Shared or Private)
- 关联的文件或设备(如果内存区域是文件映射的)
- 内存区域的偏移量
- 内存区域的类型(如匿名映射、文件映射、设备映射等)
当注入frida后,在maps文件中就会存在 frida-agent-64.so
、frida-agent-32.so
等文件。
bool check_maps() {
char line[512];
FILE* fp = fopen("/proc/self/maps", "r");
if (fp) {
while (fgets(line, sizeof(line), fp)) {
if (strstr(line, "frida") || strstr(line, "gadget")) {
fclose(fp);
return true;
}
}
fclose(fp);
} else {
}
return false;
}
方法1
anti脚本
function anti_maps() {
var pt_strstr = Module.findExportByName("libc.so", 'strstr');
var pt_strcmp = Module.findExportByName("libc.so", 'strcmp');
Interceptor.attach(pt_strstr, {
onEnter: function (args) {
var str1 = args[0].readCString();
var str2 = args[1].readCString();
if (str2.indexOf("REJECT") !== -1 || str2.indexOf("frida") !== -1) {
this.hook = true;
}
},
onLeave: function (retval) {
if (this.hook) {
retval.replace(0);
}
}
});
Interceptor.attach(pt_strcmp, {
onEnter: function (args) {
var str1 = args[0].readCString();
var str2 = args[1].readCString();
if (str2.indexOf("REJECT") !== -1 || str2.indexOf("frida") !== -1) {
this.hook = true;
}
},
onLeave: function (retval) {
if (this.hook) {
retval.replace(0);
}
}
});
}
方法2
重定向maps
function mapsRedirect() {
var FakeMaps = "/data/data/com.zj.wuaipojie/maps";
const openPtr = Module.getExportByName('libc.so', 'open');
const open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
var readPtr = Module.findExportByName("libc.so", "read");
var read = new NativeFunction(readPtr, 'int', ['int', 'pointer', "int"]);
var MapsBuffer = Memory.alloc(512);
var MapsFile = new File(FakeMaps, "w");
Interceptor.replace(openPtr, new NativeCallback(function(pathname, flag) {
var FD = open(pathname, flag);
var ch = pathname.readCString();
if (ch.indexOf("/proc/") >= 0 && ch.indexOf("maps") >= 0) {
console.log("open : ", pathname.readCString());
while (parseInt(read(FD, MapsBuffer, 512)) !== 0) {
var MBuffer = MapsBuffer.readCString();
MBuffer = MBuffer.replaceAll("/data/local/tmp/re.frida.server/frida-agent-64.so", "FakingMaps");
MBuffer = MBuffer.replaceAll("re.frida.server", "FakingMaps");
MBuffer = MBuffer.replaceAll("frida-agent-64.so", "FakingMaps");
MBuffer = MBuffer.replaceAll("frida-agent-32.so", "FakingMaps");
MBuffer = MBuffer.replaceAll("frida", "FakingMaps");
MBuffer = MBuffer.replaceAll("/data/local/tmp", "/data");
MapsFile.write(MBuffer);
}
var filename = Memory.allocUtf8String(FakeMaps);
return open(filename, flag);
}
return FD;
}, 'int', ['pointer', 'int']));
}
方法3
用eBPF来hook系统调用并修改参数实现目的,使用bpf_probe_write_user向用户态函数地址写内容直接修改参数
char placeholder[] = "/data/data/com.zj.wuaipojie/maps";
bpf_probe_write_user((void*)addr, placeholder, sizeof(placeholder));
3.检测status(线程名)
ls /proc/pid/task 列出线程id
cat /proc/pid/task/线程id/status
- 在
/proc/pid/task
目录下,可以通过查看不同的线程子目录,来获取进程中每个线程的运行时信息。这些信息包括线程的状态、线程的寄存器内容、线程占用的CPU时间、线程的堆栈信息等。通过这些信息,可以实时观察和监控进程中每个线程的运行状态,帮助进行调试、性能优化和问题排查等工作。
- 在某些app中就会去读取
/proc/stask/线程ID/status
文件,如果是运行frida产生的,则进行反调试。例如:gmain/gdbus/gum-js-loop/pool-frida
等
- gmain:Frida 使用 Glib 库,其中的主事件循环被称为 GMainLoop。在 Frida 中,gmain 表示 GMainLoop 的线程。
- gdbus:GDBus 是 Glib 提供的一个用于 D-Bus 通信的库。在 Frida 中,gdbus 表示 GDBus 相关的线程。
- gum-js-loop:Gum 是 Frida 的运行时引擎,用于执行注入的 JavaScript 代码。gum-js-loop 表示 Gum 引擎执行 JavaScript 代码的线程。
- pool-frida:Frida 中的某些功能可能会使用线程池来处理任务,pool-frida 表示 Frida 中的线程池。
- linjector 是一种用于 Android 设备的开源工具,它允许用户在运行时向 Android 应用程序注入动态链接库(DLL)文件。通过注入 DLL 文件,用户可以修改应用程序的行为、调试应用程序、监视函数调用等,这在逆向工程、安全研究和动态分析中是非常有用的。
PS:由于frida可以随时附加到进程,所以写的检测必须覆盖APP的全周期,或者至少是敏感函数执行前
bool check_status() {
DIR *dir = opendir("/proc/self/task/");
struct dirent *entry;
char status_path[MAX_PATH];
char buffer[MAX_BUFFER];
int found = false;
if (dir) {
while ((entry = readdir(dir)) != NULL) {
if (entry->d_type == DT_DIR) {
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) {
continue;
}
snprintf(status_path, sizeof(status_path), "/proc/self/task/%s/status", entry->d_name);
if (read_file(status_path, buffer, sizeof(buffer)) == -1) {
continue;
}
if (strcmp(buffer, "null") == 0) {
continue;
}
char *line = strtok(buffer, "\n");
while (line) {
if (strstr(line, "Name:") != NULL) {
const char *frida_name = strstr(line, "gmain");
if (frida_name || strstr(line, "gum-js-loop") || strstr(line, "pool-frida") || strstr(line, "gdbus")) {
found = true;
break;
}
}
line = strtok(NULL, "\n");
}
if (found) break;
}
}
closedir(dir);
}
return found;
}
anti脚本
function replace_str() {
var pt_strstr = Module.findExportByName("libc.so", 'strstr');
var pt_strcmp = Module.findExportByName("libc.so", 'strcmp');
Interceptor.attach(pt_strstr, {
onEnter: function (args) {
var str1 = args[0].readCString();
var str2 = args[1].readCString();
if (str2.indexOf("tmp") !== -1 ||
str2.indexOf("frida") !== -1 ||
str2.indexOf("gum-js-loop") !== -1 ||
str2.indexOf("gmain") !== -1 ||
str2.indexOf("gdbus") !== -1 ||
str2.indexOf("pool-frida") !== -1||
str2.indexOf("linjector") !== -1) {
this.hook = true;
}
}, onLeave: function (retval) {
if (this.hook) {
retval.replace(0);
}
}
});
Interceptor.attach(pt_strcmp, {
onEnter: function (args) {
var str1 = args[0].readCString();
var str2 = args[1].readCString();
if (str2.indexOf("tmp") !== -1 ||
str2.indexOf("frida") !== -1 ||
str2.indexOf("gum-js-loop") !== -1 ||
str2.indexOf("gmain") !== -1 ||
str2.indexOf("gdbus") !== -1 ||
str2.indexOf("pool-frida") !== -1||
str2.indexOf("linjector") !== -1) {
this.hook = true;
}
}, onLeave: function (retval) {
if (this.hook) {
retval.replace(0);
}
}
})
}
4.检测inlinehook
通过Frida查看一个函数hook之前和之后的机器码,以此来判断是否被Frida的inlinehook注入。

下面的方案以内存中字节和本地对应的字节进行比较,如果不一致,那么可以认为内存中的字节被修改了,即被inlinehook了
#include <jni.h>
#include <string>
#include <dlfcn.h>
#include "dlfcn/local_dlfcn.h"
bool check_inlinehook() {
const char *lib_path;
#ifdef __LP64__
lib_path = "/system/lib64/libc.so";
#else
lib_path = "/system/lib/libc.so";
#endif
const int CMP_COUNT = 8;
const char *sym_name = "open";
struct local_dlfcn_handle *handle = static_cast<local_dlfcn_handle *>(local_dlopen(lib_path));
if (!handle) {
return JNI_FALSE;
}
off_t offset = local_dlsym(handle, sym_name);
local_dlclose(handle);
FILE *fp = fopen(lib_path, "rb");
if (!fp) {
return JNI_FALSE;
}
char file_bytes[CMP_COUNT] = {0};
fseek(fp, offset, SEEK_SET);
fread(file_bytes, 1, CMP_COUNT, fp);
fclose(fp);
void *dl_handle = dlopen(lib_path, RTLD_NOW);
if (!dl_handle) {
return JNI_FALSE;
}
void *sym = dlsym(dl_handle, sym_name);
if (!sym) {
dlclose(dl_handle);
return JNI_FALSE;
}
int is_hook = memcmp(file_bytes, sym, CMP_COUNT) != 0;
dlclose(dl_handle);
return is_hook ? JNI_TRUE : JNI_FALSE;
}
获取hook前字节码的脚本
let bytes_count = 8
let address = Module.getExportByName("libc.so","open")
let before = ptr(address)
console.log("")
console.log(" before hook: ")
console.log(hexdump(before, {
offset: 0,
length: bytes_count,
header: true,
ansi: true
}));
anti脚本
function hook_memcmp_addr(){
var memcmp_addr = Module.findExportByName("libc.so", "fread");
if (memcmp_addr !== null) {
console.log("fread address: ", memcmp_addr);
Interceptor.attach(memcmp_addr, {
onEnter: function (args) {
this.buffer = args[0];
this.size = args[1];
this.count = args[2];
this.stream = args[3];
},
onLeave: function (retval) {
console.log(this.count.toInt32());
if (this.count.toInt32() == 8) {
Memory.writeByteArray(this.buffer, [0x50, 0x00, 0x00, 0x58, 0x00, 0x02, 0x1f, 0xd6]);
retval.replace(8);
console.log(hexdump(this.buffer));
}
}
});
} else {
console.log("Error: memcmp function not found in libc.so");
}
}
5.刷入魔改的frida-server端
注意版本!!!
strongR-frida-android
五、答疑
待更新
六、视频及课件地址
百度云
阿里云
哔哩哔哩
教程开源地址
PS:解压密码都是52pj,阿里云由于不能分享压缩包,所以下载exe文件,双击自解压
七、其他章节
《安卓逆向这档事》一、模拟器环境搭建
《安卓逆向这档事》二、初识APK文件结构、双开、汉化、基础修改
《安卓逆向这档事》三、初识smail,vip终结者
《安卓逆向这档事》四、恭喜你获得广告&弹窗静默卡
《安卓逆向这档事》五、1000-7=?&动态调试&Log插桩
《安卓逆向这档事》六、校验的N次方-签名校验对抗、PM代{过}{滤}理、IO重定向
《安卓逆向这档事》七、Sorry,会Hook真的可以为所欲为-Xposed快速上手(上)模块编写,常用Api
《安卓逆向这档事》八、Sorry,会Hook真的可以为所欲为-xposed快速上手(下)快速hook
《安卓逆向这档事》九、密码学基础、算法自吐、非标准加密对抗
《安卓逆向这档事》十、不是我说,有了IDA还要什么女朋友?
《安卓逆向这档事》十二、大佬帮我分析一下
《安卓逆向这档事》番外实战篇1-某电影视全家桶
《安卓逆向这档事》十三、是时候学习一下Frida一把梭了(上)
《安卓逆向这档事》十四、是时候学习一下Frida一把梭了(中)
《安卓逆向这档事》十五、是时候学习一下Frida一把梭了(下)
《安卓逆向这档事》十六、是时候学习一下Frida一把梭了(终)
《安卓逆向这档事》十七、你的RPCvs佬的RPC
《安卓逆向这档事》番外实战篇2-【2024春节】解题领红包活动,启动!
八、参考文档
frida常用检测点及其原理--一把梭方案
frida 检测
从inlinehook角度检测frida
Frida检测手段探究
Lsposed 技术原理探讨 && 基本安装使用
strongR-frida-android
[原创]绕过bilibili frida反调试
基础反检测 frida-server
关于frida检测的一个新思路
读取本地so文件的符号的偏移
多种姿势花样使用Frida注入