前几天一个网友给我发消息请我帮他个忙。他的问题是,他正在使用的D2JSP是免费试用版,试用版在运行时会在游戏的所有游戏画面中央显示一行很大 的“Trial Version”字样(见下图中的红圈),很烦人,他想去掉这行字。我想正好用此做个教程解释前面介绍过的hack工作原理,于是答应帮他看看。
我已经很久没有开BOT了,最后一次使用D2JSP还是2003年1.09d时期的事。根据我以前的理解,D2JSP和其他hacks一样,由两部 分组成,d2jsp.exe只是一个loader,负责把d2jsp.dll加载到游戏进程。而d2jsp.dll才是D2JSP的运行引擎-真正发挥作 用的东西。因此“Trial Version”字符串肯定在d2jsp.dll中。最开始的想法是“Trial Version”可能会以一个常量字符串的形式保存在d2jsp.dll中,因此最容易想到的做法是用UltraEdit打开d2jsp.dll,寻找 “Trial Version”,找到的话把它改为空字符串就OK了。然而打开后发现d2jsp.dll是加了壳的,这才想起自从D2 1.10以来D2JSP开始收费,采取了一些措施防止别人破解,加壳就是其中之一(当然还是被人破了)。在d2jsp.dll文件的前面几行看到了 aspack字串,看来加的是aspack壳。脱壳我很不在行,但是我知道有一些自动脱壳工具,针对aspack的脱壳工具也肯定有,不妨试试,于是到网 上抓了个脱壳软件-AspackDie。当然试验的结果是失败了。又想,不管怎样,d2jsp.dll运行的时候它自己得脱壳吧,那我可以在它自己脱壳后 在进程内存中找吧。于是加载D2JSP后用WinHex打开游戏进程,查找Trial Version,然而还是没找到。看来可能用了即时脱壳技术,或者干脆Trial Version根本不是常量字符串,比如我知道如果在C/C++中这样写szVersion就不是常量字符串:
char
szVersion[]
=
{
'
T
'
,
'
r
'
,
'
i
'
,
'
a
'
,
'
l
'
,
'
'
,
'
V
'
,
'
e
'
,
'
r
'
,
'
s
'
,
'
i
'
,
'
o
'
,
'
n
'
,
'
'
};
然后怎么办,继续研究脱壳?不,其实我早就有了更好、更通用的解决方法,但是需要编程。现在看来简单的思路行不通,还是写程序算了。换个角度来看, 不管字符串是什么,总得调用某个函数来输出显示吧,看上图中的字样,显然是调用了Diablo II的内部函数显示的,因此如果能截获这个函数过滤掉"Trial Version"字符串就搞定了。而且这种方法不用修改D2JSP程序,对所有D2JSP版本都起作用,因此更通用。问题是它调用了D2X的哪个函数呢? 我想起我的d2hackmap中也用到了类似的字符串显示的功能,它们用的有可能是同一个!于是把d2hackmap的源代码翻出来看了看,发现了 d2win.dll中导出序号为10064(Diablo II中所有dll导出的函数都只有序号没有名字)的函数做此用途,其函数声明为:
void
__fastcall D2WIN_DrawText(wchar_t
*
str,
int
xpos,
int
ypos, DWORD color, DWORD unknown);
然后用VC++ attach到游戏进程进行调试,想在D2WIN_DrawText设断点监控,结果发现调试器刚attach上去D2JSP就会崩溃。看来它做了反调试 处理,这也在意料之中。还是直接写个DLL注入到游戏进程截获D2WIN_DrawText来观测得了,字符串可以用Win32 API OutputDebugString输出然后用DbgView观测:
void
__fastcall MyD2WIN_DrawText(wchar_t
*
str,
int
xpos,
int
ypos, DWORD color, DWORD unknown)
{
if (str) OutputDebugStringW(str);
}
结果发现“Trial Version”果然是通过D2WIN_DrawText输出的,见下图。
现在只要改变MyD2WIN_DrawText()的实现,判断输出字符串如果为"Trial Version"则跳过真正的D2WIN_DrawText()代码就搞定了,代码见后面的详细源代码分析。改变后的显示结果如下图,画面上方的 d2jsp v1.2.0字样还在,Trial Version字样已经没了:
至此,给D2JSP打补丁的程序d2jsppatch.dll已经做好了,然而还有一个问题,就是d2jsppatch.dll如何加载呢?用 D2JSP开的BOT一般是无人职守自动运行的,因此d2jsppatch.dll最好能够随着d2jsp.dll自动加载,否则每次都需要人工加载那就 太麻烦了。那怎么做到随d2jsp.dll自动加载呢?一个办法是把d2jsp.dll改名为d2jsp2.dll,d2jsppatch.dll改名为 d2jsp.dll,这样d2jsp.exe每次加载d2jsp.dll时其实加载的是d2jsppatch.dll。d2jsppatch.dll加载 起来后,在它的启动代码里再手工加载d2jsp2.dll(即原来的d2jsp.dll),这样就达到了自动加载d2jsppatch.dll的目的。这 种方法的缺点是它改变了D2JSP原有模块之间的关系,可能会导致一些兼容性问题,比如d2jsp.exe可能会校验d2jsp.dll检查它是否被人改 过,或者某个依赖于d2jsp.dll工作的程序可能会用LoadLibrary("d2jsp.dll")/GetProcAddress()获取 d2jsp.dll的到处函数,这时就会失败。
伴随d2jsp.dll自动加载的另外一种做法是把d2jsppatch.dll静态绑定到d2jsp.dll或者d2jsp.dll会用到的某个 DLL。由于d2jsp.dll加了壳,DLL文件的IAT(Import Address Table,即导入表)已经被破坏,最好是从d2jsp.dll会用到的DLL下手。我们知道D2JSP支持jscript脚本程序,它肯定会加载 jscript脚本引擎js32.dll,所以我们可以把d2jsppatch.dll静态绑定到js32.dll上去。熟悉Windows编程且喜欢玩 API hooking的朋友可能知道微软的Detours Library,其中附带了一个程序setdll.exe可以把一个DLL静态绑定到其他EXE或者DLL。在命令行运行:
setdll
.
exe -d
:d2jsppatch
.
dll js32
.
dll
就把d2jsppatch.dll绑定到了js32.dll,如下图。
以下是对d2jsppatch.dll源代码的详细分析,给感兴趣的朋友参考。d2jsppatch.dll虽然是针对D2JSP的,但是它包含了 一个hack的所有基本组成部分,而且功能很简单,用它来了解hack的工作原理是很合适的。d2maphack、d2hackmap等hacks的工作 原理和它完全一样,不同的只是实现的功能。
1,d2jsppatch.dll的加载和卸载。DLL入口函数DllMain中一般用来做安装(InstallPatch)、卸载 (RemovePatch)旁路点(detour patch)。安装、卸载工作可能导致程序崩溃(见以前的文章),放在DllMain中进行最合适,因为Windows保证在DllMain中的代码执行 时进程内的其他线程不会运行,见:http://www.microsoft.com/whdc/driver/kernel/DLL_bestprac.mspx
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, LPVOID lpReserved)
{
if (DLL_PROCESS_ATTACH == dwReason)
{
DisableThreadLibraryCalls((HMODULE)hModule);
return InstallPatch();
}
else if(DLL_PROCESS_DETACH == dwReason)
{
RemovePatch();
}
return TRUE;
}
2,旁路点的安装和卸载。安装旁路点一般就是往旁路点处插入一个跳转(call或jmp,5字节)指令,让程序运行到此处时跳转到自己的 patch代码。要注意程序代码段的页面属性一般都是只读的,安装前先要把页面属性改成可读写,装完后再恢复原来的页面属性。卸载旁路点就是把patch 点原先的代码写回去,因此在安装时要做一下备份。
void
WriteLocalBYTES(
void
*
pAddress,
void
*
buf,
int
len)
{
DWORD oldprot = 0, dummy;
VirtualProtect(pAddress, len, PAGE_EXECUTE_READWRITE, &oldprot);
WriteProcessMemory(GetCurrentProcess(), pAddress, buf, len, &dummy);
VirtualProtect(pAddress, len, oldprot, &dummy);
}
void
PatchCALL(DWORD pOldCode, DWORD pNewCode, DWORD len)
{
BYTE buf1[5];
buf1[0] = 0xe8; // inst call
*(DWORD *)(buf1+1) = pNewCode-(pOldCode+5);
WriteLocalBYTES((void*)pOldCode, buf1, len);
}
FARPROC g_pDrawText;
BYTE g_oldcode[
5
];
BOOL InstallPatch()
{
HMODULE hD2Win = GetModuleHandle("d2win.dll");
if (!hD2Win) hD2Win = LoadLibrary("d2win.dll");
if (hD2Win)
{
g_pDrawText = GetProcAddress(hD2Win, (LPCSTR)10064);
if (g_pDrawText)
{
memcpy(g_oldcode, g_pDrawText, 5);
PatchCALL((DWORD)g_pDrawText, (DWORD)D2WinDrawTextPatch_ASM, 5);
return TRUE;
}
}
return FALSE;
}
void
RemovePatch()
{
if (g_pDrawText)
WriteLocalBYTES(g_pDrawText, g_oldcode, 5);
}
3,旁路点的patch代码。patch代码根据所要实现的功能的不同而不同,需要具体情况具体分析。一般来说逻辑稍微复杂些的patch代码会由两步分 组成,一是用汇编写的入口代码,对寄存器和栈指针进行精细控制以免破坏原先的数据;二是用C/C++写的逻辑实现代码。汇编入口代码调用C/C++的逻辑 实现代码。在此例中,D2WinDrawTextPatch_ASM为纯(嵌入)汇编函数,第一行代码运行前的寄存器内容和栈布局如注释所 示,D2WinDrawTextPatch有5个参数,由于是__fastcall,因此前两个参数在ecx和edx中传递(VC的规定,C++ Builder可能有所不同),剩下三个从栈中传入。由于C函数D2WinDrawTextPatch可能破坏ecx和edx里的内容,因此调用前必须保 存它们的值,然后调用D2WinDrawTextPatch做真正的字符串过滤,注意D2WinDrawTextPatch也声明为 __fastcall,因此参数str为ecx。根据str是否为"Trial Version",D2WinDrawTextPatch返回TRUE或FALSE(返回值存放在eax中)。 D2WinDrawTextPatch_ASM在D2WinDrawTextPatch返回后先恢复ecx和edx的原先内容,接着根据eax的值判断是 否应该执行真正的D2WinDrawText函数来显示字符串。标号draw_text后面的代码流程将跳到真正的D2WinDrawText去执行,由 于旁路点把它的前两个指令(共5个字节)改掉了(见下图,上半部分为patch前的代码,下半部分为patch后的代码),在跳转之前必须先执行这两条指 令。
BOOL __fastcall D2WinDrawTextPatch(wchar_t
*
str)
{
return str && (*str == L'T' && wcscmp(str, L"Trial Version") == 0);
}
/*
* [esp+0x10] = arg 5 - unknown
* [esp+0x0c] = arg 4 - col
* [esp+0x08] = arg 3 - ypos
* [esp+0x04] = return address of caller of D2WIN_DrawText, e.g., from D2JSP
* [esp+0x00] = return address of D2WIN_DrawText
* edx = arg 2, xpos
* ecx = arg 1, text string
*/
void
__declspec(naked) D2WinDrawTextPatch_ASM()
{
__asm
{
push edx; // arg 2 - xpos
push ecx; // arg 1 - str
call D2WinDrawTextPatch;
pop ecx;
pop edx;
test eax, eax;
jz draw_text;
pop eax; // discard return address of caller of D2WIN_DrawText
ret 0x0c; // discard three arguments
draw_text:
pop eax; // return address of D2WIN_DrawText
// original code, overwritten by detour patch
push ebx;
mov ebx, dword ptr [esp+0x10];
jmp eax;
}
}
4,给静态绑定用的空导出函数。在一般的hack中这一部分是不需要的。setdll.exe在做静态绑定时需要把d2jsppatch.dll的一个导出函数(随便一个,有一个就行)插入js32.dll的IAT中。
extern
"
C
"
__declspec( dllexport ) VOID FakedInterface()
{
}
5,做为一个行为良好、成熟的hack,在安装旁路点前还应该检测自己用到的旁路点有没有已经被其他hack改掉了,否则就会导致其他hack不 能正常工作甚至整个游戏崩溃。如果别人已经改掉了,自己要么额外处理要么只好主动退出。那么如果要patch很多点(像maphack、hackmap有 上百个),怎么检查这些点是否被改了呢?一个快速而且有效的做法是对这些点的内容预先计算一个校验和,运行时每次安装旁路点前再校验一遍然后和预先计算的 值比较。d2jsppatch.dll的功能很简单,没有做这部分处理,具体做法请参考d2hackmap 2.24的源代码,也可以到这里下载:http://newd2event.net/index.php?id=hacks/Sting_Hackmap。
附:完整的d2jsppatch.dll源代码,只有短短的100行不到。
Code
1#define WIN32_LEAN_AND_MEAN
2#include <windows.h>
3
4// helper stuff
5void WriteLocalBYTES(void* pAddress, void *buf, int len)
6{
7 DWORD oldprot = 0, dummy;
8 VirtualProtect(pAddress, len, PAGE_EXECUTE_READWRITE, &oldprot);
9 WriteProcessMemory(GetCurrentProcess(), pAddress, buf, len, &dummy);
10 VirtualProtect(pAddress, len, oldprot, &dummy);
11}
12
13void PatchCALL(DWORD pOldCode, DWORD pNewCode, DWORD len)
14{
15 BYTE buf1[5];
16 buf1[0] = 0xe8; // inst call
17 *(DWORD *)(buf1+1) = pNewCode-(pOldCode+5);
18 WriteLocalBYTES((void*)pOldCode, buf1, len);
19}
20
21// patch stuff
22//BOOL __fastcall D2DrawTextPatch(wchar_t *str, int xpos, int ypos, DWORD col, DWORD unknown)
23BOOL __fastcall D2WinDrawTextPatch(wchar_t *str)
24{
25 return str && (*str == L'T' && wcscmp(str, L"Trial Version") == 0);
26}
27
28/**//*
29* [esp+0x10] = arg 5 - unknown
30* [esp+0x0c] = arg 4 - col
31* [esp+0x08] = arg 3 - ypos
32* [esp+0x04] = return address of caller of D2WIN_DrawText, e.g., from D2JSP
33* [esp+0x00] = return address of D2WIN_DrawText
34* edx = arg 2, xpos
35* ecx = arg 1, text string
36*/
37void __declspec(naked) D2WinDrawTextPatch_ASM()
38{
39 __asm
40 {
41 push edx; // arg 2 - xpos
42 push ecx; // arg 1 - str
43 call D2WinDrawTextPatch;
44 pop ecx;
45 pop edx;
46 test eax, eax;
47 jz draw_text;
48 pop eax; // discard return address of caller of D2WIN_DrawText
49 ret 0x0c; // discard three arguments
50draw_text:
51 pop eax; // return address of D2WIN_DrawText
52
53 // original code, overwritten by detour patch
54 push ebx;
55 mov ebx, [esp+0x10];
56
57 jmp eax;
58 }
59}
60
61FARPROC g_pDrawText;
62BYTE g_oldcode[5];
63BOOL InstallPatch()
64{
65 HMODULE hD2Win = GetModuleHandle("d2win.dll");
66 if (!hD2Win) hD2Win = LoadLibrary("d2win.dll");
67 if (hD2Win)
68 {
69 g_pDrawText = GetProcAddress(hD2Win, (LPCSTR)10064);
70 if (g_pDrawText)
71 {
72 memcpy(g_oldcode, g_pDrawText, 5);
73 PatchCALL((DWORD)g_pDrawText, (DWORD)D2WinDrawTextPatch_ASM, 5);
74 return TRUE;
75 }
76 }
77 return FALSE;
78}
79
80void RemovePatch()
81{
82 if (g_pDrawText)
83 WriteLocalBYTES(g_pDrawText, g_oldcode, 5);
84}
85
86BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, LPVOID lpReserved)
87{
88 if (DLL_PROCESS_ATTACH == dwReason)
89 {
90 DisableThreadLibraryCalls((HMODULE)hModule);
91 return InstallPatch();
92 }
93 else if(DLL_PROCESS_DETACH == dwReason)
94 {
95 RemovePatch();
96 }
97 return TRUE;
98}
99
100// faked exported function used to bind with js32.dll
101extern "C" __declspec( dllexport ) VOID FakedInterface()
102{
103}
104