how to build IPsec packet with scapy

Page content

import modules

$ python
>>> from scapy.all import *
>>> from scapy.layers.ipsec import *

build plaintext packet

>>> p = IP(src='1.1.1.1', dst='2.2.2.2') / TCP(sport=45012, dport=80) / Raw('testdata') 
>>> p = IP(str(p))

setup SA

>>> sa = SecurityAssociation(ESP, spi=0xdeadbeef, crypt_algo='AES-CBC',crypt_key='sixteenbytes key')

Encrypt w/o IV

>>> e = sa.encrypt(p, 5)
>>> e
<IP  version=4L ihl=5L tos=0x0 len=76 id=1 flags= frag=0L ttl=64 proto=esp chksum=0x747a src=1.1.1.1 dst=2.2.2.2 |<ESP  spi=0xdeadbeef seq=5 data='uD\x7fdj19\xe7\xc4\xff8\x10\xcdQ\xf0\xa6\x1e!\x84\xc3>!\x18\xa6\xf6\xb8\x93\xc6it\x9a\xfc\x1c\xee\xe5C\xcd\xf0\x7fD\xca\x8d\xadKh\xa8\xe5x' |>>
>>> e.show()
###[ IP ]###
  version   = 4L
  ihl       = 5L
  tos       = 0x0
  len       = 76
  id        = 1
  flags     = 
  frag      = 0L
  ttl       = 64
  proto     = esp
  chksum    = 0x747a
  src       = 1.1.1.1
  dst       = 2.2.2.2
  \options   \
###[ ESP ]###
     spi       = 0xdeadbeef
     seq       = 5
     data      = 'uD\x7fdj19\xe7\xc4\xff8\x10\xcdQ\xf0\xa6\x1e!\x84\xc3>!\x18\xa6\xf6\xb8\x93\xc6it\x9a\xfc\x1c\xee\xe5C\xcd\xf0\x7fD\xca\x8d\xadKh\xa8\xe5x'

Encrypt w/ IV

>>> e = sa.encrypt(p, 5, "1234567890123456")
>>> e
<IP  version=4L ihl=5L tos=0x0 len=76 id=1 flags= frag=0L ttl=64 proto=esp chksum=0x747a src=1.1.1.1 dst=2.2.2.2 |<ESP  spi=0xdeadbeef seq=5 data='1234567890123456\xa4\x0b\xebZ\xa7\xc8\xb6\x95\xfb\x13\x07\xc5TD\xa2\xe7DP\xfcP\xa5y\xc4\x06W\xe8\xf5\xf0\x86\xe1\x0c\xfd' |>>
>>> e.show()
###[ IP ]###
  version   = 4L
  ihl       = 5L
  tos       = 0x0
  len       = 76
  id        = 1
  flags     = 
  frag      = 0L
  ttl       = 64
  proto     = esp
  chksum    = 0x747a
  src       = 1.1.1.1
  dst       = 2.2.2.2
  \options   \
###[ ESP ]###
     spi       = 0xdeadbeef
     seq       = 5
     data      = '1234567890123456\xa4\x0b\xebZ\xa7\xc8\xb6\x95\xfb\x13\x07\xc5TD\xa2\xe7DP\xfcP\xa5y\xc4\x06W\xe8\xf5\xf0\x86\xe1\x0c\xfd'

data 의 시작 부분에 IV값 ‘1234567890123456’이 있음을 알 수 있다. ESP는 SPI(4B), SEQ(4B), IV(16B) , Encrypted Data 형태로 구성된다.

만일 IV값의 길이가 틀리면 다음와 같이 에러 출력

>>> e = sa.encrypt(p, 5, "1234567890")
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "ipsec.py", line 903, in encrypt
    return self._encrypt_esp(pkt, seq_num=seq_num, iv=iv)
  File "ipsec.py", line 787, in _encrypt_esp
    raise TypeError('iv length must be %s' % self.crypt_algo.iv_size)
TypeError: iv length must be 16

SecurityAssocaition 옵션

1. tunnel_header

Tunnel mode를 사용하려면 tunnel_header 인자로 outer IP header instance를 넘긴다.

>>> outer_ip = IP(src='10.10.10.10', dst='20.20.20.20')
>>> sa = SecurityAssociation(ESP, spi=0xdeadbeef, crypt_algo='AES-CBC',crypt_key='sixteenbytes key', tunnel_header=outer_ip)
>>> e = sa.encrypt(p, 5, "1234567890123456")
>>> e 
<IP  version=4L ihl=5L tos=0x0 len=108 id=1 flags= frag=0L ttl=64 proto=esp chksum=0x3e24 src=10.10.10.10 dst=20.20.20.20 |<ESP  spi=0xdeadbeef seq=5 data='1234567890123456`\xc6\xa9\xe9Q\x97-\xd6\xe5\x07\xb3\x85\xfb\xc9\xeaz\x07~\xfe\x14\xd1\x19\xd8F\x8cS\x10[\x8f\xfe\x93_ut\xef1\xc0\xc4|\xdb\xccH\xea\xf8\xd2\xd0jj\xeeD\x88\x80\xc5}U\xb5_x)\xaal/,\x96' |>>
>>> e.show()
###[ IP ]###
  version   = 4L
  ihl       = 5L
  tos       = 0x0
  len       = 108
  id        = 1
  flags     = 
  frag      = 0L
  ttl       = 64
  proto     = esp
  chksum    = 0x3e24
  src       = 10.10.10.10
  dst       = 20.20.20.20
  \options   \
###[ ESP ]###
     spi       = 0xdeadbeef
     seq       = 5
     data      = '1234567890123456`\xc6\xa9\xe9Q\x97-\xd6\xe5\x07\xb3\x85\xfb\xc9\xeaz\x07~\xfe\x14\xd1\x19\xd8F\x8cS\x10[\x8f\xfe\x93_ut\xef1\xc0\xc4|\xdb\xccH\xea\xf8\xd2\xd0jj\xeeD\x88\x80\xc5}U\xb5_x)\xaal/,\x96'   

Tunnel mode를 사용하지 않은 경우와 별반 차이가 없어 보이지만, IP의 total length값이 기존 76바이에서 108바이트로 변경되었고, ESP data값도 달라졌다. Tunnel mode의 경우 ESP data에 원본 패킷 전체가 포함되는 반면 Transport mode이 경우 L4 layer이상만 포함되므로 값이 달라진다. IP total length의 경우 32바이트가 차이나는데 20 + 12 ???? FIMXE

2. nat_t_header

NAT traversal을 위해는 UDP encapsulation을 이용하는데 이 역시 SA 를 생성할 때 지정할 수 있다. IP total length의 경우 추가된 8byte의 UDP header가 고려되어 108byte에서 116byte로 커졌다.

>>> nat_t_udp = UDP(dport=4500)            
>>> sa = SecurityAssociation(ESP, spi=0xdeadbeef, crypt_algo='AES-CBC',crypt_key='sixteenbytes key', tunnel_header=outer_ip, nat_t_header=nat_t_udp)
>>> e = sa.encrypt(p, 5, "1234567890123456")
>>> e
<IP  version=4L ihl=5L tos=0x0 len=116 id=1 flags= frag=0L ttl=64 proto=udp chksum=0x3e3d src=10.10.10.10 dst=20.20.20.20 options=[] |<UDP  sport=domain dport=ipsec_nat_t len=8 chksum=0x0 |<ESP  spi=0xdeadbeef seq=5 data='1234567890123456`\xc6\xa9\xe9Q\x97-\xd6\xe5\x07\xb3\x85\xfb\xc9\xeaz\x07~\xfe\x14\xd1\x19\xd8F\x8cS\x10[\x8f\xfe\x93_ut\xef1\xc0\xc4|\xdb\xccH\xea\xf8\xd2\xd0jj\xeeD\x88\x80\xc5}U\xb5_x)\xaal/,\x96' |>>>
>>> e.show()
###[ IP ]###
  version   = 4L
  ihl       = 5L
  tos       = 0x0
  len       = 116
  id        = 1
  flags     = 
  frag      = 0L
  ttl       = 64
  proto     = udp
  chksum    = 0x3e3d
  src       = 10.10.10.10
  dst       = 20.20.20.20
  \options   \
###[ UDP ]###
     sport     = domain
     dport     = ipsec_nat_t
     len       = 8
     chksum    = 0x0
###[ ESP ]###
        spi       = 0xdeadbeef
        seq       = 5
        data      = '1234567890123456`\xc6\xa9\xe9Q\x97-\xd6\xe5\x07\xb3\x85\xfb\xc9\xeaz\x07~\xfe\x14\xd1\x19\xd8F\x8cS\x10[\x8f\xfe\x93_ut\xef1\xc0\xc4|\xdb\xccH\xea\xf8\xd2\xd0jj\xeeD\x88\x80\xc5}U\xb5_x)\xaal/,\x96'