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

文章首发地址:
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
}


原文始发于微信公众号(T0daySeeker):以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试

版权声明:admin 发表于 2024年5月6日 上午9:59。
转载请注明:以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试 | CTF导航

相关文章