本章中我們進一步介紹,大家在學習和工作中使用Frida的實際場景,比如動態查看安卓應用程序在當前內存中的狀態,比如指哪兒就能hook哪兒,比如脫殼,還有使用Frida來自動化獲取參數、返回值等數據,主動調用API獲取簽名結果sign等工作實際高頻場景,最后介紹一些經常遇到的高頻問題解決思路,希望可以切實地幫助到讀者。
1 內存漫游
Frida只是提供了各種API供我們調用,在此基礎之上可以實現具體的功能,比如禁用證書綁定之類的腳本,就是使用Frida的各種API來組合編寫而成。於是有大佬將各種常見、常用的功能整合進一個工具,供我們直接在命令行中使用,這個工具便是objection。
objection功能強大,命令眾多,而且不用寫一行代碼,便可實現諸如內存搜索、類和模塊搜索、方法hook打印參數返回值調用棧等常用功能,是一個非常方便的,逆向必備、內存漫游神器。objection的界面及命令如下圖圖2-1所示。
1.1 獲取基本信息
首先介紹幾個基本操作:
- 鍵入命令之后,回車執行;
- help:不知道當前命令的效果是什么,在當前命令前加
help比如,help env,回車之后會出現當前命令的解釋信息; - 按空格:不知道輸入什么就按空格,會有提示出來,上下選擇之后再按空格選中,又會有新的提示出來;
- jobs:作業系統很好用,建議一定要掌握,可以同時運行多項(
hook)作業;
我們以安卓內置應用“設置”為例,來示范一下基本的用法。
在手機上啟動frida-server,並且點擊啟動“設置”圖標,手機進入設置的界面,首先查看一下“設置”應用的包名。
# frida-ps -U|grep -i setting
7107 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 提取內存信息
- 查看內存中加載的庫
運行命令memory list modules,效果如下圖2-3所示。
- 查看庫的導出函數
運行命令memory list exports libssl.so,效果如下圖2-4所示。
- 將結果保存到
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
- 提取整個(或部分)內存
命令是memory dump all from_base,這部分內容與下文脫殼部分有重疊,我們在脫殼部分介紹用法。
- 搜索整個內存
命令是memory search --string --offsets-only,這部分也與下文脫殼部分有重疊,我們在脫殼部分詳細介紹用法。
1.3 內存堆搜索與執行
- 在堆上搜索實例
我們查看AOSP源碼關於設置里顯示系統設置的部分,發現存在着DisplaySettings類,可以在堆上搜索是否存在着該類的實例。首先在手機上點擊進入“顯示”設置,然后運行以下命令,並得到相應的實例地址:
# android heap search instances com.android.settings.DisplaySettings
Using exsiting matches for com.android.settings.DisplaySettings. Use --fresh flag for new instances.
Handle Class toString()
-------- ------------------------------------ -----------------------------------------
0x252a com.android.settings.DisplaySettings DisplaySettings{69d91ee #0 id=0x7f0a0231}
- 調用實例的方法
查看源碼得知com.android.settings.DisplaySettings類有着getPreferenceScreenResId()方法(后文也會介紹在objection中直接打印類的所有方法的命令),這樣就可以直接調用該實例的getPreferenceScreenResId()方法,用excute命令。
# android heap execute 0x2526 getPreferenceScreenResId
Handle 0x2526 is to class com.android.settings.DisplaySettings
Executing method: getPreferenceScreenResId()
2132082764
可見結果被直接打印了出來。
- 在實例上執行
js代碼
也可以在找到的實例上直接編寫js腳本,輸入android heap evaluate 0x2526命令后,會進入一個迷你編輯器環境,輸入console.log("evaluate result:"+clazz.getPreferenceScreenResId())這串腳本,按ESC退出編輯器,然后按回車,即會開始執行這串腳本,輸出結果。
# android heap evaluate 0x2526
(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
- 直接啟動
activity
直接上代碼,想要進入顯示設置,可以在任意界面直接運行以下代碼進入顯示設置:
# android intent launch_activity com.android.settings.DisplaySettings
(agent) Starting activity com.android.settings.DisplaySettings...
(agent) Activity successfully asked to start.
- 查看當前可用的
activity
可以使用android hooking list命令來查看當前可用的activities,然后使用上述命令進行調起。
# 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
- 直接啟動
service
也可以先使用android hooking list services查看可供開啟的服務,然后使用android intent launch_service com.android.settings.bluetooth.BluetoothPairingService命令來開啟服務。
2 Frida hook anywhere
很多新手在學習Frida的時候,遇到的第一個問題就是,無法找到正確的類及子類,無法定位到實現功能的准確的方法,無法正確的構造參數、繼而進入正確的重載,這時候可以使用Frida進行動態調試,來確定以上具體的名稱和寫法,最后寫出正確的hook代碼。
2.1 objection(內存漫游)
- 列出內存中所有的類
# 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
- 內存中搜索所有的類
在內存中所有已加載的類中搜索包含特定關鍵詞的類。
# 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
- 內存中搜索所有的方法
在內存中所有已加載的類的方法中搜索包含特定關鍵詞的方法,上文中可以發現,內存中已加載的類就已經高達11885個了,那么他們的方法一定是類的個數的數倍,整個過程會相當龐大和耗時,見下圖2-6。
# android hooking search methods display
- 列出類的所有方法
當搜索到了比較關心的類之后,就可以直接查看它有哪些方法,比如我們想要查看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)
列出的方法與源碼相比對之后,發現是一模一樣的。
- 直接生成
hook代碼
上文中在列出類的方法時,還直接把參數也提供了,也就是說我們可以直接動手寫hook了,既然上述寫hook的要素已經全部都有了,objection這個“自動化”工具,當然可以直接生成代碼。
# 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)
上述操作均是基於在內存中直接枚舉搜索,已經可以獲取到大量有用的靜態信息,我們再來介紹幾個方法,可以獲取到執行時動態的信息,當然、同樣地,不用寫一行代碼。
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類中的多個方法。
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()這個方法的;雖然這個方法沒有參數,大家可以再找個有參數的試一下。
hook方法的所有重載
objection的help中指出,在hook給出的單個方法的時候,會hook它的所有重載。
# help android hooking watch class_method
Command: android hooking watch class_method
Usage: 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.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-9。
2.3 ZenTracer(hook)
前文中介紹的objection已經足夠強大,優點是hook准確、粒度細。這里再推薦個好友自己寫的批量hook查看調用軌跡的工具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)
我們來完整的演示一遍,比如現在看java.io.File類的所有方法,我們可以這樣操作,首先是精准匹配:
- 點擊打開“設置”應用;
- 選擇
Action→Match RegEx - 輸入
E:java.io.File,點擊add,然后關閉窗口 - 點擊
Action→Start
可以觀察到java.io.File類的所有方法都被hook了,,並且像java.io.File.createTempFile方法的所有重載也被hook了,見下圖2-12。
- 在“設置”應用上進行操作,打開幾個子選項的界面之后,觀察方法的參數和返回值;
- 導出
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"
},
- 點擊
Action→Stop,再點擊Action→Clean,本次觀察結束。 - 也可以使用模糊匹配模式,比如輸入
M:java.io.File之后,會將諸如java.io.FileOutputStream類的諸多方法也都hook上,見下圖2-14。
ZenTracer的目前已知的缺點,無法打印調用棧,無法hook構造函數,也就是$init。當然這些“缺點”無非也就是加幾行代碼的事情,整個工具非常不錯,值得用於輔助分析。
3 Frida用於抓包
我們拿到一個app,做的第一件事情往往是先抓包來看,它發送和接收了哪些數據。收包發包是一個app的命門,企業為用戶服務過程中最為關鍵的步驟——注冊、流量商品、游戲數據、點贊評論、下單搶票等行為,均通過收包發包來完成。如果對收包發包的數據沒有校驗,黑灰產業可以直接制作相應的協議刷工具,脫離app本身進行實質性業務操作,為企業和用戶帶來巨大的損失。
3.1 推薦抓包環境
由上所述,抓包是每一位安全工程師必須掌握的技能。而抓包一般又分為以下兩種情形:
- 應用層:
Http(s)協議抓包 - 會話層:
Socket端口通信抓包
在抓包工具的選擇上,如果是抓應用層Http(s),推薦的專業工具是BurpSuite,如果只是想簡單的抓包、用的舒服輕松,也可以使用花瓶(Charles)。推薦不要使用fiddle,因為它無法導入客戶端證書(p12、Client SSL Certificates),對於服務器校驗客戶端證書的情況無法Bypass;如果是會話層抓包,則選擇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的策略時會加上服務器校驗客戶端證書的策略。
接下來我們具體分析每一種情況。
- Http
對於Http的抓包,只要在電腦的Charles上配置好Socks5服務器,手機上用Postern開啟VPN連上電腦上的Charles的Socks5服務器,所有流量即可導出到Charles上。當然使用BurpSuite也是一樣的道理。至於具體的操作步驟網上文檔浩如煙海,讀者可以自行取閱。
一般大型app、服務器數量非常多的,尤其還配置了多種CDN在全國范圍、三網內進行內容分發和加速分發的,通常app里絕大多數內容都是走的Http。
當然他們會在最關鍵的業務上,比如用戶登錄時,配置Https協議,來保證最基本的安全。
- Https客戶端校驗服務器
這時候我們抓app的Http流量的時候一切正常,圖片、視頻、音樂都直接下載和轉儲。
但是作為用戶要登錄的時候,就會發現抓包失敗,這時候開啟Charles的SSL抓包功能,手機瀏覽器輸入Charles的證書下載地址chls.pro/ssl,下載證書並安裝到手機中。
注意在高版本的安卓上,用戶安裝的證書並不會安裝到系統根證書目錄中去,需要
root手機后將用戶安裝的證書移動到系統根證書目錄中去,具體操作步驟網上非常多,這里不再贅述。
當Charles的證書安裝到系統根目錄中去之后,系統就會信任來自Charles的流量包了,我們的抓包過程就會回歸正常。
當然,這里還是會有讀者疑惑,為什么導入Charles的證書之后,app抓包就正常了呢?這里我們就需要理解一下應用層Https抓包的根本原理,見下圖2-15(會話層Socket抓包並不是這個原理,后文會介紹Socket抓包的根本原理)。
有了Charles置於中間之后,本來C/S架構的通信過程會“分裂”為兩個獨立的通信過程,app本來驗證的是服務器的證書,服務器的證書手機的根證書是認可的,直接內置的;但是分裂成兩個獨立的通信過程之后,app驗證的是Charles的證書,它的證書手機根證書並不認可,它並不是由手機內置的權威根證書簽發機構簽發的,所以手機不認,然后app也不認;所以我們要把Charles的證書導入到手機根證書目錄中去,這樣手機就會認可,如果app沒有進行額外的校驗(比如在代碼中對該證書進行校驗,也就是SSL pinning系列API,這種情況下一小節具體闡述)的話,app也會直接認可接受。
- 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住這段校驗的代碼,使這段判斷的機制失效即可。
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);
}
}
});
}
- 使用
objection,直接將SSL pinning給disable掉
# android sslpinning disable
- 如果還有一些情況沒有覆蓋的話,可以來看看大佬的代碼
- 目錄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分析。



















