“libtasn1”


最近因工作需要,要从Kerberos(接下来简称krb5)的 pcap 文件中提取 cname 之类的字段,在网上搜了一大推,基本上都是介绍 krb5 的原理,其中我觉得讲的比较好的是《Wireshark网络分析就是这么简单》一书中的“无懈可击的Kerberos”,在微信读书里就可以收到,看完之后开始研究如何从 pcap 文件中提取想要字段的值,发现 wireshark 中展示的字段之间都有一些不知道是啥的数据,比如下面两幅图展示的:

在 pvno 和 msg-type 中差了有4个字节,意义不明,百度了一大圈不知道为啥,同事看了下说它的格式看起来像是 tlv(type-len-value),算了算好像差不多是这样,然后Google了一圈,再结合Kerberos的RFC文档,发现Kerberos 使用了一个叫 ASN.1 的抽象数据定义格式,并且将数据再通过 DER 编码方式(基本上可以看做稍微复杂一点的 TLV)编码了一下,我直接好家伙,ASN.1 定义了十多种基本数据格式,还有几种复合数据类型,这要是自己写代码手动解析工作量直接上天。

这里先插几句:

  1. Kerberos 的 ASN.1 定义可以在其 RFC 文档的附录中找到,你要是不想手动复制下来(接下来要用到),在 wireshark 源码里可以找到,直接下下来就好;
  2. ASN.1 文件在 vscode 里没有高亮,看着很难受,可以安装一个就要 ASN.1 的插件,其中介绍部分讲了很多 ASN.1 的介绍之类(对 ASN.1 的吐槽)的可以好好看看;

这里做一个总结,Kerberos 使用 ASN.1 定义其整个数据结构,可以把它当做是一种比较负责的结构体,然后将这个结构体使用 DER 编码后就得到了我们在 wireshark 中看到的数据了。要是这么说难以理解的话可以做一个类比——Kerberos 使用 JSON 定义其使用的各种字段,然后将其用 BASE64 编码,这样是不是好理解一些。


有了上述基础知识: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 的官方文档使用更佳。


安装完 Libtasn1 后会有如下一个命令:asn1Coding asn1Decoding asn1Parser,我们先从 asn1Parserasn1Decoding 入手熟悉下 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

其中第三个参数表明要解码的类型,结果如下图所示,可以看到先前赋值的结果都解析出来了包括类型、名字和取值。


上面介绍了使用命令行进行 ASN.1 文件的解析,数据的编解码,接下来介绍使用 Libtasn1 提供的 API 函数用代码完成这一过程

首先,Libtasn1 的头文件是 ,需要自行在代码中包含;

第1步:使用 ASN.1 格式的协议有许多,大家的协议字段定义都不相同,我们拿到了编码后的二进制文件,然后希望从中解析出有用的数据,首先就需要知道协议的 ASN.1 是什么,Libtasn1 库中使用树来描述这些定义,有两个 API 函数完成这一任务 asn1_parser2treeasn1_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 {
    	...
    }
}

Libasn1 使用 ?1、?2 这样的形式访问列表中的第一、二个元素,也就是代码中的req-body.cname.name-string.?%d,然后使用 asn1_read_value 读取元素的值。这个函数和 asn1_number_of_elements 的使用类似,就不解释了。


基本上通过使用 Libasn1 API 函数对 Kerberos 进行解析读取的套路就是上面那四步,如果你有其他需求的话,比如写入数据之类的,可以采用官方 API 配合源码测试代码的方式学习使用。


完。