论坛首页 安全编程开发区 阅读主题

[原创]基于LLVM的通用自包含化(Shellcode)编译器开发思路

385 浏览 22 回复
#1 楼主 2026-06-01 21:08:56
大家好我是Teddybe4r,好久不见,这是2026年的第一篇文章,这篇文章旨在帮助想要开发自己shellcode编译器的朋友,为你们提供思路与解决方案。由于文章产物的特殊性本文将不会提供代码,请各位读者在阅读完文章之后自行实现。现在我们步入正题。    在x64系统的时代shellcode的开发变得极富有技巧性,由于x64的调用约定的改变与微软引入的shadow space的技术出现导致masm32汇编在64上部分具有效率的语法糖彻底失效,由于shadow space机制手写x64汇编会变得有些恶心,在去年我开发了一款 分段式加载 的Rootkit,其中很多功能代码都是通过网络传送并且加载到内存运行的,这也让我的RDK开发变得十分繁琐,于是便有了这篇文章的项目。
    在现代编译器的环境下我们想要在原生编译器环境下写出shellcode是一个具有技巧的事情,我们要在代码层面抗优化,从譬如 全局变量,数组赋值,数组,连续变量定义,连续变量赋值,swich-case,条件转移语句等等的代码结构上下功夫利用编译期不可知写法才能够有效规避一些具有全局特征的代码优化, 在这之后还需要对抗MSVC的链接优化,以及一些安全检查 譬如 __chkstk 等等。
    本文基于 LLVM 实现了一套面向 Shellcode 的自包含编译框架,通过全局变量下沉、上下文透传、调用链重写消除全局与外部依赖,并结合编译期 API 哈希与Runtime动态符号解析,实现无导入表、无外部符号的纯位置无关代码生成,并且生成的Shellcode R3 R0都可以用。    在本文中我们主要分为如下章节:整体架构流程全局变量下沉与透传上下文结构化设计编译期 API 哈希与运行时动态符号解析   在一开始做这个下沉的时候我还踩了一些坑,当时我直接把调用链上每一个函数都下沉了一份到栈上,这其实是错误的因为根据全局变量的语义如果每个函数都持有一份在自己的栈中那么这个变量将会退化为局部变量,在语义上就发生了根本性的变化所以在设计这个结构的时候我们要引入上下文透传机制。  
    在常规 C 语言程序中,全局变量与静态变量由操作系统加载器统一分配虚拟地址,并在进程启动时完成初始化,其访问依赖固定的全局符号地址与可执行文件的数据段布局。但在纯 Shellcode 执行环境下,程序不具备独立的进程地址空间管理能力、无加载器支持、无数据段权限初始化机制,直接保留全局变量会导致以下问题:全局地址硬编码导致无法位置无关执行、多份 Shellcode 实例间数据冲突、全局符号引入外部依赖破坏自包含性。因此,需要通过全局变量下沉(Global Variable Lowering) 技术,将分散的全局状态统一收拢为结构化上下文,并在调用链中透明传递。并且由于程序结构的特性只有一个Entry,所以我们在Entry的基础栈帧中插入下沉的GV变成CTX的形式在调用链中传播CTX指针这样就可以做到下沉且保持语义不变。
透我归纳为以下8步函数属性加固:为调用链中所有函数添加 no-builtins、no-stack-arg-probe 等属性,阻止编译器生成外部依赖代码(如 memset、__chkstk),确保上下文透传过程无额外依赖。下图展示了透传前后的对比,以及透传之后的栈帧结构
    ShellcodeCtx 是一个由 Pass 自动生成的 packed 结构体,其字段顺序与 收集到的全局变量顺序严格一致。每个字段的类型直接取自对应全局变量的 getValueType()(即去掉指针层的底层类型)。    在完成全局变量收集与 ShellcodeCtx 类型构造之后,Pass 需要对调用链中每一个函数执行两类不同的处理:Entry 函数保持原有签名不变(loader 侧仍可通过 (void*)entry 取地址),在其入口块插入 alloca 实例化 ctx 并完成内联初始化;非 entry 函数则需要改写签名,在参数列表末尾追加 ShellcodeCtx* 参数,实现指针向下透传。Entry 函数之所以保持签名不变,是因为 loader 侧通常以 (void*)entry 的形式提取 Shellcode 起始地址或计算代码长度,改变签名会破坏这一用法。下面给出部分透传代码(addCtxParameter)签名改写的具体实现是在 addCtxParameter() 中完成的:构造新的 FunctionType(在原参数列表末尾追加 ShellcodeCtx*),用 Function::Create() 创建新函数,通过 ValueToValueMapTy 建立旧参数到新参数的映射,最后调用 CloneFunctionInto() 将原函数体完整克隆到新函数中。旧函数在所有调用点重写完成并确认无引用后被 eraseFromParent() 删除。调用点重写步骤如下: 遍历 fnRemap 映射,找到所有 CallInst 中调用了旧函数的位置,在参数列表末尾追加当前函数的 fnToCtxPtr(entry 传 alloca 地址,非 entry 传接收到的 ctx 参数),用 IRBuilder 构造新的 CallInst 并替换旧指令。全局变量的访问替换在重写调用点之后进行,然后每个函数的入口块前置缓存 GEP 指令(避免重复生成),将所有对 @g_xx 的直接引用和 ConstantExpr 包裹的间接引用全部替换为 GEP ctxPtr, 0, fieldIdx。    Pass 的入口工作是定位所有 Shellcode 入口函数,随后以此为根节点递归展开完整的调用图。这两步共同决定了后续所有变换的作用域——只有被纳入调用链的函数才会被改写,链外的函数保持不变。    入口函数定位通过扫描 llvm.global.annotations 元数据实现。在 C 源码侧,开发者通过 __attribute__((annotate("shellcode"))) 标注入口函数,Clang 会将该注解以 ConstantStruct 数组的形式写入 IR 中的 llvm.global.annotations 全局变量,Pass 遍历该数组并比对注解字符串即可精确定位入口。递归收集以 DFS 方式实现:对每个函数遍历其所有基本块内的全部 CallInst,取 getCalledFunction(),若被调函数有函数体(!isDeclaration())且未曾访问,则递归进入。visited 集合(即最终的 chainFuncs)防止环状调用导致无限递归。值得注意的是我们在实现调用链分析的时候需要检测函数指针逃逸函数指针逃逸检测是非常重要的安全机制:由于 Pass 要改写所有非 entry 函数的签名,如果某函数的地址在改写前已被存入变量(store)或作为参数传给外部回调(call @qsort),那么运行时调用时签名不匹配会直接导致崩溃。检测逻辑遍历每个链内函数的所有 Use,凡是出现在 CallInst 的 callee 位置之外的用法,且不属于 LLVM 元数据(llvm.global.annotations / llvm.used)的,均视为

...(已截断)

---
来源: 看雪论坛
原文链接: https://bbs.kanxue.com/thread-290935.htm
#2 2026-06-01 21:08:56
Oxygen1a1

大佬牛逼,这样是不是可以自己写pass来对shellcode进行混淆了
可以用pass也可以自己做混淆框架,我两个都做了
#3 2026-06-01 21:08:56
TeddyBe4r

你要不要看看自己在说什么?点进去examples 文件夹 有一个是点C开头的文件吗?基于llvm的vmp更是不知所云,你汇编都没有IR LLVM的pass是处理IR的假设你更高级一些你用了machin ...
他说的可能是这个 f18K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6D9j5h3W2F1M7%4N6G2M7X3E0Q4x3V1k6K6K9r3g2D9L8r3y4G2k6r3g2Q4x3X3c8X3j5h3y4@1L8%4u0&6i4K6t1$3L8X3u0K6M7q4)9K6b7W2!0q4y4q4!0n7z5q4)9^5b7g2!0q4z5g2)9&6c8q4!0m8x3W2!0q4z5g2)9^5x3W2!0m8x3#2!0q4y4q4!0n7z5q4!0m8b7g2!0q4y4q4!0n7b7#2!0n7x3q4!0q4z5q4!0m8c8g2!0m8x3g2!0q4y4W2)9&6z5q4!0m8c8W2!0q4y4q4!0n7b7W2)9&6y4W2!0q4y4#2)9&6b7W2!0n7y4q4!0q4y4W2)9^5c8g2!0m8y4g2!0q4y4W2)9&6x3q4)9&6b7#2!0q4y4#2)9&6b7g2)9^5y4q4!0q4y4W2!0n7x3W2!0m8x3g2!0q4y4#2)9^5x3W2!0n7z5g2!0q4z5q4!0n7c8W2)9&6b7W2!0q4y4g2)9^5c8g2!0n7b7W2!0q4y4#2)9&6b7#2)9^5b7R3`.`.
#4 2026-06-01 21:08:56
qj111111

你确定你看了这个项目?
这个项目可以直接用全局变量,全局字符串,类,开o1优化,你能实现的功能这个项目都能实现
还能换编译后端,llvm,msvc编译器都随便替换,ollvm,基于llvm的vmp ...
你要不要看看自己在说什么?点进去examples 文件夹 有一个是点C开头的文件吗?基于llvm的vmp更是不知所云,你汇编都没有IR LLVM的pass是处理IR的假设你更高级一些你用了machine function pass 但是那也是编译管线中针对IR来做汇编生成的的,整个项目中我只看到了macro宏来加速开发,你提到的其他东西我什么都没有看到,建议补充一下基础知识后再来讨论。
#5 2026-06-01 21:08:56
TeddyBe4r

汇编谁都会写,我用C你用汇编可以比比看谁的效率高些。
你确定你看了这个项目?
这个项目可以直接用全局变量,全局字符串,类,开o1优化,你能实现的功能这个项目都能实现
还能换编译后端,llvm,msvc编译器都随便替换,ollvm,基于llvm的vmp也实现了

就是有点小坑,我都修完了
它实现的直接解析coff,比写llvm简洁优雅多了,我只是说你这样把事情搞复杂了,它实现的更简洁优雅
#6 2026-06-01 21:08:56
大佬牛逼
#7 2026-06-01 21:08:56
大佬牛逼
#8 2026-06-01 21:08:56
TQL
#9 2026-06-01 21:08:56
TQL
#10 2026-06-01 21:08:56
谢谢分享
#11 2026-06-01 21:08:56
牛逼
#12 2026-06-01 21:08:56
如果对于反调试块这种东西llvm 侧不好做需要通过Machinefunctionpass改源码树来实现,我有这个版本也有通过自己的混淆引擎来进行反调试块的插入。
#13 2026-06-01 21:08:56
TQL
#14 2026-06-01 21:08:56
大佬牛逼,这样是不是可以自己写pass来对shellcode进行混淆了
#15 2026-06-01 21:08:56
mark
#16 2026-06-01 21:08:56
感谢分享!!!
‹ 上一页 1 2 下一页 ›

请登录后参与讨论

立即登录 注册账号