Home | History | Annotate | Download | only in layers
      1 ## This file is part of Scapy
      2 ## See http://www.secdev.org/projects/scapy for more informations
      3 ## Copyright (C) Philippe Biondi <phil (at] secdev.org>
      4 ## This program is published under a GPLv2 license
      5 
      6 """
      7 DHCP (Dynamic Host Configuration Protocol) and BOOTP
      8 """
      9 
     10 from __future__ import absolute_import
     11 from __future__ import print_function
     12 from collections import Iterable
     13 import struct
     14 
     15 from scapy.packet import *
     16 from scapy.fields import *
     17 from scapy.ansmachine import *
     18 from scapy.data import *
     19 from scapy.compat import *
     20 from scapy.layers.inet import UDP,IP
     21 from scapy.layers.l2 import Ether
     22 from scapy.base_classes import Net
     23 from scapy.volatile import RandField
     24 
     25 from scapy.arch import get_if_raw_hwaddr
     26 from scapy.sendrecv import *
     27 from scapy.error import warning
     28 import scapy.modules.six as six
     29 from scapy.modules.six.moves import range
     30 
     31 dhcpmagic=b"c\x82Sc"
     32 
     33 
     34 class BOOTP(Packet):
     35     name = "BOOTP"
     36     fields_desc = [ ByteEnumField("op",1, {1:"BOOTREQUEST", 2:"BOOTREPLY"}),
     37                     ByteField("htype",1),
     38                     ByteField("hlen",6),
     39                     ByteField("hops",0),
     40                     IntField("xid",0),
     41                     ShortField("secs",0),
     42                     FlagsField("flags", 0, 16, "???????????????B"),
     43                     IPField("ciaddr","0.0.0.0"),
     44                     IPField("yiaddr","0.0.0.0"),
     45                     IPField("siaddr","0.0.0.0"),
     46                     IPField("giaddr","0.0.0.0"),
     47                     Field("chaddr",b"", "16s"),
     48                     Field("sname",b"","64s"),
     49                     Field("file",b"","128s"),
     50                     StrField("options",b"") ]
     51     def guess_payload_class(self, payload):
     52         if self.options[:len(dhcpmagic)] == dhcpmagic:
     53             return DHCP
     54         else:
     55             return Packet.guess_payload_class(self, payload)
     56     def extract_padding(self,s):
     57         if self.options[:len(dhcpmagic)] == dhcpmagic:
     58             # set BOOTP options to DHCP magic cookie and make rest a payload of DHCP options
     59             payload = self.options[len(dhcpmagic):]
     60             self.options = self.options[:len(dhcpmagic)]
     61             return payload, None
     62         else:
     63             return b"", None
     64     def hashret(self):
     65         return struct.pack("L", self.xid)
     66     def answers(self, other):
     67         if not isinstance(other, BOOTP):
     68             return 0
     69         return self.xid == other.xid
     70 
     71 
     72 class _DHCPParamReqFieldListField(FieldListField):
     73     def getfield(self, pkt, s):
     74         ret = []
     75         while s:
     76             s, val = FieldListField.getfield(self, pkt, s)
     77             ret.append(val)
     78         return b"", [x[0] for x in ret]
     79 
     80 #DHCP_UNKNOWN, DHCP_IP, DHCP_IPLIST, DHCP_TYPE \
     81 #= range(4)
     82 #
     83 
     84 DHCPTypes = {
     85                 1: "discover",
     86                 2: "offer",
     87                 3: "request",
     88                 4: "decline",
     89                 5: "ack",
     90                 6: "nak",
     91                 7: "release",
     92                 8: "inform",
     93                 9: "force_renew",
     94                 10:"lease_query",
     95                 11:"lease_unassigned",
     96                 12:"lease_unknown",
     97                 13:"lease_active",
     98                 }
     99 
    100 DHCPOptions = {
    101     0: "pad",
    102     1: IPField("subnet_mask", "0.0.0.0"),
    103     2: "time_zone",
    104     3: IPField("router","0.0.0.0"),
    105     4: IPField("time_server","0.0.0.0"),
    106     5: IPField("IEN_name_server","0.0.0.0"),
    107     6: IPField("name_server","0.0.0.0"),
    108     7: IPField("log_server","0.0.0.0"),
    109     8: IPField("cookie_server","0.0.0.0"),
    110     9: IPField("lpr_server","0.0.0.0"),
    111     12: "hostname",
    112     14: "dump_path",
    113     15: "domain",
    114     17: "root_disk_path",
    115     22: "max_dgram_reass_size",
    116     23: "default_ttl",
    117     24: "pmtu_timeout",
    118     28: IPField("broadcast_address","0.0.0.0"),
    119     35: "arp_cache_timeout",
    120     36: "ether_or_dot3",
    121     37: "tcp_ttl",
    122     38: "tcp_keepalive_interval",
    123     39: "tcp_keepalive_garbage",
    124     40: "NIS_domain",
    125     41: IPField("NIS_server","0.0.0.0"),
    126     42: IPField("NTP_server","0.0.0.0"),
    127     43: "vendor_specific",
    128     44: IPField("NetBIOS_server","0.0.0.0"),
    129     45: IPField("NetBIOS_dist_server","0.0.0.0"),
    130     50: IPField("requested_addr","0.0.0.0"),
    131     51: IntField("lease_time", 43200),
    132     53: ByteEnumField("message-type", 1, DHCPTypes),
    133     54: IPField("server_id","0.0.0.0"),
    134     55: _DHCPParamReqFieldListField("param_req_list", [], ByteField("opcode", 0), length_from=lambda x: 1),
    135     56: "error_message",
    136     57: ShortField("max_dhcp_size", 1500),
    137     58: IntField("renewal_time", 21600),
    138     59: IntField("rebinding_time", 37800),
    139     60: "vendor_class_id",
    140     61: "client_id",
    141     
    142     64: "NISplus_domain",
    143     65: IPField("NISplus_server","0.0.0.0"),
    144     69: IPField("SMTP_server","0.0.0.0"),
    145     70: IPField("POP3_server","0.0.0.0"),
    146     71: IPField("NNTP_server","0.0.0.0"),
    147     72: IPField("WWW_server","0.0.0.0"),
    148     73: IPField("Finger_server","0.0.0.0"),
    149     74: IPField("IRC_server","0.0.0.0"),
    150     75: IPField("StreetTalk_server","0.0.0.0"),
    151     76: "StreetTalk_Dir_Assistance",
    152     82: "relay_agent_Information",
    153     255: "end"
    154     }
    155 
    156 DHCPRevOptions = {}
    157 
    158 for k,v in six.iteritems(DHCPOptions):
    159     if isinstance(v, str):
    160         n = v
    161         v = None
    162     else:
    163         n = v.name
    164     DHCPRevOptions[n] = (k,v)
    165 del(n)
    166 del(v)
    167 del(k)
    168     
    169     
    170 
    171 
    172 class RandDHCPOptions(RandField):
    173     def __init__(self, size=None, rndstr=None):
    174         if size is None:
    175             size = RandNumExpo(0.05)
    176         self.size = size
    177         if rndstr is None:
    178             rndstr = RandBin(RandNum(0,255))
    179         self.rndstr=rndstr
    180         self._opts = list(DHCPOptions.values())
    181         self._opts.remove("pad")
    182         self._opts.remove("end")
    183     def _fix(self):
    184         op = []
    185         for k in range(self.size):
    186             o = random.choice(self._opts)
    187             if isinstance(o, str):
    188                 op.append((o,self.rndstr*1))
    189             else:
    190                 op.append((o.name, o.randval()._fix()))
    191         return op
    192 
    193 
    194 class DHCPOptionsField(StrField):
    195     islist=1
    196     def i2repr(self,pkt,x):
    197         s = []
    198         for v in x:
    199             if isinstance(v, tuple) and len(v) >= 2:
    200                 if  v[0] in DHCPRevOptions and isinstance(DHCPRevOptions[v[0]][1],Field):
    201                     f = DHCPRevOptions[v[0]][1]
    202                     vv = ",".join(f.i2repr(pkt,val) for val in v[1:])
    203                 else:
    204                     vv = ",".join(repr(val) for val in v[1:])
    205                 r = "%s=%s" % (v[0],vv)
    206                 s.append(r)
    207             else:
    208                 s.append(sane(v))
    209         return "[%s]" % (" ".join(s))
    210         
    211     def getfield(self, pkt, s):
    212         return b"", self.m2i(pkt, s)
    213     def m2i(self, pkt, x):
    214         opt = []
    215         while x:
    216             o = orb(x[0])
    217             if o == 255:
    218                 opt.append("end")
    219                 x = x[1:]
    220                 continue
    221             if o == 0:
    222                 opt.append("pad")
    223                 x = x[1:]
    224                 continue
    225             if len(x) < 2 or len(x) < orb(x[1])+2:
    226                 opt.append(x)
    227                 break
    228             elif o in DHCPOptions:
    229                 f = DHCPOptions[o]
    230 
    231                 if isinstance(f, str):
    232                     olen = orb(x[1])
    233                     opt.append( (f,x[2:olen+2]) )
    234                     x = x[olen+2:]
    235                 else:
    236                     olen = orb(x[1])
    237                     lval = [f.name]
    238                     try:
    239                         left = x[2:olen+2]
    240                         while left:
    241                             left, val = f.getfield(pkt,left)
    242                             lval.append(val)
    243                     except:
    244                         opt.append(x)
    245                         break
    246                     else:
    247                         otuple = tuple(lval)
    248                     opt.append(otuple)
    249                     x = x[olen+2:]
    250             else:
    251                 olen = orb(x[1])
    252                 opt.append((o, x[2:olen+2]))
    253                 x = x[olen+2:]
    254         return opt
    255     def i2m(self, pkt, x):
    256         if isinstance(x, str):
    257             return x
    258         s = b""
    259         for o in x:
    260             if isinstance(o, tuple) and len(o) >= 2:
    261                 name = o[0]
    262                 lval = o[1:]
    263 
    264                 if isinstance(name, int):
    265                     onum, oval = name, b"".join(lval)
    266                 elif name in DHCPRevOptions:
    267                     onum, f = DHCPRevOptions[name]
    268                     if  f is not None:
    269                         lval = [f.addfield(pkt,b"",f.any2i(pkt,val)) for val in lval]
    270                     oval = b"".join(lval)
    271                 else:
    272                     warning("Unknown field option %s", name)
    273                     continue
    274 
    275                 s += chb(onum)
    276                 s += chb(len(oval))
    277                 s += oval
    278 
    279             elif (isinstance(o, str) and o in DHCPRevOptions and 
    280                   DHCPRevOptions[o][1] == None):
    281                 s += chb(DHCPRevOptions[o][0])
    282             elif isinstance(o, int):
    283                 s += chb(o)+b"\0"
    284             elif isinstance(o, (str, bytes)):
    285                 s += raw(o)
    286             else:
    287                 warning("Malformed option %s", o)
    288         return s
    289 
    290 
    291 class DHCP(Packet):
    292     name = "DHCP options"
    293     fields_desc = [ DHCPOptionsField("options",b"") ]
    294 
    295 
    296 bind_layers( UDP,           BOOTP,         dport=67, sport=68)
    297 bind_layers( UDP,           BOOTP,         dport=68, sport=67)
    298 bind_bottom_up( UDP, BOOTP, dport=67, sport=67)
    299 bind_layers( BOOTP,         DHCP,          options=b'c\x82Sc')
    300 
    301 @conf.commands.register
    302 def dhcp_request(iface=None,**kargs):
    303     if conf.checkIPaddr != 0:
    304         warning("conf.checkIPaddr is not 0, I may not be able to match the answer")
    305     if iface is None:
    306         iface = conf.iface
    307     fam,hw = get_if_raw_hwaddr(iface)
    308     return srp1(Ether(dst="ff:ff:ff:ff:ff:ff")/IP(src="0.0.0.0",dst="255.255.255.255")/UDP(sport=68,dport=67)
    309                  /BOOTP(chaddr=hw)/DHCP(options=[("message-type","discover"),"end"]),iface=iface,**kargs)
    310 
    311 
    312 class BOOTP_am(AnsweringMachine):
    313     function_name = "bootpd"
    314     filter = "udp and port 68 and port 67"
    315     send_function = staticmethod(sendp)
    316     def parse_options(self, pool=Net("192.168.1.128/25"), network="192.168.1.0/24",gw="192.168.1.1",
    317                       domain="localnet", renewal_time=60, lease_time=1800):
    318         self.domain = domain
    319         netw,msk = (network.split("/")+["32"])[:2]
    320         msk = itom(int(msk))
    321         self.netmask = ltoa(msk)
    322         self.network = ltoa(atol(netw)&msk)
    323         self.broadcast = ltoa( atol(self.network) | (0xffffffff&~msk) )
    324         self.gw = gw
    325         if isinstance(pool, six.string_types):
    326             pool = Net(pool)
    327         if isinstance(pool, Iterable):
    328             pool = [k for k in pool if k not in [gw, self.network, self.broadcast]]
    329             pool.reverse()
    330         if len(pool) == 1:
    331             pool, = pool
    332         self.pool = pool
    333         self.lease_time = lease_time
    334         self.renewal_time = renewal_time
    335         self.leases = {}
    336 
    337     def is_request(self, req):
    338         if not req.haslayer(BOOTP):
    339             return 0
    340         reqb = req.getlayer(BOOTP)
    341         if reqb.op != 1:
    342             return 0
    343         return 1
    344 
    345     def print_reply(self, req, reply):
    346         print("Reply %s to %s" % (reply.getlayer(IP).dst,reply.dst))
    347 
    348     def make_reply(self, req):        
    349         mac = req.src
    350         if isinstance(self.pool, list):
    351             if mac not in self.leases:
    352                 self.leases[mac] = self.pool.pop()
    353             ip = self.leases[mac]
    354         else:
    355             ip = self.pool
    356             
    357         repb = req.getlayer(BOOTP).copy()
    358         repb.op="BOOTREPLY"
    359         repb.yiaddr = ip
    360         repb.siaddr = self.gw
    361         repb.ciaddr = self.gw
    362         repb.giaddr = self.gw
    363         del(repb.payload)
    364         rep=Ether(dst=mac)/IP(dst=ip)/UDP(sport=req.dport,dport=req.sport)/repb
    365         return rep
    366 
    367 
    368 class DHCP_am(BOOTP_am):
    369     function_name="dhcpd"
    370     def make_reply(self, req):
    371         resp = BOOTP_am.make_reply(self, req)
    372         if DHCP in req:
    373             dhcp_options = [(op[0],{1:2,3:5}.get(op[1],op[1]))
    374                             for op in req[DHCP].options
    375                             if isinstance(op, tuple)  and op[0] == "message-type"]
    376             dhcp_options += [("server_id",self.gw),
    377                              ("domain", self.domain),
    378                              ("router", self.gw),
    379                              ("name_server", self.gw),
    380                              ("broadcast_address", self.broadcast),
    381                              ("subnet_mask", self.netmask),
    382                              ("renewal_time", self.renewal_time),
    383                              ("lease_time", self.lease_time), 
    384                              "end"
    385                              ]
    386             resp /= DHCP(options=dhcp_options)
    387         return resp
    388     
    389 
    390