首页
论坛
课程
招聘
如何优雅的在 Windows 上使用 LLVM Pass 插件进行代码混淆
2022-4-17 19:19 8396

如何优雅的在 Windows 上使用 LLVM Pass 插件进行代码混淆

2022-4-17 19:19
8396

如何优雅的在 Windows 上使用 LLVM Pass 插件进行代码混淆

1. 背景

LinuxAndroid上使用 LLVM 进行代码混淆已经非常方便了,网上也有很多相关的文章(比如 【LeadroyaL】 这位大佬的一系列关于 LLVM 的文章),但是在 Windows 上目前还没有找到一种优雅的方式使用 LLVM 进行代码混淆,本文的目的就是为了这个不足,让 LLVMWindows 上也能优雅的动态加载外部插件进行代码混淆。

2. 前提准备

2.1 安装 VS2019Pro 【可选】

Microsoft Visual Studio Professional 2019 (简称 VS2019Pro 或者 VS) 作为 Windows 上主要的开发工具,我们需要提前安装一份,当然,如果你不是使用 VS2019Pro 开发,那这个步骤也是可以省略的,可以不用安装 VS2019Pro,现在安装 VS2019Pro 是为了演示在 VS2019Pro 中使用 LLVM Pass 插件。本文所使用的 VS2019Pro 版本是 16.3.6 (对应的 LLVM 版本是 8.0.1),安装 VS2019Pro 的过程如下所示:

 

打开 VS2019Pro 的安装程序,在 【工作负载】 页面默认会选中 【使用 C++ 的桌面开发】,保持默认即可:

 

 

切换到 【单个组件】 页面,在搜索框中输入 clang ,把搜索出来的两项打上勾。
添加【clang】工具集

 

【安装位置】可以自己选择,不过 【下载缓存】最好指向自己安装包解压出来的位置,可以避免浪费磁盘空间。
指定安装位置【可选】

 

【语言包】可以自由选择,然后就可以正常安装 VS2019Pro

 

本文提供的 VS2019Pro 安装包:【VS2019Pro_WithLLVM8.0.1.7z】原文链接

2.2 安装 MSYS2

要想在 Windows 上编译并使用 LLVM,首先必须要有编译 LLVM 的工具,通常情况下,我们都会选择 VS 作为 Windows 平台上的编译工具,但是由于 VS 编译出来的 LLVM 不支持动态加载插件(可以在编译日志中看到这样的输出信息: LLVMHello ignored -- Loadable modules not supported on this platform.),因此,我们不使用 VS 作为编译 LLVM 的工具。在本文中,我们选用的编译工具是 【MSYS2】,本文所使用的 MSYS2 版本是 【msys2-x86_64-20220319.exe】(该版本的 MSYS2 对应的 GCC 版本是 11.2.0,当然,使用基于 GCC-10.3.0 版本的 MSYS2 也是可以的)。

 

安装过程中,到达 66% 的时候会出现卡住的现象(会停留在 Updating trust database... 的时候),需要等久一点:

 

MSYS2安装包

 

安装好 MSYS2 之后,打开 MSYS2 MSYS 快捷方式,如下所示:

 

打开 【MSYS2 MSYS】 快捷方式

 

并在 MSYS2 中安装对应的编译工具 -- GCCG++ 等,安装命令如下所示:

1
2
3
# 为了方便,我们一次性把 x86 和 x64 的编译工具链都装上去
# 如果安装过程中出现错误,则只需要再次执行该命令,直到安装成功
pacman -S --needed base-devel mingw-w64-x86_64-toolchain mingw-w64-i686-toolchain cmake

在 MSYS2 中安装编译工具链
---- 分割线 ----
在 MSYS2 中安装编译工具链
---- 分割线 ----
在 MSYS2 中安装编译工具链

 

至此,整套 MSYS2 的编译工具链都已经安装完成。

 

本文提供的 MSYS2 安装包:【msys2-x86_64-20220319_GCC_11.2.0.exe.7z】,以及安装好 GCC 编译工具链的压缩包(开箱即用):【MSYS2-GCC11.2.0_x86_x64.7z】

2.3 安装 CMake

除了准备 Vs2019ProMSYS2 之外,还需要安装 CMake,以此来编译 LLVMCMake 的安装流程比较简单,直接在官网上 【https://cmake.org/download/】 下载安装即可(本教程中安装的是 CMake 3.18.2 版本)。

 

安装 CMake

 

安装 CMake 的过程中需要注意的点:就是在安装的时候记得勾上 Add CMake to the system PATH for all users 或者 Add CMake to the system PATH for current user。这样可以在 cmd 中直接使用 CMake

 

本文提供的 CMake 安装包:【cmake-3.18.2-win64-x64.msi.7z】

3. 编译动态库版本 LLVM

使用 VS2019Pro 编译出来的 LLVM,我们称之为静态库版本,因为编译出来的 clang.exe 是可以单独使用的,没有外部依赖,而本文所要编译的 LLVM 是属于动态库版本,因此,编译出来的 clang.exe 不能单独使用,需要有相应的依赖库才能使用。

 

本文提供的 LLVM 源码安装包:【src_llvm8_patched_for_win10.7z】,该压缩包中的 LLVM 源码已经被 Patched,可以直接在 Windows 上使用 MSYS2 中的 GCC 编译。

3.1 下载并解压 LLVM 源码

本文所使用的 LLVM 版本是 LLVM-8.0.0(当然,如果使用 LLVM-12.0.0.0 也是可以的,但是需要更新 VS2019 的版本,比如使用 VS2019Pro v16.11.5 这个版本,这个版本的对应的 LLVM 就是 12.0.0),LLVM-8.0.0 的源码下载地址可以从这里下载:【LLVM-8.0.0 下载地址】,当然,也可以使用 wget 下载:

1
2
3
4
5
6
7
8
9
https://releases.llvm.org/8.0.0/cfe-8.0.0.src.tar.xz
https://releases.llvm.org/8.0.0/libcxx-8.0.0.src.tar.xz
https://releases.llvm.org/8.0.0/clang-tools-extra-8.0.0.src.tar.xz
https://releases.llvm.org/8.0.0/compiler-rt-8.0.0.src.tar.xz
https://releases.llvm.org/8.0.0/libcxx-8.0.0.src.tar.xz
https://releases.llvm.org/8.0.0/libcxxabi-8.0.0.src.tar.xz
https://releases.llvm.org/8.0.0/lld-8.0.0.src.tar.xz
https://releases.llvm.org/8.0.0/lldb-8.0.0.src.tar.xz
https://releases.llvm.org/8.0.0/llvm-8.0.0.src.tar.xz

这里的代码,并不需要下载全部,因为即使下载了,在 Windows 上也编译不了,现在我们只需要下载在 Windows 上可以编译的几个部分(必要的部分)就可以了,也就是 cfe-8.0.0.src.tar.xzllvm-8.0.0.src.tar.xzlld-8.0.0.src.tar.xz,如下图所示:

 

LLVM8 源码

 

下载好这些源码之后,解压出来,并对它们进行重命名,方便后续管理,重命名规则如下所示:

  • cfe-8.0.0.src.tar.xz -> clang
  • llvm-8.0.0.src.tar.xz -> llvm
  • lld-8.0.0.src.tar.xz -> lld

重命名之后,本小节的任务就完成了,如下所示:

 

LLVM8 源码(重命名之后)

3.2 修改 LLVM 源码

原始的 LLVM 源码在 Windows 上并不能直接编译,如果直接编译,会出现很多问题。现在需要对这些代码就行修改,使得修改后的代码可以在 Windows 上编译,所需要修改的地方有 5 个文件,如下所示:

3.2.1 修改 clang\lib\Driver\ToolChains\Clang.cpp

在这个文件中需要注释掉第 3729 行开始的 4 行代码,如下所示:

 

注释掉4行代码

 

这几行代码会在编译 Release 模式的 Pass 的时候,无法获取到 BasicBlock 的名字,但有时候我们需要保留 BasicBlock 的名字,所以把这几行代码注释掉,但是在 LinuxMacOS 上不会出现这个问题。

3.2.2 修改 llvm\lib\CMakeLists.txt

在这个文件中,只需要把第 30 行的 add_subdirectory(Testing) 这行注释掉即可,如下所示:

 

注释掉Testing代码

3.2.3 修改 llvm\include\llvm\Demangle\MicrosoftDemangleNodes.h

在这个文件中,需要在文件的第 8 行代码处手动添加几个头文件,被添加的头文件为:stdio.hstdlib.hstring,如下所示:

 

手动添加头文件

3.2.4 修改 lld\COFF\Writer.cpp

把这个文件中出现的两处 for_each(parallel::par 都修改为 for_each(parallel::seq,第一处在该文件的第 1071 行,第二处在该文件的第 1577 行,如下图所示:

 

修改并行为串行

3.2.5 修改 lld\include\lld\Common\Threads.h

把这个文件中在 71 行出现的 llvm::parallel::par 修改为 llvm::parallel::seq,如下所示:

 

修改并行为串行

3.3 编译 LLVM 源码

编译 LLVM 可以按照需要编译不同的版本,即 x64 版本和 x32 版本,但是一般情况下,只需要 x64 版本即可。

3.3.1 编译 x32 版本的 LLVM

修改好 LLVM 源码之后,就可以开始编译步骤了,有了前面的基础,编译步骤就变得很简单了,首先打开 cmd(记得是打开 Windows 上纯正的 cmd 命令行程序,而不是 PowerShell 等变种工具,因为我们还需要修改被打开的 cmd 的环境变量),打开 cmd 之后,进入到 LLVM 源码的 同级 目录中(示例中是: E:\LLVM\llvm8),并把前面安装的 MSYS2 编译工具所在的路径添加在 cmd 的环境变量中(笔者前面安装的 MSYS2 路径是:C:\MyProgramFiles\MSYS2),现在假设我们先编译一份 x32 版本的 LLVM,在源码的同级目录下执行如下命令:

1
2
3
4
5
6
7
8
9
10
:: 设置 MSYS2 的 gcc x32 环境变量到 cmd 中
set PATH=%PATH%;C:\MyProgramFiles\MSYS2\mingw32\bin
:: 然后执行 gcc --version 查看 gcc 版本,并确保该命令能正常输出 gcc 的版本号
gcc --version
:: 接下来执行 LLVM 的 config 命令,使之生成 LLVM 的 Makefile 文件
:: 这里要注意两个参数:【-DBUILD_SHARED_LIBS=ON】 和 【-DLLVM_ENABLE_DUMP=ON】
cmake -G "MinGW Makefiles" -S ./src/llvm -B ./build_dyn_x32  -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_PROJECTS="clang;lld;" -DLLVM_TARGETS_TO_BUILD="X86" -DBUILD_SHARED_LIBS=ON -DLLVM_INCLUDE_TESTS=OFF -DLLVM_BUILD_TESTS=OFF -DLLVM_INCLUDE_BENCHMARKS=OFF -DLLVM_BUILD_BENCHMARKS=OFF -DLLVM_ENABLE_DUMP=ON
:: 最后执行编译命令
:: 根据自己机器的核心数量,设置 -j 后面的参数,4 表示使用 4 个线程进行并行编译
cmake --build ./build_dyn_x32 -j 4

配置 LLVM8 x32 源码

 

配置完成之后,就可以执行编译命令了,假设我们现在的编译目录是 build_dyn_x32,执行编译命令 cmake --build ./build_dyn_x32 -j 4 之后,系统就开始帮我们编译 LLVM 了,如下所示:

 

配置完成并开始编译 LLVM8 源码

 

经过一段时间的等待(笔者的机器比较老,编译过程花费了两个小时左右),就可以看到编译完成的日志了,如下所示:

 

编译完成 LLVM8 源码

3.3.2 编译 x64 版本的 LLVM

当然,上一小节演示的是编译 32 位的 LLVM,而我们一般也会需要编译 64 位版本的 LLVM,编译命令和 32 位基本一样,区别只有如下两点:

  • 指定的 MSYS2 编译工具的路径不一样,编译 x32 版本的 LLVM 的时候指定的是 set PATH=%PATH%;C:\MyProgramFiles\MSYS2\mingw32\bin,而编译 x64 版本的 LLVM 的时候指定的是 set PATH=%PATH%;C:\MyProgramFiles\MSYS2\mingw64\bin
  • 在编译和配置的时候指定不同的 BUILD 目录,编译 x32 版本的 LLVM 的时候指定的是 ./build_dyn_x32,而编译 x64 版本的 LLVM 的时候指定的是 ./build_dyn_x64,如下所示:

配置 LLVM8 x64 源码

3.3.3 x64 和 x32 版本 LLVM 的区别

对于 VS2019Pro 版本是 16.3.6 的情况下,VS2019Pro 会使用 LLVM8x64 版本来编译程序,不管 VS2019Pro 编译的目标程序是 x86 还是 x64,它都统一使用 LLVM8x64 版本来编译程序。

 

但是,对于 16.11.5 版本的 VS2019Pro,它会使用 LLVM12x64 版本来编译 x64 程序,使用 LLVM12x32 版本来编译 x86 程序。

 

因此,如果想要 Patch 16.3.6 版本的 VS2019Pro,只需要编译一份基于 x64LLVM8 即可,如果想要 Patch 16.11.5 版本的 VS2019Pro,则需要编译一份基于 x64LLVM12 和一份基于 x32LLVM12

 

编译好 x32x64 版本的 LLVM 之后,在 LLVM 源码的同级目录下会生成两个版本的 BUILD 目录,如下所示(原文出处):

 

编译好 x32 和 x64 的 LLVM8

3.4 安装 LLVM 到指定位置

编译完成之后,开始安装 LLVM 到指定目录,我们可以使用 CMake 的参数 -DCMAKE_INSTALL_PREFIX=C:\LLVM8 来指定安装目录,笔者以安装 x64 版本的 LLVM 为例,指定的安装目录是:C:\MyProgramFiles\LLVM\llvm8\llvm8_dyn_x64,然后执行如下所示命令进行安装:

1
2
3
cd /d E:\LLVM\llvm8
set PATH=%PATH%;C:\MyProgramFiles\MSYS2\mingw64\bin
cmake -DCMAKE_INSTALL_PREFIX=C:\MyProgramFiles\LLVM\llvm8\llvm8_dyn_x64 -P .\build_dyn_x64\cmake_install.cmake

安装 LLVM8 x64 版本

 

安装好之后,到安装目录下查看已安装的 LLVM,发现大小只有 231MB,体积相对于使用 VS2019Pro 直接编译出来的 LLVM 版本是小很多的,如下所示:

 

安装 LLVM8 x64 版本

3.5 给 LLVM 打上补丁

动态编译出来的 LLVM 工具是不能直接使用的,需要给它添加对应的依赖库,或者把依赖库所在的路径添加到系统的环境变量中。

3.5.1 添加 .dll 动态库文件

安装好 LLVM 之后,我们可以打开 cmd 进入安装目录,并尝试运行 .\bin\clang.exe --version 查看 clang 能不能正常工作,但是很遗憾,由于缺少必要的 dll 文件,暂时还不能运行,如下所示:

 

直接运行 clang.exe 报错

 

这是因为我们的 LLVM 是使用 MSYS2 里面的 GCC 编译的,并且编译的模式还是 动态编译,因此,我们编译出来的 LLVM 还会依赖 GCC 的部分库文件,如果我们想要让 LLVM 能正常运行,最简答的方法是直接添加 MSYS2 工具链的路径到环境变量中,如下所示:

 

添加 MSYS2 路径到环境变量中之后运行 clang.exe 可正常运行

 

当然,也可以把这个 MSYS2 工具链的路径到系统的环境变量中。

 

另一个解决方案是把这几个缺失的库手动拷贝到 LLVM 安装目录下的 bin 目录中,为了方便,先把这几个缺失的库拷贝到 LLVM 安装目录下的 deps 目录中(自己创建该目录),然后再从这个目录中拷贝到 bin 目录。经过研究发现,x64 版本的 LLVM 缺失的库包括如下 4 个,并且这几个库都在目录 C:\MyProgramFiles\MSYS2\mingw64\bin 中:

  • libstdc++-6.dll
  • libgcc_s_seh-1.dll
  • libwinpthread-1.dll
  • zlib1.dll

Patch GCC 中的依赖库

 

把这几个 dll 依赖库拷贝到 LLVM 安装目录下的 bin 目录中之后,再次打开 cmd,进入安装目录之后,直接运行 .\bin\clang.exe --version,此时已经可以正常运行,如下所示:

 

Patch 之后可以直接运行 clang.exe

 

注意:
如果是 x32 版本的 LLVM,所需要的依赖库要从目录 C:\MyProgramFiles\MSYS2\mingw32\bin 中查找,并且依赖库的名称和 x64 版本的不完全一样, x32 版本的 LLVM 所需要的依赖库如下所示:

  • libstdc++-6.dll
  • libgcc_s_dw2-1.dll (这个库名称不一样)
  • libwinpthread-1.dll
  • zlib1.dll

3.5.2 添加 .a 静态库文件

添加完以上几个 dll 动态库文件之后,我们已经可以正常的使用 LLVM 来编译自己的程序了,但是如果想要使用 LLVM 加载外部 Pass 插件,则还会出现问题,会出现 符号缺失 或者 找不到符号 等类似的错误,为了解决这类错误,我们还需要手动添加一些静态库 .a 文件(LLVM 编译目录下生成的静态库文件)到 LLVM 的安装目录中。

 

从下图中我们可以看到,LLVM 的编译目录下的 lib 目录中总共有 108 个静态库文件,而 LLVM 的安装目录下的 lib 目录中总共只有 41 个静态库文件,而我们如果想要让 LLVM 能正常加载外部 Pass 插件,我们还需要把这些静态库文件从编译目录拷贝到安装目录下:

 

安装目录和编译目录下的lib比较

 

直接把所有的静态库文件从编译目录拷贝到安装目录下对应的文件夹中即可,如下所示:

 

安装目录和编译目录下的lib比较

 

至此,LLVMPatch 工作已经全部完成,此时 LLVM 安装目录下就是一份完整的 LLVM 编译工具链。

4. 使用

到了这个步骤,使用 LLVM 的方法就和 Linux 中使用的方法一致了,也可以让 LLVM 编译和加载任意的 Pass 插件,下面我们就以一个简单的 mydemo Pass 插件为例进行说明:

4.1 编译 LLVM Pass 插件

我们的 mydemo Pass 插件包含 4 个文件:

  • mydemo.cpp -- Pass 的主体文件
  • mydemo.h -- Pass 的头文件
  • CMakeLists.txt -- CMake 的配置文件
  • cmake_build.bat -- 编译 Pass 的脚本文件

本文提供的 mydemo 插件源码:【mydemo.7z】

4.1.1 mydemo.cpp 文件

mydemo.cpp 文件的核心代码如下所示:

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
41
42
43
44
45
46
47
48
49
50
#include "mydemo.h"
namespace
{
    // 定义我们的 Pass 类
    class MyDemoPass : public ModulePass
    {
    public:
        static char ID// Nessessary attribute
        MyDemoPass();    // Nessessary function
        ~MyDemoPass();   // Optional function
        virtual StringRef getPassName() const override;       // Optional function
        virtual bool doInitialization(Module &M) override;    // Optional function
        virtual bool doFinalization(Module &M) override;      // Optional function
        virtual bool runOnModule(Module &M) override;         // Nessessary function
    };
    char MyDemoPass::ID = 1;    // Nessessary initialization, however, the value can be an arbitary integer
    // ...
}
// ...
// Pass 的入口函数
bool MyDemoPass::runOnModule(Module &M)
{
    string funcName = "";
    string modName = "";
    modName = M.getName().str();
    MyPrint("Running core function [runOnModule] of the pass [MyDemoPass] for module [" + modName + "]!");
    // Print the function's name
    for(Function &F : M)
    {
        funcName = F.getName().str();
        MyPrint("Got a function [" + funcName + "]!");
    }
    return false;
}
// ...
// 注册 Pass 代码:
// Register module pass
void RegisterMyModulePass(const PassManagerBuilder &PMB, legacy::PassManagerBase &PM)
{
    PM.add(new MyDemoPass());
}
// For loading ModulePass at different optimization level
// [1]. Trigger at flag: `-O1` or `-O2` or `-O3`
// static RegisterStandardPasses RegisterMyPassVar1 (PassManagerBuilder::EP_ModuleOptimizerEarly, RegisterMyModulePass);
// [2]. Trigger at flag: `-O1` or `-O2` or `-O3`
// static RegisterStandardPasses RegisterMyPassVar2 (PassManagerBuilder::EP_OptimizerLast, RegisterMyModulePass);
// [3]. Trigger at flag: `-O0` or `default`
static RegisterStandardPasses RegisterMyPassVar3(PassManagerBuilder::EP_EnabledOnOptLevel0, RegisterMyModulePass);
// [4]. Trigger at flag: `-O1` or `-O2` or `-O3`
static RegisterStandardPasses RegisterMyPassVar4 (PassManagerBuilder::EP_ModuleOptimizerEarly, RegisterMyModulePass);

4.1.2 mydemo.h 文件

mydemo.h 头文件:

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
#ifndef __MYDEMO_H__
#define __MYDEMO_H__
// #include <iostream>
#include <string>
#include <stdlib.h>
#include <vector>
#include <set>
#include <llvm/Pass.h>
#include <llvm/Support/raw_ostream.h>
#include <llvm/IR/Function.h>
#include <llvm/IR/Module.h>
#include <llvm/Transforms/IPO/PassManagerBuilder.h>
#include <llvm/IR/LegacyPassManager.h>
#include <llvm/IR/IRBuilder.h>
#include <llvm/IR/LLVMContext.h>
#include <llvm/IR/IntrinsicInst.h>
#include <llvm/IR/Intrinsics.h>
#include <llvm/IR/IRBuilder.h>
#include <llvm/Demangle/Demangle.h>
using namespace llvm;
using namespace std;
namespace
{
    class MyDemoPass;
    void MyPrint(std::string str, bool isAddWhiteLine = false);
    void MyPrintLine(uint64_t lineSize = 100, char c = '=');
    void MyPrintEx(std::string str);
    void MyAbort(std::string str);
}
#endif // __MYDEMO_H__

4.1.3 CMakeLists.txt 文件

CMakeLists.txtCMake 的配置文件,主要用于配置 LLVM 相关的一些变量信息,如下所示:

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
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
project(mydemo)
cmake_minimum_required(VERSION 3.10)
 
if(NOT DEFINED ENV{LLVM_HOME})
    # User must define the LLVM_HOME environment that point to the root installation dir of llvm
    message(FATAL_ERROR "Environment variable $LLVM_HOME is not defined!")
endif()
 
message(STATUS "LLVM_HOME = [$ENV{LLVM_HOME}]")
 
if(NOT DEFINED ENV{LLVM_DIR})
    # Default llvm config file path
    set(ENV{LLVM_DIR} $ENV{LLVM_HOME}/lib/cmake/llvm)
endif()
 
# Check the path
if (NOT EXISTS $ENV{LLVM_DIR})
    message(STATUS "Path ($ENV{LLVM_DIR}) not found!")
 
    # If default llvm config path not found, try this one,
    # which is config with [-DLLVM_LIBDIR_SUFFIX=64] before building llvm
    set(ENV{LLVM_DIR} $ENV{LLVM_HOME}/lib64/cmake/llvm)
    if (NOT EXISTS $ENV{LLVM_DIR})
        message(FATAL_ERROR "Path ($ENV{LLVM_DIR}) not found!")
    else()
        message(STATUS "Path ($ENV{LLVM_DIR}) found!")
    endif()
else()
    message(STATUS "Path ($ENV{LLVM_DIR}) found!")
endif()
 
find_package(LLVM REQUIRED CONFIG)
add_definitions(${LLVM_DEFINITIONS})
include_directories(${LLVM_INCLUDE_DIRS})
link_directories(${LLVM_LIBRARY_DIRS})
 
add_library(mydemo SHARED
    # Add your source file here, header file is not neccessary
    mydemo.cpp
)
 
# Use C++11 to compile your pass (i.e., supply -std=c++11).
target_compile_features(mydemo PRIVATE cxx_range_for cxx_auto_type)
 
include_directories(./)
 
# LLVM is (typically) built with no C++ RTTI. We need to match that;
# otherwise, we'll get linker errors about missing RTTI data.
set_target_properties(mydemo PROPERTIES COMPILE_FLAGS "-fno-rtti")
 
# Get proper shared-library behavior (where symbols are not necessarily
# resolved when the shared library is linked) on OS X.
if(APPLE)
    set_target_properties(mydemo PROPERTIES LINK_FLAGS "-undefined dynamic_lookup")
endif(APPLE)
 
# 这里是 Windows 和 Linux 上的主要区别,Linux 上不需要这个步骤,
# Windows 上对于 Demo Pass,包含一下几个静态库文件即可,如果是复杂的 Pass 还需要包含额外的静态库
target_link_libraries(mydemo
    libLLVMCore.dll.a
    libLLVMSupport.dll.a
    libLLVMipo.dll.a
)

如果编写的插件比较复杂,则在 CMakeLists.txt 文件中需要再补充缺失的依赖库(使用 target_link_libraries 函数添加),下面列举一些常见的依赖库:

1
2
3
4
5
6
7
libLLVMCore.dll.a
libLLVMSupport.dll.a
libLLVMipo.dll.a
libLLVMDemangle.dll.a
libLLVMTransformUtils.dll.a
libLLVMAnalysis.dll.a
libpthread.a

一般来说,在 Windows 上添加以上几个依赖库之后,都能够正常编译大部分的插件,包括: OLLVMHikariArmariris 等。

4.1.4 cmake_build.bat 文件

cmake_build.bat 文件需要把 MSYS2 编译工具链的路径添加到环境变量中,并指定 LLVM_HOME 环境变量到 LLVM 的安装目录下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
:: Set MSYS2 env
set PATH=%PATH%;C:\MyProgramFiles\MSYS2\mingw64\bin
 
:: Set LLVM_HOME
set LLVM_HOME=C:/MyProgramFiles/LLVM/llvm8/llvm8_dyn_x64
set CC=%LLVM_HOME%/bin/clang.exe
set CXX=%LLVM_HOME%/bin/clang++.exe
 
:: Remove build dir
rd /Q /S .\build
 
:: Build release version
cmake -S ./ -B ./build -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_ASSERTIONS=ON  -G "MinGW Makefiles"
:: cmake -S ./ -B ./build -DCMAKE_BUILD_TYPE=Release -DLLVM_ENABLE_ASSERTIONS=ON  -G "MinGW Makefiles" -DCMAKE_VERBOSE_MAKEFILE=ON
 
:: Build debug version
:: cmake -S ./ -B ./build -DCMAKE_BUILD_TYPE=Debug -DLLVM_ENABLE_ASSERTIONS=ON
 
:: Build  project
cmake --build ./build -j 4
pause

编译的时候,修改好编译脚本 cmake_build.bat 文件中的 MSYS2 文件路径和 LLVM_HOME 文件路径即可,然后双击运行该脚本即可编译成功,如下所示:

 

编译 LLVM Pass 插件

4.2 在命令行中直接使用 LLVM Pass 插件

编译好 LLVMPass 插件之后,我们就可以使用这个插件了,现在我们以 test_mydemo 为例,这个测试例子包含两个文件:

  • test_mydemo.c -- 被测试的源文件
  • build.bat -- 编译脚本

本文提供的 test_mydemo 示例源码:【test_mydemo.7z】

4.2.1 test_mydemo.c 文件

test_mydemo.c 文件的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void test1()
{
    puts("Running function test1().");
}
int main()
{
    puts("Hello world.");
    test1();
    return 0;
}

4.2.1 build.bat 文件

build.bat 脚本如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
:: Set MSYS2 env
set PATH=%PATH%;C:\MyProgramFiles\MSYS2\mingw64\bin
 
:: Using GCC build:
:: gcc ./test_mydemo.c -o test_mydemo.bin
 
:: Set LLVM_HOME
set LLVM_HOME=C:/MyProgramFiles/LLVM/llvm8/llvm8_dyn_x64
set CC=%LLVM_HOME%/bin/clang.exe
set CXX=%LLVM_HOME%/bin/clang++.exe
set DEMO_PASS=E:\LLVM\pass\mydemo\build\libmydemo.dll
 
:: Using clang build without loading pass plugin
:: %CC% ./test_mydemo.c -o ./test_mydemo.exe
 
:: Using clang build with loading pass plugin
%CC% -Xclang -load -Xclang %DEMO_PASS% ./test_mydemo.c -o ./test_mydemo.exe
pause
 
:: Run exe:
.\test_mydemo.exe
pause

在编译脚本 build.bat 中,除了设置 MSYS2LLVM_HOME 这两个环境变量之外,还需要指定 LLVM Pass 插件的位置(示例中使用变量 DEMO_PASS 保存),然后再编译命令中添加 -Xclang -load -Xclang /path/to/libpass.dll 参数来指定插件所在的位置,用这样的方式编译可执行文件,在编译过程中就会自动加载 LLVM Pass 插件,如下所示:

 

编译 test_demo 并使用 Pass 插件

4.3 在 VS2019Pro 中使用 LLVM Pass 插件

学会了在命令行中直接使用 Pass 插件之后,在 VS2019Pro 中使用 Pass 插件的方法就很简单了,只需要 4 个步骤就可以了:

4.3.1 Patch VS2019Pro 中的 LLVM 编译工具链

16.3.6 版本的 VS2019Pro 中自带的 LLVM 版本是 8.0.1,因此,我们使用自己编译的 LLVM-8.0.0 版本可以直接替换它的编译工具链。打开 VS2019Pro 的安装目录(笔者的安装目录是:C:\MyProgramFiles\VS\VS2019Pro),然后进入 VC\Tools 子目录,就可以看到 VS2019Pro 自带的两套编译工具链:一套是 VC 的,另一套是 LLVM 的,如下图所示:

 

VS2019Pro自带的编译工具链

 

然后把该目录下的 Llvm 目录重命名一下(方便后续恢复),比如,把它重命名为 Llvm_org,然后再把我们自己编译的 LLVM (在 LLVM 安装目录下)拷贝过去,如下所示:

 

拷贝 LLVM 编译工具链到 VS2019Pro 中

 

最后把拷贝过来的 llvm8_dyn_x64 目录重命名为 Llvm 即可,至此,Patch VS2019Pro 中自带的 LLVM 已经完成。

4.3.2 在 VS2019Pro 的项目中指定使用 LLVM 编译工具链编译项目

PatchVS2019Pro 中自带的 LLVM 编译工具链之后,我们现在可以新建一个 VS2019demo 项目,然后打开项目属性,修改项目的【平台工具集】LLVM,如下所示:

 

修改 VS2019 项目的编译工具集

 

修改完 VS2019 项目的【平台工具集】之后,可以在项目名称上明显的看到项目名称后面跟了一个括号(LLVM - clang-cl),表明当前正在使用的是 LLVM 编译工具链,如下图所示:

 

修改完 VS2019 项目的编译工具集之后

 

此时,已经可以使用 LLVM 工具集编译项目了,只是还没有启用 Pass 插件而已,点击清理解决方案,然后再点击运行,如下图所示:

 

编译 VS2019 项目(未启用插件)

4.3.3 在 VS2019Pro 的项目中配置使用 Pass 的参数

有了前面的步骤,要在 VS2019Pro 项目中使用 Pass 插件,只需要在项目属性中添加如下参数即可:

1
2
:: 【右键项目名称】->【属性(R)】->【配置属性】->【C/C++->【命令行】->【其它选项】
-Xclang -load -Xclang E:\LLVM\pass\mydemo\build\libmydemo.dll

如下图所示:

 

配置 VS2019 项目(启用插件)

4.3.4 编译 VS2019Pro 项目

一切准备就绪,现在只需要点击【重新生成解决方案】即可看到 libmydemo.dll 插件的输出信息了,如下所示:

 

重新编译 VS2019 项目(启用插件)

 

当然,也可以先清理一下解决方案,然后直接运行项目,效果是一样的,如下所示:

 

运行 VS2019 项目(启用插件)

 

至此,在 VS2019Pro 项目中使用 Pass 插件的方法已经全部完成。

5. 总结

本文介绍了在 Windows 使用 LLVM 动态加载外部插件的方法,其实和 Linux 上的方法差不多,只是在 Windows 相对麻烦一些,不能使用 VS 编译工具来编译 LLVM,转而使用 MSYS2 中的 GCC 工具链编译,使得编译出来的 LLVM 属于动态库版本,体积相对较小,编译出来 LLVM 之后,再使用 LLVM 编译插件和使用插件都不会存在什么问题。

6. 参考文档

7. 原创说明

本文是作者原创作品,转载请注明出处,谢谢!

 

原文链接:https://bbs.pediy.com/thread-272346.htm


【公告】 [2022大礼包]《看雪论坛精华22期》发布!收录近1000余篇精华优秀文章!

最后于 2022-4-20 12:58 被NewMai编辑 ,原因:
收藏
点赞15
打赏
分享
最新回复 (26)
雪    币: 288
活跃值: 活跃值 (1212)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
kakasasa 活跃值 2022-4-17 21:15
2
0
感谢分享,收藏备用
雪    币: 6979
活跃值: 活跃值 (2947)
能力值: (RANK:1130 )
在线值:
发帖
回帖
粉丝
海风月影 活跃值 22 2022-4-17 23:56
3
1

好文章,牛逼
雪    币: 5331
活跃值: 活跃值 (955)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
option 活跃值 2022-4-18 06:05
4
0
海风月影 好文章,牛逼
大佬都点赞了
雪    币: 3358
活跃值: 活跃值 (459)
能力值: ( LV9,RANK:320 )
在线值:
发帖
回帖
粉丝
vasthao 活跃值 6 2022-4-18 06:14
5
0
当前windows使用clang/llvm插件有几个问题:
1.单个DLL导出函数有数量限制,好像是2^16个,解决这个问题需要把LLVM的各个组件都编译成DLL。
2.DLL链接问题,比如LLVM各组件相互依赖,还有各组件导出的全局变量/常量/构造函数怎么链接导入,有一种可能的方法是LLVM和新的pass插件静态编译成DLL,CLANG链接导入。
3.LLVM的pass管理要使用新版本的,LLVM版本好像要10以上才支持,新的插件机制解决pass的构造函数初始化问题。
4.当前只支持LLVM插件,CLANG插件不支持,当前CLANG的函数没有导出,即使导出了也会有以上3点问题。
5.综上所述,CLANG/LLVM如果要使用插件,操作系统不要用WINDOWS,如果是用NDK实现CLANG/LLVM插件,NDK的工具链在MAC上有一些问题,可能需要自举编译CLANG/LLVM。
雪    币: 3358
活跃值: 活跃值 (459)
能力值: ( LV9,RANK:320 )
在线值:
发帖
回帖
粉丝
vasthao 活跃值 6 2022-4-18 06:48
6
1
如果非要在Windows上实现编译器混淆插件,最好基于mingw/gcc的pass实现。gcc好像是从5版本开始,支持编译器前/中/后端插件,mingw从8版本开始,也支持了编译器前/中/端插件,主要是因为mingw/gcc大部分是c实现的,并且编译器前/中/后端没有分离,实现一站式插件比较容易,而CLANG的前端和LLVM的中后端分离,并且CLANG/LLVM是用C++实现的,导出导入函数还会有函数名修饰问题,实现一站式不容易。现在关键问题在于mingw/gcc没有成熟的开源编译器混淆插件。
雪    币: 4527
活跃值: 活跃值 (1924)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
LeadroyaL 活跃值 1 2022-4-18 10:28
7
0
强,领先时代
雪    币: 0
活跃值: 活跃值 (95)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
whitelen 活跃值 2022-4-18 14:22
8
0
牛逼了。
雪    币: 1047
活跃值: 活跃值 (918)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
wuxiwudi 活跃值 2022-4-20 10:36
9
0
不要编译x86版本的,之前试过大项目内存直接爆炸导致编译工程失败,后面全统一成x64的了,另外llvm其实可以直接用vs做编译开发,之前做到llvm11版本的,后面没再做了
雪    币: 1058
活跃值: 活跃值 (674)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
NewMai 活跃值 1 2022-4-20 11:17
10
0
vasthao 当前windows使用clang/llvm插件有几个问题: 1.单个DLL导出函数有数量限制,好像是2^16个,解决这个问题需要把LLVM的各个组件都编译成DLL。 2.DLL链接问题,比如LLV ...
你上面说的几个问题应该是使用 VS 编译的时候出现的问题,我也遇到过,比如 DLL 导出函数数量的限制,会导致编译不成功,所以才避开使用 VS 来编译 LLVM,但是使用 MSYS2 中安装的 GCC 来编译 LLVM 就可以解决这类问题,另外,你说的 Clang 插件我暂时还没试过,我只使用了 LLVM 的插件,LLVM 插件是没有问题的,我们已经有相关的项目是使用 Windows 上的混淆插件进行保护的。

另外,第5点你说的也不完全正确,我们已经在 Windows 、Linux (以及 Android) 和 iOS 上都有使用 LLVM 插件,目前也没有什么问题。
雪    币: 1058
活跃值: 活跃值 (674)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
NewMai 活跃值 1 2022-4-20 11:22
11
0
wuxiwudi 不要编译x86版本的,之前试过大项目内存直接爆炸导致编译工程失败,后面全统一成x64的了,另外llvm其实可以直接用vs做编译开发,之前做到llvm11版本的,后面没再做了
你说的没错,对于 大项目,确实不需要 x86 版本的,但是有些项目,由于某些原因,又是需要 x86 版本的,所以说, x86 版本的编译出来也没有什么不好,确实不需要的,当然可以不用编译 x86 版本,因为对于LLVM8 来说,编译 x64 就够了,但是对于 LLVM12 来说,如果要混淆基于 x86 的程序,则需要编译 x86 版本的 LLVM,原因已经在上文中 【3.3.3 x64 和 x32 版本 LLVM 的区别】小节有说明。
雪    币: 1047
活跃值: 活跃值 (918)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
wuxiwudi 活跃值 2022-4-20 11:36
12
0
NewMai 你说的没错,对于 大项目,确实不需要 x86 版本的,但是有些项目,由于某些原因,又是需要 x86 版本的,所以说, x86 版本的编译出来也没有什么不好,确实不需要的,当然可以不用编译 x86 版本 ...
其实我想说。。我编译个700kb的工程就直接内存爆掉了,尝试减小混淆粒度依然无法解决,所以就没有继续跟x86的,我是最后支持到16.11.x版本,那个版本也是只支持llvm12,修改部分llvm11的代码就可以支持,没发现需要编译x86的情况,可能编译环境不一样导致把,我是用原生的llvm2019插件+vs2019编译llvm
雪    币: 10
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_Mccc 活跃值 2022-4-22 16:43
13
0
666
雪    币: 3358
活跃值: 活跃值 (459)
能力值: ( LV9,RANK:320 )
在线值:
发帖
回帖
粉丝
vasthao 活跃值 6 2022-4-23 01:26
14
0
NDK的工具链有一些问题,是指直接用NDK工具链的LLVM头文件编译插件有一些问题,当然重新编译LLVM/CLANG可以解决,这属于编译器自举。
雪    币: 152
活跃值: 活跃值 (586)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Yecate 活跃值 2022-4-24 21:08
15
0
好文.以前折腾过MSVC 编译的一直没成功
雪    币: 21
活跃值: 活跃值 (180)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
拉闸太子 活跃值 2022-5-3 09:33
16
0


我哪里没配置好吗

雪    币: 22
活跃值: 活跃值 (28)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Englishmajor 活跃值 2022-5-4 01:11
17
0
你这样太折腾了
直接生成Ninja的build
cmake -G Ninja -DLLVM_ENABLE_PROJECTS="clang" -DLLVM_EXPORT_SYMBOLS_FOR_PLUGINS=On ..\llvm

-DLLVM_EXPORT_SYMBOLS_FOR_PLUGINS=On有这个就可以动态加载dll

参考https://discourse.llvm.org/t/how-to-create-pass-independently-on-windows/474/8
雪    币: 2871
活跃值: 活跃值 (1640)
能力值: ( LV13,RANK:240 )
在线值:
发帖
回帖
粉丝
IamHuskar 活跃值 4 2022-5-4 09:35
18
0
Englishmajor 你这样太折腾了 直接生成Ninja的build cmake -G Ninja -DLLVM_ENABLE_PROJECTS="clang" -DLLVM_EXPORT_SYMB ...
niubility
雪    币: 3358
活跃值: 活跃值 (459)
能力值: ( LV9,RANK:320 )
在线值:
发帖
回帖
粉丝
vasthao 活跃值 6 2022-5-4 09:57
19
0
由于DLL导出函数数量限制和DLL链接问题,LLVM_EXPORT_SYMBOLS_FOR_PLUGINS宏没多大用的,windows平台上最好是静态编译llvm插件
雪    币: 2772
活跃值: 活跃值 (4214)
能力值: ( LV5,RANK:60 )
在线值:
发帖
回帖
粉丝
舒默哦 活跃值 1 2022-5-4 16:13
20
0
先点赞,后收藏
雪    币: 3921
活跃值: 活跃值 (943)
能力值: ( LV7,RANK:115 )
在线值:
发帖
回帖
粉丝
yimingqpa 活跃值 1 2022-5-4 18:55
21
0
pass优化的出一个啊,正好可以优化去混淆垃圾代码。
雪    币: 1058
活跃值: 活跃值 (674)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
NewMai 活跃值 1 2022-5-5 12:03
22
0
拉闸太子 我哪里没配置好吗

你的 CMake 没装好,或者是 MSYS2 没装好,如果两者都装好了, 你可以试试运行 `cmake -G` 命令,你会看到如下的输出:

雪    币: 1058
活跃值: 活跃值 (674)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
NewMai 活跃值 1 2022-5-5 12:05
23
0
Englishmajor 你这样太折腾了 直接生成Ninja的build cmake -G Ninja -DLLVM_ENABLE_PROJECTS="clang" -DLLVM_EXPORT_SYMB ...

不过这个方法我没试过,不确定 clang 能不能加载,
而我的方法,我试过大型项目(LLVM Pass)都是没有问题的。
雪    币: 1058
活跃值: 活跃值 (674)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
NewMai 活跃值 1 2022-5-5 12:05
24
0
yimingqpa pass优化的出一个啊,正好可以优化去混淆垃圾代码。
你可以去尝试修改一下 Hikari 的代码,也能够使用的,
雪    币: 1058
活跃值: 活跃值 (674)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
NewMai 活跃值 1 2022-5-5 12:06
25
0
Yecate 好文.以前折腾过MSVC 编译的一直没成功
MSVC 编译能成的,就是不能单独加载外部的 Pass 插件,如果要使用 MSVC,则 Pass 插件的代码需要集成到 LLVM 源码目录中,并且每次修改 Pass 代码都需要重新编译 LLVM 项目。
游客
登录 | 注册 方可回帖
返回