[软件]国服游戏-路尼亚战记
[工具]OD,Wep,以及其它的一些文本工具
[目的]研究游戏保护技术,深论协议级分析。
意在抛砖引玉,抵制游戏外挂。我会在每个分析点做出一些保护上的思考。
开始正文。
一个多月前,有看过一些游戏,DNF,路尼亚战记。他们大概是属于那种靠操作,连招一张地图一张地图那种游戏。DNF是由腾迅公司代理的,自己做了点不强的小保护,但是还是被别人开发出了外挂。居然还有全屏秒杀怪功能。
从朋友那里大概了解了点游戏开发的一些设计思路,就服务器和客户端而言,有强服务器弱客户端,和强客户端,弱服务器。之类的分别。
大概dnf这样的游戏是属于强客户端这样的游戏,所以才有可能开发出全屏秒杀。二者主要的区别在于,把主要运算是放在服务器端,还是放在客户端。
经过我的分析,像路尼亚战记这样的游戏,是属于强服务器的。
前边所有的东西------我就不说了,开始分析。
首先,注册个帐号,创建角色,然后登陆游戏。OD--附加游戏进程。
既然是分析协议,我们就在send处下段。
在开始讲解之前,首先要明确以下一些事。第一,我们明确的知道,我们的发送包是经过加密处理了的。第二,我们要明确,我们要分析的send动作大概是什么,比如说走一步,又比如说要打开一个仓库。
对于第二点,我们采用在send处,下段,然后在很快的反映时间里,给游戏一个动作。然后观察send数据。
这里我选择的是打开武器铺,我们发现其打开武器铺的数据长度是0x0e.
71A24288 90 nop
71A24289 90 nop
71A2428A > 8BFF mov edi, edi ; dword ptr [esp+0x0c] == 0x0e
71A2428C . 55 push ebp
71A2428D . 8BEC mov ebp, esp
71A2428F . 83EC 10 sub esp, 10
71A24292 . 56 push esi
71A24293 . 57 push edi
71A24294 . 33FF xor edi, edi
71A24296 . 813D 2840A371>cmp dword ptr [71A34028], 71A29448 ; 入口地址
71A242A0 . 0F84 AD730000 je 71A2B653
71A242A6 > 8D45 F8 lea eax, dword ptr [ebp-8]
71A242A9 . 50 push eax
下条件断点。
然后,当我们打开武器铺的时候,程序就会中断在那里。
然后,这时候,我们想知道的是,什么时候,其向send数据包里,那段内存写入了数据,我们才能回烁跟踪。
方法很多,我就不一一说了,就针对这个游戏。谈谈...
我们多次打开武器库,观察发现,其每次发送的数据的内存地址都是一个。我们根据这个地方下硬件访问断点就好了。
-------这里,要谈谈游戏保护技术了。我觉得好点的保护,特别是在send点这里,send的数据内存地址,应该尽力保持活跃,跳动。不能一直固定。好象(分析有段时间了,记忆就忘记了),朱仙这点就做的比较好,在send数据的时候。内存点会变。
但是使用alloc和reallloc等函数,又难免会被别人在这些关键点的地方下断点。作为一个破解分析者,首先会考虑的是以最高效的方法做出分析。不会把所有的游戏代码,反汇编读完。所以一些关键点,应该考虑离散性高,偶合性高。高的偶合会让分析者迷茫,找不到关键点。高的离散,会让分析者解读不出确实的意义。
接下来继续。
007332C0 51 push ecx
007332C1 8B4424 0C mov eax, dword ptr [esp+C]
007332C5 85C0 test eax, eax
007332C7 55 push ebp
007332C8 8B6C24 0C mov ebp, dword ptr [esp+C]
007332CC 57 push edi
007332CD 8BF9 mov edi, ecx
007332CF 74 63 je short 00733334
007332D1 53 push ebx
007332D2 894424 18 mov dword ptr [esp+18], eax
007332D6 56 push esi
007332D7 EB 07 jmp short 007332E0
007332D9 8DA424 00000000 lea esp, dword ptr [esp]
007332E0 8A45 00 mov al, byte ptr [ebp]
007332E3 884424 18 mov byte ptr [esp+18], al
007332E7 8B47 04 mov eax, dword ptr [edi+4]
007332EA 8D48 01 lea ecx, dword ptr [eax+1]
007332ED 894424 10 mov dword ptr [esp+10], eax
007332F1 04 04 add al, 4
007332F3 894F 04 mov dword ptr [edi+4], ecx
007332F6 8D5424 10 lea edx, dword ptr [esp+10]
007332FA 8AC8 mov cl, al
007332FC BE 03000000 mov esi, 3
00733301 8A42 01 mov al, byte ptr [edx+1]
00733304 42 inc edx
00733305 B3 49 mov bl, 49
00733307 F6EB imul bl
00733309 34 15 xor al, 15
0073330B 02C8 add cl, al
0073330D 4E dec esi
0073330E ^ 75 F1 jnz short 00733301
00733310 0FB64424 18 movzx eax, byte ptr [esp+18]
00733315 0FB6D1 movzx edx, cl
00733318 8B4F 08 mov ecx, dword ptr [edi+8]
0073331B C1E2 08 shl edx, 8
0073331E 03D0 add edx, eax
00733320 8A140A mov dl, byte ptr [edx+ecx]
00733323 8B4424 1C mov eax, dword ptr [esp+1C]
00733327 8855 00 mov byte ptr [ebp], dl
0073332A 45 inc ebp ; 这里
0073332B 48 dec eax
0073332C 894424 1C mov dword ptr [esp+1C], eax
00733330 ^ 75 AE jnz short 007332E0
00733332 5E pop esi
00733333 5B pop ebx
00733334 5F pop edi
00733335 5D pop ebp
00733336 59 pop ecx
00733337 C2 0800 retn 8
我们在硬件断点的第二次F9条到这里。
一般经过N次的观察之后,我们会发现。这里其实就是封包的加密函数。
这里谈谈经验之谈。通常加密函数,都会和普通函数有所不同。因为从意义上来说,加密函数,主要完成的是数据加密,变换。所以其使用的指令,和其指令的方式会和正常函数有所不同。比如涉及到位操作,byte操作,会比较多。比如md5等,一看就shl什么指令就是一篇篇。
这里我们再谈谈,保护上的一些东西。--我觉得,位的变换,和其它的东西,不能一步写死到一个函数头,不然,解密者,就会像我做的一样。找到加密call,分析加密call参数,然后分析出具体加密函数。然后就可以自己写封包加密了。
这游戏这点做的相当之差。
然后一个ctrl+F9,就来到下边这里。
00733340 56 push esi
00733341 8B7424 08 mov esi, dword ptr [esp+8]
00733345 8B06 mov eax, dword ptr [esi]
00733347 57 push edi
00733348 8BF9 mov edi, ecx
0073334A 8BCE mov ecx, esi
0073334C FF50 04 call dword ptr [eax+4] ; 取长度
0073334F 8B16 mov edx, dword ptr [esi]
00733351 50 push eax
00733352 8BCE mov ecx, esi
00733354 FF52 10 call dword ptr [edx+10] ; 取包明问
00733357 50 push eax
00733358 8BCF mov ecx, edi
0073335A E8 61FFFFFF call 007332C0 ; 堆栈结构依次为-封包明问-长度-解码表地址
0073335F 8B06 mov eax, dword ptr [esi]
00733361 8BCE mov ecx, esi
00733363 FF50 04 call dword ptr [eax+4]
00733366 5F pop edi
00733367 5E pop esi
00733368 C2 0400 retn 4
然后就慢慢分析了哈。
下边贴出,一个月前,写的针对这个游戏的内挂的一些测试代码。各位可以配合到看,方便理解。
#include ""
//BYTE nCmd[0x0e]={0x0E,0x00,0xe0,0x55,0x91,0x10,0x09,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
unsigned long MyApi = 0;
unsigned long hookApi = 0;
DWORD dwWrite = 0;
BYTE lpResetSend[0x05]={0x8B,0XFF,0X55,0X8B,0XEC};//用于恢复HookSend的5个字节
HINSTANCE hws2_32 = NULL;//ws2_32句柄
HANDLE my_sendhandle;//保存用语发送send的句柄
int WINAPI DllMain(HANDLE hinstDll, DWORD fdwReason, LPVOID lpvReserved)
{
// MessageBox( NULL, "yes", "yes", MB_OK);
hModule = hinstDll;
DWORD dwThread;
// UiThread( NULL);
//
switch(fdwReason)
{
case DLL_PROCESS_ATTACH:
MessageBox( NULL, "Debug", "Debug", MB_OK);
CreateThread( NULL, 0,(unsigned long (__stdcall *)(void *))UiThread,NULL,0,&dwThread);
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
DWORD WINAPI UiThread(LPARAM lParam)
{
MSG msg;
HWND hWnd;
hWnd = CreateDialog( (HINSTANCE)hModule, MAKEINTRESOURCE(IDD_MAIN_PAGE), NULL, MainProc);
ShowWindow( hWnd, SW_SHOW);
UpdateWindow( hWnd);
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
int CALLBACK MainProc( HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
BYTE nCmd[RUN_RIGHT_LEN]={RUN_RIGHT};
HINSTANCE hws2_32;
static DWORD dwCount[2] = {0x58548565,0x00c45878};
static HANDLE hFile;
BYTE lpBuffer[0x2];
char szFormat[7];
static DWORD dwWrite = 7;
RtlZeroMemory( lpBuffer,0x2 );
DWORD dwRead;
DWORD lpRead=0;
int nindex=0;
switch( uMsg)
{
case WM_COMMAND:
switch(wParam)
{
case IDC_BTN_TEST:
// MessageBox( NULL,"debug","debug",MB_OK);
GoRight();
//Speck_Something( "yangzhihao");
/*
封包加密call
组织然后send
*/
// MessageBox( NULL, "call","call", MB_OK);
/*__asm{
pushad
push 0x0e
lea eax, nCmd
push eax
mov eax,0x23ba078
mov ecx,eax
mov ebx,0x729a00
mov eax,0x0e
call ebx
popad
}
hws2_32 = LoadLibrary( "ws2_32.dll");
(unsigned long)::GetProcAddress( hws2_32, "send");
__asm
{
push 0x00
push 0x0e
lea ebx, nCmd
push ebx
mov ebx,my_sendhandle
push ebx
call eax
}
*/
break;
case IDC_READ_TABLE:
MessageBox( NULL, "write", "write", MB_OK);
for( nindex;nindex<0xa7a9;nindex++)
{
lpRead = 0x00c45878+nindex;
ReadProcessMemory( GetCurrentProcess(), (LPVOID)lpRead,lpBuffer, 0x01, &dwRead);
sprintf( szFormat,"0x%2x",lpBuffer[0]);
WriteFile( hFile, szFormat, dwWrite, &dwRead, 0);
}
break;
case IDC_BTN_HOOK:
hws2_32 = LoadLibrary( "ws2_32.dll");
hookApi = (unsigned long)::GetProcAddress( hws2_32, "send");
MyApi = (unsigned long )GetSendPara;
_HOOK_APIN( MyApi, hookApi);
break;
default:
break;
}
break;
case WM_INITDIALOG:
hFile = CreateFile( "c:\\",GENERIC_READ | GENERIC_WRITE ,FILE_SHARE_READ|FILE_SHARE_WRITE,
NULL,OPEN_ALWAYS ,FILE_ATTRIBUTE_NORMAL,0);
break;
case WM_CLOSE:
EndDialog( hWnd, 0);
break;
default:
DefWindowProc( hWnd, uMsg, wParam, lParam);
}
return 0;
}
/*取send句柄*/
void GetSendPara(void)
{
__asm
{
/*首先要堆栈平衡下 因为VC6前边会有压栈操作*/
pop eax
pop eax
pop eax
pop eax
/*执行判断操作,取send的句柄*/
mov eax, dword ptr [esp + 0x0c]
cmp eax,0x0e
jnz JMP_HOOM
mov eax,dword ptr[esp+0x04]
mov my_sendhandle,eax
}
WriteProcessMemory( GetCurrentProcess(), (void*)hookApi, lpResetSend, 0x05, &dwWrite);
dwWrite = 0;
__asm
{
JMP_HOOM:
/*跳会原来的地方*/
sub ebp,0x04
mov eax,hookApi
mov edi,edi
push ebp
mov ebp,esp
add eax,5
jmp eax
}
return;
}
void Encode( BYTE* pCmd, int nLen)
{
__asm{
pushad
mov eax,dword ptr[esp+0x34]//长度
push eax
mov eax, dword ptr[esp+0x34]//命令明文序列
push eax
mov eax,0x2fd078 // 硬编码---编码表--和计数表
mov ecx,eax
mov ebx,0x729a00//加密call
mov eax,dword ptr[esp+0x08]//长度
call ebx
popad
}
return;
}
void GoRight()
{
BYTE lpCmd[RUN_RIGHT_LEN] = {RUN_RIGHT};
Encode( lpCmd,RUN_RIGHT_LEN);
/*发送封包*/
hws2_32 = LoadLibrary( "ws2_32.dll");
(unsigned long)::GetProcAddress( hws2_32, "send");
__asm
{
push 0x00
push RUN_RIGHT_LEN
lea ebx, lpCmd
push ebx
mov ebx,my_sendhandle
push ebx
call eax
}
}
void Speck_Something(char* pSpeckBuffer)
{
//MessageBox( NULL, "speck","speck",MB_OK);
BYTE packLen;
int count = 0;
WCHAR wszSpec[MAX_PATH];
RtlZeroMemory( wszSpec,MAX_PATH*2);
count = strlen( pSpeckBuffer);
long nwLong = MultiByteToWideChar( CP_ACP, 0, pSpeckBuffer, strlen(pSpeckBuffer),
wszSpec,sizeof(wszSpec));
BYTE temp[0x08]={SPECK_ONE};
BYTE *lpCmd;
lpCmd = new BYTE [200] ; //申请一块封包的内存
RtlZeroMemory( lpCmd, 200);
memcpy( lpCmd,temp,0x08); //com封包命令
memcpy( lpCmd,(void*)&count,0x01);
packLen = (BYTE)0x0c+nwLong*2+2; //包头 封包整个长度
lpCmd[0] = packLen;
lpCmd [0x0A] = (BYTE)nwLong+1;//包的11个字节 字符串长度
memcpy( (lpCmd+12), wszSpec, nwLong*2+1);//把字符放入消息
Encode( lpCmd, (int)lpCmd[0]);
hws2_32 = LoadLibrary( "ws2_32.dll");
(unsigned long)::GetProcAddress( hws2_32, "send");
__asm
{
push 0x00
push packLen
lea ebx, lpCmd
push ebx
mov ebx,my_sendhandle
push ebx
call eax
}
// delete [] lpCmd;
}
由于是测试代码,写的相当潦草。这是个dll代码,这个代码是注如到游戏进程的。有hook api操作。
在操作这前,先抓出明文封包动作
#ifndef __COMMON_H_
#define __COMMON_H_
#include <>
BOOL _HookApi( unsigned long _My_Addr, unsigned long _Hook_Addr);
/*command数据*/
/*向右走*/
#define RUN_RIGHT_LEN 0x0e
#define RUN_RIGHT 0x0e,0x00,0xe0,0x55,0x8d,0xe2,0x00,0x00,0x00,0x00,0x06,0x00,0x00,0x00
/*喊话*/
//喊话封包的长度不固定-首部为封包长度-然后8个字节的命令.接着4个字节的字符长度.跟到字符串
#define SPECK_ONE 0x00,0x00,0xe0,0x55,0xb9,0x6f,0x00,0x00
#endif
然后就xxxx.....