分类 信息安全 下的文章

本文为elastic security团队成员Gabriel Landau《What you need to know about Process Ghosting, a new executable image tampering attack》文章中文翻译,因本人水平有限如发现相关错误欢迎指正。

基于Windows环境的防御企业安全团队通常依赖于反病毒产品作为抵御恶意可执行文件的第一道防线。微软为安全产品供应商提供了注册系统回调的功能,这些回调将在系统创建进程的时候被调用。相关安全产品的驱动程序开发人员可以调用诸如PsSetCreateProcessNotifyRoutineEx之类的API来感知系统创建进程的事件。

尽管如此,PsSetCreateProcessNotifyRoutineEx设置的回调函数在创建进程那一瞬时实际上并不会被调用,而是在该进程中创建第一个线程之后才会被回调所感知到。这就在创建进程和系统通知安全产品创建进程的事件之间产生了时间差。它这就给恶意软件的制作者提供了一个攻击窗口,可以在安全产品扫描磁盘中的可执行文件之前篡改它。近些年来这种篡改磁盘PE文件攻击的例子包括Process DoppelgängingProcess Herpaderping,它们使用这种技术来逃避安全产品的静态文件扫描。

本文描述了一种新的可执行映像篡改攻击,类似于Doppelgänging和Herpaderping,但与他们又有一些不同的地方。利用这种新技术,攻击者可以将恶意软件写入磁盘并使其难以扫描或删除,在可执行文件加载之后再执行删除命令,将恶意程序伪装成就像磁盘上的一个普通文件一样。这种技术不涉及代码注入、傀儡进程或事务性NTFS (TxF)。

进程的诞生(The birth of a process)

Windows任务管理器会显示系统上运行的进程列表,这些进程的每一个都与磁盘上的一个可执行文件相对应,例如svchost.exe。这是因为Windows的进程是从磁盘中的可执行文件启动,通常可执行文件以EXE文件扩展名结尾。

任务管理器

重要的是要注意进程不是可执行文件,可执行文件也不是进程。在上面任务管理器的示例中,从RuntimeBroker.exe和svchost.exe就启动了多个进程,可执行文件和进程是一个一对多的关系。

要启动一个新进程,必须执行一系列步骤。在现代的Windows中,进程创建通常由NtCreateUserProcess在内核中执行,然而某些个别组件API如NtCreateProcessEx等,仍因为向后兼容的目的还在公开使用。这些步骤是:

  1. 打开可执行文件的句柄。 Example: hFile = CreateFile(“C:\Windows\System32\svchost.exe”)
  2. 为文件创建一个映像节区(image sections)。节区(section )将文件或文件的一部分映射到内存中。映像节区是一种特殊类型的节,对应于可移植可执行文件(PE),并且只能从PE (EXE, DLL等)文件中创建。Example: hSection = NtCreateSection(hFile, SEC_IMAGE)
  3. 使用映像节区(image sections)创建一个进程。 Example: hProcess = NtCreateProcessEx(hSection)
  4. 分配进程参数和环境变量。Example: CreateEnvironmentBlock/NtWriteVirtualMemory
  5. 创建一个在进程中执行的主线程。 Example: NtCreateThreadEx

img

下面是它在Process Monitor中的样子:

img

如Process Monitor中所示,explorer.exe启动notepad.exe的过程

进程是从可执行文件启动的,但是可执行文件中的一些数据在映射到进程时可能被修改。为了考虑这些修改,Windows内存管理器在创建映像节区时会再内存中缓存对应的映像节区。这就意味着映像节区(image sections)可能偏离其可执行文件。

扫描恶意软件进程(Scanning processes for malware)

微软为安全产品供应商提供了注册回调的功能,这些回调将在系统上创建进程和线程时被调用。驱动程序开发人员可以调用诸如PsSetCreateProcessNotifyRoutineExPsSetCreateThreadNotifyRoutineEx等API来接收此类事件。

尽管如此,PsSetCreateProcessNotifyRoutineEx回调实际上不是在进程创建时调用的,而是在这些进程中创建第一个线程时调用的。这就在创建进程和通知安全产品创建进程之间产生了时间差。这就给恶意软件的制作者提供了一个攻击窗口,可以在安全产品扫描磁盘中的可执行文件之前篡改它。

请注意,未文档化的进程创建API NtCreateProcess使用的是节区(Section)句柄,而不是文件句柄:

NTSYSCALLAPI
NTSTATUS
NTAPI
NtCreateProcess(
    _Out_ PHANDLE ProcessHandle,
    _In_ ACCESS_MASK DesiredAccess,
    _In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
    _In_ HANDLE ParentProcess,
    _In_ BOOLEAN InheritObjectTable,
    _In_opt_ HANDLE SectionHandle,
    _In_opt_ HANDLE DebugPort,
    _In_opt_ HANDLE ExceptionPort
    );

当一个进程启动时,安全产品将通过系统回调获得关于正在启动的进程的以下信息:

typedef struct _PS_CREATE_NOTIFY_INFO {
  SIZE_T              Size;
  union {
    ULONG Flags;
    struct {
      ULONG FileOpenNameAvailable : 1;
      ULONG IsSubsystemProcess : 1;
      ULONG Reserved : 30;
    };
  };
  HANDLE              ParentProcessId;
  CLIENT_ID           CreatingThreadId;
  struct _FILE_OBJECT *FileObject;
  PCUNICODE_STRING    ImageFileName;
  PCUNICODE_STRING    CommandLine;
  NTSTATUS            CreationStatus;
} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;

值得注意的是FILE_OBJECT,它是与上一节中传递给NtCreateSection的HANDLE对应的文件内核对象。这个FILE_OBJECT通常对应磁盘上的一个文件,可以安全产品用来被扫描恶意软件。

安全产品还可能使用文件系统minifilter 回调,当文件被创建、修改或关闭时,它会接收通知。扫描每个读和写操作对系统的性能影响可能很大,因此出于性能考虑,文件通常在打开和关闭时进行扫描。

还有其他潜在的安全产品拦截点,我们将不在这里讨论。更多信息请看这篇演讲

前人工作(Prior work)

Process Doppelgänging

Windows事务性NTFS (TxF)是一种允许应用程序作为单个原子事务执行一系列文件系统操作,然后提交或回滚该事务的机制。事务中可能存在文件回滚,则底层的文件系统永远不会看到这些文件更改。使用TxF,可以从在事务中的文件创建映像节区,然后回滚该事务。可以从这样修改后的映像节区创建一个进程,而磁盘中的文件还是原来的内容。

Process Herpaderping

在创建映像节区之后,Process Herpaderping利用一个诱饵PE文件使用现有的文件句柄覆盖其可执行文件。将诱饵PE文件留在磁盘上,但它与运行在内存中的映像节区不同。在有payload进程的整个生命周期中,诱饵PE文件都保留在磁盘上。

推荐一篇分析该项技术很好的文章以便读者更好理解 bbs.pediy.com/thread-271554.htm (成哥yyds

Process Reimaging

进程重映像利用了Windows内核中的缓存同步问题,它会导致可执行文件的路径与从该可执行文件创建的映像节区所报告的路径不匹配。通过在一个诱饵路径上加载DLL,然后卸载它,然后从一个新路径加载它,许多Windows API将返回旧路径。这可能可以欺骗安全产品,使其在错误的路径上查找加载的映像。

进程重影(Ghosting a process)

我们可以在Doppelgänging和Herpaderping的基础上运行已删除的可执行文件。在Windows上有几种删除文件的方法,包括:

  1. 在设置FILE_SUPERSEDECREATE_ALWAYS标志的在旧文件之上创建一个新文件。
  2. 在创建或打开文件时设置 FILE_DELETE_ON_CLOSEFILE_FLAG_DELETE_ON_CLOSE标志。
  3. 通过 NtSetInformationFile设置FileDispositionInformation时,将 FILE_DISPOSITION_INFORMATION结构中的 DeleteFile 字段设置为 TRUE。

Windows会试图阻止修改映射后的可执行文件,一旦一个文件被映射到一个节区(section)部分,尝试用FILE_WRITE_DATA参数打开它(修改它),将会报ERROR_SHARING_VIOLATION错误。使用FILE_DELETE_ON_CLOSE/FILE_FLAG_DELETE_ON_CLOSE参数尝试删除操作,会报ERROR_SHARING_VIOLATION错误。

NtSetInformationFile(FileDispositionInformation)函数调用时需要有DELETE访问权限的文件。即使将DELETE访问权限授予映射到映像节区的文件,设置NtSetInformationFile(FileDispositionInformation)时也会出现STATUS_CANNOT_DELETE错误。尝试删除设置了 FILE_SUPERCEDE/CREATE_ALWAYS 参数的文件会导致ACCESS_DENIED错误。(ps: 按照译者理解在NtSetInformationFile应该表达的是在设置FileDispositionInformation的时候不能是映射后的文件,得是默认状态下的磁盘文件否则会出现STATUS_CANNOT_DELETE错误)

然而需要注意的是,这个删除限制只有在可执行文件映射到映像节区时才生效。这意味着可以创建一个文件,将其标记为删除,将其映射到一个映像节区,关闭文件句柄以完成删除,然后从现在的无文件节区创建一个进程。这就是进程重影(Process Ghosting)。

简单来说就是一种通过自删除的手法进行静态文件绕过的解决方案

攻击流程为:

  1. 创建一个临时文件
  2. 调用NtSetInformationFile(FileDispositionInformation)将文件设置为删除挂起(delete-pending )状态。注意:尝试使用FILE_DELETE_ON_CLOSE将不会直接删除文件。
  3. 将payload内容写入刚才创建的临时文件,因为文件已经处于删除挂起状态,所以内容不会被一直保存,删除挂起状态还会阻止外部进程尝试打开文件。
  4. 为该文件创建一个映像节区。
  5. 关闭删除挂起状态的文件句柄,随后系统会删除该临时文件。
  6. 使用第4步的映像节区创建一个进程。
  7. 设置进程参数和环境变量。
  8. 创建主线程

img

反病毒程序的回调在线程创建时被调用,线程创建时其对应的文件已经被删除了。对于已经被删除的文件做打开或者I/O执行会产生STATUS_FILE_DELETED的报错。如果尝试打开处于删除挂起状态的文件则会报STATUS_DELETE_PENDING错误。

这种类型的篡改也可以应用于DLL,因为DLL也是映射到映像节区的。

样例(Demo)

本节因为特殊原因与原文有些省略,不过代码都是使用这个仓库读者可以参考源码进行理解

通过观察Process Monitor的记录可以发现MsMpEng.exe(Windows Defender中的一个后台进程)想要打开payload文件时会出现STATUS_FILE_DELETED/STATUS_DELETE_PENDING的报错,进而绕过反病毒程序的静态文件扫描。

img

检测(Detection)

Elastic Security团队已经可以检测诸如Doppelgänging, Herpaderping和Ghosting这类的进程映像篡改攻击,检测的原理大致为在进程创建的回调期间检验FILE_OBJECT 结构是否有异常。

img

技术比较(Comparing techniques)

基于Process Herpaderping文档中的一个有用的表格,我们可以比较各种技术之间的API调用流程:

image-20221228110154996

总结(Conclusion)

在这篇博客中,我们研究了Windows可执行映像篡改攻击的现状,然后披露了一种新的类似攻击。然后,我们演示了这种绕过常见安全软件的攻击,并演示了如何使用Elastic Security检测它。若要在您的系统环境中发现进程篡改等威胁,请在Elastic Cloud上安装最新版本的Elastic Security,并确保学习了我们的快速入门培训,为成功实验做好准备。Happy hunting!

漏洞披露责任:我们在2021-05-06向MSRC提交了一份漏洞报告,包括这篇博客文章的草稿、演示视频和PoC的源代码。他们在2021-05-10认为这不算是一个漏洞。

以下内容为译者学习后的总结及参考文献

仓库代码流程

源码参考的是https://github.com/hasherezade/process_ghosting,其大致流程如下:

image-20221228112152860

参考链接

  1. https://www.elastic.co/cn/blog/process-ghosting-a-new-executable-image-tampering-attack
  2. https://bbs.pediy.com/thread-271554.htm#msg_header_h2_5
  3. https://mp.weixin.qq.com/s/HE0Re6RZ0wojTwPnHjeF3Q
  4. https://www.hackingarticles.in/process-ghosting-attack/
  5. https://github.com/hasherezade/process_ghosting
  6. https://github.com/knightswd/ProcessGhosting

0x01 指令替换基本介绍

指令替换就是将正常的二元运算指令(如加法、减法、异或等等),替换为等效而更复杂的指令序列,以达到混淆计算过程的目的。它仅支持整数运算的替换,因为替换浮点指令会造成舍入的错误和误差。
最近这四篇LLVM混淆的文章都是来自看雪“LLVM与代码混淆技术”这门课,推荐大家可以去看一下绝对物超所值,感谢34r7hm4n的倾情讲解。

例如:

a = b + c 
    ===> a = b - (-c)
    ===> a = -(-b + (-c))

该混淆唯一的难点就在于找到和原指令等效的复杂指令,并将所有的原指令替换成复杂指令即可,其他都是重复操作没啥特别的。

0x02 实现方法

实现复杂指令替换过程中主要注意要将创建的每条指令插到BI(当前原指令,这种只是物理上的插入并不是逻辑上的,所以对原来的指令没有影响)前面,后续利用replaceAllUsesWith函数将新创建的复杂指令把原指令在物理上替换掉就行。

保存原指令块,switch-case判断进入对应混淆

bool Substitution::runOnFunction(Function &F){
    for(int i = 0;i < ObfuTime;i ++){
        for(BasicBlock &BB : F){
            vector<Instruction*> origInst;
            for(Instruction &I : BB){
                origInst.push_back(&I);
            }
            for(Instruction *I : origInst){
                if(isa<BinaryOperator>(I)){
                    BinaryOperator *BI = cast<BinaryOperator>(I);
                    substitute(BI);
                }
            }
        }
    }
}

void Substitution::substitute(BinaryOperator *BI){
    bool flag = true;
    switch (BI->getOpcode()) {
        case BinaryOperator::Add:
            substituteAdd(BI);
            break;
        case BinaryOperator::Sub:
            substituteSub(BI);
            break;
        case BinaryOperator::And:
            substituteAnd(BI);
            break;
        case BinaryOperator::Or:
            substituteOr(BI);
            break;
        case BinaryOperator::Xor:
            substituteXor(BI);
            break;
        default:
            flag = false;
            break;
    }
    if(flag){
        // 是否删去不影响执行结果,此时已经替换掉BI了,原BI没有地方执行了
        BI->eraseFromParent();
    }
}

1. 加法替换

image-20220709211305441

void Substitution::substituteAdd(BinaryOperator *BI){
    int choice = rand() % NUMBER_ADD_SUBST;
    switch (choice) {
        case 0:
            addNeg(BI);
            break;
        case 1:
            addDoubleNeg(BI);
            break;
        case 2:
            addRand(BI);
            break;
        case 3:
            addRand2(BI);
            break;
        default:
            break;
    }
}
void Substitution::addNeg(BinaryOperator *BI){
    BinaryOperator *op;
    op = BinaryOperator::CreateNeg(BI->getOperand(1), "", BI);
    op = BinaryOperator::CreateSub(BI->getOperand(0), op, "", BI);
    BI->replaceAllUsesWith(op);
}

void Substitution::addDoubleNeg(BinaryOperator *BI){
    BinaryOperator *op, *op1, *op2;
    op1 = BinaryOperator::CreateNeg(BI->getOperand(0), "", BI);
    op2 = BinaryOperator::CreateNeg(BI->getOperand(1), "", BI);
    op = BinaryOperator::CreateAdd(op1, op2, "", BI);
    op = BinaryOperator::CreateNeg(op, "", BI);
    BI->replaceAllUsesWith(op);
}

void Substitution::addRand(BinaryOperator *BI){
    ConstantInt *r = (ConstantInt*)CONST(BI->getType(), rand());
    BinaryOperator *op, *op1, *op2;
    op = BinaryOperator::CreateAdd(BI->getOperand(0), r, "", BI);
    op = BinaryOperator::CreateAdd(op, BI->getOperand(1), "", BI);
    op = BinaryOperator::CreateSub(op, r, "", BI);
    BI->replaceAllUsesWith(op);
}

void Substitution::addRand2(BinaryOperator *BI){
    ConstantInt *r = (ConstantInt*)CONST(BI->getType(), rand());
    BinaryOperator *op, *op1, *op2;
    op = BinaryOperator::CreateSub(BI->getOperand(0), r, "", BI);
    op = BinaryOperator::CreateAdd(op, BI->getOperand(1), "", BI);
    op = BinaryOperator::CreateAdd(op, r, "", BI);
    BI->replaceAllUsesWith(op);
}

2. 减法替换

image-20220709211846015

void Substitution::substituteSub(BinaryOperator *BI){
    int choice = rand() % NUMBER_SUB_SUBST;
    switch (choice) {
        case 0:
            subNeg(BI);
            break;
        case 1:
            subRand(BI);
            break;
        case 2:
            subRand2(BI);
            break;
        default:
            break;
    }
}
void Substitution::subNeg(BinaryOperator *BI){
    BinaryOperator *op;
    op = BinaryOperator::CreateNeg(BI->getOperand(1), "", BI);
    op = BinaryOperator::CreateAdd(BI->getOperand(0), op, "", BI);
    BI->replaceAllUsesWith(op);
}

void Substitution::subRand(BinaryOperator *BI){
    ConstantInt *r = (ConstantInt*)CONST(BI->getType(), rand());
    BinaryOperator *op, *op1, *op2;
    op = BinaryOperator::CreateAdd(BI->getOperand(0), r, "", BI);
    op = BinaryOperator::CreateSub(op, BI->getOperand(1), "", BI);
    op = BinaryOperator::CreateSub(op, r, "", BI);
    BI->replaceAllUsesWith(op);
}

void Substitution::subRand2(BinaryOperator *BI){
    ConstantInt *r = (ConstantInt*)CONST(BI->getType(), rand());
    BinaryOperator *op, *op1, *op2;
    op = BinaryOperator::CreateSub(BI->getOperand(0), r, "", BI);
    op = BinaryOperator::CreateSub(op, BI->getOperand(1), "", BI);
    op = BinaryOperator::CreateAdd(op, r, "", BI);
    BI->replaceAllUsesWith(op);
}

3. 与替换

image-20220709212107850

void Substitution::substituteAnd(BinaryOperator *BI){
    int choice = rand() % NUMBER_AND_SUBST;
    switch (choice) {
        case 0:
            andSubstitute(BI);
            break;
        case 1:
            andSubstituteRand(BI);
            break;
        default:
            break;
    }
}
void Substitution::andSubstitute(BinaryOperator *BI){
    BinaryOperator *op;
    op = BinaryOperator::CreateNot(BI->getOperand(1), "", BI);
    op = BinaryOperator::CreateXor(BI->getOperand(0), op, "", BI);
    op = BinaryOperator::CreateAnd(op, BI->getOperand(0), "", BI);
    BI->replaceAllUsesWith(op);
}

void Substitution::andSubstituteRand(BinaryOperator *BI){
    ConstantInt *r = (ConstantInt*)CONST(BI->getType(), rand());
    BinaryOperator *op, *op1;
    op = BinaryOperator::CreateNot(BI->getOperand(0), "", BI);
    op1 = BinaryOperator::CreateNot(BI->getOperand(1), "", BI);
    op = BinaryOperator::CreateOr(op, op1, "", BI);
    op = BinaryOperator::CreateNot(op, "", BI);
    op1 = BinaryOperator::CreateNot(r, "", BI);
    op1 = BinaryOperator::CreateOr(r, op1, "", BI);
    op = BinaryOperator::CreateAnd(op, op1, "", BI);
    BI->replaceAllUsesWith(op);
}

4. 或替换

void Substitution::substituteOr(BinaryOperator *BI){
    int choice = rand() % NUMBER_OR_SUBST;
    switch (choice) {
        case 0:
            orSubstitute(BI);
            break;
        case 1:
            orSubstituteRand(BI);
            break;
        default:
            break;
    }
}
void Substitution::orSubstitute(BinaryOperator *BI){
    BinaryOperator *op, *op1;
    op = BinaryOperator::CreateAnd(BI->getOperand(0), BI->getOperand(1), "", BI);
    op1 = BinaryOperator::CreateXor(BI->getOperand(0), BI->getOperand(1), "", BI);
    op = BinaryOperator::CreateOr(op, op1, "", BI);
    BI->replaceAllUsesWith(op);
}

void Substitution::orSubstituteRand(BinaryOperator *BI){
    ConstantInt *r = (ConstantInt*)CONST(BI->getType(), rand());
    BinaryOperator *op, *op1;
    op = BinaryOperator::CreateNot(BI->getOperand(0), "", BI);
    op1 = BinaryOperator::CreateNot(BI->getOperand(1), "", BI);
    op = BinaryOperator::CreateAnd(op, op1, "", BI);
    op = BinaryOperator::CreateNot(op, "", BI);
    op1 = BinaryOperator::CreateNot(r, "", BI);
    op1 = BinaryOperator::CreateOr(r, op1, "", BI);
    op = BinaryOperator::CreateAnd(op, op1, "", BI);
    BI->replaceAllUsesWith(op);
}

5. 异或替换

void Substitution::substituteXor(BinaryOperator *BI){
    int choice = rand() % NUMBER_XOR_SUBST;
    switch (choice) {
        case 0:
            xorSubstitute(BI);
            break;
        case 1:
            xorSubstituteRand(BI);
            break;
        default:
            break;
    }
}
void Substitution::xorSubstitute(BinaryOperator *BI){
    BinaryOperator *op, *op1, *op2, *op3;
    op1 = BinaryOperator::CreateNot(BI->getOperand(0), "", BI);
    op1 = BinaryOperator::CreateAnd(op1, BI->getOperand(1), "", BI);
    op2 = BinaryOperator::CreateNot(BI->getOperand(1), "", BI);
    op2 = BinaryOperator::CreateAnd(BI->getOperand(0), op2, "", BI);
    op = BinaryOperator::CreateOr(op1, op2, "", BI);
    BI->replaceAllUsesWith(op);
}

void Substitution::xorSubstituteRand(BinaryOperator *BI){
    ConstantInt *r = (ConstantInt*)CONST(BI->getType(), rand());
    BinaryOperator *op, *op1, *op2, *op3;
    op1 = BinaryOperator::CreateNot(BI->getOperand(0), "", BI);
    op1 = BinaryOperator::CreateAnd(op1, r, "", BI);
    op2 = BinaryOperator::CreateNot(r, "", BI);
    op2 = BinaryOperator::CreateAnd(BI->getOperand(0), op2, "", BI);
    op = BinaryOperator::CreateOr(op1, op2, "", BI);
    op1 = BinaryOperator::CreateNot(BI->getOperand(1), "", BI);
    op1 = BinaryOperator::CreateAnd(op1, r, "", BI);
    op2 = BinaryOperator::CreateNot(r, "", BI);
    op2 = BinaryOperator::CreateAnd(BI->getOperand(1), op2, "", BI);
    op3 = BinaryOperator::CreateOr(op1, op2, "", BI);
    op = BinaryOperator::CreateXor(op, op3, "", BI);
    BI->replaceAllUsesWith(op);
}

0x03 参考链接

https://www.kanxue.com/book-88-2113.htm

https://github.com/bluesadi/Pluto-Obfuscator/blob/kanxue/Transforms/src/Substitution.cpp

0x01 虚假控制流基本介绍

简单的说就是在原来的程序流程中克隆原基本块,利用不透明谓词将克隆的基本块作为永不可达的虚假分支,并在这个不可达的虚假分支中随机添加垃圾指令,以达到程序混淆的效果,得到的程序流程图和控制流平坦化不同,是程长条型的。

image-20220708202046640

所谓不透明谓词就是:

“不透明谓词是指一个表达式,他的值在执行到某处时,对程序员而言必然是已知的,但是由于某种原因,编译器或者说静态分析器无法推断出这个值,只能在运行时确定。”简单来说是程序员在编写的时候知道该表达式的结果,但是IDA在分析的时候无法分析出该结果,达到混淆的目的。

0x02 实现方法

image-20220708212444838

添加混淆轮次参数

// 混淆次数,混淆次数越多混淆结果越复杂
static cl::opt<int> obfuTimes("bcf_loop", cl::init(1), cl::desc("Obfuscate a function <bcf_loop> time(s)."));

1. 基本块拆分

将基本块拆分成头部、中部和尾部三个基本块。

通过 getFirstNonPHI 函数获取第一个不是 PHINode 的指令,以该指令为界限进行分割,得到 entryBB 和 bodyBB。以 bodyBB 的终结指令为界限进行分割,最终得到头部、中部和尾部三个基本块,也就是 entryBB, bodyBB 和 endBB。

// 第一步,拆分得到 entryBB, bodyBB, endBB
// 其中所有的 PHI 指令都在 entryBB(如果有的话)
// endBB 只包含一条终结指令
BasicBlock *bodyBB = entryBB->splitBasicBlock(entryBB->getFirstNonPHI(), "bodyBB");
BasicBlock *endBB = bodyBB->splitBasicBlock(bodyBB->getTerminator(), "endBB");

2. 基本块克隆

LLVM 自带 CloneBasicBlock 函数,但该函数为不完全克隆,在克隆的基本块中,仍然引用了原基本块中的 %a 变量,该引用是非法的,故需要将 %a 映射为 %a.clone,所以还需要做优化。

image-20220708212741779

// 第二步,克隆 bodyBB 得到克隆块 cloneBB
BasicBlock *cloneBB = createCloneBasicBlock(bodyBB);
BasicBlock* createCloneBasicBlock(BasicBlock *BB){
    // 克隆之前先修复所有逃逸变量
    vector<Instruction*> origReg;
    BasicBlock &entryBB = BB->getParent()->getEntryBlock();
    for(Instruction &I : *BB){
        if(!(isa<AllocaInst>(&I) && I.getParent() == &entryBB) 
            && I.isUsedOutsideOfBlock(BB)){
            origReg.push_back(&I);
        }
    }
    for(Instruction *I : origReg){
        DemoteRegToStack(*I, entryBB.getTerminator());
    }
    
    ValueToValueMapTy VMap;
    BasicBlock *cloneBB = CloneBasicBlock(BB, VMap, "cloneBB", BB->getParent());
    // 对克隆基本块的引用进行修复
    for(Instruction &I : *cloneBB){
        for(int i = 0;i < I.getNumOperands();i ++){
            Value *V = MapValue(I.getOperand(i), VMap);
            if(V){
                I.setOperand(i, V);
            }
        }
    }
    return cloneBB;
}

3. 构造虚假跳转

将 entryBB 到 bodyBB 的绝对跳转改为条件跳转;将 bodyBB 到 endBB 的绝对跳转改为条件跳转;添加 cloneBB 到 bodyBB 的绝对跳转

image-20220708213030706

// 第三步,构造虚假跳转
// 1. 将 entryBB, bodyBB, cloneBB 末尾的绝对跳转移除
entryBB->getTerminator()->eraseFromParent();
bodyBB->getTerminator()->eraseFromParent();
cloneBB->getTerminator()->eraseFromParent();
// 2. 在 entryBB 和 bodyBB 的末尾插入条件恒为真的虚假比较指令
Value *cond1 = createBogusCmp(entryBB); 
Value *cond2 = createBogusCmp(bodyBB); 
// 3. 将 entryBB 到 bodyBB 的绝对跳转改为条件跳转
BranchInst::Create(bodyBB, cloneBB, cond1, entryBB);
// 4. 将 bodyBB 到 endBB的绝对跳转改为条件跳转
BranchInst::Create(endBB, cloneBB, cond2, bodyBB);
// 5. 添加 bodyBB.clone 到 bodyBB 的绝对跳转
BranchInst::Create(bodyBB, cloneBB);

createBogusCmp函数的具体实现,构造恒成立式子

Value* BogusControlFlow::createBogusCmp(BasicBlock *insertAfter){
    // if((y < 10 || x * (x + 1) % 2 == 0))
    // 等价于 if(true)
    Module *M = insertAfter->getModule();
    GlobalVariable *xptr = new GlobalVariable(*M, TYPE_I32, false, GlobalValue::CommonLinkage, CONST_I32(0), "x");
    GlobalVariable *yptr = new GlobalVariable(*M, TYPE_I32, false, GlobalValue::CommonLinkage, CONST_I32(0), "y");
    LoadInst *x = new LoadInst(TYPE_I32, xptr, "", insertAfter);
    LoadInst *y = new LoadInst(TYPE_I32, yptr, "", insertAfter);
    ICmpInst *cond1 = new ICmpInst(*insertAfter, CmpInst::ICMP_SLT, y, CONST_I32(10));
    BinaryOperator *op1 = BinaryOperator::CreateAdd(x, CONST_I32(1), "", insertAfter);
    BinaryOperator *op2 = BinaryOperator::CreateMul(op1, x, "", insertAfter);
    BinaryOperator *op3 = BinaryOperator::CreateURem(op2, CONST_I32(2), "", insertAfter);
    ICmpInst *cond2 = new ICmpInst(*insertAfter, CmpInst::ICMP_EQ, op3, CONST_I32(0));
    return BinaryOperator::CreateOr(cond1, cond2, "", insertAfter);
}

0x03 参考资料

https://www.anquanke.com/post/id/212768

https://bbs.pediy.com/thread-266201.htm

https://www.zhihu.com/question/46259412/answer/199689652

https://github.com/obfuscator-llvm/obfuscator/blob/llvm-4.0/lib/Transforms/Obfuscation/BogusControlFlow.cpp

https://github.com/bluesadi/Pluto-Obfuscator/blob/kanxue/Transforms/src/BogusControlFlow.cpp

0x01 控制流平坦化基本介绍

控制流平坦化是指将正常程序控制流中基本块之间的跳转关练删除,用一个集中的主分发块来调度基本块的执行顺序。相当于把原有程序正常的逻辑改为一个循环嵌套一个switch的逻辑。

正常情况:

正常情况

控制流平坦化之后:

混淆后

控制流平坦化的基本结构如下:

image-20220708115813228

  • 入口块:进入函数第一个执行的基本块
  • 分发块:负责跳转到下一个要执行的原基本块
  • 原基本块:混淆之前的基本块,实际完成程序工作的基本块
  • 返回块:返回到主分发块

修改了程序的控制流,导致逆向分析人员不容易直接的理清程序执行流程,增加分析难度。

0x02 实现方式

本节以https://github.com/bluesadi/Pluto-Obfuscator/tree/kanxue 项目为基准进行分析。主要的代码实现分为五大块如下图所示:

image-20220708122724034

1. 保存原基本块

将除入口块以外的以外的基本块保存到 vector 容器中,方便后续处理。如果入口块的终结指令是条件分支指令,则将该指令单独分离出来作为一个基本块,加入到 vector 容器的最前面。

    // 将除入口块(第一个基本块)以外的基本块保存到一个 vector 容器中,便于后续处理
    // 首先保存所有基本块
    vector<BasicBlock*> origBB;
    for(BasicBlock &BB: F){
        origBB.push_back(&BB);
    }
    // 从vector中去除第一个基本块
    origBB.erase(origBB.begin());
    BasicBlock &entryBB = F.getEntryBlock();
    // 如果第一个基本块的末尾是条件跳转,单独分离
    if(BranchInst *br = dyn_cast<BranchInst>(entryBB.getTerminator())){
        if(br->isConditional()){
            BasicBlock *newBB = entryBB.splitBasicBlock(br, "newBB");
            origBB.insert(origBB.begin(), newBB);
        }
    }

2. 创建分发块和返回块

除了原基本块之外,我们还要续创建一个分发块来调度基本块的执行顺序。并建立入口块到分发块的绝对跳转。再创建一个返回块,原基本块执行完后都需要跳转到这个返回块,返回块会直接跳转到分发块进行下一次的基本块跳转。

// 创建分发块和返回块
    BasicBlock *dispatchBB = BasicBlock::Create(*CONTEXT, "dispatchBB", &F, &entryBB);
    BasicBlock *returnBB = BasicBlock::Create(*CONTEXT, "returnBB", &F, &entryBB);
    BranchInst::Create(dispatchBB, returnBB);
    entryBB.moveBefore(dispatchBB);
    // 去除第一个基本块末尾的跳转
    entryBB.getTerminator()->eraseFromParent();
    // 使第一个基本块跳转到dispatchBB
    BranchInst *brDispatchBB = BranchInst::Create(dispatchBB, &entryBB);

3. 实现分发块调度

在入口块中创建并初始化 switch 要使用的变量,在调度块中插入switch-case 指令实现分发功能。将原基本块移动到返回块之前,并给每一个原基本块分配随机的 case 值,并将其添加到 switch 指令的对应case分支中。

// 在入口块插入alloca和store指令创建并初始化switch变量,初始值为随机值
    int randNumCase = rand();
    AllocaInst *swVarPtr = new AllocaInst(TYPE_I32, 0, "swVar.ptr", brDispatchBB);
    new StoreInst(CONST_I32(randNumCase), swVarPtr, brDispatchBB);
    // 在分发块插入load指令读取switch变量
    LoadInst *swVar = new LoadInst(TYPE_I32, swVarPtr, "swVar", false, dispatchBB);
    // 在分发块插入switch指令实现基本块的调度
    BasicBlock *swDefault = BasicBlock::Create(*CONTEXT, "swDefault", &F, returnBB);
    BranchInst::Create(returnBB, swDefault);
    SwitchInst *swInst = SwitchInst::Create(swVar, swDefault, 0, dispatchBB);
    // 将原基本块插入到返回块之前,并分配case值
    for(BasicBlock *BB : origBB){
        BB->moveBefore(returnBB);
        swInst->addCase(CONST_I32(randNumCase), BB);
        randNumCase = rand();
    }

4. 实现调度变量自动调整

在每个原基本块最后添加修改 switch 要使用的变量值的指令,以便返回分发块之后,能够正确执行到下一个基本块。删除原基本块末尾的跳转,使其结束执行后跳转到返回块,这一步需要注意判断原基本块末尾跳转的语句。(类似于VMP3每一个handler的末尾指定下一个要跳转的handler)

     // 在每个基本块最后添加修改switch变量的指令和跳转到返回块的指令
    for(BasicBlock *BB : origBB){
        // retn BB
        if(BB->getTerminator()->getNumSuccessors() == 0){
            continue;
        }
        // 非条件跳转
        else if(BB->getTerminator()->getNumSuccessors() == 1){
            BasicBlock *sucBB = BB->getTerminator()->getSuccessor(0);
            BB->getTerminator()->eraseFromParent();
            ConstantInt *numCase = swInst->findCaseDest(sucBB);
            new StoreInst(numCase, swVarPtr, BB);
            BranchInst::Create(returnBB, BB);
        }
        // 条件跳转
        else if(BB->getTerminator()->getNumSuccessors() == 2){
            ConstantInt *numCaseTrue = swInst->findCaseDest(BB->getTerminator()->getSuccessor(0));
            ConstantInt *numCaseFalse = swInst->findCaseDest(BB->getTerminator()->getSuccessor(1));
            BranchInst *br = cast<BranchInst>(BB->getTerminator());
            SelectInst *sel = SelectInst::Create(br->getCondition(), numCaseTrue, numCaseFalse, "", BB->getTerminator());
            BB->getTerminator()->eraseFromParent();
            new StoreInst(sel, swVarPtr, BB);
            BranchInst::Create(returnBB, BB);
        }
    }

当原基本块出现switch-case等大于2个分支的情况时,我们可以在优化过程中使用lowerswitch将其变成只有2个及以下分支的状态,使用命令如下:

opt -lowerswitch -S TestProgram_orig.ll -o TestProgram_lowerswitch.ll

关于lowerswitch的使用也可以直接在代码中实现,该项目是在LLVM12.0.1中编译的,所以不能直接使用createLowerSwitchPass函数否则会导致崩溃,应该在Flattening中添加如下函数

void getAnalysisUsage(AnalysisUsage &AU) const override{
    errs() << "Require LowerSwitchPass\r\n";
    AU.addRequiredID(LowerSwitchID);
    FunctionPass::getAnalysisUsage(AU);
}

image-20220708144027160

5. 修复PHI指令和逃逸变量

PHI 指令的值由前驱块决定,平坦化后所有原基本块的前驱块都变成了分发块,因此 PHI 指令发生了损坏。

逃逸变量指在一个基本块中定义,并且在另一个基本块被引用的变量。在原程序中某些基本块可能引用之前某个基本块中的变量,平坦化后原基本块之间不存在确定的前后关系了(由分发块决定),因此某些变量的引用可能会损坏。

修复的方法是,将 PHI 指令和逃逸变量都转化为内存存取指令。

void fixStack(Function &F) {
    vector<PHINode*> origPHI;
    vector<Instruction*> origReg;
    BasicBlock &entryBB = F.getEntryBlock();
    // 搜索PHI指令和逃逸变量添加到对应vector容器
    for(BasicBlock &BB : F){
        for(Instruction &I : BB){
            if(PHINode *PN = dyn_cast<PHINode>(&I)){
                origPHI.push_back(PN);
            }else if(!(isa<AllocaInst>(&I) && I.getParent() == &entryBB) 
                && I.isUsedOutsideOfBlock(&BB)){
                origReg.push_back(&I);
            }
        }
    }
    for(PHINode *PN : origPHI){
        DemotePHIToStack(PN, entryBB.getTerminator());
    }
    for(Instruction *I : origReg){
        DemoteRegToStack(*I, entryBB.getTerminator());
    }
}

0x03 参考链接

https://security.tencent.com/index.php/blog/msg/112

https://www.kanxue.com/book-88-2111.htm

https://github.com/bluesadi/Pluto-Obfuscator/blob/kanxue/Transforms/src/Flattening.cpp

https://github.com/obfuscator-llvm/obfuscator/blob/llvm-4.0/lib/Transforms/Obfuscation/Flattening.cpp

https://www.52pojie.cn/thread-1369130-1-1.html

0x01 LLVM环境搭建

该小节环境搭建和原版OLLVM混淆使用无关,仅为后续LLVM PASS编写做准备,如仅需要进行OLLVM混淆该小节可跳过

我选择的环境为Ubuntu 18.04、LLVM 12.0.1、CMake 3.16.6

先下载LLVM和Clang的源码并利用CMake进行源码编译

https://github.com/llvm/llvm-project/releases/tag/llvmorg-12.0.1

image-20220706151310564

image-20220706151325543

因为Ubuntu 18.04利用包管理器默认安装的CMake最高版本达不到LLVM编译需求,所以我们需要自行安装,具体命令如下:

wget http://www.cmake.org/files/v3.16/cmake-3.16.6.tar.gz
tar xf cmake-3.16.6.tar.gz
cd cmake-3.16.6
sudo apt-get install build-essential
sudo apt-get install libssl-dev
sudo chmod -R 777 cmake-3.16.6
./bootstrap
make
sudo make install
cmake --version

然后如下图创建build、llvm、clang目录,将llvm和clang对应源码放入目录中,后面来编写build.sh文件

image-20220706152104662

cd build
cmake -G "Unix Makefiles" -DLLVM_ENABLE_PROJECTS="clang" \
-DCMAKE_BUILD_TYPE=Release -DLLVM_TARGETS_TO_BUILD="X86" \
-DBUILD_SHARED_LIBS=On ../llvm
make
make install

最后给予build.sh执行权限并执行等待一段时间后就编译好了,可以通过clang --version进行验证是否编译成功,在编译的时候最好给虚拟机大一点的运行内存避免发生未知意外

LLVM编译基本命令

LLVM IR 有两种表现形式,一种是人类可阅读的文本形式,对应文件后缀为 .ll ;另一种是方便机器处理的二进制格式,对应文件后缀为 .bc 。

clang -S -emit-llvm hello.cpp -o hello.ll

clang -c -emit-llvm hello.cpp -o hello.bc

使用 opt 指令对 LLVM IR 进行优化

opt -load LLVMObfuscator.so -hlw -S hello.ll -o hello_opt.ll

-load 加载特定的 LLVM Pass (集合)进行优化(通常为.so文件)
-hlw 是 LLVM Pass 中自定义的参数,用来指定使用哪个 Pass 进行优化

从 LLVM IR 到可执行文件中间还有一系列复杂的流程,Clang 帮助我们整合了这个过程:

clang hello_opt.ll -o hello

0x02 Obfuscator-LLVM环境搭建

为避免出现冲突,这一节我是专门拿了个新的Ubuntu虚拟机进行操作的

使用 nickdiego/ollvm-build这个docker环境进行OLLVM源码编译,docker的安装参考网络上教程即可

docker pull nickdiego/ollvm-build

然后将源码下载下来

git clone https://github.com/nickdiego/docker-ollvm.git
git clone -b llvm-4.0 https://github.com/obfuscator-llvm/obfuscator.git

在ollvm-build.sh的第150行添加DOCKER_CMD+=" -DLLVM_INCLUDE_TESTS=OFF"

image-20220707142910600

最后执行ollvm-build.sh

chmod 777 ollvm-build.sh
sudo ./ollvm-build.sh ../obfuscator

编译完成后在 obfuscator/build_release 目录执行指令创建硬链接

sudo ln ./bin/* /usr/bin/
clang --version

OLLVM基本用法

使用的demo(hello.cpp)

#include <stdio.h>
#include <stdlib.h>

int encryptFunc(int inputNum_1,int inputNum_2){
    int tmpNum_1 = 666, tmpNum_2 = 888, tmpNum_3 = 777;
    return tmpNum_1 ^ tmpNum_2 + tmpNum_3 * inputNum_1 - inputNum_2;
}

int main(int argc,char *argv[]){

    int printNum = 55;
    if (argc > 1)
    {
        printNum = encryptFunc(printNum, atoi(argv[1]));
    }else{
        printNum = encryptFunc(printNum, argc);
    }
    
    printf("Hello OLLVM %d\r\n", printNum);

    return 0;
}

控制流平坦化(Control Flow Flattening)

可用选项:

  • -mllvm -fla : 激活控制流平坦化
  • -mllvm -split : 激活基本块分割
  • -mllvm -split_num=3 : 指定基本块分割的数目
clang -mllvm -fla -mllvm -split -mllvm -split_num=3 hello.cpp -o hello_fla

image-20220707155342174

如果提示stdio.h头文件找不到可以尝试下载g++和gcc,如果提示stddef.h或者stdarg.h头文件找不到可以sudo find -L /usr -name "*stddef*" -type f将其复制到 /usr/include目录下

image-20220707155253075

虚假控制流(Bogus Control Flow)

可用选项:

  • -mllvm -bcf : 激活虚假控制流
  • -mllvm -bcf_loop=3 : 混淆次数,这里一个函数会被混淆3次,默认为 1
  • -mllvm -bcf_prob=40 : 每个基本块被混淆的概率,这里每个基本块被混淆的概率为40%,默认为 30 %
clang -mllvm -bcf -mllvm -bcf_loop=3 -mllvm -bcf_prob=40 hello.cpp -o hello_bcf

image-20220707160139661

指令替换(Instruction Substitution)

可用选项:

  • -mllvm -sub : 激活指令替代
  • -mllvm -sub_loop=3 : 混淆次数,这里一个函数会被混淆3次,默认为 1次
clang -mllvm -sub -mllvm -sub_loop=3 hello.cpp -o hello_sub

image-20220707160516874

通过LLVM IR生成多平台可执行文件,以控制流平坦化为例

clang -mllvm -fla -mllvm -split -mllvm -split_num=3 -S -emit-llvm hello.cpp -o hello_fla.ll

# 切换至Windows(提前安装好clang)
clang hello_fla.ll -o hello_fla.exe

0x03 去混淆

可以参考TSRC的那篇文章使用符号执行脚本deflat的方式进行去混淆,对于指令替换可以使用D810这个IDA插件进行操作,详细的去混淆方法后续文章在深入讨论。

参考链接

https://www.cnblogs.com/jsdy/p/12689470.html

https://www.kanxue.com/book-section_list-88.htm

https://blog.wuxu92.com/stdargs.h-no-such-file-or-directory/

https://security.tencent.com/index.php/blog/msg/112

https://github.com/joydo/d810