NetAnalyzer笔记 之 十一 打造自己的协议分析语言(1)初衷与语法构想
回头看看NetAnalyzer开发系文档上次一篇竟然是2016年,老脸一红。不过这几年墨云成功的讨到一个温柔贤淑的老婆,有了一个幸福的家庭,去年9月又有了一个大胖儿子,想想也就释然了^_^
其实这几年NetAnalyzer的开发一直也没有中断过,上一篇的NetAnalyzer还是3.x系列的版本,现在最新的版本已经是 5.6.0.38 版本了,去年8月份更新的
NetAnalyzer官网地址: http://twzy.sinaapp.com/
废话不多说了,回到今天的主题--打造自己的协议分析语言。
1. 初衷
《道德经》中有“道生一,一生二,二生三,三生万物”的说法,描述了万物从少到多,从简单到复杂的一个过程。在计算机中我们所面对的各种各样的文件,如:图片,文本,音乐甚至最基本的程序文件其实都是通过二进制数据也就是大量的0或1的方式存储在硬盘或内存中的。但是如何从0和1转换为我们熟知的各种媒体数据呢,这就需要根据0和1不同的排列顺来完成,这就是编码方案,而这种编码方案更通俗的来说就是一种协议,这种协议来约束不同的设备,不同的系统当遇到对应的数据是应该将其解析为什么文件。
当今网络作为与我们生活朝夕相关的事物,给我们带来了便利的生活体验,有些应用甚至可以做到计算机与智能手机之间的无缝切换,这就得益于网络中各个层次的协议完美对接。目前的互联网模型大部分都是基于经典的TCP/IP协议,虽然其安全性、传输效率等问题在这些年逐步暴露出来,但是其拥有的完整协议体系却是其他协议体系不具备的。从物理层使用的CSMA/CD(载波监听多路访问冲突检测)协议实现端到端的数据传输,再到网络层中IP通信协议,RIP、OSPF网络路径发现协议,实现从主机与主机实现跨网数据传输的功能,在而到保证让主机接口可以获取到无差别数据的TCP协议、实现最终数据呈现应用层协议,如http协议。这些协议都是公共开放的协议类型,而有部分软件就是基于这些公共协议进行工作的,如基于http的各种浏览器、基于FTP的各种文件传输 软件,虽然基于公共协议的软件很多,但是我们大部分情况下使用的更多的是专属软件,这部分软件具有自己独立的协议,而且很大的一部分是在TCP协议之上建立起来的私有独立应用协议。
2.MangoScript
这里打造自己的协议分析语言的初衷就是为了解析这部分协议,而我给它起了一个名字MangoScript,私有协议是一个公司或一个组织定义的一套专属于内部的数据交流方案、这些协议可能因为涉密或是团体影响力过小并不能被外部人员获取到。而想要分析这些数据,箱借助协议分析工具进行分析是不可能的,而手动从各种二进制数据中获取信息,效率又极其低下。MangoScript的思想就是通过将数据方案转换为对应的脚本代码,将代码绑定到NetAnalyzer,通过NetAnalyzer实现与解析公共协议无差别的数据分析。
MangoScript作为NetAnalyzer扩展协议分析的专职语言,区别于现有流行的C\C++ java C# 之类的语言,设计的更像一种配置文件,可以通过不同的配置方式,实现对数据流的解析。脚本使用协议分析树的逻辑方法,脚本编辑方式就是协议树的呈现方式,即是没有接触过编程的人也可以轻松进行代码编写。
当然,因为MangoScript正处于测试开发阶段,所提供的功能也不近完善,这需要读者的体谅,也很希望读者可以提供一些好的建议与意见。目前脚本采取宽泛执行的方式,即对于一些语法错误会自动忽略,以保证尽可能的完成数据分析。
3.MangoScript简单语法规则
通过MangoScript可以快速的对数据的结构进行描述与呈现。并且语法非常简单,适合快速入手使用。 在MangoScript中大小写不敏感(部分函数提供的参数除外),支持定义中文字段。
代码整体可以认为有两部分:
- 对代码整体结构的约束定义(block代码)
- 对具体数据呈现方式的定义(node代码)
从某种意义上来说MangoScript更像是一种配置文件,因为该语言目前还不支持判断、循环等逻辑,只支持一种简单的分支,此外还不能自定义数据处理函数。 这也是墨云一直以来称其为语言有迟疑的地方。 然而从解析数据来看却要比真正所谓的脚本语言要快捷的多。
这是一段最简单的MangoScript代码:
1 /* 2 定义block(结构块) 3 */ 4 5 block main 6 { 7 //定义标题 8 title "我是标题"; 9 //定义一个node(节点) 10 node node1= select(0,1/*注释 选则长度*/); 11 } 12 13
block
block 我称之为结构块。用于定义一组数据呈现的结构体。block中包含一整块数据的规划与处理,代码定义方式为
1 block <name> 2 { 3 … 4 }
其中代码中name为必填项。
如在的上面的代码中就定义了一个名字叫main的block。
在结果呈现上,大部分情况下表现为父级节点。 在一段代码中可以定义多个block,显式定义中,不允许存在嵌套(在switch函数下可以定义匿名block,这种定义方式为隐式定义。具体请看该函数的功能说明)。并且在该段代码中必须存在一个名称为main的block作为代码起点,结构块的先后顺序不受影响数据解析方式。
node
node是MangoScript用于描述数据呈现方式的最基本部分。node通常用来描述一个子节点由数据转为自己制定类型过程和数据的呈现方式,具体的呈现内容则是通过一系列函数链进行不断演进的结果。 函数部分通过内部定义(MangoScript不支持自定义函数)MangoScipt通过对各种函数进行对应的选取排列来获取需要的值,而具体的函数方式可以通过函数API文档得到相关的帮助信息,如下代码为定义一个节点:
1 node data = select(2, 8).text("ascii");
首先使用node:作为前缀,然后定义节点名称对应的变量没成data ,定义完名称之后通过=开始函数部分的编写。
首先我们需要选取数据区域,在这里我们通过选取函数select获取从数据块中第2个开始8个字节的数据块。
当完成数据选取以后,再执行text将选取到的8个字节转为文本编码为“ASCII”的字符串。
最后将结果转换后的结果赋予变量data ,node定义以分号“;”结束。部分node还有方法体,使用大括号包裹起来,还是以分号“;”结尾。
函数
在node中用于描述数据转换的方式,就是这里要说的函数。函数通常使用 “.”符号开始,如上面的代码,其中的select函数和text的函数都是通过 “.” 符号开始的,接下来就是函数名称,并且在括号中输入相关的控制参数。因为不同的函数输入的参数类型和内容不一样,并且随后还会不断的扩展函数库, 通过多个函数一起链接,node就形成列函数链,函数链从左往右,前一个的函数输出数据是后一个函数的输入数据,对于第一个函数,默认为全部的待处理数据,这就是为大节点都是以select函数作为开始的,对于最后一个函数则统一处理为文本进行输出。
一些特殊函数的说明
在MangoScript中有一些特殊的函数需要单独的说明一下。这些函数为脚本提供了最基本的数据访问和结构控制的功能,整个MangoScript都是建立在这些功能上面的。
select(offset,length) select函数,数据选择函数,是MangoScript中核心函数之一,主要功能是从待分析数据块中根据offset参数和length参数获取到需要处理的数据,如上面的代码中从第3个字节开始(索引都是从0开始的,所以offset=2)找到8个字节(length=8)作为待处理的子字节数组。对于select函数,除了可以进行正常的选择转换之外,还可以进行细节扩展,对该字段进行进一步的描述,通过以子节点的方式呈现出来。代码如下:
1 node 时间戳= select([帧长度]-12,14).text("ascii") 2 { 3 node 年=select(0,4).text("ascii"); 4 node 月=select(4,2).text("ascii"); 5 node 日=select(6,2).text("ascii"); 6 node 时=select(8,2).text("ascii"); 7 node 分=select(10,2).text("ascii"); 8 node 秒=select(12,2).text("ascii"); 9 };
如上面的时间戳节点,呈现的只是简单的将选中的数据转为以ASCII编码的文本,但是如果我们想要知道其内部的具体细节,则需要借助子节点功能。在主节点最后一个函数后面添加大括号,再在大括号中定义子节点,这里的子节点选择的数据为主节点选中的数据,所以需要将索引值置为0重新开始。如:
node 年= select(0,4).text("ascii");
最后需要注意的是,在大括号后面加需要有一个分号,结束对该节点的定义。
while(offset,length) while函数,结构循环函数。在数据分析过程中,需要特定的结构,对数据块依次进行相同的分析,很多情况下,我们并不知道该循环需要执行的次数,这就需要通过脚本来自行判断,这时候就用到了while函数,该函数是有方法和select带子节点结构一致,但是解析方式是不一样的,该函数只需要定义开始分析位置,以及所要分析的长度,之后再在函数后面添加需要循环的分析块,该函数会自动根据填写的内容自动判断需要循环的次数。示例代码如下:
1 node 序号= while(4,16) 2 { 3 node sub=select(0,2); 4 node sub2=select(2,6); 5 };
该代码会根据偏移量最大的一个节点(该节点的偏移量加选择长度)作为一次循环的结束的标志,进行自我判断,对数据进行循环解析,直到所选数据结束为止。如上面代码,会被解析两次,因为sub2节点结束时候数据偏移到8(2+6),当前选择的数据为16,所以可以再次进行一次循环。
switch(offset,length) switch函数,转换函数。在一些业务分析过程中,通过会有根据不同字段,后续所要分析协议格式不同的问题。这种情况下就会用到switch函数,在switch中有两个参数,和select一样用来选择数据,但是与select不同的是,当switch选择完数据后,会直接转为数字类型(也就是说length最大为4), 并在switch对应的函数体内进行判断,在改函数体内,通过case关键字列出不同的值,并且指向不同的block,如果switch选择的数据与其中一个case的值相对应,则会指向对应的block代码, 在case中指向的block有两种方式:1.通过>方式的指向,该种指向为外部定义的block,后面只需要输入对应的block名称即可;2.通过:方式的指向,这种指向使用匿名方式建立一个block,不需要是使用block关键字,不需要定义名称,只要在后面输入大括号,就可以进行代码输入了,和平时定义block一样。
如下代码:
node date = switch(0,2) //switch 自动将其转为无符号整数 { case 0x0101>Test,//指向一个block case 0x0102: //该种结构又叫做匿名block { node test=select(3,34); } }; …… //定义的另外一个节点 block Test { node name = select(0,1).num(4,"hex"); }
对于输入数据[0x01 0x01 0x03 ……],我们通过switch(0,2) 获取到数字0x0101(十六进制方式),则会跳转到block名称为Test的结构块进行分析,注意我们这里使用 case 0x0101>Test 这是使用外部block调用的方式。
如果输入数据为[0x01 0x02 0x03 ……] 得到的数字为0x0102,在这里使用的方式是内建匿名block:
1 case 0x0102: 2 { 3 node test= select(3,34); 4 }
这两种方式都可以按照普通的block一样使用。 当分析完数据后,并不会显示switch所在的节点内容,而是使用对应case所指向的block中的所有节点来代替。
ifblock(flag) ifblock函数,结构判定函数,在处理一些协议总会看到Magic字段,如某款IM软件协议中第一个字节就是0x02,这些协议通常是和其他服务共用了某些特征,如某款IM软件使用8000端口号,但是有好多应用都会使用这个端口,为了正确的识别这些协议,于是有了ifblock方法。 该方法的flag为数字类型,所以前面select所输入的长度不能超过4。比如我们在判定是否为某款IM软件协议的时候,输入代码:
1 node flag=select(0,4).ifblock(0x02);
如果待测数据第一个字节为0x02 则继续进行下面的分析,如果不是则直接跳出,返回空的block。
display(flag) display,显示函数,在一些协议中,有些字段本身其代表的是一种类型,如:icmp中的类型字段,对应每个字段都有不同的意义。但是在做协议分析的时候,我们拿到的仅仅是代码,如果能将代码对应的字段使用文本方式呈现出来,则更加具有可读性。 display函数正是基于这种思想实现的。在使用display之前,我们首先需要定义对应关系。display的对应关系我们这里叫做enum,该结构被定义在block外部,主要是为了共享enum,以下以某款IM软件协议(精简过)为例,定义方式如下:
1 block main 2 { 3 …… //nodes 4 } 5 6 enum imcomond 7 { 8 case 0x0001 > "注销登录", 9 case 0x0002 > "心跳信息", 10 case 0x0004 > "更新用户信息", 11 case 0x0005 > "搜索用户" 12 ……… 13 }
在block中定义display Node
1 node 命令= select(3,2).display(imcomond);
当待测数据为[0x01 0x01 0x01 0x00 0x02 0x00],输出为:
命令:心跳信息
其他功能点
MangoScript还包含了enum 等功能项以及函数信息,详细内容可以参考官网的NetAnalyzer开发文档。
这里有个针对某协议的示例:
1 block main 2 { 3 title "协议测试"; 4 node 前缀= select(0,2); 5 node 序号= select(2,2).num(); 6 node 版本= select(4,3).eachbyte(".","num");//num text 7 node 数据类型= select(7,1).display(FrameType); 8 node 帧长度= select(8,2).num(); 9 node 数据类型=select(10,1).display(mtype); 10 node 代码= select(11,3).reverse().num(); 11 node 数据块= select(14,[帧长度]-26); 12 node 时间戳= select([帧长度]-12,14).text("ascii") 13 { 14 node 年= select(0,4).text("ascii"); 15 node 月= select(6,2).text("ascii"); 16 node 日= select(6,2).text("ascii"); 17 node 时= select(8,2).text("ascii"); 18 node 分= select(10,2).text("ascii"); 19 node 秒= select(12,2).text("ascii"); 20 }; 21 node 校验和=select([帧长度]+2,2).num(); 22 node 后缀=select([帧长度]+4,2); 23 24 } 25 enum FrameType 26 { 27 case 0x00 > "帧类型1", 28 case 0x01 > "帧类型2" 29 } 30 31 enum mtype 32 { 33 case 0x00 > "测试类型1", 34 case 0x01 > "测试类型2", 35 case 0x02 > "测试类型3" 36 }
在该代码中,可以看到只定义了一个数据块main和enum块,main数块的title 为“协议测试”然后定义了11个Node,都是常规定义定义方法,这里需要注意一点,从数据块开始在select函数的参数中有一个用中括号括起来的帧长度字段。这是一种引用字段数据的方式,但是使用引用字段的字段必须定义在被引用字段之后。除了引用字段还有找一种叫做公共参数的变量、虽然目前在MangoScript中只定义了一个也就是END 表示一直到数据末尾,所以我能可以这样使用它:
1 node 数据块= select(0,END);
通过改代码可以获取到整个数据块。
最后我们来看看代码的运行情况吧:
在下面的一篇中我们将会详细说明MangoScript编译器