这是最基础也是最常见的检测手段。由于绝大多数 Frida 教程都指导用户将服务端文件命名为 frida-server 并放置在 /data/local/tmp/ 目录下,App 的防护机制会针对这些“标准特征”进行扫描。
防护代码通常会在 App 启动时执行以下检查:
App 会尝试访问 Android 系统中的常见临时目录,查找是否存在包含 frida 关键字的文件。
代码示例 (伪代码) :
App 可以通过 Shell 命令或读取系统文件来获取当前运行的所有进程列表,并匹配黑名单。
扫描方式:执行 ps -A 查看进程状态。

敏感关键字:frida-server, frida-helper, frida-launcher
代码示例 (伪代码) :
针对上述特征检测,最直接有效的方法就是 “改名” 。
这是最简单的一步。将 frida-server 重命名为类似系统进程的名字,既能避开文件扫描,启动后的进程名也会随之改变,从而避开进程扫描。
操作步骤:
有些深度检测不仅仅匹配文件名,还会扫描二进制文件内部的字符串特征(如 frida:rpc 字符串)。
此时,单纯重命名文件无效。你需要使用 HLuda(魔改版 Frida)。
Frida 在运行时需要开启 TCP 端口与电脑端进行数据传输。如果不加配置,Frida 会默认绑定特定的特征端口,这成为了 App 检测的重要依据。
Frida 服务端启动后,默认会监听以下两个端口:
防护代码通常采用以下方式进行探测:
App 可以执行 netstat 命令,查看当前系统是否有名为 0.0.0.0:27042 或 127.0.0.1:27042 的监听状态。

绕过的核心思想是 “避开默认” 。只要我们不使用 27042 端口,上述针对固定端口的扫描就会失效。
在手机端启动 frida-server 时,使用 -l (listen) 参数指定一个随机的非标准端口(例如 8888 或 6666)。
修改端口后,电脑端的 Frida 客户端不会自动识别,必须手动进行端口转发或指定连接地址。
方式 A:通过 USB 转发 (推荐)
我们需要将电脑的 8888 端口转发到手机的 8888 端口。
方式 B:通过 WiFi 连接
如果手机和电脑在同一局域网,可以直接指定手机 IP。
这是 Android Native 层反调试最经典的手段之一。
Linux/Android 内核规定:同一个进程同时只能被一个调试器(Tracer)附加。
Frida 的工作原理:Frida 本质上也是一个调试器,它需要通过 ptrace 系统调用附加(Attach)到目标 App 进程上才能进行 Hook。
App 的防御手段:App 在启动的极早期(通常在 .init_array 或 JNI_OnLoad 中),主动调用 ptrace(PTRACE_TRACEME, 0, 0, 0) 自己附加自己。
冲突结果:
代码示例 (C/C++) :
针对 ptrace 占坑,单纯的“Attach 模式”是必死的。我们需要利用 Frida 的 Spawn 模式 抢在 App 运行代码前介入,并修改 App 的检测逻辑。
Spawn 模式 (-f) 会由 Zygote 进程 fork 出目标 App,并在 App 执行任何自己的代码(包括反调试代码)之前将 Frida 的 Gadget/Agent 注入进去。
这给了我们一个“时间差”:Frida 先进去了,现在轮到 Frida 掌控局面。
仅仅进入是不够的。如果 Frida 保持附加状态,App 随后的 ptrace 调用会失败,App 依然会发现并退出。
根本的解决办法是:Hook libc.so 中的 ptrace 函数,拦截 App 的调用,直接返回 0(假装成功),并且不要执行真正的 ptrace 系统调用。
示例脚本:
Frida 的客户端与服务端通信基于 D-Bus 协议(一种进程间通信/RPC 机制)。即使我们将 Frida 服务端改名并迁移目录,其通信协议的特征(尤其是握手阶段)依然是固定的。
App 可以遍历本地所有开放的 TCP 端口,并尝试建立 Socket 连接。一旦连接成功,App 会发送 D-Bus 协议特定的握手消息。如果服务端响应了特定的 D-Bus 关键字(如 REJECT),则可认定该端口运行着 Frida。
检测代码示例 (伪代码) :
这是您提到的方法。原理是拦截 App 在处理网络响应时的比较逻辑。当 App 拿收到的数据去匹配 "REJECT" 时,我们欺骗它说“没匹配上”。
注意:strcmp 和 strstr 是系统底层极其高频调用的函数。直接 Hook 会导致 App 巨卡无比甚至崩溃,必须加严格的过滤条件。
相比于在运行时 Hook 极不稳定的 strstr,最彻底的办法是从源码层面消除特征。
HLuda(或其他去特征版本)在编译 Frida 时做了如下修改:
使用此类版本后,App 发送探测包收到的将不再是标准的 REJECT,从而直接绕过检测机制,无需编写任何 Hook 脚本。
在 Linux/Android 系统中,一切皆文件。/proc/pid/fd/ 目录包含了当前进程打开的所有文件描述符(File Descriptors)。App 通过遍历该目录,可以获知进程加载了哪些文件、建立了哪些 Socket 连接。
App 遍历 /proc/self/fd 目录下的所有文件,并使用 readlink 读取符号链接指向的真实路径。
相关命令:ls -l /proc/self/fd 或 readlink -f /proc/self/fd/<id>
检测代码示例 (伪代码) :
如果 Frida 注入成功,该目录下可能会出现指向以下路径的链接:
传统特征 (旧版 Frida) :
在 高版本 Frida (>=16) 时:
检查 FD 已经基本无法检测 Frida。
在 Android 10+ 中,Google 对 memfd 的名称做了更严格的匿名化,Frida 使用的 memfd 不再暴露名称。
无论特征如何变化,检测的核心动作都是调用 readlink 或 readlinkat 系统函数。
核心思路:Hook 这两个函数,当 App 试图读取的文件描述符指向包含 frida、gum 或 memfd 的路径时,将其替换为合法的系统路径(如 /dev/null 或 libc.so)。
在 Android (Linux) 系统中,进程的每一个线程在 /proc/pid/task/ 目录下都有一个对应的子目录(目录名为线程 ID)。
App 可以遍历该目录,读取每个线程的 status 或 comm 文件,获取线程名称。由于 Frida 依赖 GLib 库,运行时会创建特定的辅助线程,这些线程的默认名称具有极高的辨识度。
如果 Frida 注入成功,进程中通常会出现以下命名的线程:


核心思路是拦截对 /proc 目录下 comm 或 status 文件的读取操作。当发现 App 试图读取特定线程的信息时,检查读取到的内容,如果是 gmain 等敏感名称,将其替换为合法的系统线程名(如 epoll、config_store 等)。
由于 gmain 和 gdbus 是 GLib 库硬编码的名称,gum-js-loop 是 Frida 源码硬编码的。
HLuda 等修改版 Frida 在编译阶段修改了这些字符串常量(例如将 gmain 改为 fmain 或其他无意义字符串),从根本上规避了针对特定字符串的检测。这是最推荐的方式。
/proc/self/maps 是 Android/Linux 系统中极为关键的文件,它记录了当前进程所有连续的虚拟内存区域的分布情况。App 通过读取该文件,可以精准地知道进程加载了哪些 SO 库、DEX 文件,以及内存段的权限属性。
maps 文件的一行通常包含以下信息:
如果 Frida 注入成功,App 遍历该文件通常会发现以下痕迹:
既然 App 必须通过 open 打开文件,再通过 read 读取内容,我们可以在它打开 /proc/self/maps 的瞬间,将目标路径重定向到一个我们准备好的、内容干净的“假 maps”文件上。
在未注入 Frida 之前(或使用 Spawn 模式暂停时),先手动提取一份正常的 maps 文件:
并在该文件中手动删去可能存在的敏感行(如果有)。
编写 Frida 脚本,拦截系统底层的文件打开操作。
关键细节:直接修改 args[0] 指向的内存可能会导致缓冲区溢出(如果新路径比旧路径长)。安全的做法是 Memory.allocUtf8String 分配新内存,并将 args[0] 指针替换过去。
对于 fd (文件描述符)、status (线程状态)、maps (内存映射) 这三类基于 /proc 文件系统的检测,上述 IO 重定向 方案是通解。只要将敏感文件指向无害文件(如 /dev/null 或提前备份的正常文件),即可实现完美绕过。
Inline Hook 是 Frida Interceptor 模块的核心机制。它的原理是直接修改内存中目标函数的前几条指令(Prologue) ,将其替换为一条跳转指令(Trampoline/跳板),从而将程序执行流引导至 Frida 的处理函数。
App 可以通过检查关键函数开头的机器码(Machine Code)是否被篡改来检测 Hook。
在 ARM64 架构下,Frida 通常需要替换前 16 个字节(4 条指令)来实现长跳转。它会将原始指令备份,并写入以下经典的 "LDR + BR" 跳板指令:
Hook 前 (原始指令) :
这是函数原本正常的汇编指令(示例):
Hook 后 (Frida 特征) :
内存中的指令变成了跳转逻辑:
App 会计算内存中函数前 N 个字节的 CRC32 或 MD5 值,并与本地文件(SO 库)中该函数的原始机器码进行比对。
检测逻辑的核心在于“比对”。App 通常会读取磁盘上的 SO 文件内容与内存内容进行比较。我们可以 Hook libc.so 中的 memcmp 函数。
当发现 App 正在比对我们已 Hook 的函数地址时,强行让 memcmp 返回 0(即相等),从而“指鹿为马”。
Frida 的 Inline Hook 机制本质上是 “篡改内存代码” 。即使只修改了几个字节(写入跳转指令),也会导致该内存区域的校验和(Checksum)发生变化。
App 通常采用 “磁盘 vs 内存” 的比对方式来检测代码是否被篡改。
检测代码逻辑 (伪代码) :
既然修改代码会被检测,那就不要修改代码。
Frida 提供了硬件断点功能(基于 CPU 的调试寄存器),可以在不修改内存字节的情况下实现拦截。
找到 App 计算或比对 CRC 的核心函数(如 memcmp、自定义的 check_crc),对其进行 Hook。
如果 App 是通过 open/read 读取磁盘文件来计算基准值的,我们可以 Hook 这些 IO 函数。
思路:当 App 试图读取磁盘上的 SO 文件时,我们将其重定向到内存中已经被 Hook 的那段数据上(或者反过来,当它读内存时给它返回磁盘数据)。
File file = new File("/data/local/tmp/frida-server");
if (file.exists()) {
killApp();
}
File file = new File("/data/local/tmp/frida-server");
if (file.exists()) {
killApp();
}
Process process = Runtime.getRuntime().exec("ps -A");
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("frida-server")) {
throw new RuntimeException("Frida detected!");
}
}
Process process = Runtime.getRuntime().exec("ps -A");
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
if (line.contains("frida-server")) {
throw new RuntimeException("Frida detected!");
}
}
pkill -f frida-server
cd /data/local/tmp
mv frida-server-16.x.x-android-arm64 fs
chmod +x fs
./fs &
pkill -f frida-server
cd /data/local/tmp
mv frida-server-16.x.x-android-arm64 fs
chmod +x fs
./fs &
./frida-server -l 0.0.0.0:8888 &
./frida-server -l 0.0.0.0:8888 &
adb forward tcp:8888 tcp:8888
frida -H 127.0.0.1:8888 -l hook.js -f com.example.target
adb forward tcp:8888 tcp:8888
frida -H 127.0.0.1:8888 -l hook.js -f com.example.target
frida -H 192.168.1.10:8888 -l hook.js -f com.example.target
frida -H 192.168.1.10:8888 -l hook.js -f com.example.target
int status = ptrace(PTRACE_TRACEME, 0, 0, 0);
if (status == -1) {
kill(getpid(), SIGKILL);
}
int status = ptrace(PTRACE_TRACEME, 0, 0, 0);
if (status == -1) {
kill(getpid(), SIGKILL);
}
function bypassPtrace() {
var ptracePtr = Module.findExportByName("libc.so", "ptrace");
if (ptracePtr) {
Interceptor.replace(ptracePtr, new NativeCallback(function(request, pid, addr, data) {
console.log("[+] ptrace called - bypassing...");
console.log(" request: " + request);
if (request == 0) {
console.log(" [+] Blocked PTRACE_TRACEME!");
return 0;
}
return 0;
}, 'long', ['int', 'int', 'pointer', 'pointer']));
console.log("[+] ptrace bypass applied!");
} else {
console.log("[-] ptrace not found");
}
}
setImmediate(function() {
bypassPtrace();
});
function bypassPtrace() {
var ptracePtr = Module.findExportByName("libc.so", "ptrace");
if (ptracePtr) {
Interceptor.replace(ptracePtr, new NativeCallback(function(request, pid, addr, data) {
console.log("[+] ptrace called - bypassing...");
console.log(" request: " + request);
if (request == 0) {
console.log(" [+] Blocked PTRACE_TRACEME!");
return 0;
}
return 0;
}, 'long', ['int', 'int', 'pointer', 'pointer']));
console.log("[+] ptrace bypass applied!");
} else {
console.log("[-] ptrace not found");
}
}
setImmediate(function() {
bypassPtrace();
});
Socket socket = new Socket("127.0.0.1", port);
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
os.write(new byte[]{0});
byte[] buffer = new byte[512];
int len = is.read(buffer);
String response = new String(buffer, 0, len);
if (response.contains("REJECT")) {
}
Socket socket = new Socket("127.0.0.1", port);
InputStream is = socket.getInputStream();
OutputStream os = socket.getOutputStream();
os.write(new byte[]{0});
byte[] buffer = new byte[512];
int len = is.read(buffer);
String response = new String(buffer, 0, len);
if (response.contains("REJECT")) {
}
function bypassDBusDetection() {
var strstr = Module.findExportByName("libc.so", "strstr");
if (strstr) {
Interceptor.attach(strstr, {
onEnter: function(args) {
this.needle = Memory.readUtf8String(args[1]);
if (this.needle && (this.needle.indexOf("REJECT") !== -1 || this.needle.indexOf("frida") !== -1)) {
this.detected = true;
}
},
onLeave: function(retval) {
if (this.detected) {
retval.replace(0);
}
}
});
}
}
function bypassDBusDetection() {
var strstr = Module.findExportByName("libc.so", "strstr");
if (strstr) {
Interceptor.attach(strstr, {
onEnter: function(args) {
this.needle = Memory.readUtf8String(args[1]);
if (this.needle && (this.needle.indexOf("REJECT") !== -1 || this.needle.indexOf("frida") !== -1)) {
this.detected = true;
}
},
onLeave: function(retval) {
if (this.detected) {
retval.replace(0);
}
}
});
}
}
File fdDir = new File("/proc/self/fd");
for (File fd : fdDir.listFiles()) {
String realPath = Os.readlink(fd.getAbsolutePath());
if (realPath.contains("frida") || realPath.contains("gum-js")) {
}
}
File fdDir = new File("/proc/self/fd");
for (File fd : fdDir.listFiles()) {
String realPath = Os.readlink(fd.getAbsolutePath());
if (realPath.contains("frida") || realPath.contains("gum-js")) {
}
}
function bypassProcCheck() {
var readlink = Module.findExportByName("libc.so", "readlink");
var readlinkat = Module.findExportByName("libc.so", "readlinkat");
var handleReadlink = function(buffer, size, retval) {
if (retval.toInt32() > 0) {
var path = Memory.readUtf8String(buffer, retval.toInt32());
if (path && (path.indexOf("frida") !== -1 ||
path.indexOf("gum-js") !== -1 ||
path.indexOf("gmain") !== -1 ||
path.indexOf("linjector") !== -1)) {
var fakePath = "/system/lib64/libc.so";
Memory.writeUtf8String(buffer, fakePath);
retval.replace(fakePath.length);
}
}
};
if (readlinkat) {
Interceptor.attach(readlinkat, {
onEnter: function(args) {
this.buf = args[2];
},
onLeave: function(retval) {
handleReadlink(this.buf, 0, retval);
}
});
}
if (readlink) {
Interceptor.attach(readlink, {
onEnter: function(args) {
this.buf = args[1];
},
onLeave: function(retval) {
handleReadlink(this.buf, 0, retval);
}
});
}
}
setImmediate(bypassProcCheck);
function bypassProcCheck() {
var readlink = Module.findExportByName("libc.so", "readlink");
var readlinkat = Module.findExportByName("libc.so", "readlinkat");
var handleReadlink = function(buffer, size, retval) {
if (retval.toInt32() > 0) {
var path = Memory.readUtf8String(buffer, retval.toInt32());
if (path && (path.indexOf("frida") !== -1 ||
path.indexOf("gum-js") !== -1 ||
path.indexOf("gmain") !== -1 ||
path.indexOf("linjector") !== -1)) {
var fakePath = "/system/lib64/libc.so";
Memory.writeUtf8String(buffer, fakePath);
retval.replace(fakePath.length);
}
[培训]Windows内核深度攻防:从Hook技术到Rootkit实战!
最后于 2025-12-8 16:59
被xiusi编辑
,原因: