重写编译二进制文件中的函数 - 利用McSema和LLVM实现自动化函数替换

本文介绍了Fennec工具的开发过程,该工具基于McSema和LLVM,能够自动替换编译二进制文件中的函数调用,特别适用于修复密码学漏洞如静态初始化向量问题。

重写编译二进制文件中的函数

作者:Aditi Gupta, 卡内基梅隆大学
日期:2019年9月2日
标签:密码学, 实习项目, mcsema

作为Trail of Bits的暑期实习生,我一直在开发Fennec工具,这是一个基于McSema(由Trail of Bits开发的二进制提升工具)构建的自动化替换编译二进制文件中函数调用的工具。

问题描述

假设你有一个编译后的二进制文件,但无法访问原始源代码。现在,想象你发现程序有问题或想要进行某些更改。你可以尝试直接在二进制文件中修复——例如,通过十六进制编辑器修补文件——但这很快就会变得繁琐。相反,能够编写一个C函数并将其替换进去将大大加快这一过程。

我花了整个夏天开发了一个工具,使你可以轻松做到这一点。知道要替换的函数名称后,你可以编写另一个想要使用的C函数,编译它,并将其输入Fennec,该工具将自动创建一个新的改进版二进制文件。

密码学示例

为了展示Fennec的功能,让我们看一个在现实世界中相当常见的密码学漏洞:在AES加密的CBC模式中使用静态初始化向量。在CBC加密的第一步中,明文块与初始化向量(IV)进行异或操作。IV是一个128位块(与块大小相同),在任何加密中仅使用一次,以防止密文重复。加密后,该密文充当下一个明文块的IV,并与该明文块进行异或操作。

当初始化向量在整个明文中保持恒定时,此过程可能变得不安全。在固定IV下,如果每条消息都以相同的明文块开始,它们都将对应相同的密文。换句话说,静态IV可以让攻击者将多个密文作为一个组进行分析,而不是作为单独的消息。以下是一个静态生成IV的示例。

1
2
3
unsigned char *generate_iv() {
  return (unsigned char *)"0123456789012345";
}

有时,开发人员会使用像OpenSSL这样的密码学库进行实际加密,但编写自己的函数来生成IV。这可能很危险,因为非随机IV可能使AES变得不安全。我构建Fennec来帮助修复此类问题——它检查IV生成是随机还是静态的,并在必要时用新的安全IV替换函数。

开发过程

最终目标是通过McSema将可执行二进制文件提升为LLVM位码,并结合一些LLVM操作来自动替换任何函数。在开始之前,我首先理解了我的密码学示例,并探索了修补二进制文件的不同方法作为背景知识。

我的第一步是完成几个Matasano Cryptopals挑战,以了解AES及其使用和破解方式。项目的这一阶段让我在C语言中编写了调用OpenSSL的加密和解密程序,以及一些攻击我实现的Python脚本。我的加密程序使用了静态IV生成函数,我希望以后能自动替换它。

整个夏天我一直在使用这些C二进制文件。然后,我开始研究二进制修补。我花了一些时间研究LD_PRELOAD和Witchcraft Compiler Collection,如果我的IV生成函数是动态链接到程序中的,这些方法会有效。然而,我的项目目标是替换二进制文件中的函数调用,而不仅仅是动态加载的函数。

我还不想用提升的位码使一切复杂化,所以我开始使用直接从源代码生成的干净位码。我想在此位码上运行LLVM传递,以更改部分程序的功能——即生成IV的部分。

我首先尝试在我的传递中直接更改函数的位码,但很快改为用C编写一个新函数,并让我的原始程序调用该函数。对旧函数的每次调用都将替换为对新函数的调用。

经过一些实验,我创建了一个LLVM传递,将所有对旧函数的调用替换为对新函数的调用。在转向提升的位码之前,我添加了代码以确保我仍然能够调用原始函数(如果需要)。在我的密码学示例中,这意味着能够检查原始函数是否生成静态IV,如果是,则用下面的代码替换它,而不是假设它不安全并无论如何都替换它。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 代表原始二进制文件中函数的存根函数
unsigned char *generate_iv_original() {
  unsigned char *result = (unsigned char *)"";
  // 此函数的内容无关紧要
  return result;
}

unsigned char *random_iv() {
  unsigned char *iv = malloc(sizeof(int) * 16);
  RAND_bytes(iv, 16);  // OpenSSL调用
  return iv;
}

unsigned char *replacement() {
  unsigned char *original = generate_iv_original();
  for (int i = 0; i < 10; i++) {
    unsigned char *iv = generate_iv_original();
    if (iv == original) { . // 如果IV是静态的
      return random_iv();
    }
  }
  return original;
}

当我的工具在干净位码上工作时,是时候开始研究提升的位码了。我通过提升和重新编译二进制文件并查看中间表示来熟悉McSema的工作原理。由于McSema改变了函数的调用方式,我需要额外努力使我的工具在提升的位码上以与干净位码相同的方式工作。我必须用McSema提升原始二进制文件和替换函数。还需要额外努力,因为非提升二进制文件中的替换函数不遵循McSema的调用约定,因此不能简单地交换。

通过McSema,函数名称和类型更加复杂,但我最终制定了一个有效的程序。与干净位码的工具一样,原始函数可以保留以供替换使用。

最后一步是将我的过程通用化,并将所有内容包装成一个其他人可以使用的命令行工具。因此,我在各种目标上进行了测试(包括剥离的二进制文件和动态加载的函数),添加了测试,并测试了我的安装过程。

函数替换传递

完整过程包括三个主要步骤:1)使用McSema将二进制文件提升为位码,2)使用LLVM传递在位码中执行函数替换,3)重新编译新的二进制文件。LLVM传递是该工具的核心,因为它实际替换函数。传递通过迭代程序中的每条指令并检查它是否是对要替换函数的调用来工作。

在以下代码中,检查每条指令是否调用了要替换的函数。

1
2
3
4
5
6
7
8
9
for (auto &B : F) {
  for (auto &I : B) {
    // 检查指令是否是对要替换函数的调用
    if (auto *op = dyn_cast<CallInst>(&I)) {
      auto function = op->getCalledFunction();
      if (function != NULL) {
        auto name = function->getName();
        if (name == OriginalFunction) {
...

然后,我们通过查找具有指定名称和与原始函数相同类型的新函数来找到替换函数。

1
2
3
4
5
6
Type *retType = function->getReturnType();
FunctionType *newFunctionType =
  FunctionType::get(retType, function->getFunctionType()->params(), false);

// 创建新函数
newFunction = (Function *)(F.getParent()->getOrInsertFunction(ReplacementFunction, newFunctionType));

下一步是将原始函数的参数传递给新调用。

1
2
3
4
5
6
7
8
CallSite CS(&I);

// 获取要传递给替换函数的原始函数参数
std::vector<Value *> arguments;

for (unsigned int i = 0; i < CS.arg_size(); i++) {
  arguments.push_back(CS.getArgument(i));
}

然后,我们创建对新函数的调用并替换旧调用。

1
2
3
4
5
6
7
8
// 创建对新函数的调用
auto newCall = CallInst::Create(newFunction, arguments, "", &I);

// 将旧调用的所有用法替换为新调用
for (auto &U : I.uses()) {
  User* user = U.getUser();
  user->setOperand(U.getOperandNo(), newCall);
}

完整工具

虽然LLVM传递完成了替换给定函数的工作,但它与其他步骤一起包装在实现完整过程的bash脚本中。首先,我们使用McSema反汇编和提升两个输入二进制文件。

使用McSema提升二进制文件

接下来,我们分析和调整位码以找到McSema表示的函数名称。此代码部分包括对动态加载函数和剥离二进制文件的支持,这些会影响函数名称。我们需要知道这些名称,以便在实际进行替换时可以将它们作为参数传递给LLVM传递。如果我们从原始二进制文件中查找名称,LLVM传递将无法找到任何匹配的函数,因为我们使用的是提升的位码。

查找要替换的函数名称

最后,我们运行传递。如果我们不需要访问原始函数,我们只需要在原始二进制文件上运行传递。但是,如果我们想从替换函数中调用原始函数,我们在原始二进制文件和替换二进制文件上运行传递。在第二种情况下,我们用替换函数替换原始函数,并用原始函数替换存根函数。最后,我们将所有内容重新编译为一个新的工作二进制文件。

运行传递并从更新的位码编译新二进制文件

结果

Fennec使用二进制提升和重新编译使一个困难的问题变得相对易于管理。它对于修复遗留软件中的安全错误特别有用,在这些软件中你可能无法访问源代码。

使用此工具,可以自动修复密码学IV漏洞。如下所示,原始二进制文件每次使用静态IV相同地加密消息。然而,在运行Fennec后,新创建的二进制文件使用不同的IV,从而在每次运行时产生唯一的密文,即使在同一明文上(蓝色)。

 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
# 原始二进制文件
aditi@nessie:~/ToB-Summer19$ ./encrypt ""
MDEyMzQ1Njc4OTAxMjM0NQ==/reJh+5rktBatDpyuJNQEBo++0pyIRGZiNsmZkN09HTPIOBVqQ9ov6CrxPXO7dC4cUJGYzBEsejHuTQyjVQh+XsLCHyDkURmfCuJ+a97raPY+o8pKKt8yf/xTmYMtyq2zf7EQxqPxv2bXKdP+6K+h9KyuO3q4+3JbuJFTesNLy8Np1m9ShJ9UAHvAdO6LCZvQ
N91kz0ytIH+s7LgajIWyises+yz26UBQwOzZLeLcQp4=
176

aditi@nessie:~/ToB-Summer19$ ./encrypt ""
MDEyMzQ1Njc4OTAxMjM0NQ==/reJh+5rktBatDpyuJNQEBo++0pyIRGZiNsmZkN09HTPIOBVqQ9ov6CrxPXO7dC4cUJGYzBEsejHuTQyjVQh+XsLCHyDkURmfCuJ+a97raPY+o8pKKt8yf/xTmYMtyq2zf7EQxqPxv2bXKdP+6K+h9KyuO3q4+3JbuJFTesNLy8Np1m9ShJ9UAHvAdO6LCZvQ
N91kz0ytIH+s7LgajIWyises+yz26UBQwOzZLeLcQp4=
176

aditi@nessie:~/ToB-Summer19$ ./encrypt ""
MDEyMzQ1Njc4OTAxMjM0NQ==/reJh+5rktBatDpyuJNQEBo++0pyIRGZiNsmZkN09HTPIOBVqQ9ov6CrxPXO7dC4cUJGYzBEsejHuTQyjVQh+XsLCHyDkURmfCuJ+a97raPY+o8pKKt8yf/xTmYMtyq2zf7EQxqPxv2bXKdP+6K+h9KyuO3q4+3JbuJFTesNLy8Np1m9ShJ9UAHvAdO6LCZvQ
N91kz0ytIH+s7LgajIWyises+yz26UBQwOzZLeLcQp4=
176

aditi@nessie:~/ToB-Summer19$ bash run.sh 2 ../mcsema-2.0.0-ve/remill-2.0.0/remill-build-2/ /home/aditi/ToB-Summer19/ida-6.9/idal64 encrypt replaceIV generate_iv replacement generate_iv_original -lcrypto

# Fennec修改后的二进制文件
aditi@nessie:~/ToB-Summer19$ ./encrypt.new ""
L+PYRFiOKMcu18hSqdGQEw==/aK2hYm/GXHwA2tqZxPmoNccQwW+Zhj7E0PQUSRF+lOLJiEMwOc7yv+/Z2AA0pEJjP7Jq4lHMpq2eIVl73lvav0pJiVlOcmfnFwQ9cu0MW0EWqUdgl2FCsWKtO/TAfGhcQPopJyvP8KD/LHlru4QIfZiym7//tt0V9vvabFCLNiSTRG350XKO/zoydeuRFfSu
0HmNNQbAcLSQkcUETH424RyQ4SxmcreW3krOw30kfJY=
176

aditi@nessie:~/ToB-Summer19$ ./encrypt.new ""
hYnowxN2Z3QyPIzwNaFzJw==/pzCq+V1q5ipHoqJXZ9MaeDr+nMdV5E1RbeI+YrcQqXjFHcVmDSq4yZboEuIJJjkbNbdO5DG6n3CQnZ1C7CumGdaZsddaYJueORROk7X+PnQZUq5bKqvdN7ZJEhK7qaerjogOF4TAotDV3ryLC6l/EWY01DkhGrf0hlXAkjQnOz28lXF40GNMd6pIjcoIbZze
V72v5s5q67fVdKdCzVE3BH76qX8qYS9YnN5JkGLERYA=
176

aditi@nessie:~/ToB-Summer19$ ./encrypt.new ""
r3/wMu5nD3rEFn7N88fCjQ==/MisK9RcK8RLsqjV2nrAfprghBYrBmeJS3FbJ4YG6zHBk+uA0CcZ+R4CSDolAaAPlCmkupfxy6bFHNEqyMVv7moPaiJEAkHDDU/FKen8eAJjMvz9+RK+xmQja238jk7xmaS6JbJOdh8teQ2XiMzlHsBYBVpw89UBFrTqOSN8qtlgU3aR4xUVlwZAA1+Pg2GHy
2CIWQI6ioHGDhN3P3po7MaOldJAgHGZO5d2GluroI70=
176

你可以在此处下载Fennec并找到使用说明。

如果你对该工具有任何问题或意见,可以在Twitter上找到Aditi:@aditi_gupta0!

如果你喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News

页面内容

  • 问题描述
  • 密码学示例
  • 开发过程
  • 函数替换传递
  • 完整工具
  • 结果
  • 最近文章
    • 使用Deptective调查你的依赖项
    • 系好安全带,Buttercup,AIxCC的评分回合正在进行中!
    • 使你的智能合约超越私钥风险
    • Go解析器中意外的安全陷阱
    • 我们从审查Silence Laboratories的首批DKLs23库中学到了什么

© 2025 Trail of Bits. 使用Hugo和Mainroad主题生成。

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