“libtasn1”
KRB5、ASN.1 简介
最近因工作需要,要从Kerberos(接下来简称krb5)的 pcap 文件中提取 cname 之类的字段,在网上搜了一大推,基本上都是介绍 krb5 的原理,其中我觉得讲的比较好的是《Wireshark网络分析就是这么简单》一书中的“无懈可击的Kerberos”,在微信读书里就可以收到,看完之后开始研究如何从 pcap 文件中提取想要字段的值,发现 wireshark 中展示的字段之间都有一些不知道是啥的数据,比如下面两幅图展示的:
在 pvno 和 msg-type 中差了有4个字节,意义不明,百度了一大圈不知道为啥,同事看了下说它的格式看起来像是 tlv(type-len-value),算了算好像差不多是这样,然后Google了一圈,再结合Kerberos的[RFC文档](rfc4120 (ietf.org)),发现Kerberos 使用了一个叫 ASN.1 的抽象数据定义格式,并且将数据再通过 DER 编码方式(基本上可以看做稍微复杂一点的 TLV)编码了一下,我直接好家伙,ASN.1 定义了十多种基本数据格式,还有几种复合数据类型,这要是自己写代码手动解析工作量直接上天。
这里先插几句:
- Kerberos 的 ASN.1 定义可以在其 RFC 文档的附录中找到,你要是不想手动复制下来(接下来要用到),在 [wireshark 源码](epan/dissectors/asn1/kerberos/KerberosV5Spec2.asn · master · Wireshark Foundation / wireshark · GitLab)里可以找到,直接下下来就好;
- ASN.1 文件在 vscode 里没有高亮,看着很难受,可以安装一个就要 ASN.1 的插件,其中介绍部分讲了很多 ASN.1 的介绍之类(对 ASN.1 的吐槽)的可以好好看看;
这里做一个总结,Kerberos 使用 ASN.1 定义其整个数据结构,可以把它当做是一种比较复杂的结构体,然后将这个结构体使用 DER 编码后就得到了我们在 wireshark 中看到的数据了。要是这么说难以理解的话可以做一个类比——Kerberos 使用 JSON 定义其使用的各种字段,然后将其用 BASE64 编码,这样是不是好理解一些。
关于 ASN.1 ,这里有一篇文章讲的还不错,可以参考下,说的是更加简单些的 snmp 协议:SNMP从入门到开发:基础篇
Libtasn1
安装
有了上述基础知识:Kerberos 认证的基本流程、ASN.1 格式各个字段的基本了解(SEQUENCE 和 SEQUENCE OF 一定要搞明白)、DER(或者BER,DER 基本就是加了一点限制的 BER,是它的子集)编码,接下来就要正式提取 Kerberos 字段了。
经过搜索发现有两个库可以参考:vlm/asn1c: The ASN.1 Compiler (github.com) 和 Libtasn1 - GNU Project - Free Software Foundation,前者生成出来的 .c 文件过于复杂繁多,不够简洁,于是这里我选择了 Libtasn1,Libtasn1 的安装十分简单,从我上面给的链接下载最新版本解压后执行./configure
,然后make && make install
即可,安装完后,源码不要删掉,里面 test 文件夹下有许多可以参考的代码,结合 GNU Libtasn1 4.17.0 的官方文档使用更佳。
libasn1 命令
安装完 Libtasn1 后会有如下几个命令:asn1Coding asn1Decoding asn1Parser
,我们先从 asn1Parser
和 asn1Decoding
入手熟悉下 Libtasn1 的基本用法,首先新建 example.asn 文件:(摘自官方文档,居然有语法错误你敢信!)
MYPKIX1 {}
DEFINITIONS IMPLICIT TAGS ::=
BEGIN
OtherStruct ::= SEQUENCE {
x INTEGER,
y CHOICE {
y1 INTEGER,
y2 OCTET STRING
}
}
Dss-Sig-Value ::= SEQUENCE {
r INTEGER,
s INTEGER,
other OtherStruct,
z INTEGER OPTIONAL
}
END
然后输入
ans1Parse example.asn
会在同级目录下生成一个example_asn1_tab.c
文件,其中包含了以后调用 Libtasn1 函数需要的一些数组。
接下来创建一个 example.asg
文件,文件名随意,后缀名也随意,这个文件用于给我们上面定义的 ASN.1 赋值,内容如下:
dp MYPKIX1.Dss-Sig-Value
r 42
s 47
other.x 66
other.y y1
other.y.y1 15
z (NULL)
然后输入:
asn1Coding example.asn example.asg
会得到如下结果:
最下面的 16 个字节就是Dss-Sig-Value
这个数据类型赋值以后 DER 编码后的结果,看一下是不是和 Kerberos 在 wireshark 中展示的很像呢。
等等,我们要的不是对数据编码要的是解码啊!莫急,这就来了,上面我们得到的二进制文件保存到了 example.out
文件里,现在我想从里面读出先前赋值的那些数据咋办呢,在命令行中输入:
asn1Decoding example.asn example.out MYPKIX1.Dss-Sig-Value
其中第三个参数表明要解码的类型,结果如下图所示,可以看到先前赋值的结果都解析出来了包括类型、名字和取值。
Libtasn1 API
上面介绍了使用命令行进行 ASN.1 文件的解析,数据的编解码,接下来介绍使用 Libtasn1 提供的 API 函数用代码完成这一过程
首先,Libtasn1 的头文件是 <libtasn1.h>,需要自行在代码中包含;
第1步:使用 ASN.1 格式的协议有许多,大家的协议字段定义都不相同,我们拿到了编码后的二进制文件,然后希望从中解析出有用的数据,首先就需要知道协议的 ASN.1 是什么,Libtasn1 库中使用树来描述这些定义,有两个 API 函数完成这一任务 asn1_parser2tree
和 asn1_array2tree
,asn1_parser2tree 的参数是一个 asn 定义文件,而 asn1_array2tree 的参数是先前我们使用 ans1Parse
自动生成文件中定义的asn1_static_node
数组,第一种方式需要读取本地文件并解析,所以本人揣测第二种方式效率更高,更佳推荐;参考代码如下:
asn1_node definitions = NULL;
char error_description[ASN1_MAX_ERROR_DESCRIPTION_SIZE];
ret = asn1_array2tree(krb5_asn1_tab, &definitions, error_description);
if(ret != ASN1_SUCCESS) return -1;
上面的代码只包括最关键的部分,你也可以参考源码中 tests 目录下的代码,基本上都包含有这一步。
第1.1步:现在已经知道了协议的 ASN.1 定义,观察 Kerberos 的定义你会发现最顶层的类型定义不唯一,有好几个同级的定义,比如 AS-REQ、TGS-REQ,类型都是 KDC-REQ,但是其 APPLICATION 类型其实不同,在真正读取数据并解析之前,你需要知道你要解析的数据是这些类型中间的哪一个,这里可以使用 asn1_get_tag_der
得到 APPLICATION 后面那个具体的值,具体参数就不解释了,这一步也并非所有人都需要,有需要的看官方的 API 文档写的很清楚。
第2步:在正式读取数据之前,我们要先给数据准备一个窝,这么说可能不太恰当,实际上就是创建一个 asn1_node
,指明我们要解码的数据具体是定义中的哪一个,示例代码如下:
ret = asn1_create_element(definitions, "KerberosV5Spec2.AS-REQ", &asn1_element)
// definitions 是第一步得到的,"KerberosV5Spec2.AS-REQ" 表明读取解析的详细类型,asn1_element 使我们创建的 asn1_node
第3步:经过前面的准备,就可以正式读取解析数据了,使用的 API 函数是asn1_der_decoding2
,示例代码如下:
ret = asn1_der_decoding2(&asn1_element, buffer, &size, ASN1_DECODE_FLAG_ALLOW_PADDING | ASN1_DECODE_FLAG_STRICT_DER, error_description);
// asn1_element 是第二步创建的,以后就可以通过这个变量读取你想要的元素值了,buffer是要解析的数据,size 是 buffer 的大小
// ASN1_DECODE_FLAG_ALLOW_PADDING 表示忽略 der 编码后的 padding(来自文档),听起来很有用就加上了,
// ASN1_DECODE_FLAG_STRICT_DER 表明不会解析任何 BER 编码数据
上述代码将数据按照 asn1_element 的定义,结合我们先前的代码就是把数据当做是一个AS-REQ
对象进行解析,并保存到 asn1_element 中。
第4步:读取数据,不要忘了我们最终的目的是读取数据,这里以读取 Kerberos 中的 cname 为例进行讲解;根据 Kerberos 的定义:
cname 的类型为 PrincipalName,其中 name-string 是一个列表,它的长度是不确定的,因此首先我们要获取 name-string 的元素个数,如下:
ret = asn1_number_of_elements(*element, "req-body.cname.name-string", &ele_num)
这里"req-body.cname.name-string"表明你要读取的元素名称,element 使我们解析数据得到的 AS-REQ
对象,也就是之前代码里的 asn1_element,req-body 就在 AS-REQ 定义中的第一层,然后一层层用 .
作为分隔符写出你要的元素的路径,ele_num 保存元素个数。接下来循环遍历获取每一个 cname:
for (i = 0; i < ele_num; i++)
{
sprintf(name, "req-body.cname.name-string.?%d", i + 1);
ret = asn1_read_value(*element, name, data, &len);
if (ret != ASN1_SUCCESS) {
break;
} else {
...
}
}
Libtasn1 使用 ?1、?2
这样的形式访问列表中的第一、二个元素,也就是代码中的req-body.cname.name-string.?%d
,然后使用 asn1_read_value
读取元素的值。这个函数和 asn1_number_of_elements
的使用类似,就不解释了。
基本上通过使用 Libtasn1 API 函数对 Kerberos 进行解析读取的套路就是上面那四步,如果你有其他需求的话,比如写入数据之类的,可以采用官方 API 配合源码测试代码的方式学习使用。
基于以上讲解,我写了个解析 Kerberos 网络包的小例子,地址在这里pcap_krb5_example,希望对你有所帮助。
注意事项
!!!!
内存泄漏问题:在使用 asn1_create_element 或者 asn1_array2tree 之类的函数创建了一个 asn1_node 后,需要调用 asn1_delete_structure 释放,否则会造成内存泄漏!
线程安全问题: 4.14 版本之前的 libtasn1,asn1_array2tree 是线程不安全的!!他妈的坑爹啊,centos7 使用 yum 安装的是 4.10 版本,需要手动编译安装。
完。