Frida-Hook
Frida
Frida常用模块
Frida文档
Frida是一种动态插桩工具,可以插入一些代码到原生App的内存空间去动态地监视和修改其行为 ,但是持久化还是要依靠Xposed和HookZz开发框架。使用Frida导出的API和方法堆内存空间里的对象方法进行监视、修改或者替换的一段代码
我们可以直接编写JS脚本,然后命令行模式(CLI)运行,也可以使用Python进行JS脚本的注入工作(RPC模式)
Frida命令行提示
1 2 3 4 5 6 7 8 9 setTimeout ( function ( ) { Java.perform(function ( ) { console .log("hello world" ) }) } )
首先保证frida-server在手机上运行,然后使用frida-ps -U查看手机上正在运行的进程,接着在计算机的shell中执行frida -U -l testFrida.js Easy_Xor命令以attach模式注入指定应用 ,-l表示脚本路径
JS匿名函数
Frida Hook Java类
首先我们编写一个简单的APK
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); while (true ){ try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } func(50 ,30 ); } } public void func (int x,int y) { String TAG="MainActivity" ; Log.d(TAG, String.valueOf(x+y)); } }
每隔一秒就打印一次50+30的结果
我们的目标是编写Hook func()函数并打印出func()函数的参数值 ,也就是Hook到func函数之后执行我们的Hook代码,然后再调用func函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function main ( ) { console .log("Script Loaded Successfully" ) Java.perform(function ( ) { console .log("Inside Java Perform Function" ) var MainActivity=Java.use('com.flag.test1.MainActivity' ) console .log("Find Class" ) MainActivity.func.implementation=function (x,y ) { console .log("x => " ,x,"y => " ,y) var ret_value=this .func(x,y); return ret_value } }) } setImmediate(main)
下面截图可以看到参数值被成功打印
使用main函数存放Hook脚本,然后调用Frida的API函数Java.perfrom()将脚本的内容注入到Java运行库,匿名函数内容是监控和修改Java函数逻辑的主体内容,所有对App中Java层的操作都必须包裹在Java.perfrom()函数 。
匿名函数中,调用Frida的API函数Java.use(),这个函数饿参数是Hook的函数所在类的全类名路径,参数的类型是一个字符串类型。这个函数的返回值 动态 地为相应Java类获取一个JavaScript Wrapper,可以通俗地理解为一个JavaScript对象 。当获取到JavaScript对象之后,通过".“来获取我们要Hook的方法,implementation关键字表示实现MainActivity对象的func()方法 。通过”="连接一个匿名函数,参数内容和原Java内容一致,JS不需要指明参数类型,匿名函数的内容取决于想要修改这个Hook函数的逻辑 。通过this.fun()函数再次调用原函数,然后把原来的参数传递进这个func()函数,最后通过return返回
setImmediate(Frida的API函数)函数传递的参数是要被执行 的函数,比如传入main参数,表示当Frida注入App后立即执行 main()函数,而setTimeout一般用于延时注入
在Hook一个函数时,还有一个地方需要注意,那就是最好不要 修改被Hook的函数的返回值类型,否则可能会引起程序崩溃等问 题,比如直接通过调用原函数将原函数的返回值返回
我们可以在调用函数时修改参数,此时使用adb logcat 即可发现Log.d结果改变
重载方法的Hook实现
方法重载
Hook时只需要指定函数签名即可
对于Hook另一个方法
Java层主动调用
主动调用分为实例方法和类函数
类函数和实例方法通俗地讲就是静态的方法和动态的方法,类函数使用关键字static修饰,和对应类是绑定的,也就是当类的字节码文件加载到内存时,类函数的入口地址就会分配完成。而实例方法只有当类对象创建之后才分配入口地址。从而实例方法可以被类创建的对象调用
如果是类函数主动调用,直接使用Java.use()函数找到类进行调用 即可,如果是实例方法的主动调用,则需要再找到对应的实例后对方法进行调用,Java.close()函数可以在Java的堆中寻找指定类的实例
我们在APK中写一个类方法和实例方法
接下来写代码即可
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 function main ( ) { console .log("Script Run Successfully" ) Java.perform(function ( ) { console .log("Java Internal" ) var MainActivity=Java.use('com.flag.test1.MainActivity' ) console .log("Find Class" ) MainActivity.secret() Java.choose('com.flag.test1.MainActivity' ,{ onMatch :function (instance ) { console .log("instance found" ,instance) instance.secret_instance() }, onComplete :function ( ) { console .log('search Complete' ) } }) }) } setImmediate(main)
类方法主动调用
实例方法主动调用,我们也可以直接new一个实例然后进行调用实例方法
Frida Hook Native层
也是分为两种情况
一种是静态注册,一种是动态注册 ,**在JNI逆向过程中首先需要找到Java层函数在native层中对应的函数地址,然后再进行Hook。 Android系统为了快速找到JNI函数在native层的对应函数, 因而其命名规则都是固定的,并且在内存中也是不会发生变化的 **
逆向流程
使用Objection注入进程之后,使用memory list modules 查看so文件是否被注入,so文件在内存中的加载基址、文件大小以及文件目录 都打印出来了
接下来查看所有导出符号 memory list exports libfrida_so.so ,其中有类型、符号以及起始地址
我们可以找到StringFromJNI这个函数的起始地址,**在Android中每次加载模块时基地址都是变化的,最终会导致每 次加载后的函数地址也是不同的,真正可靠的是函数地址相对于模块基地址的偏移 **,使用当前函数起始地址减去模块加载地址即可
如果在cpp文件中没有声明函数为extern “C” ,导出时会进行重载,我们可以使用C++ filt工具对函数名进行还原
native层Hook基础
so层常用API
ida结构体创建和导入
ida对so层的分析
so层分析
native层导出函数
下面是Frida Hook native层函数的模板
1 2 3 4 5 6 7 8 Interceptor.attach(addr,{ onEnter (args ) { }, onLeave (ret ) { } })
实现native层Hook得API函数是Interceptor.attach()函数,他的第一个参数是要Hook得函数地址,第二个参数是callback回调,在这个函数中存在两个函数,onEnter()函数是在函数调用前产生的回调,在这个函数中可以处理函数参数的相关内容,被Hook的函数参数内容以数组的方式存储在onEnter()函数的参数args中,onLeave()函数则是在被Hook的目标函数执行完成之后执行的函数,被Hook的函数返回值用onLeave()函数中的ret变量来表示 ,简单来说就是一个处理函数参数,一个处理返回值
关键的一步就是要获取Hook函数的首地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Module.getExportByName(moduleName|null ,exportName) Module.findExportByName(moduleName|null ,exportName) Mudule.enumearteExports(); var Exports=Module.enumerateExports();for (var i=0 ;i<Exports.length;++i){ console .log(Exports[i].name+" " +Exports[i].address); } Process.enumerateModules(); hexdump(addr);
为了将返回值的内容打印出来,调用了
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 function hook_native ( ) { console .log("=====Hooking Native=====" ) var addr=Module.getExportByName("libfrida_so.so" ,"Java_com_flag_frida_1so_MainActivity_stringFromJNI" ) console .log(addr) Interceptor.attach(addr,{ onEnter : function (args ) { console .log("JniEnv pointer => " ,args[0 ]) console .log("Jobj pointer => " ,args[1 ]) }, onLeave : function (retval ) { console .log("Retutn Value => " ,Java.vm.getEnv().getStringUtfChars(retval,null ).readCString()) console .log("====================" ) } }) } function main ( ) { hook_native() } setImmediate(main)
首先使用Java.vm.getEnv()获取当前线程的JNIEnv结构,并参照JNI函数GetStringUtfChars()获取到的Java的字符串对用的C字符首地址,获取到C字符串指针后,通过readCString()这个API函数读取内存中的C字符串
为了能够保证stringFromJNI()函数的执行在完成Hook后执行, 这 里 先 在 MainActivity 类 的 onCreate() 函 数 中 加 上 一 个 对 stringFromJNI()函数的循环调用
native未导出函数
由于每次App重新运行后native函数加载的绝对地址,而Frida也为我们提供了获取基址的函数,Module.findBaseAddress(name)或者Module.getBaseAddress(name)获取对应模块的及地址,然后通过add(offset)函数传入固定的便宜来获取最后函数的绝对地址
下面是动态注册的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 jstring JNICALL stringFromJNI (JNIEnv*env,jobject MainActivity) { std::string hello="Hello from C++ Firda_so" ; return env->NewStringUTF (hello.c_str ()); }; jint JNI_OnLoad (JavaVM*vm,void *reserved) { JNIEnv* env; vm->GetEnv ((void **)&env,JNI_VERSION_1_6); JNINativeMethod methods[]={ {"stringFromJNI" ,"()Ljava/lang/String;" ,(void *)stringFromJNI}, }; env->RegisterNatives (env->FindClass ("com/flag/frida_so/MainActivity" ),methods,1 ); return JNI_VERSION_1_6; }
frida_hook_libart ,这个项目包含着对JNI函数和art相关函数的Frida_Hook脚本,这里要使用的是RegisterNatives()函数的Hook脚本
在github下载下来发现不会打印偏移,去网上找了一个可以打印offset的代码
计算offset的代码
也可以直接通过so文件获取
为了能顺利地在RegisterNatives()函数未被调用前Hook到,需要使用spwan模式(即将APP的启动权限交给Frida,Frida会重启程序)注入进程
此时StringFromJNI函相对so文件的偏移为
获取到函数偏移之后即可使用查找函数地址的方式获取相应函数地址进行Hook,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function main ( ) { var native_addr=Module.findBaseAddress('libfrida_so.so' ) console .log('native_addr is =>' ,native_addr) var stringfromJNi=native_addr.add(0xf11c ) console .log("StringFromJNI address => " ,stringfromJNi) Interceptor.attach(stringfromJNi,{ onEnter :function (args ) { console .log("jnienv pointer => " ,args[0 ]) console .log("jobj pointer => " ,args[1 ]) }, onLeave :function (retval ) { console .log("retval => " ,Java.vm.getEnv().getStringUtfChars(retval,null ).readCString()) console .log("=================" ) } }) } setImmediate(main)
native主动调用和替代函数
native函数替代
Interceptor.replace
1 2 3 4 5 6 7 8 9 10 11 Interceptor.replace(addr,NativeCallback); Interceptor.replace(this .context.x2,new NativeCallback(function (a1 ) { console .log("Replace Success" );return ;},'void' ,[]));
主动调用
感觉和replace差不多,得到需要主动调用的函数的地址之后通过**NativeFunction()**创建一个函数后调用
1 2 3 4 5 NativeFunction(addr,Return_Type,Arrgument_List); var add_method = new NativeFunction(Module.findExportByName('libhello.so' , 'c_getSum' ), 'int' ,['int' ,'int' ]);console .log("result:" ,add_method(1 ,2 ));
如果传入的参数是结构体
native修改函数的参数和返回值
修改函数的返回值
使用**.replace()**
首先使用Interceptor.attach()拦截到我们想要修改的函数,
1 2 3 4 5 6 7 8 9 10 Interceptor.attach(connect,{ onEnter :function (args ) { }, onLeave :function (retval ) { console .log("Change Success!!" ); retval.replace(1 ); } })
修改数值参数
使用ptr()即可,ptr可以将数值转为 NativePointer 类型
**toInt32()**将参数转为int型数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Interceptor.attach(nativePointer, { onEnter : function (args ) { send(args[0 ]); send(args[1 ]); send(args[2 ].toInt32()); send(args[3 ].toInt32()); send(args[4 ].toInt32()); args[4 ] = ptr(1000 ); send(args[4 ].toInt32()); }, onLeave : function (retval ) { send(retval.toInt32()); retval.replace(10000 ); send(retval.toInt32()); } })
修改字符串参数
有多种办法,这里采用将字符串写入内存中的办法来修改
主要用到Memory.allocUtf8String(str) ,然后直接将地址赋值给参数即可
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("libc.so" ,"strcpy" ); Interceptor.attach(strcpy,{ onEnter :function (args ) { args[1 ]=newstraddr; console .log(args[1 ].readCString()); }, onLeave :function (retval ) { console .log("hello" ); } })
libssl和libc库的Hook
其中会使用到frida-trace
RPC及其自动化
可以使用Python完成JS脚本对进程的注入以及相应的Hook
首先先修改APK代码
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 public class MainActivity extends AppCompatActivity { private String total="hello" ; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); while (true ){ try { Thread.sleep(1000 ); } catch (InterruptedException e) { e.printStackTrace(); } func(50 ,30 ); func("Jack" ); } } public void func (int x,int y) { String TAG="MainActivity" ; Log.d(TAG, String.valueOf(x+y)); } public void func (String name) { Log.d("MainActivity" ,name); } void secret () { total+=" secretFunc" ; Log.d("MainActivity" ,"Call Static Secret Method" ); } void secret_instance () { Log.d("MainActivity" ,"Call Static Secret_Instance Method" ); } }
编写JS脚本获取值(instance.total.value )和主动调用实例方法
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 function RunMethod ( ) { Java.perform(function ( ) { Java.choose('com.flag.test1.MainActivity' ,{ onMatch : function (instance ) { instance.secret() }, onComplete : function ( ) { }} ) }) } function main ( ) { console .log("Script Run Successfully" ) Java.perform(function ( ) { console .log("Java Internal" ) var MainActivity=Java.use('com.flag.test1.MainActivity' ) RunMethod() Java.choose('com.flag.test1.MainActivity' ,{ onMatch :function (instance ) { console .log('total value=' ,instance.total.value); }, onComplete :function ( ) { console .log("search Complete" ) } }) }) } setImmediate(main);
接下来我们使用RPC远程调用,首先将两个函数导出,使得外部可以调用,在JS代码末尾加上RPC相关代码
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 import fridaimport sysdef on_message (message,data ): if message['type' ]=='send' : print ("[*] {0}" .format (message['payload' ])) else : print (message) process = frida.get_usb_device(3000 ).attach('test1' ) with open ('get_value.js' ) as f: jscode=f.read() script=process.create_script(jscode) script.on('message' ,on_message) script.load() command="" while 1 ==1 : command=input () if command=="1" : break elif command=="2" : script.exports.getvalue() elif command=="3" : script.exports.runmethod()
Frida-InlineHook
也就是Hook一条汇编指令
实际上和Hook函数差不多,都是使用Interceptor.attach ,获取到指令地址就可以对其进行操作
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); } })
修改汇编指令
首先我们要将地址的权限修改为可读可写可执行(不然有时候会有bug),使用的是Memory.protect()
1 2 Memory.protect(addr,size,'rwx' );
接下来就需要使用写入内存的函数writeByteArray
Frida其他用法
hexdump打印内存的数据 ——可以用于打印加密后的结果和函数参数
打印调用栈(也可以使用Objection)
Frida创建Java数组
使用Java.array()
1 2 var objArray = Java.array("Ljava.lang.String;" ,["12" ,"2" ,"3" ,"4" ,"4" ]);
Frida读取内存数据并转为字节数组并转为字符串
有时候我们需要将内存中的数据读取到Hook代码中并进行运算
使用到的是readByteArray(size)
1 2 addr.readByteArray(size);
接下来使用js自带的Uint8Array 来接收
1 var bytes= new Uint8Array (addr.readByteArray(size));
接下来可以使用JS自带的**String.fromCharCode()**将字节转为字符
1 2 3 4 5 var final='' for (var i=0 ;i<24 ;++i){ final+=String .fromCharCode(encodeanwser[i]^xorkey[i]); } console .log(final);
这样就可以将xor后的结果转为字符串后打印出来
Frida从内存中读取字符串
使用addr.readCString()
1 var xorkey = addr1.readCString();
这样读取到的即为字符串,读取到’\0’为止
JS中将字符串转为字节数组
Python实现常见加密
Python中bytes和String的转换
因为加密的数据都需要为字节流,所以要先转为bytes型数据
对于数组直接使用bytes
1 2 b = [0x12 , 0x34 , 0x56 , 0x99 ] print (bytes (b))
bytes转为String使用decode()方法即可
String转为bytes使用encode()方法即可
Hash
主要用到的是hashlib ,Python自带的
直接通过hashlib.md5()等即可进行加密
1 2 3 4 5 6 import hashlibenc1 = hashlib.md5(flag) print (enc1.digest())
提供的加密有
使用起来也是比较方便的,这里以md5为例
1 print (hashlib.md5(b'helloworld' ).hexdigest())
如果想得到字节流,只需要将hexdigest()改为**digest()**即可
1 hashlib.md5(b'helloworld' ).digest()
对称加密
1 2 3 4 5 6 from Crypto.Cipher import AESfrom Crypto.Cipher import Blowfishfrom Crypto.Cipher import ARC4from Crypto.Cipher import Salsa20from Crypto.Cipher import ChaCha20from Crypto.Cipher import DES
使用的方法也是一致的,首先先new 一个对象,需要传入key、指定的Mode、IV
然后通过**.encrypt(flag),.decrypt(enc)**进行解密,注意必须是字节流
1 2 3 4 5 6 aes = AES.new(password, AES.MODE_ECB) enc = aes.encrypt(flag) print (enc)print (aes.decrypt(enc).decode())
下面使用AES的CBC模式进行加密
1 2 3 4 5 6 7 8 aes1 = AES.new(password, AES.MODE_CBC, iv) enc2 = aes1.encrypt(flag) print (enc2)aes2 = AES.new(password, AES.MODE_CBC, iv) print ((aes2.decrypt(enc2)).decode())
需要注意的是CBC模式下解密需要重新new一个AES对象
Base64编码解码
1 2 import base64print (base64.b64encode(b'123123' ))
也可以指定其他base家族的
Java实现常见加密、编码
bytes和String的互相转换
参考
Base64编码解码