C语⾔使⽤openssl库解析TLS报⽂(SNI和证书)
前⾔
项⽬中需要对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连接,在项⽬开始探索的初期,我尝试借鉴博客模拟构建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! ");
}
pos += 3 + len;
}
}
pos += 4 + extension_item_len;
}
return NULL;
}
虽然不是我⾃⼰动⼿写出来的sni字段提取的代码,但在此期间学习⼀些openssl和tls知识也算是有点收获吧。
2.证书的解析
TLS中的证书在openssl库中⽤X509表⽰,X509证书库封装了⼀些对证书的操作,这部分调库即可。对于证书,需要验证证书链、服务器实体证书的校验:与域名匹配、有效期等。
参考中内容,实现了以下函数
// 读取内存中(数组)的证书,将其解析为X509结构体
const unsigned char *data = ... ;
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);
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]) {
parse error怎么解决
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的解析过滤

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。