對(duì)游戲數(shù)據(jù)包進(jìn)行抓取截圖,有序要的朋友可以看看
截獲API是個(gè)很有用的東西,比如你想分析一下別人的程序是怎樣工作的。這里我介紹一下一種我自己試驗(yàn)通過的方法。
首先
我們必須設(shè)法把自己的代碼放到目標(biāo)程序的進(jìn)程空間里去。Windows Hook可以幫我們實(shí)現(xiàn)這一點(diǎn)。SetWindowsHookEx的聲明如下:
HHOOK SetWindowsHookEx(
int idHook, // hook type
HOOKPROC lpfn, // hook procedure
HINSTANCE hMod, // handle to application instance
DWORD dwThreadId // thread identifier
);
具體的參數(shù)含義可以翻閱msdn,沒有msdn可謂寸步難行。
這里Hook本身的功能并不重要,我們使用它的目的僅僅只是為了能夠讓W(xué)indows把我們的代碼植入別的進(jìn)程里去。hook Type我們?nèi)芜x一種即可,只要保證是目標(biāo)程序肯定會(huì)調(diào)用到就行,這里我用的是WH_CALLWNDPROC。lpfn和hMod分別指向我們的鉤子代碼及其所在的dll,dwThreadId設(shè)為0,表示對(duì)所有系統(tǒng)內(nèi)的線程都掛上這樣一個(gè)hook,這樣我們才能把代碼放到別的進(jìn)程里去。
之后,我們的代碼就已經(jīng)進(jìn)入了系統(tǒng)內(nèi)的所有進(jìn)程空間了。必須注意的是,我們只需要截獲我們所關(guān)心的目標(biāo)程序的調(diào)用,因此還必須區(qū)分一下進(jìn)程號(hào)。我們自己的鉤子函數(shù)中,第一次運(yùn)行將進(jìn)行最重要的API重定向的工作。也就是通過將所需要截獲的API的開頭幾個(gè)字節(jié)改為一個(gè)跳轉(zhuǎn)指令,使其跳轉(zhuǎn)到我們的API中來。這是最關(guān)鍵的部分。這里我想截三個(gè)調(diào)用,ws2_32.dll中的send和recv、user32.dll中的GetMessageA。
DWORD dwCurrentPID = 0;
HHOOK hOldHook = NULL;
DWORD pSend = 0;
DWORD pRecv = 0;
GETMESSAGE pGetMessage = NULL;
BYTE btNewBytes[8] = { 0x0B8, 0x0, 0x0, 0x40, 0x0, 0x0FF, 0x0E0, 0 };
DWORD dwOldBytes[3][2];
HANDLE hDebug = INVALID_HANDLE_value;
LRESULT CALLBACK CallWndProc( int nCode, WPARAM wParam, LPARAM lParam )
{
DWORD dwSize;
DWORD dwPIDWatched;
HMODULE hLib;
if( dwCurrentPID == 0 )
{
dwCurrentPID = GetCurrentProcessId();
HWND hwndMainHook;
hwndMainHook = ::FindWindow( 0, "MainHook" );
dwPIDWatched = ::SendMessage( hwndMainHook, (WM_USER+100), 0, 0 );
hOldHook = (HHOOK)::SendMessage( hwndMainHook, (WM_USER+101), 0, 0 );
if( dwCurrentPID == dwPIDWatched )
{
hLib = LoadLibrary( "ws2_32.dll" );
pSend = (DWORD)GetProcAddress( hLib, "send" );
pRecv = (DWORD)GetProcAddress( hLib, "recv" );
::ReadProcessMemory( INVALID_HANDLE_value, (void *)pSend, (void *)dwOldBytes[0], sizeof(DWORD)*2, &dwSize );
*(DWORD *)( btNewBytes + 1 ) = (DWORD)new_send;
::WriteProcessMemory( INVALID_HANDLE_value, (void *)pSend, (void *)btNewBytes, sizeof(DWORD)*2, &dwSize );
::ReadProcessMemory( INVALID_HANDLE_value, (void *)pRecv, (void *)dwOldBytes[1], sizeof(DWORD)*2, &dwSize );
*(DWORD *)( btNewBytes + 1 ) = (DWORD)new_recv;
::WriteProcessMemory( INVALID_HANDLE_value, (void *)pRecv, (void *)btNewBytes, sizeof(DWORD)*2, &dwSize );
hLib = LoadLibrary( "user32.dll" );
pGetMessage = (GETMESSAGE)GetProcAddress( hLib, "GetMessageA" );
::ReadProcessMemory( INVALID_HANDLE_value, (void *)pGetMessage, (void *)dwOldBytes[2], sizeof(DWORD)*2, &dwSize );
*(DWORD *)( btNewBytes + 1 ) = (DWORD)new_GetMessage;
::WriteProcessMemory( INVALID_HANDLE_value, (void *)pGetMessage, (void *)btNewBytes, sizeof(DWORD)*2, &dwSize );
hDebug = ::CreateFile( "C:\\Trace.log", GENERIC_WRITE, 0, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0 );
}
}
if( hOldHook != NULL )
{
return CallNextHookEx( hOldHook, nCode, wParam, lParam );
}
return 0;
}
上面的鉤子函數(shù),只有第一次運(yùn)行時(shí)有用,就是把三個(gè)函數(shù)的首8字節(jié)修改一下(實(shí)際上只需要7個(gè))。btNewBytes中的指令實(shí)際就是
mov eax, 0x400000
jmp eax
這里的0x400000就是新的函數(shù)的地址,比如new_recv/new_send/new_GetMessage,此時(shí),偷梁換柱已經(jīng)完成。再看看我們的函數(shù)中都干了些什么。以GetMessageA為例:
BOOL _stdcall new_GetMessage( LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax )
{
DWORD dwSize;
char szTemp[256];
BOOL r = false;
//Watch here before it's executed.
sprintf( szTemp, "Before GetMessage : HWND 0x%8.8X, msgMin 0x%8.8X, msgMax 0x%8.8x \r\n", hWnd, wMsgFilterMin, wMsgFilterMax );
::WriteFile( hDebug, szTemp, strlen(szTemp), &dwSize, 0 );
//Watch over
// restore it at first
::WriteProcessMemory( INVALID_HANDLE_value, (void *)pGetMessage, (void *)dwOldBytes[2], sizeof(DWORD)*2, &dwSize );
// execute it
r = pGetMessage( lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax );
// hook it again
*(DWORD *)( btNewBytes + 1 ) = (DWORD)new_GetMessage;
::WriteProcessMemory( INVALID_HANDLE_value, (void *)pGetMessage, (void *)btNewBytes, sizeof(DWORD)*2, &dwSize );
//Watch here after it's executed
sprintf( szTemp, "Result of GetMessage is %d.\r\n", r );
::WriteFile( hDebug, szTemp, strlen( szTemp ), &dwSize, 0 );
if( r )
{
sprintf( szTemp, "Msg : HWND 0x%8.8X, MSG 0x%8.8x, wParam 0x%8.8X, lParam 0x%8.8X\r\nTime 0x%8.8X, X %d, Y %d\r\n",
lpMsg->hwnd, lpMsg->message,
lpMsg->wParam, lpMsg->lParam, lpMsg->time,
lpMsg->pt.x, lpMsg->pt.y );
::WriteFile( hDebug, szTemp, strlen( szTemp ), &dwSize, 0 );
}
strcpy( szTemp, "\r\n" );
::WriteFile( hDebug, szTemp, strlen( szTemp ), &dwSize, 0 );
//Watch over
return r;
}
先將截獲下來的參數(shù),寫入到一個(gè)log文件中,以便分析。然后恢復(fù)原先保留下來的GetMessageA的首8字節(jié),然后執(zhí)行真正的GetMessageA調(diào)用,完畢后再將執(zhí)行結(jié)果也寫入log文件,然后將GetMessageA的執(zhí)行結(jié)果返回給調(diào)用者。
整個(gè)截獲的過程就是這樣。你可以把其中的寫log部分改成你自己想要的操作。這里有個(gè)不足的地方是,截獲動(dòng)作是不能夠并發(fā)進(jìn)行的,如果目標(biāo)進(jìn)程是多線程的,就會(huì)有問題。解決辦法是,可以在每次new_GetMessage中加入一個(gè)CriticalSection的鎖和解鎖,以使調(diào)用變?yōu)榇羞M(jìn)行,但這個(gè)我沒有試驗(yàn)過。
網(wǎng)絡(luò)游戲的封包技術(shù)是大多數(shù)編程愛好者都比較關(guān)注的關(guān)注的問題之一,在這一篇里就讓我們一起研究一下這一個(gè)問題吧。
別看這是封包這一問題,但是涉及的技術(shù)范圍很廣范,實(shí)現(xiàn)的方式也很多(比如說APIHOOK,VXD,Winsock2都可以實(shí)現(xiàn)),在這里我們不可能每種技術(shù)和方法都涉及,所以我在這里以Winsock2技術(shù)作詳細(xì)講解,就算作拋磚引玉。
由于大多數(shù)讀者對(duì)封包類編程不是很了解,我在這里就簡(jiǎn)單介紹一下相關(guān)知識(shí):
APIHooK:
由于Windows的把內(nèi)核提供的功能都封裝到API里面,所以大家要實(shí)現(xiàn)功能就必須通過API,換句話說就是我們要想捕獲數(shù)據(jù)封包,就必須先要得知道并且捕獲這個(gè)API,從API里面得到封包信息。
VXD:
直接通過控制VXD驅(qū)動(dòng)程序來實(shí)現(xiàn)封包信息的捕獲,不過VXD只能用于win9X。
winsock2:
winsock是Windows網(wǎng)絡(luò)編程接口,winsock工作在應(yīng)用層,它提供與底層傳輸協(xié)議無(wú)關(guān)的高層數(shù)據(jù)傳輸編程接口,winsock2是winsock2.0提供的服務(wù)提供者接口,但只能在win2000下用。
好了,我們開始進(jìn)入winsock2封包式編程吧。
在封包編程里面我準(zhǔn)備分兩個(gè)步驟對(duì)大家進(jìn)行講解:1、封包的捕獲,2、封包的發(fā)送。
首先我們要實(shí)現(xiàn)的是封包的捕獲:
Delphi的封裝的winsock是1.0版的,很自然winsock2就用不成。如果要使用winsock2我們要對(duì)winsock2在Delphi里面做一個(gè)接口,才可以使用winsock2。
1、如何做winsock2的接口?
1)我們要先定義winsock2.0所用得到的類型,在這里我們以WSA_DATA類型做示范,大家可以舉一仿三的來實(shí)現(xiàn)winsock2其他類型的封裝。
我們要知道WSA_DATA類型會(huì)被用于WSAStartup(wVersionRequired: word; var WSData: TWSAData): Integer;,大家會(huì)發(fā)現(xiàn)WSData是引用參數(shù),在傳入?yún)?shù)時(shí)傳的是變量的地址,所以我們對(duì)WSA_DATA做以下封裝:
const
WSADESCRIPTION_LEN = 256;
WSASYS_STATUS_LEN = 128;
type
PWSA_DATA = ^TWSA_DATA;
WSA_DATA = record
wVersion: Word;
wHighVersion: Word;
szDescription: array[0..WSADESCRIPTION_LEN] of Char;
szSystemStatus: array[0..WSASYS_STATUS_LEN] of Char;
iMaxSockets: Word;
iMaxUdpDg: Word;
lpVendorInfo: PChar;
end;
TWSA_DATA = WSA_DATA;
2)我們要從WS2_32.DLL引入winsock2的函數(shù),在此我們也是以WSAStartup為例做函數(shù)引入:
function WSAStartup(wVersionRequired: word; var WSData: TWSAData): Integer; stdcall;
implementation
const WinSocket2 = 'WS2_32.DLL';
function WSAStartup; external winsocket name 'WSAStartup';
通過以上方法,我們便可以對(duì)winsock2做接口,下面我們就可以用winsock2做封包捕獲了,不過首先要有一塊網(wǎng)卡。因?yàn)樯婕暗秸谶\(yùn)作的網(wǎng)絡(luò)游戲安全問題,所以我們?cè)谶@里以IP數(shù)據(jù)包為例做封包捕獲,如果下面的某些數(shù)據(jù)類型您不是很清楚,請(qǐng)您查閱MSDN:
數(shù)據(jù)類型:
1)我們要起動(dòng)WSA,這時(shí)個(gè)要用到的WSAStartup函數(shù),用法如下:
INTEGER WSAStartup(
wVersionRequired: word,
WSData: TWSA_DATA
);
2)使用socket函數(shù)得到socket句柄,m_hSocket:=Socket(AF_INET, SOCK_RAW, IPPROTO_IP); 用法如下:
INTEGER socket(af: Integer,
Struct: Integer,
protocol: Integer
);
m_hSocket:=Socket(AF_INET, SOCK_RAW, IPPROTO_IP);在程序里m_hSocket為socket句柄,AF_INET,SOCK_RAW,IPPROTO_IP均為常量。
3)定義SOCK_ADDR類型,跟據(jù)我們的網(wǎng)卡IP給Sock_ADDR類型附值,然后我們使用bind函數(shù)來綁定我們的網(wǎng)卡,Bind函數(shù)用法如下:
Type
IN_ADDR = record
S_addr : PChar;
End;
Type
TSOCK_ADDR = record
sin_family: Word;
sin_port: Word;
sin_addr : IN_ADDR
sin_zero: array[0..7] of Char;
End;
var
LocalAddr:TSOCK_ADDR;
LocalAddr.sin_family: = AF_INET;
LocalAddr.sin_port: = 0;
LocalAddr.sin_addr.S_addr: = inet_addr('192.168.1.1'); //這里你自己的網(wǎng)卡的IP地址,而inet_addr這個(gè)函數(shù)是winsock2的函數(shù)。
bind(m_hSocket, LocalAddr, sizeof(LocalAddr));
4)用WSAIoctl來注冊(cè)WSA的輸入輸出組件,其用法如下:
INTEGER WSAIoctl(s:INTEGER,
dwIoControlCode : INTEGER,
lpvInBuffer :INTEGER,
cbInBuffer : INTEGER,
lpvOutBuffer : INTEGER,
cbOutBuffer: INTEGER,
lpcbBytesReturned : INTEGER,
lpOverlapped : INTEGER,
lpCompletionRoutine : INTEGER
);
5)下面做死循環(huán),在死循環(huán)塊里,來實(shí)現(xiàn)數(shù)據(jù)的接收。但是徇環(huán)中間要用Sleep()做延時(shí),不然程序會(huì)出錯(cuò)。
6)在循環(huán)塊里,用recv函數(shù)來接收數(shù)據(jù),recv函數(shù)用法如下:
INTEGER recv (s : INTEGER,
buffer:Array[0..4095] of byte,
length : INTEGER,
flags : INTEGER,
);
7)在buffer里就是我們接收回來的數(shù)據(jù)了,如果我們想要知道數(shù)據(jù)是什么地方發(fā)來的,那么,我們要定義一定IP包結(jié)構(gòu),用CopyMemory()把IP信息從buffer里面讀出來就可以了,不過讀出來的是十六進(jìn)制的數(shù)據(jù)需要轉(zhuǎn)換一下。
看了封包捕獲的全過程序,對(duì)你是不是有點(diǎn)起發(fā),然而在這里要告訴大家的是封包的獲得是很容易的,但是許多游戲的封包都是加密的,如果你想搞清楚所得到的是什么內(nèi)容還需要自己進(jìn)行封包解密。