首页 > 代码库 > 逆向工程第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类相关书籍来了解导入表的位置与格式。运行结果如下:


图2 自行编程查找API函数

        由结果可以看到,我们所编写的程序和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,结果如下:


图3 使用OD查找相应函数

        很快就锁定了函数的调用地址,在该位置下一个断点,便于调试。按F9运行至该断点处,如下所示:


图4 查看函数参数

        从图中右下角的栈窗口以及左下角的数据窗口中,可以确定此时SetWindowTextW()的第二个参数为“0”,码值为0x30。这就是计算器刚启动时,计算器输出框中显示的数值。我们试一下把这个“0”改为“零”。查询汉字“零”的码值为“96F6”,那么就可以直接对数据窗口进行编辑:


图5 修改数字码值

        注意这里是小端显示,所以应当反向输入。修改完成后按F9运行:


图6 运行计算器

        此时计算器中已经显示出了中文汉字,可以认为SetWindowTextW()就是我们所要寻找的函数。当然,这里为了保险起见,可以再多进行尝试。比如此时按下计算器中的“1”,那么OD还会在SetWindowTextW()函数上断下,此时参数二的字符串指针地址会改变,但是原理还是一样的,将其修改为汉字“壹”的码值,进行测试。当然有兴趣的读者也可以尝试利用这种方法测试SetDlgItemTextW()函数。这里不再赘述。

 

五、小结

        本篇文章讲解了程序修改的基本思想,利用这种思想,我们就可以为程序自定义很多的功能。毕竟逆向工程就是要通过对目标对象的分析,从而了解其实现机理,以此为基础来改善不足,添加新意。这也为我以后的文章打下了理论基础。而具体的代码实现,我会在下一篇文章中详述。

逆向工程第003篇:令计算器程序显示汉字(上)