【驗證碼逆向專欄】xx80 郵箱多種類驗證碼逆向分析

K哥爬虫發表於2024-04-01

7yAu85.png

宣告

本文章中所有內容僅供學習交流使用,不用於其他任何目的,不提供完整程式碼,抓包內容、敏感網址、資料介面等均已做脫敏處理,嚴禁用於商業用途和非法用途,否則由此產生的一切後果均與作者無關!

本文章未經許可禁止轉載,禁止任何修改後二次傳播,擅自使用本文講解的技術而導致的任何意外,作者均不負責,若有侵權,請在公眾號【K哥爬蟲】聯絡作者立即刪除!

前言

又到了粉絲答疑時間,之前已經分析了兩位粉絲存疑的站點,並編寫了相應的逆向文章,私信中還有些小夥伴提出了在逆向一些網站的時候碰到的問題,後期仍會選擇其中一些,寫成文章,以供參考:

7yAQj3.png

逆向目標

目標:2980 郵箱多種類驗證碼逆向分析

網址:aHR0cHM6Ly93d3cuMjk4MC5jb20vbG9naW4v

這個網站的驗證碼,會不斷變換,非常有意思,堪比一個驗證碼產品。目前遇到的種類有:滑塊、點選、旋轉、拼圖亂序、鐘錶,不知道還有沒有別的,不過不同類別的驗證碼加解密操作一樣,主要就是明文引數構造的不同。我們就來解決一下它的滑塊、點選、旋轉驗證碼,因為這幾個比較常見:

7yAqmj.png

流程分析

我們就以滑塊驗證碼來分析加解密操作,先抓包分析,發現首頁載入,驗證碼載入兩處地方都有 debugger

7y51T5.png

發現這兩處 debugger 的構造都是一樣的,不過在不同的 js 檔案中,可以發現它是透過函式的 constructor 來執行 debugger 操作,解決的方法很多,主要講兩種:

  • HOOK
(()=>{
    Function.prototype.__constructor = Function.prototype.constructor;
    Function.prototype.constructor = function(){
        if(arguments && typeof arguments[0]==='string'){
            if("debugger"===arguments[0]){
                return
            }
            return Function.prototype.__constructor.apply(this,arguments);
        }
    }
})()

相關知識,可以閱讀K哥往期文章:JS 逆向之 Hook,吃著火鍋唱著歌,突然就被麻匪劫了!

  • 檔案替換(推薦),可以直接使用瀏覽器的替換功能 ,操作如下:
function _0x49fb64(_0x4e04ef) {
    function _0x36c27c(_0x53f377) {
        var _0xbd8c62 = _0x2d52;
        if (typeof _0x53f377 === 'string')
            return function(_0x18f1bb) {}
            [_0xbd8c62(0x2d1)]('while\x20(true)\x20{}')[_0xbd8c62(0x353)](_0xbd8c62(0x222));
        else
            ('' + _0x53f377 / _0x53f377)['length'] !== 0x1 || _0x53f377 % 0x14 === 0x0 ? function() {
                return !![];
            }
            ['constructor'](_0xbd8c62(0x1f8) + 'gger')[_0xbd8c62(0x31a)](_0xbd8c62(0x526)) : function() {
                return ![];
            }
            ['constructor'](_0xbd8c62(0x1f8) + _0xbd8c62(0x25d))[_0xbd8c62(0x353)]('stateObject');
        _0x36c27c(++_0x53f377);
    }
    try {
        if (_0x4e04ef)
            return _0x36c27c;
        //else
        //  _0x36c27c(0x0);
    } catch (_0x5b38c0) {}
}

我們看滑塊驗證碼的圖片請求介面:

7y5Phm.png

發現返回的資料 mes 密文,相關 type 型別如下:

  • 21:滑塊;
  • 23:點選;
  • 16:旋轉;
  • ......

我們在來看看滑塊圖片的請求引數:

7y5RY4.png

發現有 token 、appid、k 需要解決,我們先搜尋,發現 token 、appid 是透過 getBehaviorRegister 介面返回:

7y5Ydh.png

7y5tw9.png

小結一下

目前我們需要逆向分析的目標有:

  • getBehaviorRegister 介面的 sign 值生成;
  • 圖片請求介面的 k 值生成;
  • 圖片請求介面的 mes 值解密。

我們一步步來解決。

sign 值

透過啟動器查詢,或者 xhr 斷點很快就能找到如下生成位置:

7y5zOY.png

更進 i 函式後,就會很明顯的看出其加密方式為 SHA256,明文 i 的構造比較簡單,就不分析了:

7y54iH.png

k 值

同樣的方法很快就能找到如下生成位置:

7y5JlZ.png

跟進去:

7y5XVU.png

7y5jXq.png

很明顯是 AES 加密,加密模式為 ECB:

  • ECB:Electronic Code Book(電子碼本模式),是一種基礎的加密方式,密文被分割成分組長度相等的塊(不足補齊),然後單獨一個個加密,一個個輸出組成密文。

後面傳入的引數分別是明文以及 key 值。明文部分為 fingerPrinterList[0x2] + ',' + fingerPrinterSon,都是指紋資訊加密生成,就不具體分析,生成位置如下:

  • fingerPrinterList[0x2]:

7y5ITs.png

  • fingerPrinterSon:

7y52wV.png

key 值 :

(_0x37cb01 + _0x134810)['substring'](8, 24)
  • _0x37cb01: 在 js 檔案中,可以固定;
  • _0x134810 :時間戳,與圖片介面請求時 params 中的 t 值對應。

mes 值解密

這個我們有許多方法定位,比如打 xhr 斷點跟值或者 HOOK 等等。其實上面分析 k 值的生成時 AES 加密的下面就是 AES 解密演算法的位置,我們其實都可以猜到。這裡選擇跟值,找到 'success' : 裡面的操作,很快就能定位到解密函式:

7y5FiJ.png

7y5d7G.png

第一個引數就是 mes 值,第二個引數 key 跟上面 k 值加密的 key 保持一致。

解密後的資料類似如下:

'{"dif":"0","answer":["9","未","雨"],"bg":"https://csmoss.duoyi.com/19/124bf7b8f82292","num":3,"sn":"9a363bc74a8a","type":23,"list":[]}'
  • 滑塊的圖片時亂序的,需要還原,其他的不需要,附上還原圖片測試程式碼:

    def split_and_reorder_image(image_path, reorder_array, split_ratios=(10, 1)):
        """
            
        :param image_path: 亂序的圖片路徑
        :param reorder_array: 滑塊圖片介面返回的 mes 解密後的 list
        :param split_ratios: 
        """
        # 開啟並讀取原始圖片
        original_image = Image.open(image_path)
        width, height = original_image.size
    
        # 按照指定的比例分割圖片
        split_width = width // split_ratios[0]
        split_height = height // split_ratios[1]
        images = [(i * split_width, j * split_height, (i + 1) * split_width, (j + 1) * split_height)
                  for i in range(split_ratios[0]) for j in range(split_ratios[1])]
    
        images = [original_image.crop(box) for box in images]
    
        reordered_images = ["" for _ in range(len(images))]
        # 遍歷reorder_array,根據其中的索引將images列表中的元素新增到reordered_images列表的相應位置
        for i, new_index in enumerate(reorder_array):
            reordered_images[new_index] = images[i]
    
        # 將重新排序的子圖片拼接回原始圖片
        new_width = split_width * split_ratios[0]
        new_height = split_height * split_ratios[1]
        new_image = Image.new('RGB', (new_width, new_height))
        for i, img in enumerate(reordered_images):
            x = (i % split_ratios[0]) * split_width
            y = (i // split_ratios[0]) * split_height
            new_image.paste(img, (x, y))
            
        new_image.save(image_path)
    

驗證請求

成功:{"message":{"pass":"cd5201bdb47748fe8b9114769d7122cb"},"code":1};

失敗:{"message":"not match","code":0}。

請求引數:

7y5eWB.png

分析一下:

  • token 、appid 上面介面返回,跟圖片介面保持一致;
  • portion、cn、timestamp、signature 需要逆向分析;
  • sn 圖片介面解密後返回;
  • 請求負載 需要逆向分析,這也是不同驗證碼型別的唯一區別。

直接從啟動器入手,點選進去就可以發現生成位置:

7y5oXt.png

  • portion、cn、timestamp、signature 分析如下:
portion = Math['random']()['toFixed'](2);

cn = md5('num' + parseInt(10000000 + Math['random']() * 1000000) + 'time' + new Date()['getTime']());

timestamp =  new Date()['getTime']();

// token 、sn 上面介面返回; :5b350044ac092f7bf3c2bc791638ca2f 檔案寫死
signature = md5(md5(portion + ':' + timestamp + ':' + token) + ':' + sn + ':1:' + cn + ':auth' + ":5b350044ac092f7bf3c2bc791638ca2f")
  • 請求負載:就是上面圖片中的 'data' 操作;發現也是進入到之前的 AES 加密函式中,明文由一堆環境加驗證碼相關資訊,JSON.stringify 之後生成;key 由固定值 + signature, 擷取 8 到 24 生成。

我們來分析一下不同驗證碼的明文有什麼差異,是如何生成的:

點選關鍵引數

  • slide:點選軌跡;

  • click_behavior:[點選座標 + 時間 + 順序];

  • portion,計算後點選座標,計算如下,就是和圖片寬高的一個比例:

# center_points 點選座標 
portion = []
for i in range(len(center_points)):
    portion.append([str(format(center_points[i][0]/320, '.5f')), str(round((center_points[i][1]+0.83332824707031)/200, 5))])

滑塊關鍵引數

  • slide:滑動軌跡;
  • portion:計算後的滑塊距離,比較特殊點, 計算如下, 原理就是不斷滑動滑動條,當計算出來的識別距離與我們真實的識別距離接近時,返回結果:
/**
 * 
 * @param x : ddddocr / cv2 識別距離
 * @param startposition : 這是獲取圖片介面返回的startPosition引數
 * @returns {{portion: number, value: number}}  : value: 滑動條滑動的距離, portion: 計算後的距離
 */
function get_slide_distance(x, startposition) {
    var _0x3c1ee0 = 72;
    var _0x3d74b9 = 1.11;
    var _0x4b4e13 = 0.74;
    var _0x1fda62 = startposition; // 這是獲取圖片介面返回的startPosition引數
    var _0x101145 = 1;
    var _0x3d3596;

    for (var i = 0; i < 999; i++) {
        _0x101145++;
        var _0x3d3596 = Math["pow"](_0x3c1ee0 * _0x101145, 0.4 * Math['abs'](Math['sin'](0.025 * _0x101145))) + Math["pow"](_0x101145, _0x3d74b9) * _0x4b4e13 + _0x1fda62;
		// _0x3d3596 :計算出來的識別距離
        if (Math.abs(_0x3d3596 - x) <= 1) {
            break;
        }
    }
    var portion = (Math["pow"](_0x3c1ee0 * _0x101145, 0.4 * Math['abs'](Math['sin'](0.025 * _0x101145))) + Math["pow"](_0x101145, _0x3d74b9) * _0x4b4e13) / 320;

   var  res={"portion":portion,"value":_0x101145}
    return res;
}

旋轉關鍵引數

  • portion:識別角度;

  • slide,旋轉滑動軌跡,是根據滑動條滑動的距離來計算軌跡,滑動條滑動的距離計算如下:

    int(portion * 0.6)
    

軌跡參考程式碼

# 軌跡
import random


def __ease_out_expo(sep):
    """
    緩動函式 easeOutExpo
    參考:https://easings.net/zh-cn#easeOutExpo
    """
    if sep == 1:
        return 1
    else:
        return 1 - pow(2, -10 * sep)


def get_slide_track(distance):
    """
    根據滑動距離生成滑動軌跡
    :param distance: 需要滑動的距離
    :return: 滑動軌跡<type 'list'>: [[x,y,t], ...]
        x: 已滑動的橫向距離
        y: 已滑動的縱向距離, 除起點外, 均為0
        t: 滑動過程消耗的時間, 單位: 毫秒
    """

    if not isinstance(distance, int) or distance < 0:
        raise ValueError(f"distance型別必須是大於等於0的整數: distance: {distance}, type: {type(distance)}")
    # 初始化軌跡列表
    slide_track = [
        [0, 0, -(distance * 20)]
    ]
    # 共記錄count次滑塊位置資訊
    count = 50 + int(distance / 2)
    # 初始化滑動時間
    t = slide_track[0][2]
    # 記錄上一次滑動的距離
    _x = 0
    _y = slide_track[0][1]
    for i in range(count):
        _y -= 1 if i % 9 == 0 else 0

        # 已滑動的橫向距離
        x = slide_track[0][0] + round(__ease_out_expo(i / count) * distance)
        # 滑動過程消耗的時間
        t -= random.randint(10, 20)
        if x == _x:
            continue
        slide_track.append([x, _y, t])
        _x = x
    slide_track.append(slide_track[-1])
    return slide_track

結果驗證

  • 滑塊:

7yV1Fh.png

  • 點選:

7yAkmb.png

  • 旋轉:

7yABhe.png

相關文章