Having fun with NFQUEUE and Scapy
sorry for the long silence! I know, I know, it has been long time from my last entry in this blog. During this year I have been very busy with my last year at university and thus I focused all my attention to pass the exams and to find a good final project and so I spent less time on security and related projects.
In this post I am going to explain how to use nfqueue python bindings to fool a service or a malware when it tries to use DNS and/or ICMP, it is just a proof of concept, nothing more. This can be useful in different kind of situations, for example to see the behaviour of a sample when it tries to contact a server that is no more up, or when again the same sample tries to test if it is able to surf the net pinging google.
nfqueue bindings is an handy set of functions to read the target NFQUEUE of Iptables. Using this target we are able to decide to accept or drop a packet from userland, in addition we can modify the packet and send it again for example. Let’s see how to use NFQUEUE. First of all it is necessary to set a rule using Iptables in order to specify which packets
will be involved in the jump for that target, let’s set it
iptables -A OUTPUT -p tcp –dport 6667 -j NFQUEUE
For example this rule means that all the outgoing TCP traffic with destination port 6667 (generally IRC port) should jump to the target NFQUEUE at the queue num 0 (by default the queue is 0 otherwise you have to specify it manually). Once the rule is set we have to use the python bindings. The functions are not well documented but fortunately there are some good tutorials on the net to figure out the main features such as Zardus’ Blog, Malware Forge etc.
Now it is time to see how we can read that sort of queue, let’s see the code below:
def main(): q = nfqueue.queue() q.open() q.bind(socket.AF_INET) q.set_callback(process) q.create_queue(0) try: q.try_run() except KeyboardInterrupt: print "Exiting..." q.unbind(socket.AF_INET) q.close() sys.exit(1) main()
Basically this code opens the NFQUEUE and parses the packets within it through the callback function, in our case it is called “process”.
In the callback we can do whatever we want. “process” is defined as:
def process(i, payload): data = payload.get_data() p = IP(data) ... etc ...
for us it is very important the payload that means the IP packet.
We are ready to play a bit with ICMP. My idea is to write a rule for iptables to intercept ICMP echo request packets, then we drop them, but we send to the client the expected ICMP echo reply packet. In this way the client does not contact any external server. The code is quite simple and it uses scapy. If you don’t know Scapy it is quite simple and you can have more information here, in particular for us it is important to know how the ICMP packet is handled by Scapy:
11:53:58 dave ~>scapy WARNING: No route found for IPv6 destination :: (no default route?) Welcome to Scapy (2.1.0) >>> ls(ICMP) type : ByteEnumField = (8) code : MultiEnumField = (0) chksum : XShortField = (None) id : ConditionalField = (0) seq : ConditionalField = (0) ts_ori : ConditionalField = (78842914) ts_rx : ConditionalField = (78842914) ts_tx : ConditionalField = (78842914) gw : ConditionalField = ('0.0.0.0') ptr : ConditionalField = (0) reserved : ConditionalField = (0) addr_mask : ConditionalField = ('0.0.0.0') unused : ConditionalField = (0) >>>
Let’s some snippets of code:
def send_echo_reply(self, pkt): ip = IP() icmp = ICMP() ip.src = pkt[IP].dst ip.dst = pkt[IP].src icmp.type = 0 icmp.code = 0 icmp.id = pkt[ICMP].id icmp.seq = pkt[ICMP].seq logger.info("Sending back an echo reply to %s" % ip.dst) data = pkt[ICMP].payload send(ip/icmp/data, verbose=0) def process(i, payload): data = payload.get_data() pkt = IP(data) proto = pkt.proto # Check if it is a ICMP packet if proto is 0x01: logger.info("It's an ICMP packet") # Idea: intercept an echo request and immediately send back an echo reply packet if pkt[ICMP].type is 8: logger.info("It's an ICMP echo request packet") self.send_echo_reply(pkt) else: pass ....
Using Scapy we check if the packet is ICMP or not then we check if the ICMP packet is an echo request ( type 8 ) and in this case we invoke the function send_echo_reply. This function generates an echo reply but it is important to highlight some points. Obviously the id field of the ICMP packet should be the same one of the echo request and this is true for the seq field too. Another important feature to create a valid echo reply is that the payload of the reply is the same of the request, otherwise the trick will not work Last but least keep in mind the words of the Scapy’s FAQ:
In order to speak to local applications, you need to build your packets one layer upper, using a PF_INET/SOCK_RAW socket instead of a PF_PACKET/SOCK_RAW (or its equivalent on other systems than Linux):
<class __main__.L3PacketSocket at 0xb7bdf5fc>
(see the full code icmp.py)
The ICMP packets of the client have been pwned by our simple script now let’s try to fool the DNS. We know that before pinging a domainthe client try to resolve the name of that domain in order to have the IP address, basically this operation is a simple DNS request query. Once the query is successful the client will ping its target. DNS is a bit more complex than ICMP, let’s see DNS in Scapy:
>>> ls(DNS) id : ShortField = (0) qr : BitField = (0) opcode : BitEnumField = (0) aa : BitField = (0) tc : BitField = (0) rd : BitField = (0) ra : BitField = (0) z : BitField = (0) rcode : BitEnumField = (0) qdcount : DNSRRCountField = (None) ancount : DNSRRCountField = (None) nscount : DNSRRCountField = (None) arcount : DNSRRCountField = (None) qd : DNSQRField = (None) an : DNSRRField = (None) ns : DNSRRField = (None) ar : DNSRRField = (None) >>> ls(DNSQR) qname : DNSStrField = ('') qtype : ShortEnumField = (1) qclass : ShortEnumField = (1) >>> ls(DNSRR) rrname : DNSStrField = ('') type : ShortEnumField = (1) rclass : ShortEnumField = (1) ttl : IntField = (0) rdlen : RDLenField = (None) rdata : RDataField = ('') >>>
In order to fully understand all the fields remember that google is your friend.
What we are going to do it is an idea quite simple. We are going to write a iptables rule for the DNS traffic and then we will deal with the packets in the NFQUEUE, basically DNS queries, then we will parse them and of course these packets will be dropped, we don’t want to contact external servers and we will generate the DNS response packet to trick the client. Let’s see the code:
def fake_dns_reply(self, pkt, qname): ip = IP() udp = UDP() ip.src = pkt[IP].dst ip.dst = pkt[IP].src udp.sport = pkt[UDP].dport udp.dport = pkt[UDP].sport solved_ip = "18.104.22.168" # I'm lazy, reader you might create a function to generate random IP :)) qd = pkt[UDP].payload dns = DNS(id = qd.id, qr = 1, qdcount = 1, ancount = 1, arcount = 1, nscount = 1, rcode = 0) dns.qd = qd[DNSQR] dns.an = DNSRR(rrname = qname, ttl = 257540, rdlen = 4, rdata = solved_ip) dns.ns = DNSRR(rrname = qname, ttl = 257540, rdlen = 4, rdata = solved_ip) dns.ar = DNSRR(rrname = qname, ttl = 257540, rdlen = 4, rdata = solved_ip) print "Sending the fake DNS reply to %s:%s" % (ip.dst, udp.dport) send(ip/udp/dns)
This is the main function for generating a fake DNS reply. It is worth noting that the id field should be the same of the request, the ttl values come from looking at wireshark, they are common values in a DNS reply. Last but not least remember that the counters (qdcount, ancount etc) should contain the exact number of entries for the given section of the DNS packet. In the final code we have to handle the UDP protocol too (yes, DNS is over UDP) and in particular for our experiment we check the common port for a DNS server, the UDP port number 53. Before going on you should remember to set the rule for the DNS traffic:
iptables -A OUTPUT -p udp –dport 53 -j NFQUEUE
Finally if it works as expected:
(see the final code icmp_dns_fun.py). Keep in mind we can ping also a not available site and we will obtain anyway the DNS answer and obviously the ICMP echo replies.