Scapy uses the python interpreter as a command board. That means that you can use directly python language (assign variables, use loops, define functions, etc.) If you give a file as parameter when you run scapy, your session (variables, functions, intances, ...) will be saved when you leave the interpretor, and restored the next time you launch scapy.
Scapy is not user proof yet. But it is almost reliable. Some more things need to be done to support more platforms.
The idea is simple. Those kind of tools do two things : sending packets and receiving answers. That's what scapy does : you define a set of packets, it sends them, receives answers, matches requests with answers and returns a list of packet couples (request, answer) and a list of unmatched packets. This has the big advantage over tools like nmap or hping that an answer is not reduced to (open/closed/filtered), but is the whole packet.
On top of this can be build more high level functions, for example one that does traceroutes and give as a result only the start TTL of the request and the source IP of the answer. One that pings a whole network and gives the list of machines answering. One that does a portscan and returns a LaTeX report.
First, we introduce the Net class, which implicitely defines a set of IP addresses. Note that this class does not need to be used to give set of addresses as parameters, it will be automatically used. We also see that sessions work :)
# ./scapy.py -s mysession New session [mysession] Welcome to Scapy (0.9beta) >>> Net("192.168.1.0/24") <Net 192.168.1.0/24> >>> target=Net("www.target.com") >>> targetnet=Net("www.target.com/30") >>> [ip for ip in targetnet] ['173.29.39.100', '173.29.39.101', '173.29.39.102', '173.29.39.103'] >>> ^D # ./scapy.py -s mysession Using session [mysession] Welcome to Scapy (0.9beta) >>> target <Net www.target.com> |
The configuration is hold into a variable named conf, that is saved with the session.
>>> conf L2listen = <class scapy.L2ListenSocket at 0x83a77dc> L2socket = <class scapy.L2Socket at 0x83aabbc> L3socket = <class scapy.L3PacketSocket at 0x83aa8cc> filter = 'not implemented' histfile = '/home/pbi/.scapy_history' iff = 'eth0' promisc = 'not implemented' session = '' sniff_promisc = 0 stealth = 'not implemented' verb = 2 >>> conf.verb=1 |
Now, let's manipulate some packets. Here you can see layers that are supported for the moment. It's really easy to add one.
>>> ls() DNSRR : DNS Resource Record DNSQR : DNS Question Record LLC : LLC Dot1Q : 802.1Q ICMPerror : ICMP in ICMP citation Ether : Ethernet Raw : Raw LLPPP : PPP Link Layer TCP : TCP TCPerror : TCP in ICMP citation ICMP : ICMP Dot3 : 802.3 Packet : abstract packet IP : IP Padding : Padding IPerror : IP in ICMP citation ARP : ARP DNS : DNS EAPOL : EAPOL UDPerror : UDP in ICMP citation STP : Spanning Tree Protocol UDP : UDP EAP : EAP >>> ls(Ether) dst : DestMACField (None) src : SourceMACField (None) type : XShortField (0) >>> ls(IP) version : BitField (4) ihl : BitField (None) tos : XByteField (0) len : ShortField (None) id : ShortField (1) flags : BitField (0) frag : BitField (0) ttl : ByteField (64) proto : ByteField (0) chksum : XShortField (None) src : SourceIPField (None) dst : IPField ('127.0.0.1') options : IPoptionsField ('') >>> IP() <IP |''> >>> a=IP(dst="172.16.1.40") >>> a <IP dst=172.16.1.40 |''> >>> a.dst '172.16.1.40' >>> a.ttl 64 |
>>> a.ttl=32 >>> a <IP dst=172.16.1.40 ttl=32 |''> >>> del(a.ttl) >>> a <IP dst=172.16.1.40 |''> >>> a.ttl 64 |
>>> t=TCP() >>> t.flags="SA" >>> t.flags 18 >>> t <TCP flags=SA |> >>> t.flags=23 >>> t <TCP flags=FSRA |> >>> >>> i=IP(flags="DF+MF") >>> i.flags 3 >>> i <IP flags=MF+DF |> >>> i.flags=6 >>> i <IP flags=DF+evil |> |
>>> a.dst '172.16.1.40' >>> a.src '172.16.1.24' >>> del(a.dst) >>> a.dst '127.0.0.1' >>> a.src '127.0.0.1' >>> a.dst="192.168.11.10" >>> a.src '192.168.11.1' >>> a.dst=target >>> a.src '172.16.1.24' >>> a.src="1.2.3.4" >>> a <IP src=1.2.3.4 dst=<Net www.target.com> |''> |
$ route -n Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface 172.16.0.0 0.0.0.0 255.255.0.0 U 0 0 0 eth0 192.168.11.0 0.0.0.0 255.255.255.0 U 0 0 0 eth1 0.0.0.0 172.16.1.1 0.0.0.0 UG 0 0 0 eth0 |
The / operator has been used as a composition operator between two layers. When doing so, the lower layer can have one or more of its defaults fields overloaded according to the upper layer. (You still can give the value you want). A string can be used as a raw layer.
>>> IP() <IP |''> >>> IP()/TCP() <IP proto=6 |<TCP |''>> >>> Ether()/IP()/TCP() <Ether type=0x800 |<IP proto=6 |<TCP |''>>> >>> IP()/TCP()/"GET /index.html HTTP/1.0\n\n" <IP proto=6 |<TCP |<Raw load='GET /index.html HTTP/1.0\n\n' |''>>> >>> Ether()/IP()/IP()/IP()/UDP() <Ether type=0x800 |<IP proto=0 |<IP proto=0 |<IP proto=17 |<UDP |''>>>>> >>> IP(proto=55)/TCP() <IP proto=55 |<TCP |''>> |
Each packet can be build or dissected (note: in python _ (underscode) is the latest result) :
>>> str(IP()) 'E\x00\x00\x14\x00\x01\x00\x00@\x00|\xe7\x7f\x00\x00\x01\x7f\x00\x00\x01' >>> IP(_) <IP frag=0 src=127.0.0.1 proto=0 tos=0x0 dst=127.0.0.1 chksum=0x7ce7 len=20 version=4 flags=0 ihl=5 ttl=64 id=1 |''> >>> a=Ether()/IP(dst=target)/TCP()/"GET /index.html HTTP/1.0 \n\n" >>> b=str(a) >>> b '\x00\x90\x7f\x1e \xc8\x00\x03G\x88\x1d/\x08\x00E\x00\x00C\x00\x01\x00\x00@\x06 \xcd7\xac\x10\x01\x18\xad\x1d\'e\x00P\x00P\x00\x00\x00\x00\x00\x00\x00\x00P\x0 2\x00\x00/\xf9\x00\x00GET /index.html HTTP/1.0 \n\n' >>> c=Ether(b) >>> c <Ether src=00:03:47:88:1d:2f dst=00:90:7f:1e:26:c8 type=0x800 |<IP frag=0 src=172.16.1.24 proto=6 tos=0x0 dst=173.29.39.101 chksum=0xcd37 len=67 options='' version=4 flags=0 ihl=5 ttl=64 id=1 |<TCP reserved=0 seq=0L ack=0L dataofs=5 dport=80 window=0 flags=0x2 chksum=0x2ff9 urgptr=0 sport=80 options='' | <Raw load='GET /index.html HTTP/1.0 \n\n' |''>>>> |
>>> c.hide_defaults() >>> c <Ether src=00:03:47:88:1d:2f dst=00:90:7f:1e:26:c8 type=0x800 |<IP src=172.16.1.24 proto=6 dst=173.29.39.101 chksum=0xcd37 len=67 ihl=5 |<TCP dataofs=5 chksum=0x2ff9 | <Raw load='GET /index.html HTTP/1.0 \n\n' |''>>>> |
For the moment, we have only generated one packet. Let see how to specify sets of packets as easily. Each field of the whole packet (ever layers) can be a set. This implicidely define a set of packets, generated using a kind of cartesian product between all the fields.
>>> a=IP(dst=targetnet) >>> a <IP dst=<Net www.target.com/30> |''> >>> [p for p in a] [<IP dst=173.29.39.100 |''>, <IP dst=173.29.39.101 |''>, <IP dst=173.29.39.102 |''>, <IP dst=173.29.39.103 |''>] >>> b=IP(ttl=[1,2,(5,9)]) >>> b <IP ttl=[1, 2, (5, 9)] |''> >>> [p for p in b] [<IP ttl=1 |''>, <IP ttl=2 |''>, <IP ttl=5 |''>, <IP ttl=6 |''>, <IP ttl=7 |''>, <IP ttl=8 |''>, <IP ttl=9 |''>] >>> c=TCP(dport=[80,443]) >>> [p for p in a/c] [<IP dst=173.29.39.100 proto=6 |<TCP dport=80 |''>>, <IP dst=173.29.39.100 proto=6 |<TCP dport=443 |''>>, <IP dst=173.29.39.101 proto=6 |<TCP dport=80 |''>>, <IP dst=173.29.39.101 proto=6 |<TCP dport=443 |''>>, <IP dst=173.29.39.102 proto=6 |<TCP dport=80 |''>>, <IP dst=173.29.39.102 proto=6 |<TCP dport=443 |''>>, <IP dst=173.29.39.103 proto=6 |<TCP dport=80 |''>>, <IP dst=173.29.39.103 proto=6 |<TCP dport=443 |''>>] |
Now, let's try to do some fun things. The sr() function is for sending packets and receiving answers. The function returns a couple of packet and answers, and the unanswered packets. The function sr1() is a variant that only return one packet that answered the packet (or the packet set) sent. The packets must be layer 3 packets (IP, ARP, etc.). The function srp() do the same for layer 2 packets (Ethernet, 802.3, etc.).
>>> p=sr1(IP(dst="172.16.1.40")/ICMP()/"XXXXXXXXXXX") Finished to send 1 packets. * Received 1 packets, got 1 answers, remaining 0 packets >>> p <IP frag=0 src=172.16.1.40 proto=1 tos=0x0 dst=172.16.1.24 chksum=0xd56c len=39 options='' version=4 flags= ihl=5 ttl=255 id=35848 |<ICMP code=0 type=echo-reply id=0x0 seq=0x0 chksum=0xee45 |<Raw load='XXXXXXXXXXX' | <Padding load='\x00\x00\x00\x00\x00\x00\x00' |>>>> >>> p.display() ---[ IP ]--- version = 4 ihl = 5 tos = 0x0 len = 39 id = 35848 flags = frag = 0 ttl = 255 proto = ICMP chksum = 0xd56c src = 172.16.1.40 dst = 172.16.1.24 options = '' ---[ ICMP ]--- type = echo-reply code = 0 chksum = 0xee45 id = 0x0 seq = 0x0 ---[ Raw ]--- load = 'XXXXXXXXXXX' ---[ Padding ]--- load = '\x00\x00\x00\x00\x00\x00\x00' |
A TCP traceroute.
>>> ans,unans=sr(IP(dst=target, ttl=(4,25),id=RandShort())/TCP(flags=0x2)) *****.******.*.***..*.**Finished to send 22 packets. ***...... Received 33 packets, got 21 answers, remaining 1 packets >>> for snd,rcv in ans: ... print snd.ttl, rcv.src, isinstance(rcv.payload, TCP) ... 5 194.51.159.65 0 6 194.51.159.49 0 4 194.250.107.181 0 7 193.251.126.34 0 8 193.251.126.154 0 9 193.251.241.89 0 10 193.251.241.110 0 11 193.251.241.173 0 13 208.172.251.165 0 12 193.251.241.173 0 14 208.172.251.165 0 15 206.24.226.99 0 16 206.24.238.34 0 17 173.109.66.90 0 18 173.109.88.218 0 19 173.29.39.101 1 20 173.29.39.101 1 21 173.29.39.101 1 22 173.29.39.101 1 23 173.29.39.101 1 24 173.29.39.101 1 |
A DNS query (rd = recursion desired).
>>> sr1(IP(dst="172.16.1.40")/UDP()/DNS(rd=1,qd=DNSQR(qname="www.target.com"))) Finished to send 1 packets. .* Received 2 packets, got 1 answers, remaining 0 packets <IP frag=0 src=172.16.1.40 proto=UDP tos=0x0 dst=172.16.1.24 chksum=0xdfb8 len=212 options='' version=4 flags=DF ihl=5 ttl=64 id=0 |<UDP dport=80 sport=53 len=192 chksum=0x188 |<DNS aa=0 qr=1 an=<DNSRR rdata='173.29.33.99' ttl=300L rrname='www.target.com.' type=A class=IN |> ns=<DNSRR rdata='ns4.target.com.' ttl=345600L rrname='target.com.' type=NS class=IN |<DNSRR rdata='ns1.target.com.' ttl=345600L rrname='target.com.' type=NS class=IN |<DNSRR rdata='ns2.target.com.' ttl=345600L rrname='target.com.' type=NS class=IN |<DNSRR rdata='ns3.target.com.' ttl=345600L rrname='target.com.' type=NS class=IN |>>>> nscount=4 qdcount=1 tc=0 ancount=1 rd=1 arcount=4 ar=<DNSRR rdata='173.29.32.10' ttl=326818L rrname='ns1.target.com.' type=A class=IN |<DNSRR rdata='173.29.34.10' ttl=326818L rrname='ns2.target.com.' type=A class=IN |<DNSRR rdata='173.29.36.10' ttl=326818L rrname='ns3.target.com.' type=A class=IN |<DNSRR rdata='173.29.38.10' ttl=326818L rrname='ns4.target.com.' type=A class=IN |>>>> opcode=0 ra=1 z=0 rcode=0 id=0 qd=<DNSQR qclass=IN qtype=A qname='www.target.com.' |> |>>> >>> _.an <DNSRR rdata='173.29.33.99' ttl=300L rrname='www.target.com.' type=A class=IN |> |
The process of sending packets and receiving is quite complicated. As I wanted to use the PF_PACKET interface to go through netfilter, I also needed to implement an ARP stack and ARP cache, and a LL stack. Well it seems to work, on ethernet and PPP interfaces, but I don't guarantee anything. Anyway, the fact I used a kind of super-socket for that mean that you can switch your IO layer very easily, and use PF_INET/SOCK_RAW, or use PF_PACKET at level 2 (giving the LL header (ethernet,...) and giving yourself mac addresses, ...). I've just added a super socket which use libdnet and libpcap, so that it should be portable :
>>> conf.L3socket=L3dnetSocket >>> conf.L3listen=L3pcapListenSocket |
We can easily capture some packets or even clone tcpdump or tethereal. If no interface is given, sniffing will happen on every interfaces.
>>> sniff(count=2) [<Ether src=00:d0:b7:88:50:f2 dst=00:03:47:88:1d:2f type=0x800 |<IP frag=0 src=172.16.1.40 proto=1 tos=0x0 dst=172.16.1.24 chksum=0x3974 len=84 options='' version=4 flags=0 ihl=5 ttl=255 id=10196 |<ICMP code=0 type=0 id=0xdc0f seq=0x7138 chksum=0x25e5 |<Raw load='>r\x15f\x00\x07M\xf0\x08\t\n\x0b \x0c\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e \x1f !"#$%&\'()*+,-./01234567' |''>>>>, <Ether src=00:d0:b7:88:50:f2 dst=00:03:47:88:1d:2f type=0x800 |<IP frag=0 src=172.16.1.40 proto=1 tos=0x0 dst=172.16.1.24 chksum=0x3973 len=84 options=''i version=4 flags=0 ihl=5 ttl=255 id=10197 |<ICMP code=0 type=0 id=0xdc0f seq=0x7238 chksum=0x1792 |<Raw load='>r\x15f\x00\x07[C\x08\t\n\x0b\x0c\r\x0e \x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\' ()*+,-./01234567' |''>>>>] >>> sniff(iface="wifi0", prn=lambda x: x.summary()) 802.11 Management 8 ff:ff:ff:ff:ff:ff / 802.11 Beacon / Info SSID / Info Rates / Info DSset / Info TIM / Info 133 802.11 Management 4 ff:ff:ff:ff:ff:ff / 802.11 Probe Request / Info SSID / Info Rates 802.11 Management 5 00:0a:41:ee:a5:50 / 802.11 Probe Response / Info SSID / Info Rates / Info DSset / Info 133 802.11 Management 4 ff:ff:ff:ff:ff:ff / 802.11 Probe Request / Info SSID / Info Rates 802.11 Management 4 ff:ff:ff:ff:ff:ff / 802.11 Probe Request / Info SSID / Info Rates 802.11 Management 8 ff:ff:ff:ff:ff:ff / 802.11 Beacon / Info SSID / Info Rates / Info DSset / Info TIM / Info 133 802.11 Management 11 00:07:50:d6:44:3f / 802.11 Authentication 802.11 Management 11 00:0a:41:ee:a5:50 / 802.11 Authentication 802.11 Management 0 00:07:50:d6:44:3f / 802.11 Association Request / Info SSID / Info Rates / Info 133 / Info 149 802.11 Management 1 00:0a:41:ee:a5:50 / 802.11 Association Response / Info Rates / Info 133 / Info 149 802.11 Management 8 ff:ff:ff:ff:ff:ff / 802.11 Beacon / Info SSID / Info Rates / Info DSset / Info TIM / Info 133 802.11 Management 8 ff:ff:ff:ff:ff:ff / 802.11 Beacon / Info SSID / Info Rates / Info DSset / Info TIM / Info 133 ARP who has 172.20.70.172 says 172.20.70.171 / Padding ARP is at 00:0a:b7:4b:9c:dd says 172.20.70.172 / Padding ICMP echo-request 0 / Raw ICMP echo-reply 0 / Raw >>> sniff(iface="lo", prn=lambda x: x.display()) ---[ Ethernet ]--- dst = 00:00:00:00:00:00 src = 00:00:00:00:00:00 type = 0x800 ---[ IP ]--- version = 4 ihl = 5 tos = 0x0 len = 84 id = 0 flags = 2 frag = 0 ttl = 64 proto = 1 chksum = 0x3ca7 src = 127.0.0.1 dst = 127.0.0.1 options = '' ---[ ICMP ]--- type = echo-request code = 0 chksum = 0x4f7c id = 0xe10f seq = 0x0 ---[ Raw ]--- load = '>r\x15\xe0\x00\n\x88\x14\x08\t\n\x0b\x0c\r\x0e\x0f\x10\x11 \x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./01234567' ---[ Ethernet ]--- dst = 00:00:00:00:00:00 src = 00:00:00:00:00:00 type = 0x800 ---[ IP ]--- version = 4 ihl = 5 tos = 0x0 len = 84 id = 35452 flags = 0 frag = 0 ttl = 64 proto = 1 chksum = 0xf22a src = 127.0.0.1 dst = 127.0.0.1 options = '' ---[ ICMP ]--- type = echo-reply code = 0 chksum = 0x577c id = 0xe10f seq = 0x0 ---[ Raw ]--- load = '>r\x15\xe0\x00\n\x88\x14\x08\t\n\x0b\x0c\r\x0e\x0f\x10 \x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !"#$%&\'()*+,-./01234567' |
We can sniff and do passive OS fingerprinting.
>>> p <Ether src=00:40:33:96:7b:60 dst=00:10:4b:b3:7d:4e type=0x800 |<IP frag=0 src=192.168.8.10 proto=6 tos=0x10 dst=192.168.8.1 chksum=0xb85e len=60 options='' version=4 flags=2 ihl=5 ttl=64 id=61681 |<TCP reserved=0 seq=2023566040L ack=0L dataofs=10 dport=80 window=5840 flags=SEC chksum=0x570c urgptr=0 sport=46511 options={'Timestamp': (342940201L, 0L), 'MSS': 1460, 'NOP': (), 'SAckOK': '', 'WScale': 0} |''>>> >>> p0f(p) (1.0, ['Linux 2.4.2 - 2.4.14 (1)']) >>> a=sniff(prn=prnp0f) (1.0, ['Linux 2.4.2 - 2.4.14 (1)']) (1.0, ['Linux 2.4.2 - 2.4.14 (1)']) (0.875, ['Linux 2.4.2 - 2.4.14 (1)', 'Linux 2.4.10 (1)', 'Windows 98 (?)']) (1.0, ['Windows 2000 (9)']) |
Demo of both bpf filter and sprintf() method :
>>> a=sniff(filter="tcp and ( port 25 or port 110 )", prn=lambda x: x.sprintf("%IP.src%:%TCP.sport% -> %IP.dst%:%TCP.dport% %2s,TCP.flags% : %TCP.payload%")) 192.168.8.10:47226 -> 213.228.0.14:110 S : 213.228.0.14:110 -> 192.168.8.10:47226 SA : 192.168.8.10:47226 -> 213.228.0.14:110 A : 213.228.0.14:110 -> 192.168.8.10:47226 PA : +OK <13103.1048117923@pop2-1.free.fr> 192.168.8.10:47226 -> 213.228.0.14:110 A : 192.168.8.10:47226 -> 213.228.0.14:110 PA : USER toto 213.228.0.14:110 -> 192.168.8.10:47226 A : 213.228.0.14:110 -> 192.168.8.10:47226 PA : +OK 192.168.8.10:47226 -> 213.228.0.14:110 A : 192.168.8.10:47226 -> 213.228.0.14:110 PA : PASS tata 213.228.0.14:110 -> 192.168.8.10:47226 PA : -ERR authorization failed 192.168.8.10:47226 -> 213.228.0.14:110 A : 213.228.0.14:110 -> 192.168.8.10:47226 FA : 192.168.8.10:47226 -> 213.228.0.14:110 FA : 213.228.0.14:110 -> 192.168.8.10:47226 A : |
Soon to come : examples for arping, scanning, arp cache poisoning, dns spoofing, etc. (they are present in the scapy presentation slides)