前几天在家没事情干,就写了一个通用游戏修改器。代码很简单,利用argv参数获取传入文件位置,然后反复调用EditFile。重复映射文件,并且定位目标位置,修改指定数量的数据,然后保存。然后,我为了某些特殊目的,决定赋予程序动态运行和可扩展的功能。并且加入搜索支持。其实后者并不困难,我们只要反复调用函数对比和赋值,并且逐步推进指针就好。但是前者的实现具有一定困难,为此我不得不增加了部分程序开销,来保存一个函数指针,并且可以动态的调用。这样我们就可以实现诸如轩辕剑中的物品修改。其过程大致如下所述。 轩辕剑中的物品是一个数组,但是数组头的RVA是不固定的,仅仅知道大致位置。我们必须先定位某个物品的代码(特征代码),然后修改后面紧跟的一个WORD,实现修改物品数量。程序会自动的建立一个指针,并且逐步推进。我们的实现函数仅仅需要判断当前指针指向的WORD对象是否等于一个特定值,等于的时候进行修改就好。主体框架如下: #define NOSEARCH 0x00000000 #define SEARCHALL 0xFFFFFFFF #define RADDR(x) (LPVOID)((DWORD)RVABase+(x)) typedef DWORD (*tpEditData)(LPVOID Data); struct _RVATable{ DWORD RVA; DWORD SearchEnd; int size; tpEditData pfnEditData; LPVOID OldData; LPVOID DataBuff; }; _RVATable RvaTab[]={ {0xBA4C, NOSEARCH, 01, NULL, NULL, “x09”}, {0xBA4E, NOSEARCH, 01, NULL, NULL, “x5B”}, {0x76AB, NOSEARCH, 01, NULL, NULL, “x09”}, {0x0000, NOSEARCH, 0, NULL, NULL, NULL} }; void EditFile(LPTSTR lpPath); int CompareMemory(LPVOID mem1, LPVOID mem2, int size); void main(int argc, TCHAR* argv[]){ for(int i=1;i<argc;i++) EditFile(argv[i]); return ; } void EditFile(LPTSTR lpPath){ int i; HANDLE hMap, hFile; LPVOID RVABase=NULL, DataByRva, DataEndRva, DataNow; DWORD FileSize; __try{ hFile=CreateFile(lpPath, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if( hFile==INVALID_HANDLE_VALUE ) return ; FileSize=GetFileSize(hFile, NULL); hMap=CreateFileMapping(hFile, NULL, PAGE_READWRITE | SEC_COMMIT, 0, 0, NULL); if( !hMap ) return ; RVABase=MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0); if( !RVABase ) return ; for(i=0;RvaTab[i].size;i++){ //init start pointer & end pointer DataByRva=RADDR(RvaTab[i].RVA); if( RvaTab[i].SearchEnd==NOSEARCH ) DataEndRva=(LPVOID)0xFF; else if( RvaTab[i].SearchEnd>=FileSize ) DataEndRva=RADDR(FileSize); else DataEndRva=RADDR(RvaTab[i].SearchEnd); //init now pointer & start search //search only run once when no search mode //DataNow(memory pointer)<0xFF(NOSEARCH)-<16(RvaTab[i].size) DataNow=DataByRva; do{ if( !RvaTab[i].pfnEditData ){ //non-function mode if( !RvaTab[i].OldData ) CopyMemory(DataNow, (LPVOID)RvaTab[i].DataBuff, RvaTab[i].size); else if( !CompareMemory(DataNow, RvaTab[i].OldData, RvaTab[i].size) ) CopyMemory(DataNow, (LPVOID)RvaTab[i].DataBuff, RvaTab[i].size); }else //function edit, no write action needed. (*RvaTab[i].pfnEditData)(DataNow); *((PDWORD)&DataNow)+=1; }while( (DWORD)DataNow< (DWORD)DataEndRva-RvaTab[i].size ); } }__except(( GetExceptionCode()==EXCEPTION_ACCESS_VIOLATION ) ?EXCEPTION_EXECUTE_HANDLER:EXCEPTION_CONTINUE_SEARCH){ if( RVABase ) UnmapViewOfFile(RVABase); CloseHandle(hMap); CloseHandle(hFile); ExitProcess(-1); } if( RVABase ) UnmapViewOfFile(RVABase); CloseHandle(hMap); CloseHandle(hFile); return ; } int CompareMemory(LPVOID mem1, LPVOID mem2, int size){ for(int i=0;i<size;i++) if( (*(PBYTE)mem1)!=(*(PBYTE)mem2) ) return ((*(PBYTE)mem1)>(*(PBYTE)mem2))?1:-1; return 0; } 为了系统在写入时候的安全性考虑,我加入了一个SHE error处理框架。此处仅仅是关闭了所有内核对象,如果真正使用中可以提示错误等等。 因为整个程序非常简单,所以我并没有花多少时间去写。真正困难的部分在后面,我决定优化程序达到增加速度和减少体积的目的。程序的当前大小约135K,经过Release编译后是24K,我估计终极目标大约是1.5K。 此处我们优化的时候涉及两个关键问题,section和CRT。并且涉及了三个相关问题,调用参数,指定节,SEH。(BTW。哈拉点无关话题,我的word每次都自动修正SEH的名字到SHE上,结果每次我都要把这个名字打两次。) 首先我是基于节考虑的,将节的长度缩短,增加利用效率。因此添加了一行代码 #pragma comment(linker, “/ALIGN:0x1000”) 编译出来的程序大小是13K,删除的部分全部都是0。这里和下面涉及linker参数的全部可以参考MSDN上的Linker Options说明。 然后我考虑的是去除CRT(C++动态运行库),这则牵涉了很多部分。 首先是所有的*printf和str*都不能调用(大家看到我的代码中,没有这两组函数吧),其实所有的C++标准函数都不能用,只能用WIndowsAPI。写屏就是GetStdHandle()…… 然后是main问题。如果我们仅仅不调用API,省略动态运行库后编译会产生错误,找不到_mainCRTStartup。因为我们的编译器默认启动函数是_mainCRTStartup,这段代码在CRT0.C中,负责执行CRT的初始化。并且解析命令行,做准备工作后会调用我们的main函数。目前我们舍弃了CRT,自然不能指望_mainCRTStartup了。解决方法有两个,一个是重命名主函数为_mainCRTStartup,另外就是指定linker的ENTRY项,使得linker将我们的main函数作为入口点。我采用了后者。关于这部分,可以参看Microsoft Visual StudioVC98CRTSRCCRT0.C。 #pragma comment(linker, “/nodefaultlib”) #pragma comment(linker, “/ENTRY:wmain”) void wmain(){ int CmdArgc; LPWSTR *argv; argv=CommandLineToArgvW(GetCommandLine(), &CmdArgc); for(int i=1;i<CmdArgc;i++) EditFile(argv[i]); return ; } 注意我void main(int argc, TCHAR* argv[])函数已经换成了void wmain()。这是因为我们函数的入口点使用的是CreateProcess调用启动的。这个调用传递我们的启动入口给Kernel32中的BaseProcessStart,由这个函数反向调用(CallBack)我们的主函数,所以没有传入的参数(事实上可能有两个,不过别问我CreateProcess传啥给我们 ,甚至他们可能是BaseProcessStart自己的临时变量)。如果照void main(int argc, TCHAR* argv[])调用,argc就是一个随机值(根据我的调试结论,这应该是一个地址,指向一个可执行的内存块,不过我找不到到底是做什么用的),进而百分百的发生访问错误。关于BaseProcessStart的过程,请看参考文献[1]。 另外注意我函数使用的是wmain(),其实这是被逼出来的。因为CommandLineToArgvW只有UNICODE版本(??!!……)。当然,我们前面的所有调用都是使用的LPTSTR而非LPCSTR。另外CommandLineToArgvW是<Shellapi.h>的一个声明函数,MSDN中说<windows.h>会自动的引用。不过可能因为我前面的宏设置不对,所以没有引用。管他呢,直接include就好。 至于关闭CRT,在连接里面禁止连接默认库就好了。 然后我们使用命令行获取和解析的方法,如同CRT中的一样。如果在结束的时候调用ExitProcess(0);就更像CRT了。 然后我面临了一个两难的抉择。Msvc的SEH(混蛋Word又来了,真希望把Excel改成Execl……)是进过封装处理的。里面涉及了几个宏,还有一个函数__except_handler3。这个函数是SEH(又来……无力中……)的处理函数的原型,属于真正的API调用。 EXCEPTION_DISPOSITION  __cdecl _except_handler( struct   _EXCEPTION_RECORD *  ExceptionRecord, void *  EstablisherFrame, struct   _CONTEXT* ContextRecord, void *  DispatcherContext ); 偏偏这个函数是个CRT,连带宏一起不能用(仅仅是宏问题还不大)。要么我们就自己编写SEH处理函数,实现SEH理机制。要么我们就必须舍弃SHE。经过分析,我认为代码的强度足够了,所以舍弃了SEH。(可能有部分的原因是因为这个名字每次我都要打上两遍,而且加上三个BS。)我所查到的资料里面是这么论述的: 利用try-catch结构能比较大的简化错误的处理的方式,我个人任务应该是很有用的东西,不过使用try-catch会带来额外的开销,这个开销主要是体现在代码的长度加大,运行的速度都没有什么太大的影响(这个可以从编译器的实现代码上面看出来,但是很多反对异常的人都任务他会降低运行速度.呵呵) 因此,上面的所有代码都被包含到了宏中。 #ifndef _SEH //优化的代码或优化部分 #else//_SEH //原代码 #endif//_SEH 程序的大小已经缩减到了2.5K,下面我将展示一个技术,显示如何操作section(节),以获得特殊的属性。例如不同进程的数据共享,或者是数据压缩加壳等等。当然,要做这些还需要其他的辅助技术。关于section部分,也可以参看参考文献[1]。 首先,我们确定此时代码的节表,并且观测数据。事实证明,将.data(RW) .rdata(R)合并在0x200的空间内是可行的。这两节分别使用了0x200的空间,.data内是我的修改表,.rdata内则是导入表。 _RVATable RvaTab[]={ {0xBA4C, NOSEARCH, 01, NULL, NULL, “x09”}, {0xBA4E, NOSEARCH, 01, NULL, NULL, “x5B”}, {0x76AB, NOSEARCH, 01, NULL, NULL, “x09”}, {0x0000, NOSEARCH, 0, NULL, NULL, NULL} }; 其实512字节的空间并不致命,不过我们此时仅仅是为了讨论优化技术。所以不要抱怨了……来吧。 我们最终的目的是将.data中的数据注入.rdata中(反向应该也可以,不过导入表操作起来恐怕更困难)。此时我们发现了两个问题,如何注入,以及.rdata应该是什么属性。如果属性是只读,那么我在程序中的任何对于修改表的变更都会导致0x3E6访问异常(事实上此时我已经回想并且确定程序中没有变更修改表的行为,但是正如英语所说,you can’t be too careful to programing,再小心也不为过)。因此我们通知linker,我们的.rdata节要变节啦,请用RW属性。 #ifndef _SEH #pragma comment(linker, “/SECTION:.rdata,RW”) #endif//_SEH #ifndef _SEH #pragma data_seg(".rdata") //__declspec(allocate(".rdata")) #endif//_SEH _RVATable RvaTab[]={ {0xBA4C, NOSEARCH, 01, NULL, NULL, “x09”}, {0xBA4E, NOSEARCH, 01, NULL, NULL, “x5B”}, {0x76AB, NOSEARCH, 01, NULL, NULL, “x09”}, {0x0000, NOSEARCH, 0, NULL, NULL, NULL} }; #ifndef _SEH #pragma data_seg() #endif//_SEH 完成后编译,如同我们所想,程序正常运行,大小2K。这个值取决于机器和编译器。我曾经在某个编译器中得到2.5K的代码,这是个莫名其妙的值。我察看文件头后发现了问题。Linker展开了PE头部所有的Data_Directory,这样导致了头部最后的部分,即section table冲出了0x200位置大约1,2个字节。这直接导致了后面的两个段全部被向后移动了0x200的大小,即512字节。关于如何察看PE头,可以试着使用软件ExeScope。 期间我还用了一个小小的技巧来保护多次编译的安全和便捷。如大家所知,我使用了_SEH宏来保证优化代码和非优化代码的区分,因此我在DEBUG配置中的C/C++选项卡中的预编译定义中增加了,_SEH宏,并且新建立了一个配置,叫SHE。复制了Release的配置,仅仅是增加了_SEH宏。从而在编译后得到了三个exe文件。调试版的,135K。SHE版的,13K。Release版的,1.5K。 完成代码如下: //#define _SEH #pragma comment(linker, “/ALIGN:0x1000”) #ifndef _SEH #pragma comment(linker, “/nodefaultlib”) #pragma comment(linker, “/ENTRY:wmain”) #pragma comment(linker, “/SECTION:.rdata,RW”) #endif//_SEH #define NOSEARCH 0x00000000 #define SEARCHALL 0xFFFFFFFF #define RADDR(x) (LPVOID)((DWORD)RVABase+(x)) /************************************* peach data at frist, then calculate target data. write it into target buffer, then return zero. if not peach, return SEARCHALL, which mean -1. *************************************/ typedef DWORD (*tpEditData)(LPVOID Data); /************************************* _RVATable file doc: RVA: Start offset of file. if no searching, it’s the target offset. SearchEnd: End offset of file. if less then RVA, no searching will token place. size: Easy one. zero if this is the last one of RVATable. pfnEditData: Pointer of function that edit data. In searching mode, the function will be run once and once again. OldData, DataBuff: Complex. It should be NULL in pfnEditData mode. In non-pfnEditData mode, a auto-function which replace data will run. It replace data in OldData to data in DataBuff. This could also use in searching mode. If OldData is NULL, whatever will be replaced, even in searching mode. *************************************/ struct _RVATable{ DWORD RVA; DWORD SearchEnd; int size; tpEditData pfnEditData; LPVOID OldData; LPVOID DataBuff; }; #ifndef _SEH #pragma data_seg(".rdata") //__declspec(allocate(".rdata")) #endif//_SEH _RVATable RvaTab[]={ {0xBA4C, NOSEARCH, 01, NULL, NULL, “x09”}, {0xBA4E, NOSEARCH, 01, NULL, NULL, “x5B”}, {0x76AB, NOSEARCH, 01, NULL, NULL, “x09”}, {0x0000, NOSEARCH, 0, NULL, NULL, NULL} }; #ifndef _SEH #pragma data_seg() #endif//_SEH void EditFile(LPTSTR lpPath); int CompareMemory(LPVOID mem1, LPVOID mem2, int size); #ifndef _SEH void wmain(){ int CmdArgc; LPWSTR *argv; argv=CommandLineToArgvW(GetCommandLine(), &CmdArgc); for(int i =1;i<CmdArgc;i++) EditFile(argv[i]); return ; } #else//_SEH void wmain(int argc, TCHAR* argv[]){ for(int i=1;i<argc;i++) EditFile(argv[i]); return ; } #endif//_SEH void EditFile(LPTSTR lpPath){ int i; HANDLE hMap, hFile; LPVOID RVABase=NULL, DataByRva, DataEndRva, DataNow; DWORD FileSize; #ifdef _SEH __try{ #endif//_SEH hFile=CreateFile(lpPath, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL); if( hFile==INVALID_HANDLE_VALUE ) return ; FileSize=GetFileSize(hFile, NULL); hMap=CreateFileMapping(hFile, NULL, PAGE_READWRITE | SEC_COMMIT, 0, 0, NULL); if( !hMap ) return ; RVABase=MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0); if( !RVABase ) return ; for(i=0;RvaTab[i].size;i++){ //init start pointer & end pointer DataByRva=RADDR(RvaTab[i].RVA); if( RvaTab[i].SearchEnd==NOSEARCH ) DataEndRva=(LPVOID)0xFF; else if( RvaTab[i].SearchEnd>=FileSize ) DataEndRva=RADDR(FileSize); else DataEndRva=RADDR(RvaTab[i].SearchEnd); //init now pointer & start search //search only run once when no search mode //DataNow(memory pointer)<0xFF(NOSEARCH)-<16(RvaTab[i].size) DataNow=DataByRva; do{ if( !RvaTab[i].pfnEditData ){ //non-function mode if( !RvaTab[i].OldData ) CopyMemory(DataNow, (LPVOID)RvaTab[i].DataBuff, RvaTab[i].size); else if( !CompareMemory(DataNow, RvaTab[i].OldData, RvaTab[i].size) ) CopyMemory(DataNow, (LPVOID)RvaTab[i].DataBuff, RvaTab[i].size); }else //function edit, no write action needed. (*RvaTab[i].pfnEditData)(DataNow); *((PDWORD)&DataNow)+=1; }while( (DWORD)DataNow< (DWORD)DataEndRva-RvaTab[i].size ); } #ifdef _SEH }__except(( GetExceptionCode()==EXCEPTION_ACCESS_VIOLATION ) ?EXCEPTION_EXECUTE_HANDLER:EXCEPTION_CONTINUE_SEARCH){ if( RVABase ) UnmapViewOfFile(RVABase); CloseHandle(hMap); CloseHandle(hFile); ExitProcess(-1); } #endif//_SEH if( RVABase ) UnmapViewOfFile(RVABase); CloseHandle(hMap); CloseHandle(hFile); return ; } int CompareMemory(LPVOID mem1, LPVOID mem2, int size){ for(int i=0;i<size;i++) if( (*(PBYTE)mem1)!=(*(PBYTE)mem2) ) return ((*(PBYTE)mem1)>(*(PBYTE)mem2))?1:-1; return 0; } 注意其中我并未添加头文件,亦没有配置。请自行添加头文件,更改配置并适当添加宏_UNICODE,UNICODE,_SEH。并且自己写修改表(有说明的),或者添加修改函数。然后才能重新编译。 以上代码在Win2KSp4+Msvc6.0Sp环境下通过,修改目标为AI2存盘文件。 参考文献: [1].Windows核心编程 (美)Jeffrey Richter. 译者:王建华等. 书号:7-111-07945-0. [2].Windows 95 System Programming SECRENTS学习笔记—第三章(7) Kendiv http://blog.csdn.net/Kendiv/archive/2005/01/11/247938.aspx