Python反反爬蟲 - Frida破解某安卓社區token反爬蟲

前言

很少逼逼,這個安卓社區就是酷安,之前想過要爬這軟件,可是都忘了,幾天前抓了下它的包,發現請求 headers 裏有一個 token 驗證,果斷就給破了javascript

分析過程

先抓個包 java

能夠看到其中有個請求頭 X-App-Token,這就是驗證,至於 X-App-Device這玩意兒應該是獲取你手機信息的,無論它,先看看軟件源代碼,找到請求方法

一、jeb分析

沒加固,好像也沒混淆,舒服python

搜索關鍵字:X-App-Tokengit

很明顯找到了咱們要的東西了,(jeb3.0按tab鍵反編譯)github

這個 X-App-Token 是變量 v2_1,v2_1是,一個AuthUtils類裏的getAS方法返回的api

跟進能夠發現這是一個native方法,lib是native-libapp

到這裏就無法用jeb分析了,咱們先看看參數2 deviceId 是個什麼玩意兒測試

能夠看到是一個 SystemUtils 類裏的 getDeviceID方法返回的,傳入一個context參數ui

咱們去app的application hook這個方法this

我找到一個代碼量最少的方法,這樣能夠幫助咱們不破壞原邏輯的狀況下hook

hook的代碼很簡單

Java.perform(function() {
    var CoolMarket = Java.use('com.coolapk.market.CoolMarketApplication');
    CoolMarket.onLog.implementation = function() {
        
        var deviceId = Java.use('com.coolapk.market.util.SystemUtils').getDeviceId(this);
        console.log('Device Id: ', deviceId);

        var app_token = Java.use('com.coolapk.market.util.AuthUtils').getAS(this, deviceId);
        console.log('App Token: ', app_token);

        console.log('----------');
        return 1;
    }
})
複製代碼

拿到 deviceId 後分析so

ida分析

解壓apk拿到 native-lib.so,用ida打開

咱們已知方法名和參數個數,那麼就先搜索方法名

Function Window 按 option+t 搜索 getAS,能夠看到,毛都沒有

那咱們就到 IDA View 裏搜索,快捷鍵同樣

找到了這個,參數是兩個,但這個不是方法,無法 F5 反編譯

不瞞大家,這個DCB是個什麼玩意兒我也不知道. 可是我在 Function Window 瞎翻想找一些我能看得懂的方法名時看到了這個

我一看,這個方法好像和 getAS 有點關係就順手 F5 了

簡單看了看裏面的代碼,我估計我要找的是這個方法,爲何是這個方法而不是其餘的

我分析了 X-App-Token 這個驗證的組成,它長這樣:f2c29a109fde487e9350d3e6b881036a8513efac-09ea-3709-b214-95b366f1a1850x5d024391

我以前就獲取到了個人 Device Id,我無心間看到了個人 device id就在裏面,而後我把它拆分紅了這樣:

  • f2c29a109fde487e9350d3e6b881036a
  • 8513efac-09ea-3709-b214-95b366f1a185
  • 0x5d024391

第一項很明顯是md5密文,第二部分就是device id,最後是一個十六進制,不知道什麼玩意兒,但我在那個 getAuthString 代碼裏看到了這段

這是一個字符串拼接的過程,其中 v82 是md5密文,也是字符串的頭部,後面接着是 v43(device id)、字符串0x、最後是 hex_time (這是我改了後的命名),因此我就能肯定這個方法就是我想要的;

上面說的也就是接下來要分析的,其中那個十六進制的東西就是時間戳,咱們只須要分析出md5是怎麼來的就好了,咱們知道md5是 v61v61 的加密代碼在這

加密的內容是v58,v58是一個通過base64編碼後的變量

我懶得去看它是什麼了,我直接hook了md5加密類,有三個方法,一個是md5(應該是構造方法把)、update、finalize,hook 代碼以下

// 這裏算基址是學的四哥的
var JNI_LOAD_POINTER = Module.getExportByName('libnative-lib.so', 'JNI_OnLoad'); // 首先拿到 JNI_OnLoad方法的地址
// 這裏減去的是從so中獲得的JNI_OnLoad的地址 0x31A04
var BASE_ADDR = parseInt(JNI_LOAD_POINTER) - parseInt('0x31A04'); // 用程序運行中JNI_OnLoad的絕對地址減去它的相對地址獲得基址

// MD5::MD5
Java.perform(function() {
    // 而後用基址 + 要hook的方法的相對地址就獲得了絕對地址
    var hookpointer = '0x' + parseInt(BASE_ADDR + parseInt('0x32168')).toString(16) // 獲取要hook方法的地址
    var pointer = new NativePointer(hookpointer) // 根據方法地址構建NativePointer
    console.log('[MD5::MD5] hook pointer: ', pointer)
    
    var arg0, arg1, arg2, arg3
    Interceptor.attach(pointer, {
            onEnter: function(args) {
                arg0 = args[0]
                arg1 = args[1]
                console.log('\n')
                console.log('=====> [MD5::MD5] -> [方法調用前]')
                console.log('參數1: {0} => {1}'.format(arg0, Memory.readCString(arg0))) // Memory.readCString 是讀取地址爲字符串,相似的還有readUtf8String、readUtf16String等
                console.log('參數2: {0} => {1}'.format(arg1, Memory.readCString(arg1)))
                console.log('\n')
            },
            onLeave: function(retval) {
                console.log('\n')
                console.log('=====> [MD5::MD5] -> [方法調用後]:')
                console.log('返回值: ', retval)
                console.log('參數1: {0} => {1}'.format(arg0, Memory.readCString(arg0)))
                console.log('參數2: {0} => {1}'.format(arg1, Memory.readCString(arg1)))
                console.log('\n')
            }
        }   
    )
})



// MD5::update
Java.perform(function() {
    var hookpointer = '0x' + parseInt(BASE_ADDR + parseInt('0x329AC')).toString(16) // 獲取要hook方法的地址
    var pointer = new NativePointer(hookpointer) // 根據方法地址構建NativePointer
    console.log('[MD5::update] hook pointer: ', pointer)
    
    var arg0, arg1, arg2, arg3
    Interceptor.attach(pointer, {
            onEnter: function(args) {
                arg0 = args[0]
                arg1 = args[1]
                arg2 = args[2]
                console.log('\n')
                console.log('=====> [MD5::update] -> [方法調用前]')
                console.log('參數1: {0} => {1}'.format(arg0, Memory.readCString(arg0)))
                console.log('參數2: {0} => {1}'.format(arg1, Memory.readCString(arg1)))
                console.log('參數3: {0} => {1}'.format(arg2, Memory.readCString(arg2)))

                console.log('\n')
            },
            onLeave: function(retval) {
                console.log('\n')
                console.log('=====> [MD5::update] -> [方法調用後]:')
                console.log('返回值: ', retval)
                console.log('參數1: {0} => {1}'.format(arg0, Memory.readCString(arg0)))
                console.log('參數2: {0} => {1}'.format(arg1, Memory.readCString(arg1)))
                console.log('參數3: {0} => {1}'.format(arg2, Memory.readCString(arg2)))
                console.log('\n')
            }
        }   
    )
})


// MD5::finalize
Java.perform(function() {
    var hookpointer = '0x' + parseInt(BASE_ADDR + parseInt('0x321C4')).toString(16) // 獲取要hook方法的地址
    var pointer = new NativePointer(hookpointer) // 根據方法地址構建NativePointer
    console.log('[MD5::finalize] hook pointer: ', pointer)
    var arg0, arg1, arg2, arg3
    Interceptor.attach(pointer, {
            onEnter: function(args) {
                arg0 = args[0]
                arg1 = args[1]
                arg2 = args[2]
                arg3 = args[3]
                console.log('\n')
                console.log('=====> [MD5::finalize] -> [方法調用前]')
                console.log('參數1: {0} => {1}'.format(arg0, Memory.readCString(arg0)))
                console.log('參數2: {0} => {1}'.format(arg1, Memory.readCString(arg1)))
                console.log('參數3: {0} => {1}'.format(arg2, Memory.readCString(arg2)))
                console.log('參數4: {0} => {1}'.format(arg3, Memory.readCString(arg3)))
                console.log('\n')
            },
            onLeave: function(retval) {
                console.log('\n')
                console.log('=====> [MD5::finalize] -> [方法調用後]:')
                console.log('返回值: ', retval)
                console.log('參數1: {0} => {1}'.format(arg0, Memory.readCString(arg0)))
                console.log('參數2: {0} => {1}'.format(arg1, Memory.readCString(arg1)))
                console.log('參數3: {0} => {1}'.format(arg2, Memory.readCString(arg2)))
                console.log('參數4: {0} => {1}'.format(arg3, Memory.readCString(arg3)))
                console.log('\n')
            }
        }   
    )
})
複製代碼

運行後獲得了那個base64編碼過的內容:

dG9rZW46Ly9jb20uY29vbGFway5tYXJrZXQvYzY3ZWY1OTQzNzg0ZDA5NzUwZGNmYmIzMTAyMGYwYWI/MzgyMzIxNWQ5MWQyOWQ5ODg3ZWJjMDVmMGQ3ZmQzMGQkODUxM2VmYWMtMDllYS0zNzA5LWIyMTQtOTViMzY2ZjFhMTg1JmNvbS5jb29sYXBrLm1hcmtldA==

通過解碼後:

token://com.coolapk.market/c67ef5943784d09750dcfbb31020f0ab?3823215d91d29d9887ebc05f0d7fd30d$8513efac-09ea-3709-b214-95b366f1a185&com.coolapk.market

在我看到這段代碼後

上面解碼後的內容能夠拆分爲:

  • token://com.coolapk.market/c67ef5943784d09750dcfbb31020f0ab?
  • 3823215d91d29d9887ebc05f0d7fd30d
  • $
  • 8513efac-09ea-3709-b214-95b366f1a185
  • &
  • com.coolapk.market

據我分析,只須要的到第二部分的md5加密的來歷就好了,繼續分析,找到了加密的地方

根據這圖的畫線,能夠明確的知道這md5就是時間戳,在我hook的輸出中也能夠看到這個就是時間戳

至於 token://com.coolapk.market/c67ef5943784d09750dcfbb31020f0ab? 這個是不變的

結論

token://com.coolapk.market/c67ef5943784d09750dcfbb31020f0ab? + md5加密後的時間戳 + $ + device id + & + com.coolapk.market(包名),將其md5加密後獲得 第一部分

token的來歷就是:第一部分 + deivce id + 0x + 十六進制轉換後的時間戳

簡單的測試代碼:

import requests
import time
import hashlib
import base64


DEVICE_ID = "8513efac-09ea-3709-b214-95b366f1a185"

def get_app_token():
    t = int(time.time())
    hex_t = hex(t)

    # 時間戳加密
    md5_t = hashlib.md5(str(t).encode('utf-8')).hexdigest()

    # 不知道什麼鬼字符串拼接
    a = 'token://com.coolapk.market/c67ef5943784d09750dcfbb31020f0ab?{}${}&com.coolapk.market' \
        .format(md5_t, DEVICE_ID)

    # 不知道什麼鬼字符串拼接 後的字符串再次加密
    md5_a = hashlib.md5(base64.b64encode(a.encode('utf-8'))).hexdigest()

    token = '{}{}{}'.format(md5_a, DEVICE_ID, hex_t)
    print(token)
    return token


def request():
    url = "https://api.coolapk.com/v6/main/indexV8?page=1"
    headers = {
        "User-Agent": "Dalvik/2.1.0 (Linux; U; Android 9; MI 8 SE MIUI/9.5.9) (#Build; Xiaomi; MI 8 SE; PKQ1.181121.001; 9) +CoolMarket/9.2.2-1905301"
    }
    headers = {
        "User-Agent": "Dalvik/2.1.0 (Linux; U; Android 9; MI 8 SE MIUI/9.5.9) (#Build; Xiaomi; MI 8 SE; PKQ1.181121.001; 9) +CoolMarket/9.2.2-1905301",
        "X-App-Id": "com.coolapk.market",
        "X-Requested-With": "XMLHttpRequest",
        "X-Sdk-Int": "28",
        "X-Sdk-Locale": "zh-CN",
        "X-Api-Version": "9",
        "X-App-Version": "9.2.2",
        "X-App-Code": "1903501",
        "X-App-Device": "QRTBCOgkUTgsTat9WYphFI7kWbvFWaYByO1YjOCdjOxAjOxEkOFJjODlDI7ATNxMjM5MTOxcjMwAjN0AyOxEjNwgDNxITM2kDMzcTOgsTZzkTZlJ2MwUDNhJ2MyYzM",
        "Host": "api.coolapk.com",
        "X-Dark-Mode": "0",
        "X-App-Token": get_app_token(),
    }

    resp = requests.get(url, headers=headers)
    print(resp.text)


if __name__ == '__main__':
    request()


複製代碼

最後

代碼啥的都提交到Github了,CoolapkTokenCrack

相關文章
相關標籤/搜索