以中国为目标的DinodasRAT Linux后门剖析及通信解密尝试
T0daySeeker 发表于 四川 历史精选 2237浏览 · 2024-04-30 01:22

概述

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中找到了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数据即为第一段解密后数据载荷,用于计算后续载荷长度
  • 循环解密
    • 将crypted32数据赋值给c32_prev数据(前8字节加密数据)
    • 取8字节数据赋值给crypted32数据
    • p32_prev = p32_prev(前8字节解密数据) ^ crypted32(8字节数据)
    • 调用qq_decipher函数对p32_prev数据进行解密,解密获得p32_prev数据
      • plain32 = p32_prev(解密后数据) ^ c32_prev(前8字节加密数据)
      • plain32数据即为解密后数据载荷

实际解密案例如下:

#会话流数据
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
}
0 条评论
某人
表情
可输入 255

没有评论