遇到一个测试项目,需要模拟多用户去dhcp拿取地址,其实现在有很多工具都可以实现,如龙卷风这些,但龙卷风好像大佬没有维护后,经常出现识别不了网卡,好像也不支持持续发送dhcp广播报文,导致不能满足我目前的测试环境,于是结合python+scapy实现多用户并发持续发送dhcp广播报文拿取地址的实验
1、实验只针对ipv4网络环境
备注:只有路由器发送了ack后才代表给你分配的地址已经可以使用了,所以 在用python模拟终端拿取地址的过程,首先需要发送dhcp discover报文进行广播发现,等待路由器回复了dhcp offer后,需要捕获offer报文并提取offer报文中的相关必要字段如xid,your client ip,dhcp server等,然后将提取后的字段填到dhcp request报文中并发送出去,到这里,python需要做的事情其实就已经完成了。
import threading
import time
import re
from scapy.all import *
from scapy.layers.l2 import Ether
import binascii
from queue import Queue
DISCOVER_THREADS_NUM = 10
REQUEST_THREADS_NUM = 10
dhcp_offer_queue = Queue()
mac_ip_queue = Queue()
class DhcpDiscoverThread(threading.Thread):
def __init__(self, mac):
super().__init__()
self.mac = mac
# self.dstmac= "FF:FF:FF:FF:"
def run(self):
xid = random.randint(0, 0xFFFFFFFF)
srcmac=binascii.a2b_hex(self.mac.replace(":",""))
options = binascii.a2b_hex("63825363")
discover_pkt = Ether(dst = "FF:FF:FF:FF:FF:FF",src = srcmac)\
/scapy.all.IP(tos = None,len = None,id = 1,flags = "",frag = 0,ttl = 64 ,proto = 17,chksum = None,src ="0.0.0.0",dst = "255.255.255.255")\
/scapy.all.UDP(sport = 68,dport = 67,len = None,chksum = None)\
/scapy.all.BOOTP(op = 1,xid = xid,htype = 1,hlen = 6,hops = 0,secs = 0,flags = "B",ciaddr = "0.0.0.0",yiaddr = "0.0.0.0",siaddr = "0.0.0.0",giaddr = "0.0.0.0",chaddr = srcmac,sname = "",file = "",options = options)\
/scapy.all.DHCP(options=[("message-type","discover"),("client_id",b"\x01"+srcmac),("requested_addr","192.168.1.6"),("hostname","chahong"),("vendor_class_id",'MSFT 5.0'),("param_req_list",[1, 3, 6, 15, 31, 33, 43, 44, 46, 47, 119, 121, 249, 252]),"end"])
offers = []
def offer_handler(pkt):
if pkt.getlayer("DHCP").options[0][1] == 2 and pkt.getlayer("Ethernet").src =="94:e3:ee:bb:69:6a":
offers.append(pkt)
print("已经收到")
sendp(discover_pkt, iface="以太网",verbose=False)
# print(1111111111)
sniff(prn=offer_handler, iface="以太网",filter="udp and (port 67 or port 68)", timeout=10)
dhcp_offer_queue.put(offers)
# for offer in offers:
# ip_address =str(offer["BOOTP"].yiaddr)
# dhcp_server =str(offer["BOOTP"].siaddr)
# xid = offer["BOOTP"].xid
# mad_addr = offer["BOOTP"].chaddr
# mac_ip_queue.put((ip_address,dhcp_server,xid,mad_addr))
2、这里使用多线程的run方法,后续需要模拟并发终端去拿地址,如何组包,建议使用wires hark在自己的电脑上进行抓包,然后使用pkt.show()方法查看对应包的layer字段,然后一层一层的进行组包,有些包组出来可能相关字段名字不多,建议查看scapy的相关源码进行组包;
我这里的discover_pkt的组成由以太网头部(Ether)/IPV4头部(IP)/UDP头部(UDP)/BOOTP/DHCP组成,最复杂的一成就是bootp和dhcp头部
下面是这两部分使用python打印出来的一个dhcp discover在python中相关字段的描述,然后就比较容易看懂我上面的组的包。
使用这段code可以打印,前提是你要使用wireshark抓到包并报错对应文件问pcap格式
pcap = r'D:\wireshark_pcap\xxxx_dhcp.pcap'
f=rdpcap(pcap)
f[0].show()
备注:报文里面的有些字段需要进行转码,比如srcmac要转化成二进制的格式,总之我写的脚本里面不需要进行大改,,一些字符串我已经转好格式,都是踩过的坑才搞好。
python+scapy模拟dhcp request报文
class DhcpRequestThread(threading.Thread):
def __init__(self,mac):
super().__init__()
self.requestip=mac[0]
self.dhcpserver=mac[1]
self.xid=mac[2]
self.srcmac=mac[3]
def run(self):
src_mac = ":".join([binascii.hexlify(self.srcmac[i:i+1]).decode() for i in range(0, 6)])#将二进制字符串转化为16禁止字符串然后在转化成11:22:33这种格式的mac地址
# formatted_mac = ':'.join(mac[i:i+2] for i in range(0, len(mac), 2))
# srcmac=binascii.a2b_hex(self.srcmac.replace(":",""))
options = binascii.a2b_hex("63825363")
pkt_dhcp_quest = Ether(dst = "FF:FF:FF:FF:FF:FF",src = src_mac)\
/scapy.all.IP(tos = None,len = None,id = 1,flags = "",frag = 0,ttl = 64 ,proto = 17,chksum = None,src ="0.0.0.0",dst = "255.255.255.255")\
/scapy.all.UDP(sport = 68,dport = 67,len = None,chksum = None)\
/scapy.all.BOOTP(op = 1,htype = 1,hlen = 6,hops = 0,xid = self.xid,secs = 0,flags = "B",ciaddr = "0.0.0.0",yiaddr = "0.0.0.0",siaddr = "0.0.0.0",giaddr = "0.0.0.0",chaddr = self.srcmac,sname = "",file = "",options = options)\
/scapy.all.DHCP(options=[("message-type","request"),("client_id",b"\x01"+ self.srcmac),("requested_addr",self.requestip),("server_id",self.dhcpserver),("hostname",b'DESKTOP-7716SI9'),("vendor_class_id",b'MSFT 5.0'),("param_req_list",[1, 3, 6, 15, 31, 33, 43, 44, 46, 47, 119, 121, 249, 252]),"end"])
sendp(pkt_dhcp_quest,iface="以太网",verbose=False)
# print(222222222222222)
组包方法和上面一样,要发送dhcp request报文的前提是要捕获到对应的dhcp offer报文
原代码如下:
import threading
import time
import re
from scapy.all import *
from scapy.layers.l2 import Ether
import binascii
from queue import Queue
DISCOVER_THREADS_NUM = 3
REQUEST_THREADS_NUM = 3
dhcp_offer_queue = Queue()
mac_ip_queue = Queue()
class DhcpDiscoverThread(threading.Thread):
def __init__(self, mac):
super().__init__()#使用run方法必须要要调用父类thread中的构造方法
self.mac = mac
# self.dstmac= "FF:FF:FF:FF:"
def run(self):
xid = random.randint(0, 0xFFFFFFFF)
srcmac=binascii.a2b_hex(self.mac.replace(":",""))
# print("arcmac%s"%srcmac)
options = binascii.a2b_hex("63825363")
discover_pkt = Ether(dst = "FF:FF:FF:FF:FF:FF",src = srcmac)\
/scapy.all.IP(tos = None,len = None,id = 1,flags = "",frag = 0,ttl = 64 ,proto = 17,chksum = None,src ="0.0.0.0",dst = "255.255.255.255")\
/scapy.all.UDP(sport = 68,dport = 67,len = None,chksum = None)\
/scapy.all.BOOTP(op = 1,xid = xid,htype = 1,hlen = 6,hops = 0,secs = 0,flags = "B",ciaddr = "0.0.0.0",yiaddr = "0.0.0.0",siaddr = "0.0.0.0",giaddr = "0.0.0.0",chaddr = srcmac,sname = "",file = "",options = options)\
/scapy.all.DHCP(options=[("message-type","discover"),("client_id",b"\x01"+srcmac),("requested_addr","192.168.1.6"),("hostname","chahong"),("vendor_class_id",'MSFT 5.0'),("param_req_list",[1, 3, 6, 15, 31, 33, 43, 44, 46, 47, 119, 121, 249, 252]),"end"])
offers = []
def offer_handler(pkt):
if pkt.getlayer("DHCP").options[0][1] == 2:
offers.append(pkt)
print("已经收到")
sendp(discover_pkt, iface="以太网",verbose=False)
# print(1111111111)
sniff(prn=offer_handler, iface="以太网",filter="udp and (port 67 or port 68)", timeout=10)
dhcp_offer_queue.put(offers)
# for offer in offers:
# ip_address =str(offer["BOOTP"].yiaddr)
# dhcp_server =str(offer["BOOTP"].siaddr)
# xid = offer["BOOTP"].xid
# mad_addr = offer["BOOTP"].chaddr
# mac_ip_queue.put((ip_address,dhcp_server,xid,mad_addr))
class DhcpRequestThread(threading.Thread):
def __init__(self,mac):
super().__init__()
self.requestip=mac[0]
self.dhcpserver=mac[1]
self.xid=mac[2]
self.srcmac=mac[3]
def run(self):
src_mac = ":".join([binascii.hexlify(self.srcmac[i:i+1]).decode() for i in range(0, 6)])#将二进制字符串转化为16禁止字符串然后在转化成11:22:33这种格式的mac地址
# formatted_mac = ':'.join(mac[i:i+2] for i in range(0, len(mac), 2))
# srcmac=binascii.a2b_hex(self.srcmac.replace(":",""))
options = binascii.a2b_hex("63825363")
srcmac=binascii.a2b_hex(src_mac.replace(":",""))
# print("testest%s"%srcmac)
pkt_dhcp_quest = Ether(dst = "FF:FF:FF:FF:FF:FF",src = src_mac)\
/scapy.all.IP(tos = None,len = None,id = 1,flags = "",frag = 0,ttl = 64 ,proto = 17,chksum = None,src ="0.0.0.0",dst = "255.255.255.255")\
/scapy.all.UDP(sport = 68,dport = 67,len = None,chksum = None)\
/scapy.all.BOOTP(op = 1,htype = 1,hlen = 6,hops = 0,xid = self.xid,secs = 0,flags = "B",ciaddr = "0.0.0.0",yiaddr = "0.0.0.0",siaddr = "0.0.0.0",giaddr = "0.0.0.0",chaddr = self.srcmac,sname = "",file = "",options = options)\
/scapy.all.DHCP(options=[("message-type","request"),("client_id",b"\x01"+ srcmac),("requested_addr",self.requestip),("server_id",self.dhcpserver),("hostname",b'DESKTOP-7716SI9'),("vendor_class_id",b'MSFT 5.0'),("param_req_list",[1, 3, 6, 15, 31, 33, 43, 44, 46, 47, 119, 121, 249, 252]),"end"])
sendp(pkt_dhcp_quest,iface="以太网",verbose=False)
# print(222222222222222)
def start_dhcp_discover_threads(mac_addresses):
threads = []
count = 0
for mac in mac_addresses:
thread = DhcpDiscoverThread(mac)
thread.start()
threads.append(thread)
count += 1
if count % DISCOVER_THREADS_NUM == 0 or count == len(mac_addresses):
for t in threads:
t.join()
threads = []
offers = []
while not dhcp_offer_queue.empty():
offers += dhcp_offer_queue.get()
start_dhcp_request_threads(offers)
# dhcp_offer_queue.Queue().queue.clear()
def start_dhcp_request_threads(offers):
# print(6666666666)
# print(offers)
threads = []
count = 0
mac_ip_list = []
for offer in offers:
xid = offer["BOOTP"].xid
dhcp_server_ip = offer["BOOTP"].siaddr
requested_ip = offer["BOOTP"].yiaddr
mac_address = offer["BOOTP"].chaddr
mac_ip_list.append((requested_ip, dhcp_server_ip, xid, mac_address))
# print(mac_ip_list)
#不知道为啥有重复的列表去重
mac_ip_list=list(set(mac_ip_list))
print(mac_ip_list)
for mac in mac_ip_list:
thread = DhcpRequestThread(mac)
thread.start()
threads.append(thread)
count += 1
print(count)
if count % REQUEST_THREADS_NUM == 0 or count == len(mac_addresses):
for t in threads:
t.join()
threads = []
# for i in range(REQUEST_THREADS_NUM):
# thread = DhcpRequestThread(mac_ip_list)
# thread.start()
# threads.append(thread)
# for t in threads:
# t.join()
if __name__ == "__main__":
mac_addresses = []
for i in range(51):
mac_address = "{:012x}".format(int("000002000001", 16) + 1 * i)
mac_address = re.sub("(.{2})", "\\1:", mac_address, 0)[:-1]
mac_addresses.append(mac_address)
# print(mac_addresses)
offers = start_dhcp_discover_threads(mac_addresses)
# print(offers)
start_dhcp_request_threads(offers)
主要采用了多线程和队列的方法来完成多用户去dhcp拿取地址;大致流程就是先通过start_dhcp_discover_threads(mac_addresses)传入所有的mac地址,这里以列表的形式,然后start_dhcp_discover_threads方法通过多线程去用这些mac地址发送dhcp discover方法,每次只启动10个线程,然后DhcpDiscoverThread这个类就是去发送dhcp discover报文,并捕获DHCP offer报文,并将其保存到一个队列里面,后面来取,10个线程发送完了后,start_dhcp_discover_threads在来取上述队列里面存放的所有抓到的dhcp offer 报文并返回,最后start_dhcp_request_threads传入所返回的offer,也是一个列表,然后在从offer报文中取出必要字段用于后面的dhcp request报文的发送,必要字段有 xid,dhcp_server_ip,requested_ip,mac_address,最后在用多线程去启动发送dhcp request报文。大致流程就是这样。
经测试,脚本有效,且在路由器的后台的dhcp.lease文件也看到了路由器所分配的地址。
如需一直发送dhcp广播报文,可以在后面加个死循环,建议不要在公司的办公网使用,之前搞了一次导致好多电脑重启后都不能拿地址,最后重启了上游的交换机就好了。