欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

C语言使用openssl库解析TLS报文(SNI和证书)

程序员文章站 2022-03-01 12:57:38
...

文章目录

前言

一、需求概述

二、总体设计

1.SNI字段的过滤

2.证书的解析

总结


前言

项目中需要对TLS报文中SNI和证书部分进行过滤,并且之前并没有接触过这方面的内容,为了解决这一需求,期间学习了不少知识,也走了不少弯路。就写下这篇博客记录分享一下。

项目背景:项目是一个类似防火墙的软件,可以过滤特定的TLS流量。实现原理就是通过对TLS数据包流量进行解析,得到想要过滤的字段数据,根据过滤规则决定是否放行该TLS流量。


一、需求概述

本次需要过滤的TLS数据包主要针对SNI和证书,TLS报文的版本为1.2。使用版本为1.0.2k的openssl库进行C编程。

二、总体设计

1.SNI字段的过滤

sni字段存在于Client hello 报文中,在https中表示客户端浏览器对自己要访问域名的说明。openssl中获取sni字段的api也有:

const char *SSL_get_servername(const SSL *s, const int type);

但这个函数依赖于SSL连接,在项目开始探索的初期,我尝试借鉴博客旁路解密https模拟构建ssl连接的方法,发现这种方案需要修改openssl源码,而我本身对openssl库都不了解,修改源码更是难上加难,当时并没有放弃,我想着先了解openssl再去看源码,当时还发现了一本好书:《深入浅出 https从原理到实战》,如果你和我一样并不了解openssl库以及tls,可以先看这本书搭建起来知识框架,实现对openssl以及tls的一些基本了解。

看完这本书后,我尝试阅读openssl源码,发现对openssl的了解依旧不够深入,现在我已经对openssl的大致框架有了了解,想着先用好openssl再去修改源码。这一阶段我阅读了openssl库的中文api手册,对openssl有了更近一步的了解,也尝试参考openssl提供的examples写出了一对简易的server和client程序。

在阅读api和写server/client程序中,我发现一些库并不依赖于ssl连接,于是我去搜索有没有不依赖于ssl连接提取sni的api,在stack overflow上有大牛自己动手实现了一个(链接我找不到了)。代码我贴在下面:

static char* get_server_extension_name(const char* data, uint32_t datalen) {
    /* Skip past fixed length records:
       1	Handshake Type
       3	Length
       2	Version (again)
       32	Random
       next	Session ID Length
    */

    int pos = 38;
    /* session id */
    if (datalen < pos + 1)
        return NULL;
    uint16_t len = data[pos];
    pos += len + 1;

    /* Cipher Suites */
    if (datalen < pos + 2)
        return NULL;
    memcpy(&len, &data[pos], 2);
    len = ntohs(len);
    pos += len + 2;

    /* Compression Methods */ if (datalen < pos + 1)
        return NULL;
    len = data[pos];
    pos += len + 1;

    /* Extensions */
    if (datalen < pos + 2)
        return NULL;
    memcpy(&len, &data[pos], 2);
    len = ntohs(len);  // 此时len为Extensions的长度
    pos += 2;
    // parse extensions to get sni
    uint16_t extension_item_len;
    /* Parse each 4 bytes for the extension header */
    while (pos + 4 <= len) {
        memcpy(&extension_item_len, &data[pos + 2], 2);
        extension_item_len = ntohs(extension_item_len);
        if (data[pos] == 0x00 && data[pos + 1] == 0x00) {  // sni 字段
            if (pos + 4 + extension_item_len > len)
                return NULL;
            // get sni string
            pos += 6;
            uint16_t server_name_len;
            uint16_t extension_end = pos + extension_item_len - 2;
            while (pos + 3 < extension_end) {
                memcpy(&server_name_len, &data[pos + 1], 2);
                server_name_len = ntohs(server_name_len);
                if (pos + 3 + server_name_len > extension_end)
                    return NULL;
                char* hostname = (char*)malloc(server_name_len + 1);
                switch (data[pos]) {
                    case 0x00: /*host name*/
                        if (hostname == NULL) {
                            fprintf(stderr, "malloc hostname failed!\n");
                            return NULL;
                        }
                        strncpy(hostname, (char*)(data + pos + 3),
                                server_name_len);
                        hostname[server_name_len] = '\0';
                        return hostname;
                        break;
                    default:
                        puts("encouter error! debug me....");
                }
                pos += 3 + len;
            }
        }
        pos += 4 + extension_item_len;
    }
    return NULL;
}

 虽然不是我自己动手写出来的sni字段提取的代码,但在此期间学习一些openssl和tls知识也算是有点收获吧。

2.证书的解析

TLS中的证书在openssl库中用X509表示,X509证书库封装了一些对证书的操作,这部分调库即可。对于证书,需要验证证书链、服务器实体证书的校验:与域名匹配、有效期等。

参考博客中内容,实现了以下函数

// 读取内存中(数组)的证书,将其解析为X509结构体
const unsigned char *data = ... ;
size_t len = ... ;

X509 *cert = d2i_X509(NULL, &data, len);
if (!cert) {
	fprintf(stderr, "unable to parse certificate in memory\n");
	return EXIT_FAILURE;
}

// any additional processing would go here..

X509_free(cert);

/**
 * @brief 创建一个证书链,方便后续新增需求,方便后续验证证书链
 *
 * @param data
 * @param datalen
 * @return int
 */
int create_stack(const unsigned char* data,
                 uint32_t datalen,
                 STACK_OF(X509) * sk) {
    uint32_t len;
    uint32_t pos = 0;
    while (pos + 3 < datalen) {
        ntoh(data + pos, &len);
        if (len == 0)
            return 1;

        const unsigned char* tmp = data + pos + 3;
        X509* cert = d2i_X509(NULL, &tmp, len);
        if (cert == NULL) {
            fprintf(stderr, "parse certificates failed!\n");
            return -1;
        }
        // verify signature,只能实现对self signature的验证
        EVP_PKEY* pkey = X509_get_pubkey(cert);
        if (pkey == NULL) {
            fprintf(stderr, "%s\n", ERR_error_string(ERR_get_error(), NULL));
            return 0;
        }
        int r = X509_verify(cert, pkey);
        if (r <= 0) {
           fprintf(stderr, "certificate signature error!\n");
           return 0;
        }
        EVP_PKEY_free(pkey);
        sk_X509_push(sk, cert);
        pos += 3 + len;
    }
    return 1;
}

/**
 * @brief 检查证书的有效期
 *
 * @param cert
 * @return true
 * @return false
 */
bool check_certificate_validity(X509* cert) {
    if (cert == NULL)
        return false;
    ASN1_TIME* not_before = X509_get_notBefore(cert);
    ASN1_TIME* not_after = X509_get_notAfter(cert);
    int day, sec;
    if (!ASN1_TIME_diff(&day, &sec, NULL, not_before)) {
        fprintf(stderr, "asn1 time format error!\n");
        return false;
    }
    if (day >= 0 || sec >= 0) {
        return false;
    }
    if (!ASN1_TIME_diff(&day, &sec, NULL, not_after)) {
        fprintf(stderr, "asn1 time format error!\n");
        return false;
    }
    if (day <= 0 || sec <= 0) {
        return false;
    }
    return true;
}


/**
 * @brief 从证书中加载位置信息到location中
 *
 * @param location
 * @return int
 */
int get_subject_location_string(STACK_OF(X509) * sk,
                                char* location[ENTRY_DEPTH]) {
    unsigned len = sk_X509_num(sk);
    if (len == 0) {  //空的证书链
        return -1;
    }
    // 提取subject的信息,进行过滤
    X509* cert = sk_X509_value(sk, 0);
    X509_NAME* subj = X509_get_subject_name(cert);
    for (int i = 0; i < X509_NAME_entry_count(subj); i++) {
        X509_NAME_ENTRY* e = X509_NAME_get_entry(subj, i);
        ASN1_STRING* d = X509_NAME_ENTRY_get_data(e);
        int nid = OBJ_obj2nid(X509_NAME_ENTRY_get_object(e));
        char* value = (char*)ASN1_STRING_data(d);
        location[nid - NID_commonName] = new char[strlen(value) + 1];
        // (char*)malloc(sizeof(char) * (strlen(value) + 1));

        strncpy(location[nid - NID_commonName], value, strlen(value));
        location[nid - NID_commonName][strlen(value)] = '\0';
        // puts(value);
    }
    return 0;
}

总结

作为一个编程菜鸟,对于开源库的使用并没有什么经验,通过此次openssl的学习使用,我能得到如下经验教训:

对于openssl的学习过程,应该要先看书/资料对openssl本身要有一个大致的了解,然后再去学习怎么使用,最后才应该是挖掘它的实现原理以及阅读/修改源码。这样循序渐进的学习方式看似耗时实则效率最高。

最后,附上此项目的源代码:实现了从捕获流量-->tcp报文的乱序重组-->tls的解析过滤