Hacking Diablo II之外挂实战教程:去除D2JSP试用版显示的Trial Version信息

前几天一个网友给我发消息请我帮他个忙。他的问题是,他正在使用的D2JSP是免费试用版,试用版在运行时会在游戏的所有游戏画面中央显示一行很大的“Trial Version”字样(见下图中的红圈),很烦人,他想去掉这行字。我想正好用此做个教程解释前面介绍过的hack工作原理,于是答应帮他看看。

Hacking Diablo II之外挂实战教程:去除D2JSP试用版显示的Trial Version信息_第1张图片

我已经很久没有开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输出的,见下图。

Hacking Diablo II之外挂实战教程:去除D2JSP试用版显示的Trial Version信息_第2张图片

现在只要改变MyD2WIN_DrawText()的实现,判断输出字符串如果为"Trial Version"则跳过真正的D2WIN_DrawText()代码就搞定了,代码见后面的详细源代码分析。改变后的显示结果如下图,画面上方的d2jsp v1.2.0字样还在,Trial Version字样已经没了:

Hacking Diablo II之外挂实战教程:去除D2JSP试用版显示的Trial Version信息_第3张图片

至此,给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,如下图。
Hacking Diablo II之外挂实战教程:去除D2JSP试用版显示的Trial Version信息_第4张图片

以下是对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;
    }

}

Hacking Diablo II之外挂实战教程:去除D2JSP试用版显示的Trial Version信息_第5张图片

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行不到。

#define  WIN32_LEAN_AND_MEAN
#include 
< windows.h >

//  helper stuff
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);
}


//  patch stuff
// BOOL __fastcall D2DrawTextPatch(wchar_t *str, int xpos, int ypos, DWORD col, DWORD unknown)
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, [esp
+0x10];

        jmp eax;
    }

}


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);
}


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;
}


//  faked exported function used to bind with js32.dll
extern   " C "  __declspec( dllexport ) VOID FakedInterface()
{
}

你可能感兴趣的:(游戏,jsp,工作,汇编,dll,patch)