UTScapy: Unit Testing with Scapy

About UTScapy

What is Scapy

Scapy is a powerful interactive packet manipulation program. It is able to forge or decode packets of a wide number of protocols, send them on the wire, capture them, match requests and replies, and much more. It can easily handle most classical tasks like scanning, tracerouting, probing, unit tests, attacks or network discovery (it can replace hping, 85% of nmap, arpspoof, arp-sk, arping, tcpdump, tethereal, p0f, etc.). It also performs very well at a lot of other specific tasks that most other tools can't handle, like sending invalid frames, injecting your own 802.11 frames, combining technics (VLAN hopping+ARP cache poisoning, VOIP decoding on WEP encrypted channel, ...), etc.

See the Scapy project page for more details on Scapy, what makes it different from most other networking tools, how to install it or some examples.

What is UTScapy

UTScapy is a small program that reads a campaign of unit tests, runs it with Scapy and output a text/ansi/HTML/LaTeX report.

A test campaign is compounded of one or more test sets. A test set is a set of unit tests. An unit test is a list of Scapy commands that will be run by Scapy and whose end result will determine the truth value of the test.

Each test set and each unit test can be given some keywords. When running the campaign, tests can be selected according to their keyworkds.

For each unit test, test set and campaign, a CRC32 of the test is calculated and displayed so that an abstract of the test is sufficient to check the actual test was the one you expected and not one that has been slightliy modified for any reason. In case your dealing with evil people that try to modify or corrupt the file without changing the CRC32, a global SHA1 is computed on the whole file.

If you are interested in those kind of tests, you should also have a look at Scapytain, web application that can store, organise and run test campaigns on top of Scapy

UTScapy project

Download

Help, documentation

Usage

Usage: UTscapy [-m module] [-f {text|ansi|HTML|LaTeX}] [-o output_file] 
               [-t testfile] [-k keywords [-k ...]] [-K keywords [-K ...]]
               [-l] [-d|-D] [-F] [-q[q]]
-l              : generate local files
-F              : expand only failed tests
-d              : dump campaign
-D              : dump campaign and stop
-C              : don't calculate CRC and SHA
-q              : quiet mode
-qq             : [silent mode]
-n <testnum>    : only tests whose numbers are given (eg. 1,3-7,12)
-m <module>     : additional module to put in the namespace
-k <kw1>,<kw2>,...      : include only tests with one of those keywords (can be used many times)
-K <kw1>,<kw2>,...      : remove tests with one of those keywords (can be used many times)

Mailing-list

Send questions, bug reports, suggestions, ideas, cool usages of Scapy, etc.

Example

The following example is a test campaign. A test campaign is compounded of several test sets. A test set is a set of unit tests.

# ./UTscapy.py -t demo_campaign.txt -f html -o demo_campaign.html -F
passed 14F389A8 Get conf
passed 19EC7768 List layers
passed B614219C List commands
passed 17F96DD3 Configuration
failed 80FE5B0D Fake wrong test
passed 2306670D Building some packets packet
passed 1E674999 Manipulating some packets
passed A572EE61 Checking overloads
passed 63973A6A sprintf() function
passed DCD84ABB sprintf() function
passed 111C8A3A haslayer function
passed 47614F5A getlayer function
passed 6D1CC6B7 equality
passed 5D24A719 Creation of a layer with FieldLenField
passed 201D5022 Assembly of an empty packet
passed 174DF639 Assembly of non empty packet
passed 5CB93CC7 Disassembly
passed F2BF1D32 Creation of a layer
passed EF5DAC0E Assembly of an empty packet
passed 92FBE492 Assembly of a non-empty packet
passed 60BD0B6E Disassemble
passed 91BCBCC8 Manipulate
passed D82F28CC Create a layer
passed 33099673 Test the PacketListField assembly
passed A39DF413 Test the PacketListField assembly 2
passed 7ACA7707 Test disassembly
passed E2DAAA2E Nested PacketListField
passed 545C3B6B ISAKMP creation
passed EFF2F68C ISAKMP manipulation
passed 98051DEF ISAKMP assembly
passed A0CD4650 ISAKMP disassembly
passed 19D713FC WEP tests
passed 0CF1FB69 Sending and receiving an ICMP
passed B6AA1633 DNS request
passed 68FE0D72 Implicit logic
passed 87D8199C Port scan
passed 97755875 Traceroute function
passed 4A3C3B08 Result manipulation
passed 658B8F34 DNS packet manipulation
passed 70C79069 Arping
Campaign CRC=7174A369  SHA=BA9669C94F64634488397E1558D8D86EDED7CF03
PASSED=39 FAILED=1
You can see the result of this test campaign.

% Regression tests for Scapy

# More informations at http://www.secdev.org/projects/UTscapy/
# $Id: regression.uts,v 1.4 2006/07/18 18:17:21 pbi Exp pbi $

############
############
+ Informations on Scapy

= Get conf
~ conf command
* Dump the current configuration
conf

= List layers
~ conf command
ls()

= List commands
~ conf command
lsc()

= Configuration
~ conf
conf.debug_dissect=1

############
############
+ Basic tests

* Those test are here mainly to check nothing has been broken
* and to catch Exceptions

= Fake wrong test
a = 3
assert(a == 3)
a+1 == 3

= Building some packets packet
~ basic IP TCP UDP NTP LLC SNAP Dot11
IP()/TCP()
Ether()/IP()/UDP()/NTP()
Dot11()/LLC()/SNAP()/IP()/TCP()/"XXX"
IP(ttl=25)/TCP(sport=12, dport=42)

= Manipulating some packets
~ basic IP TCP
a=IP(ttl=4)/TCP()
a.ttl
a.ttl=10
del(a.ttl)
a.ttl
TCP in a
a[TCP]
a[TCP].dport=[80,443]
a
a=3


= Checking overloads
~ basic IP TCP Ether
a=Ether()/IP()/TCP()
a.proto
_ == 6


= sprintf() function
~ basic sprintf Ether IP UDP NTP
a=Ether()/IP()/IP(ttl=4)/UDP()/NTP()
a.sprintf("%type% %IP.ttl% %#05xr,UDP.sport% %IP:2.ttl%")
_ in [ '0x800 64 0x07b 4', 'IPv4 64 0x07b 4']


= sprintf() function 
~ basic sprintf IP TCP SNAP LLC Dot11
* This test is on the conditionnal substring feature of sprintf()
a=Dot11()/LLC()/SNAP()/IP()/TCP()
a.sprintf("{IP:{TCP:flags=%TCP.flags%}{UDP:port=%UDP.ports%} %IP.src%}")
_ == 'flags=S 127.0.0.1'


= haslayer function
~ basic haslayer IP TCP ICMP ISAKMP
x=IP(id=1)/ISAKMP_payload_SA(prop=ISAKMP_payload_SA(prop=IP()/ICMP()))/TCP()
TCP in x, ICMP in x, IP in x, UDP in x
_ == (True,True,True,False)

= getlayer function
~ basic getlayer IP ISAKMP UDP
x=IP(id=1)/ISAKMP_payload_SA(prop=IP(id=2)/UDP(dport=1))/IP(id=3)/UDP(dport=2)
x[IP]
x[IP:2]
x[IP:3]
x[IP:4]
x[UDP]
x[UDP:1]
x[UDP:2]
x[IP].id == 1 and x[IP:2].id == 2 and x[IP:3].id == 3 and \
 x[UDP].dport == 1 and x[UDP:2].dport == 2 and x[UDP:3] is None

= equality
~ basic
w=Ether()/IP()/UDP(dport=53)
x=Ether()/IP(dst="127.0.0.1")/UDP()
y=Ether()/IP()/UDP(dport=4)
z=Ether()/IP()/UDP()/NTP()
t=Ether()/IP()/TCP()
x==y, x==z, x==t, y==z, y==t, z==t, w==x
_ == (False, False, False, False, False, False, True)

############
############
+ Tests on FieldLenField

= Creation of a layer with FieldLenField
~ field 
class TestFLenF(Packet):
    name = "test"
    fields_desc = [ FieldLenField("len", None, "str", "B"),
                    StrLenField("str", "default", "len", shift=1) ]

= Assembly of an empty packet
~ field
TestFLenF()
str(_)
_ == "\x08default"

= Assembly of non empty packet
~ field
TestFLenF(str="123")
str(_)
_ == "\x04123"

= Disassembly
~ field
TestFLenF("\x04ABCDEFGHIJKL")
_
_.len == 4 and _.str == "ABC" and Raw in _

############
############
+ Tests on FieldListField

= Creation of a layer
~ field
class TestFLF(Packet):
    name="test"
    fields_desc = [ FieldLenField("len", None, "lst", "B"),
                    FieldListField("lst", None, IntField("elt",0), "len")
                   ]

= Assembly of an empty packet
~ field
a = TestFLF()
str(a)

= Assembly of a non-empty packet
~ field
a = TestFLF()
a.lst = [7,65539]
ls(a)
str(a)
_ == struct.pack("!BII", 2,7,65539)

= Disassemble
~ field
TestFLF("\x00\x11\x12")
assert(_.len == 0 and Raw in _ and _[Raw].load == "\x11\x12")
TestFLF(struct.pack("!BIII",3,1234,2345,12345678))
assert(_.len == 3 and _.lst == [1234,2345,12345678])

= Manipulate
~ field
a = TestFLF(lst=[4])
str(a)
assert(_ == "\x01\x00\x00\x00\x04")
a.lst.append(1234)
TestFLF(str(a))
a.show2()
a.len=7
str(a)
assert(_ == "\x07\x00\x00\x00\x04\x00\x00\x04\xd2")
a.len=2
a.lst=[1,2,3,4,5]
TestFLF(str(a))
assert(Raw in _ and _[Raw].load == '\x00\x00\x00\x03\x00\x00\x00\x04\x00\x00\x00\x05') 


############
############
+ PacketListField tests

= Create a layer
~ field
class TestPLF(Packet):
    name="test"
    fields_desc=[ FieldLenField("len", None, "plist"),
                  PacketListField("plist", [], IP, "len",) ]

= Test the PacketListField assembly
~ field
x=TestPLF()
str(x)
_ == "\x00\x00"

= Test the PacketListField assembly 2
~ field 
x=TestPLF()
x.plist=[IP()/TCP(), IP()/UDP()]
str(x)
_.startswith('\x00\x02E')

= Test disassembly
~ field
x=TestPLF(plist=[IP()/TCP(seq=1234567), IP()/UDP()])
TestPLF(str(x))
_.show()
IP in _ and TCP in _ and UDP in _ and _[TCP].seq == 1234567

= Nested PacketListField
~ field
y=IP()/TCP(seq=111111)/TestPLF(plist=[IP()/TCP(seq=222222),IP()/UDP()])
TestPLF(plist=[y,IP()/TCP(seq=333333)])
_.show()
IP in _ and TCP in _ and UDP in _ and _[TCP].seq == 111111 and _[TCP:2].seq==222222 and _[TCP:3].seq == 333333


############
############
+ ISAKMP transforms test

= ISAKMP creation
~ IP UDP ISAKMP 
p=IP(src='192.168.8.14',dst='10.0.0.1')/UDP()/ISAKMP()/ISAKMP_payload_SA(prop=ISAKMP_payload_Proposal(trans=ISAKMP_payload_Transform(transforms=[('Encryption', 'AES-CBC'), ('Hash', 'MD5'), ('Authentication', 'PSK'), ('GroupDesc', '1536MODPgr'), ('KeyLength', 256), ('LifeType', 'Seconds'), ('LifeDuration', 86400L)])/ISAKMP_payload_Transform(res2=12345,transforms=[('Encryption', '3DES-CBC'), ('Hash', 'SHA'), ('Authentication', 'PSK'), ('GroupDesc', '1024MODPgr'), ('LifeType', 'Seconds'), ('LifeDuration', 86400L)])))
p.show()
p


= ISAKMP manipulation
~ ISAKMP
p[ISAKMP_payload_Transform:2]
_.res2 == 12345

= ISAKMP assembly
~ ISAKMP 
hexdump(p)
str(p) == "E\x00\x00\x96\x00\x01\x00\x00@\x11\xa7\x9f\xc0\xa8\x08\x0e\n\x00\x00\x01\x01\xf4\x01\xf4\x00\x82\xbf\x1e\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00z\x00\x00\x00^\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00R\x01\x01\x00\x00\x03\x00\x00'\x00\x01\x00\x00\x80\x01\x00\x07\x80\x02\x00\x01\x80\x03\x00\x01\x80\x04\x00\x05\x80\x0e\x01\x00\x80\x0b\x00\x01\x00\x0c\x00\x03\x01Q\x80\x00\x00\x00#\x00\x0109\x80\x01\x00\x05\x80\x02\x00\x02\x80\x03\x00\x01\x80\x04\x00\x02\x80\x0b\x00\x01\x00\x0c\x00\x03\x01Q\x80"


= ISAKMP disassembly
~ ISAKMP
q=IP(str(p))
q.show()
q[ISAKMP_payload_Transform:2]
_.res2 == 12345



############
############
+ Dot11 tests


= WEP tests
~ wifi wep Dot11 LLC SNAP IP TCP
conf.wepkey = "ABCDEFGH"
str(Dot11WEP()/LLC()/SNAP()/IP()/TCP(seq=12345678))
assert(_ == '\x00\x00\x00\x00\x1e\xafK5G\x94\xd4m\x81\xdav\xd4,c\xf1\xfe{\xfc\xba\xd6;T\x93\xd0\t\xdb\xfc\xa5\xb9\x85\xce\x05b\x1cC\x10\xd7p\xde22&\xf0\xbcUS\x99\x83Z\\D\xa6')
Dot11WEP(_)
assert(TCP in _ and _[TCP].seq == 12345678)


############
############
+ Network tests

* Those tests need network access

= Sending and receiving an ICMP
~ netaccess IP ICMP
x=sr1(IP(dst="www.apple.com")/ICMP(),timeout=3)
x
x is not None and ICMP in x and x[ICMP].type == 0

= DNS request
~ netaccess IP UDP DNS
* A possible cause of failure could be that the open DNS (147.210.18.138)
* is not reachable or down.
dns_ans = sr1(IP(dst="147.210.18.138")/UDP()/DNS(rd=1,qd=DNSQR(qname="www.slashdot.com")))
dns_ans



############
############
+ More complex tests

= Implicit logic
~ IP TCP
a=IP(ttl=(5,10))/TCP(dport=[80,443])
[p for p in a]
len(_) == 12


############
############
+ Real usages

= Port scan
~ netaccess IP TCP
ans,unans=sr(IP(dst="www.google.com/30")/TCP(dport=[80,443]),timeout=2)
ans.make_table(lambda (s,r): (s.dst, s.dport, r.sprintf("{TCP:%TCP.flags%}{ICMP:%ICMP.code%}")))

= Traceroute function
~ netaccess
* Let's test traceroute
traceroute("www.slashdot.org")
ans,unans=_

= Result manipulation
~ netaccess
ans.nsummary()
s,r=ans[0]
s.show()
s.show(2)

= DNS packet manipulation
~ netaccess DNS
* We have to recalculate IP and UDP length because
* DNS is not able to reassemble correctly
dns_ans.show()
del(dns_ans[IP].len)
del(dns_ans[UDP].len)
dns_ans.show2()
dns_ans[DNS].an.show()
DNS in IP(str(dns_ans))

= Arping
~ netaccess
* This test assumes the local network is a /24. This is bad.
conf.route.route("0.0.0.0")[2]
arping(_+"/24")