撰写这一篇文章的起因是看到CrowdStrike最近的一篇博客, 文章中介绍一个MSI样本将Mythic的agent编译为LLVM的IR bitcode,在目标上通过LLVM解释器执行。
这种技术似曾相识,.NET代码可以转换为公共中间语言(CIL)在内存中编译执行,Python代码通过解释器执行,JAVA代码通过JVM虚拟机执行,如今C代码也可以转换为LLVM IR语言解释执行。
LLVM是一套编译器基础设施项目,以C++写成,包含一系列模块化的编译器组件和工具链,用来开发编译器前端和后端。
其中Clang是基于LLVM开发的一个编译器前端工具,用来解析C/C++代码,将其转换为LLVM中间语言IR,最后通过不同的编译器后端编译成可执行代码。差不多过程就是下面这样
C/C++代码 -->LLVM IR --> MachineCode(x86、ARM...)
clang.exe和gcc.exe差不多,Visual Studio则是cl.exe
clang工具可以自行下载llvm的源码进行编译,也可以通过https://github.com/llvm/llvm-project/releases 下载,注意要下载clang+llvm-18.1.8-x86_64-pc-windows-msvc.tar.xz,这里面才包含可以执行LLVM IR bitcode的工具lli.exe。
首先打开x64 Native Tools Command Prompt for VS 2019 并切换到源代码目录
以简单的hellloworld代码为例,使用clang正常编译cpp文件为可执行文件:
(clang-cl是clang适配MSVC的版本)
clang-cl helloworld.cpp -o hello.exe
使用clang编译cpp文件为LLVM IR bitcode
clang -emit-llvm -c helloworld.cpp -o helloworld.bc
bitcode文件是这样的:
那么我们使用工具中的llc.exe来执行bitcode文件:
llc helloworld.bc
可以看到bitcode文件被成功执行了,而且将lli.exe以及bc文件放到另一个主机可以成功执行。
其实LLVM IR还有另外一种可读形态ll,通过下面的方式即可获得,但是这样会暴露源码中的信息,所以上述博客中的技术才会使用bitcode。
clang -emit-llvm -S helloworld.cpp -o helloworld.ll
可读形态ll文件内容如下所示
既然可以能够执行helloworld,为什么不来加载shellcode呢
通过msf生成一段弹计算器的shellcode,使用CreateThread加载执行
clang-cl shellcode_calc.cpp -o shellcode_calc.exe
shellcode_calc.exe
在关闭Windows Defender的情况下可以正常运行
但是一旦放到开启Windows Defender的环境中,就会被检测删除
如果使用lli.exe + shellcode_calc.bc呢,则可以成功在Windows Defender下执行这段shellcode
clang -emit-llvm -c shellcode_calc.cpp -o shellcode_calc.bc
lli.exe shellcode_calc.bc
让我们使用msf生成一个reverse_tcp,使用lli工具在开启了Windows Defender的环境下执行,同样可以看到成功回连。
那么lli文件是如何加载执行我们的代码?让我们使用x64dbg对lli文件进行调试,并执行之前弹计算器的bitcode,同时在NtCreateThreadEx下断点(为什么自在这下断点?后面会说)
当在NtCreateThreadEx断下时
持续回溯,当我们回到lli.exe的代码空间时,会看到这段类似于shellcode_calc.cpp中功能的汇编代码。不难看出,这里调用的是KernelBase中的CreateThread,而其他函数也是调用的KernelBase的API,所以在NtCreateThreadEx下断点。
而这段代码的地址为245B0DA0000,在内存布局中可以看到这应该是属于堆内存,并且由执行权限。汇编代码中的245B0DB0000则是用于弹计算器的shellcode,该空间只读。245B0DC0000也是同样的shellcode,而这个地址空间则有通过VirtualAlloc函数分配的RWX权限。
查看此时的堆栈,返回到lli的代码7FF61F44D42E,而且还返回到ntdll.RtlFreeHeap。
重新运行,在7FF61F44D42C处下断点(在执行shellcode_calc汇编代码之前)
通过此时的堆栈,能知道是使用RtlAllocateHeap函数分配的内存。在之前调用了memmove函数将bitcode转成的本机代码移动到rax指向的地址空间,再call进去。
至于lli.exe如何将bitcode转成的本机代码,则涉及到编译器的范畴了。
1 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!