当前位置: 首页 > article >正文

安卓逆向之过frida检测总结版

一:检测文件名、端口名、双进程保护、失效的检测点

1.检测/data/local/tmp路径下的是否有frida特征文件,server端改名,例如:fr

在 Android 应用程序中,检测 /data/local/tmp 路径下是否存在 Frida 特征文件是一种常见的反调试和反逆向工程技术。Frida 在运行时会在该路径下创建一些特征文件,攻击者可以通过这些文件来识别 Frida 的存在。

1. Frida 特征文件

Frida 在 /data/local/tmp 目录下可能会创建以下文件:

  • frida-server: Frida 的服务器文件,通常用于与 Frida 客户端进行通信。
  • frida-agent: Frida 的代理文件,可能用于注入到目标应用程序中。
  • frida-*.so: 以 frida- 开头的共享库文件,可能是 Frida 的动态链接库。

2. 检测方法

a. 检查特征文件

应用程序可以在启动时检查 /data/local/tmp 目录,查看是否存在上述特征文件。例如,使用以下代码片段:

File dir = new File("/data/local/tmp");
File[] files = dir.listFiles();
if (files != null) {
    for (File file : files) {
        if (file.getName().startsWith("frida")) {
            // 检测到 Frida 特征文件
            System.out.println("Frida detected: " + file.getName());
        }
    }
}

b. 检查进程

应用程序可以检查当前运行的进程,查看是否有与 Frida 相关的进程在运行。Frida 通常会以 frida-server 或类似名称的进程存在。

Process process = Runtime.getRuntime().exec("ps");
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
    if (line.contains("frida")) {
        // 检测到 Frida 进程
        System.out.println("Frida process detected: " + line);
    }
}

c. 检查网络连接

Frida 服务器通常会在特定端口(如 27042)上监听。应用程序可以检查是否有异常的网络连接。

// 示例代码,检查特定端口是否被占用
try (ServerSocket socket = new ServerSocket(27042)) {
    // 端口未被占用
} catch (IOException e) {
    // 端口被占用,可能是 Frida 服务器
    System.out.println("Frida server might be running on port 27042");
}

3. 其他反检测措施

除了检测特征文件和进程外,应用程序还可以采取其他措施来防止 Frida 的使用:

  • 重命名 Frida 服务器: 改名为其他不易识别的名称(如 fr),可以减少被检测的可能性。
  • 检查系统属性: 检查系统属性中是否有与 Frida 相关的条目。
  • 监控调试状态: 检查应用程序是否在调试模式下运行,使用 android.os.Debug.isDebuggerConnected() 方法。
  • 使用反调试技术: 例如,使用 ptrace 检测是否有调试器附加到进程。

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

1. 启动服务

./fs1 -l 0.0.0.0:6666

  • ./fs1: 这是一个可执行文件,通常是一个自定义的或特定的工具,可能是用于与 Frida 进行交互的服务或代理。
  • l 0.0.0.0:6666: 这个参数指定了服务监听的地址和端口。0.0.0.0 表示服务将监听所有可用的网络接口,6666 是指定的端口号。这意味着任何可以访问该机器的客户端都可以通过 6666 端口连接到这个服务。

2. 设置端口转发

adb forward tcp:6666 tcp:6666

  • adb: Android Debug Bridge,是一个命令行工具,允许与 Android 设备进行通信。
  • forward: 这个命令用于设置端口转发。
  • tcp:6666 tcp:6666: 这表示将本地计算机的 6666 端口转发到连接的 Android 设备的 6666 端口。这样,您可以通过本地计算机的 6666 端口与 Android 设备上的服务进行通信。

3. 使用 Frida 连接到服务

frida -H 127.0.0.1:6666 wuaipojie -l hook.js

  • frida: 这是 Frida 的命令行工具,用于与 Frida Server 进行交互。
  • H 127.0.0.1:6666: 这个参数指定了 Frida 连接的主机和端口。127.0.0.1 是本地回环地址,表示连接到本地计算机的 6666 端口。
  • wuaipojie: 这是要注入的目标应用程序的包名。在这个例子中,wuaipojie 可能是一个特定的 Android 应用程序。
  • l hook.js: 这个参数指定了要加载的脚本文件。hook.js 是一个 JavaScript 文件,通常包含 Frida 的 hook 代码,用于拦截和修改目标应用程序的行为。

总结

这段代码的整体流程是:

  1. 启动一个服务(fs1),监听 0.0.0.0:6666
  2. 使用 adb 设置端口转发,使得本地计算机的 6666 端口可以与 Android 设备的 6666 端口通信。
  3. 使用 Frida 连接到这个服务,指定要注入的目标应用程序(wuaipojie)和要加载的脚本(hook.js)。

3.spawn启动过双进程保护


frida -U -f 进程名 -l hook.js

PS:学会看注入报错的日志,比如说当app主动附加自身进程时,这时候再注入就会提示run frida as root(以spawn的方式启动进程即可)

在 Android 应用程序中,双进程保护是一种常见的安全措施,旨在防止应用程序被调试或逆向工程。它通常通过在应用程序中创建多个进程来实现,只有主进程可以执行特定的操作,而其他进程则会监控主进程的状态。如果主进程被调试或被检测到异常行为,其他进程可能会终止主进程或采取其他保护措施。

使用 Frida 处理双进程保护

当使用 Frida 进行动态分析时,可能会遇到双进程保护机制。以下是一些常见的处理方法和技巧:

1. 使用 spawn 启动应用程序

在 Frida 中,您可以使用 spawn 命令启动应用程序并附加到它。使用 spawn 启动应用程序时,可以在应用程序启动时注入代码,这样可以避免某些保护机制的触发。

frida -U -f com.example.app -l hook.js --no-pause

  • U: 表示连接到 USB 设备。
  • f com.example.app: 指定要启动的应用程序包名。
  • l hook.js: 指定要加载的 JavaScript 脚本。
  • -no-pause: 启动后不暂停应用程序,允许它继续运行。

2. 处理多进程

如果应用程序使用了多进程,可能需要在 Frida 脚本中处理这些进程。可以使用 Process.enumerateProcesses() 方法来列出所有进程,并根据需要附加到特定进程。

Process.enumerateProcesses({
    onMatch: function(process) {
        console.log('Process: ' + process.name + ' PID: ' + process.pid);
    },
    onComplete: function() {
        console.log('Enumeration complete.');
    }
});

3. 监控进程状态

可以在 Frida 脚本中监控进程的状态,以便在检测到主进程被调试或异常行为时采取措施。例如,可以在脚本中添加逻辑来检测是否有其他进程正在监控主进程。

4. 使用 attach 代替 spawn

如果 spawn 方法无法绕过双进程保护,可以尝试使用 attach 方法附加到正在运行的进程。请注意,这可能会触发保护机制,因此需要谨慎使用。

frida -U -n com.example.app -l hook.js

  • n com.example.app: 指定要附加的正在运行的应用程序的名称。

5. 其他技巧

  • 修改应用程序代码: 如果有权限,可以尝试修改应用程序的代码以禁用双进程保护。
  • 使用 Frida 插件: 有些 Frida 插件专门用于处理双进程保护,可以搜索并使用这些插件。
  • 调试和测试: 在不同的环境中测试您的 Frida 脚本,以确保它们能够有效地绕过双进程保护。

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);
                }
            }
        }
    );
}

用于监控 Android 应用程序中动态库的加载情况。具体来说,它通过拦截 android_dlopen_ext 函数来检测应用程序加载的共享库(.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);
                }
            }
        }
    );
}

1. hook_dlopen 函数

  • 这是一个定义的函数,名为 hook_dlopen,用于设置拦截器。

2. Interceptor.attach

  • Interceptor.attach 是 Frida 提供的一个方法,用于拦截指定函数的调用。在这里,它用于拦截 android_dlopen_ext 函数。

3. Module.findExportByName

  • Module.findExportByName(null, "android_dlopen_ext") 用于查找当前进程中名为 android_dlopen_ext 的导出函数。第一个参数为 null 表示在当前进程中查找。

4. onEnter 回调

  • onEnter 是一个回调函数,当拦截的函数被调用时会执行该函数。在这里,args 是传递给 android_dlopen_ext 函数的参数。

5. 获取路径指针

var pathptr = args[0];

  • args[0] 是传递给 android_dlopen_ext 的第一个参数,通常是要加载的库的路径指针。

6. 检查路径指针

if (pathptr !== undefined && pathptr != null) {

  • 这行代码检查 pathptr 是否已定义且不为 null,以确保后续操作的安全性。

7. 读取字符串

var path = ptr(pathptr).readCString();

  • ptr(pathptr).readCString() 将路径指针转换为一个字符串,读取指针指向的 C 字符串。这是要加载的共享库的路径。

8. 输出加载的库路径

console.log("load " + path);

  • 这行代码将加载的库路径输出到控制台,方便开发者或分析人员查看。

使用方法

要使用这个脚本,可以将其保存为一个 JavaScript 文件(例如 hook.js),然后通过 Frida 运行它。以下是一个示例命令:

frida -U -f com.example.app -l hook.js --no-pause

  • U: 连接到 USB 设备。
  • f com.example.app: 启动并附加到指定的应用程序。
  • l hook.js: 加载并执行指定的脚本。
  • -no-pause: 启动后不暂停应用程序。

5.随着firda的版本迭代,以前诸多检测点以失效

(1.)例如检测D-Bus

D-Bus是一种进程间通信(IPC)和远程过程调用(RPC)机制,最初是为Linux开发的,目的是用一个统一的协议替代现有的和竞争的IPC解决方案。

 复制代码 隐藏代码
bool check_dbus() {
    // 定义一个socket地址结构体变量sa
    struct sockaddr_in sa;
    // 创建一个socket文件描述符
    int sock;
    // 定义一个字符数组res,用于存储接收到的数据
    char res[7];

    // 循环遍历所有可能的端口号,从0到65535
    for(int i = 0; i <= 65535; i++) {
        // 创建一个新的socket连接
        sock = socket(AF_INET, SOCK_STREAM, 0);
        // 设置socket地址结构体的端口号
        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);
            // 初始化res数组,清零
            memset(res, 0, 7);
            // 向socket发送一个空字节
            send(sock, "\x00", 1, 0); // 注意这里的NULL被替换为0
            // 发送AUTH请求
            send(sock, "AUTH\r\n", 6, 0);
            // 等待100微秒
            usleep(100);
            // 尝试接收响应
            if (recv(sock, res, 6, MSG_DONTWAIT) != -1) {
                // 如果接收到响应,检查响应内容是否为"REJECT"
                if (strcmp(res, "REJECT") == 0) {
                    // 如果是,关闭socket并返回true,表示检测到了Frida服务器
                    close(sock);
                    return true; // Frida server detected
                }
            }
        }
        // 如果当前端口连接失败或没有检测到Frida服务器,关闭socket
        close(sock);
    }
    // 如果遍历完所有端口都没有检测到Frida服务器,返回false
    return false; // No Frida server detected
}

(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,所以这里在旧版也会被当做一个检测点,而新版已不再释放

二:检测map

在 Android 应用程序中,检测内存映射(memory mapping)可以帮助识别是否存在 Frida 或其他调试工具的特征。内存映射通常涉及到使用 mmap 系统调用来将文件或设备映射到内存中。

1. 检查 /proc/self/maps

在 Android 中,可以通过读取 /proc/self/maps 文件来获取当前进程的内存映射信息。该文件包含了进程的内存区域、权限、偏移量、设备和映射的文件名等信息。

示例代码

以下是一个示例代码,展示如何读取 /proc/self/maps 文件并检查是否存在与 Frida 相关的映射:

import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class MemoryMapChecker {
    public static void checkMemoryMaps() {
        String line;
        try (BufferedReader reader = new BufferedReader(new FileReader("/proc/self/maps"))) {
            while ((line = reader.readLine()) != null) {
                // 检查是否有与 Frida 相关的映射
                if (line.contains("frida") || line.contains("frida-agent")) {
                    System.out.println("Frida detected in memory maps: " + line);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        checkMemoryMaps();
    }
}

2. 检查特定的内存区域

除了检查 /proc/self/maps,还可以通过 JNI 或 NDK 直接访问内存区域,检查是否有特定的内存地址被映射。以下是一个使用 C/C++ 的示例:

C/C++ 示例代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void check_memory_maps() {
    FILE *fp = fopen("/proc/self/maps", "r");
    if (fp == NULL) {
        perror("fopen");
        return;
    }

    char line[256];
    while (fgets(line, sizeof(line), fp)) {
        // 检查是否有与 Frida 相关的映射
        if (strstr(line, "frida") || strstr(line, "frida-agent")) {
            printf("Frida detected in memory maps: %s", line);
        }
    }

    fclose(fp);
}

int main() {
    check_memory_maps();
    return 0;
}

3. 监控内存映射的变化

还可以在应用程序运行时监控内存映射的变化,以检测是否有新的映射被添加。这可以通过定期读取 /proc/self/maps 文件并比较内容来实现。

4. 其他检测方法

  • 使用 ptrace: 检查是否有调试器附加到进程。Frida 通常会使用 ptrace 来注入代码。
  • 检查动态库: 检查加载的动态库,查看是否有 Frida 相关的库被加载。
  • 监控系统调用: 通过拦截系统调用(如 mmap)来检测是否有异常的内存映射行为。

5.对应的解决策略


adb shell ps | findstr com.zj.wuaipojie
cat /proc/12186/maps|grep frida

!https://pic.rmb.bdstatic.com/bjh/240506/8d434c16d1a2af86d824723e4525de9e8343.png

字段描述
u0_a504用户ID和应用ID:在Android系统中,u0代表系统用户(user 0),而a504是该应用在用户0下的唯一标识符。
28082PID(进程ID):该进程在操作系统中的标识符。
1935PPID(父进程ID):该进程的父进程的PID。
6511212虚拟内存:进程使用的虚拟内存大小,通常以字节为单位。
125728共享内存:进程使用的共享内存大小,同样以字节为单位。
0CPU时间/线程数:这通常表示进程的CPU时间或者是线程数,具体含义取决于ps命令的输出格式。
S状态:其中S代表睡眠状态(Sleeping),即进程没有在执行,而是在等待某些事件或资源。

/proc/self/maps 是一个特殊的文件,它包含了当前进程的内存映射信息。当你打开这个文件时,它会显示一个列表,其中包含了进程中每个内存区域的详细信息。这些信息通常包括:

  • 起始地址(Start Address)
  • 结束地址(End Address)
  • 权限(如可读、可写、可执行)
  • 共享/私有标志(Shared or Private)
  • 关联的文件或设备(如果内存区域是文件映射的)
  • 内存区域的偏移量
  • 内存区域的类型(如匿名映射、文件映射、设备映射等)当注入frida后,在maps文件中就会存在 frida-agent-64.sofrida-agent-32.so 等文件。

bool check_maps() {
    // 定义一个足够大的字符数组line,用于存储读取的行
    char line[512];
    // 打开当前进程的内存映射文件/proc/self/maps进行读取
    FILE* fp = fopen("/proc/self/maps", "r");
    if (fp) {
        // 如果文件成功打开,循环读取每一行
        while (fgets(line, sizeof(line), fp)) {
            // 使用strstr函数检查当前行是否包含"frida"字符串
            if (strstr(line, "frida") || strstr(line, "gadget")) {
                // 如果找到了"frida",关闭文件并返回true,表示检测到了恶意库
                fclose(fp);
                return true; // Evil library is loaded.
            }
        }
        // 遍历完文件后,关闭文件
        fclose(fp);
    } else {
        // 如果无法打开文件,记录错误。这可能意味着系统状态异常
        // 注意:这里的代码没有处理错误,只是注释说明了可能的情况
    }
    // 如果没有在内存映射文件中找到"frida",返回false,表示没有检测到恶意库
    return false; // No evil library detected.
}

方法1

anti脚本

 复制代码 隐藏代码
// 定义一个函数anti_maps,用于阻止特定字符串的搜索匹配,避免检测到敏感内容如"Frida"或"REJECT"
function anti_maps() {
    // 查找libc.so库中strstr函数的地址,strstr用于查找字符串中首次出现指定字符序列的位置
    var pt_strstr = Module.findExportByName("libc.so", 'strstr');
    // 查找libc.so库中strcmp函数的地址,strcmp用于比较两个字符串
    var pt_strcmp = Module.findExportByName("libc.so", 'strcmp');
    // 使用Interceptor模块附加到strstr函数上,拦截并修改其行为
    Interceptor.attach(pt_strstr, {
        // 在strstr函数调用前执行的回调
        onEnter: function (args) {
            // 读取strstr的第一个参数(源字符串)和第二个参数(要查找的子字符串)
            var str1 = args[0].readCString();
            var str2 = args[1].readCString();
            // 检查子字符串是否包含"REJECT"或"frida",如果包含则设置hook标志为true
            if (str2.indexOf("REJECT") !== -1  || str2.indexOf("frida") !== -1) {
                this.hook = true;
            }
        },
        // 在strstr函数调用后执行的回调
        onLeave: function (retval) {
            // 如果之前设置了hook标志,则将strstr的结果替换为0(表示未找到),从而隐藏敏感信息
            if (this.hook) {
                retval.replace(0);
            }
        }
    });

    // 对strcmp函数做类似的处理,防止通过字符串比较检测敏感信息
    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) {
                // strcmp返回值为0表示两个字符串相等,这里同样替换为0以避免匹配成功
                retval.replace(0);
            }
        }
    });
}

方法2

重定向maps

 复制代码 隐藏代码
// 定义一个函数,用于重定向并修改maps文件内容,以隐藏特定的库和路径信息
function mapsRedirect() {
    // 定义伪造的maps文件路径
    var FakeMaps = "/data/data/com.zj.wuaipojie/maps";
    // 获取libc.so库中'open'函数的地址
    const openPtr = Module.getExportByName('libc.so', 'open');
    // 根据地址创建一个新的NativeFunction对象,表示原生的'open'函数
    const open = new NativeFunction(openPtr, 'int', ['pointer', 'int']);
    // 查找并获取libc.so库中'read'函数的地址
    var readPtr = Module.findExportByName("libc.so", "read");
    // 创建新的NativeFunction对象表示原生的'read'函数
    var read = new NativeFunction(readPtr, 'int', ['int', 'pointer', "int"]);
    // 分配512字节的内存空间,用于临时存储从maps文件读取的内容
    var MapsBuffer = Memory.alloc(512);
    // 创建一个伪造的maps文件,用于写入修改后的内容,模式为"w"(写入)
    var MapsFile = new File(FakeMaps, "w");
    // 使用Interceptor替换原有的'open'函数,注入自定义逻辑
    Interceptor.replace(openPtr, new NativeCallback(function(pathname, flag) {
        // 调用原始的'open'函数,并获取文件描述符(FD)
        var FD = open(pathname, flag);
        // 读取并打印尝试打开的文件路径
        var ch = pathname.readCString();
        if (ch.indexOf("/proc/") >= 0 && ch.indexOf("maps") >= 0) {
            console.log("open : ", pathname.readCString());
            // 循环读取maps内容,并写入伪造的maps文件中,同时进行字符串替换以隐藏特定信息
            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");
                // 将修改后的内容写入伪造的maps文件
                MapsFile.write(MBuffer);
            }
            // 为返回伪造maps文件的打开操作,分配UTF8编码的文件名字符串
            var filename = Memory.allocUtf8String(FakeMaps);
            // 返回打开伪造maps文件的文件描述符
            return open(filename, flag);
        }
        // 如果不是目标maps文件,则直接返回原open调用的结果
        return FD;
    }, 'int', ['pointer', 'int']));
}

方法3

用eBPF来hook系统调用并修改参数实现目的,使用bpf_probe_write_user向用户态函数地址写内容直接修改参数

三:检测status(线程名)

在 Android 应用程序中,检测线程状态和线程名称可以帮助识别潜在的调试或逆向工程活动。展示一下如何检测线程状态和名称。

1. 获取当前线程信息

在 Java 中,可以使用 Thread 类来获取当前线程的信息,包括线程名称和状态。以下是一个示例代码,展示如何获取当前线程的名称和状态:

public class ThreadStatusChecker {
    public static void checkCurrentThreadStatus() {
        Thread currentThread = Thread.currentThread();
        String threadName = currentThread.getName();
        Thread.State threadState = currentThread.getState();

        System.out.println("Current Thread Name: " + threadName);
        System.out.println("Current Thread State: " + threadState);
    }

    public static void main(String[] args) {
        checkCurrentThreadStatus();
    }
}

2. 获取所有线程信息

如果想要获取所有活动线程的信息,可以使用 Thread.getAllStackTraces() 方法。以下是一个示例代码,展示如何列出所有活动线程的名称和状态:

import java.util.Map;

public class AllThreadsStatusChecker {
    public static void checkAllThreadsStatus() {
        Map<Thread, StackTraceElement[]> allThreads = Thread.getAllStackTraces();

        for (Thread thread : allThreads.keySet()) {
            String threadName = thread.getName();
            Thread.State threadState = thread.getState();
            System.out.println("Thread Name: " + threadName + ", State: " + threadState);
        }
    }

    public static void main(String[] args) {
        checkAllThreadsStatus();
    }
}

3. 检测特定线程

如果想要检测特定名称的线程,可以在获取所有线程信息后进行筛选。例如,可以检查是否存在名为 “Frida” 的线程:

import java.util.Map;

public class SpecificThreadChecker {
    public static void checkForSpecificThread(String targetThreadName) {
        Map<Thread, StackTraceElement[]> allThreads = Thread.getAllStackTraces();
        boolean found = false;

        for (Thread thread : allThreads.keySet()) {
            if (thread.getName().equals(targetThreadName)) {
                found = true;
                System.out.println("Detected specific thread: " + thread.getName() + ", State: " + thread.getState());
                break;
            }
        }

        if (!found) {
            System.out.println("No thread named " + targetThreadName + " found.");
        }
    }

    public static void main(String[] args) {
        checkForSpecificThread("Frida"); // 检查是否存在名为 "Frida" 的线程
    }
}

4. 监控线程状态变化

如果需要监控线程状态的变化,可以使用 ThreadsetDaemon 方法将线程设置为守护线程,并在运行时定期检查其状态。以下是一个简单的示例:

public class ThreadMonitor implements Runnable {
    private final String threadName;

    public ThreadMonitor(String threadName) {
        this.threadName = threadName;
    }

    @Override
    public void run() {
        while (true) {
            try {
                Thread.sleep(1000); // 每秒检查一次
                Thread[] threads = new Thread[Thread.activeCount()];
                Thread.enumerate(threads);
                for (Thread thread : threads) {
                    if (thread != null && thread.getName().equals(threadName)) {
                        System.out.println("Monitoring thread: " + thread.getName() + ", State: " + thread.getState());
                    }
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
    }

    public static void main(String[] args) {
        Thread monitorThread = new Thread(new ThreadMonitor("Frida"));
        monitorThread.start();
    }
}

5.对应的解决策列


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
  1. gmain:Frida 使用 Glib 库,其中的主事件循环被称为 GMainLoop。在 Frida 中,gmain 表示 GMainLoop 的线程。
  2. gdbus:GDBus 是 Glib 提供的一个用于 D-Bus 通信的库。在 Frida 中,gdbus 表示 GDBus 相关的线程。
  3. gum-js-loop:Gum 是 Frida 的运行时引擎,用于执行注入的 JavaScript 代码。gum-js-loop 表示 Gum 引擎执行 JavaScript 代码的线程。
  4. pool-frida:Frida 中的某些功能可能会使用线程池来处理任务,pool-frida 表示 Frida 中的线程池。
  5. 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) {
                //console.log("strcmp-->", str1, str2);
                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) {
                //console.log("strcmp-->", str1, str2);
                this.hook = true;
            }
        }, onLeave: function (retval) {
            if (this.hook) {
                retval.replace(0);
            }
        }
    })

}

6.检测inlinehook

通过Frida查看一个函数hook之前和之后的机器码,以此来判断是否被Frida的inlinehook注入。

!https://pic.rmb.bdstatic.com/bjh/240415/25f2a3c1d478357bb60008780a9539bf6487.png

下面的方案以内存中字节和本地对应的字节进行比较,如果不一致,那么可以认为内存中的字节被修改了,即被inlinehook了


#include <jni.h>#include <string>#include <dlfcn.h>#include "dlfcn/local_dlfcn.h"bool check_inlinehook() {
    // 根据系统架构选择对应的libc.so库路径
    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;
    // 指定要查找的符号名,这里是"open"函数
    const char *sym_name = "open";

    // 使用local_dlopen函数打开指定的共享库,并获取操作句柄
    struct local_dlfcn_handle *handle = static_cast<local_dlfcn_handle *>(local_dlopen(lib_path));
    if (!handle) {
        return JNI_FALSE; // 如果无法打开共享库,返回false
    }

    // 获取"open"函数在libc.so中的偏移量
    off_t offset = local_dlsym(handle, sym_name);

    // 关闭handle,因为我们接下来使用标准的dlopen/dlsy来获取函数地址
    local_dlclose(handle);

    // 打开libc.so文件,准备读取数据
    FILE *fp = fopen(lib_path, "rb");
    if (!fp) {
        return JNI_FALSE; // 如果无法打开文件,返回false
    }

    // 定义一个缓冲区,用于存储读取的文件内容
    char file_bytes[CMP_COUNT] = {0};
    // 读取指定偏移量处的CMP_COUNT个字节
    fseek(fp, offset, SEEK_SET);
    fread(file_bytes, 1, CMP_COUNT, fp);
    fclose(fp);

    // 使用dlopen函数打开libc.so共享库,并获取操作句柄
    void *dl_handle = dlopen(lib_path, RTLD_NOW);
    if (!dl_handle) {
        return JNI_FALSE; // 如果无法打开共享库,返回false
    }

    // 使用dlsym函数获取"open"函数的地址
    void *sym = dlsym(dl_handle, sym_name);
    if (!sym) {
        dlclose(dl_handle);
        return JNI_FALSE; // 如果无法找到符号,返回false
    }

    // 比较原libc.so中的"open"函数内容与通过dlsym获取的"open"函数内容是否一致
    int is_hook = memcmp(file_bytes, sym, CMP_COUNT) != 0;

    // 关闭dlopen打开的共享库句柄
    dlclose(dl_handle);

    // 返回比较结果,如果函数被hook则返回JNI_TRUE,否则返回JNI_FALSE
    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(){
    //hook反调试
    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];   // 保存 buffer 参数
            this.size = args[1];     // 保存 size 参数
            this.count = args[2];    // 保存 count 参数
            this.stream = args[3];   // 保存 FILE* 参数
        },
        onLeave: function (retval) {
            // 这里可以修改 buffer 的内容,假设我们知道何时 fread 被用于敏感操作
            console.log(this.count.toInt32());
            if (this.count.toInt32() == 8) {
                // 模拟 fread 读取了预期数据,伪造返回值
                Memory.writeByteArray(this.buffer, [0x50, 0x00, 0x00, 0x58, 0x00, 0x02, 0x1f, 0xd6]);
                retval.replace(8); // 填充前8字节
                console.log(hexdump(this.buffer));
            }
        }
    });
    } else {
        console.log("Error: memcmp function not found in libc.so");
    }
}

7.刷入魔改的frida-server端

https://github.com/hzzheyang/strongR-frida-android
注意和自己的版本号一致,建议一直用最新版的frida

最后感谢正己大佬的总结,以此为基础添加些补充。
参考文章:

《安卓逆向这档事》十八、表哥,你也不想你的Frida被检测吧!(上) - 吾爱破解 - 52pojie.cn


http://www.kler.cn/a/378917.html

相关文章:

  • 代码随想录第十五天| 110.平衡二叉树 、 257. 二叉树的所有路径 、404.左叶子之和、222.完全二叉树的节点个数
  • 【C++】继承的理解
  • clickhouse运维篇(三):生产环境一键生成配置并快速部署ck集群
  • MongoDB 8.0.3版本安装教程
  • 大学适合学C语言还是Python?
  • ELK之路第三步——日志收集筛选logstash和filebeat
  • VR游戏:多人社交将是VR的下一个风口
  • SpringMvc请求
  • Spring Boot Admin应用
  • 照明灯十大知名品牌有哪些?2024灯具十大公认品牌排行榜出炉!
  • 洛阳建筑设计资质电子化申报操作流程
  • 怎麼解除IP阻止和封禁?
  • 2-139 基于matlab的弹道轨迹仿真
  • 低压补偿控制器维修措施
  • ES6中数组新增了哪些扩展?
  • Java项目实战II基于Spring Boot的智能家居系统(开发文档+数据库+源码)
  • 【jvm】为什么Xms和Xmx的值通常设置为相同的?
  • 利用Matlab工具生成滤波器
  • 在Springboot中更好的打印日志
  • 基于STM32的数控DC-DC电源系统设计
  • 【MyBatis源码】SqlSession实例创建过程
  • 《Python修炼秘籍》01踏上编程之旅
  • 大零售时代下融合发展的新路径:定制开发技术的应用与思考
  • IT 运维:流量回溯与视频质量监控的秘籍
  • vue3项目中实现el-table分批渲染表格
  • scrapy服务器重试机制失效问题