在开始分析之前,我们得先了解一下DNP3协议的规约,以便我们在下文更容易的分析其DNP3存在的安全问题以及构造出攻击DNP3协议的EXP,文中难免出现错误地方,如有不对的地方,还望大家斧正。
为了能够更详细的表达出DNP3规约,所以在本文分析中大量引用了原DNP3规约的描述文档
DNP3.0规约的文本共分为三个部分:数据链路层、传输层、应用层。
数据链路层规约文件规定了DNP3.0版的数据链路层,链路规约数据单元(LPDU)以及数据链路服务和传输规程。数据采用一种可变帧长格式: FT3
FT3 帧长格式:
一个 FT3帧被定义为一个固定长度的报头,随后是可以选用的数据块,每个数据块附有一个16位的CRC校验码。固定的报头含有2个字节的起始字,一个字节的长度 (LENGH) ,一个字节的链路层控制字(CONTROL) ,一个16位的目的地址,一个16位的源地址和一个16位的 CRC校验码。
05 64 为数据开始的字节,也可以说是报文头
12为长度,这里Dnp3的长度计算有些不一样,它包括链路报文头中的5个字节,加上传输层和应用层的长度以及除去CRC校验码字节。
c4 链路控制字节
第一位为 表明发送的方向
第二位为 表示发送的设备是主设备还是从设备
第三位为 如果是请求则为纠错,如果是回应则为保留位
第四位为 这一位是说明第三位是否有效、在图上为0则为未开启。
后四位为 功能码
对于主设备来说
0,链路重置
1,进程重置
3,请求发送数据
4,直接发送数据
9,查询当前链路的状态
对于从设备来说
0,同意
1,拒绝
11,回应当前链路状态
03 00 目的地址
04 00 源地址
15 2d 校验码
这部分定义对于DNP数据链路层充当伪传输层的传输层功能。伪传输层功能专门设计用于在原方站和从方站之间传送超出链路规约数据单元 (LPDU) 定义长度的信息。其格式如下:
TH传输层报头
数据块
TH传输层报头:
第一位是final,标识是否为最后一个包
第二位是first,标识是否为最后一个包
后六位为seq,表明当前是第几个包
数据块:
应用用户数据1-249个字节由于数据链路层FT3 帧格式中的长度字的最大限制255,因此传输层数据块的最大长度为255-5(链路层control + source + destination)-1(TH)= 249。
当应用用户数据长度大于249 字节时,传输层将以多帧报文方式传送,并每帧前加TH控制字。如1234=249+249+249+249+238, 分5帧进行传送。
本文本定义了应用层报文(APDU)的格式。
主站被定义为发送请求报文的站,而从站则为从属设备。被请求回送报文的RTU或智能终端 (IEDS)是事先规定了的。在DNP中,只有被指定的主站能够发送应用层的请求报文,而从站则只能发送应用层的响应报文。
应用报文格式:
Request Header
请求报文
Object Header
对象标题
Data
数据
Object Header
对象标题
Data
数据
1. 应用响应报文格式:
Response Header
请求报文
Object Header
对象标题
Data
数据
Object Header
对象标题
Data
数据
请求(响应)报头:标识报文的目的,包含应用规约控制信息(ACPI)
对象标题:标识随后的数据对象
数据:在对象标题内的指定的数据对象
2.应用报文报头字段的定义:
请求报头有两个字段。每个字段为8位的字节,说明如下:
Application Control
应用控制
Function Code
功能码
响应报头有三个字段。前两个字段为8位的字节,第三个字段为两个字节,说明如下:
Application Control
应用控制
Function Code
功能码
Internal Indication
内部信号字
应用控制:一个字节的长度,格式如下:
FIR :此位置“ 1” , 表示本报文分段是整个应用报文的第一个分段
FIN :此位置“ 1” , 表示本报文分段是整个应用报文的最后一个分段
CON :此位置“1” , 表示接受到本报文时,对方须要给予确认,为“0”则不需要
序号:表示分段的序号,1-15
功能码:标识报文的目的,一个字节的长度
例如:请求报文:
0x01 -- 读,请从站读所指定的数据对象
0x02 -- 写,向从站写入指定的数据对象
……
上图数据包分析此处为写信号write (0x02)
内部信号:
共两个字节, 16位,每一位分别表示从站的当前的各种状态。
3.对象标题 (Object Header):
报文的对象标题指定包含在报文中的数据对象或是被用来响应此报文的数据对象。格式如下:
Object
对象
Qualifier
限定词
Range
变程(范围)
对象( Object):
两个字节,指定对象组以及跟在标题后面的对象的变化。对象段的格式如下:
Group
对象组
Variation
变体
对象段规定一个对象组和在该组内的对象变体。对象的组别与变体结合起来可以唯一的规定报文所指定的对象。对象组指定数据的基本形式(如:模拟输入
),对象变体指定数据的形式(如16位模拟输入或32位模拟输入)。
下图可以看到数组为1个,所以为0x01
32 01 这里表达的意思为要读取的数据的基本类型,上图的数据包为Time的类型
07 为限定词。限定词为一个 8位的字节段,规定交程段的意义。变程说明数据对象的数量,起点和终点的索引成所讨论的对象的标识符。
限定词段的格式如下:
R
Index Size
索引规模
Qualifier Code
4位限定词码
R:保留位,置为零。
索引规模( Index Size):
3个Bits,规定前置于每个数据对象的索引规模或对象的规模。
在请求报文中,当限定词码 (Qualifier Code) 等于 11时,1、2、3分别代表数据对象前的索引是1、2、4个字节。0无效。 4、5、6、7保留。
限定词码 (Qualifier Code) :
4个Bits,用以规定变程 (Range) 意义。当限定词码取值 0~5时,变程段包含1个开始范围 (Start Range) 和1个结束范围 (Stop range)。当限定词码取值 6时,则 Range 段的长为零 (即无变程段 ),因为所指定的是所要求的数据类型的全部数据对象。当限定词码取值为 7~ 9 时,则变程段由一个计数值所组成,它指明所讨论的数据对象的数目。
限定词段中限定词码和索引规模的有效组合主要有“0x00,0x01,0x02,0x03,0x04,0x05,0x40,0x43,0x51, 0x54,0x62,0x65,0x07,0x17,0x27,0x37,0x08,0x18,0x28, 0x38,0x09,0xl9,0x29,0x39,0xlb,0x2b,0x3b。
上图可以看到,此处的限定词码(Qualifier Code)为0x07
我们用一张图来概括
好了,到这个地方,DNP3协议规约大致就是这样,那么下面我们来看看它的安全问题。
安全风险分析:
DNP3协议和大多数工控协议一样,在TCP上进行传输时都是未作加密处理的,所以存在中间人攻击和数据重放攻击等安全风险。
本文以文件读取为例子,来讲讲通过协议构造任意文件读取。
通过下图我们可以知道,读取文件的函数操作码为0x19
按照读取文件的大致过程为
打开文件(Open)->读取文件(Read)->关闭文件(Close)
所以,我们需要发送三次完整的数据包就可以读取到文件了,
现在我们来构造数据包
前10个字节为固定字节,所以我们分成两段,第一段定义为Header,第二段定义为Body
Header为:数据头(0564)+数据包总长度(后面总长度)+链路控制c4+目的源地址+校检码
Body这个地方只有几处需要注意的地方,其他的地方直接置0即可,然后就是一些固定的格式操作码,我们不用改变它,所以构造出来的格式就为
链路控制c1+打开文件操作码(19)+数组01+后面字节总数+文件名偏移地址+文件名长度+文件句柄+文件操作码+文件缓冲区+文件名
解决完这些格式之后呢,我们就只需要再解决CRC校检的问题就可以了
DNP3使用的是dnp16的校检,也就是每隔16位就检查一次,我们这里用python来解决这个,在python里面有个包叫做crccheck,它里面已经有了dnp16这个校检函数,所以我们可以直接拿来用,最后根据规约格式,就可以很轻松编写出代码来
我们打开DNP3服务器,将开放20000端口
利用我们写好的工具进行测试,就可以成功读取win.ini文件,这里只能读取一部分文件,想要读取全部文件需要计算偏移
通过抓包,我们可以看到具体的流量情况
具体的通信过程为:
1. 请求打开文件
2. 然后DNP3服务器返回文件句柄等信息
3. 我们拿着这个句柄去读取文件的内容
4. 最后返回我们读取的文件信息
5. 关闭文件句柄
总结
通过对DNP3协议的大致分析,我们可以知道,DNP3在TCP上传输时如果通信双方没有约定一定的加密技术,那么很容易被发包伪造数据,对设备进行攻击。
引用:
https://github.com/wireshark/wireshark/blob/master/epan/dissectors/packet-dnp.c