Repository: secdev/scapy

Issue:
Dataofs change can break packet.show2() if checksum is recalculated
#### Brief description

After a packet is created with TCP options, if the dataofs field is reduced, the packet is re-parsed, and checksum deleted, scapy will break. It's not clear to me at all why this happens. This was the result of me trying to write code using scapy to verify packet checksums that arrive on my container. 

TCP is affected by this.

#### Environment

- Scapy version: 2.4.0
- Python version: 3..52
- Operating System: Ubuntu 16.04

Just sending these packets between docker containers when this broke, and I recreated the example below without networking. 

#### How to reproduce

I have pickled the packet I'm looking at so you can play with the exact same object. 

Simple example below: 
```
>>> p = pickle.loads( b'\x80\x03cscapy.layers.inet\nIP\nq\x00)Rq\x01C<E\x00\x00<\xa8\xab@\x00@\x069\xe8\xac\x11\x00\x0x10\x87\xe4\x00\x00\x02\x04\x05\xb4\x04\x02\x08\n\xf7\xa26C\x00\x00\x00\x00\x01\x03\x03\x07q\x02\x85q\x03b.')
>>> p.show()
###[ IP ]### 
  version= 4
  ihl= 5
  tos= 0x0
  len= 60
  id= 43179
  flags= DF
  frag= 0
  ttl= 64
  proto= tcp
  chksum= 0x39e8
  src= 172.17.0.2
  dst= 172.17.0.4
  \options\
###[ TCP ]### 
     sport= 43320
     dport= 21094
     seq= 3330672087
     ack= 0
     dataofs= 10
     reserved= 0
     flags= S
     window= 29200
     chksum= 0x87e4
     urgptr= 0
     options= [('MSS', 1460), ('SAckOK', b''), ('Timestamp', (4154603075, 0)), ('NOP', None), ('WScale', 7)]
>>> p["TCP"].dataofs = 8 
>>> p2 = IP(bytes(p))
>>> del p2["TCP"].chksum
>>> p2.show2()
Traceback (most recent call last):
  File "/usr/lib/python3.5/code.py", line 91, in runcode
    exec(code, self.locals)
  File "<console>", line 1, in <module>
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/packet.py", line 1071, in show2
    return self.__class__(raw(self)).show(dump, indent, lvl, label_lvl)
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/compat.py", line 96, in raw
    return bytes(x)
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/packet.py", line 345, in __bytes__
    return self.build()
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/packet.py", line 444, in build
    p = self.do_build()
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/packet.py", line 429, in do_build
    pay = self.do_build_payload()
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/packet.py", line 416, in do_build_payload
    return self.payload.do_build()
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/packet.py", line 426, in do_build
    pkt = self.self_build()
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/packet.py", line 407, in self_build
    p = f.addfield(self, p, val)
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/fields.py", line 521, in addfield
    return s + self.i2m(pkt, val)
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/layers/inet.py", line 308, in i2m
    oval = struct.pack(ofmt, *oval)
struct.error: pack expected 2 items for packing (got 1)
>>> raw(p2)
Traceback (most recent call last):
  File "/usr/lib/python3.5/code.py", line 91, in runcode
    exec(code, self.locals)
  File "<console>", line 1, in <module>
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/compat.py", line 96, in raw
    return bytes(x)
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/packet.py", line 345, in __bytes__
    return self.build()
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/packet.py", line 444, in build
    p = self.do_build()
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/packet.py", line 429, in do_build
    pay = self.do_build_payload()
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/packet.py", line 416, in do_build_payload
    return self.payload.do_build()
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/packet.py", line 426, in do_build
    pkt = self.self_build()
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/packet.py", line 407, in self_build
    p = f.addfield(self, p, val)
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/fields.py", line 521, in addfield
    return s + self.i2m(pkt, val)
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/layers/inet.py", line 308, in i2m
    oval = struct.pack(ofmt, *oval)
struct.error: pack expected 2 items for packing (got 1)
```

#### Actual result

Scapy breaks.

#### Expected result

Scapy shouldn't break. 


Discussion:
I cannot reproduce this issue. Could you share the content of `p` using `p.command()` instead or provide a shorter example? Also, please test this issue against the current development version.
Sorry, your packet seems wrong:

```
>> p = pickle.loads( b'\x80\x03cscapy.layers.inet\nIP\nq\x00)Rq\x01C<E\x00\x00<\xa8\xab@\x00@\x069\xe8\xac\x11\x00\x0x10\x87\xe4\x00\x00\x02\x04\x05\xb4\x04\x02\x08\n\xf7\xa26C\x00\x00\x00\x00\x01\x03\x03\x07q\x02\x85q\x03b.')
  File "<ipython-input-14-9bf9f0f26567>", line 1
    p = pickle.loads( b'\x80\x03cscapy.layers.inet\nIP\nq\x00)Rq\x01C<E\x00\x00<\xa8\xab@\x00@\x069\xe8\xac\x11\x00\x0x10\x87\xe4\x00\x00\x02\x04\x05\xb4\x04\x02\x08\n\xf7\xa26C\x00\x00\x00\x00\x01\x03\x03\x07q\x02\x85q\x03b.')
                     ^
SyntaxError: (value error) invalid \x escape at position 91
```

Can you fix it?
Hmm weird that the pickling isn't working. I can reproduce on scapy 2.4.0 with just the following:

```
>>> p = IP()/TCP(options=[('MSS', 1460), ('SAckOK', b''), ('Timestamp', (4154603075, 0)), ('NOP', None), ('WScale', 7)])
>>> p.show2()
###[ IP ]### 
  version= 4
  ihl= 5
  tos= 0x0
  len= 60
  id= 1
  flags= 
  frag= 0
  ttl= 64
  proto= tcp
  chksum= 0x7cb9
  src= 127.0.0.1
  dst= 127.0.0.1
  \options\
###[ TCP ]### 
     sport= ftp_data
     dport= http
     seq= 0
     ack= 0
     dataofs= 10
     reserved= 0
     flags= S
     window= 8192
     chksum= 0xfbb3
     urgptr= 0
     options= [('MSS', 1460), ('SAckOK', b''), ('Timestamp', (4154603075, 0)), ('NOP', None), ('WScale', 7)]

>>> p[TCP].dataofs=8
>>> p2 = IP(bytes(p))
>>> del p2["TCP"].chksum
>>> p2.show2()
Traceback (most recent call last):
  File "/usr/lib/python3.5/code.py", line 91, in runcode
    exec(code, self.locals)
  File "<console>", line 1, in <module>
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/packet.py", line 1071, in show2
    return self.__class__(raw(self)).show(dump, indent, lvl, label_lvl)
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/compat.py", line 96, in raw
    return bytes(x)
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/packet.py", line 345, in __bytes__
    return self.build()
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/packet.py", line 444, in build
    p = self.do_build()
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/packet.py", line 429, in do_build
    pay = self.do_build_payload()
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/packet.py", line 416, in do_build_payload
    return self.payload.do_build()
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/packet.py", line 426, in do_build
    pkt = self.self_build()
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/packet.py", line 407, in self_build
    p = f.addfield(self, p, val)
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/fields.py", line 521, in addfield
    return s + self.i2m(pkt, val)
  File "/home/kbock/.local/lib/python3.5/site-packages/scapy/layers/inet.py", line 308, in i2m
    oval = struct.pack(ofmt, *oval)
struct.error: pack expected 2 items for packing (got 1)
>>> 
```
Basically just create a packet with a couple TCP options, reduce the dataofs beneath the actual length of the options, reparse the packet (so some of the options should become packet payload), delete the checksum, and things break.
You're right, Scapy should probably issue a warning, and anyway only raise an exception if/when `conf.debug_dissector` is `True`
This works as expected with Python2 but fail with Python3:
```
cat test_1665.py 
from scapy.all import *

p = IP()/TCP(options=[('MSS', 1460), ('SAckOK', b''), ('Timestamp', (4154603075, 0)), ('NOP', None), ('WScale', 7)])
p.show2()
p[TCP].dataofs=8
p2 = IP(bytes(p))
del p2["TCP"].chksum

try:
    p2.show2()
except Exception as e:
    sys.stderr.write("Got %s\n" % e)

python3 test_1665.py > /dev/null 
Got pack expected 2 items for packing (got 1)

python2 test_1665.py > /dev/null 
```

Resolving patch:
diff --git a/scapy/layers/inet.py b/scapy/layers/inet.py
--- a/scapy/layers/inet.py
+++ b/scapy/layers/inet.py
@@ -381,7 +381,8 @@ def i2m(self, pkt, x):
                     ofmt = TCPOptions[0][onum][1]
                     if onum == 5:  # SAck
                         ofmt += "%iI" % len(oval)
-                    if ofmt is not None and (not isinstance(oval, str) or "s" in ofmt):  # noqa: E501
+                    _test_isinstance = not isinstance(oval, (bytes, str))
+                    if ofmt is not None and (_test_isinstance or "s" in ofmt):
                         if not isinstance(oval, tuple):
                             oval = (oval,)
                         oval = struct.pack(ofmt, *oval)


Test patch:
diff --git a/test/regression.uts b/test/regression.uts
--- a/test/regression.uts
+++ b/test/regression.uts
@@ -6800,6 +6800,11 @@ raw(TCP(b"\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00`\x02 \x00\x00\x00\x00\x0
 = TCP options: MPTCP - basic build using bytes
 raw(TCP(options=[(30, b"\x10\x03\xc1\x1c\x95\x9b\x81R_1")])) == b"\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x80\x02 \x00\x00\x00\x00\x00\x1e\x0c\x10\x03\xc1\x1c\x95\x9b\x81R_1"
 
+= TCP options: invalid data offset
+data = b'\x00\x14\x00P\x00\x00\x00\x00\x00\x00\x00\x00\x80\x02 \x00\x1b\xb8\x00\x00\x02\x04\x05\xb4\x04\x02\x08\x06\xf7\xa26C\x00\x00\x00\x00\x01\x03\x03\x07'
+p = TCP(data)
+assert TCP in p and Raw in p and len(p.options) == 3
+
 = TCP random options
 pkt = TCP()
 random.seed(0x2807)