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