在Windows系统中编写shellcode
步骤
- 获取kernel32.dll 基地址;
- 定位 GetProcAddress函数的地址;
- GetProcAddress函数是Windows操作系统提供的一个API函数,用于获取指定DLL(Dynamic Link Library)中导出函数的地址。
- 使用GetProcAddress确定 LoadLibrary函数的地址;
- LoadLibrary函数是Windows操作系统提供的一个API函数,它用于加载一个动态链接库(DLL)到当前进程的地址空间,从而使得这个DLL中的函数可以被当前进程调用。
- 然后使用 LoadLibrary加载DLL文件(例如user32.dll);
- 使用 GetProcAddress查找某个函数的地址(例如MessageBox);
- 指定函数参数;
- 调用函数。
进程块
在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;
具体思路
此处的偏移地址都是相对地址
- 读取PEB结构
- 跳转到0xC偏移出读取Ldr指针(2+1+1+2x4=12个字节处)
- 跳转到0x14偏移处读取 InMemoryOrderModuleList字段(8+4x3=20个字节)
- 通过遍历Flink指针访问到第3个已加载模块(kernel32.dll ),这个是相对固定的,可以通过以下代码验证
InMemoryOrderModuleList链表按照如下次序显示所有已加载模块:
- 当前可执行文件(你运行一下下面的代码就会知道我是什么意思)
- ntdll.dll
- 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;
整个流程
- 从IMAGE_DOS_HEADER(e_lfanew 0x3c)取出IMAGE_NT_HEADERS偏移地址
- MAGE_NT_HEADERS+0x18+0x60到IMAGE_DATA_DIRECTORY结构获取IMAGE_EXPORT_DIRECTORY偏移地址
- kernel32地址+IMAGE_EXPORT_DIRECTORY偏移地址到IMAGE_EXPORT_DIRECTORY
- 接下来就是获取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/