首页
社区
课程
招聘
5
[原创]《安卓逆向这档事》第十八课、表哥,你也不想你的Frida被检测吧!(上)
发表于: 2024-7-18 16:47 6229

[原创]《安卓逆向这档事》第十八课、表哥,你也不想你的Frida被检测吧!(上)

2024-7-18 16:47
6229

1.了解常见frida检测

1.教程Demo(更新)
2.jadx-gui
3.VS Code
4.jeb

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

3.spawn启动过双进程保护

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

5.随着firda的版本迭代,以前诸多检测点以失效
(1.)例如检测D-Bus
D-Bus是一种进程间通信(IPC)和远程过程调用(RPC)机制,最初是为Linux开发的,目的是用一个统一的协议替代现有的和竞争的IPC解决方案。

(2)检测fd
/proc/pid/fd 目录的作用在于提供了一种方便的方式来查看进程的文件描述符信息,这对于调试和监控进程非常有用。通过查看文件描述符信息,可以了解进程打开了哪些文件、网络连接等,帮助开发者和系统管理员进行问题排查和分析工作。

(3)检测文件
众所周知frida我们一般都会放在data/local/tmp目录下,旧版fridaserver端运行时都会释放到re.frida.server,所以这里在旧版也会被当做一个检测点,而新版已不再释放

图片

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

anti脚本

重定向maps

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

anti脚本

通过Frida查看一个函数hook之前和之后的机器码,以此来判断是否被Frida的inlinehook注入。
图片
下面的方案以内存中字节和本地对应的字节进行比较,如果不一致,那么可以认为内存中的字节被修改了,即被inlinehook了

获取hook前字节码的脚本

anti脚本

注意版本!!!
strongR-frida-android

待更新

百度云
阿里云
哔哩哔哩
教程开源地址
PS:解压密码都是52pj,阿里云由于不能分享压缩包,所以下载exe文件,双击自解压

frida常用检测点及其原理--一把梭方案
frida 检测
从inlinehook角度检测frida
Frida检测手段探究
Lsposed 技术原理探讨 && 基本安装使用
strongR-frida-android
[原创]绕过bilibili frida反调试
基础反检测 frida-server
关于frida检测的一个新思路
读取本地so文件的符号的偏移

./fs1 -l 0.0.0.0:6666
adb forward tcp:6666 tcp:6666
frida -H 127.0.0.1:6666 wuaipojie -l hook.js
./fs1 -l 0.0.0.0:6666
adb forward tcp:6666 tcp:6666
frida -H 127.0.0.1:6666 wuaipojie -l hook.js
frida -U -f 进程名 -l hook.js
frida -U -f 进程名 -l hook.js
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);
                }
            }
        }
    );
}
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);
                }
            }
        }
    );
}
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
}
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
}
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;
}
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;
}
adb shell ps | findstr com.zj.wuaipojie
cat /proc/12186/maps|grep frida
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),即进程没有在执行,而是在等待某些事件或资源。
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.
}
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.
}
// 定义一个函数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);
            }
        }
    });
}
// 定义一个函数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);
            }
        }
    });
}
// 定义一个函数,用于重定向并修改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']));
}
// 定义一个函数,用于重定向并修改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']));

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

收藏
免费 5
支持
分享
赞赏记录
参与人
雪币
留言
时间
yiyeqiuyu
你的帖子非常有用,感谢分享!
2025-3-20 12:17
mb_zfqvurgb
为你点赞!
2024-10-24 14:10
mb_lopfaygp
为你点赞!
2024-10-19 21:19
gladly
为你点赞~
2024-9-6 11:28
frankyxu
+5
谢谢你的细致分析,受益匪浅!
2024-7-18 17:17
最新回复 (3)
雪    币: 338
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2
4.借助脚本定位检测frida的so   这里hookdlopen并不能定位到检测frida的so文件
2024-8-19 21:29
0
雪    币: 4109
活跃值: (2991)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
3
mb_hdmrryzf 4.借助脚本定位检测frida的so 这里hookdlopen并不能定位到检测frida的so文件
还有的情况要hookdlsym
2024-8-20 10:05
0
雪    币: 225
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
每次看都受益匪浅
2024-10-23 14:48
0
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册