├── .gitignore ├── README.md ├── reassembler.jpg ├── reassembler ├── __init__.py ├── __main__.py └── reassembler.py ├── reassembler_in_onfile.py ├── sample_packets ├── final_frags.pcap ├── fragments.pcap ├── fragments2.pcap └── fragments3.pcap └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | reassembler/__pycache__/* 2 | build/* 3 | dist/* 4 | *.egg-info/* 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reassembler 2 | ## A Python implementation of the various OS IPv4 packet fragment reassembly engines. 3 | 4 | ### One Packet in => Six Packets out 5 | 6 | This module will reassemble fragmented packets using common used fragmentation reassembly techniques. It then generates 6 pcap files. It also prints the payloads to the screen and shows you how each of the operating systems would see the packets after they reassemble them using their defragmentation engine. 7 | 8 | This is a rewrite of the original released in 2012 to support Python3. 9 | [Associated GIAC SANS Gold Paper](https://www.sans.org/reading-room/whitepapers/tools/ip-fragment-reassembly-scapy-33969) 10 | 11 | --- 12 | 13 | ### Are Overlapping fragments still an issue? 14 | 15 | 10-16-2020: [Don Williams](https://twitter.com/bashwrapper) and I did a survey of the major OSes to confirm the status of their reassembly engines. Here are the results: 16 | 17 | - [Linux](https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=c30f1fc041b74ecdb072dd44f858750414b8b19f) 18 | : The Linux OS's have begun silently ignoring overlapping IPv4 fragments. IPv6 rejects them by defalt. 19 | 20 | - [Windows](https://portal.msrc.microsoft.com/en-us/security-guidance/advisory/ADV180022): The posted "Fix" requires that you turn off ALL fragment reassembly, not just overlaps. It is not enabled by default. I have been unable to get any Windows OS to respond to overlaps since Vista. 21 | 22 | - Macintosh: Tested on 10-16-2020 and it was still reassembling overlapping fragments without complaint. 23 | 24 | --- 25 | 26 | ### Installing 27 | 28 | ```pip install reassembler``` 29 | 30 | or 31 | 32 | ```pip install git+https://github.com/markbaggett/reassembler``` 33 | 34 | --- 35 | 36 | ### Running 37 | 38 | After pip install the command 'reassembler' is added to your path. 39 | 40 | ``` 41 | $ reassembler assemble ./sample_packets/final_frags.pcap 42 | ``` 43 | 44 | 45 | or you can execute it as a python module 46 | 47 | ``` 48 | $ python -m reassembler 49 | The first argument must by either "scan" or "assemble". 50 | Try 'reassembler assemble -h' for help assembling fragmented packets. 51 | Try 'reassembler scan -h' for help. with the scanning host to identify their reassembly policy. 52 | ``` 53 | 54 | 55 | #### - assemble submenu 56 | You can use reassembler to reassemble fragmented packet to and generate 6 individual PCAPS that show you what different reassembly policies would see. For more details run ```reassembler assemble -h``` 57 | 58 | In its simplest form the tool is run like this: 59 | 60 | ``` 61 | (573) student@SEC573:~/Documents/pythonclass/reassembler$ reassembler assemble ./sample_packets/final_frags.pcap 62 | Reading fragmented packets from disk. 63 | Packet fragments found. Collecting fragments now. 64 | Reassemble packets between hosts 172.16.120.191 and 172.16.120.1? [Y/N]y 65 | Reassembled using policy: First (Windows*, SUN, MacOS*, HPUX) 66 | GET /etc/passwd 67 | Host:www.supersecret.net 68 | User-Agent:evil-browser 69 | 70 | 71 | Reassembled using policy: Last/RFC791 (Cisco) 72 | GET /not/catdog 73 | Host:www.supersegway.org 74 | User-Agent:good-browser 75 | 76 | 77 | Reassembled using policy: Linux (Linux prior to v5.8) 78 | GET /etc/catdog 79 | Host:www.supersecret.net 80 | User-Agent:good-browser 81 | 82 | 83 | Reassembled using policy: BSD (AIX, FreeBSD, HPUX, VMS) 84 | GET /etc/passwd 85 | Host:www.supersecret.net 86 | User-Agent:good-browser 87 | 88 | 89 | Reassembled using policy: BSD-Right (HP Jet Direct) 90 | GET /not/catdog 91 | Host:www.supersegway.org 92 | User-Agent:evil-browser 93 | 94 | 95 | Reassembled using policy: Other (Some IoT Device somewhere) 96 | GET /not/passwd 97 | Host:www.supersegway.org 98 | User-Agent:evil-browser 99 | 100 | (573) student@SEC573:~/Documents/pythonclass/reassembler$ ls *.pcap 101 | reassembled-1-bsd.pcap reassembled-1-first.pcap reassembled-1-other.pcap 102 | reassembled-1-bsdright.pcap reassembled-1-linux.pcap reassembled-1-rfc791.pcap 103 | ``` 104 | 105 | #### - scan submenu 106 | You can use reassembler to scan a host and determine what reassembly policy it uses. Today reassembler identified 6 posible reassembly engine policies, but there are others. For more details run ```reassembler scan -h``` 107 | 108 | Note: The scanner requires root privileges to craft packets. When installed, the reassembler binary is placed in the /bin directory of your Python installation. Using 'sudo' changes which python interpreter you are using when running from a virtual machine. To run reassembler from a virtual machine use the syntax shown here: 109 | 110 | ``` 111 | (573) student@SEC573:~/Documents/pythonclass/reassembler$ sudo -s "PATH=$PATH" reassembler scan 192.168.1.10/30 112 | [sudo] password for student: 113 | Checking host 192.168.1.8: 114 | + 192.168.1.8 responded to a ping request! 115 | + 192.168.1.8 is reassembling normal (non-overlapping) fragmented ping packets. 116 | + 192.168.1.8 is NOT responding to overlapping fragments ping packets. 117 | + Overlapping fragments ignored by 192.168.1.8 118 | Checking host 192.168.1.9: 119 | + 192.168.1.9 responded to a ping request! 120 | + 192.168.1.9 is reassembling normal (non-overlapping) fragmented ping packets. 121 | + 192.168.1.9 is NOT responding to overlapping fragments ping packets. 122 | + Overlapping fragments ignored by 192.168.1.9 123 | Checking host 192.168.1.10: 124 | + 192.168.1.10 responded to a ping request! 125 | + 192.168.1.10 is reassembling normal (non-overlapping) fragmented ping packets. 126 | + 192.168.1.10 is reassembling overlapping fragmented ping packets. 127 | + 192.168.1.10 responds with reassembly Linux 128 | Checking host 192.168.1.11: 129 | + Can not ping 192.168.1.11. 130 | ``` 131 | 132 | --- 133 | 134 | ### As a Module 135 | 136 | ``` 137 | >>> import reassembler 138 | >>> reassembler.rfc791(reassembler.genjudyfrags()) 139 | >>> 140 | >>> reassembler.first(reassembler.genjudyfrags()) 141 | >>> 142 | >>> reassembler.linux(reassembler.genjudyfrags()) 143 | >>> 144 | >>> reassembler.scan_host("192.168.1.1") 145 | Checking host 192.168.1.1: 146 | + 192.168.1.1 responded to a ping request! 147 | + 192.168.1.1 is reassembling normal (non-overlapping) fragmented ping packets. 148 | + 192.168.1.1 is NOT responding to overlapping fragments ping packets. 149 | + Overlapping fragments ignored by 192.168.1.1 150 | 151 | ``` 152 | 153 | 154 | --- 155 | 156 | ![](reassembler.jpg) -------------------------------------------------------------------------------- /reassembler.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkBaggett/reassembler/fd9359706460d87810e60912bf25f4f3441abb5a/reassembler.jpg -------------------------------------------------------------------------------- /reassembler/__init__.py: -------------------------------------------------------------------------------- 1 | from .reassembler import * -------------------------------------------------------------------------------- /reassembler/__main__.py: -------------------------------------------------------------------------------- 1 | from reassembler import * 2 | from scapy.all import * 3 | import argparse 4 | import os 5 | import sys 6 | import ipaddress 7 | import socket 8 | 9 | 10 | def process_scan_args(args): 11 | #Check for host name instead of IPs 12 | target = args.target 13 | if not target.replace(".","").replace(r"/","").isdigit(): 14 | hostname = target.split(r"/")[0] 15 | ip = socket.gethostbyname(hostname) 16 | target = target.replace(hostname, ip) 17 | #Expand target specifier into network range. 18 | try: 19 | tgt_list = ipaddress.ip_network(target, strict=False) 20 | except ValueError: 21 | print(f"{target} doens't look like a valid IP Address or network range.") 22 | print("A single IP address is in the form '192.168.1.1'.") 23 | print("A network range uses CIDR notation in is the form '192.168.0.0/16'.") 24 | print("I'll do my best to resolve hostnames for you so target.tst/24 might work.") 25 | exit(1) 26 | else: 27 | #Scan em 28 | for ipaddr in tgt_list: 29 | reassembler.scan_host(str(ipaddr)) 30 | exit() 31 | 32 | def process_assemble_args(options): 33 | if options.demo: 34 | processfrags(genjudyfrags()) 35 | 36 | if not os.path.exists(options.pcap): 37 | print("Packet capture file not found.") 38 | sys.exit(2) 39 | 40 | print("Reading fragmented packets from disk.") 41 | packets=rdpcap(options.pcap) 42 | ippackets=[a for a in packets if a.haslayer("IP")] 43 | fragmentedpackets=[a for a in ippackets if a[IP].flags==1 or a[IP].frag > 0] 44 | 45 | if len(fragmentedpackets)==0: 46 | print("No fragments in packet capture.") 47 | sys.exit(2) 48 | 49 | uniqipids={} 50 | for a in fragmentedpackets: 51 | uniqipids[a[IP].id]='we are here' 52 | 53 | for ipid in list(uniqipids.keys()): 54 | print("Packet fragments found. Collecting fragments now.") 55 | fragmenttrain = [ a for a in fragmentedpackets if a[IP].id == ipid ] 56 | processit = input("Reassemble packets between hosts "+str(fragmenttrain[0][IP].src)+" and "+str(fragmenttrain[0][IP].dst)+"? [Y/N]") 57 | if str(processit).lower()=="y": 58 | if not options.quiet: 59 | processfrags(fragmenttrain, not options.checksum, options.bytes) 60 | if not options.nowrite: 61 | writefrags(fragmenttrain, options.prefix, not options.checksum) 62 | exit() 63 | 64 | def process_main(args): 65 | print('The first argument must by either "scan" or "assemble".') 66 | print("Try 'reassembler assemble -h' for help assembling fragmented packets. ") 67 | print("Try 'reassembler scan -h' for help. with the scanning host to identify their reassembly policy.") 68 | exit() 69 | 70 | 71 | def cli(): 72 | parser=argparse.ArgumentParser(usage="Try 'reassembler scan -h' or 'reassembler assemble -h' for help.\n\n") 73 | subparser = parser.add_subparsers() 74 | assemble = subparser.add_parser("assemble") 75 | assemble.add_argument('pcap',default="",help='Read the specified packet capture') 76 | assemble.add_argument('-d','--demo',action='store_true', help='Generate classic fragment test pattern and reassemble it.') 77 | assemble.add_argument('-n','--no-write',action='store_true', dest="nowrite", help='Suppress writing 5 files to disk with the payloads.') 78 | assemble.add_argument('-b','--bytes',action='store_true', help='Process Payloads as bytes and never as strings.') 79 | assemble.add_argument('-q','--quiet',action='store_true', help='Do not print payloads to screen.') 80 | assemble.add_argument('-p','--prefix',default='reassembled', help='Specify the prefix for file names') 81 | assemble.add_argument('-c','--checksum',action="store_true", help='Do not recalculate transport layer protocol checksums.') 82 | assemble.set_defaults(func=process_assemble_args) 83 | scan = subparser.add_parser("scan") 84 | scan.add_argument("target", help="Identify Policy used by specified IP Address or Network Range") 85 | scan.set_defaults(func=process_scan_args) 86 | parser.set_defaults(func = process_main ) 87 | 88 | args=parser.parse_args() 89 | args.func(args) 90 | 91 | 92 | 93 | cli() -------------------------------------------------------------------------------- /reassembler/reassembler.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | #This program will work in either Python2 or Python3 4 | #The following code will reassemble fragmented packets using the BSD, BSD-Right, First, Last and Linux so that an analyst gets a better understanding of how an attack wotmp affect each of his different hosts. 5 | #This program was written by @MarkBaggett and is available for download at https://github.com/markbaggett/reassembler 6 | #If you have questions about the script you can read the associated SANS Gtmp paper called "IP Fragment Reassembly with Scapy" by Mark Baggett 7 | 8 | from scapy.all import * 9 | import sys 10 | import random 11 | if sys.version_info.major==2: 12 | from cStringIO import StringIO as BytesIO 13 | input = raw_input 14 | else: 15 | from io import BytesIO 16 | 17 | 18 | def clean_reassembled_packets(pkt_in, fix_checksum = True): 19 | #This function fixes fields such as checksums,lengths and adds an ethernet frame tested on udp and icmp 20 | #Delete any fields we want recalculated 21 | pkt_in[IP].flags=0 22 | del pkt_in[IP].chksum 23 | embedded = pkt_in[IP].payload.__class__ 24 | #scapy incorrectly sets len on UDP packets and adds a "Padding" layer. Patch that here. 25 | if hasattr( pkt_in[IP].payload.__class__, "len"): 26 | tmp = embedded(bytes(pkt_in[IP].payload)) 27 | if tmp.haslayer(Padding): 28 | pkt_in[IP].payload.len = pkt_in[IP].payload.len + len(tmp[Padding].load) 29 | if hasattr( pkt_in[IP].payload.__class__, "chksum") and fix_checksum: 30 | del pkt_in[IP].payload.chksum 31 | #Force recalc of checksums and other blank fields by turning things into bytes and back 32 | newpkt = Ether() / IP(bytes(pkt_in[IP])) 33 | newpkt[embedded] = embedded(bytes(pkt_in[IP].payload)) 34 | if pkt_in.haslayer(Ether): 35 | newpkt[Ether].src = pkt_in[Ether].src 36 | newpkt[Ether].dst = pkt_in[Ether].dst 37 | return newpkt 38 | 39 | 40 | def rfc791(fragmentsin, fix_checksum=True): 41 | #Last to arrive temporaly wins 42 | buffer=BytesIO() 43 | for pkt in fragmentsin: 44 | if pkt[IP].frag == 0: 45 | first_fragment = pkt.copy() 46 | buffer.seek(pkt[IP].frag*8) 47 | buffer.write(bytes(pkt[IP].payload)) 48 | first_fragment[IP].payload = first_fragment[IP].payload.__class__(bytes(buffer.getvalue())) 49 | return clean_reassembled_packets(first_fragment, fix_checksum) 50 | 51 | def first(fragmentsin, fix_checksum=True): 52 | #First to arrive temporaly wins 53 | buffer=BytesIO() 54 | for pkt in fragmentsin[::-1]: 55 | if pkt[IP].frag == 0: 56 | first_fragment = pkt.copy() 57 | buffer.seek(pkt[IP].frag*8) 58 | buffer.write(bytes(pkt[IP].payload)) 59 | first_fragment[IP].payload = first_fragment[IP].payload.__class__(bytes(buffer.getvalue())) 60 | return clean_reassembled_packets(first_fragment, fix_checksum) 61 | 62 | 63 | def bsdright(fragmentsin, fix_checksum=True): 64 | #highest offset , Tie to last temporaly 65 | buffer=BytesIO() 66 | for pkt in sorted(fragmentsin, key= lambda x:x[IP].frag): 67 | if pkt[IP].frag == 0: 68 | first_fragment = pkt.copy() 69 | buffer.seek(pkt[IP].frag*8) 70 | buffer.write(bytes(pkt[IP].payload)) 71 | first_fragment[IP].payload = first_fragment[IP].payload.__class__(bytes(buffer.getvalue())) 72 | return clean_reassembled_packets(first_fragment, fix_checksum) 73 | 74 | def bsd(fragmentsin, fix_checksum=True): 75 | #lowest offset, Tie to first 76 | buffer=BytesIO() 77 | for pkt in sorted(fragmentsin, key=lambda x:x[IP].frag)[::-1]: 78 | if pkt[IP].frag == 0: 79 | first_fragment = pkt.copy() 80 | buffer.seek(pkt[IP].frag*8) 81 | buffer.write(bytes(pkt[IP].payload)) 82 | first_fragment[IP].payload = first_fragment[IP].payload.__class__(bytes(buffer.getvalue())) 83 | return clean_reassembled_packets(first_fragment, fix_checksum) 84 | 85 | def linux(fragmentsin, fix_checksum=True): 86 | #Lowest offset, Tie to last 87 | buffer=BytesIO() 88 | for pkt in sorted(fragmentsin, key= lambda x:x[IP].frag, reverse=True): 89 | if pkt[IP].frag == 0: 90 | first_fragment = pkt.copy() 91 | buffer.seek(pkt[IP].frag*8) 92 | buffer.write(bytes(pkt[IP].payload)) 93 | first_fragment[IP].payload = first_fragment[IP].payload.__class__(bytes(buffer.getvalue())) 94 | return clean_reassembled_packets(first_fragment, fix_checksum) 95 | 96 | def other(fragmentsin, fix_checksum=True): 97 | #highest offset, Tie to first 98 | buffer=BytesIO() 99 | for pkt in sorted(fragmentsin, key= lambda x:x[IP].frag, reverse=True)[::-1]: 100 | if pkt[IP].frag == 0: 101 | first_fragment = pkt.copy() 102 | buffer.seek(pkt[IP].frag*8) 103 | buffer.write(bytes(pkt[IP].payload)) 104 | first_fragment[IP].payload = first_fragment[IP].payload.__class__(bytes(buffer.getvalue())) 105 | return clean_reassembled_packets(first_fragment, fix_checksum) 106 | 107 | 108 | def normal_fragmented_ping(ipaddr): 109 | #Create a ping request with normal nonoverlapping fragments and see if it responds 110 | simple_fragment = scapy.plist.PacketList() 111 | simple_fragment.append(IP(dst=ipaddr, flags="MF",frag=0)/ICMP(type=8,code=0,chksum=20311)/("1"*24)) 112 | simple_fragment.append(IP(dst=ipaddr, proto="icmp",frag=4)/("2"*24)) 113 | res = sr1(simple_fragment, timeout=2, verbose=0) 114 | if not res: 115 | return False, f"{ipaddr} is NOT responding to normal fragments ping packets." 116 | elif res[0][ICMP].type==0: 117 | return True, f"{ipaddr} is reassembling normal (non-overlapping) fragmented ping packets." 118 | else: 119 | return False, f"{ipaddr} is NOT responding to normal fragments." 120 | 121 | 122 | def overlap_fragmented_ping(ipaddr): 123 | #Create a ping request with overlapping fragments and see if it responds 124 | simple_fragment = scapy.plist.PacketList() 125 | simple_fragment.append(IP(dst=ipaddr, flags="MF",frag=0)/ICMP(type=8,code=0,chksum=0xafb7)/("X"*32)) 126 | simple_fragment.append(IP(dst=ipaddr, proto="icmp",frag=4)/("X"*24)) 127 | res = sr1(simple_fragment, timeout=2, verbose=0) 128 | if not res: 129 | return False, f"{ipaddr} is NOT responding to overlapping fragments ping packets." 130 | elif res[0][ICMP].type==0: 131 | return True, f"{ipaddr} is reassembling overlapping fragmented ping packets." 132 | else: 133 | return False, f"{ipaddr} is NOT responding to overlapping fragments." 134 | 135 | 136 | def ping(ipaddr): 137 | #Try to ping a host 138 | res = sr1(IP(dst=ipaddr)/ICMP()/"ABCDEFG", timeout=2, verbose=0) 139 | if not res: 140 | return False, f"Can not ping {ipaddr}." 141 | elif res[0][ICMP].type==0: 142 | return True, f"{ipaddr} responded to a ping request! " 143 | else: 144 | return False, f"Can not ping {ipaddr}." 145 | 146 | 147 | def genjudyfrags(ipaddr = "127.0.0.1", policy='first'): 148 | #Given IP and desired fragmentation policy creates fragmented ICMP pattern and sets ICMP check sum to the specified policy 149 | chksums = {'first':22110, 'linux': 13886, 'bsd': 20054, 'bsdright':9774, 'rfc791': 7718, 'other':15942} 150 | pkts=scapy.plist.PacketList() 151 | pkts.append(IP(dst=ipaddr, flags="MF",frag=0)/ICMP(type=8,code=0,chksum=chksums[policy])/("1"*24)) 152 | pkts.append(IP(dst=ipaddr,flags="MF",proto="icmp",frag=5)/("2"*16)) 153 | pkts.append(IP(dst=ipaddr,flags="MF",proto="icmp",frag=7)/("3"*24)) 154 | pkts.append(IP(dst=ipaddr,flags="MF",proto="icmp",frag=2)/("4"*32)) 155 | pkts.append(IP(dst=ipaddr,flags="MF",proto="icmp",frag=7)/("5"*24)) 156 | pkts.append(IP(dst=ipaddr,frag=10,proto="icmp")/("6"*24)) 157 | return pkts 158 | 159 | 160 | def genoverlaps(ipaddr = "127.0.0.1"): 161 | #Given IP create packets that contain unique patters for different reassemblies but all have the same checksum 162 | pkts=scapy.plist.PacketList() 163 | ipid = random.randrange(1,65535) 164 | pkts.append(IP(dst=ipaddr, id=ipid,flags="MF",frag=0)/ICMP(type=8,code=0,chksum=30334)/("11223344"*3)) 165 | pkts.append(IP(dst=ipaddr,id=ipid,flags="MF",proto="icmp",frag=5)/("22334411"*2)) 166 | pkts.append(IP(dst=ipaddr,id=ipid,flags="MF",proto="icmp",frag=7)/("33441122"*3)) 167 | pkts.append(IP(dst=ipaddr,id=ipid,flags="MF",proto="icmp",frag=2)/("44332211"*4)) 168 | pkts.append(IP(dst=ipaddr,id=ipid,flags="MF",proto="icmp",frag=7)/("44112233"*3)) 169 | pkts.append(IP(dst=ipaddr,id=ipid,frag=10,proto="icmp")/("44223311"*3)) 170 | return pkts 171 | 172 | 173 | #Testing when reassembly stopped on Windows. This works on all modern windows tested Win7,8,10 174 | #uses UDP so 'nc -nvv -l -u -p 10000' on the remote host and observe pattern 175 | # def gensimple_nooverlap(ipaddr = "127.0.0.1"): 176 | # ipid = random.randrange(1,65535) 177 | # pkts=scapy.plist.PacketList() 178 | # pkts.append(IP(dst=ipaddr,id=ipid, flags="MF",frag=0)/UDP(sport=9000, dport=10000,chksum=15758)/("11223344"*2)) 179 | # pkts.append(IP(dst=ipaddr,id=ipid, proto="udp",frag=4)/("44112233")) 180 | # pkts.append(IP(dst=ipaddr,id=ipid, flags="MF", proto="udp",frag=3)/("44332211")) 181 | # return pkts 182 | #This does NOT work on Win7,8,10 but dooes on WINXP 183 | # def gensimple_withoverlap(ipaddr = "127.0.0.1"): 184 | # ipid = random.randrange(1,65535) 185 | # pkts=scapy.plist.PacketList() 186 | # pkts.append(IP(dst=ipaddr,id=ipid, flags="MF",frag=0)/UDP(sport=9000, dport=10000,chksum=15758)/("11223344"*2)) 187 | # pkts.append(IP(dst=ipaddr,id=ipid, proto="udp",frag=4)/("44112233")) 188 | # pkts.append(IP(dst=ipaddr,id=ipid, flags="MF", proto="udp",frag=3)/("44332211"*2)) 189 | # return pkts 190 | 191 | 192 | def fix_and_send(pkts, policy): 193 | #pkts is overlapping fragments and policy is one of the reassembly functions 194 | #Reassembles the packets to see what the checksum and len shoud be then sets them then send the packet 195 | #Returns what it sent and what was returned. 196 | reassembled = policy(pkts,True) 197 | if hasattr( pkts[0][IP].payload.__class__, "chksum"): 198 | pkts[0][IP].payload.chksum = reassembled[IP].payload.chksum 199 | if hasattr( pkts[0][IP].payload.__class__, "len"): 200 | pkts[0][IP].payload.len = reassembled[IP].payload.len 201 | ans = sr1(pkts, timeout=3) 202 | return pkts, ans 203 | 204 | def match_payload(fragmented_pkts, icmp_reply): 205 | if first(fragmented_pkts)[Raw].load == icmp_reply[Raw].load: 206 | policy = "FIRST" 207 | elif bsd(fragmented_pkts)[Raw].load == icmp_reply[Raw].load: 208 | policy = "BSD" 209 | elif bsdright(fragmented_pkts)[Raw].load == icmp_reply[Raw].load: 210 | policy = "BSDRIGHT" 211 | elif linux(fragmented_pkts)[Raw].load == icmp_reply[Raw].load: 212 | policy = "Linux" 213 | elif rfc791(fragmented_pkts)[Raw].load == icmp_reply[Raw].load: 214 | policy = "RFC791" 215 | elif other(fragmented_pkts)[Raw].load == icmp_reply[Raw].load: 216 | policy = "OTHER" 217 | else: 218 | policy = f"No Match for {icmp_reply[Raw].load}" 219 | return policy 220 | 221 | 222 | def processfrags(fragmenttrain, fix_checksum = True, print_bytes=False): 223 | def print_frag(bytes_in): 224 | if print_bytes: 225 | print(bytes_in) 226 | return 227 | as_str = bytes_in 228 | try: 229 | as_str = bytes_in.decode() 230 | except: 231 | pass 232 | print(as_str) 233 | return 234 | print("Reassembled using policy: First (Windows*, SUN, MacOS*, HPUX)") 235 | print_frag(first(fragmenttrain, fix_checksum)[Raw].load) 236 | print("\nReassembled using policy: Last/RFC791 (Cisco)") 237 | print_frag(rfc791(fragmenttrain, fix_checksum)[Raw].load) 238 | print("\nReassembled using policy: Linux (Linux prior to v5.8)") 239 | print_frag(linux(fragmenttrain, fix_checksum)[Raw].load) 240 | print("\nReassembled using policy: BSD (AIX, FreeBSD, HPUX, VMS)") 241 | print_frag(bsd(fragmenttrain, fix_checksum)[Raw].load) 242 | print("\nReassembled using policy: BSD-Right (HP Jet Direct)") 243 | print_frag(bsdright(fragmenttrain, fix_checksum)[Raw].load) 244 | print("\nReassembled using policy: Other (Some IoT Device somewhere)") 245 | print_frag(other(fragmenttrain, fix_checksum)[Raw].load) 246 | 247 | def writefrags(fragmenttrain, file_prefix, fix_checksum=True): 248 | ipid = str(fragmenttrain[0][IP].id) 249 | wrpcap(f"{file_prefix}-{ipid}-first.pcap", first(fragmenttrain, fix_checksum)) 250 | wrpcap(f"{file_prefix}-{ipid}-rfc791.pcap", rfc791(fragmenttrain, fix_checksum)) 251 | wrpcap(f"{file_prefix}-{ipid}-bsd.pcap", bsd(fragmenttrain, fix_checksum)) 252 | wrpcap(f"{file_prefix}-{ipid}-bsdright.pcap", bsdright(fragmenttrain, fix_checksum)) 253 | wrpcap(f"{file_prefix}-{ipid}-linux.pcap", linux(fragmenttrain, fix_checksum)) 254 | wrpcap(f"{file_prefix}-{ipid}-other.pcap", other(fragmenttrain, fix_checksum)) 255 | 256 | 257 | def scan_host(ipaddr): 258 | print(f"Checking host {ipaddr}:") 259 | #Lets see if we can ping it first. 260 | result, msg = ping(ipaddr) 261 | print(f" + {msg}") 262 | if not result: 263 | return 264 | #Now try a fragmented ping 265 | result,msg = normal_fragmented_ping(ipaddr) 266 | print(f" + {msg}") 267 | if not result: 268 | return 269 | #Now try an overlapping fragmented ping 270 | result,msg = overlap_fragmented_ping(ipaddr) 271 | print(f" + {msg}") 272 | #Last send overlap pattern and identify which policy 273 | pkts = genoverlaps(ipaddr) 274 | result = sr1(pkts, timeout=2, verbose=0) 275 | if result: 276 | print(f" + {ipaddr} responds with reassembly {match_payload(pkts, result)}") 277 | else: 278 | print(f" + Overlapping fragments ignored by {ipaddr}") 279 | 280 | 281 | def scan_network2(mask): 282 | #Identifies policy by seeing which chksum causes the remote host to respond 283 | #takes longer than sending 1 packet an looking at the pattern because we send 6 packets and timeout on no response 284 | ans,unans = arping(mask, verbose=0) 285 | for sent,recv in ans: 286 | ipaddr = recv.psrc 287 | policy_identified = False 288 | for policy in ['first', 'linux', 'bsd','bsdright','rfc791','other']: 289 | res = sr1(genjudyfrags(ipaddr,policy), timeout=2, verbose=0) 290 | if not res: 291 | continue 292 | elif res[0][ICMP].type==0: 293 | print(f"{ipaddr} is using fragment reassembly policy {policy}") 294 | policy_identified = True 295 | if not policy_identified: 296 | print(f"{ipaddr} did not respond to any of the 6 standard overlapping fragments policies.") 297 | 298 | 299 | #Random test 300 | #scan_network("net/24") 301 | #scan_network2("net/24") 302 | #x = gensimple_withoverlap("ip") 303 | #res = fix_and_send(x, first) 304 | #print(res) 305 | 306 | 307 | -------------------------------------------------------------------------------- /reassembler_in_onfile.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | #This program will work in either Python2 or Python3 4 | #The following code will reassemble fragmented packets using the BSD, BSD-Right, First, Last and Linux so that an analyst gets a better understanding of how an attack would affect each of his different hosts. 5 | #This program was written by @MarkBaggett and is available for download at https://github.com/markbaggett/reassembler 6 | #If you have questions about the script you can read the associated SANS Gold paper called "IP Fragment Reassembly with Scapy" by Mark Baggett 7 | 8 | from scapy.all import * 9 | import six 10 | if six.PY2: 11 | from cStringIO import StringIO as StringIO 12 | input = raw_input 13 | else: 14 | from io import BytesIO as StringIO 15 | import argparse 16 | import os 17 | import sys 18 | 19 | 20 | def clean_reassembled_packets(pkt_in): 21 | pkt_in[IP].flags=0 22 | del pkt_in[IP].chksum 23 | del pkt_in[IP].len 24 | newpkt = Ether() / pkt_in[IP] 25 | if pkt_in.haslayer(Ether): 26 | newpkt[Ether].src = pkt_in[Ether].src 27 | newpkt[Ether].dst = pkt_in[Ether].dst 28 | if hasattr( newpkt[IP].payload.__class__, "chksum") and not options.checksum: 29 | del newpkt[IP].payload.chksum 30 | return newpkt 31 | 32 | 33 | def rfc791(fragmentsin): 34 | #Last to arrive temporaly wins 35 | buffer=StringIO() 36 | for pkt in fragmentsin: 37 | if pkt[IP].frag == 0: 38 | first_fragment = pkt.copy() 39 | buffer.seek(pkt[IP].frag*8) 40 | buffer.write(bytes(pkt[IP].payload)) 41 | first_fragment[IP].payload = first_fragment[IP].payload.__class__(bytes(buffer.getvalue())) 42 | return clean_reassembled_packets(first_fragment) 43 | 44 | def first(fragmentsin): 45 | #First to arrive temporaly wins 46 | buffer=StringIO() 47 | for pkt in fragmentsin[::-1]: 48 | if pkt[IP].frag == 0: 49 | first_fragment = pkt.copy() 50 | buffer.seek(pkt[IP].frag*8) 51 | buffer.write(bytes(pkt[IP].payload)) 52 | first_fragment[IP].payload = first_fragment[IP].payload.__class__(bytes(buffer.getvalue())) 53 | return clean_reassembled_packets(first_fragment) 54 | 55 | 56 | def bsdright(fragmentsin): 57 | #highest offset , Tie to last temporaly 58 | buffer=StringIO() 59 | for pkt in sorted(fragmentsin, key= lambda x:x[IP].frag): 60 | if pkt[IP].frag == 0: 61 | first_fragment = pkt.copy() 62 | buffer.seek(pkt[IP].frag*8) 63 | buffer.write(bytes(pkt[IP].payload)) 64 | first_fragment[IP].payload = first_fragment[IP].payload.__class__(bytes(buffer.getvalue())) 65 | return clean_reassembled_packets(first_fragment) 66 | 67 | def bsd(fragmentsin): 68 | #lowest offset, Tie to first 69 | buffer=StringIO() 70 | for pkt in sorted(fragmentsin, key=lambda x:x[IP].frag)[::-1]: 71 | if pkt[IP].frag == 0: 72 | first_fragment = pkt.copy() 73 | buffer.seek(pkt[IP].frag*8) 74 | buffer.write(bytes(pkt[IP].payload)) 75 | first_fragment[IP].payload = first_fragment[IP].payload.__class__(bytes(buffer.getvalue())) 76 | return clean_reassembled_packets(first_fragment) 77 | 78 | def linux(fragmentsin): 79 | #Lowest offset, Tie to last 80 | buffer=StringIO() 81 | for pkt in sorted(fragmentsin, key= lambda x:x[IP].frag, reverse=True): 82 | if pkt[IP].frag == 0: 83 | first_fragment = pkt.copy() 84 | buffer.seek(pkt[IP].frag*8) 85 | buffer.write(bytes(pkt[IP].payload)) 86 | first_fragment[IP].payload = first_fragment[IP].payload.__class__(bytes(buffer.getvalue())) 87 | return clean_reassembled_packets(first_fragment) 88 | 89 | def other(fragmentsin): 90 | #highest offset, Tie to first 91 | buffer=StringIO() 92 | for pkt in sorted(fragmentsin, key= lambda x:x[IP].frag, reverse=True)[::-1]: 93 | if pkt[IP].frag == 0: 94 | first_fragment = pkt.copy() 95 | buffer.seek(pkt[IP].frag*8) 96 | buffer.write(bytes(pkt[IP].payload)) 97 | first_fragment[IP].payload = first_fragment[IP].payload.__class__(bytes(buffer.getvalue())) 98 | return clean_reassembled_packets(first_fragment) 99 | 100 | 101 | def genjudyfrags(): 102 | pkts=scapy.plist.PacketList() 103 | pkts.append(IP(flags="MF",frag=0)/ICMP()/("1"*24)) 104 | pkts.append(IP(flags="MF",frag=5)/("2"*16)) 105 | pkts.append(IP(flags="MF",frag=7)/("3"*24)) 106 | pkts.append(IP(flags="MF",frag=2)/("4"*32)) 107 | pkts.append(IP(flags="MF",frag=7)/("5"*24)) 108 | pkts.append(IP(frag=10)/("6"*24)) 109 | return pkts 110 | 111 | def processfrags(fragmenttrain): 112 | def print_frag(bytes_in): 113 | if options.bytes: 114 | print(bytes_in) 115 | return 116 | as_str = bytes_in 117 | try: 118 | as_str = bytes_in.decode() 119 | except: 120 | pass 121 | print(as_str) 122 | return 123 | print("Reassembled using policy: First (Windows*, SUN, MacOS*, HPUX)") 124 | print_frag(first(fragmenttrain)[Raw].load) 125 | print("\nReassembled using policy: Last/RFC791 (Cisco)") 126 | print_frag(rfc791(fragmenttrain)[Raw].load) 127 | print("\nReassembled using policy: Linux (Linux prior to v5.8)") 128 | print_frag(linux(fragmenttrain)[Raw].load) 129 | print("\nReassembled using policy: BSD (AIX, FreeBSD, HPUX, VMS)") 130 | print_frag(bsd(fragmenttrain)[Raw].load) 131 | print("\nReassembled using policy: BSD-Right (HP Jet Direct)") 132 | print_frag(bsdright(fragmenttrain)[Raw].load) 133 | print("\nReassembled using policy: Other (Some IoT Device somewhere)") 134 | print_frag(other(fragmenttrain)[Raw].load) 135 | 136 | def writefrags(fragmenttrain): 137 | ipid = str(fragmenttrain[0][IP].id) 138 | wrpcap(f"{options.prefix}-{ipid}-first.pcap", first(fragmenttrain)) 139 | wrpcap(f"{options.prefix}-{ipid}-rfc791.pcap", rfc791(fragmenttrain)) 140 | wrpcap(f"{options.prefix}-{ipid}-bsd.pcap", bsd(fragmenttrain)) 141 | wrpcap(f"{options.prefix}-{ipid}-bsdright.pcap", bsdright(fragmenttrain)) 142 | wrpcap(f"{options.prefix}-{ipid}-linux.pcap", linux(fragmenttrain)) 143 | wrpcap(f"{options.prefix}-{ipid}-other.pcap", other(fragmenttrain)) 144 | 145 | 146 | def main(): 147 | print("Reading fragmented packets from disk.") 148 | packets=rdpcap(options.pcap) 149 | ippackets=[a for a in packets if a.haslayer("IP")] 150 | fragmentedpackets=[a for a in ippackets if a[IP].flags==1 or a[IP].frag > 0] 151 | 152 | if len(fragmentedpackets)==0: 153 | print("No fragments in packet capture.") 154 | sys.exit(2) 155 | 156 | uniqipids={} 157 | for a in fragmentedpackets: 158 | uniqipids[a[IP].id]='we are here' 159 | 160 | for ipid in list(uniqipids.keys()): 161 | print("Packet fragments found. Collecting fragments now.") 162 | fragmenttrain = [ a for a in fragmentedpackets if a[IP].id == ipid ] 163 | processit = input("Reassemble packets between hosts "+str(fragmenttrain[0][IP].src)+" and "+str(fragmenttrain[0][IP].dst)+"? [Y/N]") 164 | if str(processit).lower()=="y": 165 | if not options.quiet: 166 | processfrags(fragmenttrain) 167 | if not options.nowrite: 168 | writefrags(fragmenttrain) 169 | 170 | if __name__ == '__main__': 171 | parser=argparse.ArgumentParser() 172 | parser.add_argument('pcap',default="",help='Read the specified packet capture') 173 | parser.add_argument('-d','--demo',action='store_true', help='Generate classic fragment test pattern and reassemble it.') 174 | parser.add_argument('-n','--no-write',action='store_true', dest="nowrite", help='Suppress writing 5 files to disk with the payloads.') 175 | parser.add_argument('-b','--bytes',action='store_true', help='Process Payloads as bytes and never as strings.') 176 | parser.add_argument('-q','--quiet',action='store_true', help='Do not print payloads to screen.') 177 | parser.add_argument('-p','--prefix',default='reassembled', help='Specify the prefix for file names') 178 | parser.add_argument('-c','--checksum',action="store_true", help='Do not recalculate transport layer protocol checksums.') 179 | 180 | 181 | if (len(sys.argv)==1): 182 | parser.print_help() 183 | sys.exit() 184 | 185 | options=parser.parse_args() 186 | 187 | if options.demo: 188 | processfrags(genjudyfrags()) 189 | 190 | if not os.path.exists(options.pcap): 191 | print("Packet capture file not found.") 192 | sys.exit(2) 193 | 194 | main() 195 | -------------------------------------------------------------------------------- /sample_packets/final_frags.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkBaggett/reassembler/fd9359706460d87810e60912bf25f4f3441abb5a/sample_packets/final_frags.pcap -------------------------------------------------------------------------------- /sample_packets/fragments.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkBaggett/reassembler/fd9359706460d87810e60912bf25f4f3441abb5a/sample_packets/fragments.pcap -------------------------------------------------------------------------------- /sample_packets/fragments2.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkBaggett/reassembler/fd9359706460d87810e60912bf25f4f3441abb5a/sample_packets/fragments2.pcap -------------------------------------------------------------------------------- /sample_packets/fragments3.pcap: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MarkBaggett/reassembler/fd9359706460d87810e60912bf25f4f3441abb5a/sample_packets/fragments3.pcap -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="reassembler", 8 | version="2.1.1", 9 | author="MarkBaggett", 10 | author_email="lo127001@gmail.com", 11 | description="Reassemble overlapping fragments into new pcaps with different OS reassembly policies.", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | url="https://github.com/markbaggett/reassembler", 15 | license = "GNU General Public License v3 (GPLv3", 16 | packages=setuptools.find_packages(), 17 | install_requires = [ 18 | 'scapy==2.4.4', 19 | ], 20 | include_package_data = True, 21 | classifiers=[ 22 | "Programming Language :: Python :: 3", 23 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 24 | "Operating System :: OS Independent", 25 | ], 26 | python_requires='>=3.6', 27 | entry_points = { 28 | 'console_scripts': ['reassembler=reassembler.__main__:cli' 29 | ], 30 | } 31 | ) 32 | --------------------------------------------------------------------------------