(接上文)
数据库逆向工程,第3部分:代码的复用与小结
在第二部分中,我们研究了Microcat Ford USA数据库的内部机制。具体来说,我们已经研究了代表车辆和车辆部件的通用数据结构,接下来,我们将研究零件图,这是我们需要研究的最后一个组件。现在,我们来回顾一下数据结构的依赖轴和数据库架构。
依赖轴
数据库架构
深入剖析MCImage.dat
在上文中,我们发现代表零件树的MCData.idx与包含车辆零件的MCData.dat和包含车辆零件图的MCImage[2].dat相关联。其中,后者是通过image_offset字段(具体如上图所示)和image_size字段进行关联的。下面,让我们通过[2.8]和[2.9]方法来查看图像如何存储到该文件中的。
确定图像的偏移值和图像大小
图像的开头部分
这是什么东东?这看起来不像一个广泛使用的格式,也不太可能是一个压缩图像,因为其中有许多零值和重复的字节。让我们继续往下看。
图像的中间部分
不,都是压缩的,所以,图像的开头部分是一个标题。继续检查文件中的其他图像,确保每个图像都有一个完全不同的标题,且没有字节模式。由于这里没有幻数(magic numbers),所以使情况变得复杂起来,因为我们只知道图像有标题,除此之外,无法借助其他关键词在Internet上进行搜索。
查找并调试图像的显示代码
我们在程序库中搜索“image”字符串后,得到了如下所示的列表。
C:\MCFNA\
18.12.02│186432│A │CSIMGL16.DLL
28.05.07│ 26048│A │FNASTART.DLL
19.08.12│215024│A │FNAUTIL2.DLL
31.10.97│ 6672│A │IMUTIL.DLL
23.05.06│2701 K│A │MCLANG02.DLL
06.09.06│2665 K│A │MCLANG16.DLL
14.04.97│146976│A │MFCOLEUI.DLL
06.09.06│2395 K│A │NAlang16.dll
14.04.97│ 57984│A │QPRO200.DLL
14.04.97│398416│A │VBRUN300.DLL
在CSIMG16、FNAUTIL2和IMUTIL中可以找到我们感兴趣的导出函数。
我们需要找到一个以压缩图像为输入,以解压后的图像为输出的函数。由于McImage.dat中的字节可以使用mcfna.exe内实现的某种通用算法进行压缩/加密,因此我们根本不相信存在这样的函数。因此,我们将采取其他途径,而不是直接反汇编这些程序库。实际上,这里肯定会用到在屏幕上显示图像的函数或WinAPI。我们需要做的事情就是找出这些函数,为其设置断点,并跟踪它们的调用方。
借助于WinAPI,我们可以处理不同格式的图像,但最简单的格式便是BMP了,为了显示这种格式的图像,我们只要调用USER.exe/GDI.exe
(user32.dll
和gdi32.dll
的16位等价物)即可。由于RES目录中存在BMP、RLE(压缩型BMP)、JPG、GIF格式的图片,所以,我们不妨假设零件图是一些位图。
让我们打开WinAPI引用,这里需要密切关注BMP的创建和加载例程:CreateBitmap、CreateBitmapIndirect、CreateCompatibleBitmap、CreateDIBitmap、CreateDIBSection和LoadBitmap。接下来,我们就要开始调试了。
首先,需要说明的一点是,这里有几个NE文件使用了一种称为自加载的功能,利用该功能,可以在将代码流传给OEP之前执行指令,就如PE TLS所做的那样。在我们的例子中,它用于解压缩由Shrinker打包的原始代码。
我尝试了多种16位和32位调试器,结果表明,最适合NE调试的是WinDbg。其中,16位的Open Watcom和Insight Debugger因为自加载功能的缘故而无法启动MCFNA.exe。此外,OllyDbg 1/2虽然能够通过NTVDM间接调试NE,但在16位代码断点上会抛出异常。x64dbg不支持NE。不过,WinDbg则一如既往的好用:能够区分NE模块和NTVDM PE库的加载;在硬件和软件断点上停止运行;识别和反汇编16位代码,帮助我们显示和了解segment:offset
形式的地址。不过,其反汇编窗口的显示存在问题,但由于命令窗口能够正常使用,因此,这也不是什么无可救药的问题。
现在让我们看看16位代码是如何存储到NTVDM内存中的。根据许多研究人员(参见参考资料)和我自己的发现,所有模块都被加载至0x10000到0xA0000的地址范围内,这类似于实模式的内存布局。我们需要对字节进行相应的搜索,以便找到所需的16位函数。特别是,我们需要获取位图创建例程的前面几个字节的内容,为此,可以在0x10000-0xA0000范围内找到它们。
通过前几个字节搜索例程的示例
让我们在windbg下启动这个程序,并搜索上面提到的所有WinAPI函数,我们发现,这里并没有找到USER.exe的LoadBitmap,所以,剩下的模块是GDI.exe。然后,需要我们在每个例程上设置断点。
我们继续执行,从WindBG切换到MicroCat窗口后,会马上在CreateCompatibleBitmap处发生中断。由于每次都会发生这种情况,所以,说明该接口已经被绘制,因此,我们需要禁用该断点,并再次运行。然后,我们选择了一辆车,并浏览零件树,在零件图出现时,在CreateDIBitmap上中断了两次。这是唯一的会引发中断的函数。
CreateDIBitmap上的中断
下面,我们来弄清楚这两种情况下的相应调用方。为此,我们可以从堆栈中取出两个字,其中,[ss:sp+2]
是一个段地址,[ss:sp+0]
是一个偏移量;然后,将它们组合成一个地址,并根据这个地址进行反汇编。
第一个断点上的堆栈和调用方
第二个断点上的堆栈和调用方
在这两种情况下,代码都位于不同的段中,因此,它们是硬盘上的两个库。之后,在文件中搜索“8b F8 83 3e 08 1f 00 74 2c 83 7e fa 00 74 15 ff”和“8b F8 83 7e F4 00 74 0d ff 76 Fe ff 76 F4 6a 00”字节序列。我们发现,第一个序列出现在Visual Basic Runtime Library VBRUN300.dll中,而第二个序列则出现在FNAUTIL2.dll中。也就是说,我们已经找到了与图像处理相关的导出函数所在的库!
分析图像的显示代码
在这里,我们将跳过逆向过程,直接给出带有注释的反汇编代码。
然后,在MCImage.dat中查找指定的偏移量处的内容,并调用我们在上一节中搜索的READ_AND_UNPACK_IMAGE程序。对我们来说,它仍是一个黑盒子。
当一个图像被解压缩时,它的大小被调整为screen_height和screen_width中指定的值,并调用get_palette_handle,它会使用CreatePalette WinAPI创建调色板的,然后,调用我们利用Windbg找到的create_bitmap,使用createdibitmap根据解压出来的字节创建位图。
最后,释放用来存储已解压缩字节且不再使用的内存空间,并且,导出函数将返回HBITMAP。
因此,我们找到了零件图的解压函数及其接口。接下来,我们要做的最后一件事情就是编写一个工具来调用相关函数,对所需的图像进行解压。
重用图像的解压代码
在这里,我们必须编写16位程序,因为FNAUTIL2.dll也是16位的。因此,我选择了Open Watcom C编译器。下面是从FNAUTIL2调用GETCOMPRESSEDIMAGE的代码。
typedef struct {
long unk_1;
long unk_2;
int unk_3;
int mcimage;
} ImageFileData;
HBITMAP decrypt_image(char* mcimage_path, unsigned long
enc_image_offset, unsigned long
enc_image_size) {
int mcimage = open(mcimage_path, O_RDONLY | O_BINARY);
if (mcimage == -1) {
printf(“ERROR: cannot open mcimage ‘%s’\n”, mcimage_path);
return NULL;
}
ImageFileData data = 其中,decrypt_image函数以MCIMAGE.DAT文件的路径、图像偏移量和图像大小作为其输入。该文件打开后,反汇编程序中名为unk_structure_ptr的ImageFileData结构和其他参数将被初始化,然后,传递给该导出函数。接着,decrypt_image函数将返回位图句柄。然后,调用decrypt_image函数的代码将使用save_bitmap函数将位图保存到硬盘上。
int save_bitmap(HBITMAP bitmap, char* dec_image_path) {
int ret_val = 0;
unsigned bytes_written = 0;
HDC dc = GetDC(NULL);
// 1 << 8 (biBitCount) + 0x28
unsigned lpbi_size = 256 * 4 + sizeof(BITMAPINFOHEADER);
BITMAPINFO* lpbi = (BITMAPINFO*)calloc(1, lpbi_size);
if (!lpbi) {
printf(“ERROR: memory allocation for BITMAPINFO failed\n”);
return 0;
}
// BITMAPINFOHEADER:
// 0x00: biSize
// 0x04: biWidth
// 0x08: biHeight
// 0x0C: biPlanes
// 0x0E: biBitCount
// 0x10: biCompression
// 0x14: biSizeImage
// 0x18: biXPelsPerMeter
// 0x1C: biYPelsPerMeter
// 0x20: biClrUsed
// 0x24: biClrImportant
lpbi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
lpbi->bmiHeader.biPlanes = 1;
ret_val = GetDIBits(dc, bitmap, 0, 0, NULL, lpbi,
DIB_RGB_COLORS);
if (!ret_val) {
printf(“ERROR: first GetDIBits failed\n”);
free(lpbi);
return 0;
}
// Allocate memory for image
void __huge* bits = halloc(lpbi->bmiHeader.biSizeImage, 1);
if (!bits) {
printf(“ERROR: huge allocation for bits failed\n”);
free(lpbi);
return 0;
}
lpbi->bmiHeader.biBitCount = 8;
lpbi->bmiHeader.biCompression = 0;
ret_val = GetDIBits(dc, bitmap, 0,
(WORD)lpbi->bmiHeader.biHeight, bits, lpbi, DIB_RGB_COLORS);
if (!ret_val) {
printf(“ERROR: second GetDIBits failed\n”);
hfree(bits);
free(lpbi);
return 0;
}
// Open file for writing
int dec_image;
if (_dos_creat(dec_image_path, _A_NORMAL, &dec_image) != 0) {
printf(“ERROR: cannot create decrypted image file ‘%s’\n”,
dec_image_path);
hfree(bits);
free(lpbi);
return 0;
}
// Write file header
BITMAPFILEHEADER file_header = {0};
file_header.bfType = 0x4D42; // “BM”
file_header.bfSize = sizeof(BITMAPFILEHEADER) + lpbi_size +
lpbi->bmiHeader.biSizeImage;
file_header.bfOffBits = sizeof(BITMAPFILEHEADER) + lpbi_size;
_dos_write(dec_image, &file_header, sizeof(BITMAPFILEHEADER),
&bytes_written);
// Write info header + RGBQUAD array
_dos_write(dec_image, lpbi, lpbi_size, &bytes_written);
// Write image
DWORD i = 0;
while (i < lpbi->bmiHeader.biSizeImage) {
WORD block_size = 0x8000;
if (lpbi->bmiHeader.biSizeImage — i < 0x8000) {
// Explicit casting because the difference
// will always be < 0x8000
block_size = (WORD)(lpbi->bmiHeader.biSizeImage — i);
}
_dos_write(dec_image, (BYTE __huge*)bits + i, block_size,
&bytes_written);
i += block_size;
}
_dos_close(dec_image);
hfree(bits);
free(lpbi);
return 1;
}
函数的输入参数是HBITMAP和保存该位图的文件路径。首先,为BITMAPINFO分配内存,存放BITMAPINFOHEADER和RGBQUAD,用于指定图像分辨率和颜色。然后,再分配一段内存,用来存放要转换为HBITMAP的位图字节。这个分配任务是使用halloc来完成的,它会返回一个带有__high
属性的指针,该属性表示内存可以大于64KB。在调用GetDiBits后,会根据句柄将位图复制到分配的内存中。最后,将BitmapInfoHeader、BitmapInfo和位图写入相应的文件中。不过,因为_dos_write不能一次保存大于64KB的文件,所以,我必须将完成文件写操作的代码放入循环中。
这样,我们得到了一个解决零件图解压问题的实用程序。
最终的依赖轴
小结
至此,数据库逆向工程系列文章就结束了。起初,我计划写更多的文章,但很明显,基本的、关键的信息可以分为三个部分。不用把望远镜对准用双筒望远镜看到的东西,对那些要看的人来说,反倒束缚了他们的视野。
接下来,DBRE领域未来的工作可以围绕以下主题展开。
通过创建分析软件实现文件格式逆向分析的自动化,该软件可以采用启发式算法重构表、记录和字段。此外,它应该是交互式的,允许用户修正和补充该程序猜测的数据结构。同时,它还应该是一个正反馈系统,并能够根据用户定义的数据结构,来尝试重建其他结构。我们可以将其视为“用于数据逆向工程界的IDA Pro”。
我们还可以在前面所说的软件的基础之上继续创建其他软件,从而实现交叉引用研究过程的自动化。它可以实现启发式算法,用来确定哪些字节、单词和dword是指向数据库文件的偏移量。这些任务可以通过使用数据库文件格式的相关知识来完成,同时,其本身还可以继续扩展这种知识。
开发其他DBRE方法。上一篇文章中描述的那些逆向方法,只是其中的一部分,我相信还有更多的方法,都可以用来研究数据库的逆向分析。
即使您只进行文件格式的逆向分析,而不进行数据库的逆向分析,也需要为公共资源提供逆向工程文件格式。例如,当前已经有一个由Katai Struct开发人员维护的格式库(具体见参考资料部分)。
此外,这个系列的结束对我来说也具有非常重要的象征意义。年底是总结的节点,另一方面,也为来年开一个好头。在我看来,我有责任在转向不同的逆向工程方向之前,与其他研究人员分享已有的知识。同时,也可以让大家来给我打打分。
参考资料