Windows x64 汇编编写反向Shell Code教学文档
第一章:概述与目标
本文档旨在系统性地教授如何从零开始,使用x64汇编语言手动编写可运行的反向Shell Code。整个过程分为两大阶段:
- 理解阶段:通过使用外部API调用的方式,理解反向Shell的完整建立流程与核心API。
- 实现阶段:通过纯汇编实现,手动解析PEB、动态获取API地址,最终生成不依赖外部导入表的独立Shell Code。
最终目标是生成一段可被加载器执行的原始字节码(Shell Code),并为后续的免杀处理打下基础。
第二章:理解阶段 - 使用外部API编写反向Shell
本阶段旨在通过简化代码(允许使用EXTERN外部声明),聚焦于理解网络连接与进程创建的逻辑流程。此阶段代码无法直接编译为Shell Code,但易于理解。
2.1 核心API与结构解析
反向Shell的建立主要依赖于以下几个Win32 API和数据结构:
-
WSAStartup函数- 功能:初始化Winsock网络库。这是进行任何Windows Sockets编程的第一步。
-
WSASocketA函数- 功能:创建一个套接字(Socket),用于网络通信。
-
WSAConnect函数- 功能:使用创建的套接字连接到指定的远程地址和端口。
- 关键参数:
s:要连接的socket句柄(由WSASocketA返回)。name:指向sockaddr_in结构体的指针,该结构体定义了目标IP地址和端口。namelen:name结构体的长度。
-
CreateProcessA函数- 功能:创建一个新进程(例如cmd.exe)并将其输入/输出重定向到我们创建的套接字。
- 说明:参数较多,但大部分可置为NULL或0。需重点关注用于重定向标准输入、输出、错误的参数。
-
STARTUPINFOA结构体- 功能:作为
CreateProcessA的关键参数,指定新进程的主窗口特性。对于反向Shell,核心在于设置其hStdInput、hStdOutput和hStdError成员为我们套接字的句柄,从而实现I/O重定向。
- 功能:作为
2.2 汇编代码实现逻辑
- 调用
WSAStartup初始化Winsock。 - 调用
WSASocketA创建TCP套接字。 - 填充
sockaddr_in结构体,设置目标IP和端口。 - 调用
WSAConnect连接到远程主机。 - 初始化
STARTUPINFOA结构体,将其标准句柄(stdin, stdout, stderr)设置为套接字句柄。 - 调用
CreateProcessA启动cmd.exe,并将其I/O绑定到套接字。 - 清理资源。
注意:此阶段代码编译的程序会被杀毒软件直接拦截,因它明确调用了敏感API。测试时需暂时关闭实时防护。
第三章:实现阶段 - 纯汇编Shell Code开发
这是生成真正Shell Code的核心。所有API地址必须动态解析,不能有外部依赖。
3.1 手动解析PEB定位kernel32.dll基址
由于无法使用导入表,第一步是手动获取核心DLL的基地址。
- 原理:通过线程环境块(TEB) 找到进程环境块(PEB),进而遍历PEB_LDR_DATA结构中的已加载模块链表(
InLoadOrderModuleList)。 - 目标:遍历该链表,找到
kernel32.dll(或kernelbase.dll)的基地址(DllBase)。该基地址是解析其他所有API的起点。
3.2 解析DLL导出表动态获取API地址
获取DLL基址后,需手动解析其PE(Portable Executable)文件结构以定位导出函数。
-
定位导出表:
- 从DLL基址找到DOS头(
e_lfanew)。 - 跳到NT头,找到
DataDirectory数组,其第一项(索引0)是导出表(Export Directory)的RVA(相对虚拟地址)。 - 将RVA转换为实际内存地址(VA)。
- 从DLL基址找到DOS头(
-
遍历导出表查找函数:
导出表关键数组:AddressOfNames:存储函数名称字符串RVA的数组。AddressOfNameOrdinals:存储函数序号(ordinal)的数组,与AddressOfNames一一对应。AddressOfFunctions:存储函数地址RVA的数组,通过序号(ordinal)索引。- 查找流程:
a. 遍历AddressOfNames数组,将每个名称字符串RVA转换为指针,与目标API名称字符串(如"GetProcAddress")比较。
b. 找到匹配项后,记下其在数组中的索引(i)。
c. 使用索引i从AddressOfNameOrdinals中取出函数的序号(ordinal)。
d. 使用ordinal作为索引,从AddressOfFunctions中取出目标函数的RVA。
e. 将函数RVA + DLL基址得到该API在内存中的实际地址。
-
关键API获取链:
- 首先需要获取
GetProcAddress和LoadLibraryA的地址。这通常通过硬编码哈希或直接字符串比较查找kernel32.dll的导出表完成。 - 获得
GetProcAddress后,即可用它方便地获取其他DLL(如ws2_32.dll)中的API地址,而无需再次手动遍历导出表。 - 使用
LoadLibraryA动态加载ws2_32.dll并获取其基址。
- 首先需要获取
3.3 所需API清单
实现反向Shell至少需要以下API,需按顺序解析并存储其地址:
- 来自 kernel32.dll:
GetProcAddress,LoadLibraryA,CreateProcessA。 - 来自 ws2_32.dll:
WSAStartup,WSASocketA,WSAConnect。
免杀扩展:为隐藏静态特征,可使用哈希算法(如ROR13)对API名称进行哈希。在查找时,比较函数名哈希值而非明文字符串,这样Shell Code中就不会出现敏感的API名称字符串。
3.4 网络编程与进程创建
此部分逻辑与第二章“理解阶段”完全一致,但调用的是动态获取到的API地址:
- 调用
WSAStartup。 - 调用
WSASocketA创建套接字。调用成功后,返回值(RAX)为套接字句柄。若返回0xFFFFFFFFFFFFFFFF(INVALID_SOCKET)则表示失败。 - 调用
WSAConnect进行连接。 - 构造
STARTUPINFOA结构体,设置标准句柄重定向。 - 调用
CreateProcessA启动cmd.exe。
3.5 调试与问题排查
开发过程中必须使用调试器(如x64dbg)。
- 关键断点:在每次调用动态获取的API之前设置断点,检查寄存器中存储的地址是否正确。例如,检查
GetProcAddress返回的WSAStartup地址是否有效(应指向ws2_32.WSAStartup),而非乱码。 - 返回值检查:密切关注关键API的返回值。例如,
WSASocketA调用后,RAX中应为有效的套接字句柄(一个非零、非INVALID_SOCKET的值)。
3.6 生成与测试Shell Code
- 编译链接:使用汇编器(如NASM)和链接器生成PE文件。
- 提取Shell Code:
- 使用二进制编辑工具或编程方法,从生成的PE文件的
.text代码节中提取机器码。 - 注意规避文件中的NULL字节(
\x00),因为NULL字节在C字符串中会被截断,导致Shell Code不完整。可通过指令优化来消除。
- 使用二进制编辑工具或编程方法,从生成的PE文件的
- 加载器测试:编写一个简单的C语言加载器,将提取的Shell Code字节数组放入可执行内存页,然后跳转到该内存地址执行,以验证功能。
char shellcode[] = {0x48, 0x89, ...}; void *exec = VirtualAlloc(0, sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE); memcpy(exec, shellcode, sizeof(shellcode)); ((void(*)())exec)();
第四章:总结与后续方向
通过以上步骤,即可完成从零手写x64反向Shell Code。关键路径为:手动解析PEB -> 获取kernel32基址 -> 解析导出表获取GetProcAddress/LoadLibrary -> 加载ws2_32.dll并获取网络API -> 执行网络连接与进程创建逻辑 -> 提取机器码。
生成基础Shell Code只是第一步。此代码具有明显的静态(如API哈希特征)和行为特征,会被现代杀毒软件轻易检测。后续的免杀(防御规避)工作包括但不限于:
- Shell Code编码/加密/混淆。
- 添加垃圾指令(NOP-sleds, 花指令)。
- 使用更隐蔽的进程注入技术。
- 系统调用(Syscall)直接调用,绕过用户层API监控。
- 反调试、反沙箱技术的集成。