在Windows系统中编写shellcode

Root
Root
发布于 2023-10-12 / 17 阅读 / 0 评论 / 0 点赞

在Windows系统中编写shellcode

在Windows系统中编写shellcode

步骤

  1. 获取kernel32.dll 基地址;
  2. 定位 GetProcAddress函数的地址;
    • GetProcAddress函数是Windows操作系统提供的一个API函数,用于获取指定DLL(Dynamic Link Library)中导出函数的地址。
  3. 使用GetProcAddress确定 LoadLibrary函数的地址;
    • LoadLibrary函数是Windows操作系统提供的一个API函数,它用于加载一个动态链接库(DLL)到当前进程的地址空间,从而使得这个DLL中的函数可以被当前进程调用。
  4. 然后使用 LoadLibrary加载DLL文件(例如user32.dll);
  5. 使用 GetProcAddress查找某个函数的地址(例如MessageBox);
  6. 指定函数参数;
  7. 调用函数。

进程块

在Windows操作系统中,PEB是一个位于所有进程内存中固定位置的结构体。此结构体包含关于进程的有用信息,如可执行文件加载到内存的位置,模块列表(DLL),指示进程是否被调试的标志,还有许多其他的信息。

重要的是理解操作系统如何调用这个结构体。这个结构在不同Windows操作系统版本上并不是固定的,所以它可能随着新的Windows发行版发生改变,但一些通用信息会保持不变。

正如前文中讨论的,DLL(由于ASLR机制)可以加载到不同的内存位置,因此我们不能在shellcode中使用固定的内存地址。不过,我们可以使用PEB这个结构,位于固定的内存位置,从而查找DLL加载到内存中的地址。

typedef struct _PEB {
    BYTE                          Reserved1[2];//Reserved表示保留,此处为2个字节
    BYTE                          BeingDebugged;//一个标志位,用于指示当前进程是否正在被调试。在调试器附加到进程时,这个字段会被设置为非零值。此处为1个字节
    BYTE                          Reserved2[1];//此处为1个字节
    PVOID                         Reserved3[2];//一个PVOID指针站4个字节(32位操作系统,64位占8个字节)
    PPEB_LDR_DATA                 Ldr;//指向PEB_LDR_DATA结构的指针,用于存储有关当前进程加载的模块(DLL)信息。
    PVOID                         ProcessParameters; //指向RTL_USER_PROCESS_PARAMETERS结构的指针,包含有关进程的启动参数、环境变量、命令行参数等信息。
    PVOID                         Reserved4[3];
    PVOID                         AtlThunkSListPtr;
    PVOID                         Reserved5;
    ULONG                         Reserved6;
    PVOID                         Reserved7;
    ULONG                         Reserved8;
    ULONG                         AtlThunkSListPtr32;
    PVOID                         Reserved9[45];
    BYTE                          Reserved10[96];
    PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;//指向一个回调函数,该函数在进程初始化阶段完成后会被调用。
    BYTE                          Reserved11[128];
    PVOID                         Reserved12[1];
    ULONG                         SessionId;//当前进程所属的会话标识。
} PEB, *PPEB;

PEB_LDR_DATA(Process Environment Block Loader Data)是Windows操作系统内核中PEB(Process Environment Block)数据结构中的一个字段。它存储了有关当前进程加载的所有模块(DLL)的信息,包括已加载的模块列表和模块加载的顺序。PEB_LDR_DATA结构在Windows中的定义如下(部分字段):

typedef struct _PEB_LDR_DATA {
    BYTE       Reserved1[8];//保留字段
    PVOID      Reserved2[3];//保留字段
    LIST_ENTRY InMemoryOrderModuleList; //LIST_ENTRY 结构,它是一个双向链表的头,用于存储按照内存中地址排序的已加载模块(DLL)的链表。链表中的模块按照它们在内存中加载的实际顺序排列。InMemoryOrderModuleList 是一个重要的字段,可以用于遍历当前进程加载的模块列表。通过迭代这个链表,可以访问每个已加载模块的信息,例如模块的基址、大小以及其他相关数据。
} PEB_LDR_DATA, *PPEB_LDR_DATA;

LIST_ENTRY 是 Windows 操作系统中定义的一个双向链表结构,用于实现链表的链接。这个结构在 Windows 开发中经常用于构建和管理链表数据结构。

typedef struct _LIST_ENTRY {
  struct _LIST_ENTRY *Flink; // Flink指向下一个LDR_DATA_TABLE_ENTRY
  struct _LIST_ENTRY *Blink; // Blink指向上一个LDR_DATA_TABLE_ENTRY
} LIST_ENTRY, *PLIST_ENTRY, PRLIST_ENTRY;

_LDR_DATA_TABLE_ENTRY 是 Windows 操作系统中定义的一个结构,用于表示一个加载的模块(DLL)在进程中的信息。

typedef struct _LDR_DATA_TABLE_ENTRY {
    PVOID Reserved1[2];// 保留字段
    LIST_ENTRY InMemoryOrderLinks;
    PVOID Reserved2[2];
    PVOID DllBase;//这里就是dll的基址,对于InMemoryOrderLinks位置偏移是0x10,因为InMemoryOrderLinks里面还有两个指针所以0x4*0x4=0x10
    PVOID EntryPoint;//指向模块的入口点函数地址
    PVOID Reserved3;
    UNICODE_STRING FullDllName;
    BYTE Reserved4[8];
    PVOID Reserved5[3];
    union {
        ULONG CheckSum;
        PVOID Reserved6;
    };
    ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

具体思路

此处的偏移地址都是相对地址

  1. 读取PEB结构
  2. 跳转到0xC偏移出读取Ldr指针(2+1+1+2x4=12个字节处)
  3. 跳转到0x14偏移处读取 InMemoryOrderModuleList字段(8+4x3=20个字节)
  4. 通过遍历Flink指针访问到第3个已加载模块(kernel32.dll ),这个是相对固定的,可以通过以下代码验证

InMemoryOrderModuleList链表按照如下次序显示所有已加载模块:

  1. 当前可执行文件(你运行一下下面的代码就会知道我是什么意思)
  2. ntdll.dll
  3. KERNEL32.DLL
#include <Windows.h>
#include <stdio.h>
#include <psapi.h>

int main() {
    HMODULE hModules[1024];
    DWORD cbNeeded;

    // 获取当前进程加载的模块句柄数组
    if (EnumProcessModules(GetCurrentProcess(), hModules, sizeof(hModules), &cbNeeded)) {
        DWORD numModules = cbNeeded / sizeof(HMODULE);

        // 遍历模块句柄数组,获取模块信息
        for (DWORD i = 0; i < numModules; ++i) {
            WCHAR szModName[MAX_PATH];
            if (GetModuleFileNameExW(GetCurrentProcess(), hModules[i], szModName, MAX_PATH)) {
                printf("Module Path: %ls\n", szModName);
            }
        }
    }

    return 0;
}

PE文件格式

在这里只介绍编写shellcode所必需的信息:头(header),节(section)和导出表。

IMAGE_DOS_HEADER 是一个结构,它在可执行文件(如 EXE 或 DLL)的开头部分,用于标识文件的 DOS 头部。虽然现代 Windows 系统主要使用 PE(Portable Executable)格式的可执行文件,但为了向后兼容和支持 DOS 环境,可执行文件的开头通常包含一个 DOS 头部。

typedef struct _IMAGE_DOS_HEADER {
    WORD   e_magic;        // 一个 2 字节的字段,通常被称为 DOS 魔数,用于标识该文件是否为 DOS 可执行文件。它的值为 0x5A4D(ASCII码中的 'MZ'),表示这是一个 DOS 可执行文件。
    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;       // //0x3c偏移处;一个 4 字节的字段,指向它指出了PE头所的位置,表示 PE 头部相对于文件开始的偏移。当一个 PE 可执行文件被加载到内存中执行时,它的 DOS 头部会被忽略,而 PE 头部的偏移地址由e_lfanew 字段指定。PE 头部包含了更多关于该可执行文件的信息,如导出表、导入表、节表等等。
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

IMAGE_NT_HEADERS 是 PE(Portable Executable)格式的可执行文件的头部信息结构,它位于可执行文件的 DOS 头部之后,用于描述 PE 文件的详细信息。

// 32 位可执行文件的 IMAGE_NT_HEADERS 结构
typedef struct _IMAGE_NT_HEADERS {
    DWORD Signature;	 // PE 文件签名,通常为 0x00004550 (ASCII码中的 'PE\0\0'),4个字节
    IMAGE_FILE_HEADER FileHeader;// 文件头部信息,20 字节
    IMAGE_OPTIONAL_HEADER32 OptionalHeader; // 可选头部信息,占224字节,偏移为0x18
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

IMAGE_OPTIONAL_HEADER 是 PE(Portable Executable)格式的可执行文件中的可选头部信息结构。

typedef struct _IMAGE_OPTIONAL_HEADER {
  WORD                 Magic;
  BYTE                 MajorLinkerVersion;
  BYTE                 MinorLinkerVersion;
  DWORD                SizeOfCode;
  DWORD                SizeOfInitializedData;
  DWORD                SizeOfUninitializedData;
  DWORD                AddressOfEntryPoint;	//exe/dll 开始执行代码的地址,即入口点地址。 
  DWORD                BaseOfCode;
  DWORD                BaseOfData;
  DWORD                ImageBase; //DLL加载到内存中的地址,即映像基址。 
  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];//0x60导入或导出函数等信息;需要用到这个结构里面的第一个元素IMAGE_DATA_DIRECTORY Export(即导出表)
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

IMAGE_DATA_DIRECTORY 是一个数据目录项结构,在 Windows PE(Portable Executable)可执行文件格式中使用。它用于描述可执行文件中不同的数据目录,其中包含了各种重要的数据结构,如导入表、导出表、资源表、重定位表等等。

typedef struct _IMAGE_DATA_DIRECTORY {
    DWORD VirtualAddress; // 导出目录在内存中的 RVA(相对虚拟地址) 0x78(18+60)相对于PE头
    DWORD Size;           // 数据目录的大小(字节)
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

IMAGE_EXPORT_DIRECTORY 是 Windows PE(Portable Executable)可执行文件格式中的一个结构体,用于描述导出表(Export Table)。导出表是一个非常重要的数据目录,用于标识可执行文件中所导出的函数和符号,以便其他模块或程序能够调用它们。

typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;       // 导出表的特征值
    DWORD   TimeDateStamp;         // 时间戳
    WORD    MajorVersion;          // 主版本号
    WORD    MinorVersion;          // 次版本号
    DWORD   Name;                  // 模块名称的 RVA(相对虚拟地址)
    DWORD   Base;                  // 序号基址
    DWORD   NumberOfFunctions;     // 导出函数数量
    DWORD   NumberOfNames;         // 导出函数名称数量
    DWORD   AddressOfFunctions;    // 存放着函数地址的偏移量,kernel32地址+偏移量就可以得到函数地址 0x1c
    DWORD   AddressOfNames;        // 存放着函数名的偏移量,kernel32地址+偏移量就可以得到函数名 0x20
    DWORD   AddressOfNameOrdinals; //  存放着函数的序号,不一定是从1开始的,知道序号之后就可以去AddressOfFunctions找地址,这是一个数组 函数地址=AddressOfFunctions[ 序号(函数名称) ] 0x24
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

整个流程

  1. 从IMAGE_DOS_HEADER(e_lfanew 0x3c)取出IMAGE_NT_HEADERS偏移地址
  2. MAGE_NT_HEADERS+0x18+0x60到IMAGE_DATA_DIRECTORY结构获取IMAGE_EXPORT_DIRECTORY偏移地址
  3. kernel32地址+IMAGE_EXPORT_DIRECTORY偏移地址到IMAGE_EXPORT_DIRECTORY
  4. 接下来就是获取0x1c,0x20,0x24三个偏移地址

具体代码

#include <iostream>

int main()
{
    __asm
    {
        xor ecx, ecx
        mov eax, fs: [ecx + 0x30] ; EAX = PEB
        mov eax, [eax + 0xc]; EAX = PEB->Ldr
        mov esi, [eax + 0x14]; ESI = PEB->Ldr.InMemOrder
        lodsd; EAX = Second module
        xchg eax, esi; EAX = ESI, ESI = EAX
        lodsd; EAX = Third(kernel32)
        mov ebx, [eax + 0x10]; EBX = Base address
        mov edx, [ebx + 0x3c]; EDX = DOS->e_lfanew
        add edx, ebx; EDX = PE Header
        mov edx, [edx + 0x78]; EDX = Offset export table
        add edx, ebx; EDX = Export table
        mov esi, [edx + 0x20]; ESI = Offset namestable
        add esi, ebx; ESI = Names table
        xor ecx, ecx; EXC = 0

        Get_Function:

        inc ecx; Increment the ordinal
            lodsd; Get name offset
            add eax, ebx; Get function name
            cmp dword ptr[eax], 0x50746547; GetP
            jnz Get_Function
            cmp dword ptr[eax + 0x4], 0x41636f72; rocA
            jnz Get_Function
            cmp dword ptr[eax + 0x8], 0x65726464; ddre
            jnz Get_Function
            mov esi, [edx + 0x24]; ESI = Offset ordinals
            add esi, ebx; ESI = Ordinals table
            mov cx, [esi + ecx * 2]; Number of function
            dec ecx
            mov esi, [edx + 0x1c]; Offset address table
            add esi, ebx; ESI = Address table
            mov edx, [esi + ecx * 4]; EDX = Pointer(offset)
            add edx, ebx; EDX = GetProcAddress
            mov edi,edx

            xor ecx, ecx; ECX = 0
            push ebx; Kernel32 base address
            push edx; GetProcAddress
            push ecx; 0
            push 0x41797261; aryA
            push 0x7262694c; Libr
            push 0x64616f4c; Load
            push esp; "LoadLibrary"
            push ebx; Kernel32 base address
            call edi; GetProcAddress(LL)

            add esp, 0xc; pop "LoadLibrary"
            pop ecx; ECX = 0
            push eax; EAX = LoadLibrary
            push ecx
            push 0x006c6c64; dll0 0截断字符串
            push 0x2e32336c; l32.
            push 0x6c656873; shel
            push esp; "shell32.dll"
            call eax; LoadLibrary("shell32.dll");函数执行后eax = shell32.dll指针
            ; 获取ShellExecuteA的地址
            add esp, 0x14
            mov edx, [esp + 0x4]; edx = GetProcAddress
            xor ecx, ecx
            push ecx; 0
            push 0x00000041; A0
            push 0x65747563; cute
            push 0x6578456C; lExe
            push 0x6C656853; Shel
            push esp; ShellExecuteA
            push eax; shell32.dll
            call edi; GetProcAdress(ShellExecute)
            ; ִShellExecuteA(NULL, "open", "calc.exe", NULL, NULL, SW_SHOWNORMAL)
            mov edi,eax
            add esp, 0xc
            xor ecx, ecx
            push 0
            push 0x6578652e; .exe
            push 0x636c6163; calc
            push esp; calc.exe的指针
            push 0
            push 0x6e65706f; open
            push esp; open的指针
            push 0x00000001; SW_SHOWNORMAL参数
            push 0x00000000; NULL
            push 0x00000000; NULL
            push[esp + 0x18]; calc.exe的指针, 因为前面push了6个参数,每个参数4个字节
            push[esp + 0x10]; open的指针
            push 0x00000000; NULL

        call edi; ShellExecute(NULL, "open", "calc.exe", NULL, NULL, SW_SHOWNORMAL)

    }
    return 0;
}

参考链接

https://securitycafe.ro/2016/02/15/introduction-to-windows-shellcode-development-part-3/