LLVM - 开发属于自己的llvm plugin(1)

开始

这里使用的语言是Rust!

LLVM 插件仅仅是一个 dylib,LLVM 工具(例如opt、lld )加载它时会为其提供PassBuilder。因此,你必须在你的Cargo.toml 添加下面这行:

[lib]
crate-type = ["cdylib"]

PassBuilder允许注册 LLVM 工具执行的特定操作的回调,更多详细细节和原理可以看 https://github.com/jamesmth/llvm-plugin-rs 仓库的介绍!

这里需要注意一个问题:官方的 Windows 版 LLVM 工具链并未内置插件支持。所以说Windows必须修改Clang源代码重新编译生成新的clang.exe! -- 2025.07.29 PS

首先导入我们需要的包

[dependencies]
llvm-sys = "201.0.0-rc1"
llvm-plugin = { version = "0.6", features = ["llvm20-1"] }

截至到2025.8.2 llvm20-1的相关pr依旧没有合并进去,所以说这里我说通过cargo patch提前体验了,这两个仓库是比较前沿的两个分支!

[patch.crates-io]
inkwell = { git = "https://github.com/stevefan1999-personal/inkwell", branch = "master" }
llvm-plugin = { git = "https://github.com/jamesmth/llvm-plugin-rs", branch = "feat/llvm-20" }

Hello World

use llvm_plugin::inkwell::values::FunctionValue;
use llvm_plugin::{
    FunctionAnalysisManager, LlvmFunctionPass, PassBuilder, PipelineParsing, PreservedAnalyses,
};

#[llvm_plugin::plugin(name = "HelloWorld", version = "0.1")] // 创建一个名为"HelloWorld"的LLVM插件
fn plugin_registrar(builder: &mut PassBuilder) {
    println!("(llvm-tutor) Registering HelloWorld pass");
    builder.add_function_pipeline_parsing_callback(|name, manager| { // 注册一个函数级别的pipeline解析回调
        if name == "hello-world" { // 当在命令行或配置中使用"hello-world"这个pass名称时,会添加HelloWorldPass到函数pass管理器中
            manager.add_pass(HelloWorldPass);
            PipelineParsing::Parsed
        } else {
            PipelineParsing::NotParsed
        }
    });
}

struct HelloWorldPass;
impl LlvmFunctionPass for HelloWorldPass {
    fn run_pass(
        &self,
        function: &mut FunctionValue,
        _manager: &FunctionAnalysisManager,
    ) -> PreservedAnalyses {
        eprintln!("(llvm-tutor) Hello from: {:?}", function.get_name()); // 输出当前正在处理的函数名
        eprintln!(
            "(llvm-tutor)   number of arguments: {}",
            function.count_params() // function.count_params() - 输出函数的参数个数
        );
        PreservedAnalyses::All // 表示这个pass没有修改任何IR,所有现有的分析结果都还有效
    }
}

以上是官方示例的标准例子,当然这个例子肯定是啥用没有... 这里你可以看见他调用了add_function_pipeline_parsing_callback 注册函数级别的pipeline解析回调(一个函数/方法就是一个Function),当然函数级别就还有别的级别add_module_analysis_registration_callback :用于向pass管理器注册模块级别的分析!(一个.c文件编译后就是一个模块)......

这里llvm-plugin-rs尚未支持注册:

  • CGSCC级别分析注册 PassBuilder.registerCGSCCAnalyses()

  • Loop级别分析注册PassBuilder.registerLoopAnalyses()

具体的用途:

  • Loop: 分析循环特性(是否可向量化、循环次数等)

  • Function: 分析函数内部(变量生存期、控制流等)

  • CGSCC: 分析相互递归函数(尾递归优化等)

  • Module: 分析整个编译单元(全局优化、链接时优化等)

首先把您需要的.c文件转换成ll文法文件, 再执行opt进行优化。--passes指定需要打开的pass

clang -emit-llvm -S -O0 test1.c -o test1.ll
opt --load-pass-plugin=libplugin.so --passes=hello-world test1.ll -disable-output

如果正常输出了日志说明你的插件已经完成了基础的配置了,这时候你想实现什么字符串加密都是可以的!这里就必须得提到SsageParuders大牛的SsagePass了!给我提供了完整的思路,项目有清晰的注释!

实现一个基础的字符串加密

这里我们先熟悉一下LLVM Pass Pipeline中各个扩展点的执行顺序:

这里Ssage大佬没有选择registerPipelineParsingCallback而是使用了registerPipelineStartEPCallback,原因也在他的源代码里面,这里懒得说了!

添加一个Pass需要提供了一个LlvmModulePass类型的玩意,

use llvm_plugin::inkwell::module::Module;
use llvm_plugin::{LlvmModulePass, ModuleAnalysisManager, PreservedAnalyses};

pub struct StringEncryption {
    enable: bool // 方便通过llvm参数实现控制开关
}

impl LlvmModulePass for StringEncryption {
    fn run_pass(&self, module: &mut Module<'_>, manager: &ModuleAnalysisManager) -> PreservedAnalyses {

        PreservedAnalyses::All
    }
}

impl StringEncryption {
    pub fn new(enable: bool) -> Self {
        StringEncryption { enable }
    }
}
// ... your code ... 
    builder.add_pipeline_start_ep_callback(|manager, level| {
        manager.add_pass(StringEncryption::new(true))
    });
// ... your code ...

这里补充一下返回值的意思,All就是你没有修改新增任何东西,如果修改了新增了就要返回None使得之前的分析和优化全部无效(这个时候有优化吗?LLVM的API非常不稳定,每个版本几乎大概,虽然他们会贴心的写个markdown表示改变了什么,该怎么做,但是很难有人去盯着这个啊

enum StringEncryptionType {
    XOR
}

这里先定义一个枚举表示一下加密的类型,还有一些别的什么东西?!不过我觉得需要扩展的话,应该使用bitflags,这个功能不是很复杂先用枚举占位吧。

叽里呱啦的,我不想写博客,奖励一个源代码:

use llvm_plugin::inkwell::module::{Linkage, Module};
use llvm_plugin::{inkwell, FunctionAnalysisManager, LlvmModulePass, ModuleAnalysisManager, PreservedAnalyses};
use llvm_plugin::inkwell::values::{ArrayValue, AsValueRef, BasicValueEnum, FunctionValue, GlobalValue};
use log::{error, info};

#[cfg(any(
    feature = "llvm15-0",
    feature = "llvm16-0",
))]
macro_rules! ptr_type {
    ($cx:ident, $ty:ident) => {
        $cx.$ty().ptr_type(AddressSpace::default())
    };
}

#[cfg(any(
    feature = "llvm17-0",
    feature = "llvm18-1",
    feature = "llvm19-1",
    feature = "llvm20-1"
))]
macro_rules! ptr_type {
    ($cx:ident, $ty:ident) => {
        $cx.ptr_type(AddressSpace::default())
    };
}

enum StringEncryptionType {
    XOR
}

impl StringEncryptionType {
    pub fn do_handle(&self, module: &mut Module<'_>, manager: &ModuleAnalysisManager) -> anyhow::Result<()> {
        match self {
            StringEncryptionType::XOR => xor::do_handle(module, manager),
        }
    }
}

pub struct StringEncryption {
    enable: bool,
    encryption_type: StringEncryptionType,
}

impl LlvmModulePass for StringEncryption {
    fn run_pass<'a>(&self, module: &mut Module<'a>, manager: &ModuleAnalysisManager) -> PreservedAnalyses {
        if !self.enable {
            return PreservedAnalyses::All;
        }

        if let Err(e) = self.encryption_type.do_handle(module, &manager) {
            error!("(strenc) failed to handle string encryption: {}", e);
        }

        PreservedAnalyses::None
    }
}

impl StringEncryption {
    pub fn new(enable: bool) -> Self {
        StringEncryption {
            enable,
            encryption_type: StringEncryptionType::XOR
        }
    }
}

mod xor {
    use std::fmt::format;
    use env_logger::builder;
    use inkwell::module::Module;
    use inkwell::values::FunctionValue;
    use llvm_plugin::inkwell::{AddressSpace, Either};
    use llvm_plugin::{inkwell, FunctionAnalysisManager, ModuleAnalysisManager};
    use llvm_plugin::inkwell::basic_block::BasicBlock;
    use llvm_plugin::inkwell::module::Linkage;
    use llvm_plugin::inkwell::values::{AnyValueEnum, BasicValue, BasicValueEnum, BasicValueUse, GlobalValue, InstructionValue};
    use log::{error, info, warn};
    use crate::aotu::string_encryption::array_as_const_string;

    pub(crate) fn do_handle<'a>(module: &mut Module<'a>, manager: &ModuleAnalysisManager) -> anyhow::Result<()> {
        let ctx = module.get_context();
        let i32_ty = ctx.i32_type();

        let gs: Vec<(GlobalValue<'a>, u32, GlobalValue<'a>)> = module.get_globals()
            .filter(|global| !matches!(global.get_linkage(), Linkage::External))
            .filter_map(|global| match global.get_initializer()? {
                // C-like strings
                BasicValueEnum::ArrayValue(arr) => Some((global, None, arr)),
                // Rust-like strings
                BasicValueEnum::StructValue(stru) if stru.count_fields() <= 1 => {
                    match stru.get_field_at_index(0)? {
                        BasicValueEnum::ArrayValue(arr) => Some((global, Some(stru), arr)),
                        _ => None,
                    }
                }
                _ => None,
            })
            .filter(|(_, _, arr)| {
                // needs to be called before `array_as_const_string`, otherwise it may crash
                arr.is_const_string()
            })
            .filter_map(|(global, stru, arr)| {
                // we ignore non-UTF8 strings, since they are probably not human-readable
                let s = array_as_const_string(&arr).and_then(|s| str::from_utf8(s).ok())?;
                let encoded_str = s.bytes().map(|c| c ^ 0xAA).collect::<Vec<_>>();
                let unique_name = global.get_name().to_str()
                    .map_or_else(|_| rand::random::<u32>().to_string(), |s| s.to_string());
                Some((unique_name, global, stru, encoded_str))
            })
            .map(|(unique_name, global, stru, encoded_str)| {
                let flag = module.add_global(i32_ty, None, &format!("dec_flag_{}", unique_name));
                flag.set_initializer(&i32_ty.const_int(0, false));
                flag.set_linkage(Linkage::Internal);

                if let Some(stru) = stru {
                    // Rust-like strings
                    let new_const = ctx.const_string(&encoded_str, false);
                    stru.set_field_at_index(0, new_const);
                    global.set_initializer(&stru);
                    global.set_constant(false);
                    (global,encoded_str.len() as u32, flag)
                } else {
                    // C-like strings
                    let new_const = ctx.const_string(&encoded_str, false);
                    global.set_initializer(&new_const);
                    global.set_constant(false);
                    (global, encoded_str.len() as u32, flag)
                }
            })
            .collect();

        let decrypt_fn = add_decrypt_function(module, &format!("decrypt_strings_{}", rand::random::<u32>()))?;

        for (global, len, flag) in &gs {
            let mut uses = Vec::new();
            let mut use_opt = global.get_first_use();
            while let Some(u) = use_opt {
                use_opt = u.get_next_use();
                uses.push(u);
            }

            for u in uses {
                let insert_decrypt = |inst: InstructionValue<'_>| -> anyhow::Result<()> {
                    let parent_bb = inst.get_parent().expect("inst must be in a block");
                    let parent_fn = parent_bb.get_parent().expect("block must have parent fn");
                    let builder = ctx.create_builder();

                    builder.position_before(&inst);
                    let ptr = global.as_pointer_value();
                    let len_val = i32_ty.const_int(*len as u64, false);
                    let flag_ptr = flag.as_pointer_value();
                    builder.build_call(decrypt_fn, &[ptr.into(), len_val.into(), flag_ptr.into()], "", )?;

                    Ok(())
                };
                match u.get_user() {
                    AnyValueEnum::InstructionValue(inst) => insert_decrypt(inst)?,
                    AnyValueEnum::IntValue(value) => {
                        if let Some(inst) = value.as_instruction_value() {
                            insert_decrypt(inst)?;
                        } else {
                            error!("(strenc) unexpected IntValue user: {:?}", value);
                        }
                    }
                    _ => {
                        panic!("(strenc) unexpected user type: {:?}", u.get_user());
                    }
                }
            }
        }

        Ok(())
    }

    fn add_decrypt_function<'a>(module: &mut Module<'a>, name: &str) -> anyhow::Result<FunctionValue<'a>> {
        let ctx = module.get_context();
        let i8_ty  = ctx.i8_type();
        let i32_ty = ctx.i32_type();
        let i8_ptr = ptr_type!(ctx, i8_type);
        let i32_ptr = ptr_type!(ctx, i32_type);

        // void decrypt_strings(i8* str, i32 len, i32* flag)
        let fn_ty = ctx.void_type()
            .fn_type(&[i8_ptr.into(), i32_ty.into(), i32_ptr.into()], false);
        let decrypt_fn = module.add_function(name, fn_ty, None);

        let prepare = ctx.append_basic_block(decrypt_fn, "prepare");
        let entry = ctx.append_basic_block(decrypt_fn, "entry");
        let body = ctx.append_basic_block(decrypt_fn, "body");
        let next = ctx.append_basic_block(decrypt_fn, "next");
        let exit = ctx.append_basic_block(decrypt_fn, "exit");

        let builder = ctx.create_builder();
        builder.position_at_end(prepare);
        let flag_ptr = decrypt_fn.get_nth_param(2)
            .map(|param| param.into_pointer_value())
            .ok_or_else(|| anyhow::anyhow!("Failed to get flag parameter"))?;

        let flag = builder.build_load(i32_ty, flag_ptr, "flag")?.into_int_value();
        let is_decrypted = builder.build_int_compare(inkwell::IntPredicate::EQ, flag, i32_ty.const_zero(), "is_decrypted")?;
        builder.build_conditional_branch(is_decrypted, entry, exit)?;

        builder.position_at_end(entry);
        builder.build_store(flag_ptr, i32_ty.const_int(1, false))?;

        let idx = builder.build_alloca(i32_ty, "idx")?;
        builder.build_store(idx, ctx.i32_type().const_zero())?;
        builder.build_unconditional_branch(body)?;

        builder.position_at_end(body);
        let index = builder.build_load(i32_ty, idx, "cur_idx")?.into_int_value();
        let len = decrypt_fn.get_nth_param(1)
            .map(|param| param.into_int_value())
            .ok_or_else(|| anyhow::anyhow!("Failed to get length parameter"))?;
        let cond = builder.build_int_compare(inkwell::IntPredicate::ULT, index, len, "cond")?;
        builder.build_conditional_branch(cond, next, exit)?;

        builder.position_at_end(next);
        let ptr = decrypt_fn.get_nth_param(0)
            .map(|param| param.into_pointer_value())
            .ok_or_else(|| anyhow::anyhow!("Failed to get pointer parameter"))?;
        let gep = unsafe {
            builder.build_gep(i8_ty, ptr, &[index], "gep")
        }?;
        let ch = builder.build_load(i8_ty, gep, "cur")?.into_int_value();
        let xor_ch = i8_ty.const_int(0xAA, false);
        let xored = builder.build_xor(ch, xor_ch, "new")?;
        builder.build_store(gep, xored)?;
        let next_index = builder.build_int_add(index, ctx.i32_type().const_int(1, false), "")?;
        builder.build_store(idx, next_index)?;
        builder.build_unconditional_branch(body)?;

        builder.position_at_end(exit);
        builder.build_return(None)?;

        Ok(decrypt_fn)
    }
}

pub(crate) fn array_as_const_string<'a>(arr: &'a ArrayValue) -> Option<&'a [u8]> {
    let mut len = 0;
    let ptr = unsafe { inkwell::llvm_sys::core::LLVMGetAsString(arr.as_value_ref(), &mut len) };

    if ptr.is_null() {
        None
    } else {
        unsafe { Some(std::slice::from_raw_parts(ptr.cast(), len)) }
    }
}

简单说一下官方例子

官方例子代码:https://github.com/jamesmth/llvm-plugin-rs/blob/master/examples/strings.rs#L52

把所有的字符串加密当然,他只加密了.str字符串,然后

create_decode_stub 函数使用了 inkwell(LLVM 的 Rust 语言绑定)来在 IR 级别动态生成一个新的 LLVM 函数 decode_stub。该函数的任务是在运行时依次调用用户传入的 decode_fn,对所有全局字符串进行“解码”处理。核心流程可以概括为:

  1. 创建一个无参、返回值为 的新函数 decode_stubvoid

  2. 在新函数里为每个 取得指向其第一个字节的 i8* 指针,以及字符串长度 lenGlobalString

  3. 把这两个参数传给已存在的 decode_fn(形如 void decode(u8* ptr, u32 len))。

  4. 最后在 decode_stub 末尾插入 ret void 指令并返回函数句柄。

decode_stub 的意义就在于“程序真正开始执行之前先把所有静态密文解码”,因此常见的两种接入方式是:

  1. main(或等价的入口函数)的 第一条指令之前 直接插入一次调用(示例代码的方式

  2. decode_stub 注册到 llvm.global_ctors,让它在运行时以 全局构造函数 的身份自动执行,而无需改动 main

这样实现的话,非常简单,但是也不够安全,对于一个Passes插件来说,扩展性太差了!

参考项目

  • https://github.com/SsageParuders/SsagePass