├── .gitignore ├── extra ├── proc.h └── proc.c ├── Makefile ├── output.h ├── LICENSE ├── dissect.h ├── dissect.c ├── sniffer.c ├── output.c └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | sniffer 2 | *.o 3 | -------------------------------------------------------------------------------- /extra/proc.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file proc.h 3 | * 4 | * Simple DNS Sniffer - Process lookup 5 | * Copyright (C) 2015 Tim Hentenaar. 6 | * 7 | * This code is licenced under the Simplified BSD License. 8 | * See the LICENSE file for details. 9 | */ 10 | 11 | #ifndef PROC_H 12 | #define PROC_H 13 | 14 | #include "dissect.h" 15 | 16 | /** 17 | * Lookup which process owns the destination port sepcified 18 | * in dpkt->dp. 19 | */ 20 | void lookup_proc(char *buffer, size_t s, struct dispkt *dpkt); 21 | 22 | #endif /* PROC_H */ 23 | 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # 2 | # Simple DNS Sniffer 3 | # Copyright (C) 2015 Tim Hentenaar. 4 | # 5 | # This code is licensed under the Simplified BSD License. 6 | # See the LICENSE file for details. 7 | # 8 | 9 | CC=gcc 10 | CFLAGS=-O2 -Wall -fPIC 11 | OBJS=sniffer.o dissect.o output.o 12 | LIBS=-lpcap 13 | 14 | sniffer: $(OBJS) 15 | @echo " LD $@" 16 | @$(CC) -o $@ $(OBJS) $(LIBS) 17 | 18 | all: sniffer 19 | 20 | clean: 21 | @rm -f sniffer *.o 22 | 23 | .c.o: 24 | @echo " CC $@" 25 | @$(CC) $(CFLAGS) -c -o $@ $< 26 | 27 | .SUFFIXES: .c .o 28 | .PHONY: clean all 29 | -------------------------------------------------------------------------------- /output.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file output.h 3 | * 4 | * Simple DNS Sniffer - DNS parsing / output 5 | * Copyright (C) 2015 Tim Hentenaar. 6 | * 7 | * This code is licenced under the Simplified BSD License. 8 | * See the LICENSE file for details. 9 | */ 10 | 11 | #ifndef OUTPUT_H 12 | #define OUTPUT_H 13 | 14 | #include 15 | #include "dissect.h" 16 | 17 | /** 18 | * Dissect a DNS payload, ignoring any malformed packets. 19 | * 20 | * \param[in] dpkt Dissected packet 21 | * \param[in] hdr Pcap packet header 22 | * \return 1 on a fatal error, 0 otherwise. 23 | */ 24 | int output_dns(struct dispkt *dpkt, struct pcap_pkthdr *hdr); 25 | 26 | #endif /* OUTPUT_H */ 27 | 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Tim Hentenaar 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /dissect.h: -------------------------------------------------------------------------------- 1 | /** 2 | * \file dissect.h 3 | * 4 | * Simple DNS Sniffer - Packet Dissection 5 | * Copyright (C) 2015 Tim Hentenaar. 6 | * 7 | * This code is licenced under the Simplified BSD License. 8 | * See the LICENSE file for details. 9 | */ 10 | 11 | #ifndef DISSECT_H 12 | #define DISSECT_H 13 | 14 | #include 15 | #include 16 | #include 17 | 18 | /** 19 | * Representation of a dissected TCP/UDP packet. 20 | */ 21 | struct dispkt { 22 | union { /**< Source address */ 23 | struct in6_addr ip6; 24 | uint32_t ip; 25 | } sa; 26 | 27 | union { /**< Destination address */ 28 | struct in6_addr ip6; 29 | uint32_t ip; 30 | } da; 31 | 32 | uint16_t sp; /**< Source port */ 33 | uint16_t dp; /**< Destination port */ 34 | int family; /**< Address family */ 35 | uint8_t ip_proto; /**< IP protocol */ 36 | size_t payload_offset; /**< Payload offset within the packet */ 37 | const u_char *payload; /**< Packet payload */ 38 | }; 39 | 40 | /** 41 | * Dissect a captured TCP/UDP packet. 42 | * 43 | * Because of the filter we're using, we can safely assume that 44 | * this is either TCP or UDP on top of IP. We also handle the case 45 | * where the packet may have become corrupted in several places, 46 | * by checking the bounds of \a data, and by double-checking the 47 | * protocol identifier(s). 48 | * 49 | * \param[in] lnk Pcap link type 50 | * \param[in] hdr Pcap header 51 | * \param[in] data Packet data 52 | * \param[in] dpkt Dissected packet struct to fill in 53 | * \return 0 on success, 1 on failure 54 | */ 55 | int dissect_ip_packet(int lnk, struct pcap_pkthdr *hdr, 56 | const u_char *data, struct dispkt *dpkt); 57 | 58 | /** 59 | * Determine if the destination address given 60 | * in \a dpkt belongs to an interface local to 61 | * this machine. (UNUSED) 62 | * 63 | * \param[in] dpkt Dissected packet struct 64 | * \return 1 if the destination is local, 0 otherwise. 65 | */ 66 | int is_destination_local(struct dispkt *dpkt); 67 | 68 | #endif /* DISSECT_H */ 69 | 70 | -------------------------------------------------------------------------------- /dissect.c: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple DNS Sniffer - Packet Dissection 3 | * Copyright (C) 2015 Tim Hentenaar. 4 | * 5 | * This code is licenced under the Simplified BSD License. 6 | * See the LICENSE file for details. 7 | */ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | 22 | #include "dissect.h" 23 | 24 | /** 25 | * Ensure that pkt is between data and data + len. 26 | * 27 | * If this evaluates to 1, then PKT is beyond the bounmds of 28 | * the data passed to us by libpcap. 29 | */ 30 | #define check_packet_ptr(PKT, DATA, LEN) ((PKT) >= ((DATA) + (LEN))) 31 | 32 | /** 33 | * Dissect a captured TCP/UDP packet. 34 | * 35 | * Because of the filter we're using, we can safely assume that 36 | * this is either TCP or UDP on top of IP. We also handle the case 37 | * where the packet may have become corrupted in several places, 38 | * by checking the bounds of \a data, and by double-checking the 39 | * protocol identifier(s). 40 | * 41 | * \param[in] lnk Pcap link type 42 | * \param[in] hdr Pcap header 43 | * \param[in] data Packet data 44 | * \param[in,out] dpkt Dissected packet struct to fill in 45 | */ 46 | int dissect_ip_packet(int lnk, struct pcap_pkthdr *hdr, 47 | const u_char *data, struct dispkt *dpkt) 48 | { 49 | const u_char *pkt = data; 50 | uint16_t protocol = 0; 51 | 52 | /* Get the protocol identifier from the link layer */ 53 | switch (lnk) { 54 | case DLT_LINUX_SLL: /* sockaddr_ll "cooked" packet */ 55 | protocol = ntohs(*(uint16_t *)(pkt + 14)); 56 | pkt += 16; 57 | break; 58 | case DLT_EN10MB: /* Ethernet */ 59 | protocol = ntohs(*(uint16_t *)(pkt + ETH_HLEN - 2)); 60 | pkt += ETH_HLEN; 61 | break; 62 | case DLT_IPV4: /* Raw IPv4 */ 63 | protocol = ETH_P_IP; 64 | break; 65 | case DLT_IPV6: /* Raw IPv6 */ 66 | protocol = ETH_P_IPV6; 67 | break; 68 | } 69 | 70 | if (check_packet_ptr(pkt, data, hdr->len) || !protocol) 71 | goto err; 72 | 73 | /* Get the IP protocol used, and seek past the IP header. */ 74 | switch (protocol) { 75 | case ETH_P_IP: 76 | protocol = ((struct iphdr *)pkt)->protocol; 77 | dpkt->sa.ip = ((struct iphdr *)pkt)->saddr; 78 | dpkt->da.ip = ((struct iphdr *)pkt)->daddr; 79 | dpkt->family = AF_INET; 80 | pkt = pkt + (((struct iphdr *)pkt)->ihl << 2); 81 | break; 82 | case ETH_P_IPV6: 83 | dpkt->sa.ip6 = ((struct ip6_hdr *)pkt)->ip6_src; 84 | dpkt->da.ip6 = ((struct ip6_hdr *)pkt)->ip6_dst; 85 | dpkt->family = AF_INET6; 86 | protocol = ((struct ip6_hdr *)pkt)->ip6_nxt; 87 | pkt += 40; /* IPv6 header langth */ 88 | break; 89 | default: 90 | protocol = 0; 91 | } 92 | 93 | if (check_packet_ptr(pkt, data, hdr->len) || !protocol) 94 | goto err; 95 | 96 | /* Now handle the TCP/UDP layer */ 97 | dpkt->ip_proto = protocol & 0xff; 98 | switch (dpkt->ip_proto) { 99 | case IPPROTO_UDP: 100 | dpkt->sp = ntohs(((struct udphdr *)pkt)->source); 101 | dpkt->dp = ntohs(((struct udphdr *)pkt)->dest); 102 | pkt += 8; 103 | break; 104 | case IPPROTO_TCP: 105 | dpkt->sp = ntohs(((struct tcphdr *)pkt)->source); 106 | dpkt->dp = ntohs(((struct tcphdr *)pkt)->dest); 107 | pkt += (((struct tcphdr *)pkt)->doff << 2) + 1; 108 | break; 109 | default: 110 | protocol = 0; 111 | } 112 | 113 | if (check_packet_ptr(pkt, data, hdr->len) || !protocol) 114 | goto err; 115 | 116 | dpkt->payload = pkt; 117 | dpkt->payload_offset = pkt - data; 118 | return 0; 119 | 120 | err: 121 | return 1; 122 | } 123 | 124 | /** 125 | * Determine if the destination address given 126 | * in \a dpkt belongs to an interface local to 127 | * this machine. (UNUSED) 128 | */ 129 | int is_destination_local(struct dispkt *dpkt) 130 | { 131 | struct ifaddrs *addrs, *ifa; 132 | struct sockaddr_in *sin; 133 | struct sockaddr_in6 *sin6; 134 | if (!dpkt) return 0; 135 | 136 | /* First, ensure the receiving interface is local */ 137 | if (getifaddrs(&addrs)) 138 | return 0; 139 | 140 | for (ifa=addrs;ifa;ifa=ifa->ifa_next) { 141 | /* Ignore any interfaces that aren't usable */ 142 | if (!ifa->ifa_addr || !(ifa->ifa_flags & IFF_UP)) 143 | continue; 144 | if (ifa->ifa_addr->sa_family != dpkt->family) 145 | continue; 146 | 147 | /* Compare addresses */ 148 | if (dpkt->family == AF_INET) { 149 | sin = (struct sockaddr_in *)(ifa->ifa_addr); 150 | if (sin->sin_addr.s_addr == dpkt->da.ip) 151 | break; 152 | } else { 153 | sin6 = (struct sockaddr_in6 *)(ifa->ifa_addr); 154 | if (!memcmp(sin6, &(dpkt->da.ip6), 155 | sizeof(struct in6_addr))) break; 156 | } 157 | } 158 | 159 | freeifaddrs(addrs); 160 | if (!ifa) return 0; 161 | return 1; 162 | } 163 | 164 | -------------------------------------------------------------------------------- /sniffer.c: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple DNS Sniffer 3 | * Copyright (C) 2015 Tim Hentenaar. 4 | * 5 | * This code is licenced under the Simplified BSD License. 6 | * See the LICENSE file for details. 7 | */ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | 15 | #include "dissect.h" 16 | #include "output.h" 17 | 18 | /** 19 | * Filter for packets on port 53, presumably DNS packets, 20 | * with the QR bit set (response). 21 | */ 22 | static const char *filter = 23 | "port 53 and (" 24 | "(udp and (not udp[10] & 128 = 0)) or" 25 | "(tcp and (not tcp[((tcp[12] & 0xf0) >> 2) + 2] & 128 = 0))" 26 | ")"; 27 | static char errbuf[PCAP_ERRBUF_SIZE]; 28 | 29 | /* Command-line Args */ 30 | static char intf[IFNAMSIZ] = { 'a', 'n', 'y', '\0' }; 31 | static int snaplen = 2048; 32 | static int timeout = 1000; 33 | static int promisc = 0; 34 | 35 | /** 36 | * Get the next packet and dissect it. 37 | * 38 | * \param[in] session pcap session 39 | * \param[in] link_type pcap link type 40 | * \return 0 on success, 1 on error 41 | */ 42 | static int next_packet(pcap_t *session, int link_type) 43 | { 44 | struct pcap_pkthdr *packet_hdr = NULL; 45 | const u_char *packet_data = NULL; 46 | struct dispkt dpkt; 47 | int i; 48 | 49 | i = pcap_next_ex(session, &packet_hdr, &packet_data); 50 | if (i < 0) { 51 | pcap_perror(session, "Error capturing packet: "); 52 | goto ret1; 53 | } else if (!i) goto ret; 54 | 55 | if (!packet_hdr || !packet_data) 56 | goto err; 57 | 58 | /** 59 | * caplen should always be <= len, otherwise it's likely 60 | * that the packet that pcap received was somehow corrupted. 61 | * 62 | * This should never happen, unless there's memory corruption 63 | * going on, or a really nasty bug in libpcap. 64 | */ 65 | if (packet_hdr->caplen > packet_hdr->len) 66 | goto err; 67 | 68 | /* Dissect the link/IP/IP_PROTO layers of the packet */ 69 | if (dissect_ip_packet(link_type, packet_hdr, packet_data, &dpkt)) 70 | goto ret; 71 | 72 | /* Output a representation of the DNS payload */ 73 | if (output_dns(&dpkt, packet_hdr)) 74 | goto ret; 75 | 76 | ret: 77 | return 0; 78 | 79 | err: 80 | fprintf(stderr, "Dropping corrupted packet\n"); 81 | 82 | ret1: 83 | return 1; 84 | } 85 | 86 | /** 87 | * Display basic usage info. 88 | */ 89 | static void usage(const char *procname) 90 | { 91 | printf("%s [-i interface] [-s snaplen] [-t timeout] [-p]\n\n" 92 | "\t-i interface: Interface to capture on\n" 93 | "\t-s snaplen: Snapshot length / packet buffer size\n" 94 | "\t-t timeout: Maximum read timeout (in milliseconds)\n" 95 | "\t-p: Enable promiscuous mode\n\n" 96 | "\t Defaults: -i any -s %d -t %d\n", 97 | procname, snaplen, timeout); 98 | exit(EXIT_FAILURE); 99 | } 100 | 101 | int main(int argc, char *argv[]) 102 | { 103 | pcap_t *session = NULL; 104 | int link_type, i; 105 | bpf_u_int32 netmask, ip; 106 | struct bpf_program bpf; 107 | 108 | /* Evaluate args with a simple O(n) loop */ 109 | for (i=1;i= argc) 121 | goto invalid_arg; 122 | 123 | switch (argv[i][1]) { 124 | case 'i': 125 | strncpy(intf, argv[++i], IFNAMSIZ); 126 | break; 127 | case 's': 128 | snaplen = atoi(argv[++i]); 129 | break; 130 | case 't': 131 | timeout = atoi(argv[++i]); 132 | break; 133 | default: 134 | goto invalid_arg; 135 | } 136 | } 137 | 138 | /* Create a new capture session */ 139 | memset(errbuf, 0, PCAP_ERRBUF_SIZE); 140 | session = pcap_open_live(intf, snaplen, promisc, timeout, errbuf); 141 | if (!session) { 142 | fprintf(stderr, "Unable to open '%s': %s\n", 143 | intf, errbuf); 144 | goto err; 145 | } 146 | 147 | /* Ensure that we support the interface's link-level header */ 148 | link_type = pcap_datalink(session); 149 | if (link_type != DLT_LINUX_SLL && link_type != DLT_EN10MB && 150 | link_type != DLT_IPV4 && link_type != DLT_IPV6) { 151 | fprintf(stderr, "Unsupported link type: %d\n", link_type); 152 | goto err; 153 | } 154 | 155 | /* Get the IP and netmask (for the filter) */ 156 | if (pcap_lookupnet(intf, &ip, &netmask, errbuf) == -1) { 157 | ip = 0; 158 | netmask = 0; 159 | } 160 | 161 | /* Compile and apply our filter (without BPF optimization) */ 162 | if (pcap_compile(session, &bpf, filter, 0, netmask) == -1 || 163 | pcap_setfilter(session, &bpf) == -1) { 164 | pcap_perror(session, "Error installing filter: "); 165 | goto err; 166 | } 167 | 168 | /* Grab and dissect packets until an error occurs */ 169 | while (!next_packet(session, link_type)); 170 | 171 | err: 172 | if (session) pcap_close(session); 173 | return EXIT_FAILURE; 174 | 175 | invalid_arg: 176 | fprintf(stderr, "Invalid argument: %s\n", argv[i]); 177 | usage(argv[0]); 178 | return EXIT_FAILURE; 179 | } 180 | 181 | -------------------------------------------------------------------------------- /extra/proc.c: -------------------------------------------------------------------------------- 1 | /** 2 | * DNS Sniffer Example - Process lookup 3 | * Copyright (C) 2015 Tim Hentenaar. 4 | * 5 | * This code is licenced under the Simplified BSD License. 6 | * See the LICENSE file for details. 7 | */ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "dissect.h" 17 | #include "proc.h" 18 | 19 | /* Format strings for parsing /proc/net/{tcp,udp,tcp6,udp6} */ 20 | const char *inet_fmt = "%d: %x:%4x %x:%4x %x %x:%x %x:%x %x %d %d %lu "; 21 | const char *inet6_fmt = "%d: %8x%8x%8x%8x:%4x %8x%8x%8x%8x:%4x %x %x:%x " 22 | "%x:%x %x %d %d %lu "; 23 | 24 | /** 25 | * Scan a line from /proc/net/{tcp,udp,tcp6,udp6} and if 26 | * the local address matches, get the inode number for 27 | * that socket. 28 | * 29 | * \param[in] buffer Line buffer 30 | * \param[in] dpkt Dissected packet struct 31 | * \return The inode number, or 0 on error. 32 | */ 33 | static unsigned long get_inode_for_dest(char *buffer, struct dispkt *dpkt) 34 | { 35 | struct in_addr in; 36 | struct in6_addr in6; 37 | uint16_t port; 38 | unsigned long inode, tmp; 39 | int i; 40 | 41 | if (!buffer || !dpkt) 42 | return 0; 43 | 44 | if (dpkt->family == AF_INET6) { 45 | i = sscanf(buffer, inet6_fmt, &tmp, 46 | (uint32_t *)(&in6.s6_addr[0]), 47 | (uint32_t *)(&in6.s6_addr[4]), 48 | (uint32_t *)(&in6.s6_addr[8]), 49 | (uint32_t *)(&in6.s6_addr[12]), 50 | &port, &tmp, &tmp, &tmp, &tmp, &tmp, 51 | &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, 52 | &tmp, &tmp, &inode); 53 | if (i < 20) 54 | return 0; 55 | if (!memcmp(&in6, &(dpkt->da.ip6), sizeof(struct in6_addr))) 56 | if (port == dpkt->dp) 57 | return inode; 58 | } else { 59 | i = sscanf(buffer, inet_fmt, &tmp, &in.s_addr, &port, 60 | &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, &tmp, 61 | &tmp, &tmp, &tmp, &inode); 62 | if (i < 14) 63 | return 0; 64 | if (!memcmp(&in.s_addr, &(dpkt->da.ip), sizeof(struct in_addr))) 65 | if (port == dpkt->dp) 66 | return inode; 67 | } 68 | 69 | return 0; 70 | } 71 | 72 | /** 73 | * Get the process's name from its cmdline entry. 74 | */ 75 | static void get_procname(char *buffer, size_t s, unsigned long pid) 76 | { 77 | FILE *fp; 78 | char pathbuf[25], linebuf[255], *tmp; 79 | 80 | snprintf(pathbuf, sizeof(pathbuf), "/proc/%lu/cmdline", pid); 81 | if (!(fp = fopen(pathbuf, "r"))) 82 | return; 83 | if (fgets(linebuf, sizeof(linebuf), fp)) { 84 | tmp = linebuf; 85 | while (*tmp != ' ' && *tmp != '\0') tmp++; 86 | *tmp = '\0'; 87 | do { tmp--; } while (*tmp != '/'); 88 | tmp[6] = '\0'; 89 | snprintf(buffer, s, "%s/%lu", tmp, pid); 90 | } 91 | fclose(fp); 92 | } 93 | 94 | /** 95 | * Scan a pid's file descriptors, looking for our 96 | * target socket. 97 | */ 98 | static void process_pid(char *buffer, size_t s, unsigned long pid, 99 | unsigned long inode) 100 | { 101 | DIR *dir; 102 | struct dirent *de; 103 | size_t len; 104 | char pathbuf[25], linkbuf[25], target[25]; 105 | 106 | /* Root through the process's fds */ 107 | snprintf(target, sizeof(target), "socket:[%lu]", inode); 108 | snprintf(pathbuf, sizeof(pathbuf), "/proc/%lu/fds", pid); 109 | if (!(dir = opendir(pathbuf))) 110 | return; 111 | 112 | while (!*buffer && (de = readdir(dir))) { 113 | snprintf(pathbuf, sizeof(pathbuf), 114 | "/proc/%lu/fds/%s", pid, de->d_name); 115 | len = readlink(pathbuf, linkbuf, sizeof(linkbuf)); 116 | if (len <= 0 || len > sizeof(linkbuf)) 117 | return; 118 | linkbuf[len] = '\0'; 119 | 120 | if (!strcmp(linkbuf, target)) 121 | get_procname(buffer, s, pid); 122 | } 123 | 124 | closedir(dir); 125 | 126 | } 127 | 128 | /** 129 | * Lookup which process owns the destination port sepcified 130 | * in dpkt->dp. 131 | */ 132 | void lookup_proc(char *buffer, size_t s, struct dispkt *dpkt) 133 | { 134 | char *net_file; 135 | DIR *proc_dir; 136 | FILE *fp; 137 | struct dirent *de; 138 | unsigned long inode = 0, pid; 139 | char linebuf[256], *endptr; 140 | 141 | if (!buffer || !dpkt) 142 | return; 143 | 144 | *buffer = '\0'; 145 | if (dpkt->ip_proto == IPPROTO_TCP) { 146 | if (dpkt->family == AF_INET6) 147 | net_file = "/proc/net/tcp6"; 148 | else net_file = "/proc/net/tcp"; 149 | } else { 150 | if (dpkt->family == AF_INET6) 151 | net_file = "/proc/net/udp6"; 152 | else net_file = "/proc/net/udp"; 153 | } 154 | 155 | /* Open the file from /proc/net and try to find the inode */ 156 | if (!(fp = fopen(net_file, "r"))) 157 | return; 158 | 159 | while (fgets(linebuf, sizeof(linebuf) - 1, fp)) { 160 | inode = get_inode_for_dest(linebuf, dpkt); 161 | if (inode > 0) break; 162 | } 163 | 164 | fclose(fp); 165 | if (!inode) 166 | return; 167 | 168 | /* Now that we have the inode, we can probe for the process */ 169 | if (!(proc_dir = opendir("/proc"))) 170 | return; 171 | 172 | while (!*buffer && (de = readdir(proc_dir))) { 173 | /* Make sure we actually have a base-10 numeric string */ 174 | pid = strtoul(de->d_name, &endptr, 10); 175 | if (*endptr != '\0') 176 | continue; 177 | process_pid(buffer, s, pid, inode); 178 | } 179 | 180 | closedir(proc_dir); 181 | } 182 | 183 | -------------------------------------------------------------------------------- /output.c: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple DNS Sniffer - DNS parsing / output 3 | * Copyright (C) 2015 Tim Hentenaar. 4 | * 5 | * This code is licenced under the Simplified BSD License. 6 | * See the LICENSE file for details. 7 | */ 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "dissect.h" 17 | 18 | /** 19 | * DNS header 20 | */ 21 | struct dnshdr { 22 | uint16_t id; 23 | uint16_t flags; 24 | uint16_t qdcount; 25 | uint16_t ancount; 26 | uint16_t nscount; 27 | uint16_t arcount; 28 | } __attribute__((packed)); 29 | 30 | /** 31 | * Basic DNS record types (RFC 1035) 32 | */ 33 | static const char *dns_types[] = { 34 | "UNKN", /* Unsupported / Invalid type */ 35 | "A", /* Host Address */ 36 | "NS", /* Authorative Name Server */ 37 | "MD", /* Mail Destination (Obsolete) */ 38 | "MF", /* Mail Forwarder (Obsolete) */ 39 | "CNAME", /* Canonical Name */ 40 | "SOA", /* Start of Authority */ 41 | "MB", /* Mailbox (Experimental) */ 42 | "MG", /* Mail Group Member (Experimental) */ 43 | "MR", /* Mail Rename (Experimental) */ 44 | "NULL", /* Null Resource Record (Experimental) */ 45 | "WKS", /* Well Known Service */ 46 | "PTR", /* Domain Name Pointer */ 47 | "HINFO", /* Host Information */ 48 | "MINFO", /* Mailbox / Mail List Information */ 49 | "MX", /* Mail Exchange */ 50 | "TXT", /* Text Strings */ 51 | "AAAA" /* IPv6 Host Address (RFC 1886) */ 52 | }; 53 | 54 | static u_char buf[BUFSIZ]; /* Label buffer */ 55 | static char dbuf[BUFSIZ]; /* Data bufffer */ 56 | 57 | /** 58 | * Skip a DNS label. 59 | * 60 | * \param[in] label Pointer to the label 61 | * \return Pointer to the byte following the label 62 | */ 63 | static u_char *skip_dns_label(u_char *label) 64 | { 65 | u_char *tmp; 66 | 67 | if (!label) return NULL; 68 | if (*label & 0xc0) 69 | return label + 2; 70 | 71 | tmp = label; 72 | while (*label) { 73 | tmp += *label + 1; 74 | label = tmp; 75 | } 76 | return label + 1; 77 | } 78 | 79 | /** 80 | * Convert a DNS label (which may contain pointers) to 81 | * a string by way of the given destination buffer. 82 | * 83 | * \param[in] label Pointer to the start of the label 84 | * \param[in] dest Destination buffer 85 | * \param[in] dest_size Destination buffer size 86 | * \param[in] payload Start of the packet 87 | * \param[in] end End of the packet 88 | * \return dest 89 | */ 90 | static u_char *dns_label_to_str(u_char **label, u_char *dest, 91 | size_t dest_size, 92 | const u_char *payload, 93 | const u_char *end) 94 | { 95 | u_char *tmp, *dst = dest; 96 | 97 | if (!label || !*label || !dest) 98 | goto err; 99 | 100 | *dest = '\0'; 101 | while (*label < end && **label) { 102 | if (**label & 0xc0) { /* Pointer */ 103 | tmp = (u_char *)payload; 104 | tmp += ntohs(*(uint16_t *)(*label)) & 0x3fff; 105 | while (tmp < end && *tmp) { 106 | if (dst + *tmp >= dest + dest_size) 107 | goto err; 108 | memcpy(dst, tmp+1, *tmp); 109 | dst += *tmp; tmp += *tmp + 1; 110 | if (dst > dest + dest_size) goto err; 111 | *dst = '.'; dst++; 112 | }; 113 | *label += 2; 114 | } else { /* Label */ 115 | if ((*label + **label) >= end) 116 | goto err; 117 | if (**label + dst >= dest + dest_size) 118 | goto err; 119 | memcpy(dst, *label + 1, **label); 120 | dst += **label; 121 | if (dst > dest + dest_size) goto err; 122 | *label += **label + 1; 123 | *dst = '.'; dst++; 124 | } 125 | } 126 | 127 | *(--dst) = '\0'; 128 | return dest; 129 | err: 130 | if (dest) *dest = '\0'; 131 | return dest; 132 | } 133 | 134 | /** 135 | * Dissect a DNS payload, ignoring any malformed packets. 136 | * 137 | * \param[in] dpkt Dissected packet 138 | * \param[in] hdr Pcap packet header 139 | * \return 1 on a fatal error, 0 otherwise. 140 | */ 141 | int output_dns(struct dispkt *dpkt, struct pcap_pkthdr *hdr) 142 | { 143 | struct dnshdr *dnsh; 144 | u_char *tmp; 145 | u_char *label; 146 | const char *data; 147 | const u_char *end; 148 | uint16_t len, qtype = 0; 149 | int i; 150 | 151 | /* Ensure the packet is valid */ 152 | end = dpkt->payload + (hdr->len - dpkt->payload_offset); 153 | if (end < dpkt->payload) 154 | goto ret; 155 | 156 | dnsh = (struct dnshdr *)(dpkt->payload); 157 | dnsh->id = ntohs(dnsh->id); 158 | dnsh->flags = ntohs(dnsh->flags); 159 | dnsh->qdcount = ntohs(dnsh->qdcount); 160 | dnsh->ancount = ntohs(dnsh->ancount); 161 | dnsh->nscount = ntohs(dnsh->nscount); 162 | dnsh->arcount = ntohs(dnsh->arcount); 163 | 164 | /* Disregard malformed packets */ 165 | if (!dnsh->ancount || !dnsh->qdcount) 166 | return 0; 167 | 168 | /* Parse the Question section */ 169 | tmp = (u_char *)(dpkt->payload + 12); 170 | for (i=0;iqdcount;i++) { 171 | /* Get the first question's label and question type */ 172 | if (!qtype) { 173 | label = dns_label_to_str(&tmp, buf, BUFSIZ, 174 | dpkt->payload, end); 175 | tmp++; 176 | qtype = ntohs(*(uint16_t *)tmp); 177 | } else { 178 | if (*tmp & 0xc0) tmp += 2; 179 | else tmp = skip_dns_label(tmp); 180 | } 181 | 182 | /* Skip type and class */ 183 | tmp += 4; 184 | if (tmp >= end) goto ret; 185 | } 186 | 187 | /* Output the answer corresponding to the question */ 188 | if (!qtype) goto ret; 189 | for (i=0;iancount;i++) { 190 | tmp = skip_dns_label(tmp); 191 | if (tmp + 10 > end) goto ret; 192 | 193 | /* Get the type, and skip class and ttl */ 194 | len = ntohs(*(uint16_t *)tmp); tmp += 8; 195 | if (len == qtype) break; 196 | 197 | /* Skip ahead to the next answer */ 198 | tmp += ntohs(*(uint16_t *)tmp) + 2; 199 | if (tmp > end) goto ret; 200 | } 201 | 202 | /* Get the data field length */ 203 | len = ntohs(*(uint16_t *)tmp); tmp += 2; 204 | if (qtype == 28) qtype = 17; /* 28 = AAAA */ 205 | else if (qtype > 16) qtype = 0; 206 | 207 | /* Now, handle the data based on type */ 208 | switch (qtype) { 209 | case 1: /* A */ 210 | data = inet_ntop(AF_INET, tmp, dbuf, BUFSIZ); 211 | break; 212 | case 2: /* NS */ 213 | case 5: /* CNAME */ 214 | case 12: /* PTR */ 215 | data = (char *)dns_label_to_str( 216 | &tmp, (u_char *)dbuf, BUFSIZ, 217 | dpkt->payload, tmp + len 218 | ); 219 | break; 220 | case 10: /* NULL */ 221 | data = "NULL"; 222 | break; 223 | case 15: /* MX (16-bit priority / label) */ 224 | i = snprintf(dbuf, 7, "%u ", ntohs(*(uint16_t *)tmp)); 225 | tmp += 2; 226 | data = (char *)dns_label_to_str( 227 | &tmp, (u_char *)(dbuf + i), BUFSIZ - i, 228 | dpkt->payload, tmp + len - 2 229 | ); 230 | data = dbuf; 231 | break; 232 | case 16: /* TXT (1 byte text length / text) */ 233 | if (*tmp <= len && tmp + len < end) { 234 | memcpy(dbuf, tmp+1, *tmp); 235 | dbuf[*tmp+1] = '\0'; 236 | } else *dbuf = '\0'; 237 | data = dbuf; 238 | break; 239 | case 17: /* AAAA */ 240 | data = inet_ntop(AF_INET6, tmp, dbuf, BUFSIZ); 241 | break; 242 | default: 243 | /* Ignore unhandled RR types */ 244 | *dbuf = '\0'; 245 | data = dbuf; 246 | } 247 | 248 | /* Print the output. */ 249 | printf("%ld %-5s %-30s %s\n", hdr->ts.tv_sec, 250 | dns_types[qtype], label, data); 251 | 252 | ret: 253 | return 0; 254 | } 255 | 256 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Simple DNS Sniffer 2 | ================== 3 | 4 | Background 5 | ---------- 6 | 7 | I was tasked with implementing this as a follow-up to a job interview 8 | some time ago, and since I found it interesting, I figured I would 9 | finally get around to publishing it. It can be useful for people wanting 10 | to learn how to sniff traffic with libpcap from a simple example of a 11 | real-world use case. 12 | 13 | This "project" went from inception to completion in the span of two 14 | evenings, so the code is far from perfect. But, it does exactly what 15 | it was specified to do. 16 | 17 | This README was also part of the documentation which I delivered upon 18 | completion. As for their reaction, they were quite pleased with the 19 | documentation, the functionality, and the code itself. 20 | 21 | For the curious: I declined the offer, but it was an enjoyable exercise. 22 | 23 | Synopsis 24 | -------- 25 | 26 | ``` 27 | # make clean all 28 | # ./sniffer -h 29 | ./sniffer [-i interface] [-s snaplen] [-t timeout] [-p] 30 | 31 | -i interface: Interface to capture on 32 | -s snaplen: Snapshot length / packet buffer size 33 | -t timeout: Maximum read timeout (in milliseconds) 34 | -p: Enable promiscuous mode 35 | 36 | Defaults: -i any -s 2048 -t 1000 37 | ``` 38 | 39 | Assuming that libpcap is able to capture, the sniffer will run 40 | until the process receives a termination signal, or a fatal 41 | error within libpcap occurs. Corrupted/incomplete packets are 42 | silently dropped, save for cases that might arise due to 43 | memory corruption or a flaw in libpcap. 44 | 45 | Sniffing DNS over TCP as well as UDP is supported. 46 | 47 | Assumptions 48 | ----------- 49 | 50 | * This program is a demonstrative example of a prototype application 51 | using libpcap to sniff DNS traffic. 52 | 53 | * "DNS" refers to the Domain Name System protocol specified in 54 | RFC 1035. 55 | 56 | Not all Resource Record (RR) types are handled, as some are 57 | obsolete, and I was unable to quickly test the others. 58 | 59 | * This program must be run as root, or as a user with sufficient 60 | capabilities (e.g. ``CAP_NET_ADMIN``). 61 | 62 | * Output is based on DNS responses only 63 | 64 | Only the responses are needed to generate the output 65 | as specified. As an optimization, the code uses a filter 66 | specifically designed to select only DNS responses by 67 | checking that the QR bit in the DNS header is set. 68 | 69 | * The code is not thread-safe, as thread-safety wasn't a requirement. 70 | 71 | DNS RRs Supported 72 | ------------------ 73 | 74 | * RFC 1035: 75 | A, NS, CNAME, NULL, PTR, MX, TXT 76 | 77 | * RFC 1886: 78 | AAAA 79 | 80 | NOTE: For NULL, the application prints the RR type, but 81 | none of the data, as it can be anything, as long as it's 82 | size is less than 65536 bytes. (RFC 1035 Sec. 3.3.10) 83 | 84 | Output 85 | ------ 86 | 87 | Each output line is: 88 | ``` 89 | Timestamp Question_Type Question_Label Response_Data 90 | ``` 91 | 92 | This is obtained by correlating the question type with the 93 | response RR's type; and using the first reponse that matches 94 | the RR type that was asked for. 95 | 96 | Capturing on 'any': 97 | ``` 98 | # ./sniffer 99 | 1426464428 AAAA google.ru 2a00:1450:4013:c00::5e 100 | 1426464456 TXT hentenaar.com google-site-verification=UVHRy4rkGATkxae7aLkjN8YW7H1Xv1X20oospOZ_Uy8 101 | 1426464463 MX google.com 10 aspmx.l.google.com 102 | 1426464474 PTR 146.198.21.8.in-addr.arpa alb56.clearspring.com 103 | 1426464484 A hentenaar.com 54.243.160.109 104 | 1426464490 CNAME www.hentenaar.com hentenaar.com 105 | 1426464493 A talkgadget.google.com 173.194.65.113 106 | ``` 107 | 108 | Capturing on 'wlan0': 109 | ``` 110 | # ./sniffer -i wlan0 111 | 1426464653 A www.hentenaar.com 54.243.160.109 112 | 1426464656 CNAME www.hentenaar.com hentenaar.com 113 | 1426464664 TXT hentenaar.com google-site-verification=UVHRy4rkGATkxae7aLkjN8YW7H1Xv1X20oospOZ_Uy8 114 | 1426464712 A i.ytimg.com 173.194.65.100 115 | 1426464717 A r6---sn-5hn7snl6.googlevideo.com 173.194.50.203 116 | 1426464717 A googleads.g.doubleclick.net 173.194.65.157 117 | ``` 118 | 119 | Features Unimplemented 120 | ---------------------- 121 | 122 | * Lookup of the pid / process name 123 | 124 | For traffic originating from the local machine, this can be done by 125 | peeking at the relevant files in ``/proc/net`` to locate the inode 126 | of the destination port for the response packet, and then correlating 127 | that with the target of the links in ``/proc/pid/fds``. However this 128 | approach will not work for this use case, as there's a race condition 129 | between the application receiving the response, and closing the socket; 130 | and the sniffer handling the packet. This method is used by tools 131 | such as lsof and netstat, and is better suited for tracking 132 | processes with bound / longer-lived sockets. 133 | 134 | One alternative would be to do the lookup when the question 135 | is sent out, but that would be equally racy, and this feature 136 | being a "nice to have" was only an afterthought. 137 | 138 | In order to do this properly, I'd have to look at whether or not 139 | the kernel instrumentation interface (a la Systemtap) or the 140 | auditing system might be of more help, at least in eliminating 141 | the race condition with /proc probing. The caveat with this 142 | approach, is that then I'd be making assumptions about the 143 | configuration of the kernel on the machine upon which the code 144 | would be running. 145 | 146 | For demonstrative purposes, I've provided code in the 147 | ``extra`` directory that attempts this via ``/proc`` probing. 148 | It's still a bit rough but is merely intended to demonstrate 149 | the technique. 150 | 151 | Memory Usage 152 | ------------ 153 | 154 | The VmSize stays steady, as expected. In order to save 155 | on stack space, larger buffers are allocated in the 156 | .bss section. 157 | ``` 158 | $ cat /proc/.../status 159 | VmPeak: 13152 kB 160 | VmSize: 13152 kB 161 | VmHWM: 1012 kB 162 | VmRSS: 1012 kB 163 | VmData: 228 kB 164 | VmStk: 136 kB 165 | VmExe: 12 kB 166 | ``` 167 | 168 | Code Size 169 | --------- 170 | 171 | With ``-O2``: 172 | ``` 173 | $ size sniffer 174 | text data bss dec hex filename 175 | 8390 1000 16704 26094 65ee sniffer 176 | ``` 177 | 178 | With ``-Os``: 179 | ``` 180 | $ size sniffer 181 | text data bss dec hex filename 182 | 7743 992 16672 25407 633f sniffer 183 | ``` 184 | 185 | These measurements were taken on a system with PIE and SSP enabled. 186 | Not bad for 432 lines of code according to David A. Wheeler's 187 | 'SLOCCount'. 188 | 189 | With these measurements keep in mind that they may vary from one system 190 | to another, depending on your architecture, kernel version and 191 | configuration, the C library being linked against, etc. 192 | 193 | Ideas for Future Development 194 | ---------------------------- 195 | 196 | * Implement the unhandled RFC 1035 types. 197 | * Implement further DNS extensions. 198 | * Research integration with the kernel's auditing / instrumentation 199 | framework for better tracking of requests. 200 | * Test on libc implementations other than glibc (i.e. uclibc.) 201 | * Further optimize the code. 202 | 203 | --------------------------------------------------------------------------------