2020年9月8日工业网络安全公司Claroty研究人员发布博客称,在威步(Wibu-Systems)的CodeMeter第三方许可证管理组件中发现了六个关键漏洞,这些漏洞会使工业系统遭受远程攻击,包括关停设备或进程、植入勒索软件、植入其他恶意软件或执行进一步的恶意操作。漏洞类型包含了内存损坏、加密强度不足、验证不当等,利用这些漏洞可发起拒绝服务攻击或者远程代码执行。 CodeMeter是专为软件开发商及智能设备企业提供的一体化技术,旨在保护软件免受盗版和逆向工程的侵害,提供许可管理功能,也包括了可防止篡改和其他攻击的安全功能。CodeMeter已被ICS供应商广泛使用,其中包括罗克韦尔、西门子、施耐德、ABB、Codesys、WAGO及菲尼克斯等,因此本次漏洞受影响的范围几乎波及到整个ICS行业。 经过研究披露的六个严重漏洞,可分为三类攻击面:一类是CodeMeter私有的加密通信协议、一类是许可证文件、另外一类是WebSocket接口,详细漏洞信息见下表所述。本文主要研究CodeMeter私有通信协议中的加密算法,分析加密算法的实现过程、使用的技术和方法,以上分析内容也是在详细阐述漏洞CVE-2020-14517的成因。
罗克韦尔Stuodio5000组态软件中包含了CodeMeter组件,因此在安装有Stuodio5000组态软件的Windows7 64位环境下准备调试,CodeMeter使用了很多反调试技术,此处的操作系统需要做一些特殊处理在此不详细展开论述。本文分析的CodeMeter组件版本为6.20,在本次披露漏洞的受影响范围内。 启动操作系统后可发现CodeMeter的服务随之开启,其中PID为3248的CodeMeter开启了侦听模式可接收外部连接或访问,下图中蓝底色标记出的两个进程的通信端口相互对应,有互发数据的可能性。
为了验证PID为6436的CodeMeterCC和PID为3248的CodeMeter是否有通信行为,利用wireshark抓取报文,由下图可知端口为49192的CodeMeterCC作为客户端,端口为22350的CodeMeter作为服务器,两者之间有通信行为。以此为研究对象,在客户端的CodeMeterCC相关组件中寻找算法处理位置,开始分析与调试。
利用X32dbg附加CodeMeterCC组件,根据send函数逐步跟进,初步找到通信处理的组件为wibucm32.dll,将该组件置于IDA中静态分析,快速浏览字符串等关键信息点,并结合动态调试,找到了数据报文加密处理函数,如下图所示,该函数中又包含多个子函数,针对每个子函数做静态分析与动态调试,初步获知每个函数的作用。 在getrandomKey函数中,首先客户端利用GetTickCount获取到了系统启动时间,接着将该值做一系列运算后再进行SHA-1处理,得出20字节的hash值。 经过SHA-1计算得出20字节的hash值,将该hash值分作两组存放在内存中,一组是从开头开始到第16字节结束,一组是从第4字节开始到末尾结束,每组刚好16字节,得到2组数值分别为加密算法中的key和iv。如下图为动态调试时计算出的hash值存放位置。 根据函数处理流程,在得到AES加密的key和iv后(上述分析中的第一组16字节和第二组16字节),还针对原始明文报文做了处理。处理包括:长度扩展、报文长度信息填充、原始明文CRC32校验填充。长度处理算法:原始长度+0x27后再将后边不满0x10字节舍弃,目的是保持送入的数据为0x10的倍数,避免AES算法中还需做pad处理。 动态调试中跟踪该过程的处理如下所示,红框内所示为原始明文信息,蓝框内为明文长度信息用4字节表示,绿框内为B8长度的原始明文的CRC32校验值,红框与蓝框中间的数据为内存片区内未正确清理的垃圾数据,从0A开始到F8结束总共长度为D0字节。 处理完的报文需要送入后续的AES进行计算,该函数中先取getrandomKey中计算出的key和iv值,并将处理后的D0长度报文同时送入函数进行AES-CBC模式加密处理。加密处理后即得到了发送的密文信息。
下图为加密处理后的密文信息,在加密密文过程中其使用了2次加密,达到的效果是与标准AES-CBC模式得出结果的后两行刚好颠倒顺序,比如下图中蓝色框体内的数据为标准AES-CBC算法的最后一行,而下图中的最后一行为标准AES-CBC算法的倒数第二行。 至此整个通信报文的加密算法分析清楚,见下图所示。 为了验证算法分析的正确性,有以下两个思路: A. 调试过程中从内存中扣取原始报文和系统启动时间GetTickCount值送入编写的加密函数脚本中运行,查看脚本运行结果和调试结果是否一致; B. 修改客户端和服务器GetTickCount值为定值,捕获两者之间的流量,对流量进行解密处理,解密后的明文是否符合原始报文定义;
通过两种思路验证,均得出正确结果,表明算法分析正确。此处展示第一种思路的验证过程及其数据。如下图为调试过程中捕获到的本次系统启动时间值0x019e5cb8,原始明文为0A开头的B8长度数据保存为datatest.bin文件。 在python脚本中输入datatest.bin与系统启动时间值0x019e5cb8,代码如下:
运算后的结果如下。 调试跟进使其运算至AES加密算法完,得到的内存中的数据如下,经过对比两者加密数据一致。
以上完成了加密算法的所有分析,至于如何制作伪造的客户端发送恶意攻击报文还需要突破核心问题:伪造客户端与被攻击CodeMeter服务器之间GetTickCount同步。要知道CodeMeter服务器中的系统启动时间,采用远程访问的方法不现实暂时也没有合法的方式获得远程系统的启动时间,因此更换思路,只要能通过服务器端的解密算法和CRC校验比对就说明我们使用的系统启动时间值是正确的。那么就可以采用如下步骤实现伪造客户端搭建: A. 伪造客户端使用一个猜想的系统启动时间值T1,和原始明文带入加密算法计算加密报文,并将该报文发送至CodeMeter服务器; B. 接收CodeMeter服务器的响应报文并送入解密函数,解密出的响应报文明文是否符合协议格式,如不符合再次更换T1值,记录接收到响应报文的时间点time0; C. 更换T1值再次尝试的过程需要离线操作,以提高爆破速度,直至解密出的响应报文符合协议格式内容且为成功,将成功的系统启动时间记为T2,并记录此时的时间点time1; D. 更换此次需要发送至CodeMeter服务器加密报文的GetTickCount值为(T2+ time1- time0),加密封装后发送至CodeMeter服务器,解密响应报文查看是否正确,如不正确再小范围调整GetTickCount值直至正确,此时就可以与CodeMeter服务器进行加密通信了,也可以再此基础上变异输入的原始报文进行FUZZING测试。
本文以CodeMeter被披露的六个严重漏洞为出发点,深入分析了其中的CVE-2020-14517漏洞,即CodeMeter的通信算法加密强度不足问题,搭建调试环境,结合静态分析与动态调式技术手段,逐步找到处理函数并跟进每个子函数,厘清了整个通信报文的加密过程及采用的加密算法,并编写加解密脚本对通信流量及其原始报文进行解密、加密验证,证实了分析的正确性。 截止目前CodeMeter影响的ICS厂商已经增加为10家(后续还有可能持续增加),分别为ABB、COPA-DATA、Pepperl+Fuchs、Phoenix Contact、Pilz、罗克韦尔、西门子、施耐德、Codesys、WAGO,这10家的部分产品受这些漏洞的影响,涉及的行业包括医疗设备制造商、汽车制造商、能源电力、过程设计、水务等等,通常下游的用户往往不会关注软件或设备内部的组件漏洞,因此也没意识到这一易受攻击的组件正在生产环境中运行。此类漏洞类似于Ripple20,属于供应链级别的威胁,组件被各个大厂使用分布在全球各个生产环境中,而且修复公共组件的成本和响应周期会很长,通常在这个过程中会给别有用心的攻击者提供了良好的窗口期。鉴于此,呼吁各个厂商尽快发布匹配自己产品的缓解措施和方法,并督促最终的用户及时更新或者采取安全防护措施,避免更大规模的风险。
from
socket
import
*
from
Crypto.Cipher
import
AES
import
base64
from
binascii
import
*
from
struct
import
*
import
ctypes
from
hexdump
import
*
from
Crypto.
Hash
import
SHA256
from
Crypto.
Hash
import
SHA
from
zlib
import
crc32
BLOCK_SIZE
=
16
pad
=
lambda
s: s
+
(BLOCK_SIZE
-
len
(s)
%
BLOCK_SIZE)
*
\
chr
(BLOCK_SIZE
-
len
(s)
%
BLOCK_SIZE)
unpad
=
lambda
s: s[:
-
ord
(s[
len
(s)
-
1
:])]
def
aesEncrypt(key,iv, data):
cipher
=
AES.new(key, AES.MODE_CBC,iv)
result
=
cipher.encrypt(data)
enctext
=
result
return
enctext
def
aesDecrypt(key,iv, data):
cipher
=
AES.new(key, AES.MODE_CBC,iv)
text_decrypted
=
cipher.decrypt(data)
return
text_decrypted
def
packet(data):
size
=
len
(data)
lenth
=
pack(
'I'
,size)
crcdata
=
pack(
'I'
,(crc32(data)&
0xffffffff
))
junkdata
=
a2b_hex(
'E71EBC2729DE4EEB224B234C32477FDD'
)
final_packet
=
data
+
junkdata
+
lenth
+
crcdata
return
final_packet
def
encrypt_codemeter_msg(data,tick):
ori_var
=
tick
/
/
1000
ori_var1
=
ori_var
*
1000
ori_var2
=
(ori_var1
/
/
1009
)
skey1
=
SHA.new(pack(
'I'
,ori_var2)).digest()
key
=
skey1[
0
:
16
]
vi
=
skey1[
4
:
20
]
encrypt_data
=
aesEncrypt(key,vi,data)
size
=
len
(encrypt_data)
invrt_data1
=
encrypt_data[
0
:size
-
32
]
invrt_data2
=
encrypt_data[
-
32
:
-
16
]
invrt_data3
=
encrypt_data[
-
16
:]
final_encrypt
=
invrt_data1
+
invrt_data3
+
invrt_data2
return
final_encrypt
if
__name__
=
=
'__main__'
:
with
open
(
'datatest.bin'
,
'rb'
) as fd:
data_test
=
fd.read()
msg
=
encrypt_codemeter_msg(packet(data_test),
0x019e5cb8
)
hexdump(msg)
from
socket
import
*
from
Crypto.Cipher
import
AES
import
base64
from
binascii
import
*
from
struct
import
*
import
ctypes
from
hexdump
import
*
from
Crypto.
Hash
import
SHA256
from
Crypto.
Hash
import
SHA
from
zlib
import
crc32
BLOCK_SIZE
=
16
pad
=
lambda
s: s
+
(BLOCK_SIZE
-
len
(s)
%
BLOCK_SIZE)
*
\
chr
(BLOCK_SIZE
-
len
(s)
%
BLOCK_SIZE)
unpad
=
lambda
s: s[:
-
ord
(s[
len
(s)
-
1
:])]
def
aesEncrypt(key,iv, data):
cipher
=
AES.new(key, AES.MODE_CBC,iv)
result
=
cipher.encrypt(data)
enctext
=
result
return
enctext
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
最后于 2021-3-16 19:18
被ic3blac4编辑
,原因: