安卓逆向入门-Frida入门 环境准备 1 2 3 4 5 6 7 8 9 10 pip install frida frida-tools objection frida-dexdump jnitrace frida --version adb push ./frida-server-16.3.3-android-x86_64 /data/local/tmp adb shell su cd /data/local/tmpmv ./frida-server-16.3.3-android-x86_64 ./frida-serverchmod 777 ./frida-server./frida-server frida-ps -U
对于Objection 1.11.0的打开\Lib\site-packages\objection\commands\frida_commands.py,注释掉第38行('Script Filename', frida_env['filename']),
。
下载https://github.com/hluwa/Wallbreaker ,随便找个地方放着。
若为Objection 1.11.0,可打开Python\Lib\site-packages\objection\agent.js,修改第18753行如下,这样使用命令android hooking watch class
后可对类中所有函数进行Hook,包括改之前不支持的构造函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const job = { identifier : jobs_1.jobs .identifier (), implementations : [], type : `watch-class for: ${clazz} ` , }; uniqueMethods.concat (["$init" ]).forEach ((method ) => { clazzInstance[method].overloads .forEach ((m ) => { const calleeArgTypes = m.argumentTypes .map ((arg ) => arg.className ); send (`Hooking ${color_1.colors.green(clazz)} .${color_1.colors.greenBright(method)} (${color_1.colors.red(calleeArgTypes.join(", " ))} )` ); m.implementation = function ( ) { send (color_1.colors .blackBright (`[${job.identifier} ] ` ) + `Called ${color_1.colors.green(clazz)} .${color_1.colors.greenBright(m.methodName)} (${color_1.colors.red(calleeArgTypes.join(", " ))} )` ); return m.apply (this , arguments ); }; job.implementations .push (m); }); });
入门 例如工程:
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 35 package com.example.myapplication;import android.content.DialogInterface;import android.os.Bundle;import android.widget.Button;import android.view.View;import android.app.AlertDialog;import androidx.appcompat.app.AppCompatActivity;public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button button=findViewById(R.id.button); button.setOnClickListener(new View .OnClickListener(){ @Override public void onClick (View view) { AlertDialog.Builder alterDialog=new AlertDialog .Builder(MainActivity.this ); alterDialog.setTitle("" ); alterDialog.setMessage(getString()); alterDialog.setPositiveButton("确定" ,new DialogInterface .OnClickListener(){ @Override public void onClick (DialogInterface dialog,int which) {} }); alterDialog.setNegativeButton("取消" ,new DialogInterface .OnClickListener(){ @Override public void onClick (DialogInterface dialog,int which) {} }); alterDialog.show(); } }); } private String getString () { return "正常输出" ; } }
布局文件:
1 2 3 4 5 6 7 8 9 10 11 <?xml version="1.0" encoding="utf-8" ?> <LinearLayout xmlns:android ="http://schemas.android.com/apk/res/android" android:orientation ="vertical" android:layout_width ="match_parent" android:layout_height ="match_parent" > <Button android:id ="@+id/button" android:layout_width ="match_parent" android:layout_height ="wrap_content" android:text ="click" /> </LinearLayout >
此时Hook“正常输出”改为“应用已被Hook”。
1 2 3 4 5 6 7 8 9 10 11 12 13 setImmediate (function ( ){ Java .perform (function ( ){ send ("starting script" ); var Activity =Java .use ("com.example.myapplication.MainActivity" ); Activity .getString .overload ().implementation =function ( ){ var result=this .getString (); send ("getString=" +result); var newResult="应用已被Hook!" ; send (newResult); return newResult; }; }); });
先启动APP,再用运行方法:
1 frida -U -l ./frida_hook.js -f com.example.myapplication
还可以把JavaScript写成Python脚本:
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 import frida,sysdevice=frida.get_usb_device() session=device.attach("My Application" ) front_app=device.get_frontmost_application() print ("正在运行的应用为:" ,front_app)src=""" setImmediate(function(){ Java.perform(function(){ send("starting script"); var Activity=Java.use("com.example.myapplication.MainActivity"); Activity.getString.overload().implementation=function(){ var result=this.getString(); send("getString="+result); var newResult="应用已被Hook!"; send(newResult); return newResult; }; }); }); """ def on_message (message,data ): if message["type" ]=="send" : print ("[+]{}" .format (message["payload" ])) else : print ("[-]{}" .format (message)) script=session.create_script(src) script.on("message" ,on_message) script.load() sys.stdin.read()
语法 常用命令:
1 2 3 4 5 6 7 8 9 10 11 frida-ps -U frida-ps -Ua frida-ps -Uai frida-ls-devices frida-discover -n <进程名> frida-discover -p <进程PID> frida-trace -i "recv*" -i "send*" <进程名> frida-trace -m "Objc" <进程名> frida-trace -U -f <进程名> -I "call" frida-trace -U -i "Java_*" <进程名> frida-kill -D <设备ID> <进程ID>
实战 例如Hook foo.so中的strncmp
函数:
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 35 36 import frida,sysdef on_message (message,data ): if message['type' ]=='send' : print ("[*]{0}" .format (message['payload' ])) else : print (message) jscode=""" Java.perform(function(){ console.log("[*]Hooking calls to System.exit"); const exitClass=Java.use("java.lang.System"); exitClass.exit.implementation=function(){ console.log("[*]System.exit called"); } var strncmp=undefined; var imports=Module.enumerateImportsSync("libfoo.so"); for(var i=0;i<imports.length;i++){ if(imports[i].name=="strncmp"){ strncmp=imports[i].address; break; } } Interceptor.attach(strncmp,{ onEnter:function(args){ if(Memory.readUtf8String(args[0],23)=="01234567890123456789012"){ console.log("[*]Secret string at"+args[1]+":"+Memory.readUtf8String(args[1],23)); } }, }); console.log("[*]Intercepting strncmp"); }); """ process=frida.get_usb_device().attach("owasp.mstg.uncrackable2" ) script=process.create_script(jscode) script.on('message' ,on_message) script.load() sys.stdin.read()
通过以下命令获取当前显示页面的Activity所在类:
1 adb shell dumpsys activity | grep "mResume"
基础语法 框架:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function main ( ){ console .log ("Script loaded successfully" ) Java .perform (function ( ){ console .log ("Inside java perform function" ) var MainActivity =Java .use ("com.roysue.demo02.MainActivity" ) console .lo ("Java.Use.Successfully!" ) MainActivity .fun .implementation =function (x,y ){ console .log ("x=>" ,x,"y=>" ,y) var ret_value=this .fun (x,y); return ret_value; } }) } setImmediate (main)
当fun函数有重载时,可以在第7行指定函数签名,如:
1 2 MainActivity .fun .overload ('int' ,'int' ).implementation MainActivity .fun .overload ('java.lang.String' ).implementation
Java类的函数分为类函数和实例方法。类函数用static修饰,和对应类绑定,若类函数被public修饰,则外部可直接通过类调用。实例方法没有static,需要创建对应类的实例再通过该实例调用,如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var MainActivity =Java .use ('com.roysue.demo02.MainActivity' )MainActivity .staticSecret ()Java .choose ('com.roysue.demo02.MainActivity' ,{ onMatch :function (instance ){ console .log ('instance found,total value=' ,instance.total .value ) instance.secret () }, onComplete :function ( ){ console .log ('search Complete' ) } })
例如不打算立即执行,可以将main函数改为其他函数名,分别写成函数后在Javascript代码最后进行导出:
1 2 3 4 rpc.exports ={ callsecretfunc :CallSecretFunc , gettotalvalue :getTotalValue };
然后用RPC模式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import frida,sysdef on_message (message,data ): if message['type' ]=='send' : print ("[*]{0}" .format (message['payload' ])) else : print (message) device=frida.get_usb_device() process=device.attach('com.roysue.demo02' ) with open ('4.js' ) as f: jscode=f.read() script=process.create_script(jscode) script.on('message' ,on_message) script.load() script.exports.callsecretfunc() script.exports.gettotalvalue()
RPC常用方法 获取设备:
1 2 3 import fridadevice=frida.get_usb_device() device=frida.get_device_manager().add_remote_device("192.168.50.96:6666" )
注入进程:
1 2 3 4 5 6 7 8 import time,fridapid=device.spawn(["com.android.settings" ]) device.resume(pid) time.sleep(1 ) session=device.attach(pid) session=device.attach("com.android.settings" )
注入脚本:
1 2 3 4 5 6 7 8 9 10 11 12 import fridascript=session.create_script(""" setImmediate(Java.perform(function(){ console.log("hello frida"); })) """ )script.load() with open ("hook.js" ) as f: script=session.create_script(f.read()) script.load()
JavaScript脚本向Python导出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import frida,timedevice=frida.get_usb_device() pid=device.spawn(["com.android.settings" ]) device.resume(pid) time.sleep(1 ) session=device.attach(pid) script=session.create_script(""" rpc.exports={ hello:function(){ return 'hello'; }, failPlease:function(){ return 'oops'; } }; """ )script.load() api=script.exports print ("api.hello()=>" ,api.hello())print ("api.fail_please()=>" ,api.fail_please())
三种注入分离响应函数、进程崩溃响应函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 import sys,frida,timedef on_detached (): print ("on_detached" ) def on_detached_with_reason (reason ): print ("on_detached_with_reason:" ,reason) def on_detached_with_varargs (*args ): print ("on_detached_with_varargs:" ,args) def on_process_crashed (crash ): print ("on_process_crashed\t crash:" ,crash) device=frida.get_usb_device() pid=device.spawn(["com.android.settings" ]) device.resume(pid) time.sleep(1 ) device.on('process-crashed' ,on_process_crashed) session=device.attach(pid) print ("attached" )session.on("detached" ,on_detached) session.on('detached' ,on_detached_with_reason) session.on('detached' ,on_detached_with_varargs) sys.stdin.read()
Objection初探 Objection有仨部分。第一部分为Objection重打包相关组件,将Frida运行所需frida-gadget.so重打包进App,完成Frida的无root调试。第二部分为Objection本身,和frida-gadget.so交互,运行并分析Frida的Hook。第三部分为Objection从TypeScript项目编译来的agent.js,后者在App运行中插入Frida运行库,支持Objection各功能。
打开“设置”,用Objection注入“设置”应用,并用explore命令进入REPL模式:
1 2 3 adb shell dumpsys activity activity top objection -g com.android.settings explore
REPL模式常用命令有:
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 help env #展示env命令帮助 jobs list #查看作业相关信息 frida #查看Frida相关信息 android hooking list classes #列出内存中所有类 实用但输出量大 去找历史记录C:\Users\Administrator\.objection android hooking search classes XXX #内存中搜索包含XXX关键词的类 android hooking search methods XXX #内存中搜索包含XXX关键词的方法 android hooking list class_methods XXX.XXX.XXX.XXX #列出某类所有方法 android hooking list activities #列出当前注入的进程的所有activity android hooking list services #列出进程所有service 还可改为receivers和providers android hooking list receivers android intent launch_activity <Activity类> #启动指定Activity组件 android intent launch_service <Service类> android hooking watch class <类名> android hooking watch class_method 方法名 --dump-args --dump-backtrace --dump-return #对指定方法Hook 方法名如java.io.File.$init android hooking set return_value <类名> false #设置返回值 只支持bool型 android hooking generate simple <类名> #生成Frida的Hook代码 search instances search instances <类名> #搜索指定类的实例 获取实例ID android heap search instances 类名 #搜索某类的实例 android heap execute 哈希码 方法名 #调用无参数实例方法 android heap evaluate 哈希码 #针对哈希码编写脚本 调用带参实例方法 android sslpinning disable memory list modules #枚举当前进程模块 memory list exports <库名> #查看指定模块导出函数 memroy list exports <库名> --json result.json memory search --string --offsets-only android root disable #对抗root检测 android root simulate #模拟root环境 android ui screenshot <文件名.png> android shell_exec <命令>
上述命令android hooking list classes
比较实用但输出量大,运行后去找历史记录C:\Users\Administrator.objection\objection.log。Visual Studio Code上用Alt+Shift+鼠标进行块状选中,可编写-c选项REPL批处理脚本。
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 com.android.settings on (samsung: 9) [usb] # android hooking watch class_method java.io.File.$init --dump-args --dump-b acktrace --dump-return (agent) Attempting to watch class java.io.File and method $init. (agent) Hooking java.io.File.$init(java.io.File, java.lang.String) (agent) Hooking java.io.File.$init(java.lang.String) (agent) Hooking java.io.File.$init(java.lang.String, int) (agent) Hooking java.io.File.$init(java.lang.String, java.io.File) (agent) Hooking java.io.File.$init(java.lang.String, java.lang.String) (agent) Hooking java.io.File.$init(java.net.URI) (agent) Registering job 456383. Type: watch-method for: java.io.File.$init com.android.settings on (samsung: 9) [usb] # jobs list Job ID Hooks Type ------ ----- ------------------------------------ 456383 6 watch-method for: java.io.File.$init com.android.settings on (samsung: 9) [usb] # // 这里随便点进“设置”一个选项 (agent) [456383] Called java.io.File.File(java.lang.String) (agent) [456383] Backtrace: java.io.File.<init>(Native Method) android.content.res.XResources.isFirstLoad(XResources.java:120) de.robv.android.xposed.XposedInit.cloneToXResources(XposedInit.java:282) de.robv.android.xposed.XposedInit.access$000(XposedInit.java:60) de.robv.android.xposed.XposedInit$2.afterHookedMethod(XposedInit.java:145) de.robv.android.xposed.XC_MethodHook.callAfterHookedMethod(XC_MethodHook.java:68) EdHooker_.hook(Unknown Source:175) android.app.ResourcesManager.createBaseActivityResources(ResourcesManager.java:733) android.app.ContextImpl.createActivityContext(ContextImpl.java:2384) android.app.ActivityThread.createBaseContextForActivity(ActivityThread.java:3043) android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2872) android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3093) android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78) android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108) android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68) android.app.ActivityThread$H.handleMessage(ActivityThread.java:1823) android.os.Handler.dispatchMessage(Handler.java:106) android.os.Looper.loop(Looper.java:193) android.app.ActivityThread.main(ActivityThread.java:6840) java.lang.reflect.Method.invoke(Native Method) com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493) com.android.internal.os.ZygoteInit.main(ZygoteInit.java:860) (agent) [456383] Arguments java.io.File.File(/system/priv-app/Settings/Settings.apk) (agent) [456383] Return Value: (none) com.android.settings on (samsung: 9) [usb] # jobs kill 456383 com.android.settings on (samsung: 9) [usb] # jobs list Job ID Hooks Type ------ ----- ---- com.android.settings on (samsung: 9) [usb] # android hooking watch class java.io.File (agent) Hooking java.io.File.createTempFile(java.lang.String, java.lang.String) (agent) Hooking java.io.File.createTempFile(java.lang.String, java.lang.String, java.io.File) (agent) Hooking java.io.File.listRoots() (agent) Hooking java.io.File.readObject(java.io.ObjectInputStream) (agent) Hooking java.io.File.slashify(java.lang.String, boolean) (agent) Hooking java.io.File.writeObject(java.io.ObjectOutputStream) (agent) Hooking java.io.File.canExecute() (agent) Hooking java.io.File.canRead() (agent) Hooking java.io.File.canWrite() (agent) Hooking java.io.File.compareTo(java.io.File) (agent) Hooking java.io.File.compareTo(java.lang.Object) (agent) Hooking java.io.File.createNewFile() (agent) Hooking java.io.File.delete() (agent) Hooking java.io.File.deleteOnExit() (agent) Hooking java.io.File.equals(java.lang.Object) (agent) Hooking java.io.File.exists() (agent) Hooking java.io.File.getAbsoluteFile() (agent) Hooking java.io.File.getAbsolutePath() (agent) Hooking java.io.File.getCanonicalFile() (agent) Hooking java.io.File.getCanonicalPath() (agent) Hooking java.io.File.getFreeSpace() (agent) Hooking java.io.File.getName() (agent) Hooking java.io.File.getParent() (agent) Hooking java.io.File.getParentFile() (agent) Hooking java.io.File.getPath() (agent) Hooking java.io.File.getPrefixLength() (agent) Hooking java.io.File.getTotalSpace() (agent) Hooking java.io.File.getUsableSpace() (agent) Hooking java.io.File.hashCode() (agent) Hooking java.io.File.isAbsolute() (agent) Hooking java.io.File.isDirectory() (agent) Hooking java.io.File.isFile() (agent) Hooking java.io.File.isHidden() (agent) Hooking java.io.File.isInvalid() (agent) Hooking java.io.File.lastModified() (agent) Hooking java.io.File.length() (agent) Hooking java.io.File.list() (agent) Hooking java.io.File.list(java.io.FilenameFilter) (agent) Hooking java.io.File.listFiles() (agent) Hooking java.io.File.listFiles(java.io.FileFilter) (agent) Hooking java.io.File.listFiles(java.io.FilenameFilter) (agent) Hooking java.io.File.mkdir() (agent) Hooking java.io.File.mkdirs() (agent) Hooking java.io.File.renameTo(java.io.File) (agent) Hooking java.io.File.setExecutable(boolean) (agent) Hooking java.io.File.setExecutable(boolean, boolean) (agent) Hooking java.io.File.setLastModified(long) (agent) Hooking java.io.File.setReadOnly() (agent) Hooking java.io.File.setReadable(boolean) (agent) Hooking java.io.File.setReadable(boolean, boolean) (agent) Hooking java.io.File.setWritable(boolean) (agent) Hooking java.io.File.setWritable(boolean, boolean) (agent) Hooking java.io.File.toPath() (agent) Hooking java.io.File.toString() (agent) Hooking java.io.File.toURI() (agent) Hooking java.io.File.toURL() (agent) Registering job 388472. Type: watch-class for: java.io.File com.android.settings on (samsung: 9) [usb] # jobs list Job ID Hooks Type ------ ----- ----------------------------- 388472 56 watch-class for: java.io.File // 这里随便点进“设置”一个选项 (agent) [388472] Called java.io.File.exists() (agent) [388472] Called java.io.File.getPath() (agent) [388472] Called java.io.File.equals(java.lang.Object) (agent) [388472] Called java.io.File.compareTo(java.io.File) (agent) [388472] Called java.io.File.getPath() (agent) [388472] Called java.io.File.getPath() (agent) [388472] Called java.io.File.equals(java.lang.Object) (agent) [388472] Called java.io.File.compareTo(java.io.File) (agent) [388472] Called java.io.File.getPath() (agent) [388472] Called java.io.File.getPath() (agent) [388472] Called java.io.File.lastModified()
主动调用实战例如:
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 com.android.settings on (samsung: 9) [usb] # android heap search instances java.io.File Class instance enumeration complete for java.io.File Hashcode Class toString() ----------- ------------ -------------------------------------------------------------- 133688275 java.io.File /system/priv-app/Settings/Settings.apk 47718804 java.io.File /proc 137635794 java.io.File /sys/fs/bpf/traffic_uid_stats_map -1320040561 java.io.File /proc/net/xt_qtaguid/iface_stat_all -1320030053 java.io.File /proc/net/xt_qtaguid/iface_stat_fmt -1877617375 java.io.File /proc/net/xt_qtaguid/stats 2048048455 java.io.File /sys/board_properties/soc/msv -54300376 java.io.File /data/misc/user/0 -48471265 java.io.File /data/misc/user/0/cacerts-added 2041518079 java.io.File /data/misc/user/0/cacerts-removed 1664757827 java.io.File /system/lib64 -895673764 java.io.File /system/vendor/lib64 114250896 java.io.File /data/user_de/0/com.android.settings/code_cache 958541197 java.io.File /data/user/0/com.android.settings -1227050545 java.io.File /data/user_de/0/com.android.settings -1227050545 java.io.File /data/user_de/0/com.android.settings 1664757827 java.io.File /system/lib64 -895673764 java.io.File /system/vendor/lib64 1645413532 java.io.File /data/user_de/0/com.android.settings/files/condition_state.xml 1234366 java.io.File / -1782362368 java.io.File /data/user_de/0/com.android.settings/cache 1664757827 java.io.File /system/lib64 -895673764 java.io.File /system/vendor/lib64 -895673764 java.io.File /system/vendor/lib64 1664757827 java.io.File /system/lib64 2097462291 java.io.File /system/priv-app/Settings/lib/x86_64 1664757827 java.io.File /system/lib64 -895673764 java.io.File /system/vendor/lib64 -1714625846 java.io.File /system/framework/eddexmaker.jar 1190786266 java.io.File /system/framework/eddalvikdx.jar -594014569 java.io.File /system/framework/edxp.jar 1664757827 java.io.File /system/lib64 -895673764 java.io.File /system/vendor/lib64 1664757827 java.io.File /system/lib64 -895673764 java.io.File /system/vendor/lib64 1664757827 java.io.File /system/lib64 -895673764 java.io.File /system/vendor/lib64 1664757827 java.io.File /system/lib64 -895673764 java.io.File /system/vendor/lib64 1664757827 java.io.File /system/lib64 -895673764 java.io.File /system/vendor/lib64 -1782362368 java.io.File /data/user_de/0/com.android.settings/cache -1779601385 java.io.File /data/user_de/0/com.android.settings/files -1782362368 java.io.File /data/user_de/0/com.android.settings/cache 1433727619 java.io.File /system/framework/org.apache.http.legacy.boot.jar 133688275 java.io.File /system/priv-app/Settings/Settings.apk -1975059403 java.io.File /data/misc/keychain/cacerts-added -882893419 java.io.File /data/misc/keychain/cacerts-removed 1173056351 java.io.File /system/etc/security/cacerts -853728249 java.io.File /system/etc/security/ct_known_logs -2013037938 java.io.File /data/misc/keychain/trusted_ct_logs/current 47683016 java.io.File /data -1978853580 java.io.File /mnt/expand -2123135345 java.io.File /system -1575638275 java.io.File /storage 1671863261 java.io.File /data/cache 384824 java.io.File /odm 384857 java.io.File /oem -533391 java.io.File /product -2056170586 java.io.File /vendor -1119854388 java.io.File /cache/recovery/block.map -702818824 java.io.File /system/etc/security/otacerts.zip -1577688767 java.io.File /cache/recovery/last_install 1902719031 java.io.File /cache/recovery/log 405131488 java.io.File /cache/recovery 301269365 java.io.File /cache/recovery/uncrypt_file 1310302411 java.io.File /cache/recovery/uncrypt_status 1664757827 java.io.File /system/lib64 -895673764 java.io.File /system/vendor/lib64 -895673764 java.io.File /system/vendor/lib64 1664757827 java.io.File /system/lib64 1234367 java.io.File . com.android.settings on (samsung: 9) [usb] # android heap execute 133688275 getPath Found 2 handles, this is probably a bug, please report it! Handle 133688275 is to class java.io.File Executing method: getPath() /system/priv-app/Settings/Settings.apk com.android.settings on (samsung: 9) [usb] # android heap evaluate 133688275 (The hashcode at `133688275` will be available as the `clazz` variable.) console.log('File is canWrite? =>',clazz.canWrite()) //clazz为该File类 clazz.setWritable(false) console.log('File is canWrite? =>',clazz.canWrite()) JavaScript capture complete. Evaluating... Found 2 handles, this is probably a bug, please report it! Handle 133688275 is to class java.io.File File is canWrite? => false File is canWrite? => false
SSL Pinning称为证书绑定。该方式不仅校验服务器证书是否是系统中可信凭证,通信过程中甚至连系统内置证书都不信任,而只信任指定证书。一旦发现服务器证书为非指定证书即停止通信,导致抓包软件安装到系统信任凭据中也无法生效。
Objection有自带SSL Pinning Bypass功能:
1 android sslpinning disable
此外还有ZenTrace工具:https://github.com/hluwa/ZenTracer 。使用Match RegEx可添加类的过滤,例如“M:com.cz.babySister”表示Hook,“B:com.cz.babySister”表示不过滤。Start后,ZenTracer对前台证显示的应用所有符合匹配规则的目标类进行Hook,上方显示参数和返回值信息。还可导出为JSON。
逆向工作思路 找到当前正在运行的Activity:
1 adb shell dumpsys activity top
objection注入:
1 objection -g com.example.junior explore
找出所有Activity:
1 android hooking list activities
找到目标Activity直接启动:
1 android intent launch_activity com.example.junior.CalculatorActivity
列出该类中方法:
1 android hooking list class_methods com.example.junior.CalculatorActivity
对目标方法Hook:
1 android hooking watch class_method com.example.junior.CalculatorActivity.caculate --dump-args --dump-backtrace --dump-return
用以下Frida脚本和Objection输出内容相同,但Frida和Objection不能同时Hook同一个方法,得先在Objection里删除该Job。
1 2 3 4 5 6 7 8 9 10 11 12 13 function main ( ){ Java .perform (function ( ){ var Arith =Java .use ('com.example.junior.util.Arith' ) Arith .sub .overload ('java.lang.String' ,'java.lang.String' ).implementation =function (str1,str2 ){ var JavaString =Java .use ('java.lang.String' ) var result=this .sub (str,JavaString .$new('123' )) console .log ('str,str2,result=>' ,str,str2,result) console .log (Java .use ("android.util.Log" ).getStackTraceString (Java .use ("java.lang.Throwable" ).$new())) return result } }) } setImmediate (main)
主动调用方法:
1 2 3 4 5 6 7 8 9 function CallSub (a,b ){ var Arith =Java .use ('com.example.junior.util.Arith' ) var JavaString =Java .use ('java.lang.String' ) var result=Arith .sub (JavaString .$new(a),JavaString .$new(b)) console .log (a,"-" ,b,"=" ,result) } rpc.exports ={ sub :CallSub };
编写利用脚本:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 import frida,sysdef on_message (message,data ): if message['type' ]=='send' : print ("[*]{0}" .format (message['payload' ])) else : print (message) device=frida.get_usb_device() process=device.attach('com.example.junior' ) with open ('call.js' ) as f: jscode=f.read() script=process.create_script(jscode) script.on('message' ,on_message) script.load() for i in range (20 ,30 ): for j in range (0 ,10 ): script.exports.sub(str (i),str (j))
想做到规模化就应使用Frida的网络模式,服务端运行为:
1 2 3 ./frida-server -l 0.0.0.0:8888 netstat --pantul | grep frida ifconfig
进行远程注入:
1 objection -N -h 192.168.xxx.xxx -p 8888 -g com.xxx.xxx explore
利用脚本中改为:
1 device=frida.get_device_manager().add_remote_device('192.168.xxx.xxx:8888' )
常用插件 Wallbreaker是Objection的一个插件,下载后在Objection的REPL模式下:
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 com.android.settings on (samsung: 9) [usb] # plugin load D:\Wallbreaker Loaded plugin: wallbreaker com.android.settings on (samsung: 9) [usb] # plugin wallbreaker objectsearch java.io.File #跟objection heap search功能一样 [0x2e46]: /proc ... com.android.settings on (samsung: 9) [usb] # plugin wallbreaker objectdump 0x2e46 #将某实例具体内容打印出来 后面为实例码 package java.io class File { /* static fields */ static boolean $assertionsDisabled; => false ... /* instance fields 重点在这里 观察该实例各成员的值*/ Path filePath; => null ... /* constructor methods */ java.io.File(File, String); ... /* static methods */ static File createTempFile(String, String); ... /* instance methods */ void readObject(ObjectInputStream); ... }
定位弹窗使用的类常见有android.app.AlertDialog、android.app.Dialog、android.widget.PopupWindow等,优先用这几个尝试objectsearch。
设置完Hook后,如果需要在App一运行后就挂载某Hook,可以实现:
1 objection -g com.xxx.xxx explore -s "android hooking watch class xxx.xxx.xxx"
此外,-c命令为App一运行就要跑的REPL命令脚本,如:
1 objection -g com.xxx.xxx explore -c "*.txt"
FRIDA-DEXDump为Objection的一个脱壳插件,暴力搜索内存中符合dex格式的数据完成dump。在CLI模式下可以:
1 2 frida-dexdump -FU frida-dexdump -U -f com.xxx.xxx
作为Objection插件使用:
1 2 plugin load D:\DEXDump plugin dexdump dump
脱壳后会提示dump出来的.dex文件存储路径,按照文件从小到大分别改名为classes.dex、classes2.dex、classes3.dex、classes4.dex等,放到并替换原classes.dex目录下。在Jadx搜索找到extends Application
的代码,反编译其目录下AndroidManifest.xml,把Application标签内android:name改为完整类名后,即可用apktool重新打包。
项目https://github.com/WooyunDota/DroidSSLUnpinning/blob/master/ObjectionUnpinningPlus/hooks.js 补充了一些Objection没有的SSL Pinning Bypass方式。
抓包入门 网络通信相关框架有HttpURLConnection、okhttp、okhttp3等,其中okhttp和okhttp3完全不同。HttpURLConnection的原生库底层使用okhttp。
HttpURLConnection 一个基本的HttpURLConnection如下:
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import android.os.Bundle;import android.util.Log;import java.io.IOException;import java.io.InputStream;import java.net.HttpURLConnection;import java.net.URL;import java.nio.charset.StandardCharsets;public class MainActivity extends AppCompatActivity { @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); new Thread (new Runnable () { @Override public void run () { while (true ){ try { URL url = new URL ("https://www.baidu.com" ); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET" ); connection.setRequestProperty("token" ,"r0ysue666" ); connection.setConnectTimeout(8000 ); connection.setReadTimeout(8000 ); connection.connect(); InputStream in = connection.getInputStream(); int bufferSize = 1024 ; byte [] buffer = new byte [bufferSize]; StringBuffer sb = new StringBuffer (); while ((in.read(buffer)) != -1 ) { sb.append(new String (buffer)); } Log.d("r0ysue666" , sb.toString()); connection.disconnect(); } catch (IOException e) { e.printStackTrace(); } try { Thread.sleep(10 *1000 ); } catch (InterruptedException e) { e.printStackTrace(); } } } }).start(); } }
并在AndroidManifest.xml中加入网络权限:
1 <uses-permission android:name ="android.permission.INTERNET" />
第一步先Hook整个URL类的构造函数:
1 android hooking watch class_method java.net.URL.$init --dump-args --dump-backtrace --dump-return
自吐脚本为:
1 2 3 4 5 6 7 8 9 10 11 function main ( ){ Java .perform (function ( ){ var URL =Java .use ('java.net.URL' ) URL .$init .overload ('java.lang.String' ).implementation =function (urlset ){ console .log ('url=>' ,urlstr) var result=this .$init(urlstr) return result } }) } setImmediate (main)
下一步还可以Hook整个HttpURLConnection类所有函数:
1 2 android hooking watch class java.net.HttpURLConnection android hooking watch class_method java.net.HttpURLConnection.$init --dump-args --dump--backtrace --dump-return
用WallBreaker搜索HttpURLConnection类实例,发现该类其实是个抽象类,但运行时是抽象类的具体实现类在工作。此时关注上述源码中构造该类的openConnection
,对后者进行Hook:
1 2 3 4 5 6 7 8 9 10 11 function main ( ){ Java .perform (function ( ){ var URL =Java .use ('java.net.URL' ) URL .openConnection .overload ().implementation =function ( ){ var result=this .openConnection () console .log ('openConnection() returnType=>' ,result.$className ) return result } }) } setImmediate (main)
返回值找到具体实现类为com.android.okhttp.internal.huc.HttpURLConnectionImpl类,尝试:
1 android hooking watch class com.android.okhttp.internal.huc.HttpURLConnectionImpl
Hook发现程序中每个函数都被调用到了,最终请求参数自吐脚本如下:
1 2 3 4 5 6 7 8 9 10 11 function main ( ){ Java .perform (function ( ){ var HttpURLConnectionImpl =Java .use ('com.android.okhttp.internal.huc.HttpURLConnectionImpl' ) HttpURLConnectionImpl .setRequestProperty .implementation =function (key,value ){ var result=this .setRequestProperty (key,value) console .log ('setRequestProperty=>' ,key,':' ,value) return result } }) } setImmediate (main)
okhttp3 在app/build.gradle中添加okhttp3的引用并Sync:
1 2 3 4 5 dependencies { ... implementation("com.squareup.okhttp3:okhttp:3.12.0" ) }
其中主界面如下:
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 package com.r0ysue.okhttp3demo;import android.os.Bundle;import android.util.Log;import android.view.View;import android.widget.Button;import androidx.appcompat.app.AppCompatActivity;import java.io.IOException;public class MainActivity extends AppCompatActivity { private static String TAG = "r0ysue666" ; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button btn = findViewById(R.id.mybtn); btn.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { String requestUrl = "https://www.baidu.com/" ; example myexample = new example (); try { myexample.run(requestUrl); } catch (IOException e) { e.printStackTrace(); } } }); } }
对于example类实现:
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 35 36 37 38 39 40 41 42 43 package com.r0ysue.okhttp3demo;import android.util.Log;import java.io.IOException;import okhttp3.Call;import okhttp3.Callback;import okhttp3.OkHttpClient;import okhttp3.Request;import okhttp3.Response;public class example { private static final String TAG = "r0ysue666" ; OkHttpClient client = new OkHttpClient .Builder() .build(); void run (String url) throws IOException { Request request = new Request .Builder() .url(url) .header("token" ,"r0ysue" ) .build(); client.newCall(request).enqueue( new Callback () { @Override public void onFailure (Call call, IOException e) { call.cancel(); } @Override public void onResponse (Call call, Response response) throws IOException { Log.d(TAG, response.body().string()); } } ); } }
如果单纯Hook上述newCall
,可能存在Call后没有发出实际请求的情况,该函数实际调用了RealCall.newRealCall
。RealCall对象是okhttp3.Call接口的唯一实现,表示一个等待执行的请求且只能被执行以此,到这一步请求仍可被取消。只有Hook了execute
和enqueue
才能真正保证每个从okhttp出去的请求都能被Hook到。
1 2 3 4 5 static RealCall newRealCall (OkHttpClient client, Request originalRequest, boolean forWebSocket) { RealCall call=new RealCall (client,originalRequest,forWebSocket); call.eventListener=client.eventListenerFactory().create(call); return call; }
okhttp3通过拦截器Interceptor完成监控管理、重写和重试请求。每个网络请求和接收都必须经过okhttp3本身存在的五大拦截器。拦截器可对request修改,数据返回时对response做出修改。拦截器机制实际上是一个链条,最上层拦截器首先向下传递一个request,请求下层拦截器返回一个response。传递到最后一个拦截器,它对该request进行处理并返回resposne。
1 2 3 4 5 6 7 8 9 10 11 12 13 Response getResponseWithInterceptorChain () throws IOException { List<Interceptor> interceptors = new ArrayList <>(); interceptors.addAll(client.interceptors()); interceptors.add(retryAndFollowUpInterceptor); interceptors.add(new BridgeInterceptor (client.cookieJar())); interceptors.add(new CacheInterceptor (client.internalCache())); interceptors.add(new ConnectInterceptor (client)); if (!forWebSocket) interceptors.addAll(client.networkInterceptors()); interceptors.add(new CallServerInterceptor (forWebSocket)); Interceptor.Chain chain = new RealInterceptorChain (interceptors, null , null , null , 0 , originalRequest, this , eventListener, client.connectTimeoutMillis(), client.readTimeoutMillis(), client.writeTimeoutMillis()); return chain.proceed(originalRequest); }
这里演示新建一个拦截器类,打印URL和请求headers:
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 package com.r0ysue.okhttp3demo;import android.util.Log;import java.io.EOFException;import java.io.IOException;import java.nio.charset.Charset;import java.util.concurrent.TimeUnit;import okhttp3.Connection;import okhttp3.Headers;import okhttp3.Interceptor;import okhttp3.MediaType;import okhttp3.Request;import okhttp3.RequestBody;import okhttp3.Response;import okhttp3.ResponseBody;import okhttp3.internal.http.HttpHeaders;import okio.Buffer;import okio.BufferedSource;import okio.GzipSource;public class LoggingInterceptor implements Interceptor { private static final String TAG = "okhttpGET" ; private static final Charset UTF8 = Charset.forName("UTF-8" ); @Override public Response intercept (Chain chain) throws IOException { Request request = chain.request(); RequestBody requestBody = request.body(); boolean hasRequestBody = requestBody != null ; Connection connection = chain.connection(); String requestStartMessage = "--> " + request.method() + ' ' + request.url(); Log.e(TAG, requestStartMessage); if (hasRequestBody) { if (requestBody.contentType() != null ) Log.e(TAG, "Content-Type: " + requestBody.contentType()); if (requestBody.contentLength() != -1 ) Log.e(TAG, "Content-Length: " + requestBody.contentLength()); } Headers headers = request.headers(); for (int i = 0 , count = headers.size(); i < count; i++) { String name = headers.name(i); if (!"Content-Type" .equalsIgnoreCase(name) && !"Content-Length" .equalsIgnoreCase(name)) Log.e(TAG, name + ": " + headers.value(i)); } if (!hasRequestBody) Log.e(TAG, "--> END " + request.method()); else if (bodyHasUnknownEncoding(request.headers())) Log.e(TAG, "--> END " + request.method() + " (encoded body omitted)" ); else { Buffer buffer = new Buffer (); requestBody.writeTo(buffer); Charset charset = UTF8; MediaType contentType = requestBody.contentType(); if (contentType != null ) charset = contentType.charset(UTF8); Log.e(TAG, "" ); if (isPlaintext(buffer)) { Log.e(TAG, buffer.readString(charset)); Log.e(TAG, "--> END " + request.method() + " (" + requestBody.contentLength() + "-byte body)" ); } else Log.e(TAG, "--> END " + request.method() + " (binary " + requestBody.contentLength() + "-byte body omitted)" ); } long startNs = System.nanoTime(); Response response; try { response = chain.proceed(request); } catch (Exception e) { Log.e(TAG, "<-- HTTP FAILED: " + e); throw e; } long tookMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startNs); ResponseBody responseBody = response.body(); long contentLength = responseBody.contentLength(); String bodySize = contentLength != -1 ? contentLength + "-byte" : "unknown-length" ; Log.e(TAG, "<-- " + response.code() + (response.message().isEmpty() ? "" : ' ' + response.message()) + ' ' + response.request().url() + " (" + tookMs + "ms" + (", " + bodySize + " body:" + "" ) + ')' ); Headers myheaders = response.headers(); for (int i = 0 , count = myheaders.size(); i < count; i++) Log.e(TAG, myheaders.name(i) + ": " + myheaders.value(i)); if (!HttpHeaders.hasBody(response)) Log.e(TAG, "<-- END HTTP" ); else if (bodyHasUnknownEncoding(response.headers())) Log.e(TAG, "<-- END HTTP (encoded body omitted)" ); else { BufferedSource source = responseBody.source(); source.request(Long.MAX_VALUE); Buffer buffer = source.buffer(); Long gzippedLength = null ; if ("gzip" .equalsIgnoreCase(myheaders.get("Content-Encoding" ))) { gzippedLength = buffer.size(); GzipSource gzippedResponseBody = null ; try { gzippedResponseBody = new GzipSource (buffer.clone()); buffer = new Buffer (); buffer.writeAll(gzippedResponseBody); } finally { if (gzippedResponseBody != null ) gzippedResponseBody.close(); } } Charset charset = UTF8; MediaType contentType = responseBody.contentType(); if (contentType != null ) charset = contentType.charset(UTF8); if (!isPlaintext(buffer)) { Log.e(TAG, "" ); Log.e(TAG, "<-- END HTTP (binary " + buffer.size() + "-byte body omitted)" ); return response; } if (contentLength != 0 ) { Log.e(TAG, "" ); Log.e(TAG, buffer.clone().readString(charset)); } if (gzippedLength != null ) Log.e(TAG, "<-- END HTTP (" + buffer.size() + "-byte, " + gzippedLength + "-gzipped-byte body)" ); else Log.e(TAG, "<-- END HTTP (" + buffer.size() + "-byte body)" ); } return response; } static boolean isPlaintext (Buffer buffer) { try { Buffer prefix = new Buffer (); long byteCount = buffer.size() < 64 ? buffer.size() : 64 ; buffer.copyTo(prefix, 0 , byteCount); for (int i = 0 ; i < 16 ; i++) { if (prefix.exhausted()) break ; int codePoint = prefix.readUtf8CodePoint(); if (Character.isISOControl(codePoint) && !Character.isWhitespace(codePoint)) return false ; } return true ; } catch (EOFException e) { return false ; } } private boolean bodyHasUnknownEncoding (Headers myheaders) { String contentEncoding = myheaders.get("Content-Encoding" ); return contentEncoding != null && !contentEncoding.equalsIgnoreCase("identity" ) && !contentEncoding.equalsIgnoreCase("gzip" ); } }
将上述工程编译为.apk文件,解压出classes.dex,改名为okhttp3loggin.dex放到/data/local/tmp下,用Firda脚本将该拦截器添加到原有的拦截器链条中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function hook_okhttp3 ( ) { Java .perform (function ( ) { Java .openClassFile ("/data/local/tmp/okhttp3logging.dex" ).load (); var MyInterceptor = Java .use ("com.r0ysue.okhttp3demo.LoggingInterceptor" ); var MyInterceptorObj = MyInterceptor .$new(); var Builder = Java .use ("okhttp3.OkHttpClient$Builder" ); console .log (Builder ); Builder .build .implementation = function ( ) { this .networkInterceptors ().add (MyInterceptorObj ); return this .build (); }; console .log ("hook_okhttp3..." ); }); } function main ( ) { hook_okhttp3 (); } setImmediate (main)
若用attach模式,Hook时机偏后,会带来大量干扰信息, 甚至导致App崩溃。这里用spawn模式,命令为:
1 frida -U -f com.xxx.xxx -l hookInterceptor.js --no-pause
okhttp3的证书绑定方法为:
1 client=new OkHttpClient .Builder().certificatePinner(new CertificatePinner .Builder().add("test.com" ,"sha512/14cf3JCaO4V..." ).build()).build();
Socket 无论有多少三方框架,三方框架是否被混淆,都不可避免地经过系统Socket相关类。Socket由系统完成,相关类一定不会被混淆。先搜索Socket全部类:
1 android hooking search classes socket
找历史记录,每行前面加上android hooking watch class
,重新Spawn:
1 objection -g com.xxx.xxx explore -c xxx.txt
根据Android源码/xref/libcore/ojluni/src/main/java/java/net/SocketOutputStream.java,发现java.net.AbstractPlainSocketImpl.acquireFD
被socketWrite
调用,第一个参数为网络传输的数据内容。获取request的脚本为:
1 2 3 4 5 6 7 Java .use ('java.net.SocketOutputStream' ).socketWrite .overload ('[B' ,'int' ,'int' ).implementation =function (b,off,len ){ var result=this .socketWrite (b,off,len); console .log ('socketWrite result,b,off,len=>' ,result,b,off,len); var ByteString =Java .use ("com.android.okhttp.okio.ByteString" ); console .log ('contents:=>' ,ByteString .of (b).hex ()); return result; };
先写一个函数,将Java层的byte数组打印出相应字节的dexdump:
1 2 3 4 5 6 function jhexdump (array ){ var ptr=Memory .alloc (array.length ); for (var i=0 ;i<array.length ;i++) Memory .writeS8 (ptr.add (i),array[i]); console .log (hexdump (ptr,{offset :0 ,length :array.length ,header :false ,ansi :false })); }
对于response,找java.net.SocketInputStream.read([B,int,int)
函数,得到的是gzip压缩后的数据,解压即可:
1 2 3 4 5 6 7 Java .use ('java.new.SocketInputStream' ).read .overload ('[B' ,'int' ,'int' ).implementation =function (bytearray1,int1,int2 ){ var result=this .read (bytearray1,int1,int2); console .log ('read result,bytearray1,int1,int2=>' ,result,bytearray1,int1,int2); var ByteString =Java .use ("com.android.okhttp.okio.ByteString" ); jhexdump (bytearray1); return result; };
上面是HTTP的抓包方法,现在为HTTPS抓包方法。此时关键函数为com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream.write([B,int,int)
和com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLInputStream.read([B,int,int)
。它们第一个参数为明文数据。
无论是HTTP还是HTTPS,都可Hook函数java.net.InetSocketAddress.InetSocketAddress(java.net.InetAddress,int)
。第一个参数转String为IP地址,第二个参数为端口号。
用下面命令翻一下类的结构:
1 2 plugin load D:/plugins/Wallbreaker plugin wallbreaker classdump java.net.InetAddress
其中isSiteLocalAddress
区分是本地地址还是远程地址,最终如下:
1 2 3 4 5 6 7 8 9 10 11 12 function hookAddress ( ){ Java .perform (function ( ){ Java .use ('java.net.InetSocketAddress' ).$init .overload ('java.net.InetAddress' ,'int' ).implementation =function (addr,port ){ var result=this .$init(addr,port); if (addr.isSiteLocalAddress ()) console .log ('Local address=>' ,addr.toString (),',port is ' ,port); else console .log ('Server address=>' ,addr.toString (),',port is ' ,port); } return result; }) }
WebSocket 使用WebSocket时,服务器可主动向客户端推送信息,客户端也可主动向服务器发送信息,即全双工通信。WebSocket建立连接后能保持持久化连接,不需要一个request对应一个response。
XMPP XMPP用于即时通信,基于XML。一个XML消息实体如下,message标签即为XML Stanza。Android中常用的基于XMPP协议的开发框架为Smack。
1 2 3 4 5 6 7 8 9 10 11 12 13 <?xmlversion='1.0'?> <stream:stream to ='Receiver' xmlns ='jabber:client' xmlns:stream ='http_etherx_jabber_org/stream' version ='1.0' > <message from ='Sender' to ='Receiver' xml:lang ='zh-cn' > <body > xxx</body > </message > </stream:stream >
Protobuf Protobuf时一种不依赖语言和平台类型、可扩展的用于序列化结构数据的机制,支持多种主流计算机语言,目前主要用proto2和proto3两大版本。Protobuf在直播、弹幕等实时性数据传输业务需求下,能够减少数据在传输过程中占用空间的大小,是JSON性能的100多倍。Protobuf用.proto文件定义数据格式。
1 2 3 4 5 6 7 8 9 10 11 12 13 syntax="proto3" option java_multiple_files=true ;option java_package="com.xxx.xxx" ; option java_outer_classname="HelloWorldProto" ;option objc_class_prefix="HLW" ;package helloworld; message HelloRequest { string name=1 ; int32 sex=2 ; } message HelloReply { string message=1 ; }
Protobuf通常与gRPC框架配合使用,后者是个高性能、开源和通用的RPC框架,主要面向移动设计,提供多个语言版本,支持Android和Web。gRPC基于HTTP/2标准设计,具有双向流、流控、头部压缩、单TCP连接上的多复用请求等特性,移动设备上更省电、空间占用更少。
图片抓包 Android通常用BitmapFactory类中函数加载Bitmap对象,通过ImageView控件加载Bitmap对象类型的图片。BitmapFactory类提供的静态方法有decodeFile
、decodeResource
、decodeStream
、decodeByteArray
,分别从文件系统、资源、输入流和字节数组中加载Bitmap对象。图片读写非常耗时,下面脚本新建线程进行图片文件写入。
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 35 36 37 38 39 40 41 42 43 44 45 function guid () { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' .replace(/[xy]/g,function(c){ var r=Math.random()*16 |0 ,v=c=='x' ?r:(r&0x3 |0x8 ); return v.toString(16 ); }); } function saveBitmap_3 () { Java.perform(function(){ var Runnable = Java.use("java.lang.Runnable" ); var saveImg = Java.registerClass({ name: "com.roysue.runnable" , implements: [Runnable], fields: { bm: "android.graphics.Bitmap" , }, methods: { $init: [{ returnType: "void" , argumentTypes: ["android.graphics.Bitmap" ], implementation: function (bitmap) { this .bm.value = bitmap; } }], run: function () { var path = "/sdcard/Download/tmp/" + guid() + ".jpg" console.log("path=> " , path) var file = Java.use("java.io.File" ).$new (path) var fos = Java.use("java.io.FileOutputStream" ).$new (file); this .bm.value.compress(Java.use("android.graphics.Bitmap$CompressFormat" ).JPEG.value, 100 , fos) console.log("success!" ) fos.flush(); fos.close(); } } }); Java.use('android.graphics.BitmapFactory' ).decodeByteArray.overload('[B' , 'int' , 'int' , 'android.graphics.BitmapFactory$Options' ).implementation = function(data,offset,length,opts){ var result = this .decodeByteArray(data,offset,length,opts) var ByteString = Java.use("com.android.okhttp.okio.ByteString" ); console.log("data is coming!" ) var runable = saveImg.$new (result) runable.run() return result } }) }
下面这个脚本针对任意控件点触即输出所在类的脚本:
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 var jclazz = null ;var jobj = null ;function getObjClassName (obj ) { if (!jclazz) var jclazz = Java .use ("java.lang.Class" ); if (!jobj) var jobj = Java .use ("java.lang.Object" ); return jclazz.getName .call (jobj.getClass .call (obj)); } function watch (obj, mtdName ) { var listener_name = getObjClassName (obj); var target = Java .use (listener_name); if (!target || !mtdName in target) return ; target[mtdName].overloads .forEach (function (overload ) { overload.implementation = function ( ) { console .log ("[WatchEvent] " + mtdName + ": " + getObjClassName (this )) return this [mtdName].apply (this , arguments ); }; }) } function OnClickListener ( ) { Java .perform (function ( ) { Java .use ("android.view.View" ).setOnClickListener .implementation = function (listener ) { if (listener != null ) watch (listener, 'onClick' ); return this .setOnClickListener (listener); }; Java .choose ("android.view.View$ListenerInfo" , { onMatch : function (instance ) { instance = instance.mOnClickListener .value ; if (instance) { console .log ("mOnClickListener name is :" + getObjClassName (instance)); watch (instance, 'onClick' ); } }, onComplete : function ( ) { } }) }) } function OnTouchListener ( ) { Java .perform (function ( ) { Java .use ("android.view.View" ).setOnTouchListener .implementation = function (listener ) { if (listener != null ) watch (listener, 'onTouch' ); return this .setOnTouchListener (listener); }; Java .choose ("android.view.View$ListenerInfo" , { onMatch : function (instance ) { instance = instance.mOnTouchListener ; if (instance) { console .log ("mOnTouchListener name is :" + getObjClassName (instance)); watch (instance, 'onTouch' ); } }, onComplete : function ( ) { } }) }) } setImmediate (OnClickListener );
例如修改某个类的实例变量值:
1 2 3 4 5 6 7 8 9 10 11 12 function hookVIP ( ){ Java .perform (function ( ){ Java .choose ("com.ilulutv.fulao2.film.l" ,{ onMatch :function (ins ){ console .log ("found ins:=>" ,ins) ins.q0 .value = true ; },onComplete :function ( ){ console .log ("search completed!" ) } }) }) }
Frida SO 入门 通过模块名获取Module如下,此外还可用findModuleByAddress
通过地址获取Module.
1 2 3 var module =Process .findModuleByName ("libxxx.so" );if (module !=null ) console .log (JSON .stringify (module ));
Process常用方法:
1 2 3 4 5 6 7 8 9 10 console .log ("pid:" ,Process .id );console .log ("arch:" ,Process .arch );console .log ("platform:" ,Process .platform );console .log ("pageSize:" ,Process .pageSize ); console .log ("pointerSize:" ,Process .pointerSize ); console .log ("CurrentThreadId:" ,Process .getCurrentThreadId ());var soAddr=Process .findModuleByName ("libxxx.so" ).base ;console .log ("soAddr:" ,soAddr);var range=Process .findRangeByAddress (Process .findModuelByName ("libxxx.so" ).base );console .log ("Range:" ,JSON .stringify (range));
枚举导入表:
1 2 3 4 5 6 7 8 9 10 var imports=Process .findModuleByName ("libxxx.so" ).enumerateImports ();var sprintf_addr=null ;for (let i=0 ;i<imports.length ;i++){ let _import=imports[i]; if (_import.name .indexOf ("sprintf" )!=-1 ){ sprintf_addr=_import.address ; break ; } } console .log ("sprintf_addr:" ,sprintf_addr);
枚举导出表:
1 2 3 4 5 6 7 8 9 10 var exports =Process .findModuleByName ("libxxx.so" ).enumerateExports ();var MD5Final _addr=null ;for (let i=0 ;i<exports .length ;i++){ let _export=exports [i]; if (_export.name .indexOf ("_Z8MD5FinalP7MD5_CTXPh" )!=-1 ){ MD5Final _addr=_exports.address ; break ; } } console .log ("MD5Final_addr:" ,MD5Final _addr);
枚举符号表:
1 2 3 4 5 6 7 8 var symbols=Process .findModuleByName ("libxxx.so" ).enumerateSymbols ();var RegisterNatives _addr=null ;for (let i=0 ;i<symbols.length ;i++){ var symbol=symbols[i]; if (symbol.name .indexOf ("CheckJNI" )==-1 &&symbol.name .indexOf ("RegisterNatives" )!=-1 ) RegisterNatives _addr=symbol.address ; } console .log ("RegisterNatives_addr:" ,RegisterNatives _addr);
获取JNIEnv*
指针变量的内存地址:
1 2 3 4 var env=Java .vm .tryGetEnv ();console .log (hexdump (env.handle .readPointer )); console .log (hexdump (Memory .readPointer (env)));console .log (hexdump (ptr (env).readPointer ()));
打印系统函数栈:
1 2 3 console .log (Thread .backtrace (this .context ,Backtracer .ACCURATE )); console .log (Thread .backtrace (this .context ,Backtracer .FUZZY )); console .log (DebugSymbol .fromAddress (ptr (0x79ca9793a8 )).toString ());
调试符号类:
1 2 3 4 5 6 7 8 9 10 var debsym=DebugSymbol .fromName ("strcat" );console .log ("address:" ,debsym.address );console .log ("name:" ,debsym.name );console .log ("moduleName:" ,debsym.moduleName );console .log ("fileName:" ,debsym.fileName );console .log ("lineNumber:" ,debsym.lineNumber );console .log ("toString:" ,debsym.toString ());console .log ("getFunctionByName:" ,DebugSymbol .getFunctionByName ("strcat" ));console .log ("findFunctionsNamed:" ,DebugSymbol .findFunctionsNamed ("JNI_OnLoad" ));console .log ("findFunctionMatching:" ,DebugSymbol .findFunctionsMatching ("JNI_OnLoad" ));
修改内存权限:
1 2 var soAddr=Module .findBaseAddress ("libxxx.so" );Memory .protect (soAddr.add (0x3DED ),16 ,'rwx' )
内存读写:
1 2 3 4 5 var addr=Memory .alloc (8 );addr.writeByteArray (hexToBytes ("0123456789abcdef" )); console .log (addr.readByteArray (8 ));var addr=Memory .allocUtf8String ("嗨嗨嗨" );console .log (addr.readByteArrary (16 ));
有时应用队SO文件加固,需要dump内存,这里将SO函数dump到文件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function dump_so (so_name ){ Java .perform (function ( ){ var module =Process .getModuleByName (so_name); console .log ("[nane]:" ,module .name ); console .log ("[base]:" ,module .base ); console .log ("[size]:" ,module .size ); console .log ("[path]:" ,module .path ); var currentApplication=Java .use ("android.app.ActivityThread" ).currentApplication (); var dir=currentApplication.getApplicationContext ().getFilesDir ().getPath (); var path=dir+"/" +module .name +"_" +module .base +"_" +module .size +".so" ; var file=new File (path,"wb" ); if (file){ Memory .protect (module .base ,module .size ,'rwx' ); var buffer=module .base .readByteArray (module .size ); file.write (buffer); file.flush (); file.close (); console .log ("[dump]:" ,path); } }); } dump_so ("libxxx.so" );
代码跟踪引擎Stalker:
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 var md5Addr=Module .getExportByName ("libxxx.so" ,"Java_com_xxx_ndk_NativeHelper_md5" );Interceptor .attach (md5Addr,{ onEnter :function ( ){ this .tid =Process .getCurrentThreadId (); Stalker .follow (this .tid ,{ events :{ call :true , }, onReceive (events ){ var _events=Stalker .parse (events); for (var i=0 ;i<_events.length ;i++){ var addr1=_events[i][1 ]; var module1=Process .findModuleByAddress (addr1); if (module1&&module .name =="libxxx.so" ){ var addr2=_events[i][2 ]; var module2=Process .findModuleByAddress (addr2); console .log (module1.name ,addr1.sub (module1.base ),module2.name ,addr2.sub (module2.base )); } } }, }); }, onLeave :function ( ){ Stalker .unfollow (this .tid ); } }); var md5Addr=Module .getExportByName ("libxxx.so" ,"Java_com_xxx_ndk_NativeHelper_md5" );Interceptor .attach (md5Addr,{ onEnter :function ( ){ this .tid =Process .getCurrentThreadId (); Stalker .follow (this .tid ,{ events :{ call :true , }, onCallSummary (summary ){ for (const addr in summary){ var module =Process .findModuleByAddress (addr); if (module &&module .name =="libxxx.so" ){ const num=summary[addr]; console .log (module .name ,ptr (addr).sub (module .base ),num); } } }, }); }, onLeave :function ( ){ Stalker .unfollow (this .tid ); } });
Native Hook 在Java数据类型和JNI数据类型对应中,JNI数据类型为“j+Java数据类型小写”。例如Java下的String、Object对应JNI下的jstring、jobject。
在Objection下:
1 2 memory list modules #列出进程中模块、加载地址、文件大小、存储目录 memory list exports lib*.so #查看某模块所有导出符号
在编写Native程序时,函数声明前若不加extern "C"
描述符,会被C++的名称粉碎机制而改变函数名,导致无法Java层无法找到相应native实现。例如可以恢复:
1 2 3 ┌──(root㉿computer)-[~] └─ Java_com_roysue_r0so_MainActivity_stringFromJNI2(_JNIEnv*, _jobject*)
一个模板如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function hook_native ( ){ var addr = Module .getExportByName ("libnative-lib.so" , "Java_com_roysue_r0so_MainActivity_stringFromJNI" ); Interceptor .attach (addr,{ onEnter :function (args ){ console .log ("jnienv pointer =>" ,args[0 ]) console .log ("jobj pointer =>" ,args[1 ]) },onLeave :function (retval ){ console .log ("retval is =>" ,Java .vm .getEnv ().getStringUtfChars (retval, null ).readCString ()) console .log ("=================" ) } }) } function main ( ){ hook_native () } setImmediate (main)
此外对于字符串类型的参数赋值方法为:
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 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 function stringToBytes (str ){ return hexToBytes (stringToHex (str)); } function stringToHex (str ){ return str.split ("" ).map (function (c ){ return ("0" +c.charCodeAt (0 ).toString (16 )).slice (-2 ); }).join ("" ); } function hexToBytes (hex ){ for (var bytes=[],c=0 ;c<hex.length ;c+=2 ) bytes.push (parseInt (hex.substr (c,2 ),16 )); return bytes; } function hexToString (hexStr ){ var hex=hexStr.toString (); var str='' ; for (var i=0 ;i<hex.length ;i+=2 ) str+=String .fromCharCode (parseInt (hex.substr (i,2 ),16 )); return str; } var MD5Update =Module .findExportByName ("libxxx.so" ,"_Z9MD5UpdateP7MD5_CTXPhj" );var strAddr=Module .findBaseAddress ("libxxx.so" ).add (0x3CFD );var newStr="xxx" ;var newStrAddr=Memory .allocUtf8String (newStr);Interceptor .attach (MD5Update ,{ onEnter :function (args ){ if (args[1 ].readCString ()=="xxx" ){ args[1 ]=strAddr; console .log (hexdump (args[1 ])); args[2 ]=ptr (strAddr.readCString ().length ); console .log (args[2 ].toInt32 ()); } if (args[1 ].readCString ()=="xxx" ){ let newStr="xxx\0" ; args[1 ].writeByteArray (stringToBytes (newStr)); console .log (hexdump (args[1 ])); args[2 ]=ptr (newStr.length -1 ); console .log (args[2 ].toInt32 ()); } if (args[1 ].readCString ()=="xxx" ){ args[1 ]=newStrAddr; console .log (hexdump (args[1 ])); args[2 ]=ptr (newStr.length ); console .log (args[2 ].toInt32 ()); } }, onLeave :function (retval ){ } })
Native层动态注册方法为:
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 #include <jni.h> #include <string> #include <android/log.h> #define TAG "r0so2" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO,TAG,__VA_ARGS__) #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,TAG,__VA_ARGS__) extern "C" JNIEXPORT jstring JNICALL Java_com_roysue_r0so_MainActivity_stringFromJNI (JNIEnv* env,jobject ) { std::string hello = "Hello from C++ r0ysue" ; return env->NewStringUTF (hello.c_str ()); } JNIEXPORT jstring JNICALL stringFromJNI3 (JNIEnv* env,jobject ) { std::string hello = "Hello from C++ stringFromJNI3 r0ysue " ; 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[] = {{"sI3" ,"()Ljava/lang/String;" ,(void *)stringFromJNI3}}; env->RegisterNatives (env->FindClass ("com/roysue/r0so/MainActivity" ),methods,1 ); return JNI_VERSION_1_6; }
Hook函数RegisterNatives
用https://github.com/lasting-yang/frida_hook_libart/blob/master/hook_RegisterNatives.js ,需要在Spawn方法使用,可获取动态注册时相应函数地址,并写脚本对0xf444进行Hook:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function hook_native3 ( ){ var libnative_addr = Module .findBaseAddress ('libnative-lib.so' ); console .log ("libnative_addr is => " ,libnative_addr) var stringfromJNI3 = libnative_addr.add (0xf444 ); console .log ("stringfromJNI3 address is =>" ,stringfromJNI3); Interceptor .attach (stringfromJNI3,{ onEnter :function (args ){ console .log ("jnienv pointer =>" ,args[0 ]) console .log ("jobj pointer =>" ,args[1 ]) },onLeave :function (retval ){ console .log ("retval is =>" ,Java .vm .getEnv ().getStringUtfChars (retval, null ).readCString ()) console .log ("=================" ) } }) } function main ( ){ hook_native3 () } setImmediate (main)
Native层主动调用如下:
1 2 3 4 5 6 7 8 9 10 setImmediate (function ( ){ var method01_addr=Module .findExportByName ("libroysue.so" ,"Java_com_roysue_easysol_MainActivity_method01" ); console .log ("method01 address is=>" ,method01_addr); Java .perform (function ( ){ var jstring=Java .vm .getEnv ().newStringUtf ("roysue" ); var method01=new NativeFunction (method01_addr,"pointer" ,["pointer" ,"pointer" ,"pointer" ]); var result=method01 (Java .vm .getEnv (),jstring,jstring); console .log ("Final result is=>" ,Java .vm .getEnv ().getStringUtfChars (result,null ).readCString ()); }) })
对于Socket层的抓包,之前函数com.android.org.conscrypt.ConscryptFileDescriptorSocket$SSLOutputStream.write
在Native层的实现是boringssl模块中的SSL_write
函数。查看boringssl模块的Android.bp编译配置文件可知最终编译生成模块有libcrypto.so和libssl.so,此时查看这俩模块导出函数:
1 2 memory list exports libcrypto.so --json libcrypto.so.json memory list exports libssl.so --json libssl.so.json
吐出libssl.so中SSL_write
的接收到的数据包信息脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 function hook_ssl_write ( ){ var addr = Module .getExportByName ("libssl.so" , "SSL_write" ); Interceptor .attach (addr,{ onEnter :function (args ){ console .log ("\n" ,hexdump (args[1 ],{length : args[2 ].toInt32 ()})) },onLeave :function (retval ){ console .log ("================== onLeave =================" ) } }) } function main ( ){ console .log ("Entering main" ) hook_ssl_write () } setImmediate (main)
接下来trace一下libssl.so中所有符号函数:
1 frida-trace -UF -I libssl.so
Frida还可便利进程所有模块,和某模块所有导出符号,实现类似Objection的功能:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function traceNativeExports ( ){ var modules=Process .enumerateModules (); for (var i=0 ;i<modules.length ;i++){ var module =module [i]; if (module .name .indexOf ("libssl.so" )<0 ) continue ; var exports =module .enumerateExports (); for (var j=0 ;j<exports .length ;j++) console .log ("module name is=>" ,module .name ,"symbol name is=>" ,exports [j].name ,"address:" +exports [j].address ,"offset=>" ,(exports [j].address .sub (module .base ))); } } function traceNativeSymbols ( ){ var modules=Process .enumerateModules (); for (var i=0 ;i<modules.length ;i++){ var module =modules[i]; if (module .name .indexOf ("libssl.so" )<0 ) continue ; var exports =module .enumerateSymbols (); for (var j=0 ;j<exports .length ;j++) console .log ("module name is=>" ,module .name ,"symbol name is=>" ,exports [j].name ,"address:" +exports [j].address ,"offset=>" ,(exports [j].address .sub (module .base ))); } }
Frida还可以写文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function writeSomething (path, contents ) { var fopen_addr = Module .findExportByName ("libc.so" , "fopen" ); var fputs_addr = Module .findExportByName ("libc.so" , "fputs" ); var fclose_addr = Module .findExportByName ("libc.so" , "fclose" ); var fopen = new NativeFunction (fopen_addr, "pointer" , ["pointer" , "pointer" ]) var fputs = new NativeFunction (fputs_addr, "int" , ["pointer" , "pointer" ]) var fclose = new NativeFunction (fclose_addr, "int" , ["pointer" ]) var fileName = Memory .allocUtf8String (path); var mode = Memory .allocUtf8String ("a+" ); var fp = fopen (fileName, mode); var contentHello = Memory .allocUtf8String (contents); var ret = fputs (contentHello,fp) fclose (fp); }
然后再对所有函数追踪:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function attach (name,address ){ console .log ("attaching " ,name); Interceptor .attach (address,{ onEnter :function (args ){ console .log ("Entering => " ,name) },onLeave :function (retval ){} }) } function traceNativeExport ( ){ var modules = Process .enumerateModules (); for (var i = 0 ;i<modules.length ;i++){ var module = modules[i]; if (module .name .indexOf ("libssl.so" )<0 ) continue ; var exports = module .enumerateExports (); console .log ('module.addr' ,module .base ); for (var j = 0 ;j<exports .length ;j++){ if (exports [j].type == "function" ) attach (exports [j].name ,exports [j].address ) var path = "/data/data/com.roysue.httpurlconnectiondemo/" +module .name +".txt" writeSomething (path,"type: " +exports [j].type +" function name :" +exports [j].name +" address : " +exports [j].address +" offset => 0x" +(exports [j].address - (modules[i].base )).toString (16 )+"\n" ) } } }
Frida还可以反调试:
1 2 3 4 5 6 7 function replaceKill ( ){ var kill_addr=Module .findExportByName ("libc.so" ,"kill" ); Interceptor .replace (kill_addr,new NativeCallback (function (arg0,arg1 ){ console .log ("arg0=>" ,arg0); console .log ("arg1=>" ,arg1); },'int' ,['int' ,'int' ])) }
Frida集成了Capstone,方法为:
1 2 3 4 5 6 7 8 9 10 11 function dis (adress,number ){ for (var i=0 ;i<number;i++){ var ins=Instruction .parse (address); console .log ("address:" +address+"--dis:" +ins.toString ()); address=ins.next ; } } setImmediate (function ( ){ var stringFromJniaddr=Module .findExportByName ("libroysue.so" ,"Java_com_roysue_easysol_MainActivity_stringFromJNI" ); dis (stringFromJniaddr,10 ); })
Frida还可指令级修改:
1 2 3 4 5 6 7 8 9 10 11 12 var soAddr=Module .findBaseAddress ("libxxx.so" );new Arm64Writer (soAddr.add (0x1AEC )).putNop ();console .log (Instruction .parse (soAddr.add (0x1AEC )).toString ());var codeAddr=Module .findBaseAddress ("libxxx.so" ).add (0x1AF4 );Memory .patchCode (codeAddr,4 ,function (code ){ var writer=new Arm64Writer (code,{pc :codeAddr}); writer.putBytes (hexToBytes ("0001094B" )); writer.flush (); });
Frida还可对SO动态库中函数地址进行Hook:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 var base_address=Module .findBaseAddress ("libdebug.so" )if (base_address){ var func_offset=0x1F17C ; var addr_func=base_address.add (func_offset); var pfunc=new NativePointer (addr_func); Interceptor .attach (pfunc,{ onEnter :function (args ){ console .log ("call func with args:" +args[0 ],args[1 ]); }, onLeave :function (retval ){ console .log ("func return:" ,retval); } }) }
JNI函数静态注册可用Hook dlsym。静态注册时一开始没绑定so层函数。当Java层的native函数首次被调用,系统按规则构建出对应函数名,通过dlsym去每个so文件中找符号并绑定,之后便不再触发。
1 2 3 4 5 6 7 8 9 10 11 12 var dlsymAddr=Module .findExportsByName ("libdl.so" ,"dlsym" );Interceptor .attach (dlsymAddr,{ onEnter :function (args ){ this .args1 =args[1 ]; }, onLeave :function (retval ){ var module =Process .findModuleByAddress (retval); if (module ==null ) return ; console .log (this .args1 .readCString (),module .name ,retval,retval.sub (module .base )); } });
JNI动态注册时用Hook RegisterNatives即可,定义为:
1 2 3 4 5 6 jint (*RegisterNatives)(JNIEnv*,jclass,const JNINativeMethod*,jint);typedef struct { const char * name; const char * signature; void * fnPtr; }JNINativeMethod;
RegisterNatives Hook如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 var RegisterNativesAddr =null ;var _symbols=Process .findModuleByName ("libart.so" ).enumerateSymbols ();for (var i=0 ;i<_symbols.length ;i++){ var _symbol=_symbols[i]; if (_symbol.name .indexOf ("CheckJNI" )==-1 &&_symbol.name .indexOf ("RegisterNatives" )!=-1 ) RegisterNativesAddr =_symbols[i].address ; } Interceptor .attach (RegisterNativesAddr ,{ onEnter :function (args ){ var env=Java .vm .tryGetEnv (); var className=env.getClassName (args[1 ]); var methodCount=args[3 ].toInt32 (); for (let i=0 ;i<methodCount;i++){ var methodName=args[2 ].add (Process .pointerSize *3 *i).readPointer ().readCString (); var signature=args[2 ].add (Process .pointerSize *3 *i).add (Process .pointerSize ).readPointer ().readCString (); var fnPtr=args[2 ].add (Process .pointerSize *3 *i).add (Process .pointerSize *2 ).readPointer (); var module =Process .findModuleByAddress (fnPtr); console .log (className,methodName,signature,fnPtr,module .name ,fnPtr.sub (module .base )); } }, onLeave :function (retval ){ } });
jnitrace用法:
1 jnitrace -m attach -l libxxx.so com.xxx.xxx.android
有时若用Spawn方式注入,SO文件还未加载无法注入。用Attach模式时,SO文件可能已经加载且要Hook的函数已执行完毕。Hook dlopen方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function hook_func ( ){ var myInit=Module .findBaseAddress ("libxxx.so" ).add (0x1DE8 ); Interceptor .replace (myInit,new NativeCallback (function ( ){ console .log ("success" ); },"void" ,[])); } function hook_dlopen ( ){ var android_dlopen_ext=Module .findExportByName ("libdl.so" ,"android_dlopen_ext" ); Interceptor .attach (android_dlopen_ext,{ onEnter :function (args ){ var soPath=args[0 ].readCString (); if (soPath.indexOf ("libxxx.so" )!=-1 ) this .hook =true ; }, onLeave :function (retval ){ if (this .hook ) hook_func (); } }); } hook_dlopen ();
在dlopen
执行后调用JNI_OnLoad
,所以Hook JNI_OnLoad只需Hook dlopen,并在onLeave中处理即可。SO文件加载时,init
和init_array
在dlopen
执行过程中调用,Hookinit_array
时需要dlopen
中再找个Hook点,必须在该函数执行前SO文件加载完毕,且SO文件中init
和init_array
尚未被调用,这里选择linker64.so中的call_constructors
。Hookinit_array
方法为:
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 35 function hook_dlopen (addr,soName,callback ){ Interceptor .attach (addr,{ onEnter :function (args ){ var soPath=args[0 ].readCString (); if (soPath.indexOf (soName)!=-1 ) callback (); }, onLeave :function (retval ){ } }); } function hook_initarray ( ){ var xxxAddr=Module .findBaseAddress ("libxxx.so" ); var func_addr=xxxAddr.add (0x1D14 ); Interceptor .replace (func_addr,new NativeCallback (function ( ){ console .log ("replaced" ); },'void' ,[])); Interceptor .detachAll (); } function hook_call_constructors ( ){ var _symbols=Process .getModuleByName ("linker64" ).enumerateSymbols (); var call_constructors_addr=null ; for (let i=0 ;i<_symbols.length ;i++) if (_symbol.name .indexOf ("call_constructors" )!=-1 ) call_constructors_addr=_symbol.address ; Interceptor .attach (call_constructors_addr,{ onEnter :function (args ){ hook_initarray (); }, onLeave :function (retval ){ } }); } var android_dlopen_ext=Module .findExportByName ("libdl.so" ,"android_dlopen_ext" );hook_dlopen (android_dlopen_ext,"libxxx.so" ,hook_call_constructors);
Hook pthread_create,用于检查应用为哪些函数开启了线程。
1 2 3 4 5 6 7 8 9 10 11 var pthread_create_addr=Module .findExportByName ("libc.so" ,"pthread_create" );Interceptor .attach (pthread_create_addr,{ onEnter :function (args ){ console .log (args[0 ],args[1 ],args[2 ],args[3 ]); var Module =Process .findModuleByAddress (args[2 ]); if (Module !=null ) console .log (Module .name ,args[2 ].sub (Module .base )); }, onLeave :function (retval ){ } })
修改某块内存区域的访问权限后,当应用访问这块内存时,会触发Access-violation异常。此时设置异常处理回调函数,当异常触发时,回调函数可通过修改寄存器和内存让程序从异常中恢复,返回true则Frida立即恢复线程。这种方法只能修改内存页权限,想要监控指定字节还得用Unidbg。
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 function hook_dlopen (addr,soName,callback ){ Interceptor .attach (addr,{ onEnter :function (args ){ var soPath=args[0 ].readCString (); if (soPath.indexOf (soName)!=-1 ) this .hook =true ; }, onLeave :function (retval ){ if (this .hook ) callback (); } }); } function set_read_write_break ( ){ Process .setExceptionHandler (function (details ){ console .log (JSON .stringify (details,null ,2 )); console .log ("lr" ,DebugSymbol .fromAddress (details.context .lr )); console .log ("pc" ,DebugSymbol .fromAddress (details.context .pc )); Memory .protect (details.memory .address ,Process .pointerSize ,'rwx' ); console .log (Thread .backtrace (details.context ,Backtracer .ACCURATE ).map (DebugSymbol .fromAddress ).join ('\n' )+'\n' ); return true ; }); var addr=Module .findBaseAddress ("libxxx.so" ).add (0x3DED ); Memory .protect (addr,8 ,'---' ) } var android_dlopen_ext=Module .findExportByName ("libdl.so" ,"android_dlopen_ext" );hook_dlopen (android_dlopen_ext,"libxxx.so" ,set_read_write_break);
一个对常用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 function printAddr (addr ){ var module =Process .findRangeByAddress (addr); if (module !=null ) return hexdump (addr)+"\n" ; return ptr (addr)+"\n" ; } function hookAddr (funcAddr,paramsNum ){ var module =Process .findModuleByAddress (funcAddr); Interceptor .attach (funcAddr,{ onEnter :function (args ){ this .logs =[]; this .params =[]; this .logs .push ("call " +module .name +"!" +ptr (funcAddr).sub (module .base )+"\n" ); for (let i=0 ;i<paramsNum;i++){ this .params ..push (args[1 ]); this .logs .push ("this.args" +i+"onEnter:" +printAddr (args[i])); } }, onLeave :function (retval ){ for (let i=0 ;i<paramsNum;i++) this .logs .push ("this.args" +i+"onLeave:" +printAddr (this .params [i])); this .logs .push ("retval onLeave:" +printAddr (retval)+"\n" ); console .log (this .logs ); } }); } var soAddr=Module .findBaseAddress ("libxxx.so" );hookAddr (soAddr.add (0x1ACC ),5 );