[QLIE] 封包接口Hook
[QLIE] 封包接口Hook
這個主題快拖了半個月,中間一直沒空寫,今天看著實在有點久了,必須寫一寫了,不然就快忘記了。
起因
前不久 Happy Live Show Up 發了官中,但是又搞的奇奇怪怪的加密,很是無聊,稍微調了下和之前 ハミダシクリエイティブ 官中是挺像的。
steam的dll是用Themida保護的,其實就是Themida自己手動加載dll,目錄下那個dll實際上是沒用的,不過和內存里dump出來的是一樣的,和Steamworks官方提供的dll好像也是一樣的,這都不算什么,畢竟Alpharom十多年前就玩過了,小套路而已,但是這玩意比Alpharom強的是懂得代碼虛擬化,調著真是依托shit啊。

不過GitHub好像是有幾個項目能還原Themida的虛擬機,但是我本身不怎么研究脫殼加殼,要搞這個也得有點時間,而且就算搞定,也沒用,因為我就沒研究過steam游戲的驗證機制 :),他要是稍微搞點一般steam emulator過不了的東西那我也搞不懂,到頭還得研究steam驗證機制誰有空啊。
ハミダシクリエイティブ只能說是湊巧,steam驗證上沒什么難度,但是自己開了個線程瘋狂檢查干掉就結束。
反正 Happy Live Show Up 官中是看著頭大,對Themida加載的steam dll全部api都hook重定向到Goldberg貌似是能過steam驗證的,但是返回到游戲exe里然后跳到虛擬化代碼里饒了幾圈就寄了,看著像是被檢測到了,中途還能看到檢測steam.exe是否存在的各種奇奇怪怪的檢測,函數調用和返回瘋狂往虛擬機里跳,從游戲exe走到,SteamAPI_RestartAppIFNecessary居然能把x64dbg的tracing的默認5w步走完還多8000多步,根個病毒一樣,一般這個步數撐死都不會超過500,有多慢就可想而知了。

后面發現把他檢測線程干掉了,能正常走到游戲顯示啟動項的地方,但是提示讀取資源錯誤。感覺要么是有其它檢測,要么是被檢測到隨機返回了錯誤的數據給游戲。

至于具體是什么問題就很難判斷了,因為我這根本沒steam賬號,根本跑不起來這個游戲,跟蹤更沒可能了。還是丟回收站,右鍵清空回收站實在。
好吧,先別急著丟,看看能不能直接把封包結構逆出來,直接解包一步到位。
稍微去GitHub看了下QLIE的封包結構,再次對比官中的封包,看著結構很像,其實不然,官中好像只有一個封包能用garbro讀出幾個文件名,解包也是意義不明的數據,看著就像是改過了封包結構。
ok別管,開始跟蹤解密流,跟蹤的時候就明了了,官中的封包有點像是之前Malie的形式,在原格式的外面套了一層加密,通過Hook 讀取文件的WinAPI和開辟堆空間的API來實現一個自己的文件系統,對封包文件的外層加密在游戲讀取的時候解密掉,在游戲引擎看來就像是沒加密,想著估計只是對封包的索引加密了,我先把解密后的封包文件頭搞出來,然后把hash表搞出來,然后一拍腦袋發現不對,這東西整個都加密了,太狠。看著單純提取封包的結構怕是沒辦法還原出來解密了。只能跟一下解密算法了。
下面就是解密算法的一部分,粗略統計應該有七次解密循環,只有第一次的xor是良心的,后面都是一坨。而且這么多重的解密應該也不像是自己寫的,可能是套用了什么現成的算法或變種,當然具體不清楚,唯一清楚的是,這東西跑起來巨卡。后面剛好網上有人放了破解補丁,但是我已經對破解不感興趣了,因為這玩意的數據加密,再加上QLIE初始化的時候會遍歷全部封包的文件,導致游戲一打開卡的一坨。雖然是單核戰士,并不會給你電腦卡死,除非你是單核的電腦,但是游戲確實卡。

躺平
無所謂,沒人會想去理清楚,別人隨手在地上畫的亂七八糟的線條
好吧他確實是懂加密的,不僅加了能讓游戲卡死的數據加密,還加了能讓電腦累死的虛擬機,順便開了個線程檢測,專業?。m然基本都是themida的功勞)
可是他沒有QLIE的源碼,這就從本質上決定了,加密的無效性。當然其實有源碼也很難大改,如果真的改了那就不是QLIE了,當然這個只是站在程序的角度,如果站在系統的角度,那同樣也是從本質上決定了加密的無效性。
原理
那么為什么說是從本質上決定?
就這么說吧,比如你這小子特別欠打,我邀了幾個人放學后在校門口懟你,假設只有一個校門,而且圍欄都通電,你只能從這一個校門出。那我搬一個凳子坐在校門口,就能蹲到你,當然你不出來可以嗎?可以完全可以,那游戲也別運行了。
這個校門就像是游戲的文件接口,通常的游戲特別是Gal這種,會有封包,封包就是一種自定義的文件系統,試想你自己定義了一個封包,然后你要讓游戲引擎讀取,你是不是得提供函數或者接口才能讀???你不提供接口那游戲怎么讀取你的封包?反正我是不知道。所以必然有這么一個接口給游戲引擎讀取,不管你的封包加密有多么復雜,你也不可能帶著加密的文件一路跑到結束,就算你的游戲引擎說,我一幀能跑就行,全程帶著加密跑,ok完全ok,但是當你調用圖形api的時候系統api的時候你還怎么帶著加密文件一路跑?顯然是不科學和不可能的。
通常來說一個游戲有自己私有的封包或圖片文件格式,在游戲引擎里都有對應的讀取封包和解析圖片文件的方法,這其實是廢話,就和媽媽生的一樣,當然你要說克隆那我也沒辦法了。
所以對于一個單機的游戲,無論封包是什么加密,有多么復雜,在游戲引擎里必然有解密的方法。這和你網上下個7z壓縮包要密碼那可完全不一樣。封包是加密了,但是解密的東西也給你了,只不過這個解密的東西是在一個exe里,并且是編譯過了,全是機器碼,所以就看你有沒本事從這一堆機器碼里找到解包的算法了。如果是你網上下個7z壓縮包要密碼,那就只能硬猜了。
收集
開始逆向前得多收集信息,往往這些信息可以起到事半功倍的效果。
由于我一開始把QLIE當成了C/C++開發的,一個jmp干過去,dump完了數據,準備收工,后來仔細思索發現有點不像是C/C++開發的,一查是Delphi,那這不就妥了。

來請IDR https://github.com/crypto2011/IDR

這可比IDA爽多了。很多函數名已經還原了,接著導出這些函數名和標記到IDA

可以發現封包的處理是屬于TFilePack THashFile這種名稱函數的,由于Delphi我甚至都沒用過,只是在函數調用約定里見過Pascal調用這種,具體細節不是很清楚,不過一開始我已經分析到了QLIE循環讀取封包里文件名的地方,所以從這個地方往回翻,跟蹤Create Init Get這種后綴的函數大致可以推斷出結構,由于我們的目的僅僅是調用接口解包,所以沒必要全部分析出來,除非你想移植QLIE引擎。

QLIE Extract
那么大致解釋一下,有興趣可以自己下一個來跟蹤,挺簡單的。
數據和實現也已經開源,可以從這里下載到 https://github.com/Dir-A/QLIE_Extract
調用約定
首先需要了解一下調用約定
不知道是Delphi的原因還是編譯器的原因,里面的所有的函數傳參都很詭異,不過大致符合Pascal的傳參,但又不是,第一個參數是通過eax,第二個edx, 第三個ecx,然后都通過棧傳遞,但是遵循Pascal的順序。如果要C語言調用需要轉換成stdcall或fastcall這種。
初始化封包

LPTPack_PackEntry THashFilePack_Create(LPTPack_InitTable pFileList, CHAR cUn0, LPTPack_Buffer pKeyFile, CHAR cUn1, CHAR cUn2, PCWCHAR lpPackName)
THashFilePack_Create 這個函數是用來初始化封包的,可以理解為打開封包,打開成功則返回TPack_PackEntry對象指針,內部的實現會去讀取封包里的全部文件名。函數調用結束后緊接著就保存返回的對象。
游戲啟動的時候會多次調用這個函數,初始化全部封包,保存全部的TPack_PackEntry

當然下面的結構不一定是對的,因為我只關心提取文件,沒有把全部成員的具體作用都觀察完
typedef struct TPack_Handle
{
PDWORD pTable;
HANDLE hFile;
PWCHAR lpPackName;
}*LPTPack_Handle;
typedef struct TPack_ResEntry
{
DWORD dwOffset;
DWORD dwUn0;
DWORD dwCompSize;
DWORD dwDempSize;
DWORD dwCompFlag;
DWORD dwEncFlag;
DWORD dwDecKey;
}*LPTPack_ResEntry;
typedef struct TPack_ResIndex
{
PDWORD pTable;
LPTPack_InitTable pTPack_InitTable;
LPTPack_Handle pPackHandle;
LPTPack_ResEntry* pResInfoEntry;
PWCHAR* pResNameEntry;
DWORD dwResCount;
}*LPTPack_ResIndex;
typedef struct TPack_PackEntry
{
PDWORD pTable;
DWORD dwUn0;
PDWORD dwUn1;
LPTPack_ResIndex pResIndex;
WCHAR* lpHashFileName;//L"GameData\\data1.hash"
WCHAR* lpPackFileName;//L"GameData\\data1.pack"
}*LPTPack_PackEntry;



從這里開始就可以導出全部的文件名了。
通過TPack_ResIndex的pResNameEntry和dwResCount成員就可以得到文件名和文件數量信息


讀取資源
繼續跟蹤,觀察到THashFilePack_Get這個函數,在調用之前,調用了Exists來查詢文件是否存在,并且該函數只有兩個參數非常簡單。注意,這時候游戲已經初始化完畢,停在啟動設置上,需要點開始游戲才會讀取文件。
TPack_Buffer *__usercall THashFilePack_Get(TPack_PackEntry *pPackEntry, void *lpFileName)
第一個就是之前 THashFilePack_Create 返回的TPack_PackEntry,第二個就是要讀取的文件名,成功則返回一個TPack_Buffer對象指針,Hook這里就可以動態dump文件,當然我們的目標不止于此。
typedef struct TPack_Buffer
{
PDWORD pTable;
PDWORD pBuffer;
DWORD dwSize;
}*LPTPack_Buffer;

釋放資源
現在這個函數的兩個參數我們都有,那么是不是意味著我們可以使用該函數來提取全部文件?確實,參數齊全,但是還有一步很重要,就是該函數返回的對象如何析構,也就是在內存中申請空間要如何釋放?如果不找到釋放的地方,我們就算可以成功調用該函數來提取文件,也會出現內存溢出和內存無法回收的情況。

先IDA里看看能不能撿漏,看著好像沒有,那只能往上翻了,按一下X發現很多引用,還是回到x64dbg往下跟蹤吧,實在不行還能下斷點。不過運氣比較好,第二次返回RET就看到了叫 TObject.Free

發現就是調用pTable指向的一堆函數地址的表里的個函數,調用之后buffer和size都清空了

ok至此釋放的函數也找到了。
調用接口
這里我直接把THashFilePack_Get的其中一個參數的文件名改一下,然后用Garbro來解包看看返回的對象的buffer是不是對應文件名的那個文件。

改了后發現直接讀取失敗了,難道有什么蹊蹺?仔細觀察發現它的字符串是Pascal格式的,即前面有一個長度標識把這個長度也寫上就正常了。但是這樣每次都要計算一下長度生成Pascal格式的字符串也不是很方便。
那就繼續跟蹤,進來發現是封裝了TFilePack_Get和TFilePack_Get0這兩個函數,TFilePack_Get的第一個參數是LPTPack_ResIndex,第二個是一個序號,對應為 pResNameEntry 里的順序,比如我要讀取第一個文件,傳0就可以了,第三個參數固定1,返回值就是LPTPack_Buffer了。
TFilePack_Get0則是對TFilePack_Get的封裝,里面其實是用另一個函數來查序號。


好了,現在我們只需要調用TFilePack_Get傳文件名和序號就可以提取全部資源了。

當然保存完成文件后別忘了釋放。


結束
官中和日文版都是通用的,官方只是hook了日文版里的一些函數來實現自己的加密,所以文件應該也是差不多的。
由于新版本的QLIE是采用Unicode編碼來處理文本的,那要把官中的文本搬到日文版就改兩字節和復制文件的事。
反正最終分離出來,除去視頻文件,就30多mb大的補丁。


浙公網安備 33010602011771號