python爬蟲 - js逆向之扣出某平臺的_signature加密字段

前言

好久沒有做逆向案例分析了,最近都在看同行朋友寫好的案例,感覺學到很多,算是取長補短了

不多bb,機緣巧合下,拿到個目標網站

 

aHR0c{請刪除大括號及其內容,防搜索}HM6Ly93d{請刪除大括號及其內容,防搜索}3cudG91dG{請刪除大括號及其內容,防搜索}lhby5jb20v

 

分析

首先抓個包,就看到請求參數裏帶着這幾個參數

 

 

 

 

主要就是_signature了,其他的參數都不重要得想必你一看就知道啥意思了。

 

行,開始分析_signature了

 

先,全局搜一下,很好只有一個結果:

 

 

 點進去看到主要就這三個地方:

 

 

 

三個地方都打上斷點:

 

 

 

繼續滑動下拉,發現值已經有了:

 

 

 

鼠標放到變量上面,看到我們需要得_signature其實就是變量a了,而a由函數I(n,e)生成:

 

 

 

其中,n就是V.getUrl生成,e就是傳進去的參數:

 

 

 

那取消剛纔的斷點,重新打上斷點看看:

 

 

 

 

此時,可能你在調試的時候,有時候會出現滑塊驗證:

 

 

 

這個暫時不管,以後再系統的搞滑塊哈

 

 

此時發現斷上了:

 

 

 

跟進去,發現V.getUri進入到如下,感覺是在處理參數,不知道有沒有我們要的sign

 

 

 

 怎麼確定有沒有,放開斷點往下走,如果出現了那就是了,如果沒有出現,那多半就不是了,如下:

 

 

 這個n的值明顯沒有sign,那就不是了

當然這個理論在這裏行得通,但不絕對哈

 

好,接着看I,跟進去,發現如下,這裏的e就是上面生成的n,t就是上面傳進來的參數e,就是而且看到裏面有sign相關的變量,90%是了

 

 

 

單步調試,一點一點跟着走看看:

 

 

 

找到關鍵點

 

發現上面都是些沒有什麼用的配置,就到了倒數第二行,貌似纔有關鍵的東西,而且sign也在這一行裏,先在這一行打個斷點,然後繼續走,一點放行,發現走到一個新的js裏:

 

 

 

大概的看了一眼,應該是個加密算法,那是不是就是我們要的sign呢?不好說啊,直接點這個跳出去吧:

 

 

 

跳出去之後發現,變量o出現了一個很長的字段

 

 

 

 

 

 

 

拿着跟之前瀏覽器抓包看的_signature對比下長度,發現一致,那就是這個了,而實際的加密邏輯就是剛纔進入的新的js文件alcrawler.js裏

關鍵點找到了,那怎麼把核心的加密邏輯扣出來呢?

 

我大概看了下,這裏好像還不好單獨把加密的邏輯摳出來,因爲有很長的參數調用,而且我摳了下,不好扣,索性整個拿出來吧,結果發現整個也不多,就幾百行,複製到本地吧

 

代碼調試

放在本地,取名crawler,用node執行下看看:

 

 

 

發現報了個這個:

 

 

 

什麼referrer,那我們搞爬蟲的,再熟悉不過了,但是這個是在js裏面啊,這就是涉及到補環境了,那麼referrer屬於哪個js對象裏的呢,這個沒法展開說了,我就直接說,referrer是document對象裏的,js的全局對象有,window,doucument,navigator,global,location

行,這裏補齊如下,地址給個主站的地址就行了:

 

 

 

繼續執行,報錯了:

 

 

 

但其實,懂js的朋友應該知道,其實window.document可以簡寫成document,這個就你們自己去研究爲什麼了,如下寫,執行立馬不報錯了:

 

 

 當然也可以把window也補一下:

 

 

就不會報錯了:

 

 

 

 

那現在我們要把那個生成sign的邏輯拿出來用下,怎麼用呢?回到上面這個關鍵的有sign字段的那一步:

var o = (null === (n = window.byted_acrawler) || void 0 === n ? void 0 : null === (a = n.sign) || void 0 === a ? void 0 : a.call(n, i)) || "";
            

  

這個懂點js的都不陌生,我們拆開來看:

先看最外層,最外層的括號,如果括號裏沒有值,那就給o一個空字符串

 

 那根據上面的斷點邏輯,肯定不會是空的,直接省略下,變成如下:

 

 先把var o 刪了:

 

 

再通過||符號拆下:

 

 第一行,null肯定不會全等於(n=window.byted_acrawler)的,所以會走後面的邏輯,但是這一步,同時把window.byted_acrawler複製給了變量n

再看第二行,第二行是個三目運算,首先,0 ==== n肯定是不成立的,直接走後面的null=== (a = n.sgin),這個邏輯就跟第一行類似了,反正最後會把n.sign複製給變量a

再看第三行,這個跟第二行類似,同樣的,會走到最後a.call(n,i)

 

不信可以看看這個下面:

先做簡單的替換,本質的運算邏輯是沒有區別的

 

 意思就是,那麼長一句,最後會直接執行:window.byted_acrawler.sign.call(n, i)

那麼,這兩個參數,n,i是啥,先看看n:

 

 

 

再看看i,i就是個url的路徑,沒有帶參數的那種

 

 

ok,i變量好說,這個n變量就有點不好搞了,這樣,直接在控制檯看下需要啥參數:

 

 這個報錯就很有價值了,它只需要一個url就行了,根本不需要那個什麼n變量,那就好說了

 

如下測試,發現不行,需要一個帶有url屬性的object對象 

 

 那行,整一個:

 

 

蕪湖,出來了,行的,就是這麼調用,放到本地掉就完了

 

結果一執行,完蛋,臥槽

 

 

報錯的意思就是,這個對象沒有sign屬性,很奇怪啊,在控制檯都可以用的,在node裏不能用,那說明有檢測環境的,把那個檢測環境的部分改下試試,先把代碼縮一下:

 

 

先看那個三目運算,複製到控制檯執行看看:

 

 

 

實際就還是window,那就改成window:

 

 

臥槽,這裏纔看到是jsvmp啊,這他媽,大名鼎鼎啊,說實話我有點慌了,本篇博文到此結束?

 

 

 

不不不,還是要掙扎下的,先把它當作普通的函數看待,先看,上面主要的兩段代碼,第一段是定義,第二段是調用,最後的console打印是我自己加的

 

那行,那看看參數有沒有問題:

 

 

定義的時候用的b,e,f

看看下面穿的參數是啥,好傢伙,不看不知道,一看傳了這麼多的東西,用sublime 打開看到:

上面一大段全是b變量,後面的中括號裏的值,最後會變成e和f變量,而這裏面又有三目運算符

 

 

把三目運算符整理下,先看第一個:

 

 

 

 

 那說明,這個三目運算符就是void 0了,把這相關的都替換成void 0,搜索看,只有一個,

 

 

 替換之後再執行,貌似剛纔那個sign屬性解決了,但是又出現了新的錯

 

 

這個一看還是補環境的問題了,把這個補了,href是location對象裏的,補完又發現新的報錯

 

 

到這一步的時候,因爲報length的話,大概率是補環境除了問題,那麼說明剛纔的href沒有補對,那我們直接再目標網站的控制檯copy一下,

 

 回車即可,一定要在目標網站的控制檯裏copy,copy完執行,至少當前的問題解決了,再搞新的問題

 

 

這個userAgent就再熟悉不過了,咋辦呢?也直接copy吧,因爲userAgent屬於navigator,直接如下copy:

 

 再次強調,目標網站控制檯裏執行

copy完放到代碼裏執行看看:

 

 

 

 

發現,臥槽,終於tmd沒報錯,而且有結果了

 

但是,這個長度好像不大對勁,短這麼多,好像差點啥,到底差什麼東西呢

 

仔細推敲,網上也查了相關的,有說補齊cookie的,我補齊之後執行的結果還是很短,所以,應該還有什麼東西沒有注意到的

 

在目標網站的控制檯裏執行,就是可以拿到很長的字段,這就很騷了

 

 

 

那我覺得應該還是環境的問題,應該有個我們忽視了的地方

 

先打印window看看:

 

 

location基本沒有太大區別:

 

 

 不一樣的主要是window.document和window.navigator,以及window.localStorage,但是恰恰這三個對象是沒法直接copy的:

 

 

因爲你發現,粘貼出來的要嘛是undefined,要嘛是{}:

 

 

 

 

這他媽就很秀了,難道這就是jsvm的威力嗎?

 

我另開一個瀏覽器標籤,把剛纔摳出來的代碼放到控制檯執行,然後測試看看:

 

 首先,至少說明,補的基礎環境沒問題,就差一些特徵值了

 

 

再看,目標控制檯裏的這個arguments,

 

 

 

 

新開的控制檯的這個arguments的值:

 

 

 

所以這裏就看出區別了,目標控制檯裏多了個這個:

 

 

但是就不知道是不是這裏不同導致的原因了。

 

跟着斷點接着走,新開控制檯:

 

看這個c值

 

 

 

 

 

目標的控制檯裏的:

看這個c值:

 

 

 

 差距也太大了,而且c就是window.document對象,也就是上面沒法copy對象其一

 

而且,新開標籤頁,走到後面進入到了這裏:

 

 

目標控制檯,進入到了這裏:

 

 

能走不一樣的原因就是,這裏的B[e]

 

 目標控制檯的B[e]是空的,所以,G穿的最後一個值是0,而新開控制檯的B[e]有值,所以傳的最後一個參數是1,也就導致上面走了不同的邏輯

 

 

那麼這個B到底是啥:

 

 

我去,這他媽的,最後經過我的調試,發現,主要是穿的這個參數的不同:

 

 導致取值不同,上面是目標控制檯的,下面是新標籤頁控制檯的:

 

但是這一個值的變化,不是我們能控制的,寫死也是沒用的,唉,這就是jsvmp的強大嗎?唉,想想後怕了。

 

 

但是中途放棄不是我的作風,我就不信了,我開始在漫無目的的找特徵,回到最底部調用部分看這個window對象,突然的看到localStorage部分,我激動了,這個是目標網站裏的:

 

 

新標籤的控制檯,window部分,明顯感覺有問題對吧,

 

 

 

但是,剛纔我們分析的,就是那個this裏的window對象不一樣,那麼我們儘量的去貼靠原網站的window裏需要的值,我們給賦值下localstorage,用copy看看呢:

 

 

哎,發現這個倒是可以複製哈

 

 

 

 

把這段封裝成一個自執行函數:

 

 

 

放到新標籤的控制檯裏,然後放到控制檯裏執行,回車,再訪問下驗證是否成功了,發現可行的

 

 好,現在再在控制檯執行下sign:

 

 

 

 

把localStorage部分放進去之後,再次執行:

 

 

 

 

 

臥槽,說實話,有點小激動,至少這個長度看着很像了,就不知道能不能用了,在代碼裏看看呢?

 

直接複製剛纔的sign生成好的字段,執行測試,臥槽,牛逼啊,數據結構終於有了

 

還沒完哈,現在要搞一個在本地能夠直接生成的,而不是每次生成需要去瀏覽器的控制檯執行再複製出來的

 

當把那段自治性函數放到node環境裏執行的時候:

 

 

很奇怪,提示的是沒有setItem這個屬性,這咋辦呢?

嘗試jsdom

理一下思路,目前就差一個localstorage的賦值了,但是上面的代碼的setItem無法用,因爲localstorage對象是window對象裏的,那麼我們直接用node僞造一個window吧,咋僞造呢?用jsdom,安裝nodejs就不說了,網上一堆教程

 

在本地搞一個node項目,npm init命令初始化後,然後執行命令npm install jsdom安裝jsdom,具體過程也省略了,網上教程同樣一堆

現在創建一個js文件,把window對象引入,這裏注意一下,因爲我們要用localstorage,new JSDOM的時候必須要給個主域名,不然沒法用localstorage

 

 

測試下現在setItem成功沒有:

 

 

 

ok了,現在把剛纔摳出來的代碼整合到一起,結果出現了這個,臥槽,心累啊

 

 

說明這段代碼還驗證了其他很多東西導致這個sign屬性沒有正常賦值,換路子吧

 

本地html文件生成

 

把摳出來的代碼放到一個html文件裏,同時要注意的,url裏的時間戳,必須要跟sign生成時傳進去的url裏的時間戳保持一致,不然用不了

 

 

ok,用pycharm自帶的輕量服務器執行查看:

 

 

點擊那個谷歌瀏覽器圖標,自動打開並展示如下頁面:

 

 

拿到這兩個值去請求測試,哭了,這他媽終於有數據了

 

 

 

那會過頭想想,這個沒法運用到實際啊,這個第一,還是用了瀏覽器自帶的window對象,第二,這個服務端是pycharm,還不好控制,就算把這一部換成flask,用requests去請求這個接口,拿結果?也不行啊,爲啥,因爲請求拿到的是源碼,這裏的時間戳和sign是js生成的

 

要走這條路的話,只有用瀏覽器驅動,puppeter或者selenium了,那這裏就會有人說了,這都上瀏覽器驅動了,那還摳啥代碼啊,直接一開始就用瀏覽器驅動了唄,是的,所以這套路也不是我喜歡的

 

怎麼辦

 

構造localStorage對象

 

上面試了兩個路子都不行,那究其原因就還是那個setItem沒法用,沒法用的原因是我們構造的window對象裏的localStorage不是正確的對象,這裏構造一個出來,行不行呢?理論上是可以,試下:

 

 

window.localStorage = {
    removeItem: function (key) {
        delete this[key]
    },
    getItem: function (key) {
        return this[key] ? this[key]: null;
    },
    setItem: function (key, value) {
        this[key] = "" + value;  // 將數字轉爲字符串
    },
};

 

把這段代碼放到最開始摳出來的代碼裏,並在執行setItem前面,代碼如下:

 

 

執行看看,激動萬分啊,這個長度,看着就跟目標網站出來的sign長度一致了

 

 

放到程序裏執行測試:

 

 

執行,哇的一聲就哭出來了,ok

 

相關代碼:

js:

window = global;
window.document = {
    "referrer": "https://www.xxx.com/"
}
window.location = {
    "ancestorOrigins": {},
    "href": "https://www.xxxxx.com/?wid=時間戳",
    "origin": "https://www.xxxx.com",
    "protocol": "https:",
    "host": "www.xxxx.com",
    "hostname": "www.xxxx.com",
    "port": "",
    "pathname": "/",
    "search": "?wid=時間戳",
    "hash": ""
}
window.navigator = {
    "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
};
window.localStorage = {
    removeItem: function (key) {
        delete this[key]
    },
    getItem: function (key) {
        return this[key] ? this[key]: null;
    },
    setItem: function (key, value) {
        this[key] = "" + value;  // 將數字轉爲字符串
    },
};
(function () {
    var localStorage = {
        "__tea_cache_tokens_2018": "{\"user_unique_id\":\"xxxxxxxx\",\"web_id\":\"7024414886298617379\",\"timestamp\":1635928539118}",
        "__tea_cache_tokens_24": "{\"web_id\":\"7024409476728423973\",\"user_unique_id\":\"7024409476728423973\",\"timestamp\":1635907441035,\"_type_\":\"default\"}",
        "__tea_cache_first_24": "1",
        "tt_scid": "qzapI-RUipcVl.K1CCJHv1H1h5OgJIY8XzMvPoF2aVdVW3ZvjvLViXEDvIDwHXPtfa04",
        "__tea_cache_first_2018": "1",
        "_byted_param_sw": "tmXeQzPoDDcIho6jKG8=",
        "ttcid": "xxxxxxxx"
    }
    for (var p in localStorage) {
        window.localStorage.setItem(p, localStorage[p])
    }
})()
// 摳的js部分,自己補充了

 py:

import execjs
import requests
import time


def get_sign():
    tt = int(time.time())
    f = open('crawler_end.js', encoding='utf-8') # crawler_end的內容就是上面的代碼
    js = f.read()
    f.close()
    js_obj = execjs.compile(js)
    sign = js_obj.call('window.byted_acrawler.sign', {
        "url": f"https://www.xxxxxxx.com/api/pc/list/feed?channel_id=0&max_behot_time={tt}&category=pc_profile_recommend"
    })
    return tt, sign
headers = {
    'accept': 'application/json, text/plain, */*',
    'accept-encoding': 'gzip, deflate',
    'accept-language': 'zh-CN,zh;q=0.9',
    'cookie': '你自己的cookie',
    'referer': 'https://www.xxxxx.com/',
    'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.54 Safari/537.36',
}
tt, sign = get_sign()
url = f"https://www.xxxxxxx.com/api/pc/list/feed?channel_id=0&max_behot_time={tt}&category=pc_profile_recommend&_signature={sign}"
req = requests.get(url, headers=headers)
print(req)
res = req.content.decode('utf-8')
print(res)

  

 

 

 

 

2021-11-05更新

以上方案已經不行了,大概9月份的時候還能用,從大概10月的時候開始,必須要去摳jsvmp了,目前jsvmp的資料不多,爲數不多的有這個,大家可以參考下

https://mp.weixin.qq.com/s/mH_9FpJsHLSJj6-APn_54w

 

但是這個大佬說的沒有太詳細,不過方法都有了,以後有空再搞吧,jsvmp,確實強,現在搞起來確實需要花很多時間研究了

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章