转载:实用 FRIDA 进阶 --- objection :内存漫游、hook anywhere、抓包
轉載:實用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和端口的連接# spawn啟動前就Hook objection -N -h 192.168.1.3 -p 9999 -g 包名 explore --startup-command "android hooking watch class '包名.類名'"# spawn啟動前就Hook 打印參數、返回值、函數調用棧 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方法的所有重載# 如果只需hook其中一個重載函數 指定參數類型 多個參數用逗號分隔 android hooking watch class_method 包名.類名.方法 "參數1,參數2"# hook方法的參數、返回值和調用棧(–dump-args: 顯示參數; --dump-return: 顯示返回值; --dump-backtrace: 顯示堆棧) android hooking watch class_method 包名.類名.方法 --dump-args --dump-return --dump-backtracejobs 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:27042frida -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-pauseobjection -g com.app.name explore -P ~/objection/plugins objection -g com.cdsb.newsreader explore -P objection_pluginspython r0capture.py -U -f com.cdsb.newsreader -v python r0capture.py -U com.cdsb.newsreader -v -p cdsb.pcap基于 frida 的 objection 及其插件 wallbreaker 命令列表
( :https://www.cnblogs.com/ningskyer/articles/14611822.html )
cd commandsclearhistorysave env evaluate exit filecatdownloaduploadhttp frida import iosbundlescookiesheaphookinginfojailbreakkeychainmonitornsurlcredentialstoragtensuserdefaultspasteboardplistsslpinningui jobskilllist ls memorylistexports xx.somodulessearchwritedumpallfrom_base androidclipboardmonitordeoptimizeheapevaluateexecuteprintfieldsmethodssearchinstances xxx --freshhookinggenerateclasssimplegetcurrent_activitysetreturn_valuelistactivitiesclass_loadersclass_methodsclassesreceiversservicessearchclassesmethodswatchclassclass_methodintentlaunching_activitylaunching_servicekeystorelistclearwatchproxysetrootdisablesimulateshell_execsslpinningdisableuiFLAG_SECUREscreenshot ping pluginload pwd reconnect rm sqliteconnect1. 內存漫游
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.BrowserFound 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 指令//堆內存中搜索指定類的實例, 可以獲取該類的實例idsearch 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 falseSpawn 方式 Hookobjection -g packageName explore --startup-command '[obejection_command]'activity 和 service 操作android hooking list activities //枚舉activityandroid intent launch_activity [activity_class] //啟動activityandroid hooking list services //枚舉servicesandroid 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,并且點擊啟動 "設置" 圖標,手機進入設置的界面,首先查看一下 "設置" 應用的包名。
# frida-ps -U|grep -i setting7107 com.android.settings 13370 com.google.android.settings.intelligence再使用 objection 注入 "設置"?應用。
# objection -g com.android.settings explore啟動?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。
# memory list exports libart.so --json /root/libart.json 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退出編輯器,然后按回車,即會開始執行這串腳本,輸出結果。
這個功能其實非常厲害,可以即時編寫、出結果、即時調試自己的代碼,不用再:編寫→注入→操作→看結果→再調整,而是直接出結果。
1.4 啟動 activity 或 service
1.4.1?直接啟動 activity
直接上代碼,想要進入顯示設置,可以在任意界面直接運行以下代碼進入顯示設置:
# android intent launch_activity com.android.settings.DisplaySettings (agent) Starting activity com.android.settings.DisplaySettings... (agent) Activity successfully asked to start.1.4.2?查看當前可用的 activity
可以使用 android hooking list 命令來查看當前可用的 activities,然后使用上述命令進行調起。
# android hooking list activitiescom.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$AdvancedAppsActivity1.4.3 直接啟動 service
也可以先使用 android hooking list services 查看可供開啟的服務,
然后使用 android intent launch_service com.android.settings.bluetooth.BluetoothPairingService 命令來開啟服務。
2. Frida hook anywhere
很多新手在學習 Frida 的時候,遇到的第一個問題就是:無法找到正確的類及子類,無法定位到實現功能的準確的方法,無法正確的構造參數、繼而進入正確的重載。
這時候可以使用 Frida 進行動態調試,來確定以上具體的名稱和寫法,最后寫出正確的hook代碼。
2.1 objection(內存漫游)
2.1.1 列出內存中所有的類
執行命令:android hooking list classes
# android hooking list classessun.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 voidFound 11885 classes2.1.2 內存中搜索包含特定關鍵詞的類
執行命令:android hooking search classes 關鍵字。在內存中所有已加載的類中搜索包含特定關鍵詞的類。
示例( 搜索包含關鍵 display 的 類?):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.EGLDisplayFound 144 classes2.1.3 內存中搜索所有的方法
在內存中所有已加載的類的方法中搜索包含特定關鍵詞的方法,上文中可以發現,內存中已加載的類就已經高達11885個了,那么他們的方法一定是類的個數的數倍,整個過程會相當龐大和耗時,見下圖2-6。
# android hooking search methods display內存中搜索所有的方法
2.1.4 列出指定類的所有方法
當搜索到了比較關心的類之后,就可以直接查看它有哪些方法,
比如:我們想要查看 com.android.settings.DisplaySettings 類 有哪些方法,就可以執行命令:android hooking list class_methods 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
# android hooking generate simple com.android.settings.DisplaySettingsJava.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 它的所有重載。
# help android hooking watch class_method Command: android hooking watch class_methodUsage: android hooking watch class_method <fully qualified class method> <optional overload>(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 specific overload 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.loginandroid 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-backtraceandroid 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?調試。
然后就可以愉快的使用?wallbreaker 的幾個命令了:
- 搜索 類:plugin wallbreaker classsearch <type-pattern>
? ? 根據給的 pattern 對所有類名進行匹配,列出匹配到的所有類名。[return all matched class] - 搜索 類的實例對象:plugin wallbreaker objectsearch <instance-class-name>
? ? 根據類名搜索內存中已經被創建的實例,列出?handle?和?toString()?的結果。?[return all matched object-handle and toString] - ClassDump,輸出類的結構:plugin wallbreaker classdump <class-name> [--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 <object-handle> [--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:returnf = open(jobfile, 'w')export = {}export['match_regex'] = self.app.match_regex_listexport['black_regex'] = self.app.black_regex_listtree = {}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'] = treef.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:returnf = 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 了。
1. 在 "設置"?應用上進行操作,打開幾個子選項的界面之后,觀察方法的參數和返回值;
2. 導出 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。當然這些 “缺點” 無非也就是加幾行代碼的事情,整個工具非常不錯,值得用于輔助分析。
3. 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
重點說下手機抓包
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() 只有可以重新發送的請求
坑、坑、坑、坑、坑、坑:
- 1. https://github.com/siyujie/OkHttpLogger-Frida:這個使用 -F 參數可以成功抓到包。(?okhtpfind.dex?內包含了 更改了包名的okio以及Gson )
- 2. 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(); // 獲取classloaderJava.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")//URLvar 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 = trueif (null == requestBody) {hasRequestBody = false}//Headersvar 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()requestBody[M_reqbody_writeTo](buffer)var reqByteString = getByteString(buffer)var charset = defChatsetvar contentType = requestBody[M_reqbody_contentType]()if (null != contentType) {var appcharset = contentType[M_contentType_charset](defChatset);if (null != appcharset) {charset = appcharset;}}//LOG Request Bodytry {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;}//URLlogString.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]()//Headersvar 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")}//Bodyvar 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 = defChatsetvar contentType = responseBody[M_rspBody_contentType]()if (null != contentType) {var appcharset = contentType[M_contentType_charset](defChatset)if (null != appcharset) {charset = appcharset}}//newResponsevar mediaType = responseBody[M_rspBody_contentType]()var newBody = null;try {newBody = ResponseBodyWapper[M_rspBody_create](mediaType, rspByteString.toByteArray())} catch (error) {newBody = ResponseBodyWapper[M_rspBody_create](mediaType, readBufferString(rspByteString, charset))}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 = buffer[M_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) * 150newSB.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)this[M_CallBack_onResponse](call,newResponse)}this[M_Call_enqueue](callback)realCallBack.$dispose}}//同步 RealCall[M_Call_execute].overload().implementation = function () {// console.log("-------------------------------------HOOK SUCCESS 同步--------------------------------------------------")var response = this[M_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 + "]" + " >> " + call[M_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) {call[M_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 commonJavaStringWapper = 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 OKHTTPOkHttpClientWapper = Java.use(Cls_OkHttpClient)ResponseBodyWapper = Java.use(Cls_ResponseBody)BufferWapper = Java.use(Cls_okio_Buffer)//Start HookOkHttpClientWapper[M_Client_newCall].overload(Cls_Request).implementation = function (request) {var call = this[M_Client_newCall](request)try {CallCache.push(call["clone"]())} catch (error) {console.log("not fount clone method!")}var realCallClassName = call.$classNameif (!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 = loaderconsole.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).classif (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").classvar 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分析。
總結
以上是生活随笔為你收集整理的转载:实用 FRIDA 进阶 --- objection :内存漫游、hook anywhere、抓包的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux的发展
- 下一篇: JS 逆向 --- 过无限debugge