來源:http://blog.csdn.net/prsniper/article/details/6781270
大家反應比較活躍,看來對大家幫助不少,於是有此文。
================================================================
先聲明一下,文章被很多人轉載,我歡迎大家轉載,但是我發現已有一個人轉載時,沒有保留出處,而且連轉載也沒寫明,竟然掛着“原創”的標題,實在是……
================================================================
這篇文章可能表較長,所以這裏寫個目錄,也可以說是摘要吧:
1.TCP三次握手機制
2.數據包攔截的C++源碼(VS6/VC++)
3.TCP通信C源碼,包括服務端和客戶端
4.常見協議數據包頭部的C語言定義(包括在抓包源碼中)
5.WinHex分析數據報數據
//請在轉載的時候,保留出處,至少聲明是轉載,尊重他人即尊重自己。限於水平,同時精力有限,不足指出望大家批評指正。
用到一張圖片,以便理解:(大家可以閱讀http://blog.csdn.net/prsniper/article/details/6762145 便於理解)
1.TCP三次握手機制
第一次握手:建立連接時,客戶端發送syn包(syn=j)到服務器,並進入SYN_SEND狀態,等待服務器確認;SYN:同步序列編號(Synchronize Sequence Numbers)。
第二次握手:服務器收到syn包,必須確認客戶的SYN(ack=j+1),同時自己也發送一個SYN包(syn=k),即SYN+ACK包,此時服務器進入SYN_RECV狀態;
第三次握手:客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK(ack=k+1),此包發送完畢,客戶端和服務器進入ESTABLISHED狀態,完成三次握手。
完成三次握手,客戶端與服務器開始傳送數據,簡單來說呢,就是:
A告訴B:“我要去你家做客。”,B收到,說:“歡迎”,殺雞宰羊,開門等候,A得到確認,沐浴更衣,刷牙洗臉,帶上筷子,告訴B:“我將要到達。”,雙方會面…關於三次握手得到的數據分析,參見下面WinHex部分。
2.數據包攔截的C++源碼(VS6/VC++)
VC++源碼,可能比較長,大家可以滾動鼠標往下看
////==================================================
/* Analyser.cpp - TCP/IP Pack Analysing Module
* Coded by http://blog.csdn.net/prsniper
* Please retain this information when copying
*///==================================================
#include "ANALYSER.H"
int main(int argc, char* argv[])
{ //初始化SOCKET
WSADATA wsaData;
int ret = WSAStartup(MAKEWORD(2, 2), &wsaData);
if(ret != NO_ERROR)
{ printf("Error at WSAStartup();\n");
return 0;
}
//創建套接字,soket_raw
SOCKET lpRawSocket = socket(AF_INET, SOCK_RAW, IPPROTO_IP);
if (lpRawSocket == INVALID_SOCKET)
{ printf("Error at socket(): %ld\n", WSAGetLastError());
WSACleanup();
return 0;
}
//獲取本機IP地址
char strHost[200];
if(gethostname((char*)strHost, sizeof(strHost) - 1) == SOCKET_ERROR)
{ strcpy(strHost, "localhost"); //主機名獲取失敗,使用localhost
printf("using \"localhost\" as host name!\n");
}
hostent *lpHost = gethostbyname((char*)strHost);
if(lpHost == NULL)
{ printf("gethostbyname(); failed!\n");
lpHost = gethostbyaddr("127.0.0.1", 4, PF_INET);
if(lpHost == NULL)
{ printf("get host failed!\n");
WSACleanup();
return 0;
}
}
//struct in_addr addr;
//if(lpHost->h_addrtype == AF_INET)
//{ while(lpHost->h_addr_list[ret] != 0)
// { addr.s_addr = *(u_long*)lpHost->h_addr_list[ret++];
// printf("\tIPv4 Address #%d: %s\n", ret, inet_ntoa(addr));
// }
//}
//綁定端口,把hostname與sa綁定,sa與lpRawSocket綁定
SOCKADDR_IN sa;
memcpy(&sa.sin_addr.S_un.S_addr, lpHost->h_addr_list[0], lpHost->h_length);
sa.sin_family = AF_INET;
sa.sin_port = htons(8972);
if(bind(lpRawSocket, (PSOCKADDR)&sa, sizeof(sa)) == SOCKET_ERROR)
{ printf("bind(); failed!\n");
WSACleanup();
return 0;
}
//設置RAW socket
DWORD dwBuffer[10];
DWORD dwCount = 0;
DWORD dwSize = 1;
/*
WSAIoctl()函數參數
s:一個套接口的句柄。
dwIoControlCode:將進行的操作的控制代碼。
lpvInBuffer:輸入緩衝區的地址。
cbInBuffer:輸入緩衝區的大小。
lpvOutBuffer:輸出緩衝區的地址。
cbOutBuffer:輸出緩衝區的大小。
lpcbBytesReturned:輸出實際字節數的地址。
lpOverlapped:WSAOVERLAPPED結構的地址。
lpCompletionRoutine:一個指向操作結束後調用的例程指針。
*/
ret = WSAIoctl(lpRawSocket, SIO_RCVALL, &dwSize, sizeof(dwSize), &dwBuffer, sizeof(dwBuffer), &dwCount, NULL, NULL);
if(ret == SOCKET_ERROR)
{ printf("WSAIoctl(); failed!\n");
WSACleanup();
return 0;
}
//偵聽IP報文
char Buffer[8192] = {0}; //Windows 一般一個包裹8K=8192字節(Byte)
BYTE *lpBuffer = (BYTE *)&Buffer;
FILE *lpFile = fopen("TCPIP.LOG","wb"); //存在會覆蓋
int i = 1;
while (1)
{ //memset(Buffer, 0, sizeof(Buffer));
ret = recv(lpRawSocket, Buffer, sizeof(Buffer), 0);
if(ret > 0)
{ Buffer[ret] = NULL; //結束字符串
//收到數據包,解碼(這個就算了)
//ret = *(int*)Buffer[12];
printf("from: %d.%d.%d.%d To %d.%d.%d.%d\n", Buffer[12], Buffer[13], Buffer[14], Buffer[15], Buffer[16], Buffer[17], Buffer[18], Buffer[19]);
printf("\nBegin Pack ID: %d ==================", i);
if(lpFile != NULL)
{ fseek(lpFile, 0, SEEK_END);
fwrite(lpBuffer, ret, 1, lpFile);
fclose(lpFile);
lpFile = fopen("TCPIP.LOG","ab"); //二進制追加模式
}
printf("\nEnd Pack ID: %d ==================\n", i);
i++;
}
}
return 1;
}
inline void fnAnalyse(BYTE *pData)
{ static ICMPHEADER hICMPData;
static IPHEADER hIPData;
static TCPHEADER hTCP;
static UDPHEADER hUDP;
static BYTE *lpStr;
//copy pack data
memcpy(&hIPData, pData, sizeof(IPHEADER));
//轉換爲網絡字節序,即大端模式(big-endian)
//hIPData.iSrcAddr = htons(hIPData.iSrcAddr); //32位要另外寫htons函數,或分成兩個short
//hIPData.iDstAddr = htons(hIPData.iDstAddr); //
hIPData.sLength = htons(hIPData.sLength);
hIPData.sRepare = htons(hIPData.sRepare);
//顯示信息
lpStr = pData + sizeof(IPHEADER);
printf("IPv%x Head Length:%d Bytes. Serve Type:%d\n",
hIPData.cLenVer >> 4, (hIPData.cLenVer & 0x0F) * 4, hIPData.cServer);
//不寫了...
}
編譯運行,會將得到的數據包,保存在當前文件夾的TCPIP.LOG中,二進制模式
3.TCP通信C源碼,包括服務端和客戶端
這是服務端,也就是監聽端的C語言源代碼
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib,"ws2_32.lib")
#define MAX_RECV_LENGTH 1024 //1K Bytes
#define MAX_SEND_LENGTH 1024
int main(int argc, char* argv[])
{ SOCKET sckServer;
SOCKADDR_IN lpServer;
SOCKADDR_IN lpClient;
WORD wVersionRequested;
char lpSend[MAX_SEND_LENGTH];
char lpGet[MAX_RECV_LENGTH];
WSADATA wsaData;
int ret;
wVersionRequested = MAKEWORD(1, 1);
ret = WSAStartup( wVersionRequested, &wsaData );
if (ret != 0) return 0;
if((LOBYTE( wsaData.wVersion ) != 1)||(HIBYTE( wsaData.wVersion ) != 1))
{ WSACleanup();
return 0;
}
sckServer= socket(AF_INET, SOCK_STREAM, 0); //監聽的套接字
lpServer.sin_addr.S_un.S_addr = htonl(INADDR_ANY);
lpServer.sin_family = AF_INET;
lpServer.sin_port = htons(1987); //using port:1987
if( SOCKET_ERROR == bind(sckServer, (SOCKADDR*)&lpServer, sizeof(SOCKADDR)) )
{ printf("bind error\n");
WSACleanup();
return 0;
}
if( SOCKET_ERROR == listen(sckServer,5) )
{ printf("listen error");
WSACleanup();
return 0;
}
//變量重用是大俠一大風格,不過請勿模仿,最簡單的建議是儘量不對全局變量重用
ret = sizeof(SOCKADDR);
while(1)
{ SOCKET sckReq = accept(sckServer, (SOCKADDR*)&lpClient, &ret);
if(sckReq == INVALID_SOCKET)
{ printf("accept error\n");
return 0;
}
//測試TCP三次握手,註釋下面收發部分
sprintf(lpSend,"%s","what the fuck are you doing?\n");
if( SOCKET_ERROR == send(sckReq,lpSend,strlen(lpSend)+1,0) )
{ printf("send err\n");
return 0;
}
recv(sckReq,lpGet,MAX_RECV_LENGTH,0);
printf("%s\n",lpGet);
closesocket(sckReq);
}
return 1;
}
以下是客戶端,連接請求端的C語言源碼:
#include <winsock2.h>
#include <stdio.h>
#pragma comment(lib,"ws2_32.lib")
#define MAX_RECV_LENGTH 1024 //1K
#define SEND_MAX_LENGTH 1024
int main(int argc, char* argv[])
{ SOCKET sckClient;
SOCKADDR_IN lpAddr;
char lpGet[MAX_RECV_LENGTH];
char lpSend[SEND_MAX_LENGTH];
WORD wVersionRequested;
WSADATA wsaData;
int ret;
wVersionRequested = MAKEWORD(1, 1); //爲了低版本的Windows
ret = WSAStartup(wVersionRequested, &wsaData);
if(ret != 0) return 0;
if(LOBYTE(wsaData.wVersion) != 1 || HIBYTE(wsaData.wVersion) != 1)
{ WSACleanup();
return 0;
}
sckClient = socket(AF_INET,SOCK_STREAM,0);
lpAddr.sin_addr.S_un.S_addr = inet_addr("192.168.0.2"); //connect to host
lpAddr.sin_family = AF_INET;
lpAddr.sin_port = htons(1987); //server port:1987
if( SOCKET_ERROR == connect(sckClient,(SOCKADDR*)&lpAddr,sizeof(SOCKADDR)) )
{ printf("connect error\n");
WSACleanup();
return 0;
}
//測試TCP三次握手,註釋下面收發部分
if(recv(sckClient, lpGet, MAX_RECV_LENGTH, 0) == SOCKET_ERROR)
{ printf("recive error\n");
WSACleanup();
return 0;
}else
{ printf("%s\n", lpGet);
}
strcpy(lpSend, "fucking this fucking code is what the fuck i\'m doing!\n");
send(sckClient, lpSend, sizeof(lpSend) + 1, 0);
closesocket(sckClient);
WSACleanup();
return 1;
}
4.常見協議數據包頭部的C語言定義(包括在抓包源碼中)
這裏列出TCP,UDP,ICMP,IGMP的數據包頭部C語言結構定義,ICMP常見應用是ping命令,IGMP是個危險的協議。也許有人會說,那HTTP協議呢,FTP協議呢,TELNET協議呢?這些都是基於TCP/IP的,也可能使用UDP,但是他們的命令都是ASCII明文,比如HTTP協議的頭部包含在TCP包的數據部分
// ==========================================================
// ANALYSER.H - Struct, Global Variable, Function Definitions
// Coded by http://blog.csdn.net/prsniper
// ==========================================================
#if !defined(__RANGER_ANA_H_)
#define __RANGER_ANA_H_
#if _MSC_VER >= 1000 //Visual Studio 6.0
#pragma once
#endif // _MSC_VER >= 1000
#include "Winsock2.h"
#include <stdio.h>
#define SIO_RCVALL _WSAIOW(IOC_VENDOR, 1)
#pragma comment(lib,"ws2_32.lib")
typedef struct _IPHEADER
{ BYTE cLenVer; //4位首部長度+4位IP版本號
BYTE cServer; //8位服務類型
USHORT sLength; //16位總長度(字節)
USHORT sRepare; //16位標識
USHORT sFlagOffset; //3位標誌位+13段偏移量
BYTE cTimeToLife; //8位生存時間 TTL
BYTE cProtocol; //8位協議 (1=ICMP,2=IGMP,6=TCP,17=UDP等)
USHORT sChecksum; //16位IP首部校驗和
ULONG iSrcAddr; //32位源始IP地址
ULONG iDstAddr; //32位目的IP地址
}IPHEADER;
typedef struct _TCPHEADER
{ USHORT sSrcPort; //16位源端口
USHORT sDstPort; //16位目的端口
ULONG iDataNum; //32位數據序號
ULONG iCheckNum; //32位確認序號
BYTE cHeadLen; //4位首部長度+6位保留字(其中四位)
BYTE cUAPRSF; //6位標誌位
USHORT sWindow; //16位窗口大小
USHORT sPackChecksum; //16位校驗和
USHORT sEmergency; //16位緊急數據偏移量(緊急指針)
}TCPHEADER;
typedef struct _UDPHEADER
{ USHORT sSrcPort; //16位原始端口
USHORT sDstPort; //16位目的端口
USHORT sPackLen; //16位數據包長度
USHORT sChecksum; //16位校驗和,只提示,不強制重發
}UDPHEADER;
typedef struct _ICMPHEADER
{ BYTE cType; //8位類型
BYTE cCode; //8位代碼
USHORT sChecksum; //16位校驗和
USHORT sRecCode; //16位識別號(一般爲進程號)
USHORT sPackNum; //16位報文序列號
ULONG iTimeValue; //32位時間戳,GetTickCount();
}ICMPHEADER;
//IGMP[Internet Group Management Protocol]是IP主機用作向相鄰多目路由器報告多目組成員。
typedef struct _IGMPHEADER //畸形IGMP包會試TCPIP堆棧崩潰,這裏不解析了
{ UCHAR cVerType; //4位版本 4位類型
UCHAR cUnKnow; //未使用
USHORT sChecksum; //16位校驗和
ULONG iGroupAddr; //32位組地址
}IGMPHEADER;
#endif //!defined(__RANGER_ANA_H_)
源碼到此結束,因爲是控制檯窗口,數據也保存到文件,運行效果就不截圖了
5.WinHex分析數據報數據
用WinHex打開保存的文件,如下圖首先,這是IP數據包,第一個字節是4位版本+首部長度,如圖前四位是4,後四位是5,則這個包裹是IPv4,頭長5*32bit=20Byte。第二個字節是服務類型,其值是0x00,第三第四字節是包裹總長,值爲大端模式0x0034=52,可以據此計算包裹爲紅色部分。第五第六字節爲重組標識,值爲大端模式0x4986=18822。第七第八字節爲3位標識和13位段偏移量,這兩個字節(0x4000)的二進制值爲01000000,可以算出該數據報不允許分段,段偏移量爲0。第9字節爲生存時間TTL=0x40=64,第十字節爲協議代碼,值爲0x06=6(TCP)第11,12字節爲頭部校驗和,值爲0x6F77。下面爲雙方IP地址,0xC0A80164是大端模式爲192.168.1.100,0xC0A80002一樣,爲192.168.0.2。也就是從192.168.1.100發向192.168.0.2(我給路由分了幾個網段,呵呵)。因爲頭部長度已經標誌爲5,即沒有選項,那麼IP包頭部到此結束。
然後,TCP頭部部分,前面兩個短整型是始末端口,爲大端模式0x06F9=1785,請求連接的話,本地端口是隨機的;目的端口0x07C3=1987,這個就是服務端的監聽端口。下來是數據序號0x73F5BBBE=1945484222,因爲是手提,一直不關機,合上待機就拿到辦公室,所以已經累計發了很多數據了;確認序號0x00000000,說明兩者還沒有開始傳輸數據。然後看偏移,值爲80,前四位是8=1000,也就是數據距離包頭8*32=32Byte(32正好是這個字節到IP包頭的偏移?!),後四位保留爲0。下一個字節爲0x02=00000010,前兩位保留爲0,後六位分別對應:UAPRSF,得出SYN=1,這是一個請求或者接受請求報文!窗口字段0x4000=16384,這個不管它。包校驗和0x6DF1,緊急指針0x0000,因爲URG位已經爲0,即使緊急指針不爲0也無效。
後面的數據究竟是什麼意義,我還不太清楚,不過對TCP三次握手的說明,應該沒有影響了。
下面看第二個包包,類似的我就不說了,它是從192.168.0.2發向192.168.1.100的,數據序號0x73F5BBBF,正好是請求包的數據序號+1;確認序號0xD4CF3750;標誌0x10=00010000,所以ACK=1,說明確認號有效,這是服務端給請求方發的“同意連接”確認包。
再下面第三個包是綠色部分,一樣道理分析之,我這裏就不寫這麼多了。時間有限,偷懶一下。另一方面來說,我認爲我表達的不是很通俗。這裏有價值之處在於C/C++的源代碼,大家對fnAnalyse()函數稍作修改就可以做全自動的協議分析,這纔是本文的核心,也是它的價值所在。
可能是使用了代碼的原因,不同形式的語言或者說表達方式很難融合到一起,熟悉C/C++的朋友(Java跟C++很像,簡直就是C++生的)就好理解多了。
好啦,就這麼多吧,下面發下使用編譯出來的控制檯的方法(可以直接雙擊運行,或者在VS6 IDE運行),我用的是命令行:
- :\>cd /D F:\
- F:\>cd codes
- F:\Codes>cd "Visual C++"
- F:\Codes\Visual C++>cd PackAnalyser
- F:\Codes\Visual C++\PackAnalyser>cd..
- F:\Codes\Visual C++>cd tcpClient
- F:\Codes\Visual C++\tcpClient>cd debug
- F:\Codes\Visual C++\tcpClient\Debug>dir
- 驅動器 F 中的卷是 FILES
- 卷的序列號是 0005-DD75
- F:\Codes\Visual C++\tcpClient\Debug 的目錄
- 2011-09-15 10:37 <DIR> .
- 2011-09-15 10:37 <DIR> ..
- 2011-09-15 13:45 12,268 tcp.obj
- 2011-09-15 13:45 155,714 tcpClient.exe
- 2011-09-15 13:45 176,624 tcpClient.ilk
- 2011-09-15 10:37 2,863,224 tcpClient.pch
- 2011-09-15 13:45 410,624 tcpClient.pdb
- 2011-09-15 13:49 41,984 vc60.idb
- 2011-09-15 13:45 69,632 vc60.pdb
- 7 個文件 3,730,070 字節
- 2 個目錄 61,248,696,320 可用字節
- F:\Codes\Visual C++\tcpClient\Debug>tcpclient.exe
- what the fuck are you doing?
我放在這個目錄,呵呵。大家編譯時候不要忘記更改IP哦,不然又有人來這裏發牢騷說源碼不對了……