吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

搜索
查看: 5274|回复: 62
收起左侧

[原创] 超详细!解包某知名Galgame(万华镜5)引擎——Galgame汉化中的逆向#Qlie引擎

  [复制链接]
Aobanana 发表于 2021-8-26 15:59
本文和我的个人博客(https://aobanana.tk/article/6)同时发布


前言


在等近月2.2的汉化,应该等了一年以上了。在等待的过程中,萌生出了一个问题——汉化的过程是怎样的,汉化组要做哪些工作?

在大脑里分析了一下汉化的过程(云汉化了一遍 image.png ),应该有解包/程序逆向,翻译,校对这些步骤。其中解包/程序逆向,也就是提取游戏里的资源(音频、CG、文本等)应该怎么做,我能不能自己动手解包呢。虽然很久之前就有过这样的问题,也有过这样的需求,不过之前都是找别人做的解包工具直接解包,如果没有这样的解包工具呢?所以现在想自己动手解包试试看。



这次解包针对的是使用Qlie引擎的Galgame。用这个引擎的Galgame都有个特点——点开游戏的时候都会有这样的一个窗口。
image.png
然后我硬盘里两个Galgame——近月2.2(月に寄りそう乙女の作法22ALSA)和大名鼎鼎的万华镜5都是这个引擎做的。
image.png image.png

搜索资料


完全没有解包Galgame的经验,所以我决定先找找资料,不得不说这方面的资料是真的少。但我运气不错,在吾爱破解论坛找到了@小木曾雪菜发的Galgame汉化中的逆向 (一):文本加密(压缩)与解密,感谢这位大佬的贡献。

但是按照这位大佬的教程做,发现了一个很大的问题。

大佬的表示:

找文本很简单,等游戏运行起来后直接暂停,搜索内存sjis字符串。比如说“椿子”。
然后记住这个地址(或附近的某个),下硬断点write,重新启动游戏运行,游戏中断在这里。

但是实际上内存是动态分配,重启程序后内存大概率会变动,这样的方法理论上是很难行得通的。方法虽然也不是完全不行,但是可行性比较低。(不过根据游戏引擎的不同,可行性也不太一样,可能那个贴的游戏是可行性比较高的那种)至少我没有成功过。

绕了一圈,最后还是决定自己研究,自力更生。

逆向开始——侦查


游戏的文件夹很清晰。很明显,我们的游戏数据文件应该就在GameData文件夹里面,文件夹里是一堆后缀名为.pack的文件,这些很明显就是游戏的数据了。
image.png image.png
pack文件的数据是这样的,一堆无法正常解读的数据,很明显是经过加密或者是压缩,亦或者是两者都有。
那么我们的目的很明确——弄明白pack文件的结构 只有明白了pack文件的结构是什么,才能开始解包。
调试程序


所以我们开始调试程序,看看程序对pack文件是怎样一个解读方法。
文件的读取主要使用3个API——CreateFile、SetFilePointer和ReadFile,所以我们在这三个函数上下断点。由于程序会打开很多的文件,读很多的文件,每一个都中断下来不合理,所以我们一开始先不中断,只记录看看这些函数做了什么操作。
对CreateFileW这样设置,暂停条件为0就不会中断了,日志文本我让他在命中断点时候输出[esp+4]里面的值,并让他以UTF16编码输出,看过CreateFileW函数的定义就能知道,这个函数的第一个参数是指向文件名的指针,也就是[esp+4]的值。该函数的返回值放在eax上,值是文件的句柄,所以我们在CreateFileW函数的返回上,下断点,设置成这样。
image.png image.png
同理,在SetFilePointer和ReadFile一样设置。至于为什么这么设置,具体可以看看API的文档。
image.png image.png
好了,让程序跑起来。结果发现,ReadFile的操作太多,导致根本不知道CreateFile干了什么,所以先暂时关掉ReadFile和SetFilePointer的断点,只看CreateFile的。
再跑起来,打开文件的操作很多,翻看日志,可以发现关键文件的句柄。
我们再在CreateFile上设置一下条件,设置成8:[esp+4] == 470061006D006500
你可能想问,什么鬼,这个条件是什么东西???答案是:
image.png 没错,我们想要CreateFile的文件名前4个字等于Game的时候断下来。
直接重启程序,断下来之后,执行到返回,回到用户代码的领空,在这里。
image.png
这时候,我们便可以设置Readfile的暂停条件为1了(SetFilePointer还是为0),我们想看看程序是怎么读data0.pack这个文件的。
直接运行程序,断下来后,执行到返回,跳出系统函数,回到我们用户程序的领空。大概是这样。
image.png
日志输出了
设置文件指针 句柄:430 偏移量:00 基准:1
设置文件指针 句柄:430 偏移量:00 基准:2
设置文件指针 句柄:430 偏移量:00 基准:0
设置文件指针 句柄:430 偏移量:024D0DE0 基准:0
读取文件-句柄:430 缓冲:19FB0F 字节数:1C
image.png
也就是说程序读取了文件结尾0x1C的数据,这部分数据,我们可以先大胆地猜测这些数据是什么意思,不用担心猜错,只是假设而已,后面还要求证。
首先是FilePackVer3.1,很有可能是一个签名,表示文件打包的工具的版本。
然后是35 01 4d 02,对数据敏感一点就能发现,如果用小端整型去解释这个就是0x024d0135,而我们左边文件的偏移024d0df0极其相似,能猜出这应该是一个地址,用途估计是入口点之类的。
此时我们向左一样解释一个int,值应该是0x12,估计是什么大小之类的。向右解释int,值是0,不知道表示了什么,可能是标志位,或者是什么大小。


回到我们调试器,跳出几次函数就到了这里。
image.png
对周围函数分析,这个考验你的逆向功底,图中关键的函数我已经分析完了,写好了注释,这里就不展开具体是怎么分析的了。
读取内容后,取了刚刚读出来的数据中的前0x10(看那里有一个给ecx赋值0x10)长度的数据转化为unicode,也就是说,刚刚读了的数据里前0x10是一组。
然后到下一个函数,把转成的unicode转成小写的,最后对比数据是不是等于filepackver3.1
这部分很明显是一个验证
此时我们可以对这部分数据做一个结构体的猜测
[C++] 纯文本查看 复制代码
Struct FilePackVer
{
    char sign[0x10];
    DWORD size?;
    DWORD entry?;
    DWORD unknown;
};

奇怪的Hash


接下来我们进入文件初始化工作的call
image.png
很快我们就能遇到一个文件读取的操作。具体是怎么读的呢。反复调试几遍,能发现先把文件的大小传给了eax,然后eax-0x440(sub eax,0x440),作为文件指针,然后读取了0x440的数据。
ps:注意sub eax,0x440之后的sub edx,0x0 一开始我并没发现edx有什么用,并且自动忽略了,后来发现是个伏笔
读取了数据之后,开始检查结尾(这里的结尾是指FilePackVer上面。读取把FilePackVer又重新读了一遍,FilePackVer数据之上有一大块空的数据,那个结尾如下图所示)如果大于8,获取小于0都置为0(不知道有什么用,最后发现确实没什么用)
image.png
然后往开头0x24位置开始,复制了0x100个BYTE的数据(图里面的注释的单位写错了)
image.png
然后到了关键的代码,我们进入这个计算Hash的函数
首先是这一段
image.png
判断了之前复制数据长度是否小于8个BYTE,小于就直接返回0,否则就开始了hash的计算
image.png
计算的全貌大概是这样子,写成代码大概是这样.
[C++] 纯文本查看 复制代码
DWORD Tohash(void* data, int len)
{
	if (len < 8)
	{
		return 0;
	}
	//准备工作
	__m64 mm0 = _mm_cvtsi32_si64(0);
	__m64 mm1;
	__m64 mm2 = _mm_cvtsi32_si64(0);
	DWORD key = 0xA35793A7;
	__m64 mm3 = _mm_cvtsi32_si64(key);
	 mm3 = _m_punpckldq(mm3, mm3);
	 __m64* pdata=(__m64*)data;
	//开始循环计算hash
	for (size_t i = 0; i < (len >> 3); i++)
	{
		mm1 = *pdata;
		pdata++;
		mm2 = _m_paddw(mm2, mm3);
		mm1 = _m_pxor(mm1, mm2);
		mm0 = _m_paddw(mm0, mm1);
		mm1 = mm0;
		mm0 = _m_pslldi(mm0, 3);
		mm1 = _m_psrldi(mm1, 0x1D);
		mm0 = _m_por(mm1, mm0);
	}
	mm1 = _m_psrlqi(mm0, 32);
	DWORD result = _mm_cvtsi64_si32(_m_pmaddwd(mm0, mm1));
	_m_empty();//复位浮点寄存器
	return result;
}

image.png
最后算出了hash,并与了0x0FFFFFFF,最后的值为0x0658D1A0
第一个解密函数


接下来的一个call,也是关键的函数 大致的内容是这样
image.png
写成代码应该是这样。
[C++] 纯文本查看 复制代码
void dencrypt(void* data,unsigned int len, DWORD hash)
{
	if (len >> 3 == 0)
	{
		return;
	}
	//准备工作
	DWORD key1 = 0xA73C5F9D;
	DWORD key2 = 0xCE24F523;
	DWORD key3 = (len + hash)^ 0xFEC9753E;
	__m64 mm7 = _mm_cvtsi32_si64(key1);
	mm7 = _m_punpckldq(mm7, mm7);
	__m64 mm6 = _mm_cvtsi32_si64(key2);
	mm6 = _m_punpckldq(mm6, mm6);
	__m64 mm5 = _mm_cvtsi32_si64(key3);
	mm5 = _m_punpckldq(mm5, mm5);
	__m64* datapos = (__m64*)data;
	__m64 mm0;
	for (size_t i = 0; i < len >> 3; i++)
	{
		mm7 = _m_paddd(mm7, mm6);
		mm7 = _m_pxor(mm7, mm5);
		mm0 = *datapos;
		mm0 = _m_pxor(mm0, mm7);
		mm5 = mm0;
		*datapos = mm0;
		datapos++;
	}
	_m_empty();//复位浮点寄存器
	return;
}

这个函数什么作用呢?他对之前读出数据的前0x20字节利用刚刚算出的hash解码,解码的结果如下图。
image.png
之后跳出这个函数。和之前FilePackVer一样,进行了一个验证。
[Asm] 纯文本查看 复制代码
004ED95B         | 8D85 B4FBFFFF   | lea eax,dword ptr ss:[ebp-0x44C]                   | 缓冲区
004ED961         | 8D95 B8FBFFFF   | lea edx,dword ptr ss:[ebp-0x448]                   | 需要转换的字符串
004ED967         | B9 20000000     | mov ecx,0x20                                       | 字符串长度
004ED96C         | E8 9F9EF1FF     | call <月に寄りそう乙女の作法22.multibyte2widecode>            | 字符转unicode
004ED971         | 8B85 B4FBFFFF   | mov eax,dword ptr ss:[ebp-0x44C]                   |
004ED977         | BA 28DB4E00     | mov edx,月に寄りそう乙女の作法22.4EDB28                       | 4EDB28:L"8hr48uky,8ugi8ewra4g8d5vbf5hb5s6"
004ED97C         | E8 7FA3F1FF     | call <月に寄りそう乙女の作法22.cmp_sign>                      | 比对签名

Filepack结构体进一步清晰

image.png
再之后的代码,程序取了filepack第一个DWORD,也就是之前的0x12,第二个DWORD,也就是那个地址,和第三个DWORD,也就是那个不知道表示了什么的0.
后面的操作我们依旧不清楚0x12到底表示了什么。但是用了第二个和第三个,并使用了一个汇编指令cdq
讲道理我还是第一次见到这个指令,查了一下有什么用,文档上说就是把edx作为高位,eax作为低位,然后合成一个64位的有符号数。也就是说,Qlie引擎考虑了文件大小大于4GB的情况。换句话说,之前的那个地址是低位,而后面的0是高位。
那么我们的Filepack结构体应该更清晰了。不过我们只确定了数据的类型,暂时不清楚这个地址的作用。
[C++] 纯文本查看 复制代码
struct FilePackVer
{
	char sign[0x10];
	DWORD size?;
	QWORD entry?; //写程序的时候需要变成两个int型表示低位和高位,直接用64位整型读取文件的时候会少读这部分数据
};

HashData结构体

之前读取了0x440的数据,我们暂时可以叫他HashData(因为用这里面的数据算出了一个Hash)。现在我们大致可以弄明白这部分数据的结构了。开头是加过密的字符串8hr48uky,8ugi8ewra4g8d5vbf5hb5s6,长度0x20之后有一个用int型解释是0x28D的数据,这部分数据可以看看上图,eax首先等于文件大小,然后eax-0x440(到了我们HashDat数据的偏移),然后又减去0x28D,没错,就是这个用int型解释数据,此时eax在文件中指向HashVer数据(姑且这么叫).如下图 image.png 这说明了这部分数据应该是上面这个HashVer的大小(size)或者解释成HashVer相对于HashData的偏移。我个人喜欢解释成size
那么此时HashData的结构应该是明确了。
[C++] 纯文本查看 复制代码
struct HashData
{
	char sign[0x20];
	DWORD HashVerSize;
	char data[0x100];
	DWORD Unkown; //就是那个大于8或者小于0就设置成0的数据
	char Blank[0x2F8]; //一大片用0填充的数据,应该是用于占位
	FilePackVer fpacker;
};

HashVer数据的处理
image.png
接下来,来到了
[Asm] 纯文本查看 复制代码
004ED9D2         | E8 397DF1FF     | call 月に寄りそう乙女の作法22.405710                          | 构造类?

讲道理,这个call我跟踪了半天,一直弄不明白这个call干了什么,然后以为是什么没有用的函数。在进入N遍之后,终于弄明白了,这个函数会建立一个类,返回值eax是这个类的指针。
类的内存结构大概是这样(执行了下面的CopyFrom) image.png
第一个DWORD应该是类的抽象地址,可以根据这个地址找到这个类封装的函数(貌似这个类好像是继承Dephi的Tstream)第二个DWORD是一个指针,指向数据第三个DOWRD是数据的大小之后的数据用途还未确定,不过也用不上,不用管之后有个CopyFrom函数,函数执行后,日志变化内容如下设置文件指针 句柄:430 偏移量:024D072F 基准:0
读取文件-句柄:430 缓冲:26AC5B0 字节数:28D
没错,他把HashVer的数据,总长度0x28D,读取了出来,并放进刚刚创建的类里。之后用了Tstream的setposition把指针指向Hashver数据的头部,然后进入了HashVer数据的处理函数……

进入call之后很快就能来到这里
image.png
和之前的签名验证的常规操作一样,取出签名的签名,转成unicode,然后转小写字母,这里可知最后对比签名签名的长度为0x10。

这里我们就能清楚hashver的结构,前0x20是一块数据。我们可以先来猜测,前0x10是签名,后面4个DWORD表示其他的数据。
image.png
后面对数据的读入是这样的(图里的注释是一开始以为签名长度为0xC的时候记录的,所以实际上每个都要减1)
image.png
再是这个函数,执行了之后,在上面构造的类里写入了
image.png image.png
还记得类的定义吗,由此我们可知0x249是一个大小,也就是第四个DWORD是一个size,然后看一下里面的数据,就能明白HashVer里面00填充完之后就是长度为size的HashVer里面的数据了。
image.png
后面的操作,对hashver里的数据先使用dencrypt进行解密,然后在根据第五个DWORD,如果为1,就进行dencrypt2解密。

这说明了第五个DWORD是一个标志位。

而dencrypt2函数是什么呢?分析完之后发现,应该是一个压缩算法的解压过程,难度……很难(也可能是我太菜了)
image.png
直接进入dencrypt2的call,这个函数先建了一个0-FF的表,然后尝试对比数据开头是不是0xFF435031,如果不是则直接返回。
这部分是数据 image.png
然后读取了第三个DWORD(0x922),这个数据是一个size。

紧接着是一个大循环。 image.png
代码量太多了,一张图已经放不下。

借助ida的帮助,我将代码用C++表示应该是这样的
[C++] 纯文本查看 复制代码
		//chr是逐个从数据中取出的字节,t_pos是表的下标
		//这里有两张表,一张是table,刚刚从0-FF复制过来的,另一张是other,不做任何预处理
		while (1)
		{
			if (chr > 0x7Fu)
			{
				t_pos += chr - 127;
				chr = 0;
			}
			if (t_pos > 0xFF)
			{
				break;
			}
 
			for (size_t i = 0; i < chr + 1; i++)
			{
				table[t_pos] = *datapos++;
				if (t_pos != (unsigned __int8)table[t_pos])
				{
					other[t_pos] = *datapos++;
				}
 
				++t_pos;
			}
			if (t_pos > 0xFF)
			{
				break;
			}
			chr = *datapos++;
		}

接着又是一个大循环
image.png
在表建完后,就开始用表的数据对数据解码。先读出了建完表后第一个数据(根据第二个DWORD判断类型),可能表示数据的size或者是其他什么的(理解不能)。如果这个循环算完,就跳回上一个循环(ida对这里用了goto),判断数据是否符合某条件,然后继续处理。

最后用人能理解的代码,写出来整个解密代码是这样的。
[Asm] 纯文本查看 复制代码
Dencrypt2DataOutput* dencrypt2(void* data, unsigned int len,unsigned int dencrypted_len, DWORD hash)
{
	char Sampletable[0x100],table[0x100],other[0x100];
	for (size_t i = 0; i < 0x100; i++)
	{
		Sampletable[i] = i;
	}
	Dencrypt2DataHead* head = (Dencrypt2DataHead*)data;
	//对比开头是否为0xFF425031
	if (head->sign != 0xFF435031)
	{
		cout << "数据不符合解码条件" << endl;
		return nullptr;
	}
	if (head->size> 0x20000000u)
	{
		cout << "数据量大于0x20000000" << endl;
		return nullptr;
	}
 
	Dencrypt2DataOutput* Output = new Dencrypt2DataOutput();
	Output->len = dencrypted_len;
	Output->data = new BYTE[dencrypted_len + 1];
	BYTE* outputbuff = Output->data;
 
	BYTE* datapos = (BYTE*)data + sizeof(Dencrypt2DataHead);
	BYTE* data_start = datapos;
	BYTE* data_end = (BYTE*)data + len;
	BYTE chr;
	int t_pos;
	int size;
	while (data_start < data_end)
	{
		chr = *data_start;
		datapos = data_start + 1;
		memcpy(table, Sampletable, 0x100);
		t_pos = 0;
		//建表循环
		while (1)
		{
			if (chr > 0x7Fu)
			{
				t_pos += chr - 127;
				chr = 0;
			}
			if (t_pos > 0xFF)
			{
				break;
			}
 
			for (size_t i = 0; i < chr + 1; i++)
			{
				table[t_pos] = *datapos++;
				if (t_pos != (unsigned __int8)table[t_pos])
				{
					other[t_pos] = *datapos++;
				}
 
				++t_pos;
			}
			if (t_pos > 0xFF)
			{
				break;
			}
			chr = *datapos++;
		}
		//数据类型判断
		if ((head->isWordType & 1) == 1)
		{
			size = *(WORD*)datapos;
			data_start = (datapos + 2);
		}
		else
		{
			size = *(DWORD*)datapos;
			data_start = (datapos + 4);
		}
		//解密循环
		stack<BYTE> stack;
		while (1)
		{
			BYTE result;
			if (stack.size())
			{
				result = stack.top();
				stack.pop();
			}
			else
			{
				if (!size)
				{
					break;
				}
				size--;
				result = *data_start;
				data_start++;
			}
			if (result == (BYTE)table[result])
			{
				*outputbuff = result;
				outputbuff++;
			}
			else
			{
				stack.push(other[result]);
				stack.push(table[result]);
			}
		}
	}
	return Output;
}

其中,Dencrypt2DataHead的结构体是这样,这个是解压之前代码的头部。第一个DWORD是一个签名,第二个用于表建完后,表示size的类型,为1则为WORD,为0则为DWORD。
[C++] 纯文本查看 复制代码
struct Dencrypt2DataHead
{
DWORD sign;
DWORD isWordType;
DWORD size;
};

整个数据解完后,数据是这样的,这就能看懂了。
image.png
hashver解密后的数据应该是文件名。经过后续对这里的数据下硬件断点Read,跟踪发现,这里的数据结构大概是这样的。hashver{dowrd unknown;word size;char str[]  (end of 0x00);qword unknown;word nametohash;}很明显了,这里面的数据是一个个的文件,数了一下数量刚刚好18个,还记得FilePackVer的0x12吗,两者刚刚好相等。基本可以确定了FilepackVer里的那个就是文件的数量。之后继续分析,发现HashVer的结构应该是这样的,但是同时也发现,HashVer的数据对解包并没有什么帮助。
[C++] 纯文本查看 复制代码
HashVer 
{
        char sign[16];
    	DWORD  table_size;
	DWORD  file_count;
	DWORD  index_size;
	DWORD  data_size;
	DWORD  iscompressed;
	char unknown1[32];
}

文件入口结构
此时我们跳出这个函数,发现后面直接取了FilepackVer的地址数据,然后作为文件偏移 image.png 然后尝试读取了2字节的数据,并拿这个数据*2,后读取数据*2字节的数据。然后拿这一部分数据解密。得到文件名。汇编代码大概这样
[Asm] 纯文本查看 复制代码
004E4C1E         | 53              | push ebx                                           |
004E4C1F         | 56              | push esi                                           |
004E4C20         | 57              | push edi                                           | edi:"0yC"
004E4C21         | 33DB            | xor ebx,ebx                                        |
004E4C23         | 895D FC         | mov dword ptr ss:[ebp-0x4],ebx                     |
004E4C26         | 894D F0         | mov dword ptr ss:[ebp-0x10],ecx                    |
004E4C29         | 8BDA            | mov ebx,edx                                        | edx:"l;p"
004E4C2B         | 8945 F8         | mov dword ptr ss:[ebp-0x8],eax                     |
004E4C2E         | 33C0            | xor eax,eax                                        |
004E4C30         | 55              | push ebp                                           |
004E4C31         | 68 334D4E00     | push 月に寄りそう乙女の作法22.4E4D33                          |
004E4C36         | 64:FF30         | push dword ptr fs:[eax]                            | eax:"^??~8"
004E4C39         | 64:8920         | mov dword ptr fs:[eax],esp                         |
004E4C3C         | 8BC3            | mov eax,ebx                                        | eax等于hash
004E4C3E         | C1E8 10         | shr eax,0x10                                       |
004E4C41         | 25 FFFF0000     | and eax,0xFFFF                                     |
004E4C46         | 33D8            | xor ebx,eax                                        | key = ((hash >> 0x10) & 0xFFFF) ^ hash
004E4C48         | 8B45 F0         | mov eax,dword ptr ss:[ebp-0x10]                    |
004E4C4B         | 33D2            | xor edx,edx                                        | edx:"l;p"
004E4C4D         | E8 D629F2FF     | call 月に寄りそう乙女の作法22.407628                          |
004E4C52         | 8D55 E6         | lea edx,dword ptr ss:[ebp-0x1A]                    |
004E4C55         | B9 02000000     | mov ecx,0x2                                        |
004E4C5A         | 8B45 F8         | mov eax,dword ptr ss:[ebp-0x8]                     |
004E4C5D         | 8B30            | mov esi,dword ptr ds:[eax]                         |
004E4C5F         | FF56 0C         | call dword ptr ds:[esi+0xC]                        | 读取文件
004E4C62         | 66:837D E6 00   | cmp word ptr ss:[ebp-0x1A],0x0                     | 读出的值是否大于等于0
004E4C67         | 7D 16           | jge 月に寄りそう乙女の作法22.4E4C7F                           |
004E4C69         | B9 504D4E00     | mov ecx,月に寄りそう乙女の作法22.4E4D50                       | 4E4D50:L"_LoadStringCode:Out of length error."
004E4C6E         | B2 01           | mov dl,0x1                                         | 小于0就报错
004E4C70         | A1 D4004100     | mov eax,dword ptr ds:[0x4100D4]                    |
004E4C75         | E8 D280F3FF     | call 月に寄りそう乙女の作法22.41CD4C                          |
004E4C7A         | E9 98000000     | jmp 月に寄りそう乙女の作法22.4E4D17                           |
004E4C7F         | 0FBF75 E6       | movsx esi,word ptr ss:[ebp-0x1A]                   |
004E4C83         | 85F6            | test esi,esi                                       |
004E4C85         | 0F8E 8C000000   | jle 月に寄りそう乙女の作法22.4E4D17                           |
004E4C8B         | 56              | push esi                                           | Arg1
004E4C8C         | 8D45 FC         | lea eax,dword ptr ss:[ebp-0x4]                     |
004E4C8F         | B9 01000000     | mov ecx,0x1                                        |
004E4C94         | 8B15 F44B4E00   | mov edx,dword ptr ds:[0x4E4BF4]                    | edx:"l;p"
004E4C9A         | E8 6D44F2FF     | call 月に寄りそう乙女の作法22.40910C                          | Dynarraysetlength
004E4C9F         | 83C4 04         | add esp,0x4                                        |
004E4CA2         | 8B45 F0         | mov eax,dword ptr ss:[ebp-0x10]                    |
004E4CA5         | 8BD6            | mov edx,esi                                        | edx:"l;p"
004E4CA7         | E8 F82CF2FF     | call 月に寄りそう乙女の作法22.4079A4                          |
004E4CAC         | 8BCE            | mov ecx,esi                                        |
004E4CAE         | 03C9            | add ecx,ecx                                        |
004E4CB0         | 8B55 FC         | mov edx,dword ptr ss:[ebp-0x4]                     |
004E4CB3         | 8B45 F8         | mov eax,dword ptr ss:[ebp-0x8]                     |
004E4CB6         | 8B38            | mov edi,dword ptr ds:[eax]                         | edi:"0yC"
004E4CB8         | FF57 0C         | call dword ptr ds:[edi+0xC]                        | 读取文件
004E4CBB         | 8BC6            | mov eax,esi                                        | 文件名解密开始
004E4CBD         | 35 133E0000     | xor eax,0x3E13                                     | 这里len异或了3e13
004E4CC2         | 33D8            | xor ebx,eax                                        | 这里异或了之前算出的Key
004E4CC4         | 8BC6            | mov eax,esi                                        |
004E4CC6         | F7EE            | imul esi                                           | eax=pow(esi,2)
004E4CC8         | 33D8            | xor ebx,eax                                        | 继续异或
004E4CCA         | 895D EC         | mov dword ptr ss:[ebp-0x14],ebx                    |
004E4CCD         | 8165 EC FFFF000 | and dword ptr ss:[ebp-0x14],0xFFFF                 |
004E4CD4         | 8B5D EC         | mov ebx,dword ptr ss:[ebp-0x14]                    |
004E4CD7         | 8B7D FC         | mov edi,dword ptr ss:[ebp-0x4]                     |
004E4CDA         | 8B45 F0         | mov eax,dword ptr ss:[ebp-0x10]                    |
004E4CDD         | E8 AA33F2FF     | call 月に寄りそう乙女の作法22.40808C                          |
004E4CE2         | 8945 E8         | mov dword ptr ss:[ebp-0x18],eax                    |
004E4CE5         | 8BC6            | mov eax,esi                                        |
004E4CE7         | 48              | dec eax                                            |
004E4CE8         | 85C0            | test eax,eax                                       |
004E4CEA         | 7C 2B           | jl 月に寄りそう乙女の作法22.4E4D17                            |
004E4CEC         | 40              | inc eax                                            |
004E4CED         | 33D2            | xor edx,edx                                        | edx:"l;p"
004E4CEF         | C1E3 03         | shl ebx,0x3                                        | 解密循环 ebx初值为刚刚算出的key
004E4CF2         | 8D0C1A          | lea ecx,dword ptr ds:[edx+ebx]                     | ecx = edx+ebx
004E4CF5         | 034D EC         | add ecx,dword ptr ss:[ebp-0x14]                    | ecx = ecx+key
004E4CF8         | 81E1 FFFF0000   | and ecx,0xFFFF                                     | ecx = ecx & 0xFFFF
004E4CFE         | 8BD9            | mov ebx,ecx                                        | ebx = ecx
004E4D00         | 0FB70F          | movzx ecx,word ptr ds:[edi]                        | ecx = (WORD)文件名数据
004E4D03         | 66:33CB         | xor cx,bx                                          | result = cx ^ bx
004E4D06         | 8B75 E8         | mov esi,dword ptr ss:[ebp-0x18]                    |
004E4D09         | 66:890E         | mov word ptr ds:[esi],cx                           |
004E4D0C         | 83C7 02         | add edi,0x2                                        | edi:"0yC"
004E4D0F         | 8345 E8 02      | add dword ptr ss:[ebp-0x18],0x2                    |
004E4D13         | 42              | inc edx                                            | edx为循环次数
004E4D14         | 48              | dec eax                                            |
004E4D15         | 75 D8           | jne 月に寄りそう乙女の作法22.4E4CEF                           |
004E4D17         | 33C0            | xor eax,eax                                        |
004E4D19         | 5A              | pop edx                                            | edx:"l;p"
004E4D1A         | 59              | pop ecx                                            |
004E4D1B         | 59              | pop ecx                                            |
004E4D1C         | 64:8910         | mov dword ptr fs:[eax],edx                         | eax:"^??~8", edx:"l;p"
004E4D1F         | 68 3A4D4E00     | push 月に寄りそう乙女の作法22.4E4D3A                          | Arg1 = "_^[嬪]?"
004E4D24         | 8D45 FC         | lea eax,dword ptr ss:[ebp-0x4]                     |
004E4D27         | 8B15 F44B4E00   | mov edx,dword ptr ds:[0x4E4BF4]                    | edx:"l;p"
004E4D2D         | E8 FA44F2FF     | call 月に寄りそう乙女の作法22.40922C                          | sub_40922C
004E4D32         | C3              | ret                                                |
根据这个我们可以写出C++的代码,这样就清晰很多了。
[C++] 纯文本查看 复制代码
void DencryptFileName(void* data,int character_count,DWORD hash)
{
	int key = ((hash >> 0x10) & 0xFFFF) ^ hash;
	key = character_count ^ 0x3E13 ^ key ^ (character_count * character_count);
	DWORD ebx = key;
	DWORD ecx;
	WORD* datapos = (WORD*)data;
	for (size_t i = 0; i < character_count; i++)
	{
		ebx = ebx << 3;
		ecx = (ebx + i + key) & 0xFFFF;
		ebx = ecx;
		*datapos = (*datapos ^ ebx) & 0xFFFF;
		datapos++;
	}
}

接着程序尝试读取0x1C的数据,然后又读取2字节,解密文件名,再读0x1C……这样往复。
这0x1C文件的数据是什么,先看看一个
第一个文件02696C14   23 10 00 00 00 00 00 00 57 02 00 00 F6 06 00 00 02696C24   01 00 00 00 01 00 00 00 23 F2 E4 E4 好像并看不明白。那我们多看几个!最后一个文件02696DD4   BC FE 4C 02 00 00 00 00 79 02 00 00 98 04 00 00 02696DE4   01 00 00 00 01 00 00 00 AF D5 43 24 倒数第二个02696DB8   F8 9D BB 01 00 00 00 00 C4 60 91 00 C4 60 91 0002696DC8   00 00 00 00 01 00 00 00 23 2D 91 F9 倒数第三个02696D9C   2C 99 20 01 00 00 00 00 CC 04 9B 00 CC 04 9B 0002696DAC   00 00 00 00 01 00 00 00 A6 05 51 ED 倒数第四个02696D80   F0 14 8F 00 00 00 00 00 3C 84 91 00 3C 84 91 0002696D90   00 00 00 00 01 00 00 00 13 EF 95 C5倒数第五个02696D64   D0 59 00 00 00 00 00 00 20 BB 8E 00 20 BB 8E 0002696D74   00 00 00 00 01 00 00 00 7A 2A 4C 05 对比了一下,就明白,前一个DWORD明显是一个地址,根据FilepackVer数据的经验,第二个DWORD应该是地址的高位。看第三个和第四个有点困难,对数据敏感的人可能会发现,后四个文件两者都一样,然后前两个不一样,并且发现规律,第三个DWORD<第四个DWORD,并且,第五个DOWRD都是1,而后四个一样的都是0.最后一个不知道有什么用。基本可以大胆猜测这里0x1C的结构的是这样的
[C++] 纯文本查看 复制代码
struct FileEntry
{
        QWORD offset; //老规矩写代码要写成两个int
	DWORD size;
	DWORD dencrypted_size;
	DWORD isCompressed;
	DWORD unkown1; 
	DWORD unkown2;
};
然后这些数据下硬件断点Read,跟踪可以知道这里面结构实际上应该是
[C++] 纯文本查看 复制代码
struct FileEntry
{
	DWORD offset_low;
	DWORD offset_hight;
	DWORD size;
	DWORD dencrypted_size;
	DWORD isCompressed;
	DWORD EncryptType; // 0未加密 1第一种加密算法 2为第二种加密算法
	DWORD hash;
};
倒数第二个数据是加密的类型,这里会有两种加密算法。
最后一个是用于校检的,程序用了一个hash算法,把文件名作为参数计算,算出一个hash。(和之前的HashVer解密后最后一个差不多)
具体算法和解包无关,也就不放进来了。
文件解密算法
image.png
最最最重要的函数登场。近月所有的文件都是解密类型1,没有解密类型2(而万华镜基本都是解密类型2,除了某个特殊文件没有解密类型1)
image.png
函数在第一个call往栈里开了长度为0x100的缓存区,用于第二个函数算hash。第二个call里,利用文件名和之前最早算出的hash算了一个文件名的hash。

这个算hash的算法是这样
[C++] 纯文本查看 复制代码
004ECE7C         | 55              | push ebp                                           |
004ECE7D         | 8BEC            | mov ebp,esp                                        |
004ECE7F         | 83C4 F8         | add esp,0xFFFFFFF8                                 |
004ECE82         | 53              | push ebx                                           |
004ECE83         | 56              | push esi                                           |
004ECE84         | 57              | push edi                                           | edi:"0yC"
004ECE85         | 8955 F8         | mov dword ptr ss:[ebp-0x8],edx                     |
004ECE88         | 8945 FC         | mov dword ptr ss:[ebp-0x4],eax                     |
004ECE8B         | BB 32F58500     | mov ebx,0x85F532                                   | 这两个值之后会用到
004ECE90         | BE 41F63300     | mov esi,0x33F641                                   |
004ECE95         | 8B45 08         | mov eax,dword ptr ss:[ebp+0x8]                     |
004ECE98         | 8B40 08         | mov eax,dword ptr ds:[eax+0x8]                     |
004ECE9B         | 85C0            | test eax,eax                                       |
004ECE9D         | 74 1C           | je 月に寄りそう乙女の作法22.4ECEBB                            |
004ECE9F         | 8BD0            | mov edx,eax                                        | edx:"l;p"
004ECEA1         | 83EA 0A         | sub edx,0xA                                        | edx:"l;p"
004ECEA4         | 66:833A 02      | cmp word ptr ds:[edx],0x2                          | edx:"l;p"
004ECEA8         | 74 11           | je 月に寄りそう乙女の作法22.4ECEBB                            |
004ECEAA         | 8B45 08         | mov eax,dword ptr ss:[ebp+0x8]                     |
004ECEAD         | 8B50 08         | mov edx,dword ptr ds:[eax+0x8]                     | edx:"l;p"
004ECEB0         | 8B45 08         | mov eax,dword ptr ss:[ebp+0x8]                     |
004ECEB3         | 83C0 08         | add eax,0x8                                        |
004ECEB6         | E8 F59CF1FF     | call 月に寄りそう乙女の作法22.406BB0                          |
004ECEBB         | 85C0            | test eax,eax                                       |
004ECEBD         | 74 05           | je 月に寄りそう乙女の作法22.4ECEC4                            |
004ECEBF         | 83E8 04         | sub eax,0x4                                        |
004ECEC2         | 8B00            | mov eax,dword ptr ds:[eax]                         | eax=文件名字符数
004ECEC4         | 8BD0            | mov edx,eax                                        | edx:"l;p"
004ECEC6         | 4A              | dec edx                                            | edx:"l;p"
004ECEC7         | 85D2            | test edx,edx                                       | edx:"l;p"
004ECEC9         | 7C 20           | jl 月に寄りそう乙女の作法22.4ECEEB                            |
004ECECB         | 42              | inc edx                                            | edx:"l;p"
004ECECC         | 33C0            | xor eax,eax                                        |
004ECECE         | 8D48 01         | lea ecx,dword ptr ds:[eax+0x1]                     | 循环开始
004ECED1         | 8B7D 08         | mov edi,dword ptr ss:[ebp+0x8]                     |
004ECED4         | 8B7F 08         | mov edi,dword ptr ds:[edi+0x8]                     | edi:"0yC"
004ECED7         | 0FB77C4F FE     | movzx edi,word ptr ds:[edi+ecx*2-0x2]              | 取文件名的每个字符
004ECEDC         | 8BC8            | mov ecx,eax                                        | eax为循环次数
004ECEDE         | 83E1 07         | and ecx,0x7                                        | ecx=eax & 7
004ECEE1         | D3E7            | shl edi,cl                                         | edi为文件名word字符
004ECEE3         | 03DF            | add ebx,edi                                        | ebx=ebx+(edi << cl)
004ECEE5         | 33F3            | xor esi,ebx                                        | esi^=ebx
004ECEE7         | 40              | inc eax                                            | eax++
004ECEE8         | 4A              | dec edx                                            | edx--
004ECEE9         | 75 E3           | jne 月に寄りそう乙女の作法22.4ECECE                           | 使用文件名算出了一个(两个?esi ebx)hash
004ECEEB         | 8B45 08         | mov eax,dword ptr ss:[ebp+0x8]                     |
004ECEEE         | 8B40 FC         | mov eax,dword ptr ds:[eax-0x4]                     | eax=数据大小
004ECEF1         | 35 DC328F00     | xor eax,0x8F32DC                                   |
004ECEF6         | 33C3            | xor eax,ebx                                        |
004ECEF8         | 03C3            | add eax,ebx                                        | eax=eax^ebx^0x8F32DC+ebx
004ECEFA         | 8B55 08         | mov edx,dword ptr ss:[ebp+0x8]                     |
004ECEFD         | 0342 FC         | add eax,dword ptr ds:[edx-0x4]                     | eax=eax+数据大小
004ECF00         | 8B55 08         | mov edx,dword ptr ss:[ebp+0x8]                     |
004ECF03         | 8B52 FC         | mov edx,dword ptr ds:[edx-0x4]                     | edx=数据大小
004ECF06         | 81E2 FFFFFF00   | and edx,0xFFFFFF                                   | edx=edx & 0xFFFFFF
004ECF0C         | 8BCA            | mov ecx,edx                                        | edx:"l;p"
004ECF0E         | 03D2            | add edx,edx                                        | edx:"l;p"
004ECF10         | 03D2            | add edx,edx                                        | edx:"l;p"
004ECF12         | 03D2            | add edx,edx                                        | edx=edx*8
004ECF14         | 2BD1            | sub edx,ecx                                        | edx=edx-数据大小
004ECF16         | 03C2            | add eax,edx                                        | eax+=edx
004ECF18         | 8B55 08         | mov edx,dword ptr ss:[ebp+0x8]                     |
004ECF1B         | 3342 0C         | xor eax,dword ptr ds:[edx+0xC]                     | 异或了一个奇怪的值 //这个奇怪的值就是之前最早算的hash
004ECF1E         | 03F0            | add esi,eax                                        |
004ECF20         | 8BC6            | mov eax,esi                                        |
004ECF22         | 25 FFFFFF00     | and eax,0xFFFFFF                                   | eax=(eax+esi)&0xFFFFFF
004ECF27         | 8D04C0          | lea eax,dword ptr ds:[eax+eax*8]                   | eax=eax*9
004ECF2A         | 8BC8            | mov ecx,eax                                        |
004ECF2C         | 8B55 F8         | mov edx,dword ptr ss:[ebp-0x8]                     | 循环次数
004ECF2F         | 8B45 FC         | mov eax,dword ptr ss:[ebp-0x4]                     | 输出的缓冲区
004ECF32         | E8 09FFFFFF     | call 月に寄りそう乙女の作法22.4ECE40                          |
004ECF37         | 5F              | pop edi                                            | edi:"0yC"
004ECF38         | 5E              | pop esi                                            |
004ECF39         | 5B              | pop ebx                                            |
004ECF3A         | 59              | pop ecx                                            |
004ECF3B         | 59              | pop ecx                                            |
004ECF3C         | 5D              | pop ebp                                            |
004ECF3D         | C3              | ret                                                |

写成C++代码
[C++] 纯文本查看 复制代码
DWORD* dencrypt3_hash(int hashlen,int datalen,void* filename,int character_count,DWORD hash)
{
	DWORD key1 = 0x85F532; //ebx
	DWORD key2 = 0x33F641; //esi
	WORD* character = (WORD*)filename;
	for (size_t i = 0; i < character_count; i++)
	{
		key1 = key1 + (*character << (i & 7));
		key2 ^= key1;
		character++;
	}
	DWORD key3 = (datalen ^ key1 ^ 0x8F32DC) + key1 + datalen; //eax
	DWORD key4 = ((datalen & 0xFFFFFF) << 3) - datalen; //edx
	key3 += key4;
	key3 ^= hash;
	key3 = ((key3 + key2) & 0xFFFFFF) * 9;
	//第二个计算函数
	unsigned long long rax = key3;
	DWORD* result = new DWORD[hashlen];
	for (size_t i = 0; i < hashlen; i++)
	{
		rax = (unsigned long long)(rax ^ 0x8DF21431u) * (unsigned long long)0x8DF21431u;
		rax = ((rax & 0xFFFFFFFF00000000) >> 32) + (rax & 0xFFFFFFFF);
		rax = rax & 0xFFFFFFFF;
		result[i] = rax;
	}
 
 
	return result;
}

最后是dencrypt3

关键的代码是这部分
[Asm] 纯文本查看 复制代码
004ED02B         | 59              | pop ecx                                            |
004ED02C         | 8B45 08         | mov eax,dword ptr ss:[ebp+0x8]                     |
004ED02F         | 8B40 FC         | mov eax,dword ptr ds:[eax-0x4]                     | [eax-4]:L"甄Cа"
004ED032         | C1E8 03         | shr eax,0x3                                        | eax=数据大小>>3
004ED035         | 8945 FC         | mov dword ptr ss:[ebp-0x4],eax                     |
004ED038         | 837D FC 00      | cmp dword ptr ss:[ebp-0x4],0x0                     | 数据大小 >> 3 等于0就结束
004ED03C         | 74 73           | je 月に寄りそう乙女の作法22.4ED0B1                            |
004ED03E         | 8B45 08         | mov eax,dword ptr ss:[ebp+0x8]                     |
004ED041         | 8B40 F8         | mov eax,dword ptr ds:[eax-0x8]                     |
004ED044         | 8945 F4         | mov dword ptr ss:[ebp-0xC],eax                     |
004ED047         | 8D85 F0FEFFFF   | lea eax,dword ptr ss:[ebp-0x110]                   | eax = 刚刚算出hash的指针
004ED04D         | 8945 F0         | mov dword ptr ss:[ebp-0x10],eax                    |
004ED050         | 8B85 24FFFFFF   | mov eax,dword ptr ss:[ebp-0xDC]                    | 相当于取了算出的hash指针+0x34的DWORD key1
004ED056         | 83E0 0F         | and eax,0xF                                        |
004ED059         | 03C0            | add eax,eax                                        |
004ED05B         | 03C0            | add eax,eax                                        |
004ED05D         | 03C0            | add eax,eax                                        | key1 = (val & 0xF) << 3
004ED05F         | 8945 F8         | mov dword ptr ss:[ebp-0x8],eax                     |
004ED062         | 50              | push eax                                           |
004ED063         | 53              | push ebx                                           |
004ED064         | 51              | push ecx                                           |
004ED065         | 52              | push edx                                           | edx:"l;p"
004ED066         | 56              | push esi                                           |
004ED067         | 57              | push edi                                           | edi:"0yC"
004ED068         | 8B4D FC         | mov ecx,dword ptr ss:[ebp-0x4]                     | ecx=数据大小>>3
004ED06B         | 8B55 F8         | mov edx,dword ptr ss:[ebp-0x8]                     | edx = key1
004ED06E         | 8B7D F4         | mov edi,dword ptr ss:[ebp-0xC]                     | edi指向了要解密的数据
004ED071         | 8B75 F0         | mov esi,dword ptr ss:[ebp-0x10]                    | esi指向之前算出的hash
004ED074         | 0F6F7E 18       | movq mm7,qword ptr ds:[esi+0x18]                   | mm7 = *(QWORD*)(hash+0x18)
004ED078         | 8D0432          | lea eax,dword ptr ds:[edx+esi]                     |
004ED07B         | 0F6F30          | movq mm6,qword ptr ds:[eax]                        | mm6 = *(QWORD*)(hash+key1)
004ED07E         | 0FEFFE          | pxor mm7,mm6                                       | mm7 ^= mm6
004ED081         | 0FFEFE          | paddd mm7,mm6                                      |
004ED084         | 0F6F07          | movq mm0,qword ptr ds:[edi]                        | mm0 = data
004ED087         | 0FEFC7          | pxor mm0,mm7                                       | mm0 ^= mm7
004ED08A         | 0F6FC8          | movq mm1,mm0                                       | mm1 = mm0
004ED08D         | 0F7F07          | movq qword ptr ds:[edi],mm0                        | 解密3写入数据
004ED090         | 0FFCF9          | paddb mm7,mm1                                      |
004ED093         | 0FEFF9          | pxor mm7,mm1                                       |
004ED096         | 0F72F7 01       | pslld mm7,0x1                                      |
004ED09A         | 0FFDF9          | paddw mm7,mm1                                      |
004ED09D         | 83C7 08         | add edi,0x8                                        | edi:"0yC"
004ED0A0         | 83C2 08         | add edx,0x8                                        | edx:"l;p"
004ED0A3         | 83E2 7F         | and edx,0x7F                                       | key1 = (key1 + 8)& 0x7F
004ED0A6         | 49              | dec ecx                                            |
004ED0A7         | 75 CF           | jne 月に寄りそう乙女の作法22.4ED078                           |

写成C++
[C++] 纯文本查看 复制代码
void dencrypt3(void* data,int len, void* filekey)
{
	//0x34相当于4字节数据+0xD
	DWORD key1 = (*((DWORD*)filekey + 0xD) & 0xF) << 3;
	BYTE* datapos = (BYTE*)data, * fkey = (BYTE*)filekey;
	__m64 mm7 = *((__m64*)filekey + 0x3); //这里0x3相当于BYTE的0x18
	__m64 mm6, mm0, mm1;
	for (size_t i = 0; i < len >>3; i++)
	{
		mm6 = *(__m64*)(fkey + key1);
		mm7 = _m_pxor(mm7, mm6);
		mm7 = _m_paddd(mm7, mm6);
		mm0 = *(__m64*)datapos;
		mm0 = _m_pxor(mm0, mm7);
		mm1 = mm0;
		*(__m64*)datapos = mm0;
		mm7 = _m_paddb(mm7, mm1);
		mm7 = _m_pxor(mm7, mm1);
		mm7 = _m_pslldi(mm7, 0x1);
		mm7 = _m_paddw(mm7, mm1);
		datapos += 8;
		key1 = (key1 + 8) & 0x7F;
	}
	_m_empty();
	return;
}


此时,目前的函数足够解包近月2.2了。可是并不能解包万华镜5。

万华镜5的解包


好了,我们开始调试万华镜5,我们可以用同样的方式去找到万华镜5的函数dencrypt4.
但是我不这么干,因为两个引擎是一样的,所以解密函数应该都一样,在近月上面跟踪那么久的精力不能说放弃就放弃。直接复制近月2.2的dencrypt3附近的字节码作为特征码,然后用ida搜索,嗖的一下,很快啊,关键的函数就定位成功了。然后在这边的dencrypt4和dencrypt4里面计算hash的函数下断点,分析。发现两个算法基本上一样,就是部分参数的区别和一两条代码的区别。但是dencrypt4里有一个最大的区别在这里 image.png 这里的计算多用了一个长度为0x400(注释上有误)的key
算法的没什么难度,这里最难的是要知道这个key是怎么算出来的,他从那里来,什么算法?


要找到这个key的算法基本上只有两种方法(我能想到的,靠谱的,实际可以操作的)
一种是把周围的函数都看一遍,看看有没有可疑的。这种方法讲道理就是把整个Qlie引擎的执行过程理解透彻,说的很简单,但是实际上难度很大,而且操作起来不知道自己距离目标还有多远,是一个无底洞。
另一种是一直步过回溯,回到上一层,找到这个key没有生成的时候,然后和已经生成时候之间的代码一点点排查。这个方法相比上一个会好很多。
还有一种理论上可行,但是实际上不知道怎么操作的方法。就是想办法hook住所有写内存的操作,判断写入的数据是不是key。这个方法很无脑,最大的问题是如何判断写入的数据是不是key,因为我们并不知道这个数据是怎么写入的(什么类型写入,写入的通过的寄存器是哪个,是一次性写入还是分段的,如果分段又是怎么分的…………),key很长,不是一个DWORD的事情,比较麻烦,理论上hook可以把全部内存设置为读和执行,禁止写操作,然后引发异常从而hook,但是判断写入的数据是不是key,讲道理,有点难。


回溯找key的算法
这里我们用上CE,CE本质上是一个优秀的内存搜索器,我们用他来搜索key。
key的部分内容是这样的73 A5 CC 43 00 87 66 36 C3 37 96 40 91 AD 12 FA A1 B8 A9 22 CE D9 25 AB 98 34 E2 C9 E8 9C F2 1B我们用CE的字节数组搜索。在第一次dencrypt4断下来的时候,CE的搜索结果 image.png 此时就已经有key算出来了,而且是9个。。。。
一路F8步过,回溯到一层找到调用Dencrypt4宿主函数之前的函数下断点,重启游戏,然后搜索。找到两个之间的分界。
尝试了N遍之后,回到了这里 image.png 此时领空已经不是用户代码,也不是系统函数代码, 而是引擎的代码。
在进入这个循环之前用CE搜索,结果很遗憾,key依旧存在。
继续回溯,来的这里的时候 image.png 上面的filepack_initializeerror和下面的filepack_keyerror引起了我的注意。我预感距离成功已经不远了,我往上设断,最终试出了那个计算key的函数在上面那个注释那里。
好了,具体到某个call了,我们一层层递进。进入call之后,在每个call之后都CE搜索一遍key,一点点逼近。
下一层在这里 image.png 再下一层 image.png 再下一层 image.png 在下一层 image.png 在这个call之后,来到了最关键的算key的代码上了 image.png 分析代码,发现是用程序里的一个pack_keyfile_kfueheish15538fa9or.key的文件算的,巧的是pack里面为了验证,也会有一个一模一样的文件,而且这个文件貌似只使用加密1,可以偷一下懒,这个文件就不去程序里面定位了,直接用pack文件里的,写出C的代码
[C++] 纯文本查看 复制代码
BYTE* dencypt4_keyfilehash(void* data,int len)
{
	int* keyfilehash = new int[0x100];
	int* keyfilehash_pos = keyfilehash;
	//keyhash初始数据的计算
	for (size_t i = 0; i < 0x100; i++)
	{
		if (i % 3 ==0)
		{
			*keyfilehash_pos = (i + 3u) * (i + 7u);
		}
		else
		{
			*keyfilehash_pos = -(i + 3u) * (i + 7u);
		}
		keyfilehash_pos++;
	}
	int key1 = *(BYTE*)((BYTE*)data + 0x31);
	key1 = (key1 % 0x49) + 0x80;
	int key2 = *(BYTE*)((BYTE*)data + 0x1E + 0x31);
	key2 = (key2 % 7) + 7;
	BYTE* keyfilehash_pos_byte = (BYTE*)keyfilehash;
	for (size_t i = 0; i < 0x400; i++)
	{
		key1 = (key1 + key2) % len;
		*keyfilehash_pos_byte ^= *(BYTE*)((BYTE*)data + key1);
		keyfilehash_pos_byte++;
	}
	return (BYTE*)keyfilehash;
}
此时就差不多能解包了。
解包步骤
  • 先读文件末尾0x1C数据,验证签名,然后记住入口点和文件数
  • 向上读取HashData,用HashData算出Hash
  • 验证HashVer签名,因为HashVer里的数据对解包并不重要,可以偷懒不解码
  • 到文件入口点,读取文件名长度,在读取文件名数据,然后根据后0x1C的数据,判断文件信息,选择合适的算法解密

完整代码

完整代码如下,已经上传到了Github:https://github.com/Aobanana-chan/UnpackQlie
[C++] 纯文本查看 复制代码
#include <iostream>
#include <Windows.h>
#include <string>
#include <mmintrin.h>
#include <stack>
using namespace std;
struct FilePackVer
{
	char sign[0x10];
	DWORD filecount;
	int entry_low;
	int entry_high;
};
struct HashData
{
	char sign[0x20];
	DWORD HashVerSize;
	char data[0x100];
	DWORD Unkown;
	char Blank[0x2F8];
	FilePackVer fpacker;
};
struct Dencrypt2DataHead
{
	DWORD sign;
	DWORD isWordType;
	DWORD size;
};
struct Dencrypt2DataOutput
{
	BYTE* data;
	DWORD len;
};
struct FileEntry
{
	DWORD offset_low;
	DWORD offset_hight;
	DWORD size;
	DWORD dencrypted_size;
	DWORD isCompressed;
	DWORD EncryptType; // 0未加密 1第一种加密算法 2为第二种加密算法
	DWORD hash;
};
DWORD Tohash(void* data, int len)
{
	if (len < 8)
	{
		return 0;
	}
	//准备工作
	__m64 mm0 = _mm_cvtsi32_si64(0);
	__m64 mm1;
	__m64 mm2 = _mm_cvtsi32_si64(0);
	DWORD key = 0xA35793A7;
	__m64 mm3 = _mm_cvtsi32_si64(key);
	 mm3 = _m_punpckldq(mm3, mm3);
	 __m64* pdata=(__m64*)data;
	//开始循环计算hash
	for (size_t i = 0; i < (len >> 3); i++)
	{
		mm1 = *pdata;
		pdata++;
		mm2 = _m_paddw(mm2, mm3);
		mm1 = _m_pxor(mm1, mm2);
		mm0 = _m_paddw(mm0, mm1);
		mm1 = mm0;
		mm0 = _m_pslldi(mm0, 3);
		mm1 = _m_psrldi(mm1, 0x1D);
		mm0 = _m_por(mm1, mm0);
	}
	mm1 = _m_psrlqi(mm0, 32);
	DWORD result = _mm_cvtsi64_si32(_m_pmaddwd(mm0, mm1));
	_m_empty();//复位浮点寄存器
	return result;
}
void dencrypt(void* data,unsigned int len, DWORD hash)
{
	if (len >> 3 == 0)
	{
		return;
	}
	//准备工作
	DWORD key1 = 0xA73C5F9D;
	DWORD key2 = 0xCE24F523;
	DWORD key3 = (len + hash)^ 0xFEC9753E;
	__m64 mm7 = _mm_cvtsi32_si64(key1);
	mm7 = _m_punpckldq(mm7, mm7);
	__m64 mm6 = _mm_cvtsi32_si64(key2);
	mm6 = _m_punpckldq(mm6, mm6);
	__m64 mm5 = _mm_cvtsi32_si64(key3);
	mm5 = _m_punpckldq(mm5, mm5);
	__m64* datapos = (__m64*)data;
	__m64 mm0;
	for (size_t i = 0; i < len >> 3; i++)
	{
		mm7 = _m_paddd(mm7, mm6);
		mm7 = _m_pxor(mm7, mm5);
		mm0 = *datapos;
		mm0 = _m_pxor(mm0, mm7);
		mm5 = mm0;
		*datapos = mm0;
		datapos++;
	}
	_m_empty();//复位浮点寄存器
	return;
}
Dencrypt2DataOutput* dencrypt2(void* data, unsigned int len,unsigned int dencrypted_len, DWORD hash)
{
	char Sampletable[0x100],table[0x100],other[0x100];
	for (size_t i = 0; i < 0x100; i++)
	{
		Sampletable[i] = i;
	}
	Dencrypt2DataHead* head = (Dencrypt2DataHead*)data;
	//对比开头是否为0xFF425031
	if (head->sign != 0xFF435031)
	{
		cout << "数据不符合解码条件" << endl;
		return nullptr;
	}
	if (head->size> 0x20000000u)
	{
		cout << "数据量大于0x20000000" << endl;
		return nullptr;
	}
 
	Dencrypt2DataOutput* Output = new Dencrypt2DataOutput();
	Output->len = dencrypted_len;
	Output->data = new BYTE[dencrypted_len + 1];
	BYTE* outputbuff = Output->data;
 
	BYTE* datapos = (BYTE*)data + sizeof(Dencrypt2DataHead);
	BYTE* data_start = datapos;
	BYTE* data_end = (BYTE*)data + len;
	BYTE chr;
	int t_pos;
	int size;
	while (data_start < data_end)
	{
		chr = *data_start;
		datapos = data_start + 1;
		memcpy(table, Sampletable, 0x100);
		t_pos = 0;
		//建表循环
		while (1)
		{
			if (chr > 0x7Fu)
			{
				t_pos += chr - 127;
				chr = 0;
			}
			if (t_pos > 0xFF)
			{
				break;
			}
 
			for (size_t i = 0; i < chr + 1; i++)
			{
				table[t_pos] = *datapos++;
				if (t_pos != (unsigned __int8)table[t_pos])
				{
					other[t_pos] = *datapos++;
				}
 
				++t_pos;
			}
			if (t_pos > 0xFF)
			{
				break;
			}
			chr = *datapos++;
		}
		//数据类型判断
		if ((head->isWordType & 1) == 1)
		{
			size = *(WORD*)datapos;
			data_start = (datapos + 2);
		}
		else
		{
			size = *(DWORD*)datapos;
			data_start = (datapos + 4);
		}
		//解密循环
		stack<BYTE> stack;
		while (1)
		{
			BYTE result;
			if (stack.size())
			{
				result = stack.top();
				stack.pop();
			}
			else
			{
				if (!size)
				{
					break;
				}
				size--;
				result = *data_start;
				data_start++;
			}
			if (result == (BYTE)table[result])
			{
				*outputbuff = result;
				outputbuff++;
			}
			else
			{
				stack.push(other[result]);
				stack.push(table[result]);
			}
		}
	}
	return Output;
}
void DencryptFileName(void* data,int character_count,DWORD hash)
{
	int key = ((hash >> 0x10) & 0xFFFF) ^ hash;
	key = character_count ^ 0x3E13 ^ key ^ (character_count * character_count);
	DWORD ebx = key;
	DWORD ecx;
	WORD* datapos = (WORD*)data;
	for (size_t i = 0; i < character_count; i++)
	{
		ebx = ebx << 3;
		ecx = (ebx + i + key) & 0xFFFF;
		ebx = ecx;
		*datapos = (*datapos ^ ebx) & 0xFFFF;
		datapos++;
	}
}
DWORD* dencrypt3_hash(int hashlen,int datalen,void* filename,int character_count,DWORD hash)
{
	DWORD key1 = 0x85F532; //ebx
	DWORD key2 = 0x33F641; //esi
	WORD* character = (WORD*)filename;
	for (size_t i = 0; i < character_count; i++)
	{
		key1 = key1 + (*character << (i & 7));
		key2 ^= key1;
		character++;
	}
	DWORD key3 = (datalen ^ key1 ^ 0x8F32DC) + key1 + datalen; //eax
	DWORD key4 = ((datalen & 0xFFFFFF) << 3) - datalen; //edx
	key3 += key4;
	key3 ^= hash;
	key3 = ((key3 + key2) & 0xFFFFFF) * 9;
	//第二个计算函数
	unsigned long long rax = key3;
	DWORD* result = new DWORD[hashlen];
	for (size_t i = 0; i < hashlen; i++)
	{
		rax = (unsigned long long)(rax ^ 0x8DF21431u) * (unsigned long long)0x8DF21431u;
		rax = ((rax & 0xFFFFFFFF00000000) >> 32) + (rax & 0xFFFFFFFF);
		rax = rax & 0xFFFFFFFF;
		result[i] = rax;
	}
 
 
	return result;
}
void dencrypt3(void* data,int len, void* filekey)
{
	//0x34相当于4字节数据+0xD
	DWORD key1 = (*((DWORD*)filekey + 0xD) & 0xF) << 3;
	BYTE* datapos = (BYTE*)data, * fkey = (BYTE*)filekey;
	__m64 mm7 = *((__m64*)filekey + 0x3); //这里0x3相当于BYTE的0x18
	__m64 mm6, mm0, mm1;
	for (size_t i = 0; i < len >>3; i++)
	{
		mm6 = *(__m64*)(fkey + key1);
		mm7 = _m_pxor(mm7, mm6);
		mm7 = _m_paddd(mm7, mm6);
		mm0 = *(__m64*)datapos;
		mm0 = _m_pxor(mm0, mm7);
		mm1 = mm0;
		*(__m64*)datapos = mm0;
		mm7 = _m_paddb(mm7, mm1);
		mm7 = _m_pxor(mm7, mm1);
		mm7 = _m_pslldi(mm7, 0x1);
		mm7 = _m_paddw(mm7, mm1);
		datapos += 8;
		key1 = (key1 + 8) & 0x7F;
	}
	_m_empty();
	return;
}
BYTE* dencypt4_keyfilehash(void* data,int len)
{
	int* keyfilehash = new int[0x100];
	int* keyfilehash_pos = keyfilehash;
	//keyhash初始数据的计算
	for (size_t i = 0; i < 0x100; i++)
	{
		if (i % 3 ==0)
		{
			*keyfilehash_pos = (i + 3u) * (i + 7u);
		}
		else
		{
			*keyfilehash_pos = -(i + 3u) * (i + 7u);
		}
		keyfilehash_pos++;
	}
	int key1 = *(BYTE*)((BYTE*)data + 0x31);
	key1 = (key1 % 0x49) + 0x80;
	int key2 = *(BYTE*)((BYTE*)data + 0x1E + 0x31);
	key2 = (key2 % 7) + 7;
	BYTE* keyfilehash_pos_byte = (BYTE*)keyfilehash;
	for (size_t i = 0; i < 0x400; i++)
	{
		key1 = (key1 + key2) % len;
		*keyfilehash_pos_byte ^= *(BYTE*)((BYTE*)data + key1);
		keyfilehash_pos_byte++;
	}
	return (BYTE*)keyfilehash;
}
DWORD* dencrypt4_hash(int hashlen, int datalen, void* filename, int character_count, DWORD hash)
{
	DWORD key1 = 0x86F7E2; //ebx
	DWORD key2 = 0x4437F1; //esi
	WORD* character = (WORD*)filename;
	for (size_t i = 0; i < character_count; i++)
	{
		key1 = key1 + (*character << (i & 7));
		key2 ^= key1;
		character++;
	}
	DWORD key3 = (datalen ^ key1 ^ 0x56E213) + key1 + datalen; //eax
	int key4 = (datalen & 0xFFFFFF) * 0xD; //edx
	key3 += key4;
	key3 ^= hash;
	key3 = ((key3 + key2) & 0xFFFFFF) * 0xD;
	//第二个计算函数
	unsigned long long rax = key3;
	DWORD* result = new DWORD[hashlen];
	for (size_t i = 0; i < hashlen; i++)
	{
		rax = (unsigned long long)(rax ^ 0x8A77F473u) * (unsigned long long)0x8A77F473u;
		rax = ((rax & 0xFFFFFFFF00000000) >> 32) + (rax & 0xFFFFFFFF);
		rax = rax & 0xFFFFFFFF;
		result[i] = rax;
	}
 
 
	return result;
}
void dencrypt4(void* data, int len, void* filekey,void* keyfilehash)
{
	//0x20相当于4字节数据+0x8
	DWORD key1 = (*((DWORD*)filekey + 0x8) & 0xD) << 3;
	BYTE* datapos = (BYTE*)data, * fkey = (BYTE*)filekey,* keyfilekey = (BYTE*)keyfilehash;
	__m64 mm7 = *((__m64*)filekey + 0x3); //这里0x3相当于BYTE的0x18
	__m64 mm6, mm0, mm1,mm5;
	for (size_t i = 0; i < len >> 3; i++)
	{
		mm6 = *(__m64*)(fkey + ((key1 & 0xF) << 3));
		mm5 = *(__m64*)(keyfilekey + ((key1 & 0x7F) << 3));
		mm6 = _m_pxor(mm6, mm5);
		mm7 = _m_pxor(mm7, mm6);
		mm7 = _m_paddd(mm7, mm6);
		mm0 = *(__m64*)datapos;
		mm0 = _m_pxor(mm0, mm7);
		mm1 = mm0;
		*(__m64*)datapos = mm0;
		mm7 = _m_paddb(mm7, mm1);
		mm7 = _m_pxor(mm7, mm1);
		mm7 = _m_pslldi(mm7, 0x1);
		mm7 = _m_paddw(mm7, mm1);
		datapos += 8;
		key1 = (key1 + 1) & 0x7F;
	}
	_m_empty();
	return;
}
FILE* WideChar_CreateFile(const wchar_t* filename)
{
	wchar_t* pos = (wchar_t*)filename;
	while (1)
	{
		pos = wcschr(pos, '\\');
		if (pos == nullptr)
		{
			break;
		}
		wchar_t* dir = new wchar_t[pos - filename + 1]();
		wcsncpy(dir, filename, pos - filename);
		_wmkdir(dir);
		pos++;
		delete dir;
	}
	FILE* hfile = _wfopen(filename, L"wb");
	return hfile;
}
int main()
{
	string filename;
	cin >> filename;
	FILE* hfile;
	hfile = fopen(filename.c_str(), "rb");
	//获取文件大小,支持大于4GB文件
	_fseeki64(hfile, 0, 2);
	fpos_t file_size = _ftelli64(hfile);
	//读取filepack头
	_fseeki64(hfile, file_size - 0x1C, 0);
	FilePackVer* filepacker = new FilePackVer();
	fread(filepacker, 0x1C,1 , hfile);
	if (string(filepacker->sign) != "FilePackVer3.1\x00\x00")
	{
		cout << "FilePackVer签名验证失败" << endl;
		return 0;
	}
	//读取HashData
	HashData *hashdat = new HashData();
	_fseeki64(hfile,file_size-0x440,0);
	fread(hashdat,1,0x440,hfile);
	//数据的设置
	if (hashdat->Unkown > 8 || hashdat->Unkown < 0)
	{
		hashdat->Unkown = 0;
	}
	DWORD hash = Tohash(&hashdat->data,0x100) & 0x0FFFFFFF;
	//HashVer里的数据对解包并不重要 直接略过不按照程序一样去读取了
 
 
	/////////////////////////////////
	//解码签名
	dencrypt(&hashdat->sign, 0x20, hash);
	if (strncmp(hashdat->sign,"8hr48uky,8ugi8ewra4g8d5vbf5hb5s6",0x20))
	{
		cout << "HashData签名验证失败" << endl;
		return 0;
	}
	//开始解密文件
	
	DWORD64 entry = ((long long)filepacker->entry_high << 32) + (long long)filepacker->entry_low;
	BYTE* keyfilehash = nullptr;
	for (size_t i = 0; i < filepacker->filecount; i++)
	{
		_fseeki64(hfile, entry, 0);
		WORD character_count;
		fread(&character_count, 2, 1, hfile);
		wchar_t* name = new wchar_t[character_count + 1]();
		//因为UTF16字节数是ASCII的两倍,所以要乘2
		fread(name, 1, 2 * character_count, hfile);
		//解密文件名
		DencryptFileName(name, character_count, hash);
		FileEntry *fentry = new FileEntry();
		fread(fentry, 1, 0x1C, hfile);
		entry = _ftelli64(hfile);
		//文件名hash校检 不重要 略过
 
		//文件读取
		char* filedata = new char[fentry->size];
		_fseeki64(hfile, ((long long)fentry->offset_hight << 32) + (long long)fentry->offset_low, 0);
		fread(filedata, fentry->size, 1, hfile);
 
		//解密文件
		DWORD* filehash = nullptr;
		if (fentry->EncryptType == 1)
		{
			filehash = dencrypt3_hash(0x40, fentry->size, name, character_count, hash);
			dencrypt3(filedata, fentry->size, filehash);
			if (wcsncmp(name, L"pack_keyfile_kfueheish15538fa9or.key", character_count) == 0)
			{
				keyfilehash = dencypt4_keyfilehash(filedata, fentry->size);
			}
		}
		else if(fentry->EncryptType == 2)
		{
			filehash = dencrypt4_hash(0x40, fentry->size, name, character_count, hash);
			dencrypt4(filedata, fentry->size, filehash, keyfilehash);
		}
		Dencrypt2DataOutput* Output = nullptr;
		if (fentry->isCompressed)
		{
			Output = dencrypt2(filedata, fentry->size, fentry->dencrypted_size, hash);
		}
		else
		{
			Output = new Dencrypt2DataOutput();
			Output->data = (BYTE*)filedata;
			Output->len = fentry->dencrypted_size;
		}
		//保存文件
		wstring filename = wstring(name);
		filename = L"Extract\\" + filename;
		FILE* hOut = WideChar_CreateFile(filename.c_str());
		std::fwrite(Output->data, Output->len, 1, hOut);
		std::fclose(hOut);
		delete fentry, name, filedata, filehash, Output;
	}
 
	std::fclose(hfile);
}


后记


本来解包的时候我用python,结果发现自己手写的mmx指令集的算法效率不忍直视。后来发现了一个叫peachpy的可以用python写汇编代码的神奇玩意,结果发现文档太少,不知道条件的跳转之类的怎么写,传参各种出问题,搞的最后跑都跑不起来,无奈之下还是用C++去写了。不得不说C++在处理这种UTF16字符编码问题的时候是真的麻烦。。。。

这个文章的精华部分并不在算法的分析,而是如何定位关键的算法。

最后,文件虽然可以解包成功,但是依旧有问题是,png图片是没办法直接查看的,貌似还有一层加密(镜5的汉化组打包的汉化补丁里的png是可以直接看的,估计这层加密是打包之前干的,所以并不是解包的算法有问题)。其他文本和音频都是正常的。不过讲道理这个引擎的工具非常全面,我只是研究学习,没必要重复造轮子。

免费评分

参与人数 51吾爱币 +47 热心值 +47 收起 理由
非吾所爱 + 1 + 1 用心讨论,共获提升!
淡月疏桐影 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
s644 + 1 + 1 用心讨论,共获提升!
gba626 + 1 我很赞同!
Ah0NoR + 1 + 1 谢谢@Thanks!
ghast916 + 1 用心讨论,共获提升!
TonyKing + 1 + 1 66666是个大佬
小马奔腾2 + 1 热心回复!
导演 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
FelixTennouji + 1 + 1 我很赞同!
DancingLight + 1 + 1 万华镜……可以啊
weiye588 + 1 我很赞同!
流浪星空 + 1 + 1 我很赞同!
Antik1 + 1 热心回复!
wwwnwx + 1 谢谢@Thanks!
pelephone + 1 + 1 热心回复!
yxdyxd163 + 1 + 1 热心回复!
宅の士 + 1 + 1 谢谢@Thanks!
EHckm + 1 + 1 谢谢@Thanks!
zq769427 + 1 + 1 谢谢@Thanks!
石碎大胸口 + 1 + 1 用心讨论,共获提升!
ILOVEHuhu + 1 + 1 用心讨论,共获提升!
zjc3024 + 1 + 1 热心回复!
Abraham511 + 1 --------
991547436 + 2 + 1 热心回复!
lllliiii + 1 + 1 用心讨论,共获提升!
gaosld + 1 + 1 热心回复!
dingyx99 + 1 + 1 用心讨论,共获提升!
linruo218 + 1 + 1 我很赞同!
sH4N3 + 1 + 1 用心讨论,共获提升!
Equator + 1 + 1 热心回复!
Lugia + 1 + 1 谢谢@Thanks!
26075623 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
xhdxt + 1 + 1 已经处理,感谢您对吾爱破解论坛的支持!
DaiCap + 1 + 1 我很赞同!
夜灯 + 1 + 1 谢谢@Thanks!
Dispa1r + 1 + 1 用心讨论,共获提升!
ldy2333 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
Ravey + 1 + 1 谢谢@Thanks!
独行风云 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
Cave + 1 + 1 热心回复!
XhyEax + 1 + 1 我很赞同!
fengbolee + 2 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
sakura-galaxy + 1 + 1 我很赞同!
千神奈奈子 + 1 用心讨论,共获提升!
剑来…… + 1 + 1 神人也
Forehawk + 1 + 1 我很赞同!
笙若 + 1 + 1 谢谢@Thanks!
wuai1023a + 1 热心回复!
Witheredead + 1 此贴真乃神贴,感谢!
韶阡寒 + 1 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| Aobanana 发表于 2021-8-29 15:51
VanDarkholme 发表于 2021-8-27 20:50
____不愧是科技进步的第一要素

GHS的力量是无穷大的 lsp的能力的可以靠GHS挖掘的
 楼主| Aobanana 发表于 2021-8-27 11:10
yyb1813 发表于 2021-8-27 09:51
强烈希望楼主出个视频.这东东太强大了,非常想拜师

这东西来来回回调试了一个多星期,出个视频看我一遍过其实没什么用 也就看看各种call函数具体算法分析,但是讲道理这个分析汇编算法是基本能力

最最重要的是知道怎么找关键的call
yyb1813 发表于 2021-8-27 09:51
强烈希望楼主出个视频.这东东太强大了,非常想拜师
lws0318 发表于 2021-8-27 09:54
yyb1813 发表于 2021-8-27 09:51
强烈希望楼主出个视频.这东东太强大了,非常想拜师

同愿,看完不容易
y_w_o 发表于 2021-8-27 10:58
楼主厉害,学习了
 楼主| Aobanana 发表于 2021-8-27 11:05
lws0318 发表于 2021-8-27 09:54
同愿,看完不容易

确实看完不容易,里面的一堆call我研究了1个多星期才基本弄明白。也就是说这文章是一个多星期成果的浓缩(越写越多,最后决定把文章中算法的分析简化了) 能看的完就已经很强了
VanDarkholme 发表于 2021-8-27 20:50
____不愧是科技进步的第一要素
低调灬厚道 发表于 2021-8-27 21:37
厉害厉害厉害,我这辈子都看不懂这个操作
xwencai 发表于 2021-8-27 22:08
牛逼,谢谢这么好的文章
C2021 发表于 2021-8-29 09:17
感谢分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2021-9-18 06:01

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表