环境:WINDOWS10 Visual Studio 2022 masm X64

X64架构不需要设置内存模型和指令集,因为X64的内存模型和指令集都是固定的。

完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
; Save this file with a .asm extension

.DATA
hello db 'Hello, World!', 0Ah ; String followed by newline character

.CODE;
EXTERN WriteFile: PROC
EXTERN ExitProcess: PROC
EXTERN GetStdHandle: PROC

mainCRTStartup PROC
; Get standard output handle
sub rsp, 20h ; Allocate shadow space and space for the HANDLE
mov ecx, -11 ; STD_OUTPUT_HANDLE = -11
call GetStdHandle ; Call GetStdHandle
add rsp, 20h ; Cleanup shadow space
mov rdi, rax ; Save the HANDLE for later use

; Prepare WriteFile arguments
sub rsp, 24h ; Allocate shadow space and space for arguments
mov rcx, rdi ; hFile (stdout handle)
lea rdx, hello ; lpBuffer (address of the string)
mov r8, 13 ; nNumberOfCharsToWrite (length of the string)
lea r9, [rsp+24h] ; lpNumberOfCharsWritten (pointer to a DWORD to receive the number of chars written)
xor rax, rax ; Zero out the high 32 bits of RAX for proper stack alignment
call WriteFile ; Call WriteFile
add rsp, 24h ; Cleanup shadow space and arguments

; Prepare ExitProcess arguments
xor ecx, ecx ; Exit code: 0
call ExitProcess ; Call ExitProcess

mainCRTStartup ENDP
END

设置数据段

数据段用于存储全局变量和常量。

1
2
.DATA    ;数据段标识
hello db 'Hello, World!', 0Ah ;

定义了一个字节数组hello,其中包含了字符串 “Hello, World!“ 以及一个换行符(0x0A,ASCII 码十进制值为 10)。逗号表示在 “Hello, World!” 之后,数组的下一个元素是换行符(0x0A)。这样,当我们打印这个字符串时,会在 “Hello, World!” 之后显示一个换行符

设置代码段

1
代码段用于存储程序的可执行指令
1
2
3
4
;这里我们会用到3个函数
EXTERN WriteFile: PROC
EXTERN ExitProcess: PROC
EXTERN GetStdHandle: PROC

EXTERN XXX PROC 是一个函数的声明。它表示名为 XXX 的函数在其他地方定义(例如,在另一个模块或库中)。EXTERN 关键字告诉汇编器,函数 XXX 的实现在其他地方,但你希望在当前代码中使用它

这三个外部函数均为windows api

WriteFile 函数,用于将数据写入文件或其他 I/O 设备。在本例中,我们使用它将字符串输出到控制台。

GetStdHandle 函数,用于获取标准输入、输出或错误设备的句柄。

在本篇文章的示例中,GetStdHandle函数用于获取标准输出的句柄

获取哪种类型的句柄取决于函数参数,本文示例中我们传值为-11,对应十六进制数 0xFFFFFFFFFFFFFFF5。这个值对应于 STD_OUTPUT_HANDLE,它表示我们想要获取标准输出的句柄。标准输出通常用于将文本显示在控制台或命令行界面上。

ExitProcess函数,用于结束当前进程并将退出代码返回给操作系统

接下来是自定义函数

mainCRTStartup函数实现

mainCRTStartup作为我这个环境下的链接器的默认入口函数,主要代码将会写在这个函数体中

自定义函数的格式:

1
2
3
4
myFunc PROC:      ;函数开始
;函数实现
.....
myFunc ENDP;函数结束

为了实现打印一个字符串,三个api的调用顺序为:

GetStdHandle(获取标准输出句柄)->WriteFile(将数据写入到标准输出)->ExitProcess(结束当前进程)

这里先回顾一下堆栈的知识

1.生长方向为高地址向低地址

2.最后入栈的最先出

3.ESP(X64为RSP)为栈顶指针,意味着如果有元素出栈,ESP将增加,因为ESP每一刻都指着栈顶;反之减少

4.EBP(RBP)为基址指针,也就是指向栈底部的指针,出入栈一般不会影响

还有所用到的3个函数的相关信息

函数 参数 参数在寄存器/栈中的位置
GetStdHandle nStdHandle (DWORD) 获得句柄的类型 RCX
WriteFile hFile (HANDLE) 写入的句柄 RCX
lpBuffer (LPCVOID) 写入内容的指针 RDX
nNumberOfCharsToWrite (DWORD)应该要写的长度 R8
lpNumberOfCharsWritten (LPDWORD)指向一个地址,地址存的是实际写入的长度 R9
lpOverlapped (LPOVERLAPPED)异步操作的设置,不用管 没用到,不需要设置
ExitProcess uExitCode (UINT) ECX

在 x64 Windows 系统中,一个函数的前四个参数是存储在寄存器中(‘RCX,RDX,R8,R9’),其余的参数需要存储在栈空间上。(很重要)

GetStdHandle

1
sub rsp,20h

将栈顶指针减少32个字节(0x20转换为十进制为32),也就是提前为函数分配32个字节,其中32个字节为影子空间

在 x64 Windows 调用约定中,影子(或阴影)空间是在调用函数时为了保留一定空间而预留在栈上的一块内存。它主要用于存储在寄存器中传递的前四个参数的副本。影子空间的大小通常为 32 字节,即 4 个 8 字节大小的参数。

在 x64 Windows 调用约定中,RCX、RDX、R8 和 R9这些寄存器用于传递前四个参数。影子空间与这些寄存器相关,因为它的主要目的是为了在栈上保留这些寄存器中参数的副本,从而在异常处理,恢复数据时有用

1
mov ecx, -11

将 -11(STD_OUTPUT_HANDLE 的值)移动到 ECX 寄存器中。GetStdHandle 函数需要这个值作为参数

1
call GetStdHandle

调用该函数来获取标准输出句柄,这个句柄会被放在rax寄存器中

1
add rsp, 20h

恢复RSP栈顶指针

1
mov rdi, rax;

将rax寄存器中存储的句柄存入rdi

WriteFile

1
sub rsp,24h

提前为函数分配栈空间,其中0x20h为影子空间,剩下0x4h需要分配给lpNumberOfCharsWritten参数

具体来说,lpNumberOfCharsWritten就是

lpNumberOfCharsWritten 是一个指向 DWORD 类型变量的指针,用于接收 WriteFile 函数实际写入的字符数。WriteFile 函数在写入数据后,会将实际写入的字符数存储在 lpNumberOfCharsWritten 所指向的内存地址处。(所以我们需要分配一个DWORD大小的栈空间)

在本示例中,我们将 lpNumberOfCharsWritten 的地址设置为栈上的一个位置([rsp+24h]),这样当 WriteFile 函数执行完毕后,实际写入的字符数将被存储在栈上的这个位置。

为了更好地理解这个参数,我们可以将其视为一个输出参数。当我们调用 WriteFile 函数时,我们传入一个指针,函数将在执行期间将实际写入的字符数写入到这个指针所指向的内存地址。这样,当函数返回时,我们可以检查这个值,以了解实际写入了多少字符。

通常情况下,这个值应该与我们传入的 nNumberOfCharsToWrite 相等,除非发生错误或写入被截断。在本示例中,我们没有检查这个值,因为我们只是简单地打印一条消息。但在更复杂的场景中,检查这个值可能会很重要,以确保写入操作的正确性。

1
2
3
4
mov rcx,rdi;将句柄放到rcx供WriteFile使用
lea rdx,hello;将hello变量的内存地址给rdx供writeFile使用
mov r8,13;应该写入13个字节
lea r9,[rsp+24h];在栈空间开辟4个字节的空间并将地址给r9,目的是存储writeFile执行后实际写入的字节长度的值

调用WriteFile函数之前的参数配置

1
xor rax, rax 

将 RAX 寄存器清零。这是为了确保栈对齐,因为在 x64 调用约定中,RAX 的高 32 位必须为零。

1
call WriteFile

执行WriteFile函数,往控制台写入字符串

1
add rsp, 24h  

恢复栈顶指针

1
xor ecx, ecx

将ecx寄存器清理,这是因为 ExitProcess 函数需要一个退出代码参数,这里我们选择 0 作为正常退出代码

1
call ExitProcess

调用结束进程函数

1
mainCRTStartup ENDP

mainCRTStartup函数结束

1
END

汇编文件结束

结语:

学汇编真的很考验耐心和信心,但没办法,这是迈向免杀学习中必不可少的经历,因为本人是第一次接触汇编,所以这篇文章如果有错误欢迎各位师傅指正和理解