MPQ文档布局分析[转帖]
MPQ文檔布局分析(以暗黑破壞神2的一個補丁patch_d2.MPQ和燃燒遠征的一個補丁patch-2.MPQ文檔為實例,以下簡稱D2,P2)
說明:因為MPQ實際上是由很多文件數據組成,包含很多文件,為了區別好MPQ文件與一般文件,這里將MPQ文件在作為一個集合的時候稱為文檔(ARCHIVE),表示實際存儲在硬盤上的一個文件時稱為MPQ文件(file),而文件都指保存于MPQ中的一般文件.
說明2:intel x86系統的存儲格式都是低位在前(Little-endian),比如一個int32型的數據44(00 00 00 2Ch)存儲在文件和內存中實際為2C 00 00 00h,下面不再說明.
說明3:以下以[]后綴中表示此類型一共重復幾次,以()前綴表示一個此類型所占字節數.比如(1)char[4]表示此處有4個1個字節的char類型數據.
說明4:本文主要信息來自The MoPaQ File Format 1.0.txt,只是附帶個人結合D2,P2后的分析,因為會經常提到原文件,以后簡稱其為MFF.另外也經常會提到http://www.zezula.net/en/mpq/mpqformat.html一文,簡稱為MFH.
說明5:在一段文本的開頭的十六進制數字都表示文件中的偏移地址,在段中,都以offset +十六進制數字說明此處表示在原文件的偏移地址.
文檔總體布局
-????????? 文檔頭
-????????? 文件數據
-????????? 文件數據 – 特別文件
-????????? 哈希(hash)表
-????????? 塊(block)表
-????????? 擴展塊表
-????????? 強數字標志(strong digital signature)
文檔頭:
00H: (1)char[4]: 指示這個文件是MPQ文件,肯定是ASCII 的 “MPQ” 1Ah.
D2前四個字節為4D 40 51 1Ah 吻合.
P2前四個字節為4D 40 51 1Ah吻合.
另外,從MFH中知道,當第4個字節(1)為1Ah的時候表示這里開始的是一個MPQ文檔頭,這里還可以為1Bh,表示這里開始的是一個MPQ shunt結構.
04H: (4) int32: 文檔頭文件的大小.
D2為20 00 00 00h,即32個字節.
P2為 2C 00 00 00h ,即44個字節.
08H: (4)int32: 整個文檔的大小,包括文檔頭信息.不包含強數字信息(假如有的話).在燃燒遠征中是從文件頭到文件尾的所有信息.
D2為25 1B 20 00h,即2104101個字節, 從顯示上來看,與文件大小一致,意思是此文件可能不包括強數字信息.
P2為10 CC 12 00h,即1231888個字節,從顯示上來看,與文件大小一致,意思是此文件可能不包括強數字信息.或者因為此文件為燃燒遠征文件,所以為文件大小.
0CH: (2)int16: 版本信息,表示的是MoPaQ的版本.當其為負的時候,MPQAPI將不會打開文檔.
已知版本:
原始版本:0,文檔頭應為32個字節,不支持大文檔.
燃燒遠征:1,文檔頭應為44個字節,支持大文檔.
D2為00 00h,即0,表示此MPQ文件為原始版本文件,另外,此文件的文檔頭從上面看的確為32各字節,吻合.
P2為01 00h,即1,表示此MPQ文件為燃燒遠征文檔,另外,此文件的文檔頭從上面看的確為44個字節,吻合.
0EH: (1)int8 扇區偏移大小(SectorSizeShift):指示的是每一個文檔的邏輯扇區的大小.總為512 * (2^扇區偏移大小).此處MFF指出,因為Storm library的bug,在其中規定只能為3,計算后為512* (2^3) = 4096字節.從MFH可以知道,這里的意思其實就是指一個未經壓縮的塊大小,通常為4KB.但是假如一個文件經過了壓縮,一個塊就是變長的了.
D2,P2中此處值都為3,也就是指示此文檔邏輯扇區的大小為4096字節,和Storm library的規定的一樣,可能因為大部分暴雪的MPQ文檔中此值都為3,所以Storm library才會因為不經意的存在此bug,而且就算有此bug也沒有太在意,因為不至于太影響使用.另外,在MFF中說此處為(1)int8,并且在以后也沒有說明offset 0FH處的數值含義,且D2,P2中都為00h,懷疑offset 0EH處值應為(2)int16,結合0F一起,為 03 00h,還是表示3,
10H: (4)int32 哈希表偏移值,指示從文檔開始,到哈希表開始地址的偏移量.
D2為65 0E 1C 00h 即1838693個字節, 從文檔大小2104101個字節來看,處于文檔的中部,并偏向尾部,與文檔總體布局一致,因為很明顯,文件數據和特別文件的數據才應該是占用文檔空間的主要部分.
P2為50 CB 12 00h 即1231696個字節,從文檔大小1231888來看,處于文檔的尾部,也符合以上推論,但是此是哈希表便宜值處于這么尾部而不是和D2一樣處于相對中部的位置,說明P2的哈希表,塊表,擴展塊表比較小,一共才192個字節.可能是因為P2中所含文件較大,而文件的數量較小,所以需要用來索引的內容較小,從后面的分析來看是正確的.
14H: (4)int32 塊表偏移值, 指示從文檔開始,到塊表開始地址的偏移量.
D2為65 0E 20 00,即2100837個字節,處于哈希表偏移值1838693和文檔大小2104101之間, 與文檔總體布局一致.并且從此可以得出結論,D2的哈希表大小為262144個字節.最后塊 Table 和擴展塊表加起來還有3264個字節.
P2為D0 CB 12 00,即1231824個字節, 處于哈希表偏移值1231696和文檔大小1231888之間, 與文檔總體布局一致.并且從此可以得出結論,D2的哈希表大小為128個字節. 最后塊 Table 和擴展塊表加起來還有64個字節.
18H:(4)int32 哈希表入口數量,必須是2的冪,而且對于原始版本MPQ文件必須小于2^16,對于燃燒遠征版本必須小于2^20.
D2中為00 40 00 00,即16384個,即2^24.
P2中為08 00 00 00,即8個,即2^3.
以上數據符合開始對于D2文件多,P2文件少的推論,結論有待下一步驗證.另外,從這里也可以算出每個哈希表入口的大小為128/8 = 16個字節,從16384 * 16 = 262144也可以得到驗證.
1CH: (4)int32 塊表入口數量.
D2中為CC 00 00 00,即204,
P2中為04 00 00 00,即4.
在MFH中提到,塊表的入口數量比塊的數量要大一,最后一個入口被用來得到最后塊的大小.并且每個入口為16個字節.
只在燃燒遠征(P2)及其以后版本才出現的文檔頭文件為:
20H:(8)int64 從文檔頭開始的擴展塊表偏移值,
因為D2此處已經是文件數據,從此一下不再說明.
P2中此處為00 00 00 00 00 00 00 00,即無擴展塊表.
28H:(2)int16 大文檔哈希表偏移值的高16位值.
P2中此處為00 00,因為P2并不是大文件,所以用不上此值.
2AH:(2)int16 大文檔塊表偏移值的高16位值,
P2中此處為00 00,因為P2并不是大文件,所以用不上此值.
最后通過20H到2AH上面的分析,可以得出結論就是原始格式的MPQ文件并沒有擴展塊表,因為那里沒有連擴展塊表的偏移值都沒有說明,另外,因為D2,P2中都沒有擴展塊表和強數字信息,所以塊表實際就是文檔的結尾,實際上通過塊表入口數量*塊表入口大小,也可以得出相同結論(16*204 = 3264 ,16*4 = 64)
在文檔偏移量為0的時候,文檔頭是文檔的第一個結構,然而,在包含文檔的文件中,文檔的偏移量不是必須為0.在文件中文檔的偏移量在這里被稱為文檔偏移量(Archive Offset),假如文檔不是文件的開頭,就必須開始與一個磁盤扇區的邊界(512個字節).
在MFH中又可以知道,當處理MPQ的時候,應用程序只需要一個一個查找0x200h,0x400h(即512的倍數)直到找到文檔頭,或者到達文件的末尾.這樣可以方便的將MPQ文檔儲存在其他文件格式中,比如EXE,這樣甚至可以在安裝文件setup.exe,install.exe中假如MPQ文檔.
早期版本的Storm庫需要文檔在文件的結尾(文檔偏移量 + 文檔大小 = 文件大小),但是在更新的版本中已經不需要了.(因為強數字信息不再被認為是文檔的一部分).
以上即為全部的MPQ文件頭分析.
塊表:
塊表包含文檔內每個區域的入口,區域可能是文件,空閑空間,或者沒有使用的塊表入口,其中空白空間主要來自于刪除的文件數據,可以被新文件覆蓋.空閑空間入口應該有非零的塊偏移值和塊大小.和為零的文件大小,Flags.未使用的塊表入口應該有為零的塊大小,文件大小,和Flags.塊表是使用”(block table)”為key的哈希算法加密的.
而各種數據實際上就是放在一個一個的由塊表指定的塊中.
首先,從文件頭已經知道,D2的塊表開始于00 20 0E 65h,P2的塊表開始于00 12 CB D0h.對比分析從上面兩個地址開始.為了更加明了,我將從MFF中的相對于塊表的偏移量,(對于每個文檔都適用)計算出絕對偏移量(當然只適用于D2,P2這兩個文件,不過對于分析這兩個文件來說,更加清晰,以下只分析了第一個入口).此表不是像我先想的那樣有表頭,從塊表的一開始就是塊的入口,和對此塊的描述,偏移4個字節以后就是第二個入口,以此類推.
相關的函數為EncryptBlockTable()加密, DecryptBlockTable()解密.
在加密解密的時候有兩個地方需要注意,而我開始沒有注意的是:
1.?????? 運行加解密算法前,需要先運行PrepareStormBuffer()函數,初始化好用來加解密的Buffer.
2.?????? Block加解密算法必須是一次從頭開始對一整塊數據進行,比如,可以一次解密第一個入口的第一個值(塊偏移值),但是不能單獨對第二個值(塊大小)解密.可以一次解密整個第一個入口的4個DWORD的值,但是不能單獨對第二個入口進行解密,簡而言之,在對后面數據解密的時候必須要有前面的數據.并且要得出正確的結果,那么前面的任何數據都不能有任何錯誤.很顯然,加密的時候也需要這樣,經過測試,的確如此.
第一個入口的結構如下:
00H:(4)int32 相對于文檔開始的塊偏移值.
D2中地址為00 20 0E 65h ,數據為AB 67 48 3DH,
P2中地址為00 12 CB D0h,數據為A7 67 48 3DH,
以上都為加密數據,但是其實我們是知道塊的偏移值的,文檔頭下面就應該是塊,D2中應該從32個字節以后,即從20H開始,P2應該是從44個字節以后,即從2CH開始.知道這些以后,正好可以驗證一下Strom庫的哈希算法經檢驗,用DecryptBlockTable解密數據后,和猜測吻合.
04H:(4)int32 塊大小.假如塊為文件,某些文檔表示這里就是壓縮過的文件的大小.
D2中的地址為00 20 0E 69H,數據為 B9 E4 08 CAH, 解密后為14104
P2中的地址為00 12 CB D4H,數據為 D9 0E 06 CAH, 解密后為974196
08H:(4)int32 儲存在塊中的文件數據大小,只有在塊是一個文件的時候有效;不然的話無意義,而且應該為0.假如文件是壓縮的,這里表示的是解壓后的文件數據大小.
D2中的地址為00 20 0E 6DH,數據為 7F BF 34 F8H, 解密后為85652
P2中的地址為00 12 CB D8H,數據為37 7A 5B F8H, 解密后為2089956
0CH:(4)int32 塊的位掩碼標志位.
以下為已經確定的數值:
80 00 00 00H: 接下來有表示文件數據類型的位值,那么這個塊是一個文件.不然塊是一個空閑空間或者未使用的空間.假如這個塊不是一個文件,所有其他的標志位應該為0.
01 00 00 00H: 文件作為單一單元儲存,而不是分成很多扇區.
00 02 00 00H: 文件的密鑰經過塊偏移和文件大小調整.文件必須是加密的.
00 01 00 00H: 文件是加密的.
00 00 02 00H: 文件是壓縮的.文件不能被imploded?
00 00 01 00H: 文件是imploded,文件不能被壓縮.
D2中的地址為00 20 0E 71H, 數據為75 DB 3B E8H, 解密后為80 00 01 00H,按上面的意思來看,這表示D2的此塊塊為一個imploded的未壓縮文件.
P2中的地址為00 12 CB DCH,數據為AD 16 3E EEH, 解密后為84 00 02 00H,上面的表沒有說明04 00 00 00H位表示什么意思,但是可以看看出此文件是被壓縮的文件.
哈希表:
為了快速存取,作為文件名的替代,MPQ中使用了以2的各次冪為大小的文件的哈希表.一個文件通過它的完整文件路徑名,語言和平臺唯一識別.一個文件在哈希表中的home入口用一個文件的完整文件路徑名的哈希計算得到.當此入口已經被其他文件占用的時候,使用了順序溢出方式(progressive overflow),這個文件被放置在下一個可用的哈希表入口.在哈希表中搜索一個想要的文件過程是,從home入口直到這個文件被找到,或者搜尋整個哈希表.或者是碰到一個空的哈希表入口(文件塊索引為FF FF FF FFH).哈希表通過以”(hash table)”為key的hash運算加密.
以下內容與塊表類似,也只看第一個入口,但是每一個入口的結構還是相同的.
相關的函數為加密哈希表:EncryptHashTable(),解密哈希表:DecryptHashTable(),文件完整路徑名哈希值計算方法1(A):DecryptName1(),文件完整路徑名計算方法2(B): DecryptName2(),得到一個文件的哈希表home入口:DecryptHashIndex(),得到一個文件真正的哈希表入口:GetHashEntry().
其中GetHashEntry()算法的在Blizzard的MPQ文件格式搜索算法 - GameRes游戲開發論壇.htm一文中有詳細描述:
“1.計算出字符串的三個哈希值(一個用來確定位置,另外兩個用來校驗)
2. 察看哈希表中的這個位置
3. 哈希表中這個位置為空嗎?如果為空,則肯定該字符串不存在,返回
4. 如果存在,則檢查其他兩個哈希值是否也匹配,如果匹配,則表示找到了該字符串,返
回
5. 移到下一個位置,如果已經越界,則表示沒有找到,返回
6. 看看是不是又回到了原來的位置,如果是,則返回沒找到
7. 回到3”
其中用來確定位置的哈希值,指的就是哈希表home入口.用DecryptHashIndex()函數得到,兩個用來效驗的哈希值就是用算法A和算法B得到的哈希值,分別用DecryptName1(),DecryptName2(),函數得到,另外這三個值都是僅僅用來比較,所以都是單向的,函數源碼的具體理解待理解源碼的時候再去看.雖然在strom庫里面被命名為DecryptName,其實是個從文件完整路徑名到一個DWORD整數的哈希計算過程,更像是HashEncrypt.
從上面文檔頭的分析可知:
D2的哈希表位置開始于00 1C 0E 65H. P2的哈希表開始于00 12 CB 50H.
因為需要一塊解密,所以4個字節的
00H:(4)int32 文件路徑哈希值A: 用算法A得到的文件路徑哈希值.
04H:(4)int32 文件路徑哈希值B: 用算法B得到的文件路徑哈希值.
08H:(2)int16 這個文件的語言.這是一個windows LANGID數據類型而且使用同樣的值.0表示是默認語言(American English),或者這個文件是語言無關的.
0AH:(1)int8 平臺:這個文件被用來使用的平臺.0表示默認平臺,目前沒有看到過其他的值.(很明顯應該為int16,估計是文檔中的錯誤)
0CH:(4)int32 文件塊索引:假如哈希表入口是有效地,這個是個到這個文件的塊表的索引.不然的話,可能為下面兩個值:
FF FF FF FFH: 哈希表入口為空的,而且一直是空的.結束搜索一個給定的文件.
FF FF FF FEH: 哈希表入口是空的,但是某些時候是有效的(意思就是說,這個文件以前被刪除了).繼續向下搜索給定的文件.
上面這個特性要說明的是,在MPQ中,一個文件被刪除以后,實際上并不能回收空間,僅僅是刪除了這個文件的路口,此時最后這個文件塊索引就標志為FF FF FF FEH了,那么這個文件后面還可能有文件,所以繼續搜索.補充一點的就是,雖然空間不能被回收了,但是當新的文件增加進來的時候,只要這個文件比原來刪除的文件要小,就可以放進空閑下來的空間.但是新的文件較大的時候新的文件將擴展.這點在頻繁刪除增加文件后會導致空間的很大浪費,唯一的解決辦法就是先讀出這個MPQ文檔的數據,然后重新構建一個新的MPQ文檔.感謝the MPQ API Library 2.0的創作者Justin Olbrantz(Quantam),他寫了一個mpqcompactarchive()函數,用來解決此問題.另外他還寫了另外兩個有用的函數MpqRenameFile(),用來改變MPQ文檔中已有文件的文件名, MpqAddWAVToArchive(),用來在添加WAV文件的時候對其進行特殊的更有效的壓縮.
D2中此入口的數據為:
00H:33 30 C3 79
04H:28 D9 32 98
08H:BC 73
0AH:6F 9F
0CH:B2 88 4E E9
解密后,D2中連續兩個入口都為全FF,沒有辦法判斷.用了很久時間也沒有辦法改變這一點,可能是因為文件刪除了,所以才會這樣..............
P2中可以得出比較合理的結果,文件語言和平臺都為0,塊索引在最開始的兩個哈希表入口中分別為0和2,所以推測是文件塊索引的表示不是相對于整個文件,而是相對于塊表的開始而言.(MFF中沒有描述),不然, DecryptHashTable()函數得出的結果都是錯的.
而這里又暫時還不知道具體的文件名,沒有辦法通過EncryptHashTable()函數來檢驗P2中得到的結果是否正確.
以下內容基本上翻譯自MFF,自己順面理解并記錄成中文.因為還沒有通過實際代碼編寫調用函數來檢驗.因為下面內容不像文檔頭和哈希表,塊表一樣,能知道存儲的是什么內容,光從文件中也不能反向計算哈希值前的文件完整路徑名,所以只能暫時通過這種方式了解一下文檔的大概結構.
文件數據:
每個文件的數據都由下面部分組成:
00H:(4)int32(文件中的扇區+1)扇區偏移值表(SectorOffsetTable): 相對于文件數據開始處每個扇區的偏移值.最后的入口包含文件的大小,使得它可能很容易的計算出任何給定扇區的大小.假如信息可以被計算出來,這個表是不會出現的.
扇區偏移值表后: 文件中每個扇區的數據,首尾相連的打包
通常來說,文件數據簡單的分開存在扇區里.所有的扇區,將包含文檔頭扇區偏移大小確定大小的文件數據.依據整個文件數據的大小不同,最后的扇區可能少于這個數值.假如文件是壓縮的或者imploeded的,扇區將比他包含的文件的要小.在壓縮或者imploded的文件的各個扇區中,也可能存儲這未壓縮的數據,當且僅當這個文件的這個扇區中的數據不能被需要的算法壓縮時.這時,在扇區偏移值表中的扇區值就等于在扇區中的文件數據大小.
各扇區的格式決定于扇區的種類.沒有壓縮的扇區僅僅是原始的文件數據.
Imploded扇區是原始壓縮過的數據,再經過implode算法計算過的值.
壓縮過的扇區中數據是用一個或兩個壓縮算法壓縮過的.而且有以下的結構:
00H:壓縮位掩碼:指示了這個扇區的壓縮種類,可以復合使用.它們以以下列表的順序應用,而且應該用相反的順序解壓.這個字節從總共的扇區大小計算而來,意味如果數據不能被至少兩個字節壓縮,扇區將不能作為未壓縮的方式存儲數據.也就是說,這個字節是用扇區數據加密的,假如可以的話.
??? 40h: IMA ADPCM mono
??? 80h: IMA ADPCM stereo
??? 01h: Huffman encoded
??? 02h: Deflated (see ZLib)
??? 08h: Imploded (see PKWare Data Compression Library)
??? 10h: BZip2 compressed (see BZip2)
01h: byte(SectorSize - 1) SectorData : 這個扇區被壓縮過的數據.
假如這個文件是作為單一單元存儲的(在文件標志中有指示),那么這里只有一個存儲了整個文件數據的單一扇區.
假如文件是加密的,每個扇區(假如可以應用的話,在壓縮或implosion以后)是用file的key加密的.基本的文件key(base key)是不帶路徑的文件名,假如這個key是修改過的(就像在文件標志中指示的那樣),最后的key通過((base key + 塊偏移值 – 文檔偏移值)XOR 文件大小)計算得到.(MFF原注:Strom庫中使用AND替代XOR是不對的).每個扇區都是使用這個key + 在文件中以零為基礎的扇區為索引.
假如存在扇區偏移值表(SectorOffsetTable)的話,是用值為-1的key加密的.
扇區偏移值表在大小和在文件中所有扇區的偏移值可以從文件大小中計算出來的時候是被忽略的.這可能有數種情況:
假如文件是沒有壓縮/imploded過的,這時,大小和所有扇區的偏移值在扇區偏移值大小(SectorSizeShift)的基礎上是已知的,假如文件作為單一的壓縮/imploded單元存儲,這時,扇區偏移值表被忽略,因為就如前面提到的那樣,單一文件的扇區與塊大小和文件大小相符.然而,假如文件是壓縮/imploded過的而且不是作為單一的單元存儲,甚至這個文件只有一個扇區,扇區偏移值表也要有.
文件列表:
文件列表是MPQ一個非常簡單的擴展,包含文檔中文件的(大部分)文件路徑.文件的語言和平臺不存儲在文件列表里面.文件列表包含在文件”(listfile)”(默認的語言和平臺)中,而且僅僅是一個簡單的文本文件,在里面文件的路徑通過’;’,0DH,0AH或它們的組合分隔.文件”(listfile)”可能不列在文件列表中.有了這個列表,就可以先通過這個列表了解文檔中一共包含了哪些文件,這樣就可以通過文件名的哈希計算來驗證前面得到的哈希表是否正確了.當然,假如不知道前兩個哈希表入口表示的是哪兩個文件,要么就如同算法一樣遍歷哈希表,要么就遍歷計算文件:)
附加屬性:
可選屬性,這些屬性在MPQ文檔格式已經確定了以后再加入進來,所以不是每個文檔都必須有所有(任何)這些屬性.假如一個文檔包含一個給定的屬性,將會有一個對每個在塊表中的塊分別適用的屬性值,盡管當這個塊不是文件的時候這個屬性值將沒有意義.屬性值的順序和塊在塊表重的順序一樣.屬性總是以數組的形式存儲在”(attributes”)文件中(默認的語言和平臺).屬性值對應的文件不是必須有效的.不像所有其他結構一樣,在附加屬性中入口不保證是相連的.并且,看到在一些文檔中,為了防止別人對文檔的查看,存在惡意的附加屬性位置移動.
此結構的意義如下:
00H:(4)int32 版本:定義附加屬性的版本,到目前位置肯定是100.
04H:(4)int32 存在的屬性: 在附加屬性中存在的位掩碼.
00 00 00 01H: CRC32文件
00 00 00 02H: 時間戳(timestamps)文件
00 00 00 04H: MD5文件.
08H:(4)int32 塊列表入口的CRC32值.文檔中每一個塊未壓縮的文件數據的CRC32.當文檔沒有CRC32的時候忽略.
在CRC32以后:? 文檔中每一個塊的時間戳.格式是windows的FILETIME格式.假如不存在,忽略.
在時間戳以后: MD5文檔中每一個塊的未壓縮文件數據的MD5值.沒有的話,忽略.
總結
以上是生活随笔為你收集整理的MPQ文档布局分析[转帖]的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 新电脑必备的4款宝藏软件,绿色、安全、无
- 下一篇: 关于计算机知识的趣事,最全计算机发展史