以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试 | xxx以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试 – xxx
菜单

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

五月 6, 2024 - 安全维基
文章首发地址:
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 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后门剖析及通信解密尝试

自启动

通过分析,发现DinodasRAT Linux后门运行后,将判断当前系统版本信息,若当前系统为Red Hat或ubuntu,则此后门将附加自身于/etc/rc.local或/etc/init.d/中,用以实现DinodasRAT Linux后门的开机自启动,相关代码截图如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

守护进程

通过分析,发现DinodasRAT Linux后门运行后,将调用daemon函数创建守护进程,然后其又将使用父进程PPID作为参数再次运行DinodasRAT后门程序,相关代码截图如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

实际运行效果如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

获取设备信息

通过分析,发现DinodasRAT Linux后门运行后,将尝试获取当前主机信息,并将基于主机硬件信息、当前时间等信息构造被控主机的唯一标识码,此唯一标识码后期将用于心跳通信,相关代码截图如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

硬编码外联地址

通过分析,发现DinodasRAT Linux后门的外联地址是通过硬编码的方式内置于样本文件中的,相关代码截图如下:

decd6b94792a22119e1b5a1ed99e8961样本的外联地址信息如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

8138f1af1dc51cde924aa2360f12d650样本的外联地址信息如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

多种通信方式

通过分析,发现DinodasRAT Linux后门支持TCP、UDP多种通信协议进行外联通信,相关代码截图如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

外联加密通信

通过分析,发现DinodasRAT Linux后门在进行外联通信时,将调用MackControlBuf函数对通信载荷进行加密或解密,相关截图如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

通信载荷加密、解密代码截图如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

远控功能

通过分析,发现DinodasRAT Linux后门支持24个远控功能指令,远控功能较全面,相关代码截图如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

远控功能梳理如下:

远控函数远控功能
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 Linux后门剖析及通信解密尝试

DinodasRAT通信数据包分析

由于decd6b94792a22119e1b5a1ed99e8961样本与8138f1af1dc51cde924aa2360f12d650样本分别采用的TCP、UDP通信方式,因此,我们就可基于以上两个样本获取DinodasRAT Linux后门的TCP、UDP通信上线数据包。

TCP通信数据包

尝试构建模拟环境,即为成功捕获decd6b94792a22119e1b5a1ed99e8961样本的心跳通信数据包,相关数据包截图如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

UDP通信数据包

通过网络调研,笔者发现,在any.run沙箱平台上,曾有人于2024年3月19日上传了8138f1af1dc51cde924aa2360f12d650样本,因此,any.run沙箱平台成功记录了当时8138f1af1dc51cde924aa2360f12d650样本的通信数据包,相关截图如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

相关数据包截图如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

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”项目代码截图如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

通信加解密原理

结合实际后门样本反编译代码及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  #解密后数据

相关加密代码截图如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

相关解密代码截图如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

decd6b94792a22119e1b5a1ed99e8961样本内置密钥截图如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

8138f1af1dc51cde924aa2360f12d650样本内置密钥截图如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

模拟构建解密程序

为实现批量化通信数据解密,笔者尝试使用golang语言构建了一款通信数据解密程序,可对TCP通信、UDP通信数据进行有效解密。

TCP通信解密效果

运行decd6b94792a22119e1b5a1ed99e8961样本后,decd6b94792a22119e1b5a1ed99e8961样本将持续发送心跳通信数据包,从通信会话中提取心跳通信数据包进行解密,发现可成功解密,解密效果如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

UDP通信解密效果

基于any.run沙箱平台捕获的8138f1af1dc51cde924aa2360f12d650样本的通信数据包进行分析,发现此样本使用UDP协议通信生成的通信数据包与decd6b94792a22119e1b5a1ed99e8961样本使用TCP协议通信生成的通信数据包的数据包结构略有不同。

基于逆向分析对其进行对比,发现使用UDP协议进行通信时,样本还将对加密后的通信数据进行二次封装,相关代码截图如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

进一步分析,发现可从UDP会话中直接提取加密后的通信数据,相关截图如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

尝试使用解密程序对其进行解密,发现依然可成功解密,解密效果如下:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

代码实现

代码结构:

以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

  • 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{0x000x00}
 c32_prev := []uint32{0x000x00}
 plain32 := []uint32{0x000x00}
 p32_prev := []uint32{0x000x00}

 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
}



Notice: Undefined variable: canUpdate in /var/www/html/wordpress/wp-content/plugins/wp-autopost-pro/wp-autopost-function.php on line 51