技术社区
安全培训
技术社群
积分商城
先知平台
漏洞库
历史记录
清空历史记录
相关的动态
相关的文章
相关的用户
相关的圈子
相关的话题
注册
登录
Windows Shellcode开发
一天
二进制安全
380浏览 · 2025-04-02 11:07
返回文档
一、前置知识
1.1 shellcode是什么
shellcode是一段精心编写的机器代码,它具有
位置无关、紧凑高效、直接在CPU上执行,无需编译
等属性。在攻防对抗中,shellcode常用于
网络操作
、
权限提升
、
文件操作
等。
比如说我们经常使用到Cobalt Strike的Payload Generator就是有阶段(stager)shellcode,这段shellcode,完成的主要功能就是从CS服务器上下载stage(即beacon.dll),然后跳转到beacon.dll并执行。beacon.dll通过反射式DLL注入(Reflective DLL)技术实现加密的Beacon DLL直接加载到内存中,并进行解密。
1.2 为什么要编写shellcode
在实际的渗透过程中,我们经常使用到CS或者MSF生成shellcode,由于直接使用这些工具生成的shellcode是死的,即特征固定,没办法扩展自己想要的功能,也没办法规避AV/EDR的查杀。再者网上大部分的文章都是介绍弹窗或者弹出个计数器就结束了,还是需要自己理解和编写shellcode。其次在未来的规划中,我可能自己写一个远控,为此需要模仿CS的有阶段shellcode完成上线过程的功能,在次之前我就先学习了反射DLL注入,感兴趣的读者可以去看看我写的
自举的代码幽灵——反射DLL注入(Reflective DLL Injection)
这篇文章。
总结
:掌握Shellcode编写技术十分重要,可以说是一个安全开发人员的必备技能。
1.3 汇编基础
1.3.1 寄存器
寄存器是 CPU 用于存储和处理数据的核心组件,shellcode编写中用到大多数是通用寄存器,偶尔用到段寄存器(cs、ds、es、gs等)和eip,所以x86和x64介绍的寄存器都是通用寄存器。
(1)x86
寄存器
名称
EAX
累加器 (Accumulator)
EBX
基址寄存器 (Base)
ECX
计数器 (Counter)
EDX
数据寄存器 (Data)
ESI
源索引寄存器 (Source Index)
EDI
目的索引寄存器 (Destination Index)
EBP
基址指针 (Base Pointer)
ESP
栈指针 (Stack Pointer)
(2)x64
x64指令系统中,以R开头扩展了x86的8个通用寄存器,在此之外又引进了r8、r9、r10、r11、r12、r13、r14和r15寄存器。
1.3.2 指令
指令
作用
mov
寄存器/内存之间的数据传输
xor
寄存器清零或数据解密。
push
数据压栈
pop
数据出栈
jmp
无条件跳转
call
调用函数
lea
计算内存地址,不实际访问内存
cmp
比较指令,常配合跳转指令如je、jne等
当然汇编指令不只我介绍的这些,感兴趣的读者可以自行查阅资料。
1.3.3 调用约定
调用约定定义了函数调用时
参数传递顺序
、
堆栈清理责任
(调用者或被调用者)以及
函数名修饰规则
。
WINAPI
是 Windows 开发中的一个宏定义,
用于指定函数使用
__stdcall
调用约定
。在下文的4.2中,我们将遵从stdcall调用约定,参数清理由被调用函数完成,调用者无需处理。
调用约定
参数顺序
堆栈清理者
典型应用场景
变参支持
__stdcall
右→左
被调用者
Windows API、COM接口
否
__cdecl
右→左
调用者
C/C++默认、可变参数函数
是
__fastcall
右→左
被调用者
部分寄存器传参优化场景
否
对于x86架构,调用函数之前需要将函数需要用到的参数压入到栈内,压栈的顺序是从右到左,如
对应的汇编顺序是
对应x64架构,参数传递的方式有所不通,前四个参数分别使用RCX、RDX、R8和R9从左到右顺序传递,后续的参数就使用栈传递,压栈的顺序是从右到左。如
对应的汇编顺序是
二、注意事项
我们先回顾一下pe文件的加载流程
1
首先将PE文件按照内存结构重写映射到内存中
2
修复导入表
3
修复重定向表
4
TLS(线程本地存储)初始化
5
修改C++异常、修复导入延迟表等
6
执行入口点
当我们直接运行pe文件时,上述的操作都是由操作系统完成的,可我们现在要编写shellcode,所以要尽量避免产生上述的操作。如果不省略上述操作,那还不如直接用petoshellcode或者dount等工具将pe文件转换成带有引导头的shellcode。
我们在编写位置无关的shellcode时,就要注意下面的事项
1
.rdata节中的全局变量或常量是不能用
:因为我们的shellcode并不是exe文件,没有完成重定位这个操作。如果我们使用类似
CHAR VirtualAlloc[] = "VirtualAlloc";
的常量字符串是不允许的。但话也不能说的这么绝对,如果能保证文件对齐和内存对齐相同,也就可以带上.rdata节的数据,但我感觉太麻烦了。
2
不能使用导入表
:如果需要用到Windows的API,就需要通过PEB来动态获取或者可以先用PEB获取
LoadLibrary+GetProcAddress
函数的地址,然后用这两个的组合来获取需要的函数的地址。
3
不使用C++异常、不使用导入延迟表
4
编译后提取
.text
节作为我们的shellcode。
三、环境工具
在Windows上开发shellcode,我还是建议使用Visual Studio这款强大的集成开发环境。再次之外还需要x32/64dbg、IDA等工具
我们在Visual Studio新建一个控制台应用项目
新建项目完成后,我们就来到了这个页面
接下来我们做一些环境配置
首先我们要关闭安全检查,因为启用安全检查后,会使用到security cookie这个全局变量,但是我们shellcode压根不能用.rdata的全局变量所以要关闭这个选项。
关闭优化(最小优化可以使生成shellcode的体积减小,但不能保证正常运行),启用内部函数选项改为否
对于
x64
而言,微软的MSVC不允许在C++代码中插入汇编指令,故只能采取联合编译汇编才行。当然我还是推荐纯汇编。
右键项目,点击“生成依赖性->生成自定义”
勾选masm,然后确定。
在项目中添加*.asm后缀的文件,对刚刚添加的asm文件,点击属性,看项类型是不是
Microsoft Macro Assembler
问题
:对于
x86
编写纯汇编,如果我们的MSVC版本高于14.26.28801,则msvc工具集编译不了masm32v11r环境的汇编代码。
解决方法
:安装低版本的工具集,由于本人使用的是Visual Studio2022的IDE,从管理工具中下载2019年的MSVC的版本也是高于14.26的,故我下载的是2017年的MSVC工具集。
当然还有一些配置没说,我的想法是配合具体实现的时候再提起一些注意事项。
四、弹窗shellcode
4.1 C++
首先我们来解决第一个问题,那就是不能用全局变量或全局常量,所有用到参数都只能从栈中获取。我们就安装下面的格式来声明需要用到的字符串,这样这些字符就不会存放在.rdata中,而只会存放在栈中,也就不会出现绝对地址。
CHAR getProcAddress[] = { 'G','e','t','P','r','o','c','A','d','d','r','e','s','s','\0' };
是这样存放的
而
CHAR VirtualAlloc[] = "VirtualAlloc";
这样存放的
接下来解决第二个问题:
不修复导入表也不使用导入表
。但这又会出现一个问题,不依靠导入表怎么才能完成我们想要的功能,全部功能自己自己实现是非常不明智的,
如何在不依赖导入表,仍能动态调用系统 API
(如
VirtualAlloc
,
CreateThread
等),这是编写纯独立Shellcode 的关键挑战。
为了解决这个问题,我不得不远离导入表的温柔乡,义无反顾地奔向手刃PEB的斗争中……
大致的步骤如下
1
获取PEB的地址
:从gs/fs寄存器中获取PEB的地址
2
遍历加载的模块列表
:从PEB中访问
Ldr
成员,获取
PEB_LDR_DATA
结构。遍历InMemoryOrderModuleList链表,获取每个模块的LDR_DATA_TABLE_ENTRY。
3
查找目标DLL(如kernel32.dll)
:比较每个模块的BaseDllName与目标DLL名称(不区分大小写)
4
解析目标DLL的导出表
:从DLL基地址获取PE头,定位导出表。遍历导出表中的函数名称,找到目标函数并计算其地址。
原理我就不在这里讲了,可以跳转到4.2纯汇编中查看原理,具体实现去看我的另一篇文章:
文章 - 动态获取API函数(又称隐藏IAT)实现免杀 - 先知社区
想要解析pe文件,需要在010editor的模板处安装
EXE.bt
模板才能达到下图所示的效果。可以看到末尾有很多00字节,我尝试去掉一些也能够正常执行。
我们点击.text节区,然后右键->"选择"->"保存选择"
可以看到我们我们的文件大小有1kb,说实话还是有点大了。
想要加载这个文件,我们可以使用
hasherezade/pe_to_shellcode: Converts PE into a shellcode
中的runshec工具
当然啦,我们也可以将用010 editor工具打开*.bin文件,然后选择导出为C语言格式的文件
导出的文件里面含有0x格式的shellcode
然后用各种加载器来完成加载,这里我使用
创建线程
的方式执行shellcode
4.2 纯汇编
从上文可知,我们使用C++编写的程序,经过编译后的exe文件中的.text节区的大小还是比较大的,而我们用shellcode的主要原因还是因为它的短小精悍。看了网上的一些文章,有人推荐在C中使用内联汇编或者联合编译等手段减少shellcode体积,经过深思熟虑后我还是决定使用纯汇编的方法编写shellcode,理由如下
1
指令级精确控制
2
避免对齐填充
3
规避全局变量与库依赖
缺点也很明显,就是编写效率低下,需要对汇编指令、堆栈和寄存器很熟悉,没有用C++编写来的爽一些。
下面的这个汇编代码是masm32格式的,我为什么要用这个格式呢,理由如下
1
visual studio自带相关的工具套件(比如说ml和link),免去安装环境
2
依附于visual studio,我可以极其方便调式汇编程序,查看内存和寄存器的情况
3
个人习惯,我已经离不开visual studio,已经是它的形状了
一说到shellcode开发,必定离不开Stephen Fewer,他是安全领域的重要研究者,以其在内存注入技术和 Shellcode 开发中的贡献闻名,代码参考这两个汇编文件
1
block_api.asm
:代码通过动态解析哈希值来定位所需的API函数地址:
metasploit-framework/external/source/shellcode/windows/x86/src/block/block_api.asm at master · rapid7/metasploit-framework
2
block_reverse_http.asm
:该汇编代码实现了一个通过HTTP下载并执行远程代码的Shellcode加载器:
metasploit-framework/external/source/shellcode/windows/x86/src/block/block_reverse_http.asm at master · rapid7/metasploit-framework
因为
block_reverse_http.asm
中ebp事先存放了api_call的地址,通过
call ebp
来实现函数调用,我也不知道api_call的地址是怎么存在ebp中的,为了解决这个问题,我将上述的两个文件整合成一个asm文件。
4.2.1 main
正常情况下,一个什么功能都没有的程序运行起来只会加载三个模块:进程本身、ntdll.dll和kernel32.dll。所以为了完成相应的功能,比如说调用MessageBoxA函数,就相应将user32.dll加载到进程中。
使用GetProcAddressByHash的步骤,比如说调用MessageBoxA,从右到左压入MessageBoxA的参数,最后再压入user32.dll+MessageBoxA的哈希值。
4.2.2 GetProcAddressByHash
我将
block_api.asm
的api_call命名为GetProcAddressByHash,保持了大部分代码的原样,并对少部分代码进行了修改
(1)保存寄存器和目标hash到当前栈帧上
关键指令解析
1
pushad
:该指令保存调用者所有寄存器的状态,压栈的顺序是EDI → ESI → EBP(原始值) → ESP(原始值) → EBX → EDX → ECX → EAX。
2
push eax
:将目标hash值压入当前栈帧中,当然你也可以不用这一天汇编指令,在后续要用到目标hash时,用ebp寄存器计算出目标hash值的位置。
请记住执行完上述的汇编指令,此时栈的布局,最后结尾的时候我再给出清栈后的栈布局。
(2)获取
InMemoryOrderModuleList
模块链表的第一个模块结点
关键指令解析
1
mov edx, fs:[edx + 30h]
:通过FS段寄存器获取PEB地址。因为这个代码是32位的,所有我们用到了fs寄存器,熟悉动态获取API的朋友肯定对fs寄存器很熟悉,他指向
TEB
(线程环境块),而在TEB偏移0x30的位置是则是
ProcessEnvironmentBlock
,即PEB的指针
2
mov edx,[edx+0ch]
:获取ldr指针。在PEB结构体偏移0xc则存放着
PEB_LDR_DATA
结构体的指针
3
mov edx,[edx+14h]
:获取第一个模块结点。在
PEB_LDR_DATA
结构体中0x14的位置是存储着
InLoadOrderModuleList
模块链表的指针
这个
InLoadOrderModuleList
相当于一个头结点,其
Flink
指针下一个节点。
mov edx,[edx+14h]
就相当于获得第一个模块结点(进程本身)
(3)模块遍历
Flink
指针下一个节点,有没有人好奇它指向的具体位置在哪里呢?根据我的分析,它其实指向的是
LDR_DATA_TABLE_ENTRY
结构体0x8的位置上
我们计算一下
BaseDllName
和
InMemoryOrderLinks
之间的偏移,正好是24h(10进制36),我们再看下图
UNICODE_STRING
结构体
这样就能说明
movzx ecx, word ptr [edx+24h]
和
mov esi,[edx+28h]
确实是能获取到模块的长度和字符数组指针。
⚠
注意
:在Stephen Fewer的代码中获取长度是用偏移0x26来获取的,其实两种方式都可以,用MaximumLength,以为着它将字符串末尾的多个
00
也算进去了,这会导致多执行几轮计算hash的步骤,导致hash值与用偏移0x24计算的hash值的不一样。
下面我们通过调式来验证上述汇编代码的正确性,看以看到下图我们通过esi来查看模块名称字符串所在的位置,有一点值得我们关注,就是每一个字符之间都会插入'00',这意味我们只通过字符串的长度来控制计算hash的轮数,不能用'00'来判断字符串是否结束,这将影响计算hash的代码的实现(将计算hash的汇编代码独立成一个asm文件,下文中会给出)
(4)计算模块hash
代码没什么好讲解的,看注释就能知道个大概,我只在这里提一些我任务指定关注的
1
可以将字符串统一为大写,也可以将字符串统一为小写,目的就是大小写不敏感,因为微软在给dll命名时有时会用字母大写,有时会用小写。
2
ror edi,0dh
:循环右移的位数可以自己设定,不一定要求是13位,只要保证你给的目标hash也是使用相同的手段得到即可
(5)获取导出表
很连贯的一套操作
1
mov edx,[edx+10h]
:获取模块的基址,此时edx是指向
InMemoryOrderLinks
,距离edx偏移0x10的位置上是模块的基址
2
mov eax,[edx+3ch]
:获取PE头RVA,从这条指令开始都是涉及PE头的操作。
通过计算,确实是获取位于DOS头0x3c位置的e_ifanew,这个成员存储着PE头的RVA
3
mov eax,[eax+78h]
:获取导出表RVA(可选头DataDirectory[0]的VirtualAddress,偏移0x78),可以通过结构体
IMAGE_OPTIONAL_HEADER
计算得出确实是偏移0x78
4
mov ecx,[eax+18h]
和
mov ebx, [eax+20h]
:获取按名称导出的函数数量和函数名称字符串地址数组的RVA(AddressOfNames RVA)
我们调式来看一下
为什么按名称导出的函数数量为0呢?因为我现在看的是第一个模块(本进程),而第一个模块是没有导出函数的
我们看下一个模块,这个模块是ntdll.dll,可以看到一个有0x9B6个导出函数。
(6)获取函数名
没啥好说的,直接调式看是否正确
再用dbg来验证一下是否有这个函数
(6)计算模块 hash + 函数 hash之和,没啥好说的
(7)获取目标函数指针
ecx最开始是充当了名称数组的长度,随着不断的
dec ecx
,刚好能充当一个序号数组的索引作用,并从序号数组获取导出索引,最终得到目标函数指针,大概的流程是这样的
根据调式,我们确实是找的了目标函数(LoadLibraryA)的地址
(8)清栈并调用目标函数
为了确保我们调用完目标函数后能返回到main中的下一条指令,我们需要保存原始返回地址,需要push ecx与jmp eax模拟call指令。
清栈前后的栈空间布局如下
4.3 完整代码
计算hash的asm代码,请在ret指令处下一个断点,哈希之和存放在eax中
弹窗完整代码,可以在这个模板上扩展自己想要完成的功能
按照4.1介绍的步骤,我们提取编译后exe文件中的.text节的机器码作为我们的shellcode,然后用runshc32.exe工具运行*.bin文件。
将生成的bin文件转换为C语言格式,去掉末尾一大串'00',只保留一个'00',可以明显的看到我们的shellcode体积是如此之小
能够正常运行
可能会出现的一些问题和解决方案。
当然你也可以将masm32格式的汇编转换成nasm格式(我没去实验,但应该是可以的),这样就可以直接用命令
nasm -f bin shell.asm -o shell.bin
将上面的汇编代码转换成bin文件,就省略提权.text节的步骤。
五、远程下载文件shellcode
根据之前编写shellcode的经验,我们继续扩展完成一些复杂的功能,在本例中我们使用Windows API从远程http服务器中下载文件。在此之前,需要完成
1
函数声明
:需要声明的API有
VirtualAlloc
、
InternetOpenA
、
InternetConnectA
、
HttpOpenRequestA
、
HttpSendRequestA
、
InternetReadFile
、
InternetCloseHandle
2
常量定义
:需要定义的常量有wininet、UA、IP、PATH、Method、Version
3
获取API
:动态获取上述需要用到的AP函数地址。
我们用了一个网络编程相关的动态链接库,就是
WinINet
(Windows Internet),它是 Microsoft 提供的一个高级网络编程接口库,主要用于简化 Windows 平台上的互联网通信功能开发。它是 Windows API 的一部分,封装了 HTTP、FTP 等协议的底层细节,使开发者能够更便捷地实现网络请求、文件传输等功能。现在讲解大致思路
1
使用
VirtualAlloc
创建一个本地缓存,用于存放下载的文件
2
使用
InternetOpenA
初始化Internet会话
3
使用
InternetConnectA
连接到HTTP服务器
4
使用
HttpOpenRequestA
创建HTTP请求
5
使用
HttpSendRequestA
发送HTTP请求
6
使用
InternetReadFile
读取数据到缓存中
7
使用
InternetCloseHandle
关闭之前创建的Internet句柄
3.1 C++
在运行代码之前,我们需要用python开启一个简单的http服务,在本例中服务器的ip地址是
192.168.1.1
,端口是
9100
,资源路径是
/evil.txt
。
首先看看我们的evli.txt里的内容,这里的文件类型和内容随意,为了测试方便我使用的是txt文本文件。
然后我们用Vistual Studio调式,看看程序是否正常的连接http服务器并读取文件,我选择在
pInternetReadFile(hRequest, lpbuffer, 4096, &dwRead);
这条语句下一个断点,然后获取lpbuffer的值,根据这个值查看内存的情况。
按照4.1介绍的步骤,我们提取编译后exe文件中的.text节的机器码作为我们的shellcode,然后用runshc64.exe工具运行*.bin文件。
六、总结
1
接下来你可以根据4.2中介绍的方法用纯汇编的方式编写远程下载并执行的shellcode了,写到此处感觉身体已经燃尽了,没有精力再写下去了。
2
如果你认真分析过我给的Stephen Fewer代码,你就会发现
block_reverse_http.asm
通过 jmp指令无条件跳转到缓存区中开始执行bootstrap,这个引导程序主要的作用就是找到beacon.dll中的ReflectiveLoader函数,这个函数实现beacon.dll的自加载。ReflectiveLoader的实现可以去读一下我的文章:
自举的代码幽灵——反射DLL注入(Reflective DLL Injection)-先知社区
3
按道理来说,这一步(找到ReflectiveLoader函数)应该是patch到beacon.dll中,作为beacon.dll的bootstrap(引导码)。如果可能的话我会出一篇关于引导程序的文章(又挖一个坑)。
4
编写shellcode的过程中,会遇到大量的数据结构,建议去熟悉一下这个数据结构的作用,这样才能玩好底层,去干一些好玩的事情(=。=)
一不小心写得太多了,在此非常感谢愿意花时间读到这里的读者,看我啰嗦了这么久,如果有什么问题或者疑问也请不吝赐教。
1
人收藏
2
人喜欢
转载
分享
0
条评论
某人
表情
可输入
255
字
评论
发布投稿
热门文章
1
2025ISCC练武区域赛和决赛pwn以及擂台pwn合集
2
通过Elastic EDR看smbexec并进行二次开发Bypass
3
php代码审计篇 - 信呼OA 前台注入分析一
4
D3CTF-d3kshrm(预期&非预期)题解
5
Tomcat解析XML引入的新颖webshell构造方式
近期热点
一周
月份
季度
1
2025ISCC练武区域赛和决赛pwn以及擂台pwn合集
2
通过Elastic EDR看smbexec并进行二次开发Bypass
3
php代码审计篇 - 信呼OA 前台注入分析一
4
D3CTF-d3kshrm(预期&非预期)题解
5
Tomcat解析XML引入的新颖webshell构造方式
暂无相关信息
暂无相关信息
优秀作者
1
一天
贡献值:18800
2
T0daySeeker
贡献值:18700
3
1174735059082055
贡献值:15000
4
Yale
贡献值:14000
5
1674701160110592
贡献值:13000
6
LeeH
贡献值:10000
7
MeteorKai
贡献值:9000
8
姓*户
贡献值:8600
9
熊猫正正
贡献值:8000
10
lufei
贡献值:8000
目录
一、前置知识
1.1 shellcode是什么
1.2 为什么要编写shellcode
1.3 汇编基础
1.3.1 寄存器
1.3.2 指令
1.3.3 调用约定
二、注意事项
三、环境工具
四、弹窗shellcode
4.1 C++
4.2 纯汇编
4.2.1 main
4.2.2 GetProcAddressByHash
4.3 完整代码
五、远程下载文件shellcode
3.1 C++
六、总结
转载
标题
作者:
你好
http://www.a.com/asdsabdas
文章
转载
自
复制到剪贴板