【Day 08】欢迎来到实力至上主义的 Shellcode (下) - Windows x86 Shellcode

环境

  • Windows 10 21H1
  • Visual Studio 2019
  • NASM 2.14.02

前情提要

欢迎来到实力至上主义的 Shellcode (上) 我们除了认识 Shellcode 和比较 Linux 与 Windows 之外,也了解如何透过访问各种结构得到 kernel32.dll 的 Base Address。

  1. 找到 kernel32.dll
    • 取得 PEB 位址
    • 取得 PEB_LDR_DATA 位址
    • 取得 InMemoryOrderModuleList 位址
    • 取得 kernel32.dll 的 Base Address
  2. 从 kernel32.dll 中找到 WinExec 函数
    • 取得 NT Header 位址
    • 取得 Export Directory 位址
    • 取得 Address Table、Name Pointer Table、Ordinal Table
    • 回圈寻找目标函数
  3. 执行 WinExec

详细流程

2. 从 kernel32.dll 中找到 WinExec 函数

取得 NT Header 位址

要找到 NT Header,首先要找到 DOS Header。不过已经找到了,它的位址就在 Image Base Address,因为我们在上个步骤已经取得了 kernel32.dll 的 Base Address。现在来看看 DOS Header 的结构

typedef struct _IMAGE_DOS_HEADER
{
     WORD e_magic;
     WORD e_cblp;
     WORD e_cp;
     WORD e_crlc;
     WORD e_cparhdr;
     WORD e_minalloc;
     WORD e_maxalloc;
     WORD e_ss;
     WORD e_sp;
     WORD e_csum;
     WORD e_ip;
     WORD e_cs;
     WORD e_lfarlc;
     WORD e_ovno;
     WORD e_res[4];
     WORD e_oemid;
     WORD e_oeminfo;
     WORD e_res2[10];
     LONG e_lfanew;
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

其中最後一个成员 e_lfanew 就是 NT Header 的 RVA(Relative Virtual Address),RVA 就是与 Image Base 的距离。也就是说 Image Base + e_lfanew 就是 NT Header 的位址。

取得 Export Directory 位址

Optional Header 就在 NT Header 中。

typedef struct _IMAGE_NT_HEADERS {
  DWORD                   Signature;
  IMAGE_FILE_HEADER       FileHeader;
  IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

File Header 的大小为 0x14,所以 NT Header + 0x18 就是 Optional Header 的位址。

DataDirectory 是 Optional Header 的最後一个成员,也就是 Optional Header 的 Offset 0x60 的位址。

typedef struct _IMAGE_OPTIONAL_HEADER {
  WORD                 Magic;
  BYTE                 MajorLinkerVersion;
  BYTE                 MinorLinkerVersion;
  DWORD                SizeOfCode;
  DWORD                SizeOfInitializedData;
  DWORD                SizeOfUninitializedData;
  DWORD                AddressOfEntryPoint;
  DWORD                BaseOfCode;
  DWORD                BaseOfData;
  DWORD                ImageBase;
  DWORD                SectionAlignment;
  DWORD                FileAlignment;
  WORD                 MajorOperatingSystemVersion;
  WORD                 MinorOperatingSystemVersion;
  WORD                 MajorImageVersion;
  WORD                 MinorImageVersion;
  WORD                 MajorSubsystemVersion;
  WORD                 MinorSubsystemVersion;
  DWORD                Win32VersionValue;
  DWORD                SizeOfImage;
  DWORD                SizeOfHeaders;
  DWORD                CheckSum;
  WORD                 Subsystem;
  WORD                 DllCharacteristics;
  DWORD                SizeOfStackReserve;
  DWORD                SizeOfStackCommit;
  DWORD                SizeOfHeapReserve;
  DWORD                SizeOfHeapCommit;
  DWORD                LoaderFlags;
  DWORD                NumberOfRvaAndSizes;
  IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

所以 NT Header + 0x18 + 0x60 就是 DataDirectory 结构。DataDirectory 结构如下,可以看到成员 VirtualAddress,它是我们的目标 Export Directory 的 RVA。因此 Image Base + Export Directory 的 RVA 就是 Export Directory 的位址。

typedef struct _IMAGE_DATA_DIRECTORY {
  DWORD VirtualAddress;
  DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

取得 Address Table、Name Pointer Table、Ordinal Table

观察一下 Export Directory 结构,其中的三个成员 AddressOfFunctions、AddressOfNames、AddressOfNameOrdinals 就分别是我们的目标 Address Table、Name Pointer Table、Ordinal Table 的 RVA,因此只要个别加上 Image Base 就可以取得这三个 Table 的位址。除此之外,成员 NumberOfFunctions 存放函数的数量,在等等的 Shellcode 中也会用上。

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

回圈寻找目标函数

这个步骤只要了解 Address Table、Name Pointer Table、Ordinal Table 之间的关系就很容易了,Name Table 中存放着所有 Export Function 名称字串的 RVA,找到目标函数名称後,拿对应的 Name Table 的 Index 去找 Ordinal Table。再把 Ordinal Table 对应的值当作 Index 去找 Function Table,得到的值就是目标 Function 的 RVA。

举例来说,现在目标是 WinExec 函数。

  1. 回圈扫过 Name Table,假设找到 Name Table 的第 x 项是 WinExec 字串的 RVA,也就是 Name Table + 4 * x。注意 Name Table 的每一项都是 4 Bytes 大小。
  2. 再来找 Ordinal Table 的第 x 项,也就是 Ordinal Table + 2 * x,这里存的值假设是 y。注意 Ordinal Table 的每一项都是 2 Bytes 大小。
  3. 最後找 Address Table 的第 y 项,也就是 Address Table + 4 * y,它就是 WinExec 函数的 RVA。注意 Address Table 的每一项都是 4 Bytes 大小。

3. 执行 WinExec

这个步骤就是使用前两大步骤辛苦找到的 WinExec 而已,唯一可以讲的大概就是 Calling Convention 的部分,以 32-bit Windows 的 WinExec 来说就只要把参数堆到 Stack 上就可以了,所以在呼叫这个函数前分别把 C:\Windows\System32\calc.exe 字串位址和 10 Push 到 Stack 中。

其中的 10 是 WinExec 的第二个参数 uCmdShow,意思是 SW_SHOWDEFAULT。

POC

主要是参考 Basics of Windows shellcode writing,有改一点东西以符合 NASM 还有加一些注解。在我的 GitHub zeze-zeze/2021iThome 可以找到完整版的 POC。

; 2. 从 kernel32.dll 中找到 WinExec 函数
; 取得 NT Header 位址
mov eax, [ebx + 3Ch]            ; NT Header 的 RVA
add eax, ebx                    ; NT Header = Image Base + e_lfanew

; 取得 Export Directory 位址
mov eax, [eax + 78h]            ; DataDirectory 结构,成员 VirtualAddress 是 Export Directory 的 RVA
add eax, ebx                    ; Export Directory = Image Base + Export Directory 的 RVA

; 取得 Address Table、Name Pointer Table、Ordinal Table
mov ecx, [eax + 24h]            ; Ordinal Table 的 RVA
add ecx, ebx                    ; Ordinal Table 的位址
mov [ebp-0Ch], ecx

mov edi, [eax + 20h]            ; Name Pointer Table 的 RVA
add edi, ebx                    ; Name Pointer Table 的位址
mov [ebp-10h], edi

mov edx, [eax + 1Ch]            ; Address Table 的 RVA
add edx, ebx                    ; Address Table 的位址
mov [ebp-14h], edx

mov edx, [eax + 14h]            ; Export Directory 其中一个成员 NumberOfFunctions
xor eax, eax

; 回圈寻找目标函数
.loop:
    mov edi, [ebp-10h]          ; Name Pointer Table
    mov esi, [ebp-4]            ; 目标字串 "WinExec\x00"
    xor ecx, ecx

    cld
    mov edi, [edi + eax*4]      ; Name Table + 4 * x 是函数名称的 RVA
    add edi, ebx                ; 取得函数名称的位址
    add cx, 8
    repe cmpsb                  ; 比较字串是否为 WinExec
    jz start.found

    inc eax
    cmp eax, edx
    jb start.loop

    add esp, 24h
    jmp start.end

.found:
    mov ecx, [ebp-0Ch]          ; Ordinal Table
    mov edx, [ebp-14h]          ; Address Table

    mov ax, [ecx + eax*2]       ; y = Ordinal Table + 2 * x
    mov eax, [edx + eax*4]      ; WinExec 的 RVA = Address Table + 4 * y
    add eax, ebx


    ; 3. 执行 WinExec
    xor edx, edx
    push edx                    ; C:\Windows\System32\calc.exe
    push 6578652eh
    push 636c6163h
    push 5c32336dh
    push 65747379h
    push 535c7377h
    push 6f646e69h
    push 575c3a43h
    mov esi, esp

    push 10                     ; uCmdShow: SW_SHOWDEFAULT
    push esi                    ; lpCmdLine: "C:\Windows\System32\calc.exe"
    call eax                    ; WinExec("C:\Windows\System32\calc.exe", 10)

    add esp, 44h

.end:
    pop ebp
    pop edi
    pop esi
    pop edx
    pop ecx
    pop ebx
    pop eax
    ret

另外我写了简单的 Shellcode Loader,程序不难,但因为不是这篇的重点所以先略过。

#include <windows.h>

int main()
{
    char shellcode[] = "\x50\x53\x51\x52\x56\x57\x55\x89\xE5\x83\xEC\x18\x31\xF6\x56\x68\x78\x65\x63\x00\x68\x57\x69\x6E\x45\x89\x65\xFC\x64\x8B\x1D\x30\x00\x00\x00\x8B\x5B\x0C\x8B\x5B\x14\x8B\x1B\x8B\x1B\x8B\x5B\x10\x89\x5D\xF8\x8B\x43\x3C\x01\xD8\x8B\x40\x78\x01\xD8\x8B\x48\x24\x01\xD9\x89\x4D\xF4\x8B\x78\x20\x01\xDF\x89\x7D\xF0\x8B\x50\x1C\x01\xDA\x89\x55\xEC\x8B\x50\x14\x31\xC0\x8B\x7D\xF0\x8B\x75\xFC\x31\xC9\xFC\x8B\x3C\x87\x01\xDF\x66\x83\xC1\x08\xF3\xA6\x74\x0A\x40\x39\xD0\x72\xE5\x83\xC4\x24\xEB\x3F\x8B\x4D\xF4\x8B\x55\xEC\x66\x8B\x04\x41\x8B\x04\x82\x01\xD8\x31\xD2\x52\x68\x2E\x65\x78\x65\x68\x63\x61\x6C\x63\x68\x6D\x33\x32\x5C\x68\x79\x73\x74\x65\x68\x77\x73\x5C\x53\x68\x69\x6E\x64\x6F\x68\x43\x3A\x5C\x57\x89\xE6\x6A\x0A\x56\xFF\xD0\x83\xC4\x44\x5D\x5F\x5E\x5A\x59\x5B\x58\xC3";

    // 分配记忆体,其中权限是 PAGE_EXECUTE_READWRITE
    LPVOID addressPointer = VirtualAlloc(NULL, sizeof(shellcode), 0x3000, 0x40);

    // 把 shellcode 写入分配的记忆体
    RtlMoveMemory(addressPointer, shellcode, sizeof(shellcode));

    // 建立 Thread 执行 shellcode
    CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)addressPointer, NULL, 0, 0);

    Sleep(1000);
    return 0;
}

实际测试

跟上一篇一样使用 NASM + LazyIDA 把 Shellcode 复制後放到 Shellcode Loader 中执行,成功编译并执行就会跳出一个小算盘。

参考资料


<<:  Angular 如何取得 API 资料

>>:  Day08 - 实作一个状态机 - 1

[2020铁人赛] Day26 - 用DocX汇出Word档案

今天来讲一下user的需求,要汇出一份word档,并且需要套上参数并替换值,其实nuget上有很多套...

第 06 天 有甚麽事先练再说( leetcode 105 )

https://leetcode.com/problems/construct-binary-tr...

第 14 天 我不是要压榨你我是给你个成长的机会|Reactive Form

前情提要 我们整理专案後,现在专案有更明确的模组来封装元件,不仅让 App 效能提升,也让专案更「语...

【D21】制作讯号灯#5:使用三大法人制作外资讯号灯

前言 制作了加权指数的,这次制作三大法人-外资的讯号灯,本次会做多单还是空单、留仓数量是否增加、留仓...

Day 30 - 3D绘图篇 - 噪声地形演算II - 成为Canvas Ninja ~ 理解2D渲染的精髓

这是我最後的波纹了。 其实我一直想试着讲一次这句话(X) 首先来丢一张案例完成後的图片~ 大家应该...