以前我谈过, 第1章 KRKR游戏系统汉化说明
我觉得问题在于很多商业KRKR系统都是加密过的数据,今天作为收山之文,谈下XP3加密分析和封包算法
今天我们分析的游戏是《yourdiary》,也是前久汉化组来求破解的游戏。
加密VS解密
不管KRKR还是.net 还是JAVA,以及android,其加密解密方法基本差不多,就是HOOK相应函数或者提供相应接口就OK。
无外乎解密存在两种方法
1、动态HOOK 然后内存DUMP
2、静态分析
举例:
1、动态HOOK xp3dumer
2、静态分析 Crass
JAVA 静态解密可以参考鄙人做的java Class文件解密大师发布和说明
对于大家来说动态HOOK简单实用,因为不需要考虑什么加密算法,直接内存DUMP
对于程序猿来说,显然静态分析,逆向其算法更能提高水平。
所以说本文需要程序设计基础非大众读物。
我们用Crass解包直接解包,提示CRC错误。
青子私服c06a(大)-mono.tlg: crc校验失败(0x90d63725), 应该是0xe1eb5d2b. 青子私服c06a(大).tlg: crc校验失败(0x2a395798), 应该是0x9ff1bbbe. 青子私服c06a(近)-mono.tlg: crc校验失败(0xab6e8767), 应该是0x70439e0e. 青子私服c06a(近).tlg: crc校验失败(0xac8ab284), 应该是0x346e11f5. 黒幕.tlg: crc校验失败(0xc41caf48), 应该是0x9014486f. data.xp3: 成功提取1286 / 1286 个资源文件
怎么回事,CRC错误可能就是1、文件损坏了 2、加密了
显然游戏可以正常运行,否定1。
用UE打开XP3,分析下看不出规律,现在只能从代码入手。
打开plugin目录有个yourdiary.tpm
这个就是加密DLL
用反汇编器打开看看
发现有两个导出函数
V2Link
V2Unlink
注意:KRKR分早期版本和2.22以后版本,其导出函数不一样,大家要注意.不过大家遇到KRKR都不很老,所以本文基本可以通吃。
这里需要大家有KRKR封包加密插件编写的经验才能跟到关键加密代码段
至于加密插件的使用方法请参照水螅大大的教程,这里不再赘述。
我直接跟进关键加密代码段:
.text:10001000
.text:10001000 ; Segment type: Pure code
.text:10001000 ; Segment permissions: Read/Execute
.text:10001000 _text segment para public 'CODE' use32
.text:10001000 assume cs:_text
.text:10001000 ;org 10001000h
.text:10001000 assume es:nothing, ss:nothing, ds:_data, fs:nothing, gs:nothing
.text:10001000
.text:10001000 loc_10001000: ; DATA XREF: V2Link:loc_10001078o
.text:10001000 push esi
.text:10001001 mov esi, [esp+8]
.text:10001005 cmp dword ptr [esi], 18h
.text:10001008 jz short loc_1000102C
.text:1000100A mov eax, dword_1000C2CC
.text:1000100F test eax, eax
.text:10001011 jnz short loc_10001025
.text:10001013 push offset aVoidTvpthrowex ; "void ::TVPThrowExceptionMessage(const t"...
.text:10001018 call sub_100010C0
.text:1000101D add esp, 4
.text:10001020 mov dword_1000C2CC, eax
.text:10001025
.text:10001025 loc_10001025: ; CODE XREF: .text:10001011j
.text:10001025 push offset aIncompatibleTt ; "Incompatible tTVPXP3ExtractionFilterInf"...
.text:1000102A call eax ; dword_1000C2CC
.text:1000102C
.text:1000102C loc_1000102C: ; CODE XREF: .text:10001008j
.text:1000102C xor eax, eax
.text:1000102E cmp [esi+10h], eax
.text:10001031 jbe short loc_1000104B
.text:10001033
.text:10001033 loc_10001033: ; CODE XREF: .text:10001049j
.text:10001033 mov ecx, [esi+0Ch]
.text:10001036 mov dl, [ecx+eax]
.text:10001039 xor dl, [esi+14h]
.text:1000103C add ecx, eax
.text:1000103E xor dl, 0CDh
.text:10001041 add eax, 1
.text:10001044 mov [ecx], dl
.text:10001046 cmp eax, [esi+10h]
.text:10001049 jb short loc_10001033
.text:1000104B
.text:1000104B loc_1000104B: ; CODE XREF: .text:10001031j
.text:1000104B pop esi
.text:1000104C retn 4
.text:1000104C ; ---------------------------------------------------------------------------
.text:1000104F align 10h
.text:10001050 ; Exported entry 1. V2Link
要看懂上面汇编对于新手是很难的,因为计算机不懂什么叫类,什么叫对象,什么叫结构体。
所有的这些概念在编译之后都不复存在。
数据解密最难也在于此,比如[esi+0Ch]那是啥玩意,是数组?还是结构体?还是对象?
就像在漆黑的屋子里面抓到一大团毛绒绒的东西,是啥,是兔子还是猫?
好在解密插件是开源的
大家可以打开KRKR源代码\2.26\kirikiri2\src\plugins\win32\xp3filter\xp3dec 目录下的 main.cpp
可以看到
//--------------------------------------------------------------------------- void TVP_tTVPXP3ArchiveExtractionFilter_CONVENTION TVPXP3ArchiveExtractionFilter(tTVPXP3ExtractionFilterInfo *info) { // TVPXP3ArchiveExtractionFilter 関数は本体側から呼び出される // コールバック関数です。 // 引数を一つ取り、それは tTVPXP3ExtractionFilterInfo 構造体へのポインタ // です。 // TVPXP3ArchiveExtractionFilter は、後述の V2Link 関数内で // TVPSetXP3ArchiveExtractionFilter により設定されます。 // ここでは単純に、xp3enc.dll のサンプルで作成された XP3 アーカイブを // 復号すべく、データをすべて FileHash の最下位バイトで XOR // することにします。 // この関数は複数のスレッドから同時に呼び出される可能性があるので // 注意してください。 /* tTVPXP3ExtractionFilterInfo のメンバは以下の通り * SizeOfSelf : 自分自身の構造体のサイズ * Offset : "Buffer" メンバが指し示すデータが、 * : アーカイブに格納されているそのファイルの先頭からの * : どのオフセット位置からのものか、を表す * Buffer : データ本体 * BufferSize : "Buffer" メンバの指し示すデータのサイズ(バイト単位) * FileHash : ファイルの暗号化解除状態でのファイル内容の32bitハッシュ */ // 一応構造体のサイズをチェックする if(info->SizeOfSelf != sizeof(tTVPXP3ExtractionFilterInfo)) { // 構造体のサイズが違う場合はここでエラーにした方がよい TVPThrowExceptionMessage(TJS_W("Incompatible tTVPXP3ExtractionFilterInfo size")); // TVPThrowExceptionMessage は例外メッセージを投げる関数 // この関数は戻らない ( もっと呼び出し元をさかのぼった位置で // 例外が補足されるため ) } // 復号 tjs_uint i; for(i = 0; i < info->BufferSize; i++) ((unsigned char *)info->Buffer)[i] ^= info->FileHash; } //------------
但要知道[esi+0Ch]是啥,还得研究官方源代码,所有KRKR解密依赖于tTVPXP3ExtractionFilterInfo这个结构,显然解密代码就是结构体操作。
下面请大家复习下C语言知识:
类型标识符 | 类型说明 | 长度 (字节) |
范围 | 备注 |
char | 字符型 | 1 | -128 ~ 127 | -27 ~ (27 -1) |
unsigned char | 无符字符型 | 1 | 0 ~ 255 | 0 ~ (28 -1) |
short int | 短整型 | 2 | -32768 ~ 32767 | 2-15 ~ (215 - 1) |
unsigned short int | 无符短整型 | 2 | 0 ~ 65535 | 0 ~ (216 - 1) |
int | 整型 | 4 | -2147483648 ~ 2147483647 | -231 ~ (231 - 1) |
unsigned int | 无符整型 | 4 | 0 ~ 4294967295 | 0 ~ (232-1) |
float | 实型(单精度) | 4 | 1.18*10-38 ~ 3.40*1038 | 7位有效位 |
double | 实型(双精度) | 8 | 2.23*10-308 ~ 1.79*10308 | 15位有效位 |
long double | 实型(长双精度) | 10 | 3.37*10-4932 ~ 1.18*104932 | 19位有效位 |
上面这张表是一般VC 32位程序类型的资料。大家特别注意下长度,你看int 占用4字节,不过不同编译器和平台不一样,数据类型的字节数应该是由CPU决定的,但是实际上主要由编译器决定。
显然一个类似下面这样结构体:
struct Student { int name; int sex; int age; int addr; }; /*然后定义一个Student 类型的 student变量*/ struct Student student;
下面我们看看tTVPXP3ExtractionFilterInfo结构如下:大小和内存位置我已经标注给大家(KRKR源代码\2.26\kirikiri2\src\plugins\win32\tp_stub.cpp文件中)
struct tTVPXP3ExtractionFilterInfo { const tjs_uint SizeOfSelf; // structure size of tTVPXP3ExtractionFilterInfo itself 4个字节 0-3 const tjs_uint64 Offset; // offset of the buffer data in uncompressed stream position 8个字节 4-11 void * Buffer; // target data buffer 4个字节 12-15 const tjs_uint BufferSize; // buffer size in bytes pointed by "Buffer" 16-19 const tjs_uint32 FileHash; // hash value of the file (since inteface v2) tTVPXP3ExtractionFilterInfo(tjs_uint64 offset, void *buffer, tjs_uint buffersize, tjs_uint32 filehash) : Offset(offset), Buffer(buffer), BufferSize(buffersize), FileHash(filehash), SizeOfSelf(sizeof(tTVPXP3ExtractionFilterInfo)) {;} };有了这些东西,我们去搞汇编就易如反掌了
上面加密算法伪代码应该这样:
取出[esi+0Ch]里的数据,即Buffer[i]
异或xor [esi+14h] 即 FileHash
异或xor 0CDh
好了解密算法这样写就OK了:
//--------------------------------------------------------------------------- void TVP_tTVPXP3ArchiveExtractionFilter_CONVENTION TVPXP3ArchiveExtractionFilter(tTVPXP3ExtractionFilterInfo *info) { // TVPXP3ArchiveExtractionFilter 函数是会从吉里吉里本体调用的回调函数。 // 能得到的参数只有一个,就是对 tTVPXP3ExtractionFilterInfo 结构体的指针。 // TVPXP3ArchiveExtractionFilter 需要在后述的 V2Link 函数内使用 // 吉里吉里本体的 TVPSetXP3ArchiveExtractionFilter 函数设定。 // 这个样例只是纯粹的对 xp3enc.dll 样例代码加密的 XP3 文件包解密 // 将所有的数据以 FileHash 的最后一个字节作异或(XOR)运算 // 请注意,这个函数有可能在多个线程中被调用。 /* tTVPXP3ExtractionFilterInfo 的成员如下 * SizeOfSelf : 结构体自身的大小 * Offset : "Buffer" 成员所指向的数据偏离这个文件头部的偏移值 * Buffer : 数据本体 * BufferSize : "Buffer" 成员所指向的数据的大小(字节单位) * FileHash : 解密后文件内容的32位 Hash (散列)值 */ // 检查结构体的大小 if(info->SizeOfSelf != sizeof(tTVPXP3ExtractionFilterInfo)) { // 当结构体的大小错误,则最好投出异常 TVPThrowExceptionMessage(TJS_W("Incompatible tTVPXP3ExtractionFilterInfo size")); // TVPThrowExceptionMessage 是用于投出异常的函数 // 本函数不会返回。 } // yourdiary 解密代码 by大师♂罗庄 // ext:10001033 loc_10001033: ; CODE XREF: .text:10001049j //.text:10001033 mov ecx, [esi+0Ch] //Buffer[i] //.text:10001036 mov dl, [ecx+eax]//Buffer[i] //.text:10001039 xor dl, [esi+14h] //FileHash //.text:1000103C add ecx, eax //.text:1000103E xor dl, 0CDh //.text:10001041 add eax, 1 //.text:10001044 mov [ecx], dl //.text:10001046 cmp eax, [esi+10h]//循环比较BufferSize //.text:10001049 jb short loc_10001033 //.text:1000104B //.text:1000104B loc_1000104B: ; CODE XREF: .text:10001031j //.text:1000104B pop esi //.text:1000104C retn 4 //.text:1000104C ; --------------------------------------------------------------------------- //.text:1000104F align 10h tjs_uint i; for(i = 0; i < info->BufferSize; i++) { unsigned char v4 =((unsigned char *)info->Buffer)[i] ; unsigned char v5 =info->FileHash^ (v4); unsigned char result ; result = v5 ^ 0xCD; ((unsigned char *)info->Buffer)[i] =result; } } //---------------------------------------------------------------------------编译生成*.dll 然后改成*.tmp
可以覆盖游戏*.tmp试试,但最好别这样,最好用Crass 带tmp参数进行测试,发现没有错误能解包。
好了课程就这样,下课。