利用GPU暴力破解Akira勒索软件(Linux/ESXI变种2024)加密文件

本文详细介绍了如何通过逆向工程分析Akira勒索软件的加密机制,利用GPU并行计算暴力破解时间戳种子,最终实现不支付赎金解密文件的全过程和技术细节。

解密Akira勒索软件(Linux/ESXI变种2024)加密文件:使用多块GPU的实战指南

我最近帮助一家公司从不支付赎金的情况下恢复了被Akira勒索软件加密的数据。本文将分享我的实现方法,并提供完整的源代码。

代码仓库:https://github.com/yohanes/akira-bruteforce

需要说明的是,多年来有多个勒索软件变种被命名为Akira,目前有几个版本仍在传播。我遇到的这个变种从2023年底活跃至今(该公司今年被入侵)。

2023年中期之前有一个版本存在漏洞,允许Avast创建解密器。但该漏洞公开后,攻击者更新了加密方式。预计在我发布本文后,他们会再次更改加密算法。

解密目标:Akira勒索软件

你可以在以下URL找到各种Akira恶意软件样本的哈希值: https://github.com/rivitna/Malware/blob/main/Akira/Akira_samples.txt

与我客户案例匹配的样本是: bcae978c17bcddc0bf6419ae978e3471197801c36f73cff2fc88cecbe3d88d1a 该样本被列为Linux V3版本。可以在virus.exchange上找到该样本(只需粘贴哈希值搜索即可)。

注意:赎金消息和公私钥会有所不同。

技术挑战与解决方案

我通常拒绝协助处理勒索软件案例。但当朋友向我展示这个特定案例时,初步检查让我认为这是可解决的。

从初步分析中,我观察到以下关键点:

  • 勒索软件使用当前时间(纳秒级)作为种子
  • 在我的Linux机器上,文件修改时间具有纳秒级分辨率
  • 他们提供了部分日志(shell.log)的截图,显示了勒索软件执行的时间,具有毫秒级分辨率

基于此,我最初的想法是:“这应该很容易——只需通过文件时间戳进行暴力破解。能有多难?”

但实际情况比预期复杂得多:

  1. 恶意软件不依赖单个时间点,而是使用四个时间点,每个都具有纳秒级分辨率
  2. 前两个和后两个时间点相关,因此不能简单地逐个暴力破解时间
  3. 密钥生成复杂,每个时间戳需要1,500轮SHA-256计算
  4. 每个文件最终都有唯一的密钥
  5. VMware VMFS文件系统仅记录秒级精度的文件修改时间
  6. 并非所有ESXi主机在日志文件中都有毫秒级分辨率,有些只有秒级精度
  7. 恶意软件在执行期间使用多线程
  8. 文件修改时间反映的是文件关闭时的时间,而不是开始写入时的时间

逆向工程分析

代码是用C++编写的,虽然 notoriously 难以阅读,但幸运的是没有进行混淆。二进制文件是静态链接的(分析起来稍难),但所有字符串都是明文。错误消息表明使用了Nettle库,这大大简化了代码理解。

随机数生成代码如下(实际代码在二进制文件的0x455f40处):

1
2
3
4
5
6
7
8
9
void generate_random(char *buffer, int size){
    uint64_t t = get_current_time_nanosecond();
    char seed[32]; //在实际代码中,使用C++代码将int转换为字符串
    snprintf(seed, sizeof(seed), "%lld", t);
    struct yarrow256_ctx ctx;
    yarrow256_init(&ctx, 0, NULL);
    yarrow256_seed(&ctx, strlen(seed), seed);
    yarrow256_random(&ctx, size, buffer);
}

随机生成器在yarrow256.c中实现。以下是相关代码,删除了不必要的部分:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void yarrow256_seed(struct yarrow256_ctx *ctx,
                   size_t length,
                   const uint8_t *seed_file){
  sha256_update(&ctx->pools[YARROW_FAST], length, seed_file);
  yarrow256_fast_reseed(ctx);
}

void yarrow256_fast_reseed(struct yarrow256_ctx *ctx){
  uint8_t digest[SHA256_DIGEST_SIZE];
  unsigned i;
  
  sha256_digest(&ctx->pools[YARROW_FAST], sizeof(digest), digest);
  /* Iterate */
  yarrow_iterate(digest);
  aes256_set_encrypt_key(&ctx->key, digest);
  /* Derive new counter value */
  memset(ctx->counter, 0, sizeof(ctx->counter));
  aes256_encrypt(&ctx->key, sizeof(ctx->counter), ctx->counter, ctx->counter);
}

#define YARROW_RESEED_ITERATIONS 1500

static void yarrow_iterate(uint8_t *digest){
  uint8_t v0[SHA256_DIGEST_SIZE];
  unsigned i;
  
  memcpy(v0, digest, SHA256_DIGEST_SIZE);
  
  for (i = 0; ++i < YARROW_RESEED_ITERATIONS; ){
      uint8_t count[4];
      struct sha256_ctx hash;
      
      sha256_init(&hash);
      WRITE_UINT32(count, i);
      sha256_update(&hash, SHA256_DIGEST_SIZE, digest);
      sha256_update(&hash, sizeof(v0), v0);
      sha256_update(&hash, sizeof(count), count);
      sha256_digest(&hash, SHA256_DIGEST_SIZE, digest);
  }
}

种子和加密机制

勒索软件调用随机生成器四次:

1
2
3
4
generate_random(chacha8_key, 32);
generate_random(chacha8_nonce, 16);
generate_random(kcipher2_key, 16);
generate_random(kcipher2_key, 16);

每次generate_random调用都使用当前纳秒时间戳作为种子。因此,需要识别四个唯一的时间戳。勒索软件为每个文件生成不同的密钥。

这些密钥随后使用RSA-4096加密并以PKCS#11填充方式保存在文件末尾作为尾部。

文件被分成N个块,每个块的一定百分比被加密。这个百分比由勒索软件的-n参数定义。对于每个块:

  • 前0xFFFF字节使用KCipher2加密
  • 剩余字节使用Chacha8加密

暴力破解可行性

方法如下:

  1. 生成两个时间戳(t3和t4)
  2. 将这些时间戳转换为种子并生成随机字节
  3. 使用这些字节作为KCipher2密钥和IV
  4. 加密已知明文并将结果与加密文件中的已知密文进行比较

制定计划:

  1. 检查可行性:确定暴力破解是否足够快以实用
  2. 识别明文:暴力破解需要已知明文
  3. 估计种子初始化时间:需要知道加密种子初始化的时间,至少具有秒级精度

最简单(但低效)的方法是尝试所有可能的时间戳对,其中T4 > T3。可能的对数为:N×(N−1)/2 当N=10亿时,结果是500万亿个可能的对。

我们需要优化这个过程。首先需要将所有纳秒时间转换为随机值:

在我的迷你PC CPU上,估计处理速度为每秒100,000个时间戳到随机字节的计算(利用所有核心)。 这意味着将所有时间戳转换为种子值需要约10,000秒(不到3小时)。 转换后,这些值可以保存以供重复使用。

后来,我使用GPU优化了过程,将转换时间从3小时减少到不到6分钟。

VMware文件类型分析

对于每个文件,我们需要一个明文样本:文件的前8字节用于KCipher2(偏移0),另外8字节从偏移65,535开始(仅适用于大文件)。

Flat-VMDK 这是一个原始磁盘文件。如果幸运的话,这可能是我需要恢复的唯一文件。但如果创建了快照(如本例中的客户案例),新数据将写入sesparse文件。

要获取flat VMDK的前8字节,需要安装原始VM使用的相同操作系统。不同OS版本使用多种引导加载器变体。

要确定使用了哪个OS,请检查相应的VMX文件。它应该包含部分可读的明文,允许你检查"guestOS"配置。可能会找到类似:guestOS=“ubuntu"的内容。

对于位置65,535处的字节(Chacha8的明文),几乎总是保证为零,因为分区通常从较晚的扇区开始。

Sesparse 如果为VM创建快照,每个快照都会有一个SESPARSE文件。我们可以从QEMU源代码中查看文件格式。

文件头是0x00000000cafebabe,在位置65,535处应该是0x0(至少在我的分析中观察到)。

加密时间戳确定

现在我们知道暴力破解是可行的,并且我们既有明文又有密文,下一步是确定每个文件的加密时间(因为每个文件将有不同的密钥)。

ESXI日志 用于运行恶意软件的命令记录在shell.log文件中(包括n的设置,它定义了应该加密多少文件)。

一些ESXi主机在其日志中提供毫秒级分辨率,而其他主机仅提供秒级精度。此日志为我们提供了恶意软件启动的初始时间戳。

文件系统时间戳和修改时间 不幸的是,ESXi文件系统不支持纳秒级精度。

另一个挑战是文件修改时间仅在文件关闭时记录。这意味着记录的时间戳可能不能准确反映加密过程开始的时刻,而是结束的时刻。

多线程加密

恶意软件使用多线程,每个文件在新线程中处理,工作线程池受CPU核心数量限制。这既有优点也有缺点。

如果恶意软件针对单个目录且文件数量少于CPU核心数,过程很简单——每个文件的时间戳将非常接近。在ESXi机器上,通常有大量核心的CPU(本例中服务器有64个核心)。

创建暴力破解器

计划看起来很可靠,所以下一步是实施代码。我需要确认加密过程是否与恶意软件完全相同。

为了测试这一点,我修补了恶意软件代码,使gettime函数返回常量值0,确保测试期间结果可预测且一致。

KCipher2 我专注于KCipher2,因为并非所有文件都使用Chacha8密钥,特别是小文件。虽然KCipher2是标准加密算法,但并不广为人知,我找不到优化的实现。

在实验过程中,我注意到我的结果与网上可用的标准KCipher2实现不匹配。结果发现恶意软件在初始化向量和加密过程中包含了一个轻微的修改,特别是涉及字节序交换。

CUDA优化 我不是CUDA编程专家。大约10年前,我 briefly 尝试过,但当时找不到实际用例。

为了加速开发,我请ChatGPT(o1)将代码移植到CUDA。代码编译成功但产生了不正确的结果。结果发现ChatGPT稍微修改了常量表中的数字。手动校正这些值后,代码开始工作。

虽然实现运行了,但我怀疑它不是最优的,但无法从ChatGPT(o1)获得进一步的优化建议。那时我有两个选择:花更多时间优化代码,或者继续使用预测的偏移范围并在过程中改进代码。我选择立即开始测试并根据需要优化。不幸的是,这种方法被证明是浪费金钱,因为没有产生任何成功的结果。

在项目开始时,我只有两个RTX 3060 GPU。一个专用于我的Windows机器,所以我只能在迷你PC上使用一个GPU(通过Oculink外部连接)。为了提高性能,我决定购买RTX 3090。在泰国,与4090或更高型号相比,价格仍然合理。

我通过从内存读取密钥和IV、加密零块并将结果写回内存来测试实现。性能令人失望,仅实现约6000万次加密每秒。以这个速度,整个过程将需要约10年,显然太慢无法实际恢复。

手动优化 我通过删除不必要的代码进行了一些手动优化以提高性能:

  • 只需要第一个块进行暴力破解,因此无需处理额外的块
  • 代码简化为仅加密零块,减少不必要的处理
  • 由于只需要结果的前8字节,忽略其余输出以最小化计算

共享内存 在研究CUDA的AES优化后,我发现使用共享内存显著提高了性能,与ChatGPT的建议相反。令人惊讶的是,将常量内存数据复制到共享内存所涉及的额外步骤在开销方面可以忽略不计,但导致代码运行速度快了几倍。

避免内存写入 最初,我在GPU上执行加密并在主机(CPU)上进行匹配。然而,这种方法很慢,即使并行执行:

  1. 在GPU上生成加密
  2. 将结果复制到CPU
  3. 在新线程中执行匹配并向GPU提交下一批工作

我发现完全避免写入内存要快得多。相反,匹配过程直接在GPU上处理,除非找到匹配,否则不会将数据写入内存。这种方法显著减少了处理时间并提高了效率。

最终速度

我相信GPU专家仍然可以找到进一步优化我的代码的方法。目前,我在RTX 3090上为KCipher2实现了约15亿次加密每秒。

对于测试10亿个值和单个偏移,需要约0.7秒,包括检查匹配的时间(每批最多32个匹配)。 测试200万个偏移将需要在单个GPU上大约16天,或使用16个GPU仅需1天。

我还使用Runpod进行了测试,RTX 4090结果是最佳选择。虽然比3090贵约60%,但也快2.3倍。

使用4090,相同过程在单个GPU上需要约7天。 使用16个GPU,过程可以在刚刚超过10小时内完成。

运行暴力破解

从成本角度来看,RTX 4090是此任务的绝佳选择,原因如下:

  • 不需要大内存
  • 不需要浮点运算
  • RTX 4090提供大量CUDA核心,增强处理速度
  • RTX 4090的租赁价格相对于其他高端GPU较低

如果4090不可用,3090考虑到其价格性能比也是一个不错的选择。

最初,我的客户考虑使用Google Cloud Platform(GCP)机器并寻求月租折扣。然而,这个选项被证明极其昂贵(花费数万美元)。

经过一些研究,我找到了更具成本效益的替代方案:Runpod和Vast.ai。

Runpod 要暴力破解1秒(10亿纳秒),偏移范围为200万,需要7天。RTX 4090的成本(在撰写本文时)为0.69美元/小时。暴力破解单秒将花费约116美元。租赁16个GPU将在约10小时内完成工作,成本相同,但速度更快。

使用450万范围(我们需要的范围)进行暴力破解成本为261美元。根据加密文件的数量,可能需要暴力破解10秒或更多。如果你有很多文件要恢复,周租或月租会更便宜。

注意:这些成本假设一切完美执行。任何错误或需要重复过程都可能显著增加成本。总共,包括我所有的实验和测试,我花费了约1,200美元。

Vast.ai 与runpod不同,使用vast.ai时,你是从vast.ai代理的某个随机人那里租用机器。进行暴力破解时,不会发送敏感数据,因此隐私不应成为问题。

使用vast AI,暴力破解成本可以减少一半,但这取决于你获得机器的运气。我测试的前几台机器不起作用(等待约10分钟后网络超时)。我还遇到了从docker.io拉取docker镜像的问题(我必须从另一个docker存储库选择另一个模板)。

剩余工作

现在我找到了t3和t4的值,我可以尝试找到t1和t2的值。t1的值必须小于t3,时间偏移小于1000万纳秒。这可以在几分钟内使用单个GPU快速找到。

块分割算法 以下是用于将文件分割成部分的算法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void compute_blocks(uint64_t filesize, 
    uint8_t percent,
    uint64_t *enc_block_size,
    uint64_t *part_size,
    uint64_t *encrypted_parts){
    int parts = 3;
    if ( percent > 49u )
        parts = 5;
    uint64_t enc_size = filesize * (uint64_t)percent / 100;
    *enc_block_size = enc_size / parts;
    *encrypted_parts = parts - 1;
    *part_size = (filesize - *enc_block_size * (*encrypted_parts)) / parts;
}

恢复步骤

要在不支付的情况下恢复文件,并不像运行通用解密器那样简单。你需要:

  1. 获取文件的时间戳
  2. 获取文件的密文和明文
  3. 租用GPU

关于代码的说明

老实说,我最初编写此代码是为了一次性使用,专门针对这个特定客户。共享的代码充满了实验逻辑、快速黑客行为,并且缺乏适当的测试。

除了删除一些客户特定的测试用例和注释外,我没有动力进一步清理它。它对于预期目的是功能性的。

我提供的软件仅包括主要的暴力破解和解密组件,旨在一旦你拥有必要的时间戳时使用。

我没有专用系统来管理多个GPU。相反,我依赖基本的shell脚本和一个自定义脚本,当找到匹配时发送Telegram消息。代码"对我来说足够好”,只是"对我有效"。

本质上,你需要一个能干的系统管理员,他理解过程并知道如何有效管理和故障排除系统。

构建代码

获取时间戳

我希望你没有触碰文件,因为如果时间戳未知,所有恢复希望都将消失。使用stat filename获取修改时间戳。使用find /vmfs/volumes -exec stat {} ; > /tmp/stats.txt获取所有内容的时间戳。

文件shell.log可以帮助确定要使用的最小时间戳。

获取密文

获取密文,如上所述:

  • 对于flat-vmdk,需要从使用的确切OS中提取(包括确切的安装方法,例如:使用BIOS/UEFI)
  • 对于sesparse文件,使用头0x00000000cafebabe
  • 对于其他文件,请参阅我上面写的内容

测量服务器速度

你总是可以使用1.5-500万的偏移范围,但如果你的硬件太快或太慢,这可能不是正确的范围。你可以通过查看我的github存储库中的timing-patch-1文件夹和timing-patch-2文件夹来测量这一点。

第一个仅通过直接调用函数来测量时间范围。第二个用于加密目录,但经过修补,以便在时间戳用作种子时将确切时间写入/tmp/log.bin。

分工

基于密文/明文和时间戳创建配置文件。你可以手动创建/分割,或使用脚本生成。我的代码不做任何错误检查,确保时间戳是纳秒格式,确保所有明文和密文值正确。

租用GPU

如果你想要非常快速和简单的设置,请使用runpod或其他服务。如果你想便宜,请使用vast.ai,或在你自己的硬件上运行(约1,000美元购买一个RTX 3090,之后可以转售

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计