-
-
[原创]【病毒分析】以国内某安全厂商名字为后缀的BeijingCrypt勒索家族病毒分析
-
发表于: 2024-9-5 10:23 5045
-
本文主要介绍为BeijngCrypt勒索病毒家族的最新变种,即.halo勒索病毒,该家族首次被发现时间为2020年。曾在2023全球勒索软件研究报告中,以8.39%的占比位列国内第五大活跃勒索病毒家族。从2020年发展至今,期间存在多次变种,这几年间一直保持着每年更新1-2次升级变种的频率,主要加密后缀有:.BeJing、.360、.halo、.file、.520、.genesis、.baxia;
该家族最新病毒为.halo病毒,最早发现于2023年初,该病毒会利用远程桌面爆破、数据库端口攻击和垃圾邮件等多种方式进行传播,加密用户的文件并要求用户支付赎金才能进行文件解密。
文件名: halo.exe
编译器: Microsoft Visual C/C++(19.16.27045)[C++]
大小: 1592533(1.52 MiB)
操作系统: Windows(XP)
架构: I386
模式: 32 位
类型: 控制台
字节序: LE
MD5: cfb990436ef17c7c77cac89718bca578
SHA1: e7ea093d67c723777de3d63eb809c0521aaf7b78
SHA256: bf4d07ae980e8e3968611027779664476dab1f8104c0e4d6dfb527ec106fcc12
!_INFO.txt
sierting.txt
加密文件名 = 原始文件名+加密后缀 ,例如:sierting.txt.halo
文件原始大小+128Byte数据
文件加密使用了AES-CFB加密算法,对于文件末尾的128字节是采用了RSA加密后的数据,具体RSA加密了什么可以看下面的分析。
由以下内容:
1.刚才生成的随机数IV
2.写入到密钥文件中的128字节的随机数的sha256(random_128_sha256)
3.带部分后缀的文件名(这里需要注意,假如你的文件名是1.txt.txt,它只能取到1.txt为文件名)
拼接成一段完整的数据以后,再进行sha256的计算,将得到的32位的sha256当作KEY。
由RPNG伪随机数生成器生成的16字节的随机数据,详情请查看逆向分析的文件加密部分。
使用了勒索病毒程序末尾自带的配置信息中的RSA公钥部分,进行BER解码后获得。
其中ID部分的组成为:base64+KEY。
base64的部分为随机生成的128字节数据的sha256的值再进行了base64编码
Key则是勒索病毒程序末尾自带的配置信息,具体请查看逆向分析部分的读取自身所携带的配置信息内容。
以base64的形式保存了随机生成的128字节的数据,该随机数据用于AES加密时,用作KEY的生成。
该内容由一段数字和base64数据组成。
数子部分是当前程序运行的PID
base64数据部分是随机生成的128字节的数据的base64,跟KEY文件内容一致。
从程序的TLS函数中可以看到有三个TLS函数
分别为sub_151000、sub_151026和sub_151072三个函数,该TLS回调函数作用就是分别调用这三个函数。
sub_151000函数:
主要通过sub_1F66C0函数来实现了一个对于RandomPool的初始化,相当于AutoSeededRandomPool RNG
这句代码;
其余的两个函数都是一样,主要用于后续的几种不同的随机,以防止内部状态一样。
该程序会将所有的功能函数,基本上都在一个类中实现,所以开始时会先获取一下虚表地址,之后再根据其虚表的地址加偏移的方式进行对应功能方法的调用,这时可以恢复一下该虚表的结构,重建一个结构体即可。
1.从Main函数中可以看到,首先开始的便是调用虚表地址,来获取主体函数列表,之后选择调用其hide_cmd_windows函数,实现对自身CMD窗口的隐藏。
2.进入到hide_cmd_windows函数可以看到,该函数主要是通过调用GetConsoleWindow()函数来获取当前窗口的句柄,之后调用ShowWindow函数来实现对当前窗口的隐藏。
该函数会读取处于文件末尾的一段配置文件的内容,其中包含了Key和各种加密所需内容等。
1.在执行完隐藏窗口后,该程序会执行初始化配置信息,主要通过sub_AAD468函数实现。
2.进入到sub_AAD468函数可以看到,该函数会先读取距离文件末尾0x48大小的数据
3.对Key+RSA公钥数据的大小+勒索信和黑名单等数据的大小这部分的数据进行Sha256
4.与文件末尾的那部分数据进行校验,如果一样就进行下面的
5.将RSA公钥部分0xA0大小的数据进行拷贝到结构中
6.将Key拷贝到结构中
7.将勒索信和黑名单等数据拷贝到结构中
8.清空保存以上内的内存,并且关闭文件流
2.之后会交由主功能函数中的find_process_exec函数来进行遍历当前系统所运行的进程中的可执行文件是否为自身。
3.最后若是在进程中查到了存在自身的可执行文件,即将V14和V30两个变量的值设置为1,若是没有则为0。
因为后续会涉及到protobuf数据的解析,所以这里为了后续便于逆向,要先使用工具来对protubuf对象的恢复。
恢复后的对象有:
protubuf流:
使用工具对其进行解析后可以得到内容:
程序内部整体是采用了一个Encodeconfig对象为主体,里面包含了其他几个对象作为其成员,接着就可以根据恢复的数据手动恢复一下对应的对象主体结构:
由该对象中的内容可以大致了解到,所有的路径和后缀的匹配全都是采用了正则的方式。
1.这里会先将config_head_a0的数据拷贝一下
2.调用config_init来对勒索信和黑名单那部分的数据进行解析
3.将读取到的0xBED大小的勒索信和黑名单的数据当作protubuf数据解析到protobuf的几个对象中
4,之后就是将0xBED大小的勒索信和黑名单等数据解析成protobuf对象,供后面的其他功能使用。
1.该函数会先读取程序末尾的key,之后又调用了主要功能类中get_file_name来获取当前程序的路径
2.将上述两端数据拼接到一起
3.计算一下两端数据中的sha256
这里可以写个脚本来对其内容进行验证一下:
发现跟结果一样
该函数主要由主体函数的**sub_E70BE5函数(write_register_table函数)**实现,此功能会向\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
和\HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
注册表中写入键为52245305000b880c91a0d6bf2b982de81be79a57a2c3bb290602b2819f794e37
值为"XXXX/halo.exe b30b4aba8cb7ba54b4f96f5b186828590ed1087fb3dbf5518239e4e325968d82"
1.在执行写入注册表之前会先获取key
2.之后调用write_registr_table函数来实现对开机自启注册表的启动。
3.写入完毕后,将调用unknown_libname_10函数对sha256、key和文件路径进行内存的释放等操作,并且返回。
1.获取了刚才计算的key+文件路径的sha256
2.获取key
3.获取文件路径
4.先将key+文件路径的sha256作为KEY,路径和KEY作为值,如:
写入到开启启动项\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
中
监控上可以看到写入的路径和内容与我们分析的一致
注册表内:
同理还会往\HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Run
写入同样的内容。
该sub_D52540函数功能是主要在系统C:/ProgarmData
下写入密钥文件,密钥文件名称为b30b4aba8cb7ba54b4f96f5b186828590ed1087fb3dbf5518239e4e325968d82
,密钥内容为128字节的随机数的Base64.
1.在写完注册表后,将进入写入勒索信和加密等环节
2.先获取了文件末尾的key,之后调用了sub_D52540函数来实现对密钥文件的后续操作。
该函数主要实现了密钥文件路径的获取、随机数的生成和对随机数进行base64编码,之后调用主体函数的write_key_to_file函数将base64编码以后的随机数据写入到密钥文件所在路径中。
该函数主要实现了对密钥文件路径的获取、密钥文件的创建和密钥编码内容的写入
其中密钥路径的获取主要是采用了SHGetFolderPathW函数来实现的,其中CSIDL为35即代表了C://ProgarmData
这个路径。
通过procmon的结构可以看到成功写入到了该路径中
该函数主要实现了对两个数据结构的初始化和一个链表结构的初始化,其中两个图,一个是存储了所需配置信息的图,另外一个是存储了当前系统物理存储根路径的图,而一个链表则是存储了需要加密的所遍历的路径等。
1.在完成了密钥文件的写入后,将对路径的数据结构进行了一些勒索信的文件名和加密密钥的文件名等数据的插入
该程序中的路径和其他的一些所需数据采用了图的数据结构,如该内存布局所示:
其中前三个地址指向三个结点,分别为off_1467538、off_1467080和off_1467538,而在其三个节点往下4个字节的位置即为存储的路径和所需数据,根据其关系可以绘制如下如:
其中每个节点往下的16字节位置存储的都是路径或者其他数据等,这时插入了"lock"文件名,整体的结构变成了:
这时候最好是根据其所使用的数据结构来进行恢复,并且编写一下对应的遍历脚本,才有助于我们后续的逆向。
2.在完成了所有的配置数据的初始化后,可以看到图中所保存的数据有4个
这里可以写个脚本遍历一下即可看到所有的内容了
3.在完成了对应的配置的图的初始化后,将对其加密所需的路径都添加到一个双向链表中,这里的话也放一下该链表的结构
list:
这里可以看到头指针和里面的存储的数量,之后就是恢复一下对应的节点结构:
list_node:
在获取系统中比较重要的路径是通过主体类中的get_sys_import_path函数实现的,跟写加密密钥的那部分的内容获取关键路径的所用函数一样,都是使用了SHGetFolderPathW函数来实现的。
4.获取系统中的物理驱动器中的路径是通过sub_AB0045函数实现的,其中find_root_path函数会将当前系统中的物理驱动器中的路径加入到数据结构中,用于后续的遍历等。
5.之后就该进入到下一个流程中了
上述已经通过硬逆看出来了,该数据结构是使用了图来对其路径内容进行了保存,所以这里的话就恢复一下对应的结构,具体怎么恢复的这里就不再说了,有需要的可以下去自己看一下红黑树对应的内容即可。
std::map:
std::node:
之后呢再看对应的遍历函数就比较清晰了,比如在插入函数时,里面红黑树的实现树的遍历等:
该函数会解析程序末尾自带的0xA0长度的数据,将其当作Ber编码的方式解析成公钥,以用作后续的RSA加密使用。
1.首先在初始化完毕所需数据结构后,在进入到主流程函数sub_E7974A函数后,第一步就是初始化RSA公钥
2.并且在进入到sub_E7A03F函数后,只能看到一个sub_E89085函数,其中实现了对RSA的解析和勒索信部分ID的计算。
3.进入sub_E89085函数后,将看到会存在两个比较重要的功能函数,一个是sub_E89C30函数,用于RSA的解析和初始化,另外一个就是获取一会儿写入勒索信时,所修要的部分ID的内容。
4.sub_C19C30函数主要实现了RSA公钥的创建与解析,其中RSA公钥的创建完毕后将得到如下内容结构,其中n的位置和e的位置都已标出,大家可以根据STL的String类型差不多模仿的建立一个结构体,以方便后续的逆向。
RSA公钥内存结构:
之后就是进行BERdecode的解析,实现函数是sub_CBB010函数,之后将解析完得到的RSA公钥的n和e给到刚才创建好的Public_Key上。
BER编码:
5.因为整体的执行是调用了Crypto++库中的RSA函数执行的,具体实现都是库函数内的实现,所以这里就不再细致的去分析容器内的实现了,直接就说执行后的结果吧。
执行完毕后就可以得到解析后的RSA公钥了,其中N和E都能看到具体的内容:
该函数会实现在系统桌面路径下写入勒索信
1.在上述执行完毕对RSA公钥的解析后,继续说一下上面没有说的勒索信ID的生成部分
2.在进入到get_ID函数后能够看到是一个Sha256的计算
其中计算的内容即是上面写入加密密钥文件所生成的128字节大小的随机数。
最后即可的到计算出来的sha256:0C897AE780AA16748C8E4F4BC9B9652BA6394493889B9018E3DE350FE62FA176
3.得到了勒索信ID的前半部分后将进行勒索信的替换工作了,主要实现函数是sub_E82CBD函数
4.进入到sub_E82CBD函数函数内部可以看到具体的替换过程,其中将上面得到的sha256编码成base64的形式替换勒索信模板的{{1}}的位置,将程序末尾自带的key替换到勒索信模板的{{2}}的位置即可完成。
5.最后就可以得到完整的勒索信的内容
6.最后将该内容,结合上面初始化过后的config_map里面保存着的勒索信的标题,将其写入到系统的桌面中。
该函数主要实现了对前面存储路径的数据结构中的路径的过滤与启动多线程来实现对文件的加密与勒索信和KEY文件的释放。
1.在执行完毕上面的创建完勒索信以后,将进入sub_C1604D函数执行一系列的获取路径,结合protobuf对象中的过滤内容来进行过滤等操作。
2.多线程的实现
每个线程遍历一个路径:
3.具体多线程的所触发的核心内容在sub_C5198C函数中能看到,该函数主要实现了对以上目录的遍历,外加__LOCK__文件和勒索信文件的写入,首先便是先向该遍历目录下写入勒索信!info.txt和___LOCK__文件,具体实现请看下面的写入!INFO.txt勒索信和写入密钥文件__LOCK_。
4.之后遍历几个传入目录下的子目录,如果遍历到的是目录,那么就继续递归,并且向内写入勒索信和写__lock__文件。
如果是文件则会先判断是否是config_map中所存储的文件,如:!_INFO.txt
、__LOCK__
、b30b4aba8cb7ba54b4f96f5b186828590ed1087fb3dbf5518239e4e325968d82
和当前可执行文件(halo.txt)
,如果不在其中的文件,则可以进入对文件内容进行加密的sub_A9BB67函数。
1.继续上面的文件加密前半部的内容进行详细描述,首先对于写入勒索和写入__LOCK__文件都是由sub_A9DBC6函数实现
2.进入到sub_A9DBC6函数中,可以看到即将进入写入勒索和LOCK的主要实现函数
3.进入到主要实现函数中可以看到首先实现的就是写入LOCK文件,其次才是写入勒索信文件,这里就不再说勒索信的内容是如何生成的了,想了解的可以去看在桌面写入勒索信功能点的介绍部分吧。
1.还是接着勒索信的写入部分继续说,可以看到写入LOCK函数是通过sub_AA327E函数实现的
2.进入到sub_AA327E函数可以看到,首先会先获取一下写入的Lock文件的路径,之后就是创建然后写入内容,写入的内容由当前运行的PID+random_128_bytes被base64编码后的值组成。
可以看到成功在当前遍历的目录下写入该文件
)
3.之后就会调用主要功能函数中的set_lock_file_hide函数来实现对lock文件设置成隐藏文件
。
1.首先文件加密前的准备工作,主要是由sub_A9BB67函数实现的。
2.进入到sub_A9BB67函数中可以看到,该函数首先对文件的后缀进行了判断,判断是否是加密后缀,即防止二次加密的情况发生。
3.之后该函数调用了主体函数中的judge_file_is_exists函数来判断加密文件是否存在,又调用了主体函数的change_file_permissions函数给该文件一个可读可写可执行的777权限,最后对将文件的名称修改为了加密文件的名称。
4.做完以上的准备工作后,就要进入对文件的内容进行加密了,加密前的准备工作主要在sub_AAAEC7函数中可以看到。
5.进入到sub_AAAEC7函数中可以看到,该函数主要实现了加密密钥的生成与加密对象的初始化、文件内容加密、对Key进行加密和加密KEY后的数据的写入等操作,其中对文件进行加密的函数为sub_AB358F函数。
IV是由随机数对象生成的16字节的随机数,由sub_610E10函数实现
KEY的组成是由以下内容拼接起来的数据进行sha256以后的内容,具体由sub_58E655函数实现:
1.刚才生成的随机数IV
2.写入到密钥文件中的128字节的随机数的sha256(random_128_sha256)
3.带部分后缀的文件名(这里需要注意,假如你的文件名是1.txt.txt,它只能取到1.txt为文件名)
最后将以上三部分的数据的sha256当作KEY进行加密使用
6.进入到sub_AB358F函数可以看到获取文件的大小,并且进行判断,之后会根据文件的大小是否小于0x200000字节,来选择是全加密,还是分段加密。
7.这部分可以参考该流程图:
若是大于0x200000字节则会分两部分进行加密,先会加密开头和结尾的数据,而开头数据加密的大小到底是多少,则是由随机数的值来决定的。
根据这个随机数来计算可以得到一个head_block_size,计算方法:
之后除了头部以外的则都是采用了block_size,计算方法:block_size = 0x200000/随机数+0x200000&0xffffffff
(这里的0xffffffff实际上在程序中是没有的,因为如果用别的语言复现的时候会出现溢出,所以最好这里就上了这个值进行约束一下)
这种大文件的分段加密的结构是:开头加密数据+明文数据+加密数据+明文数据+末尾加密数据
8.在对一个文件的头部数据和尾部数据加密完毕后,将要执行对于中间部分即剩余大小的数据进行加密,首先获取了一下剩余中间部分的字节大小,又计算了一下循环次数(循环次数=随机数-2
),最后计算了一下不进行加密的数据块大小,计算方式:不加密数据大小=(剩余大小-循环次数*block_size)/循环次数
。
9.得到了循环次数和不加密的数据大小后,就是开始对剩余大小的内容开始循环加密了
10.加密采用的AES具体实现是sub_750840函数,可以比较清晰的看到数据对齐与加密等操作。
11.最后在文件内容加密完成后,将会在末尾将得到的数据(\x01+\x00+IV+KEY+random_128_sha256+随机字节)一共84字节的信息进行RSA加密,所用的加密公钥上面也介绍过了,最后会得到一个128字节的密文,将其写入到文件的末尾,最后将文件的属性改成只读,就完成了整套的文件的加密操作。
12.最后文件加密完毕,将会调用主体函数change_permissions_only_read函数,实现对文件的权限修改为可读可执行。
1.在执行完毕加密流程后,将执行主功能函数中的clear_register函数,来实现对写入自启动的注册表的删除