首页 > 代码库 > 逆向工程第003篇:令计算器程序显示汉字(上)
逆向工程第003篇:令计算器程序显示汉字(上)
一、前言
计算器(Calc.exe)程序在Windows系统中已经存在了很长的时间,也是我们十分常用的软件。但是一般来说,它所显示的都是阿拉伯数字,而且也没有字符显示的切换。这次我会以两篇文章来进行讨论如何让计算器程序显示汉字的数字。本篇来讨论修改的基本原理,下一篇则来讨论如何编程实现。
二、修改原理剖析
在我以前的很多文章中,始终在强调,Windows编程在很大程度上其实就是各种API函数的堆砌,谁掌握了更多的API函数,那么他往往就能够编写出功能齐全的强大软件。当然,编写出优秀的软件还由其它的因素决定,但是对于API的掌握程度依旧是重点。回到本次论题中来,尽管计算器程序是由软件业的巨头——微软所发布的,但是本质上它并没有运用多高深的技术,依旧如我所说,是由许多API函数堆积起来而实现各种功能的。而我们的目的是要修改程序的显示,那么就有必要找到计算器程序中的相关函数,明确相关函数的功能,再进行研究。当一切明了之后,就可以通过编程来进行相应的修改了。懂得了大概的思路,那么接下来就需要分析计算器中的API函数。
三、查找目标API函数
可执行文件使用来自于其他DLL的代码或数据时,称为导入。当PE文件装入时,Windows加载器的工作之一就是定位所有被导入的函数和数据,并且让正在被装入的文件可以使用那些地址。这个过程是通过PE文件的导入表来完成的,导入表中保存的是函数名和其驻留的DLL名等动态链接所需的信息。因此,我们需要解析计算器的导入表,看看它究竟包含了哪些API函数。
当然这里可以使用专业的查看工具,比如PEiD:
图1 使用PEiD查看导入表
使用专业软件确实方便,但是我觉得,对于一个工程师来说,我们知其然更要知其所以然,因此需要知道程序的实现原理。这里给出一个Win32控制台应用程序。它主要的功能就是找到exe程序中的导入表,并枚举出来,代码如下:
#include <windows.h> #include <DbgHelp.h> #include <stdio.h> #pragma comment(lib,"DbgHelp.lib") #define FILENAME "calc.exe" //欲枚举导入表的文件名 int main() { int i, j; HANDLE hFile = NULL; HANDLE hMap = NULL; LPVOID lpBase = NULL; //打开PE文件 hFile = CreateFile(FILENAME, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if(hFile == INVALID_HANDLE_VALUE) { printf("文件打开失败!\n"); return 0; } //创建一个想共享的文件数据句柄 hMap = CreateFileMapping(hFile,NULL,PAGE_READWRITE,0,0,0); if (hMap == NULL || hMap == INVALID_HANDLE_VALUE) { printf("创建文件映像失败!"); CloseHandle(hFile); return 0; } //获取共享的内存地址 lpBase = MapViewOfFile(hMap,FILE_MAP_READ|FILE_MAP_WRITE,0,0,0); if (lpBase == NULL) { printf("获取共享的内存地址失败!"); CloseHandle(hMap); CloseHandle(hFile); return 0; } PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBase; PIMAGE_NT_HEADERS pNtHeader = (PIMAGE_NT_HEADERS)((BYTE *)lpBase + pDosHeader->e_lfanew); DWORD Rva_import_table = pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress; if(Rva_import_table == 0) { printf("无导入表!"); UnmapViewOfFile(lpBase); CloseHandle(hMap); CloseHandle(hFile); return 0; } PIMAGE_IMPORT_DESCRIPTOR pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)ImageRvaToVa( pNtHeader, lpBase, Rva_import_table, NULL ); //减去内存映射的首地址,就是文件地址了 printf("FileAddress Of ImportTable: %p\n", ((DWORD)pImportTable - (DWORD)lpBase)); //现在来到了导入表的面前:IMAGE_IMPORT_DESCRIPTOR 数组(以0元素为终止) //定义表示数组结尾的null元素 IMAGE_IMPORT_DESCRIPTOR null_iid; IMAGE_THUNK_DATA null_thunk; memset(&null_iid, 0, sizeof(null_iid)); memset(&null_thunk, 0, sizeof(null_thunk)); //每个元素代表了一个引入的DLL。 for(i=0; memcmp(pImportTable + i, &null_iid, sizeof(null_iid))!=0; i++) { LPCSTR szDllName = (LPCSTR)ImageRvaToVa( pNtHeader, lpBase, pImportTable[i].Name, //DLL名称的RVA NULL); //获取DLL名称 printf("-----------------------------------------\n"); printf("[%d]: %s\n", i, szDllName); printf("-----------------------------------------\n"); //我们来到该DLL的 IMAGE_TRUNK_DATA 数组(IAT:导入地址表)前面 PIMAGE_THUNK_DATA32 pThunk = (PIMAGE_THUNK_DATA32)ImageRvaToVa( pNtHeader, lpBase, pImportTable[i].OriginalFirstThunk, NULL); for(j=0; memcmp(pThunk+j, &null_thunk, sizeof(null_thunk))!=0; j++) { //这里通过RVA的最高位判断函数的导入方式, //如果最高位为1,按序号导入,否则按名称导入 if(pThunk[j].u1.AddressOfData & IMAGE_ORDINAL_FLAG32) { printf("\t [%d] \t %ld \t 按序号导入\n", j, pThunk[j].u1.AddressOfData & 0xffff); } else { //按名称导入,我们再次定向到函数序号和名称 //注意其地址不能直接用,因为仍然是RVA PIMAGE_IMPORT_BY_NAME pFuncName = (PIMAGE_IMPORT_BY_NAME)ImageRvaToVa( pNtHeader, lpBase, pThunk[j].u1.AddressOfData, NULL); printf("\t [%d] \t %ld \t %s\n", j, pFuncName->Hint, pFuncName->Name); } } } UnmapViewOfFile(lpBase); CloseHandle(hMap); CloseHandle(hFile); getchar(); return 0; }代码不再进行详细讲解,可自行参考PE类相关书籍来了解导入表的位置与格式。运行结果如下:
由结果可以看到,我们所编写的程序和PEiD的显示结果是一致的,说明我们的程序是正确有效的。查看所有列出的API函数可以发现,SetWindowTextW()和SetDlgItemTextW()都可以在文本框中显示文本。由于SetDlgItemTextW()在其内部又调用了SetWindowTextW(),所以这里先假设真正实现功能的是SetWindowTextW()函数,下一步就是通过测试来进行确定。
四、锁定真正的API函数
在MSDN中,SetWindowTextW()函数的定义如下:BOOL SetWindowText( HWND hWnd, // handle to window or control LPCTSTR lpString // title or text );这个函数的第一个参数是窗口句柄,第二个参数为一个字符串指针,也就是要显示的内容,可见第二个参数才是重点。那么现在就打开OD来查找这个函数。(注意:我这里所使用的是OllyDbg v2.01,如果是v1.10版,则可能会无法取得相应的结果)在OD中载入计算器程序,然后单击右键,在“Search for”中选择“All intermodular calls”,输入SetWindowTextW,结果如下:
很快就锁定了函数的调用地址,在该位置下一个断点,便于调试。按F9运行至该断点处,如下所示:
图4 查看函数参数
从图中右下角的栈窗口以及左下角的数据窗口中,可以确定此时SetWindowTextW()的第二个参数为“0”,码值为0x30。这就是计算器刚启动时,计算器输出框中显示的数值。我们试一下把这个“0”改为“零”。查询汉字“零”的码值为“96F6”,那么就可以直接对数据窗口进行编辑:
图5 修改数字码值
注意这里是小端显示,所以应当反向输入。修改完成后按F9运行:
图6 运行计算器
此时计算器中已经显示出了中文汉字,可以认为SetWindowTextW()就是我们所要寻找的函数。当然,这里为了保险起见,可以再多进行尝试。比如此时按下计算器中的“1”,那么OD还会在SetWindowTextW()函数上断下,此时参数二的字符串指针地址会改变,但是原理还是一样的,将其修改为汉字“壹”的码值,进行测试。当然有兴趣的读者也可以尝试利用这种方法测试SetDlgItemTextW()函数。这里不再赘述。
五、小结
本篇文章讲解了程序修改的基本思想,利用这种思想,我们就可以为程序自定义很多的功能。毕竟逆向工程就是要通过对目标对象的分析,从而了解其实现机理,以此为基础来改善不足,添加新意。这也为我以后的文章打下了理论基础。而具体的代码实现,我会在下一篇文章中详述。逆向工程第003篇:令计算器程序显示汉字(上)