solar应急响应 发表于 2024-12-19 00:21

【病毒分析】全网首发!袭扰国内top1家族Mallox家族破解思路及技术分享

本帖最后由 solar应急响应 于 2024-12-19 00:22 编辑

*引言:根据360安全卫士官方论坛的最新统计数据,本月国内勒索软件受害者设备所中病毒家族中Mallox家族占比22.38%居首位。https://weishi.360.cn/dnzs/12640.html*




# 1. 背景

## 1.1 前情提要

Mallox家族的加密器是solar团队处理的众多案例中,分析次数最多的家族之一。本次分析的对象是2022年10月时间段的mallox后缀加密器。通过对样本的深入分析,我们捕获到了一些关键线索,这些线索为破解提供了重要思路。**在下文中,我们将详细分享这些发现和分析过程,基于本篇思路制作的恢复工具会在下一篇工具分享中进行发布,敬请期待!**

## 1.2 家族介绍

关于Mallox家族信息可参考本篇文章[【病毒分析】Mallox勒索家族新版本:加密算法全面解析](https://bbs.kanxue.com/thread-284711.html),详情内容部分可移步solar官网处介绍https://www.solarsecurity.cn/family?id=3


## 1.3 支持的文件类型

基于本篇文章提到的破解思路,针对Mallox家族的不同版本后缀,可以通过逆向分析获取相关密钥和算法进行恢复:

| mallox版本后缀 | 时间段                | HASH                                                         |
| :------------- | :-------------------- | :----------------------------------------------------------- |
| .mallox      | 2022年10月到2023年3月 | 1f793f973fd906f9736aa483c613b82d5d2d7b0e270c5c903704f9665d9e1185 |
| .xollam      | 2023年1月             | 10f96f64659415e46c3f2f823bdb855aab42d0bfced811c9a3b72aea5f22d880 |
| .bitenc      | 2023年1月             | a340ef5adb00a2bf1a0735600491ca98ac8045b57db892dedc27575a53b25056 |
| .malox         | 2023年4月到2023年7月| 0427a9f68d2385f7d5ba9e9c8e5c7f1b6e829868ef0a8bc89b2f6dae2f2020c4 |
| .maloxx      | 2023年6月             | 03596723ecb5be777b1e3ec5deb414dc78155a30a466b2b25a4574f4db9a568a |
| .malloxx       | 2023年8月             | 4e4592a5dec8a0fc12bbd6f955cd01936aa80d7b8d54db5258140a8a1c460c66 |
| .mallab      | 2023年9月到2023年10月 | d1969e0ec464e8b09e54ea35fbe181ea391ed3674e0b35b0f6ee92821478e76c |
| .ma1x0         | 2024年2月             | 003ea0712cd31a75f5dfb6a23d2d12ea1e615f7601574ade09b6b61e3b7ed2b0 |

# 2. 逆向分析

## 2.1 前置知识

在分析如何破解之前,需要先提前了解一点mallox东西。

mallox家族最为显眼的就是这部分,采用**CryptGenRandom函数**的方式获取随机数种子,那怕最近的一些变种版本,都是沿用此类函数来进行密钥生成。


在获取到了4个字节的随机数之后,就会将该随机数当做是梅森旋转算法的随机数种子


接着会初始化一个**public_key**,然后与后面的一组55个字节大小的内容(C盘序列号+CPUID+固定内容+计算机名称)的sha256进行一个**cure25519**算法的密钥生成。这里命名**public_key**和55字节大小的数据的**sha256**产生的**share_key**为**user_private_key**



这里命名**public_key**和55字节大小的数据的**sha256**产生的**share_key**为**user_private_key,接着再利用这个生产的私钥产生一个对应的公钥,然后再用这个私钥和黑客的hack_public_key生成一个共享密钥share_key。**



这里具体可以参考Babyk家族分析中的算法分析那部分的内容[【病毒分析】BabyK加密器分析-Windows篇](https://bbs.kanxue.com/thread-281950.htm)。

OK,到了目前为止,我们大致上了解了整体的流程如下:

(注意:这里我并未将梅森算法和下面的流程连到一起,因为他们其实并不相干)


接下来,我们去掉mallox章节的算法分析部分的内容,直接来看看,加密文件所使用的key到底是怎么生成的?

(注意:这里如果看不懂,请看上一章节mallox算法分析部分的内容[【病毒分析】Mallox勒索家族新版本:加密算法全面解析](https://bbs.kanxue.com/thread-284711.htm))

首先,文件的加密函数在StarAddress函数中,并且梅森旋转算法中,生成随机数的部分也在这里,其中这个v9就是我们利用上面的梅森算法生成的256组每组4字节大小随机数,至于这个sub_E43C34、sub_E4142B和sub_E434函数,在这里就是对随机数进行变形的,所以就先不提,继续向下看。


进入到**enc_file函数**,来看文件加密的具体过程。首先会先获取一下后续**AES加密算法**的**IV**,获取方法就是从传入的256组**随机数**中,取后4组数据,然后一组数据是4个字节组成,一共16个字节。


然后继续接着上面的v7的位置从后往前再取10组数据,一共40个字节,当做**ChaCha20**加密算法的**Key**和**Nonce**。


接着就是开始初始化一下**ChaCha20**的**Key**和**Nonce**还有**AES**的**IV**。


调用**AES-128-CTR加密算法**,来对40个字节的**ChaCha20**的**Key**和**Nonce**数据进行加密。


到此,我们再做一次总结:


但是这里有个问题,AES的KEY去哪里了???

其实AES的KEY在**AES_init**函数中才会被用到,这里看一下具体内容

在**aes_init函数**的初始化部分可以看到key的部分,主要取了上面密钥生成部分,最后**share_key**的**sha256**的前16个字节的内容。


好了,我们将此过程补充完整:


   最后,我们将要用Cha20Cha20的密钥去加密文件了。当然了,遵循传统勒索软件的加密规则“既要加密的牢,还要加密的快”,依然会采用根据文件大小来进行加密部分的计算:


1 文件大小 < 0x2710 字节,文件不加密

2 文件大小>=0x2710字节并且<= 0x19000字节,文件全部加密



3 文件大小>0x19000字节采用条状加密。



这里也补充一下**条状加密的部分的规则**,因为mallox算法分析那篇确实漏掉了。

首先会将待加密的文件分成10个块,每块大小这里我称之为`block_size`。之后,循环十次,每次只加密每个块的前`enc_size`大小的内容,如图所示:


了解了加密的结构以后,这里就将这个enc_size和block_size的计算公式放出来:

```C
enc_size = 15 * (文件大小 / 10 / 100) & 0xfffff000

block_size = 文件大小 / 10
```

但是这里还会对enc_size的大小做处理,假如,enc_size的大小,如果小于0x1000(4096)字节大小时,则会将其强行扩充到0x1000(4096大小)。



以上就是全部的条状加密规则了。

加密完毕文件以后,将会在文件末尾写入100个字节大小的数据,其中包括了如下内容:




OK,到目前为止我们总结一下整个过程:



## 2.2 破解思路分析

以上我们分析了从密钥生成到文件加密的全部过程,包括了每个数据的产生和由来。所以分析可以知道,破解的思路大致上只有两种:

1 从随机数上下手,获取随机数种子

2 从55个固定的字节下手

这里我们先采用第二种方式,针对55个固定字符进行分析。


这55字节部分内容主要包括:


其中固定值部分是针对于每个mallox的变种后缀样本,定制生成的,所以会不一样。

这里我们来模拟一下这个过程:

```Go
package main

/*
#cgo CFLAGS: -std=gnu11

#include <stdint.h>

// 使用GNU内联汇编调用CPUID
// 输入: op (EAX输入值)
// 输出: eax, ebx, ecx, edx
void cpuid(uint32_t op, uint32_t *eax, uint32_t *ebx, uint32_t *ecx, uint32_t *edx) {
    uint32_t a, b, c, d;
#if defined(__x86_64__) || defined(__i386__)
    __asm__ volatile (
      "cpuid"
      : "=a"(a), "=b"(b), "=c"(c), "=d"(d)
      : "a"(op)
    );
#else
    // 非x86架构下这里可能需要不同实现或直接报错
    a = b = c = d = 0;
#endif
    *eax = a;
    *ebx = b;
    *ecx = c;
    *edx = d;
}
*/
import "C"
import (
    "encoding/binary"
    "fmt"
    "os"
    "syscall"
    "unsafe"
)

func uint32ToBytes(num uint32, bigEndian bool) []byte {
    // 创建一个字节切片
    byteArray := make([]byte, 4)

    if bigEndian {
       // 使用大端字节序
       binary.BigEndian.PutUint32(byteArray, num)
    } else {
       // 使用小端字节序
       binary.LittleEndian.PutUint32(byteArray, num)
    }

    return byteArray
}

// GetVolumeSerialNumber 返回指定盘符的卷序列号
func GetVolumeSerialNumber(drive string) (uint32, error) {
    kernel32 := syscall.NewLazyDLL("kernel32.dll")
    pGetVolumeInformationW := kernel32.NewProc("GetVolumeInformationW")

    var (
       volumeNameBuffer   = make([]uint16, syscall.MAX_PATH+1)
       volumeSerialNumber   uint32
       maxComponentLength   uint32
       fileSystemFlags      uint32
       fileSystemNameBuffer = make([]uint16, syscall.MAX_PATH+1)
    )

    drivePtr := syscall.StringToUTF16Ptr(drive)

    ret, _, err := pGetVolumeInformationW.Call(
       uintptr(unsafe.Pointer(drivePtr)),
       uintptr(unsafe.Pointer(&volumeNameBuffer)),
       uintptr(len(volumeNameBuffer)),
       uintptr(unsafe.Pointer(&volumeSerialNumber)),
       uintptr(unsafe.Pointer(&maxComponentLength)),
       uintptr(unsafe.Pointer(&fileSystemFlags)),
       uintptr(unsafe.Pointer(&fileSystemNameBuffer)),
       uintptr(len(fileSystemNameBuffer)),
    )

    if ret == 0 {
       return 0, err
    }

    return volumeSerialNumber, nil
}
func get_disk_serial(disk_path string) ([]byte, error) {
    serial, err := GetVolumeSerialNumber(disk_path)
    if err != nil {
       fmt.Println("获取C盘序列号失败:", err)
       return nil, err
    }
    return uint32ToBytes(serial, false), err
}
func get_cpuid() ([]byte, []byte, []byte, []byte) {
    var eax, ebx, ecx, edx C.uint32_t
    C.cpuid(0, &eax, &ebx, &ecx, &edx)
    return uint32ToBytes(uint32(eax), false), uint32ToBytes(uint32(ebx), false), uint32ToBytes(uint32(ecx), false), uint32ToBytes(uint32(edx), false)
}
func get_hostname() ([]byte, error) {
    hostname, err := os.Hostname()
    if err != nil {
       fmt.Println("获取计算机名称失败:", err)
       return nil, err
    }
    return []byte(hostname), err
}
func get_whole_password() []byte {
    password, err := get_disk_serial("C:\\")
    if err != nil {
       return nil
    }
    eax, ebx, ecx, edx := get_cpuid()
    hostname, err := get_hostname()
    if err != nil {
       return nil
    }
    password = append(password, eax[:]...)
    password = append(password, ebx[:]...)
    password = append(password, ecx[:]...)
    password = append(password, edx[:]...)
    //这里到时候可以写个判断,来根据不同的加密版本的固定值进行选择拼接
    password = append(password, uint32ToBytes(uint32(0x0FADACE8), false)...)
    password = append(password, uint32ToBytes(uint32(0x9AB979DD), false)...)
    password = append(password, uint32ToBytes(uint32(0x4EAFA9C2), false)...)
    password = append(password, uint32ToBytes(uint32(0x00), false)...)
    password = append(password, uint32ToBytes(uint32(0x00), false)...)
    password = append(password, hostname[:]...)
    return password
}

func main() {
    password := get_whole_password()
    fmt.Printf("55个固定字节:%x", password)
}
```

通过测试发现可以恢复这55个固定的字节,得出的数据与我们从勒索内存中所得的数据一样。但是因为要获取本机的用户名、CPU_ID和磁盘序列号,所以解密有一个前提就是只能在被加密的机器上运行才可以恢复。


接着就是利用这55个字节来和程序自带的Public_Key进行Cure25519的密钥交换,生成user_public_key。

先写一个Cure25519的密钥交换:

```Go
func get_key(private_key, public_key []byte) []byte {
    //// Curve25519: 计算共享密钥
    // 将公钥转换为 byte 类型
    var publicKey byte
    copy(publicKey[:], public_key)

    // 将私钥转换为 byte 类型
    var privateKey byte
    copy(privateKey[:], private_key[:])

    // Curve25519: 计算共享密钥
    var sharedKey byte
    curve25519.ScalarMult(&sharedKey, &privateKey, &publicKey)
    return sharedKey[:]
}
```

再来实现以下对应的生成:

```C
hash := sha256.Sum256(password)
fmt.Printf("55个固定字符的Sha256:%x\n", hash)
public_key1, err := hex.DecodeString("CD20FA7A55F384AAFF27C1456636BD6374329E217B8066811A4F204591B8C1FF")
if err != nil {
    fmt.Println("hex to bytes error!")
}
share_key := get_key(hash[:], public_key1)
fmt.Printf("User_Public_Key:%x\n", share_key)
```

接下来发现,已经能和勒索程序中的内容对应了。


之后就是一系列的重复操作,最终可以恢复到share_key和user_public_key,然后利用user_public_key与程序末尾的user_public_key进行校验,如果一样,说明share_key可以用来解密,如果不一样,则说明该勒索文件并不是在本机上进行的加密行为。



以上就是该破解的全部过程,后续就是正常的恢复算法等内容,比较简单,这里就不再继续了。

## 2.3 测试结果

### 2.3.1 恢复前



### 2.3.2 恢复后


## 视频

请看附件。

# 3. 病毒分析概览

mallox家族的加密过程主要通过生成随机数种子、利用梅森旋转算法和对特定数据(如C盘序列号、CPUID等)进行SHA-256哈希处理,形成私钥和公钥,最终生成共享密钥来加密文件。文件加密过程中,采用了AES和ChaCha20等加密算法,并根据文件大小决定加密方式(完整加密或分块加密)。破解思路主要集中在获取随机数种子和分析55个固定字节的数据。

solar应急响应 发表于 2024-12-23 17:57

MIAIONE 发表于 2024-12-23 12:20
但是如果黑客直接使用hacker_publickey加密用户端随机产生的aes256,加密后aes密钥被丢弃,只剩下加密后 ...

您好,再次重申,请仔细阅读文章,文章开头就介绍过了,关于Mallox家族信息可参考本篇文章【病毒分析】Mallox勒索家族新版本:加密算法全面解析,https://www.52pojie.cn/thread-1988804-1-1.html 这里面有提到相关内容,耐心理解下ECDH的密钥交换过程。您说的两个问题,1.这个勒索病毒的作者是疑似俄罗斯地区的黑客,这个问题你需要问他…… 2.推荐阅读 https://www.52pojie.cn/thread-1929419-1-1.html

solar应急响应 发表于 2024-12-22 19:39

MIAIONE 发表于 2024-12-19 08:48
有个问题,share_key是被hacker_public_key加密的,而share_key加密每个aes密码,加密完成后就会删除在内存 ...

您好,建议再仔细阅读下文章,Share_Key是由User_Public_Key生成,同时也需要再去了解一下ECDH Curve25519密钥交换。

Cihaoyifeng 发表于 2024-12-19 01:21

古德古德,感谢分享

chx1126 发表于 2024-12-19 02:52

大佬牛啊

ryan515 发表于 2024-12-19 02:53

技术大贴,慢慢研读{:1_893:}

zhqxl 发表于 2024-12-19 07:17

膜拜技术大佬!点赞~!

haaker 发表于 2024-12-19 07:57

感谢大佬热心分享技术, 以前在深圳,一个小工厂的一台电脑就中了,他们直接重装了系统。然后文件也解不开了。

xionghaoyun 发表于 2024-12-19 07:58

不明觉厉

yiluoen0502 发表于 2024-12-19 08:00

厉害{:1_921:}感谢分享

binyue1985 发表于 2024-12-19 08:25

对大神只有无尽的崇拜

yinyiniao 发表于 2024-12-19 08:29

大佬厉害了,慢慢学习
页: [1] 2 3 4 5
查看完整版本: 【病毒分析】全网首发!袭扰国内top1家族Mallox家族破解思路及技术分享