Frida-Practice
样本链接
https://github.com/OWASP/owasp-mastg/tree/master/Crackmes
Uncrackable3
校验so和dex
首先创建一个HashMap <key,value>,<key,value>是Entry的一个实体,获取资源值对应的字符串后转为Long类型,然后使用put方法存入HashMap中,获得到对应的文件后进行CRC校验
对so文件和dex文件进行CRC校验防止修改
遍历HashMap
调试检测
创建异步任务来检测调试
doInBackground方法是自定义的线程任务,onPostExecute在线程任务执行后进行,参数接收线程任务执行结果,参数为对UI控件进行设置,execute()用户手动调用异步任务
Root
Root环境检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var clazz=Java.use("sg.vantagepoint.util.RootDetection" ) clazz.checkRoot1.implementation=function (args ) { return false ; } clazz.checkRoot2.implementation=function (args ) { return false ; } clazz.checkRoot3.implementation=function (args ) { return false ; } var clazz11=Java.use("sg.vantagepoint.util.IntegrityCheck" ); clazz11.isDebuggable.implementation=function (args ) { return false ; }
Frida检测
使用Interceptor.replace替换循环检测Frida的函数
第一个参数是要替换函数的地址,第二个参数是一个NativePointer类型的函数,一般由new NativeCallback创建,NativeCallback包括(函数的实现,函数的返回类型,函数的参数类型列表)。返回类型最好保持和原函数一致。同时我们也可以使用NativeFunction来主动调用原函数或是so层函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 var addr=Module.findExportByName("libc.so" ,"strstr" ); console .log(addr); Interceptor.attach(addr,{ onEnter :function (args ) { }, onLeave :function (retval ) { retval.replace(0 ); return retval; } }) var pt_create_func=Module.findExportByName("libc.so" ,"pthread_create" ); console .log("pt_create_func_addr:" ,pt_create_func); var detect_frida_addr=null ; Interceptor.attach(pt_create_func,{ onEnter :function (args ) { if (detect_frida_addr==null ){ var base_addr=Module.findBaseAddress("libfoo.so" ); detect_frida_addr=base_addr.add(0x30D0 ); console .log("Ptread_Addr:" ,base_addr); Interceptor.replace(this .context.x2,new NativeCallback(function (a1 ) { console .log("Replace Success" ); return ; },'void' ,[])); } }, onLeave :function (retval ) { } })
如果想要在替换的函数中使用原函数的参数,需要注意参数类型 ,比如传入的env、jclass、data是指针类型,那么参数列表中就要声明为"pointer"
Hook 函数用于获取参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 var MainActivity = Java.use("sg.vantagepoint.uncrackable3.MainActivity" ); MainActivity.$init.implementation = function ( ) { this .$init(); attachToSecretGenerator(); }; var addr2=Module.findExportByName("libfoo.so" ,"Java_sg_vantagepoint_uncrackable3_CodeCheck_bar" ); Interceptor.attach(addr2,{ onEnter :function (args ) { console .log((args[2 ].readByteArray(32 ))); var addr1=Module.findBaseAddress("libfoo.so" ); addr1=addr1.add(0x15038 ); console .log("=====xor_key=====" ); console .log(hexdump(addr1)); }, onLeave :function (retval ) { } }) }) } function attachToSecretGenerator ( ) { Interceptor.attach(Module.findBaseAddress('libfoo.so' ).add(0x10E0 ), { onEnter : function (args ) { this .answerLocation = args[0 ]; }, onLeave : function (retval ) { console .log(this .answerLocation.readByteArray(32 )); } }); }
frida测试题
wp
Challenge-Six
反编译后的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 package com.dta.test.frida.activity;import android.view.View;import com.dta.test.frida.R;import com.dta.test.frida.base.BaseActivity;import com.dta.test.frida.base.Level;public class SixthActivity extends BaseActivity { @Override protected String getActivityTitle () { return "第六关" ; } @Override protected String getDescription () { return getString(R.string.sixth); } @Override public void onClick (View view) { super .onClick(view); try { Class<?> cls = Class.forName("com.dta.test.frida.activity.RegisterClass" ); if (((Boolean) cls.getDeclaredMethod("next" , new Class[0 ]).invoke(cls.newInstance(), new Object[0 ])).booleanValue()) { gotoNext(Level.Seventh); } else { failTip(); } } catch (Exception unused) { failTip(); } } }
代码中使用反射调用com.dta.test.frida.activity.RegisterClass中的next方法,然后接收其返回值
Frida为我们提供了java.registerClass来注册类到内存中 ,其格式如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var RegisterClass=Java.registerClass({ name :"com.dta.test.frida.activity.RegisterClass" , methods :{ next : { returnType : "boolean" , argumentTypes :[], implementation : function ( ) { return true } } } })
这样子就注册好了next方法,但是我们仍无法通过这一关
1 console .log(RegisterClass.$new().next());
使用上述代码创建一个类的实例后调用next方法会发现我们的类成功注册并且next方法创建成功
这里的问题出在了ClassLoader上
翻看Class.forName()函数参数列表可以知道在其参数列表中存在ClassLoader
默认为当前类的ClassLoader,在Android中默认的活动加载器为PathClassLoader ,Frida加载我们自定义类RegisterClass使用的ClassLoader 跟SixthActivity的PathClassLoader 并不是同一个ClassLoader,所以导致我们使用PathClassLoader来加载会出现ClassNotFoundException
1 2 var targetClassLoader = RegisterClass.class.getClassLoader() console .log(targetClassLoader);
使用class.getClassLoader 获取类加载器后打印可知其使用的是DexClassLoader
由于使用双亲委派机制,所以我们可以修改其父加载器使其能正确加载
安卓类加载器的继承关系图
双亲委派机制是指当加载类时首先不会选择自身进行加载,而是先交由其父加载器进行,如果父加载器加载不了再由自己进行加载,所以相当于从ClassLoader往下开始进行加载
我们可以在PathClassLoader与BaseDexClassLoader之间插入我们定义类的加载器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 var targetClassLoader = RegisterClass.class.getClassLoader() console .log(targetClassLoader); Java.enumerateClassLoaders({ onMatch : function (loader ) { try { if (loader.findClass("com.dta.test.frida.activity.SixthActivity" )){ var PathClassLoader = loader var BootClassLoader = PathClassLoader.parent.value PathClassLoader.parent.value = targetClassLoader targetClassLoader.parent.value = BootClassLoader } }catch (e){ } }, onComplete : function ( ) { console .log("Completed!" ) } })
也可以Hook Class.forName方法,指定ClassLoader为RegisterClass的加载器
Anti-Frida&Pass
Frida-Detect
**由于Frida是代码注入工具,所以Frida的模块会在进程中留下痕迹,proc是Linux中的伪文件系统,而在内存中存在/proc/pid目录存储进程的一些信息,应用程序可以访问获得内核的信息,/proc/pid/maps显示进程的内存区域映射信息 **
通过maps检测 :扫描/proc/pid/maps文件中的内存分布,寻找是否打开了/data/local/tmp路径下的so,(Frida在运 行时会先确定/data/local/tmp路径下是否有re.frida.server文件夹,若没有则创建该文件夹并存放frida-agent.so等文件),我们使用cat命令即可获得
通过task检测 :扫描task目录下所有/task/pid/status中的Name字段是否存在frida注入的特征,具体线程名为gmain、gdbus、gum-js-loop ,一般这三个线程是在第11-13行,同时也会存在Name字段为pool-frida的线程
通过fd检测 :通过readlink查看/proc/pid/fd和/proc/task/pid/fd下所有的文件,检测是否有frida相关文件
端口检测 :Frida默认的端口为27042 ,检测该TCP端口是否开放
通过D-Bus检测 : Frida是通过D-Bus协议进行通信的,所以可以遍历/proc/net/tcp文件,向每个开放的端口发送 D-Bus 的 认证消息 AUTH ,如果端口回复了 REJECT ,那么这个端口就是frida-server
Easy-AntiFrida-Pass
PortCheck
ThreadCheck
MapsCheck
TraceCheck
MemoryCheck
maps显示当前正在运行进程的地址映射或进程的内存地址空间的文件
fdCheck
总结
从上面的各种检测方式中,可以看出主要方式都是通过打开特定的文件和进程内存文件,然后使用字符串比较函数strstr,或者使用stat 判断文件是否存在,也可以进行端口检测,Frida默认端口是27042
对于端口检测,可以Hook掉connect函数的返回值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const module =Process.getModuleByName("libc.so" ); console .log(module .base); var connect=module .findExportByName("connect" ); console .log(connect); Interceptor.attach(connect,{ onEnter :function (args ) { }, onLeave :function (retval ) { console .log("Change Success!!" ); retval.replace(1 ); } })
也可以修改frida-server默认端口 ,**/data/local/tmp # ./frida-server-15.1.17-android-arm64 -l 0.0.0.0:1234,然后adb forward tcp:1234 tcp:1234,脚本运行: frida -H 127.0.0.1:1234 -f owasp.mstg.uncrackable3 -l .\Uncrackme3.js **
所以我们只需要Hook掉libc中的strstr函数 即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 const module =Process.getModuleByName("libc.so" );var strstr=module .findExportByName("strstr" ); console .log(strstr); Interceptor.attach(strstr,{ onEnter :function (args ) { }, onLeave :function (retval ) { retval.replace(0 ); } }) var stat=module .findExportByName("stat" ); console .log(stat); Interceptor.attach(stat,{ onEnter :function (args ) { }, onLeave :function (retval ) { retval.replace(1 ); } })
虽然这里的System.LoadLibrary是static属性的,但是不知道为啥没有提前加载=。=,所以需要我们手动Hook主活动的构造器
1 2 3 4 var MainActivity = Java.use("com.yimian.envcheck.MainActivity" ); MainActivity.$init.implementation = function ( ) { this .$init(); };
当然我们也可以Hook掉比较的字符串,首先先在内存中写入我们用于比较的字符串(allocUtf8String ),然后将地址在onEnter回调函数中进而修改strstr函数的参数
1 2 3 4 5 6 7 8 9 10 11 12 13 var newStr="new String" ; var newstraddr=Memory.allocUtf8String(newStr); var strcpy=module .findExportByName("strstr" ); Interceptor.attach(strcpy,{ onEnter :function (args ) { args[1 ]=newstraddr; console .log(args[1 ].readCString()); }, onLeave :function (retval ) { } })
在Memory的check中使用的是逐字符比较,我们可以Hook掉汇编指令中的比较值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 const byte1=[0x5F ,0x41 ,0x01 ,0x71 ]; const byte2=[0x7F ,0xE9 ,0x01 ,0x71 ]; const byte3=[0x7F ,0xE9 ,0x01 ,0x71 ]; const byte4=[0x3F ,0x42 ,0x01 ,0x71 ]; const byte5=[0x5F ,0x42 ,0x01 ,0x71 ]; var module_base=Module.findBaseAddress("libtestfrida.so" ); var func=module_base.add(0x001D5C ); console .log(Instruction.parse(func)); console .log(func); var func2=module_base.add(0x01E54 ); var func5=module_base.add(0x1FF8 ); var func3=module_base.add(0x01F24 ); var func4=module_base.add(0x20BC ); Memory.protect(func,8 ,'rwx' ); Memory.protect(func2,8 ,'rwx' ); Memory.protect(func3,8 ,'rwx' ); Memory.protect(func4,8 ,'rwx' ); Memory.protect(func5,8 ,'rwx' ); func.writeByteArray(byte1); func2.writeByteArray(byte2); func3.writeByteArray(byte3); func4.writeByteArray(byte4); func5.writeByteArray(byte5);
首先我们先找到想要Hook的CMP汇编地址,然后根据机器码创建byte数组,最后使用writeByteArray写入即可
以0x1DA4为例,[5F,C9,01,71]是地址中原来的数据,C9是用于比较的数,我们只需要修改其即可,最后写入内存中
可以使用Instruction.prase(addr)打印汇编代码,使用Memory.protect(addr,size,“rwx”);来设置修改内存的属性和大小
1 2 3 4 5 6 7 8 9 10 Interceptor.attach(func2,{ onEnter :function (args ) { console .log(this .context.x11); this .context.x11=0x45 ; console .log(this .context.x11); }, onLeave :function (retval ) { console .log(this .context.x11); } })
也可以使用Frida进行inlineHook,并且修改寄存器的值,这里的W11是X11的低32位
最终效果