最近看了《加密与解密》,想起来自己一直玩儿的一个游戏有个BUG,于是想自己DIY游戏程序将它修正。本文内容很简单,欢迎指点、批评。
顺便申请邀请码,想转正啊~~如果可以的话,EMAIL:naylon@126.com~~是与否都先感谢

这款游戏叫GTA:ViceCity(侠盗:罪恶都市),各位可以去网上搜一搜
VC这款游戏有个严重BUG,在多核心处理器的计算机上很容易死机(全屏的话基本上就得重启),而且有时候游戏里鼠标不能动,必须来回按ESC/Resume才能解决(有时候RP不好就直接黑屏了)。解决方法倒是也容易,打开任务管理器,找到gta-vc.exe进程,右键“关系设置”,然后只勾选一个CPU(通常是CPU0),点确定即可。


可是每回进游戏都得再切出来一次调试,实在是很麻烦,于是我就想修改gta-vc.exe,让它启动时自动设置为在CPU0上运行。

1.分析任务管理器的内部实现
首先要分析taskmgr.exe,也就是系统的任务管理器,看看他是怎么实现为进程指定CPU运行的,既然要操作进程,那么它内部肯定需要用OpenProcess函数打开进程,然后用返回的句柄操作,那么我们就用OD调试之,给OpenProcess下断(注意要暂停刷新,否则都会一直断在OpenProcess里面)下好断之后,选中进程点关系设置,这时就断在了OpenProcess里,返回程序领空,来看看关键代码。

代码:
0100BA26  /$  55            PUSH EBP
0100BA27  |.  8BEC          MOV EBP,ESP
0100BA29  |.  51            PUSH ECX
0100BA2A  |.  53            PUSH EBX
0100BA2B  |.  56            PUSH ESI
0100BA2C  |.  57            PUSH EDI
0100BA2D  |.  FF75 08       PUSH DWORD PTR SS:[EBP+8]                ; /ProcessId
0100BA30  |.  33DB          XOR EBX,EBX                              ; |
0100BA32  |.  53            PUSH EBX                                 ; |Inheritable => FALSE
0100BA33  |.  68 00060000   PUSH 600                                 ; |Access = SET_INFORMATION|QUERY_INFORMATION
0100BA38  |.  8BF1          MOV ESI,ECX                              ; |
0100BA3A  |.  FF15 14110001 CALL DWORD PTR DS:[<&kernel32.OpenProces>; \OpenProcess
0100BA40  |.  8BF8          MOV EDI,EAX
0100BA42  |.  85FF          TEST EDI,EDI
0100BA44  |.  74 4E         JE SHORT taskmgr.0100BA94
0100BA46  |.  8D45 FC       LEA EAX,DWORD PTR SS:[EBP-4]
0100BA49  |.  50            PUSH EAX                                 ; /pSystemAffinityMask
0100BA4A  |.  8D45 08       LEA EAX,DWORD PTR SS:[EBP+8]             ; |
0100BA4D  |.  50            PUSH EAX                                 ; |pProcessAffinityMask
0100BA4E  |.  57            PUSH EDI                                 ; |hProcess
0100BA4F  |.  FF15 08110001 CALL DWORD PTR DS:[<&kernel32.GetProcess>; \GetProcessAffinityMask
0100BA55  |.  85C0          TEST EAX,EAX
0100BA57  |.  74 30         JE SHORT taskmgr.0100BA89
0100BA59  |.  8D45 08       LEA EAX,DWORD PTR SS:[EBP+8]
0100BA5C  |.  50            PUSH EAX                                 ; /lParam
0100BA5D  |.  68 5BB60001   PUSH taskmgr.0100B65B                    ; |DlgProc = taskmgr.0100B65B
0100BA62  |.  FF76 04       PUSH DWORD PTR DS:[ESI+4]                ; |hOwner
0100BA65  |.  6A 7C         PUSH 7C                                  ; |pTemplate = 7C
0100BA67  |.  FF35 705D0101 PUSH DWORD PTR DS:[1015D70]              ; |hInst = 01000000
0100BA6D  |.  FF15 FC130001 CALL DWORD PTR DS:[<&user32.DialogBoxPar>; \DialogBoxParamW
0100BA73  |.  83F8 01       CMP EAX,1
0100BA76  |.  75 0E         JNZ SHORT taskmgr.0100BA86
0100BA78  |.  FF75 08       PUSH DWORD PTR SS:[EBP+8]
0100BA7B  |.  57            PUSH EDI
0100BA7C  |.  FF15 0C110001 CALL DWORD PTR DS:[<&kernel32.SetProcess>;  kernel32.SetProcessAffinityMask
0100BA82  |.  85C0          TEST EAX,EAX
0100BA84  |.  74 03         JE SHORT taskmgr.0100BA89
0100BA86  |>  33DB          XOR EBX,EBX
0100BA88  |.  43            INC EBX
0100BA89  |>  57            PUSH EDI                                 ; /hObject
0100BA8A  |.  FF15 5C110001 CALL DWORD PTR DS:[<&kernel32.CloseHandl>; \CloseHandle
0100BA90  |.  85DB          TEST EBX,EBX
0100BA92  |.  75 14         JNZ SHORT taskmgr.0100BAA8
0100BA94  |>  FF15 68110001 CALL DWORD PTR DS:[<&kernel32.GetLastErr>; [GetLastError
0100BA9A  |.  50            PUSH EAX                                 ; /Arg3
0100BA9B  |.  68 68750000   PUSH 7568                                ; |Arg2 = 00007568
0100BAA0  |.  FF76 04       PUSH DWORD PTR DS:[ESI+4]                ; |Arg1
0100BAA3  |.  E8 789AFFFF   CALL taskmgr.01005520                    ; \taskmgr.01005520
0100BAA8  |>  5F            POP EDI
0100BAA9  |.  5E            POP ESI
0100BAAA  |.  8BC3          MOV EAX,EBX
0100BAAC  |.  5B            POP EBX
0100BAAD  |.  C9            LEAVE
0100BAAE  \.  C2 0400       RETN 4
程序逻辑很简单,先调用OpenProcess打开进程,取得句柄,然后GetProcessAffinityMask,如果失败则设置错误信息然后返回,成功的话调用DialogBoxParamW从资源创建对话框,(也就是“关系设置”窗口),并将当前的CPU信息显示上去,知道点下“确定”,DialogBoxParamW才返回。如果用户修改了原先的CPU关系设置,那么就会调用SetProcessAffinityMask设置当前进程;如果没有,则直接关闭进程句柄,函数返回。

由此我们可以知道,用GetProcessAffinityMask可以取得一个进程的CPU设置,用SetProcessAffinityMask则可以对其进行设置,我们要是自动设置gta-vc.exe在CPU0上运行的话,显然是要用到后者,上MSDN查查这个函数,上面把用法什么的写的很详细,详见:http://msdn.microsoft.com/en-us/libr...8VS.85%29.aspx


2.修改gta-vc.exe
经过一系列搜索,我得知只要如此调用:SetProcessAffinityMask(ProcessHandle, 1)即可把对应的进程设置为在CPU0上运行,下面就是要自己修改gta-vc.exe,让它自己执行这个函数。

先用eXeScope看一下gta-vc.exe的导入表,我找到了Direct3DCreate函数,由d3d8.dll导出,显然这个函数是跟DirectX相关的,应该是在初始化阶段被调用。下面用OD断一下,看看能不能利用这个函数。

下断,然后运行,马上就断了下来,然后回到gta-vc.exe领空,可见调用代码如下:
代码:
0065B845   > \8B8424 440400>MOV EAX,DWORD PTR SS:[ESP+444]           ;  案例 0 --> 分支 0065B5DA
0065B84C   .  55            PUSH EBP
0065B84D   .  6A 78         PUSH 78
0065B84F   .  8B08          MOV ECX,DWORD PTR DS:[EAX]
0065B851   .  890D A4977800 MOV DWORD PTR DS:[7897A4],ECX
0065B857   .  E8 007D0000   CALL <JMP.&d3d8.Direct3DCreate8>
0065B85C   .  33FF          XOR EDI,EDI
0065B85E   .  A3 B4977800   MOV DWORD PTR DS:[7897B4],EAX
0065B863   .  3BC7          CMP EAX,EDI
0065B865   .  75 0C         JNZ SHORT gta-vc-b.0065B873
0065B867   .  5D            POP EBP
0065B868   .  5F            POP EDI
0065B869   .  33C0          XOR EAX,EAX
0065B86B   .  5E            POP ESI
0065B86C   .  81C4 30040000 ADD ESP,430
0065B872   .  C3            RETN
……
看到了这句:
代码:
0065B857   .  E8 007D0000   CALL <JMP.&d3d8.Direct3DCreate8>
看来可以修改这里,插入一小段代码调用SetProcessAffinityMask(ProcessHandle, 1),为了不影响程序原本的功能,我们往下看看,找一段空字节插入代码,然后用JMP修改程序流程就行了。往下拉了一段,果然在0065C853的地方找到一堆NOP指令,看起来够长,把调用SetProcessAffinityMask的代码加在这里就行了,然后把原本的CALL <JMP.&d3d8.Direct3DCreate8>修改成JMP跳转到我们自己加的这段代码处,执行完再JMP回去即可。

但是在此之前还有个问题需要处理,那就是得处理SetProcessAffinityMask的地址,gta-vc.exe原版并没有导入SetProcessAffinityMask函数,虽然我们也可以用OD直接改成“call SetProcessAffinityMask”,不过这样的话是用的是硬编码,不支持VISTA/7及以上系统,所以我们还得自己给gta-vc.exe导入这个函数。

用LordPE载入之,找到导入表,然后右键“添加导入表”,DLL是kernel32.dll,函数名即SetProcessAffinityMask,然后点“+”,记下ThunkRVA(我们后面调用要用到),确定即可。


我添加的ThunkRVA值为00614026,关掉LordPE,重新用OD载入,现在可以开始修改了。
按照上面的步骤再操作一遍,找到我们之前找好的那一段NOP(0065C853),在这里加入如下代码:
代码:
0065C853 push 1   ;参数2:1代表CPU0
0065C855 push -1   ;参数1:-1代表当前进程(GetCurrentProcess() == -1)
0065C857 call dword ptr [00A14026]   ;调用SetProcessAffinityMask
大家肯定觉得奇怪,00A14026是哪里来的?实际上这个就是我们加入的导入函数SetProcessAffinityMask的ThunkVA,前面我们知道ThunkRVA是00614026,我们在OD里用Ctrl-G呼出前往窗口,选择RVA,然后输入00614026,确定。之后就跳到了00A14026,OD就已经为我们把RVA转换成VA了,所以这个00A14026就是导入Thunk的VA。
调用完之后我们还得正常调用Direct3DCreate8,但是0065C853这段NOP不够用了,得先short jmp到后面的另一小段NOP:
代码:
0065C85D JMP SHORT 0065C863   ;jmp到后面
……
0065C863 call 0066355C   ;Direct3DCreate8
都搞定了我们还得JMP回去,让程序正常执行 :
代码:
0065C868 jmp 0065B85C   ;跳回原始CALL Direct3DCreate8的下一个指令位置
最后把原始的CALL <JMP.&d3d8.Direct3DCreate8>改成JMP 0065C853,让它执行到这里时跳转到0065C853,执行完我们自己的代码后再JMP回来。


都改好了,OD主界面右键“复制到可执行文件-所有修改-全部复制”,然后再右键“保存文件”,保存成gta-vc.exe(最好提前备份原版文件)。

3.终

至此便大功告成,运行游戏,再使用任务管理器看一看“关系设置”,会发现已经自动设置为在CPU0上运行,不用每回再手动改了,我的目的也就达到了。游戏功能没有任何影响。

4.另一种方法
其实还有一种更简单的方法,不过技术含量不高,还要附带一个DLL文件。
我们知道PE加载器在加载一个PE文件时,会处理导入表,找到相关的导入模块,然后加载之,再修正IAT。那么我们可以自己写一个DLL,导出一个函数,然后在目标文件的导入表里导入这个函数,同时把该DLL放到与可执行文件相同的目录里,那么该程序在加载的时候就会载入我们的hijack.dll,相当于注入,之后就都好办了,DllMain里CreateThread执行我们的工作,完毕后线程返回即可。
实现代码如下:
代码:
#include "misc.h"

// 需要一个导出函数,以便加入gta-vc.exe导入表
__declspec (dllexport) VOID ExportFunction(void)
{
  //MessageBox(0, "Function export succeed!", "Goodboy", 0);
  return;
}

BOOLEAN ThreadProc(LPVOID lpdwThreadParam)
{
  Sleep(2000);
  SetProcessAffinityMask(GetCurrentProcess(), 1);
  return TRUE;
}

VOID SetWorkingThread()
{
  SECURITY_ATTRIBUTES sa;
  HANDLE tid;
  HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadProc, 0, 0, (LPDWORD)&tid);
  if (hThread) {
    CloseHandle(hThread);
  }
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
  switch (fdwReason) {
    case DLL_PROCESS_ATTACH:
      SetWorkingThread();
      break;
    case DLL_PROCESS_DETACH:
      break;
  }
  return TRUE;
}
上传的附件 gta-vc.part1.rar
gta-vc.part2.rar
DeathDll.rar