文章首发地址:
https://xz.aliyun.com/t/14396
文章首发作者:
T0daySeeker
概述
2024年3月底,卡巴斯基发布了一篇分析报告《DinodasRAT Linux implant targeting entities worldwide》,在报告中,卡巴斯基对DinodasRAT Linux后门进行了简要分析描述,同时,卡巴斯基还在报告中指出:自2023年10月以来,在卡巴斯基的持续监测中,卡巴斯基发现受DinodasRAT后门影响最严重的国家和地区是中国、台湾、土耳其和乌兹别克斯坦。相关报告截图如下:
因此,为了能够快速检测发现DinodasRAT Linux后门的攻击活动,笔者准备对DinodasRAT Linux后门进行详细分析,并尝试从其网络侧提取相关通信特征,便于对其攻击活动进行检测识别。
在本篇文章中,笔者将从如下角度对DinodasRAT Linux后门进行剖析:
-
DinodasRAT Linux后门功能分析:基于逆向分析,对其样本功能进行详细剖析; -
DinodasRAT Linux后门通信数据包分析:样本运行后,会向控制端发起上线通信,因此,我们可以基于其上线通信数据包剖析DinodasRAT Linux后门的通信数据结构及原理; -
DinodasRAT Linux后门通信数据解密尝试:基于逆向分析,尝试对其通信上线数据包的数据结构进行解析,并进行手动解密; -
模拟构建DinodasRAT Linux后门通信数据解密程序:基于逆向分析,尝试模拟构建通信数据解密程序,实现自动化的对其上线通信数据包进行解密;
DinodasRAT功能分析
根据卡巴斯基报告中提供的样本hash信息,笔者成功下载了两款DinodasRAT Linux后门样本,梳理对比信息如下:
MD5 | 备注 |
---|---|
decd6b94792a22119e1b5a1ed99e8961 | 反编译代码中「带原始函数名」,使用「TCP协议」进行外联通信 |
8138f1af1dc51cde924aa2360f12d650 | 反编译代码中「不带原始函数名」,使用「UDP协议」进行外联通信 |
由于decd6b94792a22119e1b5a1ed99e8961样本的反编译代码中可以查看原始函数名,因此,笔者将以此样本作为案例进行DinodasRAT Linux后门样本功能剖析。
互斥对象
通过分析,发现DinodasRAT Linux后门运行后,将在当前目录下创建一个隐藏文件,此文件将用作互斥锁功能,用以确保当前系统中只运行一个实例,隐藏文件的文件名格式为:
(当前程序运行目录)/.(当前程序名)(当前程序运行的传递参数).mu
例如:
/home/kali/Desktop/test #当前程序运行路径
/home/kali/Desktop/.testd.mu #用于互斥锁作用的隐藏文件
相关代码截图如下:
自启动
通过分析,发现DinodasRAT Linux后门运行后,将判断当前系统版本信息,若当前系统为Red Hat或ubuntu,则此后门将附加自身于/etc/rc.local或/etc/init.d/中,用以实现DinodasRAT Linux后门的开机自启动,相关代码截图如下:
守护进程
通过分析,发现DinodasRAT Linux后门运行后,将调用daemon函数创建守护进程,然后其又将使用父进程PPID作为参数再次运行DinodasRAT后门程序,相关代码截图如下:
实际运行效果如下:
获取设备信息
通过分析,发现DinodasRAT Linux后门运行后,将尝试获取当前主机信息,并将基于主机硬件信息、当前时间等信息构造被控主机的唯一标识码,此唯一标识码后期将用于心跳通信,相关代码截图如下:
硬编码外联地址
通过分析,发现DinodasRAT Linux后门的外联地址是通过硬编码的方式内置于样本文件中的,相关代码截图如下:
decd6b94792a22119e1b5a1ed99e8961样本的外联地址信息如下:
8138f1af1dc51cde924aa2360f12d650样本的外联地址信息如下:
多种通信方式
通过分析,发现DinodasRAT Linux后门支持TCP、UDP多种通信协议进行外联通信,相关代码截图如下:
外联加密通信
通过分析,发现DinodasRAT Linux后门在进行外联通信时,将调用MackControlBuf函数对通信载荷进行加密或解密,相关截图如下:
通信载荷加密、解密代码截图如下:
远控功能
通过分析,发现DinodasRAT Linux后门支持24个远控功能指令,远控功能较全面,相关代码截图如下:
远控功能梳理如下:
远控函数 | 远控功能 |
---|---|
DirClass | 列目录 |
DelDir | 删除目录 |
UpLoadFile | 上传文件 |
StopDownLoadFile | 停止上传文件 |
DownLoadFile | 下载文件 |
StopDownFile | 停止下载文件 |
DealChgIp | 修改C&C地址 |
CheckUserLogin | 检查已登录的用户 |
EnumProcess | 枚举进程列表 |
StopProcess | 终止进程 |
EnumService | 枚举服务 |
ControlService | 控制服务 |
DealExShell | 执行shell |
DealProxy | 执行指定文件 |
StartShell | 开启shell |
ReRestartShell | 重启shell |
StopShell | 停止当前shell的执行 |
WriteShell | 将命令写入当前shell |
DealFile | 下载并更新后门版本 |
DealLocalProxy | 发送“ok” |
ConnectCtl | 控制连接类型 |
ProxyCtl | 控制代理类型 |
Trans_mode | 设置或获取文件传输模式(TCP/UDP) |
UninstallMm | 卸载自身 |
心跳通信
通过分析,发现DinodasRAT Linux后门运行后,将循环发送心跳数据包,心跳数据包内容即为前期获取设备信息构造的被控主机唯一标识码,相关代码截图如下:
DinodasRAT通信数据包分析
由于decd6b94792a22119e1b5a1ed99e8961样本与8138f1af1dc51cde924aa2360f12d650样本分别采用的TCP、UDP通信方式,因此,我们就可基于以上两个样本获取DinodasRAT Linux后门的TCP、UDP通信上线数据包。
TCP通信数据包
尝试构建模拟环境,即为成功捕获decd6b94792a22119e1b5a1ed99e8961样本的心跳通信数据包,相关数据包截图如下:
UDP通信数据包
通过网络调研,笔者发现,在any.run沙箱平台上,曾有人于2024年3月19日上传了8138f1af1dc51cde924aa2360f12d650样本,因此,any.run沙箱平台成功记录了当时8138f1af1dc51cde924aa2360f12d650样本的通信数据包,相关截图如下:
相关数据包截图如下:
DinodasRAT通信解密尝试
为了能够成功的对DinodasRAT Linux后门的通信流量进行解密,笔者也是花费了不少时间对其通信加解密函数进行剖析,最终成功实现了通信数据的解密尝试:
-
起初,笔者尝试基于逆向分析对其通信加密函数逻辑进行梳理,由于其在函数中多次调用了加密前数据、加密后数据、随机数据等,导致笔者在其加密逻辑中迷失了方向。 -
笔者尝试调整思路,推测其应该还是借助了某些标准加解密算法,因此,笔者尝试在其加解密函数中寻找算法特征,成功梳理提取了TEA对称加密算法的算子信息。 -
为了进一步梳理整体加解密算法逻辑,笔者尝试通过网络调研,发现卡巴斯基报告中对其加密算法有一段简单的描述,描述称DinodasRAT Linux后门使用了Pidgin的libqq qq_crypt库函数。 -
为了验证报告描述的真伪性,笔者在github中找到了 “https://github.com/cnangel/pidgin-libqq”
项目,在项目的qq_crypt.c代码文件中有相关加解密函数的调用源码。 -
因此,笔者尝试使用golang语言重写了qq_crypt.c代码文件中的加密函数,同时,结合动态调试,对比实际后门样本与模拟加密函数代码的加密结果是否一致,通过多轮模拟代码微调及对比,最终发现加密后的结果一致。
“https://github.com/cnangel/pidgin-libqq”
项目代码截图如下:
通信加解密原理
结合实际后门样本反编译代码及pidgin-libqq项目源码,梳理DinodasRAT Linux后门加解密逻辑如下:
加密函数逻辑如下:
-
取前8字节数据,赋值给crypted32数据和c32_prev数据 -
存放实际载荷长度及随机数据 -
p32_prev数据赋值为0 -
plain32 = crypted32 ^ p32_prev -
循环加密 -
调用qq_encipher函数对plain32数据加密,加密获得crypted32数据 -
crypted32 = crypted32 ^ p32_prev(前8字节加密前数据) -
「crypted32数据为加密后载荷数据」 -
将plain32数据赋值给p32_prev数据(加密前数据) -
将crypted32数据赋值给c32_prev数据(加密后数据) -
取8字节数据赋值crypted32数据 -
plain32 = crypted32 ^ c32_prev(前8字节加密后数据)
解密函数逻辑如下:
-
取前8字节数据,赋值给crypted32数据和c32_prev数据 -
调用qq_decipher函数对crypted32数据进行解密,解密获得p32_prev数据 -
「p32_prev数据即为第一段解密后数据载荷,用于计算后续载荷长度」 -
循环解密 -
plain32 = p32_prev(解密后数据) ^ c32_prev(前8字节加密数据) -
「plain32数据即为解密后数据载荷」 -
将crypted32数据赋值给c32_prev数据(前8字节加密数据) -
取8字节数据赋值给crypted32数据 -
p32_prev = p32_prev(前8字节解密数据) ^ crypted32(8字节数据) -
调用qq_decipher函数对p32_prev数据进行解密,解密获得p32_prev数据
实际解密案例如下:
#会话流数据
30780000009ef890d85707490248f9991ff1b21feb2ccaa70873b370b846229c9da39ca864786d75acb0d95ec443e4cace5cce58ac0371fe9eb2911303d1dfddd5f8da2fece921ab5dd79d4375ad8dd71ae45170799c9374c99be377b804e2403f75aad7e1e5d1eab21c150debe0b7f2cda39923684324ec9f0526532c
30 #固定字节
78000000 #后续载荷数据长度
9ef890d857074902
48f9991ff1b21feb
2ccaa70873b370b8
46229c9da39ca864
786d75acb0d95ec4
43e4cace5cce58ac
0371fe9eb2911303
d1dfddd5f8da2fec
e921ab5dd79d4375
ad8dd71ae4517079
9c9374c99be377b8
04e2403f75aad7e1
e5d1eab21c150deb
e0b7f2cda3992368
4324ec9f0526532c #加密数据
#******解密前8字节
9ef890d857074902 #crypted32
#qq_decipher函数解密
66C6C6C6C6C6C669 #解密后数据
#******解密8字节
48f9991ff1b21feb
^ 66C6C6C6C6C6C669 #p32_prev(前8字节解密数据)
2E3F5FD93774D982
#qq_decipher函数解密
EDF990D857077102 #p32_prev
^ 9ef890d857074902 #c32_prev(前8字节加密数据)
7301000000003800 #解密后数据(0x73为随机数)
#******解密8字节
2ccaa70873b370b8
^ EDF990D857077102
C13337D024B401BA
#qq_decipher函数解密
48F9BA1FF1B25382
^ 48f9991ff1b21feb
0000230000004C69 #解密后数据
相关加密代码截图如下:
相关解密代码截图如下:
decd6b94792a22119e1b5a1ed99e8961样本内置密钥截图如下:
8138f1af1dc51cde924aa2360f12d650样本内置密钥截图如下:
模拟构建解密程序
为实现批量化通信数据解密,笔者尝试使用golang语言构建了一款通信数据解密程序,可对TCP通信、UDP通信数据进行有效解密。
TCP通信解密效果
运行decd6b94792a22119e1b5a1ed99e8961样本后,decd6b94792a22119e1b5a1ed99e8961样本将持续发送心跳通信数据包,从通信会话中提取心跳通信数据包进行解密,发现可成功解密,解密效果如下:
UDP通信解密效果
基于any.run沙箱平台捕获的8138f1af1dc51cde924aa2360f12d650样本的通信数据包进行分析,发现此样本使用UDP协议通信生成的通信数据包与decd6b94792a22119e1b5a1ed99e8961样本使用TCP协议通信生成的通信数据包的数据包结构略有不同。
基于逆向分析对其进行对比,发现使用UDP协议进行通信时,样本还将对加密后的通信数据进行二次封装,相关代码截图如下:
进一步分析,发现可从UDP会话中直接提取加密后的通信数据,相关截图如下:
尝试使用解密程序对其进行解密,发现依然可成功解密,解密效果如下:
代码实现
代码结构:
-
main.go
package main
import (
"awesomeProject5/common"
"encoding/hex"
"fmt"
)
func main() {
//decd6b94792a22119e1b5a1ed99e8961 tcp
key, _ := hex.DecodeString("A101A8EAC010FB120671F318ACA061AF")
//8138f1af1dc51cde924aa2360f12d650 udp
//key, _ := hex.DecodeString("A1A118AA10F0FA160671B308AAAF31A1")
fmt.Println("密钥信息:", hex.EncodeToString(key))
plain, _ := hex.DecodeString("30780000009ef890d85707490248f9991ff1b21feb2ccaa70873b370b846229c9da39ca864786d75acb0d95ec443e4cace5cce58ac0371fe9eb2911303d1dfddd5f8da2fece921ab5dd79d4375ad8dd71ae45170799c9374c99be377b804e2403f75aad7e1e5d1eab21c150debe0b7f2cda39923684324ec9f0526532c")
fmt.Println("原始二进制数据:", hex.EncodeToString(plain))
if plain[0] == 0x30 {
dec_data_len := common.BytesToInt_Little(plain[1:5])
if dec_data_len == len(plain[5:]) {
plain_uint32 := common.BytesToUint32Slice(plain[5:])
key_uint32 := common.BytesToUint32Slice(key)
dec_data := common.Decrypt_out(plain_uint32, len(plain_uint32)*4, key_uint32)
fmt.Println("解密后二进制数据:", hex.EncodeToString(dec_data))
fmt.Println("解密后字符串:", string(dec_data))
}
}
}
-
common.go
package common
import (
"bytes"
"encoding/binary"
"fmt"
)
func qq_decipher(input []uint32, key []uint32) (result uint32, output []uint32) {
v7 := uint32(0xE3779B90)
v11 := input[0]
v12 := input[1]
v13 := key[0]
v14 := key[1]
v15 := key[2]
v16 := key[3]
for {
if v7 <= 0 {
break
}
v12 -= (v11 + v7) ^ (v16 + (v11 >> 5)) ^ (v15 + 16*v11)
result = v12 + v7
v11 -= result ^ (v14 + (v12 >> 5)) ^ (v13 + 16*v12)
v7 += 0x61C88647
}
output = append(output, v11)
output = append(output, v12)
return
}
func Decrypt_out(enc_data []uint32, enc_data_len int, key []uint32) (output []byte) {
crypted32 := []uint32{0x00, 0x00}
c32_prev := []uint32{0x00, 0x00}
plain32 := []uint32{0x00, 0x00}
p32_prev := []uint32{0x00, 0x00}
pos := 0
crypted32[0] = enc_data[pos]
crypted32[1] = enc_data[pos+1]
pos += 2
c32_prev[0] = crypted32[0]
c32_prev[1] = crypted32[1]
_, p32_prev = qq_decipher(crypted32, key)
output = append(output, uint32SliceToBytes(p32_prev)...)
padding := 2 + output[0]&0x7
if padding < 2 {
padding += 8
}
plain_len := enc_data_len - 1 - int(padding) - 7
if plain_len < 0 {
return
}
count64 := enc_data_len / 8
for {
count64 = count64 - 1
if count64 <= 0 {
break
}
c32_prev[0] = crypted32[0]
c32_prev[1] = crypted32[1]
crypted32[0] = enc_data[pos]
crypted32[1] = enc_data[pos+1]
pos += 2
p32_prev[0] = p32_prev[0] ^ crypted32[0]
p32_prev[1] = p32_prev[1] ^ crypted32[1]
_, p32_prev = qq_decipher(p32_prev, key)
plain32[0] = p32_prev[0] ^ c32_prev[0]
plain32[1] = p32_prev[1] ^ c32_prev[1]
if count64 == (enc_data_len/8)-1 {
output = append(output, uint32SliceToBytes(plain32)[1:]...)
} else {
output = append(output, uint32SliceToBytes(plain32)...)
}
}
return
}
func BytesToInt_Little(bys []byte) int {
bytebuff := bytes.NewBuffer(bys)
var data int32
binary.Read(bytebuff, binary.LittleEndian, &data)
return int(data)
}
func BytesToUint32Slice(data []byte) []uint32 {
if len(data)%4 != 0 {
fmt.Println("error")
}
// 计算要返回的 []uint32 的长度
numUint32 := len(data) / 4
uint32Slice := make([]uint32, numUint32)
// 逐个将 []byte 转换为 []uint32
for i := 0; i < numUint32; i++ {
// 使用 binary.LittleEndian.Uint32 将 []byte 解释为 uint32
uint32Value := binary.BigEndian.Uint32(data[i*4 : (i+1)*4])
uint32Slice[i] = uint32Value
}
return uint32Slice
}
func uint32SliceToBytes(data []uint32) []byte {
// 计算总共需要的字节数
totalBytes := len(data) * 4
// 创建一个足够容纳所有数据的 []byte 切片
byteSlice := make([]byte, totalBytes)
// 将 []uint32 逐个转换为字节序列
for i := 0; i < len(data); i++ {
// 使用 binary.LittleEndian.PutUint32 将 uint32 转换为字节序列
binary.BigEndian.PutUint32(byteSlice[i*4:(i+1)*4], data[i])
}
return byteSlice
}
原文始发于微信公众号(T0daySeeker):以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试