主頁 > 資料庫 > 抖音資料采集Frida進階:記憶體漫游、hook anywhere、抓包

抖音資料采集Frida進階:記憶體漫游、hook anywhere、抓包

2021-01-24 19:07:47 資料庫

抖音資料采集Frida進階:記憶體漫游、hook anywhere、抓包

短視頻、直播資料實時采集介面,請查看檔案: TiToData

免責宣告:本檔案僅供學習與參考,請勿用于非法用途!否則一切后果自負,


本章中我們進一步介紹,大家在學習和作業中使用Frida的實際場景,比如動態查看安卓應用程式在當前記憶體中的狀態,比如指哪兒就能hook哪兒,比如脫殼,還有使用Frida來自動化獲取引數、回傳值等資料,主動呼叫API獲取簽名結果sign等作業實際高頻場景,最后介紹一些經常遇到的高頻問題解決思路,希望可以切實地幫助到讀者,

1 記憶體漫游

Frida只是提供了各種API供我們呼叫,在此基礎之上可以實作具體的功能,比如禁用證書系結之類的腳本,就是使用Frida的各種API來組合撰寫而成,于是有大佬將各種常見、常用的功能整合進一個工具,供我們直接在命令列中使用,這個工具便是objection
objection功能強大,命令眾多,而且不用寫一行代碼,便可實作諸如記憶體搜索、類和模塊搜索、方法hook列印引數回傳值呼叫堆疊等常用功能,是一個非常方便的,逆向必備、記憶體漫游神器,objection的界面及命令如下圖圖2-1所示,
image.png
圖2-1 objection基本界面及命令

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執行的應用環境資訊命令envfrida-server版本資訊命令,
image.png
圖2-2 應用環境資訊和frida-server版本資訊

1.2 提取記憶體資訊

  • 查看記憶體中加載的庫

運行命令memory list modules,效果如下圖2-3所示,
image.png
圖2-3 記憶體中加載的庫

  • 查看庫的匯出函式

運行命令memory list exports libssl.so,效果如下圖2-4所示,
image.png
圖2-4 libssl.so庫的匯出函式

  • 將結果保存到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

image.png
圖2-5 使用json格式保存的libart.so的匯出函式

  • 提取整個(或部分)記憶體

命令是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 啟動activityservice

  • 直接啟動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

image.png
圖2-6 記憶體中搜索所有的方法

  • 列出類的所有方法

當搜索到了比較關心的類之后,就可以直接查看它有哪些方法,比如我們想要查看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

image.png
image.png
使用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

image.png
注意最后加上的三個選項--dump-args --dump-return --dump-backtrace,為我們成功列印出來了我們想要看的資訊,其實回傳值Return Value就是getName()方法的回傳值,我的藍牙耳機的型號名字OnePlus Bullets Wireless 2;從呼叫堆疊可以反查如何一步一步呼叫到getName()這個方法的;雖然這個方法沒有引數,大家可以再找個有引數的試一下,

  • hook方法的所有多載

objectionhelp中指出,在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為我們hookFile構造器的所有多載,一共是6個,在設定界面隨意進出幾個子設定界面,可以看到命中很多次該方法的不同多載,每次引數的值也都不同,見下圖2-9,
image.png
圖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所示,
image.png
圖2-10 PyQt視窗
點擊Action之后,會出現匹配模板(Match RegEx)和過濾模板(Black RegEx),匹配就是包含的關鍵詞,過濾就是不包含的關鍵詞,見下圖2-11,其代碼實作就是

圖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類的所有方法,我們可以這樣操作,首先是精準匹配:

  1. 點擊打開“設定”應用;
  2. 選擇ActionMatch RegEx
  3. 輸入E:java.io.File,點擊add,然后關閉視窗
  4. 點擊ActionStart

可以觀察到java.io.File類的所有方法都被hook了,,并且像java.io.File.createTempFile方法的所有多載也被hook了,見下圖2-12,
image.png
圖2-12 ZenTracer正在進行類的方法hook

  1. 在“設定”應用上進行操作,打開幾個子選項的界面之后,觀察方法的引數和回傳值;

image.png
圖2-13 觀察引數和回傳值

  1. 匯出json來觀察方法的呼叫樹,選擇FileExport json,匯出為tmp.json,使用vscodeformat 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"
            },
  1. 點擊ActionStop,再點擊ActionClean,本次觀察結束,
  2. 也可以使用模糊匹配模式,比如輸入M:java.io.File之后,會將諸如java.io.FileOutputStream類的諸多方法也都hook上,見下圖2-14,

image.png
圖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;如果是會話層抓包,則選擇tcpdumpWireShark相組合的方式,
使用jnettop還可以實時查看流量走勢和對方IP地址,更為直觀和生動,
在手機上設定代理時,推薦使用VPN來將流量匯出到抓包軟體上,而不是通過給WIFI設定HTTP代理的方式,使用VPN可以同時抓到Http(s)Socket的包,且不管其來自Java層還是so層,我們常用的代理軟體是老牌的Postern,開VPN服務通過連接到開啟Socks5服務端的抓包軟體,將流量匯出去,
當然有些應用會使用System.getProperty(“http.proxyHost”)、System.getProperty(“http.proxyPort”);這兩個API來查看當前系統是否掛了VPN,這時候只能用FridaXposedhook這個介面、修改其回傳值,或者重打包來nop掉,當然還有一種最為終極、最為強悍的方法,那就是制作路由器,抓所有過網卡的包,
制作路由器的方法也很簡單,給筆記本電腦裝Kali Linuxeth0口插網線上網,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連上電腦上的CharlesSocks5服務器,所有流量即可匯出到Charles上,當然使用BurpSuite也是一樣的道理,至于具體的操作步驟網上檔案浩如煙海,讀者可以自行取閱,
一般大型app、服務器數量非常多的,尤其還配置了多種CDN在全國范圍、三網內進行內容分發和加速分發的,通常app里絕大多數內容都是走的Http
當然他們會在最關鍵的業務上,比如用戶登錄時,配置Https協議,來保證最基本的安全,

  • Https客戶端校驗服務器

這時候我們抓appHttp流量的時候一切正常,圖片、視頻、音樂都直接下載和轉儲,
但是作為用戶要登錄的時候,就會發現抓包失敗,這時候開啟CharlesSSL抓包功能,手機瀏覽器輸入Charles的證書下載地址chls.pro/ssl,下載證書并安裝到手機中,

注意在高版本的安卓上,用戶安裝的證書并不會安裝到系統根證書目錄中去,需要root手機后將用戶安裝的證書移動到系統根證書目錄中去,具體操作步驟網上非常多,這里不再贅述,

Charles的證書安裝到系統根目錄中去之后,系統就會信任來自Charles的流量包了,我們的抓包程序就會回歸正常,
當然,這里還是會有讀者疑惑,為什么匯入Charles的證書之后,app抓包就正常了呢?這里我們就需要理解一下應用層Https抓包的根本原理,見下圖2-15(會話層Socket抓包并不是這個原理,后文會介紹Socket抓包的根本原理),
image.pngimage.png
圖2-15 應用層Https抓包的根本原理
有了Charles置于中間之后,本來C/S架構的通信程序會“分裂”為兩個獨立的通信程序,app本來驗證的是服務器的證書,服務器的證書手機的根證書是認可的,直接內置的;但是分裂成兩個獨立的通信程序之后,app驗證的是Charles的證書,它的證書手機根證書并不認可,它并不是由手機內置的權威根證書簽發機構簽發的,所以手機不認,然后app也不認;所以我們要把Charles的證書匯入到手機根證書目錄中去,這樣手機就會認可,如果app沒有進行額外的校驗(比如在代碼中對該證書進行校驗,也就是SSL pinning系列API,這種情況下一小節具體闡述)的話,app也會直接認可接受,

  • Https服務器校驗客戶端

既然app客戶端會校驗服務器證書,那么服務器可不可能校驗app客戶端證書呢?答案是肯定的,
在許多業務非常聚焦并且當單一,比如行業應用、銀行、公共交通、游戲等行業,C/S架構中服務器高度集中,對應用的版本控制非常嚴格,這時候就會在服務器上部署對app內置證書的校驗代碼,
上一小節中已經看到,單一通信已經分裂成兩個互相獨立的通信,這時候與服務器進行通信的已經不是app、而是Charles了,所以我們要將app中內置的證書匯入到Charles中去,
這個操作通常需要完成兩項內容:

  1. 找到證書檔案
  2. 找到證書密碼

找到證書檔案很簡單,一般apk進行解包,直接過濾搜索后綴名為p12的檔案即可,一般常用的命令為tree -NCfhl |grep -i p12,直接列印出p12檔案的路徑,當然也有一些app比較“狡猾”,比如我們通過搜索p12沒有搜到證書,然后看jadx反編譯的原始碼得出它將證書偽裝成border_ks_19檔案,我們找到這個檔案用file命令查看果然不是后綴名所顯示的png格式,將其改成p12的后綴名嘗試打開時要求輸入密碼,可見其確實是一個證書,見下圖2-17,
image.pngimage.png
圖2-17 偽裝成png的證書檔案
想要拿到密碼也很簡單,一般在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,直接將密碼列印了出來,

圖2-18 直接列印出密碼

當然其實也并不一定非要用Frida,用Xposed也可以,只是Xposed很久不更新了,最近流行的大趨勢是Frida

有了證書和密碼之后,就可以將其匯入到抓包軟體中,在Charles中是位于ProxySSL Proxy SettingsClient CertificatesAdd添加新的證書,輸入指定的域名或IP使用指定的證書即可,見下圖2-19,
image.png
圖2-19 Charles匯入客戶端證書的界面

3.3 SSL Pinning Bypass

上文中我們還有一種情況沒有分析,就是客戶端并不會默認信任系統根證書目錄中的證書,而是在代碼里再加一層校驗,這就是證書系結機制——SSL pinning,如果這段代碼的校驗過不了,那么客戶端還是會報證書錯誤,

  • Https客戶端代碼校驗服務器證書

遇到這種情況的時候,我們一般有三種方式,當然目標是一樣的,都是hook住這段校驗的代碼,使這段判斷的機制失效即可,

  1. hookcheckServerTrusted,將其所有多載都置空;
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);
            }
        }
    });
}
  1. 使用objection,直接將SSL pinningdisable
# android sslpinning disable

image.png
圖2-20 使用objectionssl pinning diable功能

  1. 如果還有一些情況沒有覆寫的話,可以來看看大佬的代碼
  • 目錄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通信天生又分為兩種JavaSocket通信和NativeSocket通信,

  • Java層:使用的是java.net.InetAddressjava.net.Socketjava.net.ServerSocket等類,與證書系結的情形類似,也可能存在著自定義框架的Socket通信,這時候就需要具體情況具體分析,比如谷歌的protobuf框架等;
  • Native層:一般使用的是C Socket API,一般hooksend()recv()函式可以得到其發送和接受的內容

抓包方法分為三種,介面轉儲、驅動轉儲和路由轉儲:

  • 介面轉儲:比如給outputStream.writehook,把內容存下來看看,可能是經過壓縮、或加密后的包,畢竟是二進制,一切皆有可能;
  • 驅動轉儲:使用tcpdump將經過網口驅動時的資料包轉儲下來,再使用Wireshark進行分析;
  • 路由轉儲:自己做個路由器,運行jnettop,觀察實時進過的流量和IP,可以使用WireShark實時抓包,也可以使用tcpdump抓包后用WireShark分析,

短視頻、直播資料實時采集介面,請查看檔案: TiToData


免責宣告:本檔案僅供學習與參考,請勿用于非法用途!否則一切后果自負,

轉載請註明出處,本文鏈接:https://www.uj5u.com/shujuku/251616.html

標籤:大數據

上一篇:【概念】詳解MapReduce原理

下一篇:抖音資料采集Frida進階:脫殼、自動化、高頻問題

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • GPU虛擬機創建時間深度優化

    **?桔妹導讀:**GPU虛擬機實體創建速度慢是公有云面臨的普遍問題,由于通常情況下創建虛擬機屬于低頻操作而未引起業界的重視,實際生產中還是存在對GPU實體創建時間有苛刻要求的業務場景。本文將介紹滴滴云在解決該問題時的思路、方法、并展示最終的優化成果。 從公有云服務商那里購買過虛擬主機的資深用戶,一 ......

    uj5u.com 2020-09-10 06:09:13 more
  • 可編程網卡芯片在滴滴云網路的應用實踐

    **?桔妹導讀:**隨著云規模不斷擴大以及業務層面對延遲、帶寬的要求越來越高,采用DPDK 加速網路報文處理的方式在橫向縱向擴展都出現了局限性。可編程芯片成為業界熱點。本文主要講述了可編程網卡芯片在滴滴云網路中的應用實踐,遇到的問題、帶來的收益以及開源社區貢獻。 #1. 資料中心面臨的問題 隨著滴滴 ......

    uj5u.com 2020-09-10 06:10:21 more
  • 滴滴資料通道服務演進之路

    **?桔妹導讀:**滴滴資料通道引擎承載著全公司的資料同步,為下游實時和離線場景提供了必不可少的源資料。隨著任務量的不斷增加,資料通道的整體架構也隨之發生改變。本文介紹了滴滴資料通道的發展歷程,遇到的問題以及今后的規劃。 #1. 背景 資料,對于任何一家互聯網公司來說都是非常重要的資產,公司的大資料 ......

    uj5u.com 2020-09-10 06:11:05 more
  • 滴滴AI Labs斬獲國際機器翻譯大賽中譯英方向世界第三

    **桔妹導讀:**深耕人工智能領域,致力于探索AI讓出行更美好的滴滴AI Labs再次斬獲國際大獎,這次獲獎的專案是什么呢?一起來看看詳細報道吧! 近日,由國際計算語言學協會ACL(The Association for Computational Linguistics)舉辦的世界最具影響力的機器 ......

    uj5u.com 2020-09-10 06:11:29 more
  • MPP (Massively Parallel Processing)大規模并行處理

    1、什么是mpp? MPP (Massively Parallel Processing),即大規模并行處理,在資料庫非共享集群中,每個節點都有獨立的磁盤存盤系統和記憶體系統,業務資料根據資料庫模型和應用特點劃分到各個節點上,每臺資料節點通過專用網路或者商業通用網路互相連接,彼此協同計算,作為整體提供 ......

    uj5u.com 2020-09-10 06:11:41 more
  • 滴滴資料倉庫指標體系建設實踐

    **桔妹導讀:**指標體系是什么?如何使用OSM模型和AARRR模型搭建指標體系?如何統一流程、規范化、工具化管理指標體系?本文會對建設的方法論結合滴滴資料指標體系建設實踐進行解答分析。 #1. 什么是指標體系 ##1.1 指標體系定義 指標體系是將零散單點的具有相互聯系的指標,系統化的組織起來,通 ......

    uj5u.com 2020-09-10 06:12:52 more
  • 單表千萬行資料庫 LIKE 搜索優化手記

    我們經常在資料庫中使用 LIKE 運算子來完成對資料的模糊搜索,LIKE 運算子用于在 WHERE 子句中搜索列中的指定模式。 如果需要查找客戶表中所有姓氏是“張”的資料,可以使用下面的 SQL 陳述句: SELECT * FROM Customer WHERE Name LIKE '張%' 如果需要 ......

    uj5u.com 2020-09-10 06:13:25 more
  • 滴滴Ceph分布式存盤系統優化之鎖優化

    **桔妹導讀:**Ceph是國際知名的開源分布式存盤系統,在工業界和學術界都有著重要的影響。Ceph的架構和演算法設計發表在國際系統領域頂級會議OSDI、SOSP、SC等上。Ceph社區得到Red Hat、SUSE、Intel等大公司的大力支持。Ceph是國際云計算領域應用最廣泛的開源分布式存盤系統, ......

    uj5u.com 2020-09-10 06:14:51 more
  • es~通過ElasticsearchTemplate進行聚合~嵌套聚合

    之前寫過《es~通過ElasticsearchTemplate進行聚合操作》的文章,這一次主要寫一個嵌套的聚合,例如先對sex集合,再對desc聚合,最后再對age求和,共三層嵌套。 Aggregations的部分特性類似于SQL語言中的group by,avg,sum等函式,Aggregation ......

    uj5u.com 2020-09-10 06:14:59 more
  • 爬蟲日志監控 -- Elastc Stack(ELK)部署

    傻瓜式部署,只需替換IP與用戶 導讀: 現ELK四大組件分別為:Elasticsearch(核心)、logstash(處理)、filebeat(采集)、kibana(可視化) 下載均在https://www.elastic.co/cn/downloads/下tar包,各組件版本最好一致,配合fdm會 ......

    uj5u.com 2020-09-10 06:15:05 more
最新发布
  • day02-2-商鋪查詢快取

    功能02-商鋪查詢快取 3.商鋪詳情快取查詢 3.1什么是快取? 快取就是資料交換的緩沖區(稱作Cache),是存盤資料的臨時地方,一般讀寫性能較高。 快取的作用: 降低后端負載 提高讀寫效率,降低回應時間 快取的成本: 資料一致性成本 代碼維護成本 運維成本 3.2需求說明 如下,當我們點擊商店詳 ......

    uj5u.com 2023-04-20 08:33:24 more
  • MySQL中binlog備份腳本分享

    關于MySQL的二進制日志(binlog),我們都知道二進制日志(binlog)非常重要,尤其當你需要point to point災難恢復的時侯,所以我們要對其進行備份。關于二進制日志(binlog)的備份,可以基于flush logs方式先切換binlog,然后拷貝&壓縮到到遠程服務器或本地服務器 ......

    uj5u.com 2023-04-20 08:28:06 more
  • day02-短信登錄

    功能實作02 2.功能01-短信登錄 2.1基于Session實作登錄 2.1.1思路分析 2.1.2代碼實作 2.1.2.1發送短信驗證碼 發送短信驗證碼: 發送驗證碼的介面為:http://127.0.0.1:8080/api/user/code?phone=xxxxx<手機號> 請求方式:PO ......

    uj5u.com 2023-04-20 08:27:27 more
  • 快取與資料庫雙寫一致性幾種策略分析

    本文將對幾種快取與資料庫保證資料一致性的使用方式進行分析。為保證高并發性能,以下分析場景不考慮執行的原子性及加鎖等強一致性要求的場景,僅追求最終一致性。 ......

    uj5u.com 2023-04-20 08:26:48 more
  • sql陳述句優化

    問題查找及措施 問題查找 需要找到具體的代碼,對其進行一對一優化,而非一直把關注點放在服務器和sql平臺 降低簡化每個事務中處理的問題,盡量不要讓一個事務拖太長的時間 例如檔案上傳時,應將檔案上傳這一步放在事務外面 微軟建議 4.啟動sql定時執行計劃 怎么啟動sqlserver代理服務-百度經驗 ......

    uj5u.com 2023-04-20 08:26:35 more
  • 云時代,MySQL到ClickHouse資料同步產品對比推薦

    ClickHouse 在執行分析查詢時的速度優勢很好的彌補了MySQL的不足,但是對于很多開發者和DBA來說,如何將MySQL穩定、高效、簡單的同步到 ClickHouse 卻很困難。本文對比了 NineData、MaterializeMySQL(ClickHouse自帶)、Bifrost 三款產品... ......

    uj5u.com 2023-04-20 08:26:29 more
  • sql陳述句優化

    問題查找及措施 問題查找 需要找到具體的代碼,對其進行一對一優化,而非一直把關注點放在服務器和sql平臺 降低簡化每個事務中處理的問題,盡量不要讓一個事務拖太長的時間 例如檔案上傳時,應將檔案上傳這一步放在事務外面 微軟建議 4.啟動sql定時執行計劃 怎么啟動sqlserver代理服務-百度經驗 ......

    uj5u.com 2023-04-20 08:25:13 more
  • Redis 報”OutOfDirectMemoryError“(堆外記憶體溢位)

    Redis 報錯“OutOfDirectMemoryError(堆外記憶體溢位) ”問題如下: 一、報錯資訊: 使用 Redis 的業務介面 ,產生 OutOfDirectMemoryError(堆外記憶體溢位),如圖: 格式化后的報錯資訊: { "timestamp": "2023-04-17 22: ......

    uj5u.com 2023-04-20 08:24:54 more
  • day02-2-商鋪查詢快取

    功能02-商鋪查詢快取 3.商鋪詳情快取查詢 3.1什么是快取? 快取就是資料交換的緩沖區(稱作Cache),是存盤資料的臨時地方,一般讀寫性能較高。 快取的作用: 降低后端負載 提高讀寫效率,降低回應時間 快取的成本: 資料一致性成本 代碼維護成本 運維成本 3.2需求說明 如下,當我們點擊商店詳 ......

    uj5u.com 2023-04-20 08:24:03 more
  • day02-短信登錄

    功能實作02 2.功能01-短信登錄 2.1基于Session實作登錄 2.1.1思路分析 2.1.2代碼實作 2.1.2.1發送短信驗證碼 發送短信驗證碼: 發送驗證碼的介面為:http://127.0.0.1:8080/api/user/code?phone=xxxxx<手機號> 請求方式:PO ......

    uj5u.com 2023-04-20 08:23:11 more