首页
社区
课程
招聘
[原创]《Windows 跨进程注入:从 OpenProcess 到 CreateRemoteThread 的 Shellcode 实践》
发表于: 3天前 274

[原创]《Windows 跨进程注入:从 OpenProcess 到 CreateRemoteThread 的 Shellcode 实践》

3天前
274

前言:

最近在学习Windwos Api, 学到了一些可以操作内存以及进程的函数:OpenProcess/CreateRemoteThread...

忽然发现,可以通过 OpenProcess->VirtualAllocEx->WriteProcessMemory->CreateRemoteThread,通过查阅

了一番资料后,开始了一段惊险刺激的跨进程注入


目标与思路: 

在目标程序内弹出一个弹窗:

1.先获取到Kernel32.dll的基地址(通过取到基地址来获取到 dos头,pe头,以及函数的实际运行地址)

2.获取LoadLibraryA函数以及GetProcAddress函数的地址

3.调用LoadLibraryA函数加载user32.dll并获取到user32.dll的模块句柄

4.通过GetProcAddress找到MessageBoxA函数的地址

5.调用MessageBoxA函数并成功弹窗


前置知识:

1. PEB与模块链表

    PEB是什么?

        PEB(Process Environment Block,进程环境块)是 Windows 为每个进程维护的核心数据结构,存储该进程的全局信

     息。每个进程只有一个 PEB,而每个线程的 TEB 中都有一个指针指向所属进程的 PEB。 本质上就是一个套娃的过程,最后

     套出模块地址

    

    如何获取?

     32位下: 位于fs:[0x30]

     64位下: 位于gs:[0x60]

    

    PEB->Ldr (Ldr指向 PEB_LDR_DATA,模块链表) 


// PEB_LDR_DATA结构体定义:
typedef struct _PEB_LDR_DATA {
    BYTE  Reserved1[8];
    PVOID Reserved2[3];
    LIST_ENTRY InMemoryOrderModuleList;  // <-----按模块在内存中的基址顺序排序的双向链表头
} PEB_LDR_DATA, *PPEB_LDR_DATA;

// LIST_ENTRY结构体定义:
typedef struct _LIST_ENTRY {
   struct    _LIST_ENTRY *Flink;  // <-----指向下一个模块的 InMemoryOrderLinks
   struct    _LIST_ENTRY *Blink;  // <-----指向上一个模块的 InMemoryOrderLinks
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

// InMemoryOrderLinks其实是LDR_DATA_TABLE_ENTRY结构体内的一个字段,我们只是使用他来遍历LDR_DATA_TABLE_ENTRY
// 来获取到对应的模块名称

// MY_LDR_DATA_TABLE_ENTRY自定义结构体:(这是根据32位Windows实际布局简化的结构体)
typedef struct _MY_LDR_DATA_TABLE_ENTRY {
    LIST_ENTRY InLoadOrderLinks;              // +0x00
    LIST_ENTRY InMemoryOrderLinks;            // +0x08  ← 链表节点在这里,通过遍历这个双向链表来获取程序加载所有的模块
    LIST_ENTRY InInitializationOrderLinks;    // +0x10
    PVOID      DllBase;                       // +0x18  ← 模块基址
    PVOID      EntryPoint;                    // +0x1C
    ULONG      SizeOfImage;                   // +0x20
    UNICODE_STRING FullDllName;               // +0x24
    UNICODE_STRING BaseDllName;               // +0x2C  ← Buffer
} MY_LDR_DATA_TABLE_ENTRY, * PMY_LDR_DATA_TABLE_ENTRY;

// UNICODE_STRING结构体定义:
typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR  Buffer;
} UNICODE_STRING;
// 注意:这些结构体属于 Windows 内部实现,未在官方文档中公开,不同 Windows 版本可能有差异。本文基于 32 位 Windows 10 实测


2. PE导出表

// 导出表结构体:
typedef struct _IMAGE_EXPORT_DIRECTORY {
    DWORD   Characteristics;
    DWORD   TimeDateStamp;
    WORD    MajorVersion;
    WORD    MinorVersion;
    DWORD   Name;
    DWORD   Base;
    DWORD   NumberOfFunctions;
    DWORD   NumberOfNames;          // AddressOfNameOrdinals/AddressOfNames数组的长度
    DWORD   AddressOfFunctions;     // 函数地址数组
    DWORD   AddressOfNames;         // 函数名称数组
    DWORD   AddressOfNameOrdinals;  // 函数名索引到函数地址索引的映射关系,每个元素都是AddressOfFunctions的索引
} IMAGE_EXPORT_DIRECTORY, * PIMAGE_EXPORT_DIRECTORY;

// 导出表获取方式:dos头->pe头->OptionalHeader->DataDirectory->export


在理解了PEB和导出表的结构后,先用C代码在本机验证思路,确认能正确拿到kernel32基址和函数地址。

HMODULE GetKernel32Base() {

	PPEB peb = (PPEB)__readfsdword(0x30);
	PPEB_LDR_DATA ldr = *(PPEB_LDR_DATA*)((DWORD)peb + 0x0c);

	LIST_ENTRY* head = &ldr->InMemoryOrderModuleList;
	LIST_ENTRY* curr = head->Flink;

	// 遍历链表获取kernel32.dll的基地址
	while (curr != head) {

		// Flink 指向的是下一个模块的 InMemoryOrderLinks,
		// 而 InMemoryOrderLinks 位于 MY_LDR_DATA_TABLE_ENTRY 的 +0x08 处。
		// 所以要用 CONTAINING_RECORD 减去这个偏移,才能拿到结构体首地址。
		PMY_LDR_DATA_TABLE_ENTRY entry = CONTAINING_RECORD(curr, MY_LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);

		// 在对比字符串时,使用_wcsicmp是因为在BaseDllName.Buffer中存储的是宽字符,每个字符占用2个字节
		if (entry->BaseDllName.Buffer && _wcsnicmp(entry->BaseDllName.Buffer, L"KERNEL32.DLL", 12) == 0) {
			return (HMODULE)entry->DllBase;
		}

		curr = curr->Flink;
	}

	return NULL;
}

// 获取函数地址
// HMODULE hModule: 模块的基地址
// const char* findFuncName:要寻找的函数名称
FARPROC GetFuncAddress(HMODULE hModule, const char* findFuncName) {

	// dos头
	IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)hModule;

	// nt头
	IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)((BYTE*)hModule + dos->e_lfanew);

	IMAGE_DATA_DIRECTORY* exportDir = &nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];

	// 导出表
	IMAGE_EXPORT_DIRECTORY* exportDirAddress = (IMAGE_EXPORT_DIRECTORY*)((BYTE*)hModule + exportDir->VirtualAddress);

	// 函数地址数组
	DWORD* functions = (DWORD*)((BYTE*)hModule + exportDirAddress->AddressOfFunctions);

	// 函数名称数组
	DWORD* names = (DWORD*)((BYTE*)hModule + exportDirAddress->AddressOfNames);

	// 函数索引数组  每个元素是 AddressOfFunctions 的索引
	WORD* ordinals = (WORD*)((BYTE*)hModule + exportDirAddress->AddressOfNameOrdinals);

	// 匹配名称,找到对应的函数地址
	for (DWORD i = 0; i < exportDirAddress->NumberOfNames; ++i) {

		char* funcName = (char*)((BYTE*)hModule + names[i]);

		// 这里使用strcmp比较字符是因为导出表里的函数名是ANSI字符串,每个字符大小1字节
		if (strcmp(funcName, findFuncName) == 0) {

			WORD ord = ordinals[i];
			DWORD funcRva = functions[ord];
			return (FARPROC)((BYTE*)hModule + funcRva);
		}
	}

	return NULL;

}

从C到汇编:

在真实的payload的编写过程中,并不能直接使用c语言去编写payload,因为:

1.不能依赖导入表:shellcode 不是 PE 文件,加载器不会为它解析 MessageBoxA 等函数的地址。

2.不能依赖 CRT:strcmp、malloc、printf 这些来自 C 运行时库,目标进程不一定加载,且需要初始化。

3.必须位置无关:VirtualAllocEx 返回的地址不确定,shellcode 里不能出现绝对地址。

所以必须用汇编重写,所有 API 地址通过 PEB + 导出表动态解析


实战以及踩坑点:

坑1:eax/ecx/edx 被 API 破坏

push 00007373h  ; 's' 's' '\0' '\0'

push 65726464h  ; 'd' 'd' 'r' 'e'

push 41636F72h  ; 'r' 'o' 'c' 'A'

push 50746547h  ; 'G' 'e' 't' 'P'

mov edx, esp

push 0eh       ; count

push edx    ; findFuncName

push ebx    ; hMoudule

call GetFuncAddress   

mov edx, eax   ; edx = GetProcAddress         ; <----------这里不应该使用edx保存GetProcAddress的地址


push 00006C6Ch   ; "ll\0\0"

push 642E3233h   ; "32.d"

push 72657375h   ; "user"

push esp

call ecx   ; LoadLibraryA("user32.dll") ; <---------这里执行完毕后 LoadLibraryA内部做了很多事情,损坏了edx中的值



push 0041786Fh   ; "oxA\0"

push 42656761h   ; "ageB"

push 7373654Dh   ; "Mess"

push esp

push eax

call edx          ; GetProcAddress(hMoudule, "MessAgeBoxA");  <--------------调用函数时跳转地址错误导致出现异常

mov ebx, eax


现象:弹窗没弹出来,目标进程直接0xC0000005

排查:通过x64dbg附加目标进程,发现在edx在调用时变成了一个无关地址

原因:eax/ecx/edx 是易失寄存器,LoadLibraryA 内部大量使用它们,不负责恢复。

解决:把跨 API 调用的值放到 ebx/esi/edi 或栈上。

坑2:pushad/popad导致堆栈不平衡

        pushad


push 0       ; \0

push 41797261h ; 'a' 'r' 'y' 'A'

push 7262694Ch ; 'L' 'i' 'b' 'r'

push 64616F4Ch ; 'L' 'o' 'a' 'd'


......


popad

ret


现象:弹窗能弹出来,但关闭弹窗后进程立刻 0xC0000005

排查:通过x64dbg附加目标进程,对比pushad和popad时的值,发现堆栈不平衡

原因:popad 只做 esp += 32,它假设当前 esp 就是 pushad 保存的位置,但中间压栈没清理

解决:在popad前进行平栈操作,将之前压入的字符全部出栈


ps:不能粗心大意

调试方法:

1. 通过x64dbg附加目标进程

2. 在执行完VirtualAllocEx函数后,获取到页面分配区域的基址处,通过x64dbg找到该地址并下断点

3. 写入payload后对比代码是否正确加载

4. 在可能出现问题的地方下断点,单步执行观察前后寄存器的值


总结:

跨进程注入的核心是“在目标进程里创建线程执行代码”,难点在于shellcode的位置无关性和寄存器/栈管理。


这篇文章记录了我从理解 PEB、PE 导出表,到用汇编实现跨进程 shellcode 的完整过程,虽然踩了不少坑但也收获了不少知识,

如果有理解错误的地方欢迎指正。


本文所有实验均在自己的虚拟机、自己的测试程序上完成,仅用于学习 Windows 系统编程原理。 如果有在学习逆向的同学,欢迎交流。


传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

收藏
点赞 0
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回