通常情况下我们要判断目标机器的指定端口是否开放 可以直接通过用Socket去连接看看是否能够连接上 但是直接TCP协议的Socket去连接的话 是建立的一个全连接 比较耗费资源而且还有timeout之类的等待 如果端口数目并不是很多可以通过TCP直接连接 但是如果端口数目很多的情况下这样的方式就有点不尽人意了 而如果此时使用无状态的方式去扫描的话效果就不一样了
所谓无状态就是指无需关心TCP状态 不占用系统TCP/IP协议栈资源 忘记syn、ack、fin、timewait 不进行会话组包 通过自己来构造TCP协议的数据包来模拟TCP建立连接的过程 自己模拟和直接使用Socket建立TCP连接有什么区别就慢慢来说明 先从TCP协议开始说起 先来看看TCP是怎么建立连接的
telnet 8.8.8.8 53上面是我通过WireShark抓取的一个我telnet谷歌DNS服务器的一组数据包 上面蓝色框里面的是建立一个TCP的过程 然后由于我连接上了DNS服务器却一段时间内没有发送数据过去谷歌那边主动把我断开了 而下面红色框里面就是断开一个连接的过程 先来看看TCP是如何建立连接的
上面是一张TCP连接连接时候的一个过程图 需要三步握手连接才算建立完成 而UDP协议则没有三步握手的过程 为什么需要三步而不是其他步 可以把上面的过程想象成一个通话的过程
A->B(SYN) :喂 你能听到吗 B->A(SYN+ACK) :嗯 我能听到 你能听到吗 A->B(ACK) :嗯 可以听到你通过这三步那么AB双方都能确认自己能够听到对方而且对方也能听到自己 那就表明这个连接比较稳定了 可能建立了 之后就是数据的正常传输了每发送一个数据包对方回应一个ACK来确认收到的数据包 通过这样的方式来传输数据就相对比较可靠了 所以说TCP是可靠的一个传输协议 而UDP则不是可靠传输 因为UDP不需要三步握手也不需要向对方确认是否收到数据
建立连接的时候需要三步握手 而关闭连接则需要四次挥手
上面是一个TCP断开连接的一个过程 同样为什么是四次 依然想象成一个通话的过程
A->B(FIN+ACK) :还有什么要说的吗 我要挂电话了 B->A(ACK) :没什么了 B->A(FIN+ACK) :那挂电话咯 A->B(ACK) :好的如果在双方都没有什么数据传输的情况下 那么就是上面的一个过程 如果A请求断开连接 可能B还有数据没有发送完毕 则就不会回应ACK而是继续发送数据 然后再断开连接 过程大致为先由请求方发送一个FIN+ACK来请求关闭 然后被请求发发送最后的FIN+ACK来确认关闭(因为被请求方可能还有数据需要发送 所以需要被请求方来做最终关闭)
我发现有些系统并没有按照四次挥手来 根据抓包情况来看有一些是三次挥手 比如我用下面的代码来连接谷歌DNS服务器 连接后然后主动关闭连接的一段代码
Socket sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); sock.Connect(new IPEndPoint(IPAddress.Parse("8.8.8.8"), 53)); sock.Close();可抓包数据是这样的
意思就是变成了这样
A->B(FIN+ACK) :还有什么要说的吗 我要挂电话了 B->A(FIN+ACK) :那挂电话吧 A->B(ACK) :好的就比较霸道了 我姑且理解为节省资源吧 对方直接省一步ACK
通过上面 可以看到如果通过Socket直接使用TCP去连接一个端口(如果开放) 那么就会产生以上以上数据包 然而实际上通过上面第二个包如果对方回应的是SYN+ACK就基本可以确定对方是开放了端口的 而下面的数据包都是多余的 所以说如果这个数据包能够自己来构造那么 就可以不用发送那么多数据包了 问题的关键来了 如何自己去模拟一个数据包
通常使用Socket的时候用的最多的就是TCP或者UDP写了而他们的创建方式通常是这样的:
Socket sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); Socket sock = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);但是现在不能这样创建Socket了 因为这样创建的套接子是上层应用 我们需要更为底层一点的套接子
//创建原始套接子 Socket sock = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.IP); //设置套接子 使其可以自己构造数据头 sock.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.HeaderIncluded, true);注:原始套接子在windows系统上有使用限制 从XP SP2开始貌似只能使用发送UDP报文 而WIN7上完全禁止使用 当然可以使用地第三方的方式来发送原始数据包比如WinpCap 只是说原生的Socket函数已经被阉割了 但是Sever版本系统还可以使用(只在2003/2008上测试过) 所以之后出现的代码 仅在Server版本系统上使用
虽说上面的代码是C#的 但是Socket函数到每种语言上都是差不多的 只是语法上有点区别
通过上面创建出来的Socket就不像平时发送TCP或者UDP那样只管发送数据就行了 而需要自己构造整个IP层的数据包
先不说自己怎么构造 先来做一个测试 把我Telnet谷歌DNS服务器的那个SYN数据包复制出来自己再发送一个一模一样的试试
上面蓝色方框里面是IP协议报文头 而红色框里面的是TCP协议报文头 现在我把IP和TCP报文头复制出来 然后使用原始套接子再发送一次效果如下
代码如下
private void button1_Click(object sender, EventArgs e) { byte[] bySyn = new byte[]{ 0x45,0x00 ,0x00,0x30,0x74,0x88,0x40,0x00,0x80,0x06,0xb5,0x10,0xc0,0xa8,0x00,0x77,0x08,0x08 ,0x08,0x08,0x05,0xa5,0x00,0x35,0x92,0x67,0x39,0x08,0x00,0x00,0x00,0x00,0x70,0x02 ,0xfa,0xf0,0xe5,0xb5,0x00,0x00,0x02,0x04,0x05,0xb4,0x01,0x01,0x04,0x02 }; Socket sock_raw = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.IP); sock_raw.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.HeaderIncluded, true); sock_raw.SendTo(bySyn, new IPEndPoint(IPAddress.Parse("8.8.8.8"), 53)); }可以看到谷歌DNS服务器正确的返回了SYN+ACK数据包 而我的系统则发送了一次RST数据包 因为当系统接受到这个SYN+ACK数据包的时候 认为是一个错误的包 因为系统觉得自己并没有和8.8.8.8建立链接 认为对方发送错了所以发送一个RST拒绝 因为那个SYN是我自己构造的数据包发送出去的而没有经过系统的TCP协议栈 不过这个对我们来说已经没有关系了 因为第二个数据包才是我们关心的重点(SendTo函数的第二个参数EndPoint的值将会被忽略 因为sock_raw发送的不再是应用层数据了 还包含了包头数据 所以系统不会给我们构造包头数据了 在bySyn中包含了IP和端口信息)
上面的bySyn稍微格式化一下差不多就是这样的
byte[] bySyn = new byte[]{ //IP协议头 0x45 //IP版本号码4 首部长度5(5个32Bit 20字节) ,0x00 //TOS服务类型 ,0x00,0x30 //数据总长度 48字节 ,0x74,0x88 //标识 ,0x40,0x00 //3位标志+13位片偏移 ,0x80 //ttl = 128 ,0x06 //使用TCP协议 ,0xb5,0x10 //数据校验和 虚拟机里面出现过校验和是0的情况 但是包没被对方丢弃 ,0xc0,0xa8,0x00,0x77 //来源端口 192.168.0.119 ,0x08,0x08,0x08,0x08 //目标端口 8.8.8.8 //TCP协议头 ,0x05,0xa5 //来源端口 1445 ,0x00,0x35 //目标端口 53 ,0x92,0x67,0x39,0x08 //seq顺序号 ,0x00,0x00,0x00,0x00 //ack确认号 ,0x70 //首部长度7(7个32Bit 28字节) ,0x02 //SYN ,0xfa,0xf0 //窗口大小 ,0xe5,0xb5 //检验和 虚拟机里面出现过校验和是0的情况 但是包没被对方丢弃 ,0x00,0x00 //紧急指针 ,0x02,0x04,0x05,0xb4,0x01,0x01,0x04,0x02 //可选字段 };
那么问题来了 要怎么样构造一个上面一样的数据包呢 可以看我之前写的一篇文章点击此处 里面有关于IP TCP UDP协议的报文头说明 可以将上面的数据与机构对照观看即可明白
然后下面来定义一个类来自己构造SYN数据包
public class TcpDefine { private static Random m_rnd = new Random(); public const int IPPROTO_TCP = 6; public enum Flag { URG = 32, ACK = 16, PSH = 8, RST = 4, SYN = 2, FIN = 1 } //定义TCP头 public struct IP_HEADER { public byte h_verlen; //4位IP版本号 4位首部长度 public byte tos; //8位服务类型TOS public ushort total_len; //16位总长度(字节) public ushort ident; //16位标 public ushort frag_and_flags; //3位标志 public byte ttl; //8位生存时间 TTL public byte proto; //8位协议(TCP,UDP等) public ushort checksum; //16位IP首部校验和 public uint sourceIP; //32位来源IP地址 public uint destIP; //32位目的IP地址 } //定义伪首部 public struct PSD_HEADER { public uint saddr; //源地址 public uint daddr; //目的地址 public byte mbz; public byte ptcl; //协议类型 public ushort length; //上层协议数据长度 } //定义TCP头 public struct TCP_HEADER { public ushort th_sport; //16位来源端口 public ushort th_dport; //16位目标端口 public int th_seq; //32位顺序号 public int th_ack; //32位确认号 public byte th_lenres; //4位首部长度/6位保留字 public byte th_flag; //6位标志位 public ushort th_win; //16位窗口大小 public ushort th_sum; //16位校验和 public ushort th_urp; //16位紧急数据偏移量 } //字节反转 public static ushort Reverse(ushort num) { return (ushort)((num << 8) | (num >> 8)); } //反转字节 public static int Reverse(int num) { int temp = (num << 24); temp |= (num << 8) & 0x00FF0000; temp |= (num >> 8) & 0x0000FF00; temp |= (num >> 24) & 0x000000FF; return temp; } public static uint IPToINT(string strIP) { string[] strs = strIP.Split('.'); uint num = 0; num = byte.Parse(strs[3]); num <<= 8; num |= byte.Parse(strs[2]); num <<= 8; num |= byte.Parse(strs[1]); num <<= 8; num |= byte.Parse(strs[0]); return num; } public static ushort CheckSum(byte[] byData, int size) { ulong cksum = 0; int index = 0; while (size > 1) { cksum += BitConverter.ToUInt16(byData, index); index += 2; size -= 2; } if (size == 1) { cksum += byData[index - 1]; } cksum = (cksum >> 16) + (cksum & 0xFFFF); cksum += (cksum >> 16); return (ushort)(~cksum); } public static TcpDefine.IP_HEADER GetIPHeader(string strSIP, string strDIP) { TcpDefine.IP_HEADER ip_header = new TcpDefine.IP_HEADER(); ip_header.h_verlen = (byte)(4 << 4 | Marshal.SizeOf(ip_header) / 4); ip_header.tos = 0; ip_header.total_len = TcpDefine.Reverse((ushort)20); ip_header.ident = 1; ip_header.frag_and_flags = 0; ip_header.ttl = 128; ip_header.proto = TcpDefine.IPPROTO_TCP; ip_header.checksum = 0; ip_header.sourceIP = TcpDefine.IPToINT(strSIP); ip_header.destIP = TcpDefine.IPToINT(strDIP); return ip_header; } public static TcpDefine.TCP_HEADER GetTcpHeader(ushort usSPort, ushort usDPort, int seq, int ack, Flag flag) { TcpDefine.TCP_HEADER tcp_header = new TcpDefine.TCP_HEADER(); tcp_header.th_sport = TcpDefine.Reverse(usSPort); tcp_header.th_dport = TcpDefine.Reverse(usDPort); tcp_header.th_seq = TcpDefine.Reverse(seq); tcp_header.th_ack = TcpDefine.Reverse(ack); tcp_header.th_lenres = (byte)(Marshal.SizeOf(tcp_header) / 4 << 4 | 0); tcp_header.th_flag = (byte)flag; tcp_header.th_win = TcpDefine.Reverse((ushort)16384); tcp_header.th_urp = 0; tcp_header.th_sum = 0; return tcp_header; } /// <summary> /// 构造目标IP和端口的SYN数据包 /// </summary> /// <param name="strSIP">来源IP地址</param> /// <param name="strDIP">目标IP地址</param> /// <param name="usSPort">来源端口号</param> /// <param name="usDPort">目标端口号码</param> /// <returns>构造的SYN数据包字节数组</returns> public static byte[] GetSynPacket(string strSIP,string strDIP,ushort usSPort,ushort usDPort) { TcpDefine.IP_HEADER ip_header = TcpDefine.GetIPHeader(strSIP, strDIP); TcpDefine.TCP_HEADER tcp_header = TcpDefine.GetTcpHeader(usSPort, usDPort, m_rnd.Next(), 0, TcpDefine.Flag.SYN); TcpDefine.PSD_HEADER psd_header = new TcpDefine.PSD_HEADER(); //定义伪首部 检验和用 byte[] byResult = new byte[Marshal.SizeOf(ip_header) + Marshal.SizeOf(tcp_header)]; ip_header.total_len = (ushort)(byResult.Length); //整个数据包长度 //填充伪首部数据 psd_header.saddr = ip_header.sourceIP; psd_header.daddr = ip_header.destIP; psd_header.mbz = 0; psd_header.ptcl = TcpDefine.IPPROTO_TCP; psd_header.length = TcpDefine.Reverse((ushort)(Marshal.SizeOf(tcp_header)));//头部长度 + 数据长度(这里没有数据) //计算TCP校验和 若包含TCP应用层数据部分 则也应该列入校验和计算(这里没有) IntPtr p = Marshal.AllocHGlobal(40); Marshal.StructureToPtr(psd_header, p, false); Marshal.Copy(p, byResult, 0, Marshal.SizeOf(psd_header)); Marshal.StructureToPtr(tcp_header, p, false); Marshal.Copy(p, byResult, Marshal.SizeOf(psd_header), Marshal.SizeOf(tcp_header)); tcp_header.th_sum = TcpDefine.CheckSum(byResult, Marshal.SizeOf(psd_header) + Marshal.SizeOf(tcp_header)); //计算IP检验和 Marshal.StructureToPtr(ip_header, p, false); Marshal.Copy(p, byResult, 0, Marshal.SizeOf(ip_header)); Marshal.StructureToPtr(tcp_header, p, false); Marshal.Copy(p, byResult, Marshal.SizeOf(ip_header), Marshal.SizeOf(tcp_header)); ip_header.checksum = TcpDefine.CheckSum(byResult, Marshal.SizeOf(ip_header));//IP检验和只需要计算IP头 //将数据拷贝到ByResult中 Marshal.StructureToPtr(ip_header, p, false); Marshal.Copy(p, byResult, 0, Marshal.SizeOf(ip_header)); Marshal.StructureToPtr(tcp_header, p, false); Marshal.Copy(p, byResult, 20, Marshal.SizeOf(tcp_header)); Marshal.FreeHGlobal(p); return byResult; } }
通过上面的GetSynPacket函数可以构造出一个SYN数据包 然后代码就可以写成这样了
Socket sock_raw = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.IP); sock_raw.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.HeaderIncluded, true); sock_raw.SendTo(TcpDefine.GetSynPacket("192.168.88.105", "8.8.8.8", 10086, 53), new IPEndPoint(IPAddress.Parse("8.8.8.8"), 53));上面来源IP写成自己的IP地址 来源端口自己也写一个 因为到时候目标主机收到这个数据包后会用你填入的IP和端口来回应 所以甚至可以伪造IP地址什么的只是到时候你就收不到回应了(不要奇怪为什么这次这个代码里面的IP段是192.168.88而上面的却是192.168.0 - -!。。。我只能说 文章不是在同一台机器上写完的 我也懒得从新截图了 意思到位就行了)
下面来看看效果
可以看到数据包同样被正确的发送了出去 所使用的来源IP地址和端口都是我填写的数据 数据包也得到了回应
说道回应 那么问题来了怎么才能得到这个回应 光是发送出去了不能得到一样没有什么卵用 因为判断全靠回来的数据判断啊 所以还需要接收代码和接数据包的代码 所一在TcpDefine里面再来一段
public static List<object> DecodePacket(byte[] byData) { List<object> lst = new List<object>(); IntPtr p = Marshal.AllocHGlobal(20); Marshal.Copy(byData, 0, p, 20); TcpDefine.IP_HEADER ip_header = (TcpDefine.IP_HEADER)Marshal.PtrToStructure(p, typeof(TcpDefine.IP_HEADER)); ip_header.total_len = TcpDefine.Reverse(ip_header.total_len); lst.Add(ip_header); Marshal.Copy(byData, (ip_header.h_verlen & 0x0F) * 4, p, 20);//这里我没判断 可能之后的数据不够一个TCP头会出问题 TcpDefine.TCP_HEADER tcp_header = (TcpDefine.TCP_HEADER)Marshal.PtrToStructure(p, typeof(TcpDefine.TCP_HEADER)); Marshal.FreeHGlobal(p); tcp_header.th_dport = TcpDefine.Reverse(tcp_header.th_dport); tcp_header.th_sport = TcpDefine.Reverse(tcp_header.th_sport); tcp_header.th_seq = TcpDefine.Reverse(tcp_header.th_seq); tcp_header.th_ack = TcpDefine.Reverse(tcp_header.th_ack); lst.Add(tcp_header);//对于我们而言byData里面的 IP和TCP才是我们要的 后面的数据部分不用管了 return lst; }然后窗体上的全部代码:
public partial class Form1 : Form { public Form1() { InitializeComponent(); } private Socket m_sock_raw; private void Form1_Load(object sender, EventArgs e) { m_sock_raw = new Socket(AddressFamily.InterNetwork, SocketType.Raw, ProtocolType.IP); m_sock_raw.Bind(new IPEndPoint(IPAddress.Parse("192.168.88.105"), 0)); //设置socket自己构造头部数据包 m_sock_raw.SetSocketOption(SocketOptionLevel.IP, SocketOptionName.HeaderIncluded, true); //设置socket接收全部数据 m_sock_raw.IOControl(IOControlCode.ReceiveAll, new byte[] { 1, 0, 0, 0 }, null); new Thread(ListenCallBack) { IsBackground = true }.Start(); } private void button1_Click(object sender, EventArgs e) { m_sock_raw.SendTo(TcpDefine.GetSynPacket("192.168.88.105", "8.8.8.8", 10086, 80), new IPEndPoint(IPAddress.Parse("8.8.8.8"), 80)); } private void ListenCallBack() { byte[] byRecv = new byte[65535]; while (true)//while(true)并不是推荐写法 这里只为演示 异步写法更佳 { int len = m_sock_raw.Receive(byRecv); if (len == 0) break; if (len < 40) continue;//IP+TCP包头至少40字节 小于这个数据的不要 List<object> lst = TcpDefine.DecodePacket(byRecv); //解包 TcpDefine.IP_HEADER st_ip = (TcpDefine.IP_HEADER)lst[0]; if (st_ip.proto != TcpDefine.IPPROTO_TCP) continue; //不是TCP协议不要 TcpDefine.TCP_HEADER st_tcp = (TcpDefine.TCP_HEADER)lst[1]; if (st_tcp.th_dport != 10086) continue; //不是发往指定端口的数据不要 //没有SYN和ACK不要 if ((st_tcp.th_flag & (byte)TcpDefine.Flag.SYN) != (byte)TcpDefine.Flag.SYN) continue; if ((st_tcp.th_flag & (byte)TcpDefine.Flag.ACK) != (byte)TcpDefine.Flag.ACK) continue; Console.WriteLine("ACK From -> " + new IPAddress(st_ip.sourceIP).ToString() + ":" + st_tcp.th_sport); } } }运行出来点击按钮 调试窗口上也正确的输出了数据:
来看看我内网里面开放80的情况
private void button1_Click(object sender, EventArgs e) { for (int i = 1; i < 255; i++) {//发包不宜过快 别把自己给塞死了 m_sock_raw.SendTo(TcpDefine.GetSynPacket("192.168.88.105", "192.168.88." + i, 10086, 80), new IPEndPoint(IPAddress.Parse("192.16.88." + i), 80)); } }
可以看到 我的主线程只管发包而另一个线程负责收包 这样只需要两个线程就够了 发完包只需要等着收就是了 而如果用Socket直接创立TCP连接 调用Socket.Connect()函数发包和收包则是一体的会被阻塞 这样就大大的节省了资源
注意 这种方式必然会出现丢包的情况 尤其是在网络情况复杂的情况下 发包的时候也不要死循环的发把自己给塞死了 至于发包的频率根据自己情况来衡量 而且是否处理丢包问题也看自己 比如发送了SYN包出去 却没有收到回应 是否重发之类的 如果直接用Socket建立TCP连接系统会自己处理
通常正常情况下 目标主机无论开与没有开发端口都会回应一个数据包 比如
上面就是一个目标端口没有开放的情况但是对方回应了一个RST+ACK 如果端口开放就会回应SYN+ACK
如果是连目标主机都不存在的情况下 那么估计就只有等timeout了
由于没有收到回应 所以系统又重新发送了两次SYN 结果还是没有回应 只能等timeout超时了 所以说只要目标机器存在的情况下 发送一个SYN包过去都会有回应
但是不排除一些机器 比如上面的命令换成telnet 8.8.8.8 其他端口 也会出现无响应的情况 谷歌就是这么任性 没开放端口都懒得鸟你 让你自己去timeout 而且有很多大型厂商的机器都是这样的
所以是直接发完SYN包就不管了 还是没有收到数据包的再发送一次 以及发包的频率和去重处理自己决定 我在这里只是说明如何发送SYN数据包以及接收