Steamファイル パート1-GCF / NCF

スチームロゴ 前の記事で約束したように、リバースエンジニアリングと長時間のブレインストーミングによってアンチスチームコミュニティが開くことができるSteamインフラストラクチャの一部についての記事を公開し始めています。



最近まで、GCFファイルはVALVEがリリースしたすべてのゲームの標準であり、他のすべてのゲームはNCFでした。 これらのファイル自体は、いくつかの保護レベルを持つファイルシステムイメージを表します。 NCFとGCFの違いは、前者にはヘッダーのみが含まれ、それらに属するファイルは別のディレクトリ( <Steamディレクトリ> / SteamApps / common / <ゲーム名> )にあることです。 したがって、GCFについて説明し、後でNCFのすべての機能を説明します。



この記事では、これらのファイルの構造を詳細に分析し、ライブラリの例を使用して作業します(リンクは記事の最後にあります)。 最初は非常に退屈です-構造の説明とそれらのフィールドの目的。 最も「おいしい」ものは彼らの後になります...



ここのすべてのコードは、Steamライブラリのリバースエンジニアリングの結果です。 ファイル形式に関する情報のほとんどはオープンソースから取得されましたが、少し補足し、キャッシュファイルでの作業を大幅に最適化しました(当時最も人気のあったHLLIBライブラリと比較しても)。



一般的なファイル構造



ファイルは、ヘッダーとコンテンツ自体の2つの部分に論理的に分割されます。 コンテンツはブロックに分割され、各ブロックは8kBのセクターに分割されます。これらのセクターは特定のファイルに属し、そのシーケンスはヘッダーに記述されています。 すべてのヘッダーには、4バイト整数のフィールドが含まれます(例外は、ファイル名とディレクトリ名のリストを担当する部分です)。



ヘッダーは次の構造で構成されます。



最初に目を引くのはChecksumSignatureです。これは、ファイルのチェックサムを処理するヘッダーの一部の暗号化されたハッシュです。

これらすべてのヘッダーとそれらのフィールドの目的については、後で説明します。

注意深く読んでいない人のために、特に明記しない限り、ほとんどすべてのヘッダーのすべてのフィールドは4バイト整数(C ++ではuint32_t )であることを思い出します。



ファイルヘッダー


名前に基づいて、ファイル全体のヘッダーであり、次のフィールドが含まれています。



HeaderVersion-このヘッダーのバージョンを示す、常に0x00000001。

CacheType -GCFの場合は0x00000001、NCFの場合は0x00000002。

FormatVersion-残りのヘッダーの構造バージョンを示します。 最新バージョンは6です。これについては後で説明します。

ApplicationID-ファイル識別子(AppID)。

ApplicationVersionは、ファイルのコンテンツのバージョンです。 更新の必要性を制御します。

IsMounted-ファイルが現在別のアプリケーションによってマウントされている場合、0x00000001が含まれます。 現在は使用されていないため、常に0x00000000です。

Dummy0は、0x00000000を含むアライメントフィールドです。

FileSizeはファイルの合計サイズです。 4GBを超える場合、このフィールドには差<file size> -ffffffffが含まれ、ファイルサイズ自体は以下に基づいて計算されます

データブロックのサイズと数量。

ClusterSize-コンテンツ内のデータブロックのサイズ。 GCFの場合、0x00002000が含まれ、NCFの場合、0x00000000が含まれます。

ClusterCount-コンテンツ内のデータブロックの数。

チェックサムは、ヘッダーのチェックサムです。 次の関数によって計算されます。



UINT32 HeaderChecksum(UINT8 *lpData, int Size) { UINT32 Checksum = 0; for (int i=0 ; i<Size ; i++) Checksum += *(lpData++); return Checksum; }
      
      





最初のパラメーターはポインターを構造体に渡し、2番目のパラメーターはサイズをチェックサムフィールドを除いて(つまり、4未満)渡します。



BlockAllocationTableHeader


ブロックテーブルの説明が含まれています(セクターではありません!):



BlockCount-ファイル内のブロックの総数が含まれています。

BlocksUsed-使用されているブロックの数。 常にブロックの総数よりも少なくなります。 それに近づくと、合計数量の値が増加し、後続のすべてのヘッダーが再構築され、最初のデータセクターがファイルの最後に移動して、ヘッダー用のスペースが解放されます。

LastUsedBlock-最後に使用されたブロックのインデックス。

Dummy0、Dummy1、Dummy2、Dummy2-アライメントフィールドには0x00000000が含まれます。

チェックサムは、ヘッダーのチェックサムです。 以前のすべてのフィールドの合計が含まれます。



BlockAllocationTable


これはBlockAllocationTableEntry構造体の配列であり 、その数はブロックの総数( BlockAllocationTableHeader.BlockCount )と同じです。



フラグ -ブロックのビットフラグが含まれます。 可能なマスク:



Dummy0アライメントフィールド、0x0000が含まれています。

FileDataOffsetには、このブロックが属するファイルに対するこのブロックのオフセットが含まれます。

FileDataSize-このブロックに保存されているファイルのフラグメントのサイズ。

FirstClusterIndex-クラスターテーブル内の最初のクラスターのインデックス。

NextBlockIndex-次のブロックのインデックス。 BlockAllocationTableHeaderの値が含まれます これがこのファイルのチェーン内の最後のブロックである場合は、 BlockCount

PreviousBlockIndex-チェーン内の前のブロックのインデックスが含まれます。 最初の場合は、値BlockAllocationTableHeaderが含まれます ブロック数

ManifestIndex-このブロックのマニフェストインデックス。

テーブルのインデックスは、 ManifestMapリストのブロック番号です。



FileAllocationTableHeader


セクターテーブルヘッダー:



ClusterCount-セクターの数が含まれています。 FileHeader.ClusterCountと等しい値が含まれます。

FirstUnusedEntry-最初の未使用セクターのインデックス。

IsLongTerminator-セクターチェーンの終わりを示す値を定義します。 0x00000000が含まれている場合、ターミネーターは0x0000FFFFであり、それ以外の場合は-0xFFFFFFFFです。

チェックサムは、ヘッダーのチェックサムです。 BlockAllocationTableHeaderと同様に、前のヘッダーフィールドの合計です。



FileAllocationTable


タイプuint32_tの FileAllocationTableHeader.ClusterCountエントリを含むセクターテーブル。 各セルには、チェーン内の次のクラスターのインデックスまたはターミネーター値が含まれます(チェーンの最後の場合は、 FileAllocationTableHeader宣言を参照してください)。

リストのインデックスはセクター番号です。



マニフェストヘッダー


マニフェストテーブルの説明が含まれます。



HeaderVersionはヘッダーのバージョンです。 0x00000004が含まれます。

ApplicationID-ファイル識別子。 FileHeader.ApplicationIDと同じです。

ApplicationVersionは、ファイルのコンテンツのバージョンです。 FileHeader.ApplicationVersionと同じです。

NodeCount-マニフェスト要素の数。

FileCount-マニフェストで宣言された(およびキャッシュに含まれた)ファイルの数。

CompressionBlockSize-圧縮ブロック(非圧縮データ)の最大サイズ。

BinarySize-マニフェストサイズ(この構造を含む)。

NameSize-要素名を含むデータブロックのサイズ(バイト単位)。

HashTableKeyCount-ハッシュテーブルの値の数。

NumOfMinimumFootprintFiles-アプリケーションの実行に最低限必要なファイルの数(これはディスクに解凍する必要があります)。

NumOfUserConfigFiles-ユーザー構成ファイルの数。 このファイルがディスク上にある場合、ゲームの開始時に上書きされず、優先度が高くなります。

ビットマスク - ビットマスクが含まれます。 ファイルのパブリックバージョンでは、常に0x00000000が含まれています。

指紋は、マニフェストが更新されるたびにランダムに生成される一意の番号です。

チェックサム -チェックサム。 Adler32アルゴリズムを使用して計算されます。 計算アルゴリズムは、ヘッダーの説明の後に記載されます。



マニフェスト


キャッシュ内のすべてのファイルの説明を含むツリー。 テーブルのサイズはManifestHeader.NodeCountの値と同じです。 テーブルのすべての要素は、次の構造で表されます。



NameOffset-対応するデータブロック内の要素の名前のオフセット。

CountOrSize-要素のサイズ。 ディレクトリの場合は、子の数に等しく、ファイルの場合は、ファイル(またはこのマニフェストで記述されたファイルの一部)のサイズに直接等しくなります。

FileId-ファイル識別子。 大きなファイルの複数のマニフェストをリンクし、チェックサムリストを検索します。

属性 -ファイル属性ビットフィールド。 可能な値(確認済みから):





ParentIndexは、親要素のインデックスです。 ルート要素の場合は0xFFFFFFFFです。

NextIndex-現在のツリーレベルでの次の要素のインデックス。

ChildIndexは、最初の子のインデックスです。

NextIndexおよびChildIndexの要素がない場合、値0x00000000が含まれます。

ツリーには少なくとも1つの要素、つまりルートが含まれている必要があります。

ツリー項目を含むリストのインデックスは項目番号です(後で使用されます)



ファイル名


サイズManifestHeader.NameSizeバイトのcharデータブロック。 マニフェストツリーに記述されている要素の名前であるヌル終了文字列が含まれています 。 必須は、最初のルート要素(空の文字列)の存在です。 要素名のオフセットは、値Manifest []で指定されます



HashTableKeys


要素名のハッシュテーブルが含まれます。 小文字の文字列のJenkinsハッシュ関数 lookup2から派生しインデックスに分散されたHashTableIndicesのインデックス値が含まれます。 詳細については、要素の検索の説明で説明します。



HashTableIndices


前のテーブルの値によって参照される要素のインデックスのテーブルが含まれます。 要素の数はManifestHeader.NodeCountです。



MinimumFootprints


アプリケーションの起動時に展開する必要があるマニフェストのアイテム番号のリストが含まれています。



Userconfigs


ユーザー構成ファイルであるManifestのアイテム番号をリストします。



ManifestMapHeader


マニフェストマップヘッダー:



HeaderVersionはヘッダーのバージョンです。 0x00000001と等しい。

Dummy0はアライメント値です。 0x00000000が含まれます。



マニフェストマップ


各要素の最初のブロック( BlockAllocationTable構造体)へのリンクのテーブルが含まれます。 要素インデックスは、マニフェストツリー内の要素の番号です。 キャッシュに保存されていないディレクトリおよびファイル(サイズがゼロまたはNCFの場合)には、 BlockAllocationTableHeader.BlockCountと等しい値が含まれます。



チェックサムデータコンテナ


チェックサムを格納するコンテナのヘッダー:



HeaderVersionはヘッダーのバージョンです。 0x00000001と等しい。

ChecksumSize-コンテナのサイズ。 LatestApplicationVersionを含む次の構造から計算されます。



FileIdChecksumTableHeader


チェックサムインデックステーブルのタイトル:



FormatCodeは定数です。 0x14893721と等しい。

Dummy0はレベリングフィールドです。 値0x00000001が含まれます。

FileIdCount - element-first-hashテーブル内の要素の数。

ChecksumCount-チェックサムリスト内の要素の数。



FileIdChecksums


ファイルをチェックサムリストにリンクするテーブル:



ChecksumCount-この要素のリスト内のチェックサムの数。

FirstChecksumIndex-リスト内の最初のチェックサムのインデックス。

インデックスは、値Manifest []。FileIdです。



チェックサム


チェックサムのリスト。 連続したサブリストが含まれ、その最初の要素は値FileIdChecksums []。FirstChecksumIndexによって参照されます。

値は、次のアルゴリズムを使用して計算されます。



 UINT32 Checksum(UINT8 *lpData, UINT32 uiSize) { return (adler32(0, lpData, uiSize) ^ crc32(0, lpData, uiSize)); }
      
      







チェックサム署名


チェックサムブロックの署名。 SHA-1アルゴリズムによって計算され、 RSASSA-PKCS1-v1_5アルゴリズムによって暗号化されたチェックサムブロックのハッシュ値が含まれます。



LatestApplicationVersion


このフィールドには、チェックサムブロックのバージョンが含まれます。 各コンテンツの更新後に最新に更新されます。



データヘッダー


キャッシュ内のデータの物理的な配置を説明するタイトル:



ClusterCount-セクターの数。 値はFileHeader.ClusterCountフィールドと同じです。

ClusterSize-セクターサイズ。 値はFileHeader.ClusterSizeフィールドと同じです。

FirstClusterOffset-ファイルの先頭に対する最初のセクターのオフセット。

ClustersUsed-使用されているセクターの数。

チェックサムは、ヘッダーのチェックサムです。 先行するヘッダーフィールドの合計に等しい。

コンテンツを更新すると、使用されるセクターの数が減少する可能性があります。 このような場合、解放されたセクターはファイルの最後に転送され、将来の更新用にスペースが確保されます。



アルゴリズム



最後に、最も興味深いものが登場しました-これらの構造で動作するコードの最も興味深い例が、詳細な説明とともにあります。 完全なソースパッケージは私のリポジトリにあります



ファイルサイズの計算


ほとんどの場合、ファイルサイズはManifest []。CountOrSizeフィールドの値と同じです。 ただし、4GBを超えるファイルの場合、この方法は適していません。 VALVEプログラマーは次の方法でこれを回避しました。2GBを超えるファイルの場合、このフィールドの上位ビットを「1」に設定し、リスト内の残りのフィールドと同じ値を持つ別の(または複数の)要素を開始して、一種のチェーンを取得します。 このチェーンのManifest []。CountOrSizeフィールドの値を合計して、合計ファイルサイズを計算します。



ファイルサイズカウントコード
 UINT64 CGCFFile::GetFileSize(UINT32 Item) { UINT64 res = lpManifest[Item].CountOrSize & 0x7FFFFFFF; if ((lpManifest[Item].CountOrSize & 0x80000000) != 0) { for (UINT32 i=0 ; i<pManifestHeader->NodeCount ; i++) { ManifestNode *MN = &lpManifest[Item]; if (((MN->Attributes & 0x00004000) != 0) && (MN->ParentIndex == 0xFFFFFFFF) && (MN->NextIndex == 0xFFFFFFFF) && (MN->ChildIndex == 0xFFFFFFFF) && (MN->FileId == lpManifest[Item].FileId)) { res += MN->CountOrSize << 31; break; } } } return res; }
      
      





ここでは、4GBを超えるファイルがまだキャッシュの一部ではないことを前提に、小さな「耳をかすめて」作成しました...



名前でアイテムを検索する


たとえば、「hl2 / maps / background_01.bsp」という名前のファイルを見つける必要があります。 持っている名前はすべてツリー形式で格納されているため、パスを区切り文字で区切られた要素(この場合は「/」)に分割する必要があります。 次に、ルート要素の子孫から「hl2」という名前の要素を探します。 彼には「maps」という名前の要素があり、その後は「background_01.bsp」という名前の要素があります。 このパスは最も明白ですが、非常に遅いです-文字列のバイト比較、さらにはツリーツアーもあります。 シアーコスト。

この手順を高速化するために、ハッシュテーブルがヘッダーにあります。



ハッシュを使用して名前でアイテムを検索する
C ++

 UINT32 CGCFFile::GetItem(char *Item) { int DelimiterPos = -1; for (UINT32 i=0 ; i<strlen(Item) ; i++) if (Item[i] == '\\') DelimiterPos = i; char *FileName = &Item[++DelimiterPos]; UINT32 Hash = jenkinsLookupHash2((UINT8*)FileName, strlen(FileName), 1), HashIdx = Hash % pManifestHeader->HashTableKeyCount, HashFileIdx = lpHashTableKeys[HashIdx]; if (HashFileIdx == CACHE_INVALID_ITEM) if (strcmp(LowerCase(Item), Item) != 0) { Hash = jenkinsLookupHash2((UINT8*)LowerCase(Item), strlen(FileName), 1); HashIdx = Hash % pManifestHeader->HashTableKeyCount; HashFileIdx = lpHashTableKeys[HashIdx]; } if (HashFileIdx == CACHE_INVALID_ITEM) return CACHE_INVALID_ITEM; HashFileIdx -= pManifestHeader->HashTableKeyCount; while (true) { UINT32 Value = this->lpHashTableIndices[HashFileIdx]; UINT32 FileID = Value & 0x7FFFFFFF; if (strcmp(GetItemPath(FileID), Item) == 0) return FileID; if ((Value & 0x80000000) == 0x80000000) break; HashFileIdx++; } return CACHE_INVALID_ITEM; }
      
      





デルファイ

 function TGCFFile.GetItemByPath(Path: string): integer; var end_block: boolean; Hash, HashIdx, HashValue: ulong; FileID, HashFileIdx: integer; PathEx: AnsiString; begin result:=-1; {$IFDEF UNICODE} PathEx:=Wide2Ansi(ExtractFileName(Path)); {$ELSE} PathEx:=ExtractFileName(Path); {$ENDIF} Hash:=jenkinsLookupHash2(@PathEx[1], Length(PathEx), 1); HashIdx:=Hash mod fManifestHeader.HashTableKeyCount; HashFileIdx:=lpHashTableKeys[HashIdx]; if HashFileIdx=-1 then begin if (LowerCase(Path)<>Path) then begin {$IFDEF UNICODE} Hash:=jenkinsLookupHash2(@LowerCaseAnsi(PathEx)[1], Length(PathEx), 1); {$ELSE} Hash:=jenkinsLookupHash2(@LowerCase(PathEx)[1], Length(PathEx), 1); {$ENDIF} HashIdx:=Hash mod fManifestHeader.HashTableKeyCount; HashFileIdx:=lpHashTableKeys[HashIdx]; if HashFileIdx=-1 then Exit; end; end; dec(HashFileIdx, fManifestHeader.HashTableKeyCount); repeat HashValue:=lpHashTableIndices[HashFileIdx]; FileID:=HashValue and $7FFFFFFF; end_block:= (HashValue and $80000000 = $80000000); if CompareStr(ItemPath[FileID], Path)=0 then begin result:=FileID; Exit; end; inc(HashFileIdx); until end_block; if (result=-1) and (LowerCase(Path)<>Path) then result:=GetItemByPath(LowerCase(Path)); end;
      
      





コードからわかるように、ファイルへのパス全体から名前のみを取得し、そのハッシュを計算します。 結果の整数除算の残りを値ManifestHeader.HashTableKeyCountで取得します。これは、 0xffffffff (そのような要素がない場合)または値X + ManifestHeader.HashTableKeyCountを含むHashTableKeysリスト内のエントリの番号になります。 これに基づいて、 Xを計算します。これは、 HashTableIndicesリスト内の要素の番号であり、 そこから検索対象の要素を見つけることができます。 このリストの値は、クエリで名前が比較される検索対象のアイテムを示します。 一致しない場合、リストの次の要素を取得し、要素番号の最上位ビットが「0」になるまで繰り返します。

混乱することがわかったが、それがどのように機能するかを理解している。そのような混乱のためにVALVEプログラマーを責めなさい。

この方法は、ツリーでの直接検索よりもはるかに優れています。ゲームを開始するときのパフォーマンスを、Steam.dllの自己記述ライブラリエミュレータライブラリと比較しました。これについては、引き続き説明します。



要素へのフルパスを取得する


このアクションは以前のアクションにいくらか戻ります-ツリーを経由してルート要素に移動し、ファイルへのパスを取得する必要がある要素の数。



ファイルパスを取得する
C ++

 char *CGCFFile::GetItemPath(UINT32 Item) { size_t len = strlen(&lpNames[lpManifest[Item].NameOffset]); UINT32 Idx = lpManifest[Item].ParentIndex; while (Idx != CACHE_INVALID_ITEM) { len += strlen(&lpNames[lpManifest[Idx].NameOffset]) + 1; Idx= lpManifest[Idx].ParentIndex; } len--; char *res = new char[len+1]; memset(res, 0, len+1); size_t l = strlen(&lpNames[lpManifest[Item].NameOffset]); memcpy(&res[len-l], &lpNames[lpManifest[Item].NameOffset], l); len -= strlen(&lpNames[lpManifest[Item].NameOffset]); res[--len] = '\\'; Item = lpManifest[Item].ParentIndex; while ((Item != CACHE_INVALID_ITEM) && (Item != 0)) { l = strlen(&lpNames[lpManifest[Item].NameOffset]); memcpy(&res[len-l], &lpNames[lpManifest[Item].NameOffset], l); len -= strlen(&lpNames[lpManifest[Item].NameOffset]); res[--len] = '\\'; Item = lpManifest[Item].ParentIndex; } return res; }
      
      





デルファイ

 function TGCFFile.GetItemPath(Item: integer): string; var res: AnsiString; begin res:=pAnsiChar(@fNameTable[lpManifestNodes[Item].NameOffset+1]); Item:=lpManifestNodes[Item].ParentIndex; while (Item>-1) do begin res:=pAnsiChar(@fNameTable[lpManifestNodes[Item].NameOffset+1])+'\'+res; Item:=lpManifestNodes[Item].ParentIndex; end; Delete(res, 1, 1); {$IFDEF UNICODE} result:=Ansi2Wide(res); {$ELSE} result:=res; {$ENDIF} end;
      
      





Delphiのコードは、C ++の場合はstd :: stringクラスを使用しなかったため、はるかに小さくなりました-それについては知りませんでした。 それにより、コードははるかに短くなります...



ストリーム


アーカイブのようなファイル形式(他のファイルを含む)のライブラリを作成するときは、「ストリームインストリーム」の原則を使用します。これにより、アーカイブ内のファイルを展開せずに開くことができます。 たとえば古いバージョンのキャッシュhalf-life.gcfには、アーカイブであるファイルpak0.pakがありました。 その結果、 half-life.gcfファイルをit- pak0.pakで開きました 。 順番に、必要なファイルを読みます。 そして、これらすべて-メモリにさえ解凍することなく、すべての機能は、ファイルストリーム(低レベル、WindowsAPIレベル)で作成したラッパーを介して実装されます。



キャッシュ内のファイルを開く
C ++

 CStream *CGCFFile::OpenFile(char* FileName, UINT8 Mode) { UINT32 Item = GetItem(FileName); if (Item == CACHE_INVALID_ITEM) return NULL; if ((lpManifest[Item].Attributes & CACHE_FLAG_FILE) != CACHE_FLAG_FILE) return NULL; return OpenFile(Item, Mode); } CStream *CGCFFile::OpenFile(UINT32 Item, UINT8 Mode) { StreamData *Data = new StreamData(); memset(Data, 0, sizeof(StreamData)); Data->Handle = (handle_t)Item; Data->Package = this; Data->Size = this->GetItemSize(Item).Size; if (IsNCF) Data->FileStream = (CStream*)new CStream(MakeStr(CommonPath, GetItemPath(Item)), Mode==CACHE_OPEN_WRITE); else BuildClustersTable(Item, &Data->Sectors); return new CStream(pStreamMethods, Data); }
      
      





デルファイ

 function TGCFFile.OpenFile(FileName: string; Access: byte): TStream; var Item: integer; begin result:=nil; Item:=ItemByPath[FileName]; if (Item=-1) then Exit; if ((lpManifestNodes[Item].Attributes and HL_GCF_FLAG_FILE<>HL_GCF_FLAG_FILE) or (ItemSize[Item].Size=0)) then Exit; result:=OpenFile(Item, Access); end; function TGCFFile.OpenFile(Item: integer; Access: byte): TStream; var res: TStream; begin res:=TStream.CreateStreamOnStream(@StreamMethods); res.Data.fHandle:=ulong(Item); res.Data.Package:=self; res.Data.fSize:=(res.Data.Package as TGCFFile).ItemSize[Item].Size; res.Data.fPosition:=0; if (IsNCF) then begin CommonPath:=IncludeTrailingPathDelimiter(CommonPath); case Access of ACCES_READ: begin res.Data.FileStream:=TStream.CreateReadFileStream(CommonPath+ItemPath[Item]); res.Methods.fSetSiz:=StreamOnStream_SetSizeNULL; res.Methods.fWrite:=StreamOnStream_WriteNULL; end; ACCES_WRITE: begin ForceDirectories(ExtractFilePath(CommonPath+ItemPath[Item])); res.Data.FileStream:=TStream.CreateWriteFileStream(CommonPath+ItemPath[Item]); end; ACCES_READWRITE: res.Data.FileStream:=TStream.CreateReadWriteFileStream(CommonPath+ItemPath[Item]); end; res.Data.FileStream.Seek(0, spBegin); end else GCF_BuildClustersTable(Item, @res.Data.SectorsTable); result:=res; end;
      
      





したがって、コンテンツの操作が大幅に簡素化されます-ファイルを開いて、不必要なジェスチャーなしでファイルからデータを読み取ることができます。



チェックサムファイルの抽出


この手順では、上記のストリームが積極的に使用されます-固定サイズ(チェックサムの最大フラグメントサイズは32Kb)のフラグメントでファイルを読み取り、それらのチェックサムを計算し、ヘッダーのテーブルの値と比較します。



COPを確認してファイルを抽出する
C ++

 UINT64 CGCFFile::ExtractFile(UINT32 Item, char *Dest, bool IsValidation) { CStream *fileIn = this->OpenFile(Item, CACHE_OPEN_READ), *fileOut; if (fileIn == NULL) return 0; if (!IsValidation) { if (DirectoryExists(Dest)) Dest = MakeStr(IncludeTrailingPathDelimiter(Dest), GetItemName(Item)); fileOut = new CStream(Dest, true); if (fileOut->GetHandle() == INVALID_HANDLE_VALUE) return 0; fileOut->SetSize(GetItemSize(Item).Size); } UINT8 buf[CACHE_CHECKSUM_LENGTH]; UINT32 CheckSize = CACHE_CHECKSUM_LENGTH; UINT64 res = 0; while ((fileIn->Position()<fileIn->GetSize()) && (CheckSize == CACHE_CHECKSUM_LENGTH)) { if (Stop) break; UINT32 CheckIdx = lpFileIDChecksum[lpManifest[Item].FileId].FirstChecksumIndex + ((fileIn->Position() & 0xffffffffffff8000) >> 15); CheckSize = (UINT32)fileIn->Read(buf, CheckSize); UINT32 CheckFile = Checksum(buf, CheckSize), CheckFS = lpChecksum[CheckIdx]; if (CheckFile != CheckFS) { break; } else if (!IsValidation) { fileOut->Write(buf, CheckSize); } res += CheckSize; } delete fileIn; if (!IsValidation) delete fileOut; return res; }
      
      





デルファイ

 function TGCFFile.ExtractFile(Item: integer; Dest: string; IsValidation: boolean = false): int64; var StreamF, StreamP: TStream; CheckSize, CheckFile, CheckFS, CheckIdx: uint32_t; buf: array of byte; Size: int64; begin result:=0; StreamP:=OpenFile(Item, ACCES_READ); if (StreamP=nil) then Exit; Size:=ItemSize[Item].Size; if Assigned(OnProgress) then OnProgress(ItemPath[Item], 0, Size, Data); if Assigned(OnProgressObj) then OnProgressObj(ItemPath[Item], 0, Size, Data); StreamF:=nil; if (not IsValidation) then begin if DirectoryExists(Dest) then Dest:=IncludeTrailingPathDelimiter(Dest)+ExtractFileName(ItemName[Item]); StreamF:=TStream.CreateWriteFileStream(Dest); StreamF.Size:=ItemSize[Item].Size; if StreamF.Handle=INVALID_HANDLE_VALUE then begin StreamF.Free; Exit; end; end; SetLength(buf, HL_GCF_CHECKSUM_LENGTH); CheckSize:=HL_GCF_CHECKSUM_LENGTH; while ((StreamP.Position<StreamP.Size) and (CheckSize=HL_GCF_CHECKSUM_LENGTH)) do begin CheckIdx:=lpFileIdChecksumTableEntries[lpManifestNodes[Item].FileId].FirstChecksumIndex+ ((StreamP.Position and $ffffffffffff8000) shr 15); CheckSize:=StreamP.Read(buf[0], HL_GCF_CHECKSUM_LENGTH); CheckFile:=Checksum(@buf[0], CheckSize); CheckFS:=lpChecksumEntries[CheckIdx]; if (CheckFile<>CheckFS) and (not IgnoreCheckError) then begin if Assigned(OnError) then OnError(GetItemPath(Item), ERROR_CHECKSUM, Data); if Assigned(OnErrorObj) then OnErrorObj(GetItemPath(Item), ERROR_CHECKSUM, Data); break; end else if (not IsValidation) then StreamF.Write(buf[0], CheckSize); inc(result, CheckSize); if Assigned(OnProgress) then OnProgress('', result, Size, Data); if Assigned(OnProgressObj) then OnProgressObj('', result, Size, Data); if Stop then break; end; SetLength(buf, 0); StreamP.Free; if (not IsValidation) then StreamF.Free; end;
      
      





Delphiのコードには、作業の進行状況を表示するための追加コードがあります-OnProgress、OnProgressObjコールバック関数の呼び出し。



ファイルの内容を解読する


多くのゲームはリリースの少し前に事前にダウンロードできるため、そのような場合のコンテンツは完全または部分的に暗号化されます。 ゲームのリリースにより、このコンテンツを復号化するためのキーが利用可能になり、次のコードで実行されます。



ファイル復号化
C ++

 UCHAR IV[16] = {0}; void DecryptFileChunk(char *buf, UINT32 size, char *key) { AES_KEY aes_key; AES_set_decrypt_key((UCHAR*)key, 128, &aes_key); AES_cbc_encrypt((UCHAR*)buf, (UCHAR*)buf, size, &aes_key, IV, false); } UINT64 CGCFFile::DecryptFile(UINT32 Item, char *key) { UINT64 res = 0; CStream *str = OpenFile(Item, CACHE_OPEN_READWRITE); if (str == NULL) return 0; char buf[CACHE_CHECKSUM_LENGTH], dec[CACHE_CHECKSUM_LENGTH]; UINT32 CheckSize = CACHE_CHECKSUM_LENGTH; INT32 CompSize, UncompSize, sz; while ((str->Position() < str->GetSize()) && (CheckSize == CACHE_CHECKSUM_LENGTH)) { UINT32 CheckIdx = lpFileIDChecksum[lpManifest[Item].FileId].FirstChecksumIndex + ((str->Position() & 0xffffffffffff8000) >> 15); INT32 CheckSize = (INT32)str->Read(buf, 8); memcpy(&CompSize, &buf[0], 4); memcpy(&UncompSize, &buf[4], 4); if (((UINT32)UncompSize > pManifestHeader->CompressionBlockSize) || (CompSize > UncompSize) || (UncompSize < -1) || (CompSize < -1)) { // Chunk is not compressed CheckSize = (UINT32)str->Read(&buf[8], CACHE_CHECKSUM_LENGTH-8); DecryptFileChunk(&buf[0], CheckSize, key); } else if (((UINT32)UncompSize <= pManifestHeader->CompressionBlockSize) && (CompSize <= UncompSize) && (UncompSize > -1) || (CompSize > -1)) { // Chunk is compressed CheckSize = (UINT32)str->Read(&buf[8], UncompSize-8); INT32 CheckFile = UncompSize; if (CompSize%16 == 0) sz = CompSize; else sz = CompSize + 16 - (CompSize%16); memcpy(dec, buf, sz); DecryptFileChunk(&dec[0], sz, key); uncompress((Bytef*)&buf[0], (uLongf*)&CheckFile, (Bytef*)&dec[0], sz); } str->Seek(-CheckSize, USE_SEEK_CURRENT); str->Write(&buf[0], CheckSize); UINT32 Check1 = Checksum((UINT8*)&buf[0], CheckSize), Check2 = lpChecksum[CheckIdx]; if (Check1 != Check2) break; res += CheckSize; } lpManifest[Item].Attributes = lpManifest[Item].Attributes & (!CACHE_FLAG_ENCRYPTED); return res; }
      
      





デルファイ

 const IV: array[0..15] of byte = (0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0); procedure DecryptFileChunk(buf: pByte; ChunkSize: integer; Key: Pointer); var AES: TCipher_Rijndael; src: array[0..HL_GCF_CHECKSUM_LENGTH-1] of byte; begin Move(buf^, src[0], HL_GCF_CHECKSUM_LENGTH); AES:=TCipher_Rijndael.Create(); AES.Init(Key^, 16, IV[0], 16); AES.Mode:=cmCFBx; AES.Decode(src[0], buf^, ChunkSize); AES.Free; end; function TGCFFile.DecryptFile(Item: integer; Key: Pointer): int64; var StreamP: TStream; CheckSize, CheckFile, CheckFS, CheckIdx, sz: uint32_t; buf: array of byte; dec: array[0..HL_GCF_CHECKSUM_LENGTH] of byte; CompSize, UncompSize: integer; Size: int64; begin result:=0; StreamP:=OpenFile(Item, ACCES_READWRITE); if (StreamP=nil) then Exit; Size:=ItemSize[Item].Size; if Assigned(OnProgress) then OnProgress(ItemName[Item], 0, Size, Data); if Assigned(OnProgressObj) then OnProgressObj(ItemName[Item], 0, Size, Data); SetLength(buf, HL_GCF_CHECKSUM_LENGTH); CheckSize:=HL_GCF_CHECKSUM_LENGTH; while ((StreamP.Position<StreamP.Size) and (CheckSize=HL_GCF_CHECKSUM_LENGTH)) do begin CheckIdx:=lpFileIdChecksumTableEntries[lpManifestNodes[Item].FileId].FirstChecksumIndex+ ((StreamP.Position and $ffffffffffff8000) shr 15); CheckSize:=StreamP.Read(buf[0], 8); Move(buf[0], CompSize, 4); Move(buf[4], UncompSize, 4); if (ulong(UncompSize)>fManifestHeader.CompressionBlockSize) or (CompSize>UncompSize) or (UncompSize<-1) or (CompSize<-1) then begin //Chunk is not compressed! CheckSize:=StreamP.Read(buf[8], HL_GCF_CHECKSUM_LENGTH-8); DecryptFileChunk(@buf[0], CheckSize, Key); end else if ((ulong(UncompSize)<=fManifestHeader.CompressionBlockSize) and (CompSize<=UncompSize)) and ((UncompSize>-1) and (CompSize>-1)) then begin CheckSize:=StreamP.Read(buf[8], UncompSize-8); CheckFile:=UncompSize; //Chunk is compressed! if (CompSize mod 16=0) then sz:=CompSize else sz:=CompSize+16-(CompSize mod 16); Move(buf[8], dec[0], sz); DecryptFileChunk(@dec[0], sz, Key); uncompress(@buf[0], CheckFile, @dec[0], sz); end; StreamP.Seek(-CheckSize, spCurrent); StreamP.Write(buf[0], CheckSize); CheckFile:=Checksum(@buf[0], CheckSize); CheckFS:=lpChecksumEntries[CheckIdx]; if (CheckFile<>CheckFS) and (not IgnoreCheckError) then begin if Assigned(OnError) then OnError(GetItemPath(Item), ERROR_CHECKSUM, Data); if Assigned(OnErrorObj) then OnErrorObj(GetItemPath(Item), ERROR_CHECKSUM, Data); break; end; inc(result, CheckSize); //StreamP.Position:=StreamP.Position+CheckSize; if Assigned(OnProgress) then OnProgress('', result, Size, Data); if Assigned(OnProgressObj) then OnProgressObj('', result, Size, Data); if Stop then break; end; lpManifestNodes[Item].Attributes:=lpManifestNodes[Item].Attributes and (not HL_GCF_FLAG_ENCRYPTED); fIsChangeHeader[HEADER_MANIFEST_NODES]:=true; SaveChanges(); SetLength(buf, 0); end;
      
      







ManifestHeaderのチェックサム計算


この値の計算には、次のヘッダー構造が使用されます。



CSを計算する前に、次のフィールドがリセットされます。



計算自体は、ハッシュ関数の計算のシーケンスに低減さAdler32チェック:すべてのこれらの構造のため



のDelphi

 function ManifestChecksum(Header: pCache_ManifestHeader; entries, names, hashs, table, MFP, UCF: pByte): uint32_t; var tmp1, tmp2: uint32; begin tmp1:=Header.Fingerprint; tmp2:=Header.Checksum; Header.Fingerprint:=0; Header.Checksum:=0; result:=adler32(0, pAnsiChar(Header), sizeof(TCache_ManifestHeader)); result:=adler32(result, pAnsiChar(entries), sizeof(TCache_ManifestNode)*Header^.NodeCount); result:=adler32(result, pAnsiChar(names), Header^.NameSize); result:=adler32(result, pAnsiChar(hashs), sizeof(uint32)*Header^.HashTableKeyCount); result:=adler32(result, pAnsiChar(table), sizeof(uint32)*Header^.NodeCount); if Header^.NumOfMinimumFootprintFiles>0 then result:=adler32(result, pAnsiChar(MFP), sizeof(uint32)*Header^.NumOfMinimumFootprintFiles); if Header^.NumOfUserConfigFiles>0 then result:=adler32(result, pAnsiChar(UCF), sizeof(uint32)*Header^.NumOfUserConfigFiles); Header.Fingerprint:=tmp1; Header.Checksum:=tmp2; end;
      
      







おわりに



説明が煩雑であるためにこの記事では考慮されていない他の機能(セクターマップの変更、このマップの再構築時など、ビジーセクターのビットマップの使用など)は、リポジトリで見ることができます(以下で説明する残りのプログラムフラグメント)記事)。これらのソースコードは、プロジェクトで使用できます(誰かがそのような珍しいものを必要とする場合)。

すべてのソースコードの最終更新のおおよその日付は、2011年の後半です。



PS:このライブラリを書くことは、対象のオペレーティングシステムに関するラボを書くときに非常に役立ちました。大学では、ファイルシステムの動作(ファイルの作成、書き込み、読み取り、削除)をシミュレートする必要がありました。私の作品は最初であり、おそらく、唯一の作品であり、ブロックとセクターに分割されたファイルシステムのイメージが使用されました。この作業の一環として、キャッシュのデフラグツールも追加しました...



All Articles