网络编程基础
一、关于语言、编译器及系统
我们知道,在计算机里面进行程序设计至少需要掌握一种程序设计语言。常见的程序设计语言包括 C/C++、ASM、BASIC、Delphi、Perl等,你随便根据兴趣去学习一种语言都可以实现程序设计这个目的,这些语言并没有谁比谁更好的问题 (这是一个理解问题,实际上一些语言应该说比一些语言更先进,比如可以说C++比C更先进,但不能说谁比谁好,每一种语言都是优秀思想的结晶,我这样理 解),只是它们的侧重点不同。比如,ASM、C比其他语言更接近系统的底层,使得它们适合做系统方面的程序设计、而用BASIC可以更快的设计应用程序 等。作为网络程序设计我更喜欢C/C++语言,它有目标程序较小、运行速度快等优点,还包括习惯问题(所以,下面的描述中如果不特别说明,都以C/C++为例),当然选择什么语言进行程序设计是见仁见智的问题(你决定选择C/C++了吗?如果决定了,下面的的描述会更有针对性)。
我们选择好一种语言后,并编写好了程序代码,这么样才能让系统运行我们的程序呢?这就需要编译器了,编译器的作用就是把类似下面的源代码编译成系统可以识别并执行的代码:
#include <stdlib.h>
int main()
{
printf(“hello word.\r\n”);
return 0;
}
所 有的语言都有自己的编译器,C语言的编译器有TC、BC等,C++语言的编译器有Visual C++、C++ Builder等。C和C++语言的关系 是很密切的,C++语言是C语言的超集,而因为向下的兼容性,所以,如果你写的C源代码符合ANSI C标准,那么其在C和C++的编译器里面都可以编 译,相反,C++的源代码只能在C++的编译器中编译。
那么编译器除了可以把源代码编译成可执行代码还有其他的作用吗?答案是有,一般的情况下,编译器都提供一个集成的环境给设计这编写、编译程序,还包括一系列的函数库,比如上面例子中的prinrf就是stdlib.h提供的一个库函数。在涉及到网络程序设计中,常听到Winsock、Winapi等说法,而这些也是编译器提供的一套库函数,而这些库函数有一些编译器是不提供的,如TC、BC等,所以这些编译器不可以你作为网络程序设计的编译器,当然,这些编译器不适合作为网络程序设计的编译器还有其他重要的原因。
常看见下面这样的问题:
这个程序我用TC怎么编译不了?
可以不可以用TC编写网络程序?
…
现 在告诉你,不可以,除了上面的原因,更重要的原因是我们现在程序所运行的平台是Windows(当然有其他的,但情况是类似的),而Winsock、 Winapi本身是由系统(Windows)提供给我们的接口,编译器是帮助我们使用这些接口的桥梁,而TC等编译器本身没有这个功能,所以不能使用它们 来帮助我们编写网络程序。还有,TC等编译器是为DOS等16位操作系统设计的,已经不能够适用我们现在32位的操作系统环境了。所以,我推荐Visual C++、C++ Builder或其他32位C++编译器。
Visual C++、 C++ Builder或其他的32位C++编译器谁更好呢?这也是见仁见智的问题,常见到有些报道说现在的C++ Builder比 Visual C++要好,这两个编译器不断的在竞争、攀比。我选择Visual C++,并不是说它比C++ Builder好,而是因为它编写的可执 行代码(目标文件)比较小,这也不是说它比C++ Builder好,而是因为它是Microsoft出品的,而Windows也是Microsoft出 品的,在Windows的发行版本中本身就包含了很多运行库,这样Visual C++的目标代码比较小。
二、关于代理
一般做坏事的时候都害怕对方发现自己的真实IP,怎么办?用代理是简单的办法。下面描述如何编程使用各种代理。
1、HTTP代理
HTTP代理可以把我们的HTTP请求通过HTTP代理服务器转发到我们要访问的HTTP服务器,再把结果返回给我们,以达到代理的目的。但其功能单一,只能实现HTTP的代理,具体可以查看RFC 2068、2616等相关RFC文档。
正常情况下,我们请求HTTP服务是这样的:首先和目的服务器的HTTP服务端口建立TCP连接,然后做类似“GET /index.html HTTP/1.0”的请求,HTTP服务器返回结果。当通过HTTP代理的时候是这样工作的:首先和HTTP代理服务器的服务端口建立TCP连接,然后做类似“GET http://目标服务器地址/index.htm HTTP/1.0”的请求,代理服务器对你的目标服务器做请求后返回结果给你。
相关的代码在网上很容易可以找到,这里就不列举了。
2、socks代理
socks是一个简单灵活的协议框架,包括4和5两个版本,sock5是由IETF核准的基于TCP/IP协议的基本应用程序代理协议,socks由两个部分组成,服务端和客户端。具体信息可以查看RFC 1928相关文档,在网上也可以搜索到许多基于socks5的开源项目,对照RFC文档,你可以了解这个协议的使用。
『以下信息来直接摘自互联网』
sock5代理客户端的工作程序是:
1.客户端向代理方服务器发出请求信息。
2.代理方服务器应答
3.客户端接到应答后发送向代理方服务器发送目的ip和端口
4.代理方服务器与目的连接
5.代理方服务器将客户端发出的信息传到目的方,将目的方发出的信息传到客户端。代理完成。
由于网上的信息传输基本上都是运用tcp或udp进行的,所以使用socks5代理可以办到网上所能办到的一切,而且不用担心目的方会查到你的ip,既安全又方便。
如何用代理TCP协议:
1.向服务器的1080端口建立tcp连接。
2.向服务器发送 05 01 00 (此为16进制码,以下同)
3.如果接到 05 00 则是可以代理
4. 发送 05 01 00 01 + 目的地址(4字节) + 目的端口(2字节),目的地址和端口都是16进制码(不是字符串)。 例 202.103.190.27 - 7201 则发送的信息 为:05 01 00 01 CA 67 BE 1B 1C 21 (CA=202 67=103 BE=190 1B=27 1C21=7201)
5.接受服务器返回的自身地址和端口,连接完成
6.以后操作和直接与目的方进行TCP连接相同。
如何用代理UDP连接
1.向服务器的1080端口建立udp连接
2.向服务器发送 05 01 00
3.如果接到 05 00 则是可以代理
4.发送 05 03 00 01 00 00 00 00 + 本地UDP端口(2字节)
5.服务器返回 05 00 00 01 +服务器地址+端口
6.需要申请方发送 00 00 00 01 +目的地址IP(4字节)+目的端口 +所要发送的信息
7.当有数据报返回时 向需要代理方发出00 00 00 01 +来源地址IP(4字节)+来源端口 +接受的信息
注:此为不需要密码的代理协议,只是socks5的一部分,完整协议请看RFC1928
下面为一个实例程序:
这个例子可以在这里找到(http://cache.baidu.com/c?word=%B4%FA%C0%ED%3B%B7%FE%CE%F1%C6%F7%2Csocks4%2Csocks5%2Chttp%3B%B4%FA%C0%ED%2C%B1%E0%B3%CC&url=http%3A//www%2Eade%2Ddesign%2Ecom/docfile/netandcomm/chap37%2Ehtm&b=9&user=baidu)
在网络程序设计过程中,我们经常要与各种类型的代理服务器打交道,比如在企业内部网通过代理去访问Internet网上的服务器等等,一般代理服务器支持几种常见的代理协议标准,如Socks4,Socks5,Http代理,其中Socks5需要用户验证,代理相对复杂。我在查阅RFC文档和相关资料后,特总结一些TCP协议穿透代理服务器的程序片断,希望对大家有所帮助。
//使用到的结构
struct sock4req1
{
char VN;
char CD;
unsigned short Port;
unsigned long IPAddr;
char other[1];
};
struct sock4ans1
{
char VN;
char CD;
};
struct sock5req1
{
char Ver;
char nMethods;
char Methods[255];
};
struct sock5ans1
{
char Ver;
char Method;
};
struct sock5req2
{
char Ver;
char Cmd;
char Rsv;
char Atyp;
char other[1];
};
struct sock5ans2
{
char Ver;
char Rep;
char Rsv;
char Atyp;
char other[1];
};
struct authreq
{
char Ver;
char Ulen;
char Name[255];
char PLen;
char Pass[255];
};
struct authans
{
char Ver;
char Status;
};
//通过Socks4方式代理
if( !ClientSock.Connect( g_ProxyInfo.m_strProxyIP,g_ProxyInfo.m_nProxyPort) )
{
m_sError = _T("不能连接到代理服务器!");
ClientSock.Close();
return FALSE;
}
char buff[100];
memset(buff,0,100);
struct sock4req1 *m_proxyreq;
m_proxyreq = (struct sock4req1 *)buff;
m_proxyreq->VN = 4;
m_proxyreq->CD = 1;
m_proxyreq->Port = ntohs(GetPort());
m_proxyreq->IPAddr = inet_addr(GetServerHostName());
ClientSock.Send(buff,9);
struct sock4ans1 *m_proxyans;
m_proxyans = (struct sock4ans1 *)buff;
memset(buff,0,100);
ClientSock.Receive(buff,100);
if(m_proxyans->VN != 0 || m_proxyans->CD != 90)
{
m_sError = _T("通过代理连接主站不成功!");
ClientSock.Close();
return FALSE;
}
//通过Socks5方式代理
if( !ClientSock.Connect( g_ProxyInfo.m_strProxyIP,g_ProxyInfo.m_nProxyPort) )
{
m_sError = _T("不能连接到代理服务器!");
ClientSock.Close();
return FALSE;
}
char buff[600];
struct sock5req1 *m_proxyreq1;
m_proxyreq1 = (struct sock5req1 *)buff;
m_proxyreq1->Ver = 5;
m_proxyreq1->nMethods = 2;
m_proxyreq1->Methods[0] = 0;
m_proxyreq1->Methods[1] = 2;
ClientSock.Send(buff,4);
struct sock5ans1 *m_proxyans1;
m_proxyans1 = (struct sock5ans1 *)buff;
memset(buff,0,600);
ClientSock.Receive(buff,600);
if(m_proxyans1->Ver != 5 || (m_proxyans1->Method!=0 && m_proxyans1->Method!=2))
{
m_sError = _T("通过代理连接主站不成功!");
ClientSock.Close();
return FALSE;
}
if(m_proxyans1->Method == 2)
{
int nUserLen = strlen(g_ProxyInfo.m_strProxyUser);
int nPassLen = strlen(g_ProxyInfo.m_strProxyPass);
struct authreq *m_authreq;
m_authreq = (struct authreq *)buff;
m_authreq->Ver = 1;
m_authreq->Ulen = nUserLen;
strcpy(m_authreq->Name,g_ProxyInfo.m_strProxyUser);
m_authreq->PLen = nPassLen;
strcpy(m_authreq->Pass,g_ProxyInfo.m_strProxyPass);
ClientSock.Send(buff,513);
struct authans *m_authans;
m_authans = (struct authans *)buff;
memset(buff,0,600);
ClientSock.Receive(buff,600);
if(m_authans->Ver != 1 || m_authans->Status != 0)
{
m_sError = _T("代理服务器用户验证不成功!");
ClientSock.Close();
return FALSE;
}
}
struct sock5req2 *m_proxyreq2;
m_proxyreq2 = (struct sock5req2 *)buff;
m_proxyreq2->Ver = 5;
m_proxyreq2->Cmd = 1;
m_proxyreq2->Rsv = 0;
m_proxyreq2->Atyp = 1;
unsigned long tmpLong = inet_addr(GetServerHostName());
unsigned short port = ntohs(GetPort());
memcpy(m_proxyreq2->other,&tmpLong,4);
memcpy(m_proxyreq2->other+4,&port,2);
ClientSock.Send(buff,sizeof(struct sock5req2)+5);
struct sock5ans2 *m_proxyans2;
memset(buff,0,600);
m_proxyans2 = (struct sock5ans2 *)buff;
ClientSock.Receive(buff,600);
if(m_proxyans2->Ver != 5 || m_proxyans2->Rep != 0)
{
m_sError = _T("通过代理连接主站不成功!");
ClientSock.Close();
return FALSE;
}
//通过HTTP方式代理
if( !ClientSock.Connect( g_ProxyInfo.m_strProxyIP,g_ProxyInfo.m_nProxyPort) )
{
m_sError = _T("不能连接到代理服务器!");
ClientSock.Close();
return FALSE;
}
char buff[600];
sprintf( buff, "%s%s:%d%s","CONNECT ",GetServerHostName(),GetPort()," HTTP/1.1\r\nUser-Agent: MyApp/0.1\r\n\r\n");
ClientSock.Send(buff,strlen(buff)); //发送请求
memset(buff,0,600);
ClientSock.Receive(buff,600);
if(strstr(buff, "HTTP/1.0 200 Connection established") == NULL) //连接不成功
{
m_sError = _T("通过代理连接主站不成功!");
ClientSock.Close();
return FALSE;
}
我们一般先与代理服务器连通,然后向代理服务器发送代理验证的用户名和密码(如果需要,如Socks5代理),验证成功后,再向代理服务器发送需要连接的目的地址和端口。以上代码仅用于TCP连接,如果在内部网侦听或通过UDP协议发送信息,可查阅RFC1928等文档资料。
3、加密代理
这个吗啥都可以代理,而且是加密的,安全的,常用openssl来架设加密代理服务器,你可以去http://www.openssl.org(这是一个开源的项目)去了解详细信息,就不要自己编写了,工程太大,用现成的三、关于编译
拿到源代码以后如何编译?出现错误如何处理?
在Windows下,如果你拿到的源码包解压后(一般要带目录解压)首先看有没有readme文件,如果这些源码是一个VC的项目,那么readme文件中会包含一个文件列表及文件的功能及一些说明性的文字(这个文件是由VC自动建立的),那么你应该查找有没有一个dsw后缀的文件,这个文件是VC的工作区文件,双击这个文件,VC会自动启 动并打开这个文件,一般情况下是可以直接编译通过的;如果没有这个文件,那么看是不是有一个dsp后缀的文件,这个文件是VC的项目文件,同dsw文件的 打开方式,在VC下编译的时候会问你是不是建立工作区,选择是后也是可以正确编译的;如果这些源码不是由VC建立的项目,那么这个readme文件中会包 含这些源码的编译方式及编译环境,看好,是不是可以在windows下编译,怎么编译。一般情况下,如果我们找到这些说明文件,那么源码是可以很好的编译 的;如果没有找到任何说明性的文件,那就比较麻烦了,是不是非编译不可?如果不是那就算了,找个好编译的。 ^_^
在windows下,源码包一般是zip、rar后缀的,如果是gz、tar后缀的,一般情况下不是windows下的源码,大多数情况下,一个完整的源码包是可以很简单的编译成功的。
经 常有人问exploit如何编译,比如xfocus就有很多exploit,有独立的也有在文章里面贴出来的,一般就是一段代码,遇到这样的情况先把代码 复制下来,存成c后缀的文件(这个很重要,编译器在编译的时候会安装源码文件的后缀区分你是用什么语言编写的而编译,C++源码文件的后缀是 cpp,C++的检查项目要比c严谨的多,而exploit大多是用c编写的,如果后缀不对,有可能编译不了)。然后打开这个c文件,看看开始的说明有没 有编译方式,一般好的作者会写上编译的环境及其方法的,比如在vc的编译环境下用cl example.c编译或者在linux下用等 gcc example example.c(gcc是linux下的C/C++编译器)等,那么我们运行vc目录下bin目录的 VCVARS32.BAT文件(vc控制台环境设置文件,vc安装的时候会设置这些环境,并在系统启动的时候生效,如果你安装的时候没有选择,那么才需要 运行这个文件),然后照说明里面的方法编译就可以了;如果没有说明如何编译,那么我们先区分一下这个代码是在什么环境下编译的,如果包括 windows.h或者winsock.h这些头文件,那肯定在windows下是可以编译的,如果不包括,那么就没准是linux下的了。:)如果是 windows下可以编译的代码,那么用vc打开这个c文件,直接编译好了,当然有可能出错误,如果出现编译(Compiling)错误,那就是代码的写 法上面有问题,要具体情况具体分析,根据知识自己改,但一般编译exploit的时候很少出现编译错误,经常会出现连接(Linking)错误,比如下面 这样:
error LNK2001: unresolved external symbol
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~
连接错误号 错误原因 函数名
是说编译器不能连接recv这个函数,不知道从那个lib文件中获得函数的详细信息,解决的办法是在msdn中查找recv这个函数,函数的说明文件会有下面的文字:
Requirements
Windows NT/2000/XP: Included in Windows NT 3.1 and later.
Windows 95/98/Me: Included in Windows 95 and later.
Header: Declared in Winsock2.h.
Library: Use Ws2_32.lib.
~~~~~~~~~~~~~~~~~~~~~~~
注意这句话
查 到后,只要在代码的开始加如下面语句:#pragma comment(lib,"ws2_32")就可以了,也可以在vc中选择 Project->Settings->Link->Object/library Modules中添加这个lib文件也可以。当然 我只是举recv这个函数,别的函数可以同样处理。
如果遇到别的连接错误,可以在msdn中搜索这个错误信息(搜索关键字不要包含函数名称)。比 如上面那个错误可以搜索“error LNK2001: unresolved external symbol”,可以在msdn中找到错误的原因及其 解决办法。如果是编译错误,呵呵,只能靠你自己了。
四、关于TCP/IP数据包的截取和分析
TCP/IP数据包的截取是一个简单的工作,在Windows 2000/xp下,下面方法可以完成TCP/IP数据包的截获:
1、通过建立rawsocket来完成对TCP/IP数据包的截获。从Windows 2000开始,Winsock 2开始支持原始socket,可以 截获所有经过本机的TCP/IP数据包,支持拨号网络,但对本机向外发送的TCP/IP数据包截获有缺陷。可以从http://www.codeguru.com/Cpp/I-N/network/tcpip/article.php/c5413得到更加详细的信息及演示代码,现在很多基于rawsocket的代码都是参照这篇文章完成的。
2、通过WinPcap Developer's pack来完成对TCP/IP数据包的截获。这个开发包可以很好的在windows 9x/nt/2000/xp下工作,不支持拨号网络。可以从http://winpcap.polito.it/获得更详细的信息和用户手册,在本版的精华版也包括一个介绍这个开发包的文章可以参照。
Windows下对TCP/IP数据包的截获大多通过这两个方法来完成。如果你需要编写一个轻量级的TCP/IP数据包的截取和分析工具,而且只在Windows 2000/xp下工作,你可以选择第一种方法,比较简单,而且不需要另外的驱动;否则,你需要选择第二种方法或其他方法。
在类UNIX系统(比如linux)下,通常使用libpcap(http://www.tcpdump.org/)完成TCP/IP数据包的截取工作,它可以工作在多种操作系统下。
可以从互联网上面搜索到很多Windows下的TCP/IP数据包截取的演示代码,还可以搜索到很多linux下的开源项目,甚至功能完备复杂的大型TCP/IP数据包截取项目。
TCP/IP数据包的分析就比较困难和麻烦了。首先这种分析是基于对TCP/IP协议族的理解之上的,如果你还不了解,《TCP-IP详解卷1:协议》是首先需要翻阅的书籍,如果懒的看,那也不要提什么分析了,呵呵。
截取了一个TCP/IP数据包后,首先分离出IP协议(IP“Internet Protocol”协议是TCP/IP协议族中最为核心的协议,所有的TCP、UDP、ICMP和IGMP数据等都是以IP数据报格式传输的)的头部分,从IP协议头中可以得到很多关键的数据,如IP头的长度、源IP、目的IP、TCP/IP协议类型等,下面演示代码实现这个功能:
typedef struct _IP_HEADER //定义IP首部
{
unsigned char h_lenver; //4位首部长度+4位IP版本号
unsigned char tos; //8位服务类型TOS
unsigned short total_len; //16位总长度(字节)
unsigned short ident; //16位标识
unsigned short frag_and_flags; //3位标志位+13位片偏移
unsigned char ttl; //8位生存时间 TTL
unsigned char proto; //8位协议 (TCP, UDP 或其他)
unsigned short checksum; //16位IP首部校验和
unsigned int sourceIP; //32位源IP地址
unsigned int destIP; //32位目的IP地址
} IP_HEADER;
typedef struct _TCP_HEADER //定义TCP首部
{
USHORT th_sport; //16位源端口
USHORT th_dport; //16位目的端口
UINT th_seq; //32位序列号
UINT th_ack; //32位确认号
UCHAR th_lenres; //4位首部长度/6位保留字
UCHAR th_flag; //6位标志位
USHORT th_win; //16位窗口大小
USHORT th_sum; //16位校验和
USHORT th_urp; //16位紧急数据偏移量
} TCP_HEADER;
unsigned short DecodeIPHeader(char *buf) //IP头解码函数 (得到IP头的长度,其他内容得到方法类似)
{
IP_HEADER * ipheader;
unsigned short ipheaderlen;
ipheader = (IP_HEADER *)buf;
ipheaderlen = sizeof(unsigned long) * (ipheader->h_lenver & 0xf);
return ipheaderlen;
}
好,现在我们获得了IP头的长度,用类似的方法也可以获得了协议类型等其他数据。假设协议类型是TCP,那么接着就可以从数据包中分离出TCP协议(TCP协议是可靠的端到端协议)头的数据,从TCP协议头中得到很多关键的数据,如源端口、目的端口、数据偏移、控制位等。从目的端口中我们可以简单的(但不一定准确)判断一下跟着的数据是什么协议,如端口是80那么是HTTP协议、端口是21是FTP等(HTTP、FTP等协议都是基于TCP协议实现的)。
unsigned short DecodeIPHeader(char *buf) //TCP头解码函数 (得到目的端口,其他内容得到方法类似)
{
TCP_HEADER * tcpheader;
unsigned short ipheaderlen;
ipheaderlen = DecodeIPHeader(buf);
tcpheader = (TCP_HEADER *)(buf + ipheaderlen);
return tcpheader->th_dport;
}
通过类似的方法,得到所有需要的数据。一个正常的TCP连接总是由三次握手开始的,也就是说开始的三个数据包它的数据偏移是0,通过控制位可以知道三次握手的完成状态。接着就是分析接下来的数据了,接下来的数据就有很多变化了,一般情况下针对每一种协议都要写解码函数,可以通过端口和数据的一些标志判断数据包是什么协议的数据包,比如SMB协议开始会有0xff加SMB的标志,当然,这首先一个条件是协议是已知并且公开的,如果一个协议是不公开的,那就需要对使用这个协议的网络应用程序进行分析以确定协议的结构并写出相应的解码函数,这就是更复杂和麻烦的工作了,需要的知识也就更多,就不是本文要解决的问题了。
关于各种协议的解码函数在Windows下我是没有找到公开的源码,但如果有时间和耐心,可以参考下面linux下的开源项目,对编写自己的协议解码还是可能有所帮助。如:
http://www.cpan.org/authors/id/T/TI/TIMPOTTER/NetPacket-0.03.tar.gz
用于基本的IP/TCP/UDP等包解码的模块,剥除各种协议头,抽取各个字段。
http://www.ethereal.com/download.html
Ethereal是一个开源项目,功能强大的数据截取分析工具。