Dissecting OpenDPI (LDAP)

A Short Introduction

Hi there! OpenDPI seems to be a hot topic on this blog so I’ve decided to provide more articles. This series is about OpenDPI code review on a protocol level. The thing is I have recently started to write a DPI engine from scratch (codename ‘Li’) so that it can be released under an unrestrictive ISC/BSD license and maintained by my company. (More on that later, because the code isn’t ready for release yet.) But anyway, along the way I found a few interesting things to fix and change about the way DPI is done and structured. The Lightweight Directory Access Protocol (LDAP) is particularly interesting, because it uses Basic Encoding Rules (BER), which can be a pain to grasp and implement. Let’s dig deeper.

OpenDPI’s Way or the Highway

Here you can find the LDAP detection code for OpenDPI, which is more or less a good baseline of how DPI looks like in free software as well as commercial products. The code starts off with a length check and the first byte in the TCP payload:

if (packet->payload_packet_len >= 14 && packet->payload[0] == 0x30) {

That’s all fine. 0x30 is the start of a SEQUENCE in BER. Now it gets weird, because some kind of “simple” type is checked:

// simple type
if (packet->payload[1] == 0x0c && packet->payload_packet_len == 14 &&
    packet->payload[packet->payload_packet_len - 1] == 0x00 && packet->payload[2] == 0x02) {

The packet length must be 14 bytes as advertised in the second byte by BER (0x0c is 12, not counting 0x30 and 0x0C), so it’s a very special case. Normally, BER is too dynamic to base assumptions on payload size. Last byte is checked via (payload_packet_len – 1), which is 13, and the compiler may not be able to optimise. The 0x02 indicates an INTEGER type that follows. It is inspected thus:

if (packet->payload[3] == 0x01 &&
    (packet->payload[5] == 0x60 || packet->payload[5] == 0x61) && packet->payload[6] == 0x07) {
	ipoque_int_ldap_add_connection(ipoque_struct);
	return;
}

if (packet->payload[3] == 0x02 &&
    (packet->payload[6] == 0x60 || packet->payload[6] == 0x61) && packet->payload[7] == 0x07) {
	ipoque_int_ldap_add_connection(ipoque_struct);
	return;
}

The INTEGER that follows is either one or two bytes long — LDAP’s message id. Notice the shift in payload[x] to payload[x + 1] from the first to the second if. Ideally, both checks should be joined into one. Now the payload length restriction kicks in, because if the length is 14 in both cases, the data length behind the message id is either 8 or 9, depending on the size of the message id. That seems odd for the same message type.

The types 0x60 and 0x61 are just two (bind request and bind response) of many LDAP types. According to ldapd(8) there are 20 types. And the high bits actually indicate application & non-primitive behaviour. Now, the “normal” type is evaluated:

// normal type
if (packet->payload[1] == 0x84 && packet->payload_packet_len >= 0x84 &&
    packet->payload[2] == 0x00 && packet->payload[3] == 0x00 && packet->payload[6] == 0x02) {

0x84 indicates long form length: 4 bytes of actual length follow in payload[2] to payload[5]. OpenDPI checks that this size is not greater than 65535 (first two bytes in network order are zero). Not sure if this is a sane assumption when pulling larger things from the directory. 0x02 then again means INTEGER type follows — the message id.

if (packet->payload[7] == 0x01 &&
    (packet->payload[9] == 0x60 || packet->payload[9] == 0x61 || packet->payload[9] == 0x63 ||
    packet->payload[9] == 0x64) && packet->payload[10] == 0x84) {
	ipoque_int_ldap_add_connection(ipoque_struct);
	return;
}

if (packet->payload[7] == 0x02 &&
    (packet->payload[10] == 0x60 || packet->payload[10] == 0x61 || packet->payload[10] == 0x63 ||
    packet->payload[10] == 0x64) && packet->payload[11] == 0x84) {
	ipoque_int_ldap_add_connection(ipoque_struct);
	return;
}

Basically, the same thing happens again: one or two bytes for message id. Apart from the bind request/response, now OpenDPI also checks for search request and search entry response. The 0x84 indicates a four byte length follows again. I don’t think it needs to be checked at this point anymore, because the LDAP types are pretty distinctive.

What I don’t like about this code is the following: redundancy, missing comments, too little protocol research and restrictive, rigid tests. And don’t get me started on the indentation. ;)

A Fresh Approach

To conclude constructively, here is the code that I have written; it is copied straight from the source code. I have tried to eliminate all the problems mentioned above. Don’t forget this is a work in progress. Let me know what you think!

LI_DESCRIBE_APP(ldap)
{
        /*
         * LDAP uses BER encoding, so the detection needs
         * to check this first. Then we can move on to
         * request/response code validation. For more on
         * the topic see RFC 4511, but don't say I didn't
         * warn you -- that stuff is weird!
         */
        struct ldap {
                uint8_t identifier_type;
                uint8_t identifier_length;
                uint32_t identifier_value;
                uint8_t message_type;
                uint8_t message_length;
                uint8_t message_value[];
#define op_type message_value[ptr->message_length]
        } __packed *ptr = (void *)packet->app.raw;

        if (packet->app_len < sizeof(struct ldap)) {
                return (0);
        }

        if (ptr->identifier_type != 0x30) {
                /*
                 * Only detect the structure tag. This makes sure
                 * that we don't have to deal with simple types,
                 * because they won't creep up at the start of a
                 * connection (at least a control message must be
                 * sent).
                 */
                return (0);
        }

        if (!(ptr->identifier_length & 0x80) ||
            (ptr->identifier_length & 0x7F) !=
            sizeof(ptr->identifier_value)) {
                /* not a long form or not 4 bytes long */
                return (0);
        }

        /* XXX what happens if short form is used? */

        if (ptr->message_type != 0x02 || !ptr->message_length) {
                /* INTEGER only */
                return (0);
        }

        if (packet->app_len < sizeof(struct ldap) + ptr->message_length) {
                /* No more data. */
                return (0);
        }

        if ((ptr->op_type & 0xE0) != 0x60) {
                /* verify application & non-primitive bits */
                return (0);
        }

        switch (ptr->op_type & 0x1F) {
        case 0:         /* LDAP_REQ_BIND */
        case 1:         /* LDAP_RES_BIND */
        case 2:         /* LDAP_REQ_UNBIND_30 */
        case 3:         /* LDAP_REQ_SEARCH */
        case 4:         /* LDAP_RES_SEARCH_ENTRY */
        case 5:         /* LDAP_RES_SEARCH_RESULT */
        case 6:         /* LDAP_REQ_MODIFY */
        case 7:         /* LDAP_RES_MODIFY */
        case 8:         /* LDAP_REQ_ADD */
        case 9:         /* LDAP_RES_ADD */
        case 10:        /* LDAP_DELETE_30 */
        case 11:        /* LDAP_DELETE */
        case 12:        /* LDAP_REQ_MODRDN */
        case 13:        /* LDAP_SEA_MODRDN */
        case 14:        /* LDAP_REQ_COMPARE */
        case 15:        /* LDAP_RES_COMPARE */
        case 16:        /* LDAP_REQ_ABANDON_30 */
        case 19:        /* LDAP_RES_SEARCH_REFERENCE */
        case 23:        /* LDAP_REQ_EXTENDED */
        case 24:        /* LDAP_RES_EXTENDED */
                break;
        default:
                return (0);
        }

        return (1);
}

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.