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,对所有全局字符串进行“解码”处理。核心流程可以概括为:
创建一个无参、返回值为 的新函数
decode_stub。void在新函数里为每个 取得指向其第一个字节的
i8*指针,以及字符串长度len。GlobalString把这两个参数传给已存在的
decode_fn(形如void decode(u8* ptr, u32 len))。最后在
decode_stub末尾插入ret void指令并返回函数句柄。
decode_stub 的意义就在于“程序真正开始执行之前先把所有静态密文解码”,因此常见的两种接入方式是:
在
main(或等价的入口函数)的 第一条指令之前 直接插入一次调用(示例代码的方式)把
decode_stub注册到llvm.global_ctors,让它在运行时以 全局构造函数 的身份自动执行,而无需改动main。
这样实现的话,非常简单,但是也不够安全,对于一个Passes插件来说,扩展性太差了!
参考项目
https://github.com/SsageParuders/SsagePass