使用LLVM Obfuscator / constexpr / PEB调用API 进行程序混淆

一直用这个东西编译WG, 现在想想, 用来免杀应该也不错
Obfuscation

先看一下效果

show.gif


使用LLVM Obfuscator进行混淆

1. 介绍

地址: llvm-obfuscator

llvm-obfuscator编译比较复杂了, 可以参考llvm官网的 [编译教程](https://llvm.org/docs/GettingStartedVS.html), 测试VS2017下编译通过 (CPU一直100%跑了两小时....

2. 使用方法

llvm-obfuscator的话, 主要是在生成的时候做一个混淆和花指令处理,
相应的参数如下

-mllvm -bcf -mllvm -bcf_loop=4 -mllvm -bcf_prob=100 -mllvm -sub -mllvm -sub_loop=2

已经做成VS配置了, 直接复制 Platforms
YOUR_PATH\Microsoft Visual Studio\2017\Community\Common7\IDE\VC\VCTargets\Platforms
目录下面,然后在VisualStudio里面编译器选择LLVM

vs_config.jpg

3. 坑

使用的时候还是有不少坑的,首先需要关闭 "符合模式",
然后新建的所有 源文件和头文件, 记得设置成UTF-8-BOM格式(VS默认UTF-16-LE)

vs_config2.jpg
vs_config3.jpg


使用 constexpr 加密字符串

1. 介绍

关于 constexpr 的介绍可以看这里:
http://zh.cppreference.com/w/cpp/language/constexpr
我们的目标是使用 constexpr 将所有的字符串在编译的时候进行加密,保证程序中看不到丝毫的字符串

2. 使用方法

首先引入这个头文件
obfuscation.h
然后将你的字符串用这种形式标注

MessageBoxA(0, XorString("World!"), XorString("Hello"), MB_OK);
MessageBoxW(0, XorStringW(L"World!"), XorStringW(L"Hello"), MB_OK);

我们来对比一下源码和ida的结果

before_constexpr.jpg

after_constexpr.jpg

加密好像有点简单?
再来看一下 llvm + constexpr 的结果

after_constexpr_llvm.jpg

ida还是能看到MessageBoxA的调用啊,没关系,还有下一步


通过PEB调用API

1. 介绍

这个方法可以说是非常 "邪门歪道" 了
先介绍一下相关概念,
线程信息块(TIB)
TIB是一个内部未记录/非官方的Windows数据结构,其中包含有关进程当前正在运行的线程的信息,并且该信息位于用户空间内存中。对于一般布局,请看维基百科。在32位可执行文件中, 具有一定偏移量的选择器fs用于访问TIB,而在64位可执行文件中则使用gs选择器。如果想要一些其他操作,找到一个指向TIB的指针会方便一些。指针可以通过读取fs:[0x18]获取(x86)。可以通过TIB访问其他信息,比如当前线程ID,当前语言环境以及GetLastError()的值等(每个线程都有一个TIB) 。我们只需要TIB里面的一个字段,那就是指向Process Environment Block的指针,并且TIB中的PEB指针对于当前进程所有TIB都是相同的,因为每个进程只有一个PEB。可以通过读取fs:[0x30]获得(x86)。

// TIB: 
mov eax, [fs:0x18]
// PEB: 
mov eax, [fs:0x30]

下面的代码可能比读取fs寄存器好一些:

#if defined(_M_X64) // x64
static PTEB tebPtr = reinterpret_cast<PTEB>(__readgsqword(reinterpret_cast<DWORD_PTR>(&static_cast<NT_TIB*>(nullptr)->Self)));
#else // x86
static PTEB tebPtr = reinterpret_cast<PTEB>(__readfsdword(reinterpret_cast<DWORD_PTR>(&static_cast<NT_TIB*>(nullptr)->Self)));
#endif

现在介绍完TIB和PEB了,在介绍PEB结构体的一个成员:Ldr,Ldr包含一个 InMemoryOrderModuleList 链表,
我们需要遍历 InMemoryOrderModuleList 找到我们需要加载的dll名称,比如user32.dll,

// peb pointer
PPEB pebPtr = tebPtr->ProcessEnvironmentBlock;
// Reference point / tail to compare against, since the list is circular
PLIST_ENTRY moduleListTail = &pebPtr->Ldr->InMemoryOrderModuleList;
PLIST_ENTRY moduleList = moduleListTail->Flink;
//Traverse the list until moduleList gets back to moduleListTail
do {
    char* modulePtrWithOffset = (char*)moduleList;
    PLDR_DATA_TABLE_ENTRY module = (PLDR_DATA_TABLE_ENTRY)modulePtrWithOffset;
    void *funcPtr = nullptr;
    void* DllBase = module->Reserved2[0];
    if (!dllName || _wcsicmp(module->FullDllName.Buffer, dllName) == 0)
        // find !
}

之后,有了DLL的基地址,可以解析Image的DOS和PE头来获取DLL导出表,然后遍历导出表,获取函数的地址。

PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)moduleBase;
PIMAGE_NT_HEADERS headers32 = (PIMAGE_NT_HEADERS)((char*)moduleBase + dosHeader->e_lfanew);
DWORD EdtOffset = headers32->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;

PEXPORT_DIRECTORY_TABLE EdtPtr = (PEXPORT_DIRECTORY_TABLE)((char*)moduleBase + EdtOffset);
PVOID OrdinalTable = (PBYTE)moduleBase + EdtPtr->OrdinalTableRVA;
PVOID NamePointerTable = (PBYTE)moduleBase + EdtPtr->NamePointerTableRVA;
PVOID ExportAddressTable = (PBYTE)moduleBase + EdtPtr->ExportAddressTableRVA;
// 导出表

for (DWORD i = 0; i < EdtPtr->NamePointerTableSize; i++) {
    DWORD NameRVA = ((PDWORD)NamePointerTable)[i];
    const char* NameAddr = (char*)moduleBase + NameRVA;
    // 比较API名称, 比如MessageBoxA等等

}

完整代码可以参考
obfuscation.h

2. 使用

使用起来就比较简单了,只需要引入第二步的头文件,然后像这个样子调用API

// 方式1
IFN(MessageBoxA)(0, XorString("hello"), XorString("msg"), MB_OK);
// 方式2
IFN_DLL(XorStringW(L"user32.dll"), MessageBoxA)(0, XorString("hello"), XorString("msg"), MB_OK);

为什么要有方式二呢,因为这个调用API方式原理是用函数的名称当做HASH(防止函数名称泄露), 遍历DLL寻找导入表比较HASH,
但是部分DLL里面会错误,
比如 Kernel32.dll 里面有一个 InitializeProcThreadAttributeList,但是是错误的,必须指定DLL名称为 KernelBase.dll

IFN_DLL(XorStringW(L"KernelBase.dll"), InitializeProcThreadAttributeList)(pAttr, 1, 0, &cbSize);

还有一种方式,比如你需要一个API函数指针而不是直接调用,可使用IFN_PTR获取指针

void *_NtSetInformationObject = IFN_PTR(NtSetInformationObject);

同样可以指定DLL

void *_NtSetInformationObject2 = IFN_PTR_DLL(XorStringW(L"ntdll.dll"),NtSetInformationObject);

最后看一下ida的结果(未使用llvm-obfuscator)

int main() {
    IFN(LoadLibraryA)(XorString("user32.dll"));
    IFN(MessageBoxA)(0, XorString("World!"), XorString("Hello"), MB_OK);
    return 0;
}

after_peb.jpg

3. 坑

这个方法同样有坑,程序怎么崩溃了啊???
再次回顾方法原理 TIB -> PEB -> Ldr -> InMemoryOrderModuleList
MessageBoxA在 user32.dll 里面,我们的程序没有依赖user32.dll,所以运行的时候 InMemoryOrderModuleList 里面是没有user32.dll的!
需要手动在程序最开始的时候加载一下

IFN(LoadLibraryA)(XorString("user32.dll"));


总结

当然是 LLVM + constexpr + PEB 全都用上啊!!!




参考:
https://stackoverflow.com/questions/7270473/compile-time-string-encryption
http://pimpmycode.blogspot.no/2015/01/win32-hacks-loading-api-functions-from.html
http://pimpmycode.blogspot.no/2015/01/win32-hacks-loading-api-functions-from_4.html

标签: none

已有 12 条评论

  1. Test Test

    llvm-obfuscator编译 完了以后,怎么配置呢?

    1. 编译完成之后需要在VS里面选择自定义的编译器, 我已经写成配置了
      https://github.com/Tai7sy/vs-obfuscation/tree/master/vs_config

      1. Test Test

        有没有详细点儿llvm的编译步骤啊,一直编译失败,编译一次几个小时,有点儿累。尝试了两次,未成功编译完,所有的。

        1. https://llvm.org/docs/GettingStartedVS.html

          1. Test Test

            "Unknown endianness of the compilation platform, check this header aes_encrypt.h"
            每次都报这个错误。不知道博主有解决方法没有。

  2. Test Test

    在LLVMipo 和 LLVMObfuscation里添加预定义 ENDIAN_LITTLE 编译过了

    1. Coder Coder

      我遇到了同样的问题,请问大佬如何设置预定义宏的?谢谢~

  3. 橘

    大佬,话说我添加完config之后只有平台LLVM-vs2017,没有工具集

    1. 橘

      加错目录了...
      现在有工具集了,但是编译的时候llvm的选项都被忽略了...
      https://i.imgur.com/12mKMmT.png
      另外我编译的llvm-obfuscator,只有bin和lib,没有include

  4. 菜鸡 菜鸡

    llvm-obfuscator编译后只有bin和lib,跟楼上问题一样....

  5. trES trES

    都好..就是编译速度??..hello world 很快,但是别的项目 就慢的要命.. 半个小时都不行..
    出问题了么

    1. ss ss

      一个for循环,编译了1个多小时,没有结束,正常么,始终编译不成功

添加新评论