安卓逆向入门-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 36 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()
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
逆向工作思路 找到当前正在运行的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 2 3 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!" ) } }) }) }
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 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)
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还可对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); } }) }
安全加固 ProGuard build.gradle中添加:
1 2 3 4 5 6 buildTypes{ release{ minifyEnabled true proguardFiles getDefaultProguardFile('proguard-optimize.txt' ),'proguard-rules.pro' } }
签名校验 上文说PMS返回值可能被替换,这里直接绕过系统API,直接读取data/app/包名/base.apk中签名文件,获取原始签名信息。系统PackageParser服务对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 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 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 package com.verify.signature;import android.content.Context;import android.content.DialogInterface;import android.os.Bundle;import androidx.appcompat.app.AlertDialog;import androidx.appcompat.app.AppCompatActivity;import android.util.Log;import android.view.View;import com.verify.signature.databinding.ActivityMainBinding;public class MainActivity extends AppCompatActivity { private ActivityMainBinding binding; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); binding = ActivityMainBinding.inflate(getLayoutInflater()); setContentView(binding.getRoot()); Context context = getApplicationContext(); Log.i("Current sign ===>" , Tools.getAppSignature(context)); Log.i("Reflect sign ===>" , Tools.getReflectSignature(context)); Log.i("Native sign ===>" , Tools.getSignByJni(context)); binding.fab.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View view) { Tools.getSignatureFileCrc(context); if (!Tools.checkSignature(context)){ new AlertDialog .Builder(MainActivity.this ).setTitle("安全提醒" ) .setMessage("当前使用的客户端为非官方版本,存在极大的安全隐患。为了您的财产安全请在官方渠道下载重新安装!" ) .setPositiveButton("确定" , new DialogInterface .OnClickListener() { @Override public void onClick (DialogInterface dialog, int which) { System.exit(0 ); } }).show(); } } }); } } package com.verify.signature;import android.content.Context;import android.content.pm.PackageInfo;import android.content.pm.PackageManager;import android.content.pm.Signature;import android.os.Build;import android.util.Log;import java.io.File;import java.lang.reflect.Constructor;import java.lang.reflect.Field;import java.lang.reflect.Method;import java.security.MessageDigest;import java.util.zip.ZipEntry;import java.util.zip.ZipFile;public class Tools { static { try { System.loadLibrary("test" ); } catch (Exception e) { e.printStackTrace(); } } public static native String getSignByJni (Context mContext) ; public static boolean checkSignature (Context context) { String sign = "8b34e97425e0e682e3a73bd55830fc28ce34a4e8" ; if (sign.equals(getReflectSignature(context))) return true ; else return false ; } public static String getSignatureFileCrc (Context context) { try { ZipFile zf; String path= context.getPackageManager().getApplicationInfo(context.getPackageName(), 0 ).publicSourceDir; zf = new ZipFile (path); ZipEntry ze = zf.getEntry("META-INF/MANIFEST.MF" ); String crcValue = String.valueOf(ze.getCrc()); Log.i("Test MANIFEST Crc ====>" ,crcValue); return crcValue; }catch (Exception e){ e.printStackTrace(); } return null ; } public static String getAppSignature (Context context) { try { return sha1(getSignature(context)); } catch (Exception e) {} return null ; } public static byte [] getSignature(Context context) { try { PackageInfo packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), PackageManager.GET_SIGNATURES); Signature[] signatures = packageInfo.signatures; if (signatures != null ) { Log.i("Test ===>" , signatures[0 ].toCharsString()); return signatures[0 ].toByteArray(); } } catch (Exception e) {} return null ; } public static String getReflectSignature (Context context) { try { String path= context.getPackageManager().getApplicationInfo(context.getPackageName(), 0 ).publicSourceDir; return sha1(getApkSignatureByReflect(path)); } catch (Exception e) {} return null ; } public static String sha1 (byte [] bytes) { try { MessageDigest md = MessageDigest.getInstance("SHA1" ); md.update(bytes); byte [] b = md.digest(); int i; StringBuilder sb = new StringBuilder (); for (byte value : b) { i = value; if (i < 0 ) i += 256 ; if (i < 16 ) sb.append("0" ); sb.append(Integer.toHexString(i)); } return sb.toString(); } catch (Exception e) { e.printStackTrace(); } return "" ; } public static byte [] getApkSignatureByReflect(String apkPath) { String fullPackageParserPath = "android.content.pm.PackageParser" ; try { Class packageParserClass = Class.forName(fullPackageParserPath); Constructor pkgParserConstructor = null ; Object pkgParserIns = null ; if (Build.VERSION.SDK_INT > 20 ){ pkgParserConstructor = packageParserClass.getConstructor(); pkgParserIns = pkgParserConstructor.newInstance(); Class[] args = {File.class, Integer.TYPE};; Method parsePackageMethod = packageParserClass.getDeclaredMethod("parsePackage" , args); Object[] valueArgs = new Object [2 ]; valueArgs[0 ] = new File (apkPath); valueArgs[1 ] = PackageManager.GET_SIGNATURES; Object parserPackage = parsePackageMethod.invoke(pkgParserIns, valueArgs); if (Build.VERSION.SDK_INT>=28 ){ Class[] typeArgs = {parserPackage.getClass(),Boolean.TYPE}; Method collectCertificatesMethod = packageParserClass.getDeclaredMethod("collectCertificates" , typeArgs); Object[] valueArgs2 = {parserPackage, Build.VERSION.SDK_INT>28 }; collectCertificatesMethod.invoke(pkgParserIns, valueArgs2); Field mSignatures = null ; if (Build.VERSION.SDK_INT>=29 ) mSignatures = parserPackage.getClass().getDeclaredField("mSigningDetails" ); else mSignatures = parserPackage.getClass().getDeclaredField("mSignatures" ); mSignatures.setAccessible(true ); Object mSigningDetails = mSignatures.get(parserPackage); Field infoField = mSigningDetails.getClass().getDeclaredField("signatures" ); infoField.setAccessible(true ); Signature[] info = (Signature[]) infoField.get(mSigningDetails); Log.i("ReflectSignature ===>" , info[0 ].toCharsString()); return info[0 ].toByteArray(); } Class[] typeArgs = {parserPackage.getClass(),Integer.TYPE}; Method collectCertificatesMethod = packageParserClass.getDeclaredMethod("collectCertificates" , typeArgs); Object[] valueArgs2 = {parserPackage, PackageManager.GET_SIGNATURES}; collectCertificatesMethod.invoke(pkgParserIns, valueArgs2); Field packageInfoFld = parserPackage.getClass().getDeclaredField("mSignatures" ); Signature[] info = (Signature[]) packageInfoFld.get(parserPackage); Log.i("ReflectSignature ===>" , info[0 ].toCharsString()); return info[0 ].toByteArray(); } } catch (Exception e) { e.printStackTrace(); } return null ; } }
JNI层:
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 #include <jni.h> #include <string.h> #include <android/log.h> #include <malloc.h> #define TAG "Test" #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__) const char *APP_SIGNATURE = "10645EA8A12BE7A2C04B1F81DF3B4D90" ;void ByteToHexStr (const char *source, char *dest, int sourceLen) { short i; char highByte, lowByte; for (i = 0 ; i < sourceLen; i++) { highByte = source[i] >> 4 ; lowByte = source[i] & 0x0f ; highByte += 0x30 ; if (highByte > 0x39 ) dest[i * 2 ] = highByte + 0x07 ; else dest[i * 2 ] = highByte; lowByte += 0x30 ; if (lowByte > 0x39 ) dest[i * 2 + 1 ] = lowByte + 0x07 ; else dest[i * 2 + 1 ] = lowByte; } } jstring sha1 (JNIEnv *env, jbyteArray source) { jclass classMessageDigest = env->FindClass ("java/security/MessageDigest" ); jmethodID midGetInstance = env->GetStaticMethodID (classMessageDigest, "getInstance" ,"(Ljava/lang/String;)Ljava/security/MessageDigest;" ); jobject objMessageDigest = env->CallStaticObjectMethod (classMessageDigest,midGetInstance,env->NewStringUTF ("sha1" )); jmethodID midUpdate = env->GetMethodID (classMessageDigest, "update" , "([B)V" ); env->CallVoidMethod (objMessageDigest, midUpdate, source); jmethodID midDigest = env->GetMethodID (classMessageDigest, "digest" , "()[B" ); jbyteArray objArraySign = (jbyteArray) env->CallObjectMethod (objMessageDigest, midDigest); jsize intArrayLength = env->GetArrayLength (objArraySign); jbyte *byte_array_elements = env->GetByteArrayElements (objArraySign, NULL ); size_t length = (size_t ) intArrayLength * 2 + 1 ; char *char_result = (char *) malloc (length); memset (char_result, 0 , length); ByteToHexStr ((const char *) byte_array_elements, char_result, intArrayLength); *(char_result + intArrayLength * 2 ) = '\0' ; jstring stringResult = env->NewStringUTF (char_result); env->ReleaseByteArrayElements (objArraySign, byte_array_elements, JNI_ABORT); free (char_result); env->DeleteLocalRef (classMessageDigest); env->DeleteLocalRef (objMessageDigest); return stringResult; } static jobject getContext (JNIEnv *env) { jobject application = NULL ; jclass activity_thread_clz = env->FindClass ("android/app/ActivityThread" ); if (activity_thread_clz != NULL ) { jmethodID currentApplication = env->GetStaticMethodID (activity_thread_clz, "currentApplication" , "()Landroid/app/Application;" ); if (currentApplication != NULL ) application = env->CallStaticObjectMethod (activity_thread_clz, currentApplication); env->DeleteLocalRef (activity_thread_clz); } return application; } JNIEXPORT jstring JNICALL getSignByJni (JNIEnv *env, jobject thiz, jobject context) { jclass cls = env->GetObjectClass (context); jmethodID mid = env->GetMethodID (cls, "getPackageManager" ,"()Landroid/content/pm/PackageManager;" ); jobject pm = env->CallObjectMethod (context, mid); mid = env->GetMethodID (cls, "getPackageName" , "()Ljava/lang/String;" ); jstring packageName = (jstring) env->CallObjectMethod (context, mid); cls = env->GetObjectClass (pm); mid = env->GetMethodID (cls, "getPackageInfo" ,"(Ljava/lang/String;I)Landroid/content/pm/PackageInfo;" ); jobject packageInfo = env->CallObjectMethod (pm, mid, packageName, 0x40 ); cls = env->GetObjectClass (packageInfo); jfieldID fid = env->GetFieldID (cls, "signatures" , "[Landroid/content/pm/Signature;" ); jobjectArray signatures = (jobjectArray) env->GetObjectField (packageInfo, fid); jobject signature = env->GetObjectArrayElement (signatures, 0 ); cls = env->GetObjectClass (signature); mid = env->GetMethodID (cls, "toByteArray" , "()[B" ); jbyteArray signatureByteArray = (jbyteArray) env->CallObjectMethod (signature, mid); return sha1 (env, signatureByteArray); }
SO防护 对不想对外暴露的函数进行隐藏:
1 2 3 __attribute__((visibility ("hidden" ))) int test (int a,int b) { return a+b; };
反调试 调试工具附加到目标进程时需要用ptrace
系统调用操作,以建立其与目标程序的跟踪关系。正常情况下每个进程同一时间只能被附加一次,若应用程序启动后fork一个子进程抢先一步附加到自身进程,则调试器将无法正常附加,达到调试目的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <dlfcn.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/syscall.h> #include <unistd.h> #include <sys/ioctl.h> #include <unistd.h> #include <sys/ptrace.h> #include <time.h> void check_debug_by_ptrace () { int pid = fork(); if (pid == 0 ) { int ppid = getppid (); if (ptrace (PTRACE_ATTACH, ppid, NULL , NULL ) == 0 ) { waitpid (ppid, NULL , 0 ); ptrace (PTRACE_CONT, NULL , NULL ); } } }
也可直接用PTRACE_TRACEME参数,在进程启动时主动请求被监控跟踪:
1 2 3 void anti_debug_by_ptrace () { ptrace (PTRACE_TRACEME,0 ,NULL ,NULL ); }
当攻击者使用调试器进行动态分析时,需要设置断点将应用挂起,此时调试器发出SIGTRAP信号。可以在应用中主动添加SIGTRAP信号监听代码:
1 2 3 4 5 6 7 8 9 void signal_handle (int sig) { exit (0 ); } void check_debug_by_signal () { long ret = (long )signal (SIGTRAP, signal_handle); raise (SIGTRAP); }
进程被附加后有些属性值被改变:
特征文件名
特征值
/proc/pid/status和/proc/pid/task/pid/status
进程status值中TracePid值非0时处于调试状态
/proc/pid/stat和/proc/pid/task/pid/stat
进程stat值中括号后跟随的第一个字母为t时处于调试状态
/proc/pid/wchan和/proc/pid/task/pid/wchan
进程wchan状态值为ptrace_stop时处于调试状态
提取特征值为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void check_debug_by_tracerpid () { char debug_file[56 ]={0 }; int state = 0 ; int pid = getpid (); sprintf (debug_file, "/proc/%d/status" , pid); FILE* fp = fopen (debug_file, "r" ); char line[1024 ]={0 }; while (fgets (line, 1024 , fp)){ if (strncmp (line, "TracerPid" , 9 ) == 0 ){ state = atoi (&line[10 ]); if (state != 0 ){ fclose (fp); exit (0 ); } break ; } memset (line, 0 , 1024 ); } fclose (fp); }
还可监测代码执行时间,可能出现单步调试现象:
1 2 3 4 5 6 7 8 9 10 void check_debug_by_time () { time_t start_time; time_t end_time; time (&start_time); time (&end_time); if (end_time - start_time > 10 ) exit (0 ); }
进行调试时需要开启并监听指定端口,若设备开启这些端口则说明在和调试器通信,处于调试状态:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 void check_debug_by_port () { char buff[BUFF_LEN]; char line[BUFF_LEN]; const char * dir = "/proc/net/tcp" ; FILE *fp = fopen (dir, "r" ); while (fgets (buff, BUFF_LEN, fp) != NULL ) if (strstr (buff, "5D8A" ) != NULL || strstr (buff, "6B0A" ) != NULL || strstr (buff, "6B0B" ) != NULL ) { fclose (fp); exit (0 ); } fclose (fp); FILE *fd = popen ("netstat -apn" , "r" ); while (fgets (line, sizeof (line), fd) != NULL ) if (strstr (line, "23946" ) != NULL || strstr (line, "27402" ) != NULL || strstr (line, "27403" ) != NULL ){ fclose (fd); exit (0 ); } pclose (fd); }
为防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 #include <stdio.h> #include <stdlib.h> #include <dlfcn.h> #include <sys/types.h> #include <sys/wait.h> #include <sys/syscall.h> #include <unistd.h> #include <sys/ioctl.h> #include <unistd.h> #include <sys/ptrace.h> void check_inject () { char pFilePath[32 ]; char pLibInfo[256 ]; char *pLibPath = NULL ; char *savePtr = NULL ; int pid = getpid (); sprintf (pFilePath, "/proc/%d/maps" , pid); FILE *fp = fopen (pFilePath, "r" ); while (fgets (pLibInfo, sizeof (pLibInfo), fp) != NULL ) { strtok_r (pLibInfo, " \t" , &savePtr); strtok_r (NULL , " \t" , &savePtr); strtok_r (NULL , " \t" , &savePtr); strtok_r (NULL , " \t" , &savePtr); strtok_r (NULL , " \t" , &savePtr); pLibPath = strtok_r (NULL , " \t" , &savePtr); if (pLibPath != NULL ) { if (check_block_list (pLibPath) == -1 ) { exit (0 ); } } memset (pLibInfo, 0 , 256 ); } fclose (fp); }
完整性校验 对应用安装包中文件计算CRC值,与原始CRC值对比,其中CRC值应保存到服务器中。
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 package com.test.integritycheck_android;import androidx.appcompat.app.AppCompatActivity;import android.content.Context;import android.os.Bundle;import android.util.Log;import android.view.View;import android.widget.Button;public class MainActivity extends AppCompatActivity { Button btn = null ; Context mContext = null ; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); mContext = getApplicationContext(); btn = findViewById(R.id.check); btn.setOnClickListener(new MyClick ()); } class MyClick implements View .OnClickListener{ @Override public void onClick (View view) { new Thread (new Runnable () { @Override public void run () { UtilTools.getFileCrc(mContext); UtilTools.getAllFileCrc(mContext); } }).start(); } } } package com.test.integritycheck_android;import android.content.Context;import android.util.Log;import java.util.Enumeration;import java.util.HashMap;import java.util.Map;import java.util.zip.ZipEntry;import java.util.zip.ZipFile;public class UtilTools { private static final String[] fileArray = {"META-INF/MANIFEST.MF" ,"classes.dex" ,"resources.arsc" ,"AndroidManifest.xml" }; public static Map getFileCrc (Context context) { Map result = new HashMap (); try { String path= context.getPackageManager().getApplicationInfo(context.getPackageName(), 0 ).publicSourceDir; ZipFile zf = new ZipFile (path); ZipEntry ze; for (String item:fileArray){ ze = zf.getEntry(item); if (ze != null ){ String crcValue = String.valueOf(ze.getCrc()); result.put(item, crcValue); Log.i(item+ " CRC ======>" ,crcValue); } } return result; }catch (Exception e){ e.printStackTrace(); } return null ; } public static Map getAllFileCrc (Context context) { Map result = new HashMap (); try { String path= context.getPackageManager().getApplicationInfo(context.getPackageName(), 0 ).publicSourceDir; ZipFile zf = new ZipFile (path); Enumeration<? extends ZipEntry > entries = zf.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); String crcValue = String.valueOf(entry.getCrc()); result.put(entry.getName(), crcValue); Log.i(entry.getName()+ " CRC ======>" ,crcValue); } return result; }catch (Exception e){ e.printStackTrace(); } return null ; } }