基础补充
什么是 Anti-Frida 保护?
Anti-Frida保护是指在移动应用或程序中采用的一种安全技术或防护机制,旨在防止或干扰Frida等动态分析工具的注入与使用。
Anti-Frida保护常见技术 有哪些?
- 检测
frida-agent.so
的注入 :
Frida通过注入一个名为frida-agent.so
的共享库到目标进程中。这是Frida工作原理的一部分,任何应用如果加载了这个库,就意味着它可能正在被Frida控制。Anti-Frida保护可以检测到这个库的加载,并通过终止进程、使其崩溃或显示警告来防止分析
- 检查
/proc/[pid]/maps
文件
如前所述,Frida注入的frida-agent.so
库会出现在进程的/proc/[pid]/maps
文件中。防护机制可能通过伪造或监控这个文件,阻止Frida的注入信息显现。或者,应用程序可以定期检查这个文件,以检测是否有Frida的注入
- 检测Frida的特定行为
Frida通过其API来修改应用程序的内存、执行代码等。某些Anti-Frida保护机制会分析进程的行为,监控是否存在类似Frida的异常行为(例如,动态修改内存、重定向函数调用等),从而判断是否有Frida工具的干预
- 反调试技术
反调试技术可以通过识别调试器或动态分析工具(如Frida)来干扰其运行。常见的反调试技术包括检查ptrace
系统调用、检测特定的调试标记或使用某些系统调用(如getppid
、sysctl
)来检查是否存在调试器
什么是maps?
在Linux系统中,/proc/[pid]/maps
文件记录了一个进程的内存映射情况。它列出了进程如何将虚拟内存地址空间映射到实际的物理内存地址、文件或共享内存区域。每一行都表示一个内存区域的映射,包括区域的起始和结束地址、权限(如读写执行权限)、偏移量、设备ID、inode号、以及该区域关联的文件名或共享内存名。
定位检测逻辑
要判断该壳是如何对抗frida的就要先定位他检测frida的代码逻辑在哪个so里
安卓中通常用来加载so的函数有 lopen 和 android_dlopen_ext ,那么可以使用frida-trace给着两个函数进行挂钩 先分析dlopen
frida-trace -U -f appname -i dlopen
默认只会显示调用了dlopen不会显示进行加载的so在哪个位置,dlopen的第一个参数就是所要加载的so的路径 可以修改dlopen.js的脚步即可
onEnter(log,args,state){log('dlopen():'+args[0].readCString());
}
结果都是系统的so而不是这个app的so
再去hook另外一个函数android_dlopen_ext 看看 这里加载的so就是app的so了 甚至可以看到app的名字
可以看到当load到libDexHelper.so的时候,frida被杀掉了,所以我们初步可以判定做检测的位置在 第三个so中,检测到了frida
对抗anti-frida
maps检测
通常检测frida的地方会检测“frida”这个字符串,那么程序代码很有可能就会用到比较字符串的函数 如strstr和strcmp等函数 ,那么就尝试hook这种函数来判断对抗逻辑
通过这些字符串的特征,可以知道它们来自maps , 而Frida的一大特征就是在注入到app中后,app的 maps中会有frida-agent.so的内存分布。所以这里我们可以通过伪造maps来绕过这里的检测
什么是伪造maps?
伪造maps指的是通过编程手段修改或模拟/proc/[pid]/maps
文件中的内容,使得其呈现出不同的内存映射情况,从而绕过某些安全检测或者防护机制。在本段话中,伪造maps的目的是为了绕过检测Frida注入的迹象
如何绕过maps检测?
第一种方式 通过hookstrstr
和strcmp
来防止它们返回与"REJECT"
或"frida"
相关的匹配结果。
// 定义一个函数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标志为trueif (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);}}});
}
第二种方式去hook open函数 如果open的路径包含"/proc/"
和"maps"
,这表明当前正在尝试打开/proc/[pid]/maps
文件 也就是在检查maps,此时我们就创建一个假的maps,再去open一个假的maps返回结果,当然事先肯定会对这个假的maps做处理来隐藏frida的特征
// 定义一个函数,用于重定向并修改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']));
}
task检测
扫描task目录下所有/task/pid/status中的Name字段寻找是否存在frida注入的特征,具体线程名为 gmain 、 gdbus 和 gum-js- loop ,一般情况下这三个线程在第11--13的位置,此外在frida运行脚本过 程中,还会存在一个Name字段为 pool-frida 的线程。
可以通过以上伪造maps的方法 去伪造task
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);}}})}
inlinehook检测
检测原理就是通过对比 一个api再hook之前与hook之后的字节码,来判断是否被frida给inlinehook了
检查的时候 程序会用到fread函数 那么我们就hook他 给他返回没有hook的函数字节码
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");}
}