IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 网络协议 -> C语言使用openssl库解析TLS报文(SNI和证书) -> 正文阅读

[网络协议]C语言使用openssl库解析TLS报文(SNI和证书)

文章目录

前言

一、需求概述

二、总体设计

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的解析过滤

  网络协议 最新文章
使用Easyswoole 搭建简单的Websoket服务
常见的数据通信方式有哪些?
Openssl 1024bit RSA算法---公私钥获取和处
HTTPS协议的密钥交换流程
《小白WEB安全入门》03. 漏洞篇
HttpRunner4.x 安装与使用
2021-07-04
手写RPC学习笔记
K8S高可用版本部署
mySQL计算IP地址范围
上一篇文章      下一篇文章      查看所有文章
加:2022-02-16 13:30:42  更:2022-02-16 13:30:54 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/26 9:35:06-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码