实用FRIDA进阶:内存漫游、hook anywhere、抓包:https://www.anquanke.com/post/id/197657
frida github 地址:https://github.com/frida/frida objection github:https://github.com/sensepost/objection objection pypi:https://pypi.org/project/objection/
本章中我们进一步介绍,大家在学习和工作中使用 Frida 的实际高频场景,比如:
动态查看 安卓应用程序 在当前内存中的状态, 指哪儿就能 hook 哪儿, 比如 脱壳, 还有使用 Frida 来自动化获取参数、返回值等数据, 主动调用 API 获取签名结果 sign 等。。。 最后介绍一些经常遇到的高频问题解决思路,希望可以切实地帮助到读者。
Objection 简单使用 Frida hook工具 --- objection 使用:https://blog.csdn.net/wang_624/article/details/115601098
更多命令行参数可以查看 cli.py 文件得到:https://github.com/sensepost/objection/blob/e7eb1d9b769edf6a98870c75a6d2a6123b7346fd/objection/console/cli.py
使用命令 pip install objection # 安装 objection --help # 查看帮助 help frida # 不知道当前命令的作用 进入objection后就在命令前加 help 会有提示
objection -g 包名 explore # 注入进程,如果objection没有找到进程,会以spwan方式启动进程 objection -N -h 192.168.1.3 -p 9999 -g 包名 explore # 指定ip和端口的连接
objection -N -h 192.168.1.3 -p 9999 -g 包名 explore --startup-command "android hooking watch class '包名.类名'"
objection -N -h 192.168.1.3 -p 9999 -g 包名 explore --startup-command "android hooking watch class_method '包名.类名.方法' --dump-args --dump-return --dump-backtrace"
android hooking list classes # 列出内存中所有的类 android hooking search classes 包名 # 在内存中所有已加载的类中搜索包含特定关键词的类 android hooking list class_methods 包名.类名 # 列出类的所有方法
android hooking watch class 包名.类名 # hook类的所有方法 android hooking watch class_method 包名.类名.方法 # 默认会Hook方法的所有重载
android hooking watch class_method 包名.类名.方法 "参数1,参数2"
android hooking watch class_method 包名.类名.方法 --dump-args --dump-return --dump-backtrace
jobs list # 查看 hook 的任务有多少个 jobs kill jobid # 把正在 hook 的任务关闭
android heap search instances 包名.类名 --fresh # 搜索堆中的实例 android heap execute 地址(hashcode的地址) 方法名 # 调用实例的方法
memory list modules # 枚举内存中所有模块 memory list exports 文件名.so # 枚举模块中所有导出函数
android hooking search classes okhttp3 android hooking watch class okhttp3.OkHttpClient --dump-args --dump-return android hooking watch class_method okhttp3.OkHttpClient.newCall --dump-args --dump-backtrace --dump-return
启动 adb forward tcp:27042 tcp:27042
frida -U -l js_okhttp.js -F com.cdsb.newsreader --no-pause frida -U -l okhttp_poker.js -F com.cdsb.newsreader --no-pause frida -U -l okhttp_poker.js -F com.huanqiu.news --no-pause frida -U -l frida_hook_js.js -f com.huanqiu.news --no-pause
objection -g com.app.name explore -P ~/objection/plugins objection -g com.cdsb.newsreader explore -P objection_plugins
python r0capture.py -U -f com.cdsb.newsreader -v python r0capture.py -U com.cdsb.newsreader -v -p cdsb.pcap objection 下所有命令 简单说明
( :https://www.cnblogs.com/ningskyer/articles/14611822.html )
! # 执行操作系统的命令(注意:不是在所连接device上执行命令) android # 执行指定的 Android 命令 clipboard monitor deoptimize # Force the VM to execute everything in the interpreter heap evaluate # 在 Java 类中执行 JavaScript 脚本。 execute # 在 Java 类中执行 方法。android heap execute 实例ID 实例方法 print search instances # 在当前Android heap上搜索类的实例。android heap search instances 类名 hooking generate class # A generic hook manager for Classes simple # Simple hooks for each Class method get current_activity # 获取当前 前景(foregrounded) activity list activities # 列出已经登记的 Activities class_loaders # 列出已经登记的 class loaders class_methods # 列出一个类上的可用的方法 classes # 列出当前载入的所有类 receivers # 列出已经登记的 BroadcastReceivers services # 列出已经登记的 Services search classes 关键字 # 搜索与名称匹配的Java类 methods 关键字 # 搜索与名称匹配的Java方法 set return_value # 设置一个方法的返回值。只支持布尔返回 watch class # Watches for invocations of all methods in a class class_method # Watches for invocations of a specific class method intent launch_activity # 使用Intent启动Activity类 launch_service # Launch a Service class using an Intent keystore clear # 清除 Android KeyStore list # 列出 Android KeyStore 中的条目 watch # 监视 Android KeyStore 的使用 proxyset # 为应用程序设置代理 root disable # 试图禁用 root 检测 simulate # 试图模拟已经 root 的环境 shell_exec # 执行shell命令 sslpinning disable # 尝试禁用 SSL pinning 在各种 Java libraries/classes ui FLAG_SECURE # Control FLAG_SECURE of the current Activity screenshot # 在当前 Activity 进行截图 cd # 改变当前工作目录 commandsclear # 清除当前会话命令的历史记录 history # 列出当前会话命令历史记录 save # 将在此会话中运行的所有惟一命令保存到一个文件中 env # 打印环境信息 evaluate # 执行 JavaScript。( Evaluate JavaScript within the agent ) exit # 退出 file cat # 打印文件内容 download # 下载一个文件 httpstart # Start's an HTTP server in the current working directory status # Get the status of the HTTP server stop # Stop's a running HTTP server upload # 上传一个文件 frida # 获取关于 frida 环境的信息 import # 从完整路径导入 frida 脚本并运行 ios 执行指定的 ios 命令 bundles cookies heap hooking info jailbreak keychain monitor nsurlcredentialstorage nsuserdefaults pasteboard plist sslpinning ui jobs kill # 结束一个任务。这个操作不会写在卸载或者退出当前脚本 list # 列出当前所有的任务 ls # 列出当前工作目录下的文件 memory dump all 文件名 # Dump 当前进程的整个内存 from_base 起始地址 字节数 文件 # 将(x)个字节的内存从基址转储到文件 list exports # List the exports of a module. (列出模块的导出) modules # List loaded modules in the current process. (列出当前进程中已加载的模块) search # 搜索模块。用法:memory search "<pattern eg: 41 41 41 ?? 41>" (--string) (--offsets-only) write # 将原始字节写入内存地址。小心使用! ping # ping agent pluginload # 载入插接 pwd # 打印当前工作目录 reconnect # 重新连接 device rm # 从 device 上删除文件 sqlite # sqlite 数据库命令 connect # 连接到SQLite数据库文件 ui alert # 显示警报消息,可选地指定要显示的消息。(目前iOS崩溃)
内存漫游 Frida 只是提供了各种 API 供我们调用,在此基础之上可以实现具体的功能,比如禁用证书绑定之类的脚本,就是使用 Frida 的各种 API 来组合编写而成。于是有大佬将各种常见、常用的功能整合进一个工具,供我们直接在命令行中使用,这个工具便是objection。objection 功能强大,命令众多,而且不用写一行代码,便可实现诸如内存搜索、类和模块搜索、方法hook打印参数返回值调用栈等常用功能,是一个非常方便的,逆向必备、内存漫游神器。
安装命令:pip3 install objection
objection 的界面及命令如图所示。
objection 是基于 frida 的命令行 hook 工具,可以让你不写代码, 敲几句命令就可以对 java 函数的高颗粒度 hook, 还支持 RPC 调用。
objection 目前只支持 Java层的 hook,但是 objection 有提供插件接口,可以自己写 frida 脚本去定义接口,
比如葫芦娃大佬的脱壳插件,实名推荐: https://github.com/hluwa/FRIDA-DEXDump
官方仓库: https://github.com/sensepost/objection
1.1 获取基本信息 首先介绍几个基本操作:
键入命令之后,回车执行; help: 不知道当前命令的效果是什么,在当前命令前加 help 比如:help env,回车之后会出现当前命令的解释信息; 按空格: 不知道输入什么就按空格,会有提示出来,上下选择之后再按空格选中,又会有新的提示出来; jobs: 作业系统很好用,建议一定要掌握,可以同时运行 多项 ( hook ) 作业; 简单使用 启动 Frida-server,并转发端口 ( adb forward tcp:27042 tcp:27042 ) 附加需要调试的 app,进入交互界面 ( objection -g [packageName] explore ) 连接逍遥模拟器,需要先进入模拟器所在目录,使用目录中 adb.exe 命令执行:adb.exe connect 127.0.0.1:21503
可以使用该 env 命令枚举与所讨论的应用程序相关的其他有趣目录:env
可以使用以下 file download 命令从远程文件系统中下载文件:file download [file] [outfile]
com.opera.mini.native on (samsung: 6.0.1) [usb] # file download fhash.dat fhash.dat Downloading /data/user/0/com.opera.mini.native/cache/fhash.dat to fhash.dat 可以列出 app 具有的所有avtivity:android hooking list activities
com.opera.mini.native on (samsung: 6.0.1) [usb] # android hooking list activities com.facebook.ads.AudienceNetworkActivity com.google.android.gms.ads.AdActivity com.google.android.gms.auth.api.signin.internal.SignInHubActivity com.google.android.gms.common.api.GoogleApiActivity com.opera.android.AssistActivity com.opera.android.MiniActivity com.opera.android.ads.AdmobIntentInterceptor com.opera.mini.android.Browser
Found 8 classes 启动指定 avtivity:android intent launch_activity [class_activity]
com.opera.mini.native on (samsung: 6.0.1) [usb] # android intent launch_activity com.facebook.ads.AudienceNetworkActivity Launching Activity: com.facebook.ads.AudienceNetworkActivity... RPC 调用命令:curl -s "http://127.0.0.1:8888/rpc/invoke/androidHookingListActivities"
$ curl -s "http://127.0.0.1:8888/rpc/invoke/androidHookingListActivities" ["com.reddit.frontpage.StartActivity","com.reddit.frontpage.IntroductionActivity", ... snip ...]
RPC调用执行脚本:
url -X POST -H "Content-Type: text/javascript" http://127.0.0.1:8888/script/runonce -d "@script.js"
$ cat script.js { send(Frida.version); }
[{"payload":"12.8.0","type":"send"}] RPC WIKI:https://github.com/sensepost/objection/wiki/API
API 介绍 以下只是写了一部分指令和功能, 详细的功能需要合理运用 空格 和 help
Memory 指令 memory list modules //枚举当前进程模块 memory list exports [lib_name] //查看指定模块的导出函数 memory list exports libart.so --json /root/libart.json //将结果保存到json文件中 memory search --string --offsets-only //搜索内存
android heap 指令 //堆内存中搜索指定类的实例, 可以获取该类的实例id search instances search instances com.xx.xx.class
//直接调用指定实例下的方法
android heap execute [ins_id] [func_name]
//自定义frida脚本, 执行实例的方法
android heap execute [ins_id]
android 指令 android root disable //尝试关闭app的root检测 android root simulate //尝试模拟root环境
android ui screenshot [image.png] //截图
android ui FLAG_SECURE false //设置FLAG_SECURE权限
内存漫游 android hooking list classes //列出内存中所有的类
//在内存中所有已加载的类中搜索包含特定关键词的类
android hooking search classes [search_name]
//在内存中所有已加载的方法中搜索包含特定关键词的方法
android hooking search methods [search_name]
//直接生成hook代码
android hooking generate simple [class_name]
hook 方式 /* hook指定方法, 如果有重载会hook所有重载,如果有疑问可以看 --dump-args : 打印参数 --dump-backtrace : 打印调用栈 --dump-return : 打印返回值 */ android hooking watch class_method com.xxx.xxx.methodName --dump-args --dump-backtrace --dump-return
//hook指定类, 会打印该类下的所有调用
android hooking watch class com.xxx.xxx
//设置返回值(只支持bool类型)
android hooking set return_value com.xxx.xxx.methodName false
Spawn 方式 Hook objection -g packageName explore --startup-command '[obejection_command]'
activity 和 service 操作 android hooking list activities //枚举activity android intent launch_activity [activity_class] //启动activity android hooking list services //枚举services android intent launch_service [services_class] //启动services
任务管理器 jobs list // 查看任务列表 jobs kill [task_id] // 关闭任务
关闭 app 的 ssl 校验 android sslpinning disable
监控系统剪贴板 // 获取Android剪贴板服务上的句柄并每5秒轮询一次用于数据。 // 如果发现新数据,与之前的调查不同,则该数据将被转储到屏幕上。 help android clipboard
执行命令行 help android shell_exec [command]
插件编写 : objection pluging:https://github.com/sensepost/objection/wiki/Plugins
不写一行代码探索应用行为 --- 使用 objection From:https://www.y4f.net/77651.html
这里拿 XCTF 的三个题目做演示,分别是mobile进阶区的第3题、第8题和第17题。
示例:以安卓 内置应用 "设置" 演示基本用法 在手机上启动 frida-server,并且点击启动 "设置" 图标,手机进入设置的界面,首先查看一下 "设置" 应用的包名。
7107 com.android.settings 13370 com.google.android.settings.intelligence 再使用 objection 注入 "设置" 应用。
启动 objection之后,会出现提示它的 logo,这时候不知道输入啥命令的话,可以按下空格,有提示的命令及其功能出来; 再按空格选中,又会有新的提示命令出来,这时候按回车就可以执行该命令,
见下图 2-2 执行的应用环境信息命令 env 和 frida-server 版本信息命令。
1.2 提取内存信息 1.2.1 查看内存中加载的库( memory list modules ) 运行命令 memory list modules,效果如下图2-3所示。内存中加载的库
1.2.2 查看库的导出函数 ( memory list exports libssl.so ) 运行命令 memory list exports libssl.so,效果如下图2-4所示。 libssl.so 库的导出函数
1.2.3 将结果保存到 json文件中 当结果太多,终端无法全部显示的时候,可以将结果导出到文件中,然后使用其他软件查看内容,见下图2-5。
Writing exports as json to /root/libart.json... Wrote exports to: /root/libart.json 使用 json 格式保存的 libart.so 的导出函数
1.2.4 提取整个(或部分)内存( memory dump all from_base ) 命令是 memory dump all from_base,这部分内容与下文脱壳部分有重叠,我们在脱壳部分介绍用法。
1.2.5 搜索整个内存( memory search --string --offsets-only ) 命令是 memory search --string --offsets-only,这部分也与下文脱壳部分有重叠,我们在脱壳部分详细介绍用法。
1.3 内存堆 (heap) 上的搜索与执行 1.3.1 在堆 (heap)上搜索实例 我们查看AOSP源码关于设置里显示系统设置的部分,发现存在着 DisplaySettings类,可以在堆上搜索是否存在着该类的实例。
首先在手机上点击进入 "显示" 设置,然后运行命令:android heap search instances com.android.settings.DisplaySettings
并得到相应的实例地址:
1.3.2 调用实例的方法 查看源码得知 com.android.settings.DisplaySettings类 有一个 getPreferenceScreenResId()方法,这样就可以直接调用该实例的 getPreferenceScreenResId()方法,
(后文也会介绍在objection中直接打印类的所有方法的命令)
用 excute 命令:android heap execute 0x2526 getPreferenceScreenResId
Handle 0x2526 is to class com.android.settings.DisplaySettings Executing method: getPreferenceScreenResId() 2132082764 可见结果被直接打印了出来。
1.3.3 在实例上执行 js 代码 也可以在找到的实例上直接编写 js 脚本,输入android heap evaluate 0x2526 命令后,会进入一个迷你编辑器环境,
输入 console.log("evaluate result:"+clazz.getPreferenceScreenResId()) 这串脚本, 按ESC退出编辑器,然后按回车,即会开始执行这串脚本,输出结果。
(The handle at 0x2526 will be available as the clazz variable.)
console.log("evaluate result:"+clazz.getPreferenceScreenResId())
JavaScript capture complete. Evaluating... Handle 0x2526 is to class com.android.settings.DisplaySettings evaluate result:2132082764 这个功能其实非常厉害,可以即时编写、出结果、即时调试自己的代码,不用再:编写→注入→操作→看结果→再调整,而是直接出结果。
1.4 启动 activity 或 service 1.4.1 直接启动 activity 直接上代码,想要进入显示设置,可以在任意界面直接运行以下代码进入显示设置:
(agent) Starting activity com.android.settings.DisplaySettings... (agent) Activity successfully asked to start. 1.4.2 查看当前可用的 activity 可以使用 android hooking list 命令来查看当前可用的 activities,然后使用上述命令进行调起。
com.android.settings.ActivityPicker com.android.settings.AirplaneModeVoiceActivity com.android.settings.AllowBindAppWidgetActivity com.android.settings.AppWidgetPickActivity com.android.settings.BandMode com.android.settings.ConfirmDeviceCredentialActivity com.android.settings.CredentialStorage com.android.settings.CryptKeeper$FadeToBlack com.android.settings.CryptKeeperConfirm$Blank com.android.settings.DeviceAdminAdd com.android.settings.DeviceAdminSettings com.android.settings.DisplaySettings com.android.settings.EncryptionInterstitial com.android.settings.FallbackHome com.android.settings.HelpTrampoline com.android.settings.LanguageSettings com.android.settings.MonitoringCertInfoActivity com.android.settings.RadioInfo com.android.settings.RegulatoryInfoDisplayActivity com.android.settings.RemoteBugreportActivity com.android.settings.RunningServices com.android.settings.SetFullBackupPassword com.android.settings.SetProfileOwner com.android.settings.Settings com.android.settings.Settings com.android.settings.Settings$AccessibilityDaltonizerSettingsActivity com.android.settings.Settings$AccessibilitySettingsActivity com.android.settings.Settings$AccountDashboardActivity com.android.settings.Settings$AccountSyncSettingsActivity com.android.settings.Settings$AdvancedAppsActivity 1.4.3 直接启动 service 也可以先使用 android hooking list services 查看可供开启的服务,
然后使用 android intent launch_service com.android.settings.bluetooth.BluetoothPairingService 命令来开启服务。
Frida hook anywhere 很多新手在学习 Frida 的时候,遇到的第一个问题就是:无法找到正确的类及子类,无法定位到实现功能的准确的方法,无法正确的构造参数、继而进入正确的重载。
这时候可以使用 Frida 进行动态调试,来确定以上具体的名称和写法,最后写出正确的hook代码。
2.1 objection(内存漫游) 2.1.1 列出内存中所有的类 执行命令:android hooking list classes
sun.util.logging.LoggingSupport sun.util.logging.LoggingSupport$1 sun.util.logging.LoggingSupport$2 sun.util.logging.PlatformLogger sun.util.logging.PlatformLogger$1 sun.util.logging.PlatformLogger$JavaLoggerProxy sun.util.logging.PlatformLogger$Level sun.util.logging.PlatformLogger$LoggerProxy void
Found 11885 classes 2.1.2 内存中搜索包含特定关键词的类 执行命令:android hooking search classes 关键字。在内存中所有已加载的类中搜索包含特定关键词的类。
示例( 搜索包含关键 display 的 类 ):android hooking search classes display
[Landroid.hardware.display.WifiDisplay; [Landroid.icu.impl.ICUCurrencyDisplayInfoProvider$ICUCurrencyDisplayInfo$CurrencySink$EntrypointTable; [Landroid.icu.impl.LocaleDisplayNamesImpl$CapitalizationContextUsage; [Landroid.icu.impl.LocaleDisplayNamesImpl$DataTableType; [Landroid.icu.number.NumberFormatter$DecimalSeparatorDisplay; [Landroid.icu.number.NumberFormatter$SignDisplay; [Landroid.icu.text.DisplayContext$Type; [Landroid.icu.text.DisplayContext; [Landroid.icu.text.LocaleDisplayNames$DialectHandling; [Landroid.view.Display$Mode; [Landroid.view.Display; android.app.Vr2dDisplayProperties android.hardware.display.AmbientBrightnessDayStats android.hardware.display.AmbientBrightnessDayStats$1 android.hardware.display.BrightnessChangeEvent com.android.settings.wfd.WifiDisplaySettings$SummaryProvider com.android.settings.wfd.WifiDisplaySettings$SummaryProvider$1 com.android.settingslib.display.BrightnessUtils com.android.settingslib.display.DisplayDensityUtils com.google.android.gles_jni.EGLDisplayImpl javax.microedition.khronos.egl.EGLDisplay
Found 144 classes 2.1.3 内存中搜索所有的方法 在内存中所有已加载的类的方法中搜索包含特定关键词的方法,上文中可以发现,内存中已加载的类就已经高达11885个了,那么他们的方法一定是类的个数的数倍,整个过程会相当庞大和耗时,见下图2-6。
内存中搜索所有的方法
2.1.4 列出指定类的所有方法 当搜索到了比较关心的类之后,就可以直接查看它有哪些方法,
比如:我们想要查看 com.android.settings.DisplaySettings 类 有哪些方法,就可以执行命令:android hooking list class_methods com.android.settings.DisplaySettings
private static java.util.List<com.android.settingslib.core.AbstractPreferenceController> com.android.settings.DisplaySettings.buildPreferenceControllers(android.content.Context,com.android.settingslib.core.lifecycle.Lifecycle) protected int com.android.settings.DisplaySettings.getPreferenceScreenResId() protected java.lang.String com.android.settings.DisplaySettings.getLogTag() protected java.util.List<com.android.settingslib.core.AbstractPreferenceController> com.android.settings.DisplaySettings.createPreferenceControllers(android.content.Context) public int com.android.settings.DisplaySettings.getHelpResource() public int com.android.settings.DisplaySettings.getMetricsCategory() static java.util.List com.android.settings.DisplaySettings.access$000(android.content.Context,com.android.settingslib.core.lifecycle.Lifecycle)
Found 7 method(s) 列出的方法与源码相比对之后,发现是一模一样的。
2.1.5 自动生成 hook 代码 上文中在列出类的方法时,还直接把参数也提供了,也就是说我们可以直接动手写 hook 了,既然上述写 hook 的要素已经全部都有了,objection 这个 "自动化" 工具,当然可以直接生成代码。
自动生成 hook 代码的命令:android hooking generate simple com.android.settings.DisplaySettings
Java.perform(function() { var clazz = Java.use('com.android.settings.DisplaySettings'); clazz.getHelpResource.implementation = function() {
//
return clazz.getHelpResource.apply(this, arguments);
}
});
Java.perform(function() { var clazz = Java.use('com.android.settings.DisplaySettings'); clazz.getLogTag.implementation = function() {
//
return clazz.getLogTag.apply(this, arguments);
}
});
Java.perform(function() { var clazz = Java.use('com.android.settings.DisplaySettings'); clazz.getPreferenceScreenResId.implementation = function() {
//
return clazz.getPreferenceScreenResId.apply(this, arguments);
}
}); 生成的代码大部分要素都有了,只是参数貌似没有填上,还是需要我们后续补充一些,看来还是无法做到完美。
2.2 objection(hook) 上述操作均是基于在内存中直接枚举搜索,已经可以获取到大量有用的静态信息,我们再来介绍几个方法,可以获取到执行时动态的信息,当然、同样地,不用写一行代码。
2.2.1 hook类的所有方法 我们以手机连接蓝牙耳机播放音乐为例,看看手机蓝牙接口的动态信息。
首先我们将手机连接上我的蓝牙耳机(一加蓝牙耳机OnePlus Bullets Wireless 2),并可以正常播放音乐; 然后我们按照上文的方法,搜索一下与蓝牙相关的类,搜到一个高度可疑的类:android.bluetooth.BluetoothDevice 运行命令,hook 这个类:# android hooking watch class android.bluetooth.BluetoothDevice
使用 jobs list 命令可以看到 objection 为我们创建的 Hooks 数为 57,也就是将 android.bluetooth.BluetoothDevice类 下的所有方法都 hook了。这时候我们在 设置→声音→媒体播放到上进行操作,在蓝牙耳机与“此设备”之间切换时,会命中这些hook之后,此时objection就会将方法打印出来,会将类似这样的信息“吐”出来:
com.android.settings on (google: 9) [usb] # (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getService() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.isConnected() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getService() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getAliasName() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getAlias() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getName() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.equals(java.lang.Object) (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getService() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.isConnected() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getService() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getAliasName() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getAlias() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getName() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.equals(java.lang.Object) (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.equals(java.lang.Object) (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getBatteryLevel() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.equals(java.lang.Object) (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getBatteryLevel() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.equals(java.lang.Object) (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getBondState() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getAliasName() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getAlias() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getName() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getBatteryLevel() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.equals(java.lang.Object) (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getBatteryLevel() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.equals(java.lang.Object) (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getBondState() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getAliasName() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getAlias() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getName() (agent) [h0u5g7uclo] Called android.bluetooth.BluetoothDevice.getService() 可以看到我们的切换操作,调用到了 android.bluetooth.BluetoothDevice 类中的多个方法。
2.2.2 hook 方法的参数、返回值、调用栈 在这些方法中,我们对哪些方法感兴趣,就可以查看哪些个方法的参数、返回值和调用栈,比如想看 getName()方法,则运行以下命令:# android hooking watch class_method android.bluetooth.BluetoothDevice.getName --dump-args --dump-return --dump-backtrace
注意最后加上的三个选项 --dump-args --dump-return --dump-backtrace,为我们成功打印出来了我们想要看的信息,其实返回值 Return Value 就是 getName()方法的返回值,我的蓝牙耳机的型号名字 OnePlus Bullets Wireless 2;从调用栈可以反查如何一步一步调用到 getName()这个方法的;虽然这个方法没有参数,大家可以再找个有参数的试一下。
2.2.3 hook 方法的所有重载 objection 的 help 中指出,在 hook 给出的单个方法的时候,会 hook 它的所有重载。
Command: android hooking watch class_method
Usage: android hooking watch class_method (optional: --dump-args) (optional: --dump-backtrace) (optional: --dump-return) Hooks a specified class method and reports on invocations, together with the number of arguments that method was called with. This command will also hook all of the methods available overloads unless a specificoverload is specified. If the --include-backtrace flag is provided, a full stack trace that lead to the methods invocation will also be dumped. This would aid in discovering who called the original method. Examples: android hooking watch class_method com.example.test.login android hooking watch class_method com.example.test.helper.executeQuery android hooking watch class_method com.example.test.helper.executeQuery "java.lang.String,java.lang.String" android hooking watch class_method com.example.test.helper.executeQuery --dump-backtrace android hooking watch class_method com.example.test.login --dump-args --dump-return 那我们可以用 File 类的构造器来试一下效果。 android hooking watch class_method java.io.File.$init --dump-args 可以看到 objection 为我们 hook 了 File 构造器的所有重载,一共是6个。在设置界面随意进出几个子设置界面,可以看到命中很多次该方法的不同重载,每次参数的值也都不同, 见下图。 方法重载的参数和值都不同 2.3 objection 插件 --- Wallbreaker 葫芦娃 github:https://github.com/hluwa?tab=repositories Wallbreaker:从内存里面进行 逆向 Wallbreaker 是一个有用的工具,用于实时分析 Java 堆,由frida提供支持。提供一些命令从内存中搜索对象或类,并精美地可视化目标的真实结构。 想知道真实的数据内容吗?项目清单?地图条目?想知道接口的实现吗?尝试一下!你所看到的就是你得到的! 使用方法:参看 github:https://github.com/hluwa/Wallbreaker 使用 Wallbreaker 快速分析 Java 类/对象结构 From:https://bbs.pediy.com/thread-260110.htm Wallbreaker 取自 wikipedia 上对《三体》"破壁者"的翻译。 wallbreaker 是一个超级懒人(我)为了减少编写重复性垃圾代码而产生的一个工具,主要作用是将内存中 Java 类或对象的结构数据进行可视化。 就像介个亚子: 应用场景 如何使用 目前我是比较喜欢以 objection 插件的形式来使用,本来我也想自己写交互式控制台,但我觉得 objection 已经写得挺好,直接上车就好了,所以暂时不打算自己实现了。 开发的时候就使用 ipython 或者写 testcase 调试。 安装 objection:pip3 install objection 下载 wallbreaker 到自己的插件目录:git clone https://github.com/hluwa/Wallbreaker ~/.objection/plugins/Wallbreaker 启动 frida-server,使用 -P 参数带着插件启动 objection:objection -g com.app.name explore -P ~/.objection/plugins 然后就可以愉快的使用 wallbreaker 的几个命令了: 搜索 类:plugin wallbreaker classsearch 根据给的 pattern 对所有类名进行匹配,列出匹配到的所有类名。[return all matched class] 搜索 类的实例对象:plugin wallbreaker objectsearch 根据类名搜索内存中已经被创建的实例,列出 handle 和 toString() 的结果。 [return all matched object-handle and toString] ClassDump,输出类的结构:plugin wallbreaker classdump [--fullname] 输出类的结构, 若加了 --fullname 参数,打印的数据中类名会带着完整的包名。 [ pretty print class structure: fields declare, static field value, methods declare. set --fullname to display package name of type name. ] ObjectDump:plugin wallbreaker objectdump [--fullname] [--as-class class-name] 在 ClassDump 的基础上,输出指定对象中的每个字段的数据。 [ pretty print object structure: fields declare and value, methods declare. set --fullname to display package name of type name; set --as-class to cast instance type(super class, not interface). if instance is a collection or map, dump all entries. ] DEMO:https://asciinema.org/a/XZf8yLWJylCKJfcaYzcKlNbIy 2.3 ZenTracer(hook) 前文中介绍的 objection 已经足够强大,优点是 hook 准确、粒度细。这里再推荐个好友自己写的批量 hook 查看调用轨迹的工具ZenTracer ( https://github.com/hluwa/ZenTracer ),可以更大范围地 hook,帮助读者辅助分析。 pyenv install 3.8.0 git clone https://github.com/hluwa/ZenTracer cd ZenTracer pyenv local 3.8.0 python -m pip install --upgrade pip pip install PyQt5 pip install frida-tools python ZenTracer.py 上述命令执行完毕之后,会出现一个 PyQt 画出来的界面,如图 2-10 所示。 点击 Action之后,会出现匹配模板(Match RegEx)和过滤模板(Black RegEx)。匹配就是包含的关键词,过滤就是不包含的关键词,见下图2-11。其代码实现就是 通过如下的代码实现,hook 出来的结果需要通过匹配模板进行匹配,并且筛选剔除掉过滤模板中的内容。 var matchRegEx = {MATCHREGEX}; var blackRegEx = {BLACKREGEX}; Java.enumerateLoadedClasses({ onMatch: function (aClass) { for (var index in matchRegEx) { // console.log(matchRegEx[index]); // 通过匹配模板进行匹配 if (match(matchRegEx[index], aClass)) { var is_black = false; for (var i in blackRegEx) { //如果也包含在过滤模板中,则剔除 if (match(blackRegEx[i], aClass)) { is_black = true; log(aClass + "' black by '" + blackRegEx[i] + "'"); break; } } if (is_black) { break; } log(aClass + "' match by '" + matchRegEx[index] + "'"); traceClass(aClass); } } },
onComplete: function () {
log("Complete.");
}
}); 通过下述代码实现的模糊匹配和精准匹配: function match(ex, text) { if (ex[1] == ':') { var mode = ex[0]; if (mode == 'E') { ex = ex.substr(2, ex.length - 2); return ex == text; } else if (mode == 'M') { ex = ex.substr(2, ex.length - 2); } else { log("Unknown match mode: " + mode + ", current support M(match) and E(equal)") } } return text.match(ex) } 通过下述代码实现的导入导出调用栈及观察结果: def export_onClick(self): jobfile = QFileDialog.getSaveFileName(self, 'export', '', 'json file(*.json)') if isinstance(jobfile, tuple): jobfile = jobfile[0] if not jobfile: return f = open(jobfile, 'w') export = {} export['match_regex'] = self.app.match_regex_list export['black_regex'] = self.app.black_regex_list tree = {} for tid in self.app.thread_map: tree[self.app.thread_map[tid]['list'][0].text()] = gen_tree(self.app.thread_map[tid]['list'][0]) export['tree'] = tree f.write(json.dumps(export)) f.close() def import_onClick(self): jobfile = QFileDialog.getOpenFileName(self, 'import', '', 'json file(*.json)') if isinstance(jobfile, tuple): jobfile = jobfile[0] if not jobfile: return f = open(jobfile, 'r') export = json.loads(f.read()) for regex in export['match_regex']: self.app.match_regex_list.append( regex), self.app.match_regex_dialog.setupList() for regex in export['black_regex']: self.app.black_regex_list.append( regex), self.app.black_regex_dialog.setupList() for t in export['tree']: tid = t[0: t.index(' - ')] tname = t[t.index(' - ') + 3:] for item in export['tree'][t]: put_tree(self.app, tid, tname, item) 示例 ( ZenTracer ):hook java.io.File类的所有方法 我们来完整的演示一遍,比如现在看java.io.File类的所有方法,我们可以这样操作,首先是精准匹配: 点击打开 "设置" 应用; 选择 Action → Match RegEx 输入E:java.io.File,点击add,然后关闭窗口 点击 Action → Start 可以看到 java.io.File 类的所有方法都被 hook 了,并且像 java.io.File.createTempFile 方法的所有重载也被 hook 了。 在 "设置" 应用上进行操作,打开几个子选项的界面之后,观察方法的参数和返回值; 导出 json 来观察方法的调用树,选择 File → Export json,导出为 tmp.json,使用 vscode 来 format Document 之后,效果如下: { "match_regex": [ "E:java.io.File" ], "black_regex": [], "tree": { "2 - main": [ { "clazz": "java.io.File", "method": "exists()", "args": [], "child": [], "retval": "false" }, { "clazz": "java.io.File", "method": "toString()", "args": [], "child": [ { "clazz": "java.io.File", "method": "getPath()", "args": [], "child": [], "retval": "/data/user/0/com.android.settings" } ], "retval": "/data/user/0/com.android.settings" }, { "clazz": "java.io.File", "method": "equals(java.lang.Object)", "args": [ "/data/user/0/com.android.settings" ], "child": [ { "clazz": "java.io.File", "method": "toString()", "args": [], "child": [ { "clazz": "java.io.File", "method": "getPath()", "args": [], "child": [], "retval": "/data/user/0/com.android.settings" } ], "retval": "/data/user/0/com.android.settings" }, { "clazz": "java.io.File", "method": "compareTo(java.io.File)", "args": [ "/data/user/0/com.android.settings" ], "child": [ { "clazz": "java.io.File", "method": "getPath()", "args": [], "child": [], "retval": "/data/user_de/0/com.android.settings" }, { "clazz": "java.io.File", "method": "getPath()", "args": [], "child": [], "retval": "/data/user/0/com.android.settings" } ], "retval": "48" } ], "retval": "false" }, 3. 点击 Action→Stop,再点击 Action→Clean,本次观察结束。 也可以使用模糊匹配模式,比如输入M:java.io.File之后,会将诸如 java.io.FileOutputStream 类的诸多方法也都 hook上,见下图2-14。 ZenTracer 的目前已知的缺点,无法打印调用栈,无法 hook 构造函数,也就是 $init。当然这些 “缺点” 无非也就是加几行代码的事情,整个工具非常不错,值得用于辅助分析。 Frida 用于抓包 我们拿到一个app,做的第一件事情往往是先抓包来看,它发送和接收了哪些数据。收包发包是一个app的命门,企业为用户服务过程中最为关键的步骤——注册、流量商品、游戏数据、点赞评论、下单抢票等行为,均通过收包发包来完成。如果对收包发包的数据没有校验,黑灰产业可以直接制作相应的协议刷工具,脱离app本身进行实质性业务操作,为企业和用户带来巨大的损失。 抓包方法 移动应用安全基础篇:APP抓包姿势总结:https://www.freebuf.com/column/207041.html Android硬核https抓包, 在多个app亲测ok, 自定义ssl也无用:https://www.52pojie.cn/thread-1405917-1-1.html 安卓应用层抓包通杀脚本:https://github.com/r0ysue/r0capture 安卓应用层协议/框架通杀抓包:实战篇:https://www.sohu.com/a/445865909_120045376 安卓应用层抓包通杀脚本发布:https://bbs.pediy.com/thread-264283.htm frida_ssl_logger:https://github.com/NetCapture/frida_ssl_logger 安卓强制代理。例如:proxydroid (代理机器人) :https://github.com/madeye/proxydroid apk下载:http://www.xfdown.com/soft/120355.html JustTrustMePlus:https://github.com/Mocha-L/JustTrustMePlus 自识别类名 自动化Hook JustTrustMe 升级版:https://bbs.pediy.com/thread-254114.htm 重点说下手机抓包 JustTrustMePlus 是升级版能够对抗 okhttp 混淆加密 对于某些 apk 无法使用 charles 抓包(我也不知道原因) 手机端抓包可以使用 httpCarry APP 抓包和微信小程序抓包-Charles 的精简使用教程:https://blog.csdn.net/liqing0013/article/details/83010531 解决安卓手机 charles 抓包网络请求 https 抓包证书认证不通过:https://www.cnblogs.com/wuxianyu/p/14271564.html Android 使用 Wireshark 抓包:https://github.com/lasting-yang/frida_bypass_ssl_example/tree/master/Android/android_wireshark_tls frida hook 住 okhttp 实现抓包:https://github.com/siyujie/OkHttpLogger-Frida ① 首先将 okhttpfind.dex 拷贝到 /data/local/tmp/ 目录下。执行命令启动 frida -U -l okhttp_poker.js -f com.example.demo --no-pause 可追加 -o [output filepath]保存到文件。注意: -f 参数是不管 app 启动没有,都直接启动 app,如果 app 已经启动,则可以使用 -F 参数,直接附加到 已经启动的 app 上 ( ) ② 调用函数开始执行 find() 要等完全启动并执行过网络请求后再进行调用 hold() 要等完全启动再进行调用 history() & resend() 只有可以重新发送的请求 坑、坑、坑、坑、坑、坑: https://github.com/siyujie/OkHttpLogger-Frida:这个使用 -F 参数可以成功抓到包。( okhtpfind.dex 内包含了 更改了包名的okio以及Gson ) https://bbs.pediy.com/thread-252129.htm:这个使用 -F 没法抓到包,使用 -f 参数可以抓到包。( 这个没有 更改包名 okio以及Gson ) 打印堆栈: function showStacks() { send(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new())); } 在 okhttp_poker.js 文件中的 printerRequest 函数中添加打印堆栈,就可以在打印抓包时候,把堆栈调用也打印出来 运行截图: 抓包结果: okhttp_poker.js function showStacks() { send(Java.use("android.util.Log").getStackTraceString(Java.use("java.lang.Exception").$new())); } function hook_okhttp3(classLoader) { Java.perform(function () { console.log("开始执行注入");
var ByteString = classLoader.use("com.android.okhttp.okio.ByteString");
console.log("com.android.okhttp.okio.ByteString ---> load");
var Buffer = classLoader.use("com.android.okhttp.okio.Buffer");
console.log("com.android.okhttp.okio.Buffer ---> load");
var Interceptor = classLoader.use("okhttp3.Interceptor");
console.log("okhttp3.Interceptor ---> load");
var MyInterceptor = Java.registerClass({
name: "okhttp3.MyInterceptor",
implements: [Interceptor],
methods: {
intercept: function (chain) {
var request = chain.request();
try {
console.log("#################################################################################")
showStacks();
console.log("\n")
console.log("MyInterceptor.intercept onEnter:", request, "\nrequest headers:\n", request.headers());
var requestBody = request.body();
var contentLength = requestBody ? requestBody.contentLength() : 0;
if (contentLength > 0) {
var BufferObj = Buffer.$new();
requestBody.writeTo(BufferObj);
try {
console.log("\nrequest body String:\n", BufferObj.readString(), "\n");
} catch (error) {
try {
console.log("\nrequest body ByteString:\n", ByteString.of(BufferObj.readByteArray()).hex(), "\n");
} catch (error) {
console.log("error 1:", error);
}
}
}
} catch (error) {
console.log("error 2:", error);
}
console.log("#################################################################################")
console.log("\n")
var response = chain.proceed(request);
try {
console.log("MyInterceptor.intercept onLeave:", response, "\nresponse headers:\n", response.headers());
var responseBody = response.body();
var contentLength = responseBody ? responseBody.contentLength() : 0;
if (contentLength > 0) {
console.log("\nresponsecontentLength:", contentLength, "responseBody:", responseBody, "\n");
var ContentType = response.headers().get("Content-Type");
console.log("ContentType:", ContentType);
if (ContentType.indexOf("video") == -1) {
if (ContentType.indexOf("application") == 0) {
var source = responseBody.source();
if (ContentType.indexOf("application/zip") != 0) {
try {
console.log("\nresponse.body StringClass\n", source.readUtf8(), "\n");
} catch (error) {
try {
console.log("\nresponse.body ByteString\n", source.readByteString().hex(), "\n");
} catch (error) {
console.log("error 4:", error);
}
}
}
}
}
}
} catch (error) {
console.log("error 3:", error);
}
console.log("#################################################################################")
console.log("\n")
return response;
}
}
});
var ArrayList = classLoader.use("java.util.ArrayList");
var OkHttpClient = classLoader.use("okhttp3.OkHttpClient");
console.log(OkHttpClient);
OkHttpClient.$init.overload('okhttp3.OkHttpClient$Builder').implementation = function (Builder) {
console.log("OkHttpClient.$init:", this, Java.cast(Builder.interceptors(), ArrayList));
this.$init(Builder);
};
var MyInterceptorObj = MyInterceptor.$new();
var Builder = classLoader.use("okhttp3.OkHttpClient$Builder");
console.log(Builder);
Builder.build.implementation = function () {
this.interceptors().clear();
//var MyInterceptorObj = MyInterceptor.$new();
this.interceptors().add(MyInterceptorObj);
var result = this.build();
return result;
};
Builder.addInterceptor.implementation = function (interceptor) {
this.interceptors().clear();
//var MyInterceptorObj = MyInterceptor.$new();
this.interceptors().add(MyInterceptorObj);
return this;
//return this.addInterceptor(interceptor);
};
console.log("hook_okhttp3...");
});
} Java.perform(function() { var application = Java.use("android.app.Application"); application.attach.overload('android.content.Context').implementation = function(context) { var result = this.attach(context); // 先执行原来的attach方法 var classloader = context.getClassLoader(); // 获取classloader Java.classFactory.loader = classloader; hook_okhttp3(Java.classFactory); } }); okhttp_poker.js /** 使用说明 首先将 okhttpfind.dex 拷贝到 /data/local/tmp/ 目录下 例:frida -U -l okhttp_poker.js -f com.example.demo --no-pause 接下来使用okhttp的所有请求将被拦截并打印出来; 扩展函数: find() 检查是否使用了Okhttp & 是否可能被混淆 & 寻找okhttp3关键类及函数 switchLoader("okhttp3.OkHttpClient") 参数:静态分析到的okhttpclient类名 hold() 开启HOOK拦截 history() 打印可重新发送的请求 resend(index) 重新发送请求 备注 : okhtpfind.dex 内包含了 更改了包名的okio以及Gson,以及Java写的寻找okhttp特征的代码。 okhttpfind.dex 源码链接 https://github.com/siyujie/okhttp_find 原理:由于所有使用的okhttp框架的App发出的请求都是通过RealCall.java发出的,那么我们可以hook此类拿到request和response, 也可以缓存下来每一个请求的call对象,进行再次请求,所以选择了此处进行hook。 */ var Cls_Call = "okhttp3.Call"; var Cls_CallBack = "okhttp3.Callback"; var Cls_OkHttpClient = "okhttp3.OkHttpClient"; var Cls_Request = "okhttp3.Request"; var Cls_Response = "okhttp3.Response"; var Cls_ResponseBody = "okhttp3.ResponseBody"; var Cls_okio_Buffer = "okio.Buffer"; var F_header_namesAndValues = "namesAndValues"; var F_req_body = "body"; var F_req_headers = "headers"; var F_req_method = "method"; var F_req_url = "url"; var F_rsp$builder_body = "body"; var F_rsp_body = "body"; var F_rsp_code = "code"; var F_rsp_headers = "headers"; var F_rsp_message = "message"; var F_rsp_request = "request"; var M_CallBack_onFailure = "onFailure"; var M_CallBack_onResponse = "onResponse"; var M_Call_enqueue = "enqueue"; var M_Call_execute = "execute"; var M_Call_request = "request"; var M_Client_newCall = "newCall"; var M_buffer_readByteArray = "readByteArray"; var M_contentType_charset = "charset"; var M_reqbody_contentLength = "contentLength"; var M_reqbody_contentType = "contentType"; var M_reqbody_writeTo = "writeTo"; var M_rsp$builder_build = "build"; var M_rspBody_contentLength = "contentLength"; var M_rspBody_contentType = "contentType"; var M_rspBody_create = "create"; var M_rspBody_source = "source"; var M_rsp_newBuilder = "newBuilder"; //---------------------------------- var JavaStringWapper = null; var JavaIntegerWapper = null; var JavaStringBufferWapper = null; var GsonWapper = null; var ListWapper = null; var ArrayListWapper = null; var ArraysWapper = null; var CharsetWapper = null; var CharacterWapper = null; var OkioByteStrngWapper = null; var OkioBufferWapper = null; var OkHttpClientWapper = null; var ResponseBodyWapper = null; var BufferWapper = null; var Utils = null; //---------------------------------- var CallCache = [] var hookedArray = [] var filterArray = ["JPG", "jpg", "PNG", "png", "WEBP", "webp", "JPEG", "jpeg", "GIF", "gif",".zip", ".data"] function buildNewResponse(responseObject) { var newResponse = null; Java.perform(function () { try { var logString = JavaStringBufferWapper.$new() logString.append("").append("\n");
logString.append("┌────────────────────────────────────────────────────────────────────────────────────────────────────────────────────").append("\n");
newResponse = printAll(responseObject, logString)
logString.append("└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────").append("\n");
logString.append("").append("\n");
console.log(logString)
} catch (error) {
console.log("printAll ERROR : " + error);
}
})
return newResponse;
} function printAll(responseObject, logString) { try { var request = getFieldValue(responseObject, F_rsp_request) printerRequest(request, logString) } catch (error) { console.log("print request error : ", error.stack) return responseObject; } var newResponse = printerResponse(responseObject, logString) return newResponse; } function printerRequest(request, logString) { var defChatset = CharsetWapper.forName("UTF-8") //URL var httpUrl = getFieldValue(request, F_req_url) logString.append("| URL: " + httpUrl).append("\n") logString.append("|").append("\n") logString.append("| Method: " + getFieldValue(request, F_req_method)).append("\n") logString.append("|").append("\n") var requestBody = getFieldValue(request, F_req_body); var hasRequestBody = true if (null == requestBody) { hasRequestBody = false } //Headers var requestHeaders = getFieldValue(request, F_req_headers) var headersList = headersToList(requestHeaders) var headersSize = getHeaderSize(headersList) logString.append("| Request Headers: ").append("" + headersSize).append("\n")
if (hasRequestBody) {
var requestBody = getWrapper(requestBody)
var contentType = requestBody[M_reqbody_contentType]()
if (null != contentType) {
logString.append("| ┌─" + "Content-Type: " + contentType).append("\n")
}
var contentLength = requestBody[M_reqbody_contentLength]()
if (contentLength != -1) {
var tag = headersSize == 0 ? "└─" : "┌─"
logString.append("| " + tag + "Content-Length: " + contentLength).append("\n")
}
}
if (headersSize == 0) {
logString.append("| no headers").append("\n")
}
for (var i = 0; i < headersSize; i++) {
var name = getHeaderName(headersList, i)
if (!JavaStringWapper.$new("Content-Type").equalsIgnoreCase(name) && !JavaStringWapper.$new("Content-Length").equalsIgnoreCase(name)) {
var value = getHeaderValue(headersList, i)
var tag = i == (headersSize - 1) ? "└─" : "┌─"
logString.append("| " + tag + name + ": " + value).append("\n")
}
}
var shielded = filterUrl(httpUrl.toString())
if (shielded) {
logString.append("|" + " File Request Body Omit.....").append("\n")
return;
}
logString.append("|").append("\n")
if (!hasRequestBody) {
logString.append("|" + "--> END ").append("\n")
} else if (bodyEncoded(headersList)) {
logString.append("|" + "--> END (encoded body omitted > bodyEncoded)").append("\n")
} else {
logString.append("| Request Body:").append("\n")
var buffer = BufferWapper.$new()
requestBodyM_reqbody_writeTo
var reqByteString = getByteString(buffer)
var charset = defChatset
var contentType = requestBody[M_reqbody_contentType]()
if (null != contentType) {
var appcharset = contentTypeM_contentType_charset;
if (null != appcharset) {
charset = appcharset;
}
}
//LOG Request Body
try {
if (isPlaintext(reqByteString)) {
logString.append(splitLine(readBufferString(reqByteString, charset), "| ")).append("\n")
logString.append("|").append("\n")
logString.append("|" + "--> END ").append("\n")
} else {
logString.append(splitLine(hexToUtf8(reqByteString.hex()), "| ")).append("\n")
logString.append("|").append("\n");
logString.append("|" + "--> END (binary body omitted -> isPlaintext)").append("\n")
}
} catch (error) {
logString.append(splitLine(hexToUtf8(reqByteString.hex()), "| ")).append("\n")
logString.append("|").append("\n");
logString.append("|" + "--> END (binary body omitted -> isPlaintext)").append("\n")
}
}
logString.append("|").append("\n");
} function printerResponse(response, logString) { var newResponse = null; try { var defChatset = CharsetWapper.forName("UTF-8") var request = getFieldValue(response, F_rsp_request)
var url = getFieldValue(request, F_req_url)
var shielded = filterUrl(url.toString())
if (shielded) {
logString.append("|" + " File Response Body Omit.....").append("\n")
return response;
}
//URL
logString.append("| URL: " + url).append("\n")
logString.append("|").append("\n")
logString.append("| Status Code: " + getFieldValue(response, F_rsp_code) + " / " + getFieldValue(response, F_rsp_message)).append("\n")
logString.append("|").append("\n")
var responseBodyObj = getFieldValue(response, F_rsp_body)
var responseBody = getWrapper(responseBodyObj)
var contentLength = responseBody[M_rspBody_contentLength]()
//Headers
var resp_headers = getFieldValue(response, F_rsp_headers)
var respHeadersList = headersToList(resp_headers)
var respHeaderSize = getHeaderSize(respHeadersList)
logString.append("| Response Headers: ").append("" + respHeaderSize).append("\n")
if (respHeaderSize == 0) {
logString.append("| no headers").append("\n")
}
for (var i = 0; i < respHeaderSize; i++) {
var tag = i == (respHeaderSize - 1) ? "└─" : "┌─"
logString.append("| " + tag + getHeaderName(respHeadersList, i) + ": " + getHeaderValue(respHeadersList, i)).append("\n")
}
//Body
var content = "";
var nobody = !hasBody(response, respHeadersList)
if (nobody) {
logString.append("| No Response Body : " + response).append("\n")
logString.append("|" + "<-- END HTTP").append("\n")
} else if (bodyEncoded(respHeadersList)) {
logString.append("|" + "<-- END HTTP (encoded body omitted)").append("\n")
} else {
logString.append("| ").append("\n");
logString.append("| Response Body:").append("\n")
var source = responseBody[M_rspBody_source]()
var rspByteString = getByteString(source)
var charset = defChatset
var contentType = responseBody[M_rspBody_contentType]()
if (null != contentType) {
var appcharset = contentTypeM_contentType_charset
if (null != appcharset) {
charset = appcharset
}
}
//newResponse
var mediaType = responseBody[M_rspBody_contentType]()
var newBody = null;
try {
newBody = ResponseBodyWapperM_rspBody_create)
} catch (error) {
newBody = ResponseBodyWapperM_rspBody_create)
}
var newBuilder = null;
if ("" == M_rsp_newBuilder) {
var ResponseBuilderClazz = response.class.getDeclaredClasses()[0]
newBuilder = Java.use(ResponseBuilderClazz.getName()).$new(response)
} else {
newBuilder = response[M_rsp_newBuilder]()
}
var bodyField = newBuilder.class.getDeclaredField(F_rsp$builder_body)
bodyField.setAccessible(true)
bodyField.set(newBuilder, newBody)
newResponse = newBuilder[M_rsp$builder_build]()
if (!isPlaintext(rspByteString)) {
logString.append("|" + "<-- END HTTP (binary body omitted)").append("\n");
}
if (contentLength != 0) {
try {
var content = readBufferString(rspByteString, charset)
logString.append(splitLine(content, "| ")).append("\n")
} catch (error) {
logString.append(splitLine(hexToUtf8(rspByteString.hex()), "| ")).append("\n")
}
logString.append("| ").append("\n");
}
logString.append("|" + "<-- END HTTP").append("\n");
}
} catch (error) {
logString.append("print response error : " + error).append("\n")
if (null == newResponse) {
return response;
}
}
return newResponse;
} /** hex to string */ function hexToUtf8(hex) { try { return decodeURIComponent('%' + hex.match(/.{1,2}/g).join('%')); } catch (error) { return "hex[" + hex + "]"; } } /** / function getFieldValue(object, fieldName) { var field = object.class.getDeclaredField(fieldName); field.setAccessible(true) var fieldValue = field.get(object) if (null == fieldValue) { return null; } var FieldClazz = Java.use(fieldValue.$className) var fieldValueWapper = Java.cast(fieldValue, FieldClazz) return fieldValueWapper } /* */ function getWrapper(javaobject) { return Java.cast(javaobject, Java.use(javaobject.$className)) } /** */ function headersToList(headers) { var gson = GsonWapper.$new() var namesAndValues = getFieldValue(headers, F_header_namesAndValues) var jsonString = gson.toJson(namesAndValues) var namesAndValuesList = Java.cast(gson.fromJson(jsonString, ListWapper.class), ListWapper) return namesAndValuesList; } function getHeaderSize(namesAndValuesList) { return namesAndValuesList.size() / 2 } function getHeaderName(namesAndValuesList, index) { return namesAndValuesList.get(index * 2) } function getHeaderValue(namesAndValuesList, index) { return namesAndValuesList.get((index * 2) + 1) } function getByHeader(namesAndValuesList, name) { var nameString = JavaStringWapper.$new(name) Java.perform(function () { var length = namesAndValuesList.size() var nameByList = ""; do { length -= 2; if (length < 0) { return null; } // console.log("namesAndValuesList: "+namesAndValuesList.$className) nameByList = namesAndValuesList.get(JavaIntegerWapper.valueOf(length).intValue()) } while (!nameString.equalsIgnoreCase(nameByList)); return namesAndValuesList.get(length + 1); })
} function bodyEncoded(namesAndValuesList) { if (null == namesAndValuesList) return false; var contentEncoding = getByHeader(namesAndValuesList, "Content-Encoding") var bodyEncoded = contentEncoding != null && !JavaStringWapper.$new("identity").equalsIgnoreCase(contentEncoding) return bodyEncoded } function hasBody(response, namesAndValuesList) { var request = getFieldValue(response, F_rsp_request) var m = getFieldValue(request, F_req_method); if (JavaStringWapper.$new("HEAD").equals(m)) { return false; } var Transfer_Encoding = ""; var respHeaderSize = getHeaderSize(namesAndValuesList) for (var i = 0; i < respHeaderSize; i++) { if (JavaStringWapper.$new("Transfer-Encoding").equals(getHeaderName(namesAndValuesList, i))) { Transfer_Encoding = getHeaderValue(namesAndValuesList, i); break } } var code = getFieldValue(response, F_rsp_code) if (((code >= 100 && code < 200) || code == 204 || code == 304) && response[M_rspBody_contentLength] == -1 && !JavaStringWapper.$new("chunked").equalsIgnoreCase(Transfer_Encoding) ) { return false; } return true; } function isPlaintext(byteString) { try { var bufferSize = byteString.size() var buffer = NewBuffer(byteString) for (var i = 0; i < 16; i++) { if (bufferSize == 0) { console.log("bufferSize == 0") break } var codePoint = buffer.readUtf8CodePoint() if (CharacterWapper.isISOControl(codePoint) && !CharacterWapper.isWhitespace(codePoint)) { return false; } } return true; } catch (error) { // console.log(error) // console.log(Java.use("android.util.Log").getStackTraceString(error)) return false; } } function getByteString(buffer) { var bytearray = bufferM_buffer_readByteArray; var byteString = OkioByteStrngWapper.of(bytearray) return byteString; } function NewBuffer(byteString) { var buffer = OkioBufferWapper.$new() byteString.write(buffer) return buffer; } function readBufferString(byteString, chatset) { var byteArray = byteString.toByteArray(); var str = JavaStringWapper.$new(byteArray, chatset) return str; } function splitLine(string, tag) { var newSB = JavaStringBufferWapper.$new() var newString = JavaStringWapper.$new(string) var lineNum = Math.ceil(newString.length() / 150) for (var i = 0; i < lineNum; i++) { var start = i * 150; var end = (i + 1) * 150 newSB.append(tag) if (end > newString.length()) { newSB.append(newString.substring(start, newString.length())) } else { newSB.append(newString.substring(start, end)) } newSB.append("\n") } var lineStr = ""; if (newSB.length() > 0) { lineStr = newSB.deleteCharAt(newSB.length() - 1).toString() } return lineStr } /** * */ function alreadyHook(str) { for (var i = 0; i < hookedArray.length; i++) { if (str == hookedArray[i]) { return true; } } return false; } /** * */ function filterUrl(url) { for (var i = 0; i < filterArray.length; i++) { if (url.indexOf(filterArray[i]) != -1) { // console.log(url + " ?? " + filterArray[i]) return true; } } return false; } function hookRealCall(realCallClassName) { Java.perform(function () { console.log(" ........... hookRealCall : " + realCallClassName) var RealCall = Java.use(realCallClassName) if ("" != Cls_CallBack) { //异步 RealCall[M_Call_enqueue].overload(Cls_CallBack).implementation = function (callback) { // console.log("-------------------------------------HOOK SUCCESS 异步--------------------------------------------------") var realCallBack = Java.use(callback.$className) realCallBack[M_CallBack_onResponse].overload(Cls_Call,Cls_Response).implementation = function(call, response){ var newResponse = buildNewResponse(response) thisM_CallBack_onResponse } thisM_Call_enqueue realCallBack.$dispose } } //同步RealCall[M_Call_execute].overload().implementation = function () { // console.log("-------------------------------------HOOK SUCCESS 同步--------------------------------------------------") var response = thisM_Call_execute var newResponse = buildNewResponse(response) return newResponse; } }) } /** check className & filter */ function checkClass(name) { if (name.startsWith("com.") || name.startsWith("cn.") || name.startsWith("io.") || name.startsWith("org.") || name.startsWith("android") || name.startsWith("kotlin") || name.startsWith("[") || name.startsWith("java") || name.startsWith("sun.") || name.startsWith("net.") || name.indexOf(".") < 0 || name.startsWith("dalvik") ) { return false; } return true; } /** print request history */ function history() { Java.perform(function () { try { console.log("") console.log("History Size : " + CallCache.length) for (var i = 0; i < CallCache.length; i++) { var call = CallCache[i] if ("" != M_Call_request) { console.log("-----> index[" + i + "]" + " >> " + callM_Call_request) } else { console.log("-----> index[" + i + "]" + " ???? M_Call_execute = """) } console.log("") } console.log("") } catch (error) { console.log(error) } }) } /** resend request */ function resend(index) { Java.perform(function () { try { console.log("resend >> " + index) var call = CallCache[index] if ("" != M_Call_execute) { callM_Call_execute } else { console.log("M_Call_execute = null") } } catch (error) { console.log("Error : " + error) } }) } /** 开启HOOK拦截 */ function hold() { Java.perform(function () { // Utils = Java.use("com.singleman.okhttp.Utils") //Init common JavaStringWapper = Java.use("java.lang.String") JavaStringBufferWapper = Java.use("java.lang.StringBuilder") JavaIntegerWapper = Java.use("java.lang.Integer") GsonWapper = Java.use("com.singleman.gson.Gson") ListWapper = Java.use("java.util.List") ArraysWapper = Java.use("java.util.Arrays") ArrayListWapper = Java.use("java.util.ArrayList") CharsetWapper = Java.use("java.nio.charset.Charset") CharacterWapper = Java.use("java.lang.Character") OkioByteStrngWapper = Java.use("com.singleman.okio.ByteString")
OkioBufferWapper = Java.use("com.singleman.okio.Buffer")
//Init OKHTTP
OkHttpClientWapper = Java.use(Cls_OkHttpClient)
ResponseBodyWapper = Java.use(Cls_ResponseBody)
BufferWapper = Java.use(Cls_okio_Buffer)
//Start Hook
OkHttpClientWapper[M_Client_newCall].overload(Cls_Request).implementation = function (request) {
var call = thisM_Client_newCall
try {
CallCache.push(call["clone"]())
} catch (error) {
console.log("not fount clone method!")
}
var realCallClassName = call.$className
if (!alreadyHook(realCallClassName)) {
hookedArray.push(realCallClassName)
hookRealCall(realCallClassName)
}
return call;
}
}) } function switchLoader(clientName) { Java.perform(function () { if ("" != clientName) { try { var clz = Java.classFactory.loader.findClass(clientName) console.log("") console.log(">>>>>>>>>>>>> ", clz, " <<<<<<<<<<<<<<<<") } catch (error) { console.log(error) Java.enumerateClassLoaders({ onMatch: function (loader) { try { if (loader.findClass(clientName)) { Java.classFactory.loader = loader console.log("") console.log("Switch ClassLoader To : ", loader) console.log("") } } catch (error) { // console.log(error) } }, onComplete: function () { console.log("") console.log("Switch ClassLoader Complete !") console.log("") } }) } } Java.openClassFile("/data/local/tmp/okhttpfind.dex").load() }) } /** find & print used location */ function find() { Java.perform(function () { ArraysWapper = Java.use("java.util.Arrays") ArrayListWapper = Java.use("java.util.ArrayList") var isSupport = false; var clz_Protocol = null; try { var clazzNameList = Java.enumerateLoadedClassesSync() if (clazzNameList.length == 0) { console.log("ERROR >> [enumerateLoadedClasses] return null !!!!!!") return } for (var i = 0; i < clazzNameList.length; i++) { var name = clazzNameList[i] if (!checkClass(name)) { continue } try { var loadedClazz = Java.classFactory.loader.loadClass(name); if (loadedClazz.isEnum()) { var Protocol = Java.use(name); var toString = ArraysWapper.toString(Protocol.values()); if (toString.indexOf("http/1.0") != -1 && toString.indexOf("http/1.1") != -1 && toString.indexOf("spdy/3.1") != -1 && toString.indexOf("h2") != -1 ) { clz_Protocol = loadedClazz; break; } } } catch (error) { } } if (null == clz_Protocol) { console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~ 寻找okhttp特征失败,请确认是否使用okhttp ") return } //enum values >> Not to be confused with! var okhttp_pn = clz_Protocol.getPackage().getName(); var likelyOkHttpClient = okhttp_pn + ".OkHttpClient" try { var clz_okclient = Java.use(likelyOkHttpClient).class if (null != clz_okclient) { console.log("~~ 未 混 淆 (仅参考)") isSupport = true; } } catch (error) { console.log(" 被 混 淆 (仅参考)~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~") isSupport = true; } } catch (error) {
console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~未使用okhttp~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
isSupport = false;
}
if (!isSupport) {
console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~ 寻找okhttp特征失败,请确认是否使用okhttp ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
return
}
var likelyClazzList = ArrayListWapper.$new()
for (var i = 0; i < clazzNameList.length; i++) {
var name = clazzNameList[i]
if (!checkClass(name)) {
continue
}
try {
var loadedClazz = Java.classFactory.loader.loadClass(name);
likelyClazzList.add(loadedClazz)
} catch (error) {
}
}
console.log("likelyClazzList size :" + likelyClazzList.size())
if (likelyClazzList.size() == 0) {
console.log("Please make a network request and try again!")
}
console.log("")
console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Start Find~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
console.log("")
try {
var OkHttpFinder = Java.use("com.singleman.okhttp.OkHttpFinder")
OkHttpFinder.getInstance().findClassInit(likelyClazzList)
console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Find Result~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
var OkCompatClazz = Java.use("com.singleman.okhttp.OkCompat").class
var fields = OkCompatClazz.getDeclaredFields();
for (var i = 0; i < fields.length; i++) {
var field = fields[i]
field.setAccessible(true);
var name = field.getName()
var value = field.get(null)
console.log("var " + name + " = \"" + value + "\";")
}
console.log("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Find Complete~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
} catch (error) {
console.log(error)
//console.log(Java.use("android.util.Log").getStackTraceString(error))
}
}) } /** */ function main() { Java.perform(function () { Java.openClassFile("/data/local/tmp/okhttpfind.dex").load() var version = Java.use("com.singleman.SingleMan").class.getDeclaredField("version").get(null) console.log(""); console.log("------------------------- OkHttp Poker by SingleMan [" + version + "]------------------------------------"); console.log("API:") console.log(" >>> find() 检查是否使用了Okhttp & 是否可能被混淆 & 寻找okhttp3关键类及函数"); console.log(" >>> switchLoader("okhttp3.OkHttpClient") 参数:静态分析到的okhttpclient类名"); console.log(" >>> hold() 开启HOOK拦截"); console.log(" >>> history() 打印可重新发送的请求"); console.log(" >>> resend(index) 重新发送请求"); console.log("----------------------------------------------------------------------------------------"); })
} setImmediate(main) http 抓包 正常代理来抓 强制代理抓 JustTrustMe 来抓 JustTrustMePlus 来抓 frida_ssl_logger 来抓 r0capture 来抓 硬核 https 抓包 3.1 推荐抓包环境 由上所述,抓包是每一位安全工程师必须掌握的技能。而抓包一般又分为以下两种情形: 应用层:Http(s)协议抓包。 如果是抓应用层Http(s),推荐的专业工具是BurpSuite,如果只是想简单的抓包、用的舒服轻松,也可以使用花瓶(Charles)。 不推荐使用 fiddle,因为它无法导入客户端证书(p12、Client SSL Certificates),对于服务器校验客户端证书的情况无法 Bypass; 会话层:Socket 端口通信抓包。 如果是会话层抓包,则选择 tcpdump 和 WireShark 相组合的方式。 使用 jnettop 还可以实时查看流量走势和对方IP地址,更为直观和生动。 在手机上设置代理时,推荐使用 VPN 来将流量导出到抓包软件上,而不是通过给 WIFI 设置 HTTP 代理的方式。使用 VPN 可以同时抓到 Http(s) 和 Socket 的包,且不管其来自 Java层还是so层。我们常用的代理软件是老牌的 Postern,开 VPN 服务通过连接到开启 Socks5 服务端的抓包软件,将流量导出去。 当然有些应用会使用 System.getProperty("http.proxyHost")、System.getProperty("http.proxyPort"); 这两个API来查看当前系统是否挂了VPN,这时候只能用 Frida 或 Xposed 来 hook 这个接口、修改其返回值,或者重打包来 nop 掉。当然还有一种最为终极、最为强悍的方法,那就是制作路由器,抓所有过网卡的包。 制作路由器的方法也很简单,给笔记本电脑装 Kali Linux,eth0口插网线上网,wlan0口使用系统自带的热点功能,手机连上热点上网。史上最强,安卓应用是无法对抗的。 另外,曾经有人问我,像这样的一个场景如何抓包: 问:最近在分析手机搬家类软件的协议,不知道用什么去抓包,系统应用,不可卸载那种。搬家场景:两台手机打开搬家软件,一台会创建热点,另一台手机连接该热点后,通过搬家软件传输数据。求大佬指点抓包方法。 这个场景是有点难度的,我们把开热点的手机假设为A,连接热点的手机假设为B。另外准备一台抓包电脑,连接上A开的热点。在B上安装VPN软件Postern,服务器设置为抓包电脑,这样B应该可以正常连接到A,B的所有流量也是从抓包电脑走的,可以抓到所有的包。 在抓包的对抗上体现的也是两个原则,一是理解的越成熟思路越多,二是对抗的战场越深上层越无法防御。 3.2 Http(s) 多场景分析 从防护的强度来看,Https 的强度是远远大于 Http 的;从大型分布式 C/S 架构的设计来看,如果服务器数量非常多、app版本众多,app在实现Https的策略上通常会采取客户端校验服务器证书的策略,如果服务器数量比较少,全国就那么几台、且app版本较少、对app版本管控较为严格,app在实现Https的策略时会加上服务器校验客户端证书的策略。 接下来我们具体分析每一种情况。 3.2.1 Http 抓包 对于Http的抓包,只要在电脑的Charles上配置好Socks5服务器,手机上用Postern开启VPN连上电脑上的Charles的Socks5服务器,所有流量即可导出到Charles上。当然使用BurpSuite也是一样的道理。至于具体的操作步骤网上文档浩如烟海,读者可以自行取阅。 一般大型app、服务器数量非常多的,尤其还配置了多种CDN在全国范围、三网内进行内容分发和加速分发的,通常app里绝大多数内容都是走的Http。 当然他们会在最关键的业务上,比如用户登录时,配置Https协议,来保证最基本的安全。 3.2.2 Https客户端校验服务器 这时候我们抓 app 的 Http 流量的时候一切正常,图片、视频、音乐都直接下载和转储。 但是作为用户要登录的时候,就会发现抓包失败,这时候开启 Charles 的 SSL 抓包功能,手机浏览器输入Charles的证书下载地址chls.pro/ssl,下载证书并安装到手机中。 注意在高版本的安卓上,用户安装的证书并不会安装到系统根证书目录中去,需要root手机后将用户安装的证书移动到系统根证书目录中去,具体操作步骤网上非常多,这里不再赘述。 当 Charles 的证书安装到系统根目录中去之后,系统就会信任来自Charles的流量包了,我们的抓包过程就会回归正常。 当然,这里还是会有读者疑惑,为什么导入Charles的证书之后,app抓包就正常了呢? 应用层 Https 抓包的根本原理 这里我们就需要理解一下应用层 Https 抓包的根本原理, 见下图2-15(会话层Socket抓包并不是这个原理,后文会介绍Socket抓包的根本原理)。 有了 Charles 置于中间之后,本来 C/S 架构的通信过程会 “分裂” 为两个独立的通信过程,app本来验证的是服务器的证书,服务器的证书手机的根证书是认可的,直接内置的;但是分裂成两个独立的通信过程之后,app验证的是Charles的证书,它的证书手机根证书并不认可,它并不是由手机内置的权威根证书签发机构签发的,所以手机不认,然后app也不认;所以我们要把Charles的证书导入到手机根证书目录中去,这样手机就会认可,如果app没有进行额外的校验(比如在代码中对该证书进行校验,也就是SSL pinning系列API,这种情况下一小节具体阐述)的话,app也会直接认可接受。 3.3.3 Https服务器校验客户端 既然 app 客户端会校验服务器证书,那么服务器可不可能校验app客户端证书呢?答案是肯定的。 在许多业务非常聚焦并且当单一,比如行业应用、银行、公共交通、游戏等行业,C/S架构中服务器高度集中,对应用的版本控制非常严格,这时候就会在服务器上部署对app内置证书的校验代码。 上一小节中已经看到,单一通信已经分裂成两个互相独立的通信,这时候与服务器进行通信的已经不是app、而是Charles了,所以我们要将app中内置的证书导入到Charles中去。 这个操作通常需要完成两项内容: 找到证书文件 找到证书密码 找到证书文件很简单,一般 apk 进行解包,直接过滤搜索后缀名为 p12 的文件即可,一般常用的命令为 tree -NCfhl |grep -i p12,直接打印出 p12 文件的路径,当然也有一些 app 比较 “狡猾”,比如我们通过搜索 p12 没有搜到证书,然后看 jadx 反编译的源码得出它将证书伪装成 border_ks_19 文件,我们找到这个文件用 file 命令查看果然不是后缀名所显示的 png 格式,将其改成 p12 的后缀名尝试打开时要求输入密码,可见其确实是一个证书,见下图2-17。 想要拿到密码也很简单,一般在 jadx 反编译的代码中或者so库拖进IDA后可以看到硬编码的明文;也可以使用下面这一段脚本,直接打印出来,终于到了Frida派上用场的时候。 function hook_KeyStore_load() { Java.perform(function () { var StringClass = Java.use("java.lang.String"); var KeyStore = Java.use("java.security.KeyStore"); KeyStore.load.overload('java.security.KeyStore$LoadStoreParameter').implementation = function (arg0) { printStack("KeyStore.load1"); console.log("KeyStore.load1:", arg0); this.load(arg0); }; KeyStore.load.overload('java.io.InputStream', '[C').implementation = function (arg0, arg1) { printStack("KeyStore.load2"); console.log("KeyStore.load2:", arg0, arg1 ? StringClass.$new(arg1) : null); this.load(arg0, arg1); }; console.log("hook_KeyStore_load...");
});
} 打印出来的效果如下图2-18,直接将密码打印了出来。 当然其实也并不一定非要用 Frida,用 Xposed 也可以,只是 Xposed 很久不更新了,最近流行的大趋势是 Frida。 有了证书和密码之后,就可以将其导入到抓包软件中,在 Charles中是位于 Proxy→SSL Proxy Settings→Client Certificates→Add 添加新的证书,输入指定的域名或IP使用指定的证书即可,见下图2-19。 3.3 SSL Pinning Bypass 上文中我们还有一种情况没有分析,就是客户端并不会默认信任系统根证书目录中的证书,而是在代码里再加一层校验,这就是证书绑定机制——SSL pinning,如果这段代码的校验过不了,那么客户端还是会报证书错误。 Https客户端代码校验服务器证书 遇到这种情况的时候,我们一般有三种方式,当然目标是一样的,都是hook住这段校验的代码,使这段判断的机制失效即可。 方法1:hook 住 checkServerTrusted,将其所有重载都置空; function hook_ssl() { Java.perform(function() { var ClassName = "com.android.org.conscrypt.Platform"; var Platform = Java.use(ClassName); var targetMethod = "checkServerTrusted"; var len = Platform[targetMethod].overloads.length; console.log(len); for(var i = 0; i < len; ++i) { Platform[targetMethod].overloads[i].implementation = function () { console.log("class:", ClassName, "target:", targetMethod, " i:", i, arguments); //printStack(ClassName + "." + targetMethod); } } }); } 方法2:使用 objection,直接将 SSL pinning 给 disable 掉 android sslpinning disable 方法3:如果还有一些情况没有覆盖的话,可以来看看大佬的代码 目录 ObjectionUnpinningPlus 增加了 ObjectionUnpinning 没覆盖到的锁定场景.(objection) 使用方法1 attach : frida -U com.example.mennomorsink.webviewtest2 —no-pause -l hooks.js 使用方法2 spawn : python application.py com.example.mennomorsink.webviewtest2 更为详细使用方法:参考我的文章 Frida.Android.Practice(ssl unpinning) 实战ssl pinning bypass 章节 . ObjectionUnpinningPlus hook list: SSLcontext(ART only) okhttp webview XUtils(ART only) httpclientandroidlib JSSE network_security_config (android 7.0+) Apache Http client (support partly) OpenSSLSocketImpl TrustKit 应该可以覆盖到目前已知的所有种类的证书绑定了。 3.4 Socket 多场景分析 当我们在使用 Charles 进行抓包的时候,会发现针对某些 IP 的数据传输一直显示 CONNECT,无法 Complete,显示 Sending request body,并且数据包大小持续增长,这时候说明我们遇到了Socket端口通信。 Socket 端口通信运行在会话层,并不是应用层,Socket抓包的原理与应用层Http(s)有着显著的区别。准确的说,Http(s)抓包是真正的“中间人”抓包,而Socket抓包是在接口上进行转储;Http(s)抓包是明显的将一套C/S架构通信分裂成两套完整的通信过程,而Socket抓包是在接口上将发送与接收的内容存储下来,并不干扰其原本的通信过程。 对于安卓应用来说,Socket通信天生又分为两种Java层Socket通信和Native层Socket通信。 Java层:使用的是java.net.InetAddress、java.net.Socket、java.net.ServerSocket等类,与证书绑定的情形类似,也可能存在着自定义框架的Socket通信,这时候就需要具体情况具体分析,比如谷歌的protobuf框架等; Native层:一般使用的是C Socket API,一般hook住send()和recv()函数可以得到其发送和接受的内容 抓包方法分为三种,接口转储、驱动转储和路由转储: 接口转储:比如给outputStream.write下hook,把内容存下来看看,可能是经过压缩、或加密后的包,毕竟是二进制,一切皆有可能; 驱动转储:使用tcpdump将经过网口驱动时的数据包转储下来,再使用Wireshark进行分析; 路由转储:自己做个路由器,运行jnettop,观察实时进过的流量和IP,可以使用WireShark实时抓包,也可以使用tcpdump抓包后用WireShark分析。
