Python Scapy随心所欲研究TCP协议栈
1.前言
如果只需要研究Linux的tcp协议栈行为,只需要使用packetdrill就能够满足我的所有需求。packetdrill才是让我随心所欲地撩tcp协议栈。packetdrill的简单使用手册。
然而悲剧的是,除了要研究Linux的TCP协议栈行为,还需要研究Windows的tcp协议栈的行为,Windows不开源,感觉里面应该有挺多未知的坑。
为了能够重现Windows的tcp协议栈的一些网络行为,这里使用python的scapy进行包构造撩撩Windows的tcp协议栈。scapy在tcp数据报文注入会有一点的时延,这个工具在要求时延严格的场景无法使用(还是packetdrill好用,囧)。针对目前遇到的场景,勉强能用,再则已经撸惯了python,上手起来比较容易。
2.基本语法
- 安装scapy
在Centos7.2中直接使用yuminstall来安装。
yuminstallscapy.noarch
- help能解决大部分问题
[root@localhost~]#scapy INFO:Can'timportpythongnuplotwrapper.Won'tbeabletoplot. INFO:Can'timportPyX.Won'tbeabletousepsdump()orpdfdump(). WARNING:NoroutefoundforIPv6destination::(nodefaultroute?) WelcometoScapy(2.2.0) >>>help(send)
在大部分时候,如果看到不明白的地方,请用help。其次是官方的参考手册
- 基本语法
ip/tcp/http数据包操纵
>>>IP()>>>>IP()/TCP() > >>>>IP(proto=55)/TCP() > >>>>Ether()/IP()/TCP() >> >>>>IP()/TCP()/"GET/HTTP/1.0\r\n\r\n"数据部分可以直接使用字符串 >> >>>>Ether()/IP()/UDP() >> >>>>Ether()/IP()/IP()/UDP() >>> >>>str(IP()) 'E\x00\x00\x14\x00\x01\x00\x00@\x00|\xe7\x7f\x00\x00\x01\x7f\x00\x00\x01' >>>IP(_) >>>a=Ether()/IP(dst="www.baidu.com")/TCP()/"GET/index.htmlHTTP/1.0\n\n" >>>hexdump(a) 000000030F196A49080027FED81208004500....jI..'.....E. 001000430001000040067078C0A873C6B461.C....@.px..s..a 0020216C0014005000000000000000005002!l...P........P. 00302000B3750000474554202F696E646578..u..GET/index 00402E68746D6C20485454502F312E30200A.htmlHTTP/1.0. 00500A. >>>b=str(a) >>>b "\x00\x03\x0f\x19jI\x08\x00'\xfe\xd8\x12\x08\x00E\x00\x00C\x00\x01\x00\x00@\x06px \xc0\xa8s\xc6\xb4a!l\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x02\x00\xb3u \x00\x00GET/index.htmlHTTP/1.0\n\n"
1.数据包发送
数据包的发送主要包括以下函数send/sendp/sr/sr1/srp主要区别是:
send函数工作在第三层
send(IP(dst="192.168.115.188")/ICMP())
sendp函数工作在第二层,你可以选择网卡和协议
sendp(Ether()/IP(dst="192.168.115.188",ttl=(1,4)),iface="eth0")
fuzz函数的作用:可以更改一些默认的不可以被计算的值(比如校验和checksums),更改的值是随机的,但是类型是符合字段的值的。
send(IP(dst="www.baidu.com")/UDP()/NTP(version=4),loop=2)#未使用fuzz() send(IP(dst="www.baidu.com")/fuzz(UDP()/NTP(version=4)),loop=2)#使用fuzz()
SR()函数用来来发送数据包和接收响应。该函数返回有回应的数据包和没有回应的数据包;该函数也算得上是scapy的核心了,他会返回两个列表数据,一个是answerlist另一个是unanswered
>>>sr(IP(dst="192.168.115.1")/TCP(dport=[21,22,23])) Beginemission: Finishedtosend3packets. *** Received3packets,got3answers,remaining0packets Results:TCP:3UDP:0ICMP:0Other:0>,Unanswered:TCP:0UDP:0ICMP:0Other:0 >>>ans,unans=_这也是scapy的核心了 >>>ans.show() 0000IP/TCP192.168.115.198:ftp_data>192.168.115.1:ftpS==>IP/TCP192.168.115.1:ftp>192.168.115.198:ftp_dataRA/Padding 0001IP/TCP192.168.115.198:ftp_data>192.168.115.1:sshS==>IP/TCP192.168.115.1:ssh>192.168.115.198:ftp_dataRA/Padding 0002IP/TCP192.168.115.198:ftp_data>192.168.115.1:telnetS==>IP/TCP192.168.115.1:telnet>192.168.115.198:ftp_dataSA/Padding >>>sr(IP(dst="192.168.115.1")/TCP(dport=[21,22,23]),inter=0.5,retry=-2,timeout=1)网络环境不好时,也可以追加interretrytimeout等附加信息,
函数sr1()是sr()一个变种,只返回应答发送的分组(或分组集)。这两个函数发送的数据包必须是第3层数据包(IP,ARP等)。而函数SRP()位于第2层(以太网,802.3,等)。
>>>p=sr1(IP(dst="192.168.115.188")/ICMP()/"test") Beginemission: .....Finishedtosend1packets. .* Received7packets,got1answers,remaining0packets >>>p>>> >>>p.show() ###[IP]### version=4L ihl=5L tos=0x0 len=32 id=26000 flags= frag=0L ttl=128 proto=icmp chksum=0x6c79 src=192.168.115.188 dst=192.168.115.198 \options\ ###[ICMP]### type=echo-reply code=0 chksum=0x1826 id=0x0 seq=0x0 ###[Raw]### load='test' ###[Padding]### load='\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
1.数据包sniff
a=sniff(count=1,filter="tcpandhost192.168.1.1andport80")
使用sniff主要是用于数据包的接收,根据filter设定的条件,将符合条件的数据包接收回来。
3.场景构造
scapy的缺点是,他只负责构造包,是单向的。不像packetdrill这么完美,packetdrill不但可以构造包,还能实现系统调用构造不同的场景,还能帮你检查协议栈发出的数据包是否符合预期。撩协tcp协议栈的过程不外乎两端,一端使用系统调用模拟协议栈行为,另外一端则是我们构造的包。常见场景主要是:服务器场景、客户端场景。
- 服务器场景:
服务器场景使用系统调用(即用户态程序),而客户端则是scapy构造的包。
在这里构造一个简单的三次握手后向服务器端发送数据。为了防止Linux客户端rst。
iptables-AOUTPUT-ptcp--tcp-flagsRSTRST-s192.168.56.1-jDROP
#!/usr/local/bin/python fromscapy.allimport* #VARIABLES src=sys.argv[1] dst=sys.argv[2] sport=random.randint(1024,65535) dport=int(sys.argv[3]) #SYN ip=IP(src=src,dst=dst) SYN=TCP(sport=sport,dport=dport,flags='S',seq=1000) SYNACK=sr1(ip/SYN) #ACK ACK=TCP(sport=sport,dport=dport,flags='A',seq=SYNACK.ack,ack=SYNACK.seq+1) send(ip/ACK)
在这里可以安装一个nginx来验证。
- 客户端场景:
客户端场景使用系统调用(即用户态程序),而服务器端则是scapy构造包。
在这里使用scapy构造一个简单的http服务器。为了防止协议栈发送RST,需要对iptables进行设置。
iptables-AOUTPUT-ptcp--tcp-flagsRSTRST--sport80-jDROP
#!/usr/bin/python fromscapy.allimport* #Interactswithaclientbygoingthroughthethree-wayhandshake. #Shutsdowntheconnectionimmediatelyaftertheconnectionhasbeenestablished. #AkaljedDec2010,http://www.akaljed.wordpress.com #Waitforclienttoconnect. a=sniff(count=1,filter="tcpandhost192.168.1.1andport80") #somevariablesforlateruse. ValueOfPort=a[0].sport SeqNr=a[0].seq AckNr=a[0].seq+1 #GeneratingtheIPlayer: ip=IP(src="192.168.1.1",dst="192.168.1.2") #GeneratingTCPlayer: TCP_SYNACK=TCP(sport=80,dport=ValueOfPort,flags="SA",seq=SeqNr,ack=AckNr,options=[('MSS',1460)]) #sendSYNACKtoremotehostANDreceiveACK. ANSWER=sr1(ip/TCP_SYNACK) #CapturenextTCPpacketswithdport80.(containshttpGETrequest) GEThttp=sniff(filter="tcpandport80",count=1,prn=lambdax:x.sprintf("{IP:%IP.src%:%TCP.dport%}")) AckNr=AckNr+len(GEThttp[0].load) SeqNr=a[0].seq+1 #PrinttheGETrequest #(Sanitycheck:sizeofdatashouldbegreaterthan1.) iflen(GEThttp[0].load)>1:printGEThttp[0].load #Generatecustomhttpfilecontent. html1="HTTP/1.1200OK\x0d\x0aDate:Wed,29Sep201020:19:05GMT\x0d\x0aServer:Testserver\x0d\x0aConnection:Keep-Alive\x0d\x0aContent-Type:text/html;charset=UTF-8\x0d\x0aContent-Length:291\x0d\x0a\x0d\x0aTestserver
-Welcometotestserver-------------------------------