微信公众号:计算机与网络安全
ID:Computer-network
有时破解一个程序后可能会将其发布,而往往被破解的程序只是修改了其中一个程序而已,无须将整个软件都进行打包再次发布,只需要发布一个补丁程序即可。发布补丁常见的有三种情况,第一种情况是直接把修改后的文件发布出去,第二种情况是发布一个文件补丁,它去修改原始的待破解的程序,最后一种情况是发布一个内存补丁,它不修改原始的文件,而是修改内存中的指定部分。
3种情况各有好处。第一种情况将已经修改后的程序发布出去,使用者只需要简单进行替换就可以了。但是有个问题,如果程序的版本较多,直接替换可能就会导致替换后的程序无法使用。第二种方法是发布文件补丁,该方法需要编写一个简单的程序去修改待破解的程序,在破解以前可以先对文件的版本进行判断,如果补丁和待破解程序的版本相同则进行破解,否则不进行破解。但是有时候修改了文件以后,程序可能无法运行,因为有的程序会对自身进行校验和比较,当校验和发生变化后,程序则无法运行。最后一种方式是内存补丁,也需要自己动手写程序,并且写好的补丁程序需要和待破解的程序放在同一个目录下,执行待破解的程序时,需要执行内存补丁程序,内存补丁程序会运行待破解的程序,然后比较补丁与程序的版本,最后进行破解。同样,如果有内存校验的话,也会导致程序无法运行。不过,无论是文件校验还是内存校验,都可以继续对被校验的部分进行打补丁来突破程序校验的部分。本文编写一个文件补丁程序和内存补丁程序。
1. 文件补丁
用OD修改CrackMe是比较容易的,如果脱离OD该如何修改呢?其实在OD中修改反汇编的指令以后,对应地,在文件中修改的是机器码。只要在文件中能定位到指令对应的机器码的位置,那么直接修改机器码就可以了。JNZ对应的机器码指令为0x75,JZ对应的机器码指令为0x74。也就是说,只要在文件中找到这个要修改的位置,用十六进制编辑器把0x75修改为0x74即可。如何能把这个内存中的地址定位到文件地址呢?这就是PE文件结构中把VA转换为FileOffset的知识了。
具体的手动步骤,请大家自己尝试,这里直接通过写代码进行修改。为了简单起见,这里使用控制台来编写,而且直接对文件进行操作,省略中间的步骤。有了思路以后,就不是难事了。
关于文件补丁的代码如下:
- #include
- #include
- int main(int argc, char* argv[])
- {
- // VA = 00401EA8
- // FileOffset = 00001EA8
- DWORD dwFileOffset = 0x00001EA8;
- BYTE bCode = 0;
- DWORD dwReadNum = 0;
- // 判断参数
- if ( argc != 2 )
- {
- printf("Please input two argument \r\n");
- return -1;
- }
- // 打开文件
- HANDLE hFile = CreateFile(argv[1],
- GENERIC_READ | GENERIC_WRITE,FILE_SHARE_READ,
- NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL, NULL);
- if ( hFile == INVALID_HANDLE_VALUE )
- {
- return -1;
- }
- SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
- ReadFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
- // 比较当前位置是否为 JNZ
- if ( bCode != '\x75' )
- {
- printf("%02X \r\n", bCode);
- CloseHandle(hFile);
- return -1;
- }
- // 修改为 JZ
- bCode = '\x74';
- SetFilePointer(hFile, dwFileOffset, 0, FILE_BEGIN);
- WriteFile(hFile, (LPVOID)&bCode, sizeof(BYTE), &dwReadNum, NULL);
- printf("Write JZ is Successfully ! \r\n");
- CloseHandle(hFile);
- // 运行
- WinExec(argv[1], SW_SHOW);
- getchar();
- return 0;
- }
代码给出了详细的注释,只需要把CrackMe文件拖放到文件补丁上或者在命令行下输入命令即可,如图1所示。
图1 对CrackMe进行文件补丁
通常,在做文件补丁以前一定要对打算进行修改的位置进行比较,以免产生错误的修改。程序使用的方法是将要修改的部分读出来,看是否与用OD调试时的值相同,如果相同则打补丁。由于这里只是介绍编程知识,针对的是一个CrackMe。如果对某个软件进行了破解,自己做了一个文件补丁发布出去给别人使用,不进行相应的判断就直接进行修改,很有可能导致软件不能使用,因为对外发布以后不能确认别人所使用的软件的版本等因素。因此,在进行文件补丁时最好判断一下,或者是用CopyFile()对文件进行备份。
2. 内存补丁
相对文件补丁来说,还有一种补丁是内存补丁。这种补丁是把程序加载到内存中以后对其进行修改,也就是说,本身是不对文件进行修改的。要将CrackMe载入内存中,载入内存可以调用CreateProcess()函数来完成,这个函数参数众多,功能强大。使用CreateProcess()创建一个子进程,并且在创建的过程中将该子进程暂停,那么就可以安全地使用WriteProcessMemory()函数来对CrackMe进行修改了。整个过程也比较简单,下面直接来阅读源代码:
- #include
- #include
- int main(int argc, char* argv[])
- {
- // VA = 004024D8
- DWORD dwVAddress = 0x00401EA8;
- BYTE bCode = 0;
- DWORD dwReadNum = 0;
- // 判断参数数量
- if ( argc != 2 )
- {
- printf("Please input two argument \r\n");
- return -1;
- }
- STARTUPINFO si = { 0 };
- si.cb = sizeof(STARTUPINFO);
- si.wShowWindow = SW_SHOW;
- si.dwFlags = STARTF_USESHOWWINDOW;
- PROCESS_INFORMATION pi = { 0 };
- BOOL bRet = CreateProcess(argv[1],
- NULL,NULL,NULL,FALSE,
- CREATE_SUSPENDED, // 将子进程暂停
- NULL,NULL,&si,&pi);
- if ( bRet == FALSE )
- {
- printf("CreateProcess Error ! \r\n");
- return -1;
- }
- ReadProcessMemory(pi.hProcess,
- (LPVOID)dwVAddress,(LPVOID)&bCode,
- sizeof(BYTE),&dwReadNum);
- // 判断是否为 JNZ
- if ( bCode != '\x75' )
- {
- printf("%02X \r\n", bCode);
- CloseHandle(pi.hThread);
- CloseHandle(pi.hProcess);
- return -1;
- }
- // 将 JNZ 修改为 JZ
- bCode = '\x74';
- WriteProcessMemory(pi.hProcess,
- (LPVOID)dwVAddress,(LPVOID)&bCode,
- sizeof(BYTE),&dwReadNum);
- ResumeThread(pi.hThread);
- CloseHandle(pi.hThread);
- CloseHandle(pi.hProcess);
- printf("Write JZ is Successfully ! \r\n");
- getchar();
- return 0;
- }
代码中的注释也比较详细,代码的关键是要进行比较,否则会造成程序的运行崩溃。在进行内存补丁前需要将线程暂停,这样做的好处是有些情况下可能没有机会进行补丁就已经执行完需要打补丁的地方了。当打完补丁以后,再恢复线程继续运行就可以了。
参考文献:C++ 黑客编程揭秘与防范(第3版)