原文链接:https://github.com/mytechnotalent/Hacking-Windows
我们从用 Windows 汇编语言编写一个非常简单的 hello world 程序开始我们的旅程。我们将在本章中仅使用纯汇编语言编写,因为我们将专注于 C 语言的演变,几乎所有 Windows 开发都发生,因此您可以更好地了解这些应用程序是如何组合在一起的,包含在 x86 和x64。
让我们首先下载我们将用作集成开发环境的 Visual Studio。在下面的链接中选择 Visual Studio 2019 社区版。确保在设置过程中选择了所有 C++ 和 Windows 选项,以确保构建环境具有所有必要的工具。如有疑问,请选中该框以在安装期间将其包括在内。
https://visualstudio.microsoft.com/downloads
安装后,让我们创建一个新项目,并按照以下步骤开始。
- 创建一个新项目
- Empty Projec(空项目)
- Next(下一步)
- Project name(项目名称):0x0001-hello_world-x86
- 选中“Place solution and project in the same directory”(将解决方案和项目放在同一目录中)
- Create(创建)
- 在解决方案资源管理器中右键单击0x0001-hello_world-x86
- Add(添加)
- New Item…(新建项)
- main.asm
- 右键单击 0x0001-hello_world-x86
- Build Dependencies(生成依赖项)
- Build Customizations(生成自定义)
- 选择masm
- OK(确定)
- 右键单击 main.asm
- Properties(属性)
- Configuration Properties(配置属性)
- General(常规)
- Item Type(项类型): Microsoft Macro Assembler
- OK(确定)
现在让我们来填充我们的main.asm文件包含以下内容 :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
.686 .model flat, stdcall .stack 4096 extrn ExitProcess@4: proc ;1 param 1x4 extrn MessageBoxA@16: proc ;4 params 4x4 .data msg_txt db "Hello World", 0 msg_caption db "Hello World App", 0 .code main: push 0 ;UINT uType lea eax, msg_caption ;LPCSTR lpCaption push eax lea eax, msg_txt ;LPCSTR lpText push eax push 0 ;HWND hWnd call MessageBoxA@16 push 0 ;UINT uExitCode call ExitProcess@4 end main |
祝贺您刚刚在x86 Windows程序集中创建了第一个hello world代码。该吃蛋糕了!
在本课程中,我们将花费大部分时间学习Win32API文档。
让我们花点时间回顾一下。首先,我们指定一个.686,这意味着可以在32位MASM中为奔腾Pro+风格的体系结构组装非特权指令。
(VISIT https://docs.microsoft.com/en-us/cpp/assembler/masm/dot-686?view=msvc-160)
我们调用的第一个Win32API是MessageBoxA,它提供了一个Windows消息框来显示。然后,我们建立了一个平面内存模型,它不使用组合段或偏移寻址。我们还使用stdcall Win32调用约定,将参数按相反顺序推送到堆栈上,然后调用过程。被调用方在调用后清除堆栈。
我们将调用的第二个Win32API是ExitProcess,它简单地退出应用程序,并将操作释放到Windows操作系统。
我们看到,该函数是一个void函数,它不返回任何内容,只有一个参数UINT uExitCode,它只检索进程的退出值。
你可能已经注意到函数后面有一个非常奇怪的@4。这是为了指定该函数有1个参数。我们将每个参数乘以4得到这个名称。
我们的下一个Win32API是MessageBoxA函数,它只显示一个带有标题和消息的模式对话框。
(VISIT https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winusermessageboxa)
我们这里有4个参数,所以我们知道在函数的末尾会有一个@16。
第一个参数是HWND HWND,它是要创建的消息框窗口的所有者的句柄,在我们的例子中,它为NULL意味着消息框没有所有者。
然后我们有了LPCSTR lpText,它将在消息框中显示我们的文本。
然后我们有了LPCSTR lpCaption,它将是消息框上的标题文本。
最后是UINT uType,它只是文档中表格中的标志组合。在我们的情况下,它将是空的。
请记住,在stdcall中,我们按相反的顺序将参数推送到堆栈上,正如您在上面的代码中看到的那样。
此时,我们可以通过单击本地Windows调试器旁边的绿色箭头来运行代码。
万岁,我们的hello world模式对话框弹出。
现在让我们创建这段代码的x64版本。
- 创建一个新项目
- Empty Projec(空项目)
- Next(下一步)
- Project name(项目名称):0x0001-hello_world-x64
- 选中“Place solution and project in the same directory”(将解决方案和项目放在同一目录中)
- Create(创建)
- 在解决方案资源管理器中右键单击0x0001-hello_world-x64
- Add(添加)
- New Item…(新建项)
- main.asm
- 右键单击 0x0001-hello_world-x64
- Build Dependencies(生成依赖项)
- Build Customizations(生成自定义)
- 选择masm
- OK(确定)
- 在解决方案浏览器中右键单击0x0001-hello_world-x64
- // 右键单击 main.asm
- Properties(属性)
- Configuration Properties(配置属性)
- Linker(链接器)
- Advanced(高级)
- Entry Point(入口点): main
- OK(确定)
选择“调试”菜单栏右侧和本地Windows调试器菜单栏左侧的x64
现在让我们来填充我们的main.asm文件包含以下内容。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
extrn MessageBoxA: proc extrn PostQuitMessage: proc .data msg_txt db 'Hello World', 0 msg_caption db 'Hello World App', 0 .code main proc sub rsp, 20h ;shadow stack mov r9, rax ;UINT uType lea r8, msg_caption ;LPCSTR lpCaption lea rdx, msg_txt ;LPCSTR lpText xor rcx, rcx ;HWND hWnd call MessageBoxA add rsp, 20h ;restore shadow stack xor rcx, rcx ;int nExitCode call PostQuitMessage ret main endp end |
我们还看到一个对PostQuitMessage的调用,该调用以int-nExitCode作为参数。
(VISIT https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuserpostquitmessage
祝贺您刚刚在x64 Windows程序集中创建了第一个hello world代码。又到吃蛋糕的时间了!
让我们花点时间回顾一下。我们首先需要了解x64呼叫约定。
VISIT https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention?view=msvc160)
我们在x64中使用的是Microsoft x64呼叫约定fastcall。我们在参数传递部分看到的是,默认情况下,x64调用约定将前四个参数传递给寄存器中的函数。用于这些参数的寄存器取决于参数的位置和类型。剩余的参数将按从右到左的顺序推送到堆栈上。调用方在调用后清理堆栈。
最左边四个位置的整数值参数分别按RCX、RDX、R8和R9的从左到右顺序传递。如前所述,第五个及以上参数在堆栈上传递。寄存器中的所有整数参数都是右对齐的,因此被调用方可以忽略寄存器的高位,只访问寄存器中必要的部分。
前四个参数中的任何浮点和双精度参数都会在XMM0-XMM3中传递,具体取决于位置。只有当存在varargs参数时,浮点值才会被放入整数寄存器RCX、RDX、R8和R9中。有关详细信息,请参阅Varargs。类似地,当相应的参数是整数或指针类型时,XMM0-XMM3寄存器将被忽略。
根据x64调用约定,我们需要为每个QWORD的内存单元提供一个阴影堆栈,并且堆栈必须与下一条指令的16字节对齐。
阴影空间是我们必须为被调用的过程保留的强制32字节(4×8字节)。在调用之前,我们在堆栈上提供32个字节。这个空间可以不初始化。
在这个调用约定中,第四个之后的参数被推送到堆栈上,堆栈位于这个阴影空间的顶部(推到32字节之前)。
然后,我们设置并再次调用MessageBoxA Win32API。我们不需要查看参数,因为我们在x86示例中已经处理过这个问题。
然后我们恢复阴影堆栈,然后调用ExitProcess。
此时,我们可以通过单击本地Windows调试器旁边的绿色箭头来运行代码。
万岁,我们的hello world模式对话框弹出。
这将是我们在所有汇编中编写代码的唯一例子,我想使用官方的Win32API进行教学,Win32API本机是C语言,但是我想首先向您展示在实际编译时,它到底在做什么。