首先,我要解释一下为什么这篇逆向分析的题目中有“不完全”这几个字。因为偶是一只小菜鸟,水平有限,而这个游戏的加密方式异常复杂,因此偶未能逆出最后一步的解密过程。只能将前面的解密过程分析写出来。所以请各位看官见谅……
游戏的资源文件以npa以及ngs后缀名结尾,其中ngs猜测应该是游戏的对白内容的文件。而npa文件中有cg.npa(cg资源文件),sound.npa以及voice.npa(声音文件),system.npa(系统设置文件)以及nss.npa(脚本资源文件)。而一般游戏脚本在解密完后都能明显地看出来的,于是我就找nss.npa来开刀了。
游戏使用了AlphaDisc加密,这个保护是以外壳的形式将游戏保护起来的。这个外壳有检测调试器(打开\\.\SICE以及\\.\TRW等驱动来检查,查找窗口名称等)的功能,甚至运行FileMon跟RegMon这些监视软件游戏都不能启动。并且外壳有完整性校验(不太确定,因为免cd补丁中只用了3个字节来修改了一个跳转,而游戏却可以运行)。顺带还破坏了程序的输入表了。
用OD载入游戏的时候要在调试选项忽略掉所有异常,然后对CreateFileA下断,一直按F9运行到打开的文件是nss.npa文件为止:
上面的代码是打开nss.npa文件然后读出29个字节.而读出的数据如下:
用WinHex打开其他几个资源文件,发现开头11个字节是一样的.而开头4个字节是明显的文件格式的标志.
接下来的代码是分别保存读取出来的数据以及判断文件头,一直到0x4012cf的一个call,跟进去后发现是一大串读取文件以及解密的操作.首先是一个设定文件指针的操作:
这段代码是将文件指针设定在开头29个字节的位置。接下来又是一串读取以及解密的操作.
这段代码是先分配80个字节的空间,这个空间在下面会用到.然后在nss.npa文件中读取4个字节,这4个字节从下面的部分可以看出是资源名称的长度.
接下来是一个循环,每次读取一个字节,然后进行解密:
上面的解密函数写成C代码大概就是如下:
代码:
void deresourcename(char *name,//name指针指向要解密的字符串
BYTE len,//字符串长度
DWORD a,//传入的是在文件读出的数据,此处是0x00013a14
DWORD b,//传入的是在文件读出的a值后一个双字,此处为0x00016b12
DWORD num//资源序号
)
{
int temp1,temp3;
BYTE t2,t4;
BYTE i,j;
for(i=0;i<len;i++)
{
temp1=a; //temp1=0x00013a14
temp1*=b; //temp1*=0x0016b12
t2=i;
t2*=0xfc;
for(j=3;j>=0;j--)
{
temp3=temp1;
temp3>>=j*8;
t2-=(char)temp3;
}
t4=(char)a; //t4=0x14;
t4*=(char)b; //t4*=0x12;
t2-=t4;
temp1=num;
for(j=3;j>=0;j--)
{
temp3=temp1;
temp3>>=j*8;
t2-=(char)temp3;
}
name[i]+=t2;
}
}
貌似弄成C更难看了- -|||
不过既然叫逆向当然要将算法写成C才行,解密完资源名后,接下来的是一段复制资源名到新分配的内存空间然后连续读取1个字节加4个双字。接下来比较重要的一段代码是:
其中计算hash的那个call代码如下:
写成C代码如下:
代码:
DWORD namehash(char *name)
{
DOWRD temp=0x87654321,i=0;
while(name[i])
{
*(char*)&temp-=name[i];
i++;
}
return i*temp;
}
接下来就是循环的最后一部分代码:
现在来看看内存窗口中的数据:
代码:
01364518 00 00 00 00 00 00 00 00 11 00 11 00 81 01 08 01 ........ . .?
01364528 B0 45 36 01 02 00 00 00 01 00 00 00 01 00 00 00 6 ... ... ...
01364538 16 04 00 00 9F CA 3F 49 00 00 00 00 FF 23 00 00 ..?I....#..
01364548 16 04 00 00 2F 1A 00 00 5E 30 00 00 29 79 EE EE ../ ..^0..)y铑
01364558 18 44 36 01 38 46 36 01 00 00 00 00 00 00 00 00 D6 8F6 ........
01364568 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
01364578 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
01364588 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
01364598 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
013645A8 11 00 11 00 90 01 08 01 62 6F 6F 74 5F 91 E6 88 . .? boot_
013645B8 EA 8F CD 2E 6E 73 73 00 00 00 00 00 00 00 00 00 ?nss.........
013645C8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
013645D8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
013645E8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
013645F8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
01364608 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
01364618 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
01364628 00 00 00 00 00 00 00 00 11 00 11 00 E3 01 08 01 ........ . .?
01364638 C0 46 36 01 02 00 00 00 01 00 00 00 01 00 00 00 6 ... ... ...
01364648 F1 03 00 00 82 D5 1A 91 00 00 00 00 15 28 00 00 ?.. ?... (..
01364658 F1 03 00 00 59 1A 00 00 5E 30 00 00 EA 7B EE EE ?..Y ..^0..铑
01364668 28 45 36 01 48 47 36 01 00 00 00 00 00 00 00 00 (E6 HG6 ........
01364678 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
01364688 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
01364698 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
013646A8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
013646B8 11 00 11 00 F2 01 08 01 62 6F 6F 74 5F 91 E6 8E . .? boot_
013646C8 B5 8F CD 2E 6E 73 73 00 00 00 00 00 00 00 00 00 ?nss.........
好了,从这些数据中我们可以看出不少有效的信息,根据以上的数据我可以得出以下文件头结构:
代码:
struct ResourceHeader{
DWORD nameoffset; //文件名在内存中的地址
DWORD resourcetype; //不确定,在解密过程中读出的第一个字节数据
DWORD unknown1; //第一次读出文件的0x29个字节中的第0x10个
DWORD unknown2; //第一次读出文件的0x29个字节中的第0xf个
DWORD unknownsize; //与从文件中读出的数据有关,具体作用不详
DWORD key1; //最后一个变换得出的数值,用于后面解密
DWORD unknown4; //解密过程中读出的第一个双字的数据
DWORD dataRVA; //数据相对文件头的偏移,解密过程中读出的第二个双字的数据
DWORD sizeoffile; //资源文件的实际大小,解密过程中读出的第三个双字的数据
DWORD unknown5; //解密过程中读出的最后一个双字
DWORD sizeofheader; //所有文件头的大小
DWORD hash; //资源名的hash值
ResourceHeader *Flink; //指向前继节点
ResourceHeader *Blink; //指向后继节点
};
其实上面有些数据到目前都不能确定的,是我在后面的调试中得出来的。其实完整的分析到此为止了,后面的分析很乱,而且也没分析个所以然来。一是由于这个游戏的加密算法实在是太变态,二自然是由于我水平不够(在此要再次向写出这个游戏提取软件的痴汉公贼大大膜拜啊)。
后面的断点怎么下?我一开始打算是继续对CreateFileA下的,但这样太麻烦,因为程序要不停打开SoftICE跟FileMon等软件的驱动来进行反调试,而且老是喜欢打开资源文件什么都不做又将其关闭,于是我想到对内存中的文件头结构下内存访问断点。
于是断到这个地方:
假如不是要分析nss.npa文件的话在第一次断下后可以就在cmp ebx,4那里按F4然后继续分析了,但我想要分析脚本文件,于是就取消内存断点,跳转语句下一句语句按f4,知道boot.nss的字符串出现到堆栈窗口.当找到目标后,对CreateFileA,SetFilePointer以及ReadFile下断,在对boot.nss对应的文件头结构下内存访问断点.在创建完文件后停到以下地方:
好了,这下总算搞明白那些结构中大部分数据的作用了.接下来f9继续运行就是读取数据,读取数据的大小比较诡异,总是读取两次,而且第二次读出的数据进行了一系列作用不明的变换.而第一次读出的数据根据结构中unknownsize的不同会是一次读取2000个字节或一次读出1000个字节.
对读出的数据下内存访问断点,找到一段变换过程:
这段写成C代码如下:
代码:
{
char *buff;
char *key=0x51ecf8;
char tmp;
int i;
for(i=0;i<unknownsize;i++)
{
tmp=key[buff[i]];
tmp-=(char)key1; //key1为文件头中的数据
tmp-=i;
buff[i]=tmp;
}
}
接下来又是一段分配内存的操作,接着就是真正的解密函数了,限于水平,最后的解密函数没逆出来,于是这篇文章就只好到此为止了。
虽然这次逆向没能将游戏的资源解密算法完整逆向出来,但经过此次分析实在是认识到自己还需继续努力学习啊。