分享
 
 
 

绕过Copy-On-Write机制安装全局Hook

王朝other·作者佚名  2006-04-28
窄屏简体版  字體: |||超大  

创建时间:2005-10-22

文章属性:原创

文章提交:Addylee (Addylee2004_at_163.com)

Jeffrey Richter在他的<<widows核心编程>>一书中对Ring 3级的API Hook方法做了详细的介绍,但是一般的Ring 3无论是修改IAT,还是插入JMP XXX都将导致Copy-On-Write的发生,如果,要在系统范围内安装一个全局的Hook的话,就不得不枚举系统中所有进程,对所有进程中的相应模块做同样的修改,这样以来,对系统性能,是有一定的负面影响的。另一方面,如果要做系统范围内的全局Hook的话,可以直接在Ring 0级通过Hook系统调,修改目标API的指令等方法实现。但是,代码在Ring 0的地址空间中,Ring 3环境下的程序无法直接调用。

由于Windows利用了PTE中的第9位用于Copy-On-Write机制。而Ring 3的代码无法访问PTE的,因此要绕过Copy-On-Write的话,该程序还是无法避免的要工作在Ring 0环境下。本文将以修改Kernel32.dll内存映象中的CreateProcessW为例,介绍绕过Copy-On-Write实现全局Hook的一种方法。我的实验环境是Windows 2000 SP4 内部版本2195。因为EProcess的未公开原因,本例在其它版本的Windows不能保证正确运行。

一般情况下,每个进程都加载了Kernel32.dll这个模块,并且绝大多数情况下Kernel32在每个进程中所加载的基址都一样,在物理内存中,也只有一份Kernel32的映象,所以可以让用户程序LoadLibrary后,把Kernel32的基地址发到Ring 0的驱动程序中,让驱动程序来修改相应PTE,禁了Copy-On-Write后再修改相应的API指令就行了,但是,为了防止某种可能,比如:之前有一个进程也对它进行了写操作,让系统中有了两份或多份Kernel32的映象,而在用户级LoadLibrary,最多只可能修改到某一个映象,所以,我从内核中枚举了所有的EPROCESS结构,再根据PEB_LDR_DATA结构中找到它的所加载的模信息,对其修改。 直接操作各个进程地址空间的数据,很不方便,可以用Windows 未公开API,KeAttachProcess, 函数来切换到指定进程的内存上下文环境。把CreateProcessW的入口处改成了JMP XXX,但是,跳到哪去呢?程序工作在

Ring 0下,CreateProcessW不可能直接那里边的一个函数中的,但是,PE文件中每个节都会存在一些“空洞”,kernel32也不例外,就把代码Copy到Kernel32的某个节区的“空洞”中去吧。如果“空洞”太小,怎么办呢?可以把我们的代码写成一个DLL,在那个“空洞”中放上一小段代码来Load这个DLL,当然,也有可能在某种极端的情况下,这点“空洞”还是不够 就:(

struct _hardware_pte_x86 (sizeof=4)

+0 bits0-0 valid

+0 bits1-1 write

+0 bits2-2 owner

+0 bits3-3 writethrough

+0 bits4-4 cachedisable

+0 bits5-5 accessed

+0 bits6-6 dirty

+0 bits7-7 largepage

+0 bits8-8 global

+0 bits9-9 copyonwrite

+0 bits10-10 prototype

+0 bits11-11 reserved

+0 bits12-31 pageframenumber

从上面可以看出,bits 9 被用于Copy-On-Write机制,以下这段内容摘自<<Undocumented Windows NT>>

The VirtualProtect() function does not mark the page as read-write–it keeps the page as

read-only. Nevertheless, to distinguish this page from normal read-only pages, it is marked for copy-on-write. Windows NT uses one of the available PTE bits for doing this. When this page is written onto, because it is a read-only page, the processor raises a page fault exception. The page fault handler makes a copy of the page and modifies the page table of the faulting process accordingly. The new copy is marked as read-write so that the process can write to it.

所以只要把Read-Only属性去掉,再对这个DLL进行写入,就可以绕过Copy-On-Write机制了。:)

#include <ntddk.h>

#include "proc.h" // 进程块的结构信息

#include "PE.h" // PE文件的一些结构信息

#include "Page.h" // 页表,页目录操作

#define PEBOFFSET 0x1B0 // PEB指针位于EPPROCESS中偏移0x1B0处

#define FLINKOFFSET 0xA0 // 进程的链表指针。这些信息可以通过kd得到。

// 为了突出重点,节省篇幅,硬编码了两个未公开API的地址,其实也可以通过操作PE

// 的导出表来得到这些信息。

typedef NTSTATUS (NTAPI *KEATTACHPROCESS)(PPEB);

typedef NTSTATUS (NTAPI *KEDETACHPROCESS)();

KEATTACHPROCESS KeAttachProcess = 0x8042bd32;

KEDETACHPROCESS KeDetachProcess = 0x8042beca;

NTSTATUS DriverEntry(IN PDRIVER_OBJECT pDriverObject, IN PUNICODE_STRING pRegistryPath)

{

UNICODE_STRING Kernel32;

RtlInitUnicodeString(&Kernel32, L"C:\\WINNT\\SYSTEM32\\KERNEL32.dll");

__try

{

Hook(&Kernel32, "CreateProcessW", NewCreateProcessW);

}

__except(EXCEPTION_EXECUTE_HANDLER)

{

DbgPrint("Error");

}

RtlFreeUnicodeString(&Kernel32);

return STATUS_SUCCESS;

}

VOID Hook(PUNICODE_STRING pModuleName, PCHAR pFunctionName, PVOID pfnNewFunction)

{

PLIST_ENTRY pCurrentList = NULL, pTempList = NULL, pLoadOrderModuleList, list;

PPEB pPeb = NULL;

PHYSICAL_ADDRESS paOld, paCurrent;

ULONG hModule, temp, pEProcess;

paOld.QuadPart = 0;

paCurrent.QuadPart = 0;

pEProcess = (ULONG)IoGetCurrentProcess();

pCurrentList = (PLIST_ENTRY)(pEProcess + FLINKOFFSET);

pTempList = pCurrentList;

// 对所有进程进行枚举。

do

{

pEProcess = (ULONG)pTempList - FLINKOFFSET;

pPeb = (PPEB)(*(PULONG)(pEProcess + PEBOFFSET));

if (pPeb != NULL)

{

KeAttachProcess(pEProcess); // 切换内存上下文到指定的进程

pLoadOrderModuleList = pPeb->LoaderData->InLoadOrderModuleList.Flink;

list = pLoadOrderModuleList;

do // 遍历进程所加载模块中,直到找到kernel32

{

if(0==RtlCompareUnicodeString(&(((PLDR_MODULE)list)->FullDllName), pModuleName, TRUE)))

{

hModule = ((PLDR_MODULE)list)->BaseAddress;

//Kernel32可能被调出,对它进行一次读操作,由于KeAttachProcess切换到了该进程的地址空间

//所以让Windows自动处理缺页,把它调入吧,以免它“漏网”:)

temp = *(PULONG)hModule;

paCurrent = MmGetPhysicalAddress(hModule);

// 如果上次处理过的,和现在的在同一物理地址上,就不处理了,当然如果,在内存中,有两个

// 以上的映像的话,这种处理方法不是很有效(还是造成了重复的工作)不过,在我实验中,

// 似乎总是只有一份Kernel32的内存映象,做个循环,只不过是为了以防万一。:)

if (paOld.QuadPart != paCurrent.QuadPart)

{

paOld.QuadPart = paCurrent.QuadPart;

Patch(hModule, pFunctionName, pfnNewFunction);

}

break;

}

list = list->Flink;

} while(list != pLoadOrderModuleList);

KeDetachProcess();

}

pTempList = pTempList->Flink;

} while(pTempList != pCurrentList);

}

VOID Patch(PVOID hModule, PCHAR pFunctionName, PVOID pfnNewFunction)

{

ULONG len, n;

PVOID pfnOrig, SectionGapStart;

pfnOrig = GetFunctionAddress(hModule, pFunctionName);

len = GetMyFunctionLen(pfnNewFunction);

SectionGapStart = GetSectionGap(hModule, len);

if (SectionGapStart == NULL)

return NULL;

PTE_ENTRY((ULONG)SectionGapStart) |= 2; // Read-Only 位。

for (n = 0; n < len; n++)

{

*(PUCHAR)((PUCHAR)SectionGapStart + n) = *(PUCHAR)((PUCHAR)pfnNewFunction + n);

}

/* 我的机器上的CreateProcessW的代码,是这样了。

KERNEL32!CreateProcessW

001B:77E6B252 55 PUSH EBP

001B:77E6B253 8BEC MOV EBP, ESP

001B:77E6B255 FF752C PUSH DWORD PTR [EBP+2C]

第二条和第三条指令正好是5Byte的长度,所以,我选择把第二条和第三条改成跳转指令。

跳转指令码为0xE9,位移计算:目的地址 - 起始地址 - 跳转指令本身的长度。

*/

for (len = 1; len <= 5; len++, n++)

*(PUCHAR)((PUCHAR)SectionGapStart + n) = *(PUCHAR)((PUCHAR)pfnOrig + len);

*(PUCHAR)((PUCHAR)SectionGapStart + n) = 0xE9;

*(PULONG)((PUCHAR)SectionGapStart+n+1) = (ULONG)(((PUCHAR)pfnOrig + 6)

- (ULONG)((PUCHAR)SectionGapStart + n) - 5);

PTE_ENTRY((ULONG)SectionGapStart) &= 0xFFD;

PTE_ENTRY((ULONG)pfnOrig) |= 2;

n += 6;

__asm CLI

*(PUCHAR)((PUCHAR)pfnOrig + 1) = 0xE9;

*(PULONG)((PCHAR)pfnOrig + 2) = ((ULONG)SectionGapStart - (ULONG)((PUCHAR)pfnOrig+1) - 5);

__asm STI

PTE_ENTRY((ULONG)pfnOrig) &= 0xFFD;

}

// 根据指定的模块获取代码节的“空洞”偏移地址。

PVOID GetSectionGap(PVOID hModule, USHORT GapSize)

{

PIMAGE_DOS_HEADER pDosHeader = hModule;

PIMAGE_NT_HEADERS pNtHeader;

PIMAGE_SECTION_HEADER pSectionHeader;

ULONG n = 0;

if (pDosHeader->e_magic != 'ZM')

return NULL;

pNtHeader = (PIMAGE_NT_HEADERS)((PCHAR)hModule + pDosHeader->e_lfanew);

if (pNtHeader->Signature != 'EP')

return NULL;

pSectionHeader = (PCHAR)pNtHeader + sizeof(IMAGE_NT_HEADERS);

for (n = 0; n < pNtHeader->FileHeader.NumberOfSections; n++)

{

pSectionHeader += n;

// 找到代码节,该法不总是有效,Borland编译器好像代码节为.CODE

if (IsStringEqual(pSectionHeader->Name, ".text"))

{

if ((PAGE_SIZE - (pSectionHeader->Misc.VirtualSize & PAGE_SIZE)) > GapSize)

{

return (ULONG)((PCHAR)hModule + pSectionHeader->VirtualAddress

+ pSectionHeader->Misc.VirtualSize);

}

}

}

return NULL;

}

// 从指定模块根据导出表获取导出函数地址。

PVOID GetFunctionAddress(PVOID hModule, PCHAR pFunctionName)

{

PIMAGE_DOS_HEADER pDosHeader = hModule;

PIMAGE_NT_HEADERS pNtHeader;

PIMAGE_EXPORT_DIRECTORY pExportDirectory;

ULONG n;

PULONG pExportFunction;

PULONG pFunctionAddress;

PUSHORT pAddressOridinals;

if (pDosHeader->e_magic != 'ZM')

return NULL;

pNtHeader = (PIMAGE_NT_HEADERS)((PCHAR)hModule + pDosHeader->e_lfanew);

if (pNtHeader->Signature != 'EP')

return NULL;

pExportDirectory = (PCHAR)hModule +

pNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress;

pExportFunction = (PCHAR)hModule + pExportDirectory->AddressOfNames;

pFunctionAddress = (PCHAR)hModule + pExportDirectory->AddressOfFunctions;

pAddressOridinals = (PCHAR)hModule + pExportDirectory->AddressOfNameOrdinals;

for (n = 0; n < pExportDirectory->NumberOfNames; n++)

{

if (IsStringEqual(((PCHAR)hModule + *(pExportFunction + n)), pFunctionName) == TRUE)

return (PVOID)((PCHAR)hModule + *(pFunctionAddress + *(pAddressOridinals + n)));

}

}

这个函数是最晦涩的了。这段代码将被Copy到Kernel32的一个“空洞”中执行,既要处理重定位, 又要在那里,手工获取LoadLibrary和GetProcAddress的地址,而且VC中inline asm也没有masm32那么直接,更可恨的是 naked 函数中申明局部变量,好像会破坏了堆栈平衡,本来,想申明一两个register变量,加强程序的可读性,但是,又不能保证编译器,总是成功分配register变量,然后,又想,在一个__forceinline函数中,写所有的代码,在这个__forceinline函数,使用局部变量,加强可读性,而这个naked函数只要调用__forceinline函数就好了,但是,似乎也没办法让编译器总是inline成功。就只有全部自己写了 :(

这个函数从PEB中取kernel32的基地址,再根据导出表获取LoadLibrary和GetProcAddress的地址,然后加载User32.dll,关获取MessageBoxW的地址,再调用它。最后,再Free User32.dll。

__declspec(naked) NewCreateProcessW()

{

__asm

{

PUSHAD

MOV EAX, DWORD PTR FS:[0x30]

MOV EAX, DWORD PTR [EAX+0xC]

MOV ESI, DWORD PTR [EAX+0x1C]

LODSD

MOV EBX, DWORD PTR [EAX+0x8] // EBX: KERNEL32.DLL的基址

MOV EAX, EBX

ADD EAX, 0x3C

MOV EAX, [EAX]

ADD EAX, EBX

// EAX 定位到 IMAGE_DIRECTORY_ENTRY_EXPORT

ADD EAX, ((TYPE IMAGE_NT_HEADERS) - ((TYPE IMAGE_DATA_DIRECTORY)*16))

MOV EAX, [EAX]

ADD EAX, EBX // EAX -> 导出表

MOV ESI, [EAX + 0x20]

ADD ESI, EBX // ESI -> 导出函数名字的 RVA 数组

PUSH ESI

// int 3

MOV ECX, 0xB

CALL a0

a0:

POP EBP

LEA EDI, a0

SUB EBP, EDI

LEA EDI, pFreeLibrary

ADD EDI, EBP

CALL GetProcAddr // GetProcAddr 在 EDX 中返回 FreeLibrary 的地址

POP ESI

PUSH EDX // EDX = FreeLibrary 的地址, 保存起来

PUSH ESI

MOV ECX, 0xE

CALL a1

a1:

POP EBP

LEA EDI, a1

SUB EBP, EDI

LEA EDI, pGetProcAddress

ADD EDI, EBP

CALL GetProcAddr // GetProcAddr 在 EDX 中返回 GetProcAddress 的地址

POP ESI

PUSH EDX // 保存 EDX = GetProcAddress 的地址, 保存起来

MOV ECX, 0xC

CALL a2

a2:

POP EBP

LEA EDI, a2

SUB EBP, EDI

LEA EDI, pLoadLibraryA

ADD EDI, EBP

CALL GetProcAddr // GetProcAddr 在 EDX 中返回 LoadLibraryA 的地址

CALL a3

a3:

POP EBP

LEA EDI, a3

SUB EBP, EDI

LEA EDI, pUser32dll

ADD EBP, EDI

PUSH EBP

CALL EDX // 调用 LoadLibraryA

POP EDX // EDX = GetProcAddress 的地址

CALL a4

a4:

POP EBP

LEA EDI, a4

SUB EBP, EDI

LEA EDI, pMessageBoxW

ADD EBP, EDI

PUSH EAX

PUSH EBP

PUSH EAX // EAX = user32.dll的模块句柄

CALL EDX // 调用 GetProcAddress

PUSH 0

PUSH [esp + 0x38]

PUSH [esp + 0x3C]

PUSH 0

CALL EAX // 调用MessageBoxW

POP EAX

POP EDX // FreeLibrary 的地址

PUSH EAX

CALL EDX

JMP over

GetProcAddr:

MOV EDX, [EAX+0x18] //以名字导出的函数个数

FindNext:

PUSH EDI

PUSH ECX

PUSH EDX

PUSH ESI

MOV ESI, [ESI]

ADD ESI, EBX

CLD

REPE CMPSB

POP ESI

POP EDX

POP ECX

POP EDI

JZ Found

ADD ESI, 4

DEC EDX

JNZ FindNext

Found:

MOV ECX, [EAX+0x18]

SUB ECX, EDX

SAL ECX, 1

MOV EDX, [EAX+0x24] // 导出函数序号表 的 RVA

ADD EDX, EBX

ADD EDX, ECX

XOR ECX, ECX

MOV CX, WORD PTR [EDX] // ECX <= 编号

SAL ECX, 1

SAL ECX, 1

MOV EDX, [EAX+0x1C]

ADD EDX, EBX

ADD EDX, ECX

MOV EDX, [EDX]

ADD EDX, EBX

RET

pLoadLibraryA:

_emit 'L'

_emit 'o'

_emit 'a'

_emit 'd'

_emit 'L'

_emit 'i'

_emit 'b'

_emit 'r'

_emit 'a'

_emit 'r'

_emit 'y'

_emit 'A'

_emit 0

pGetProcAddress:

_emit 'G'

_emit 'e'

_emit 't'

_emit 'P'

_emit 'r'

_emit 'o'

_emit 'c'

_emit 'A'

_emit 'd'

_emit 'd'

_emit 'r'

_emit 'e'

_emit 's'

_emit 's'

_emit 0

pMessageBoxW:

_emit 'M'

_emit 'e'

_emit 's'

_emit 's'

_emit 'a'

_emit 'g'

_emit 'e'

_emit 'B'

_emit 'o'

_emit 'x'

_emit 'W'

_emit 0

pUser32dll:

_emit 'U'

_emit 's'

_emit 'e'

_emit 'r'

_emit '3'

_emit '2'

_emit '.'

_emit 'd'

_emit 'l'

_emit 'l'

_emit 0

pFreeLibrary:

_emit 'F'

_emit 'r'

_emit 'e'

_emit 'e'

_emit 'L'

_emit 'i'

_emit 'b'

_emit 'r'

_emit 'a'

_emit 'r'

_emit 'y'

_emit 0

over:

POPAD

_emit 0x90

_emit 0x90

_emit 0x90

_emit 0x90

_emit 0x90

_emit 0x90

_emit 0x90

_emit 0x90

_emit 0x90

_emit 0x90

_emit 0x90

_emit 0x90

_emit 0x90

}

}

要把代码Copy到指别的地方,就要知道要Copy的字节数,本来,在masm32中很简单的一件事,定义两个标号就OK了,在这VC里确变得麻烦起来。

ULONG GetMyFunctionLen(PULONG pfn)

{

ULONG res = 0;

__asm

{

MOV EAX, 0x90909090 // 新的函数以0x90909090作为结束的标志

MOV ECX, 0xFFFFFFFF

MOV EDI, pfn

CLD

REPNZ SCASD

NOT ECX

DEC ECX

MOV res, ECX

}

return res * 4;

}

自己写了一段代码比较两个字符串。

BOOLEAN IsStringEqual(PCHAR psrc, PCHAR pdest)

{

BOOLEAN res = FALSE;

__asm

{

XOR EAX, EAX

MOV ECX, EAX

DEC ECX

MOV EDI, psrc

CLD

REPNZ SCASB

NOT ECX

DEC ECX

MOV EDX, ECX

MOV ECX, EAX

DEC ECX

MOV EDI, pdest

REPNZ SCASB

NOT ECX

DEC ECX

CMP ECX, EDX

JNE over

MOV ESI, psrc

MOV EDI, pdest

REPE CMPSB

JNZ over

MOV res, TRUE

over:

}

return res;

}

水平有限,纰漏之处难免,希望诸位大虾斧正。

以上几个头文件都是一些结构和宏的定义,就没有帖出来了。

若有不正确的地方,欢迎交流,QQ: 22517257

参考资料:

Windows 环境下32位汇编语言程序设计 罗云彬 著

Rootkits: Subverting the Windows Kernel By Greg Hoglund, James Butler

Undocumented Windows NT

Undocumented Windows 2000 Secrets

 
 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
2023年上半年GDP全球前十五强
 百态   2023-10-24
美众议院议长启动对拜登的弹劾调查
 百态   2023-09-13
上海、济南、武汉等多地出现不明坠落物
 探索   2023-09-06
印度或要将国名改为“巴拉特”
 百态   2023-09-06
男子为女友送行,买票不登机被捕
 百态   2023-08-20
手机地震预警功能怎么开?
 干货   2023-08-06
女子4年卖2套房花700多万做美容:不但没变美脸,面部还出现变形
 百态   2023-08-04
住户一楼被水淹 还冲来8头猪
 百态   2023-07-31
女子体内爬出大量瓜子状活虫
 百态   2023-07-25
地球连续35年收到神秘规律性信号,网友:不要回答!
 探索   2023-07-21
全球镓价格本周大涨27%
 探索   2023-07-09
钱都流向了那些不缺钱的人,苦都留给了能吃苦的人
 探索   2023-07-02
倩女手游刀客魅者强控制(强混乱强眩晕强睡眠)和对应控制抗性的关系
 百态   2020-08-20
美国5月9日最新疫情:美国确诊人数突破131万
 百态   2020-05-09
荷兰政府宣布将集体辞职
 干货   2020-04-30
倩女幽魂手游师徒任务情义春秋猜成语答案逍遥观:鹏程万里
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案神机营:射石饮羽
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案昆仑山:拔刀相助
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案天工阁:鬼斧神工
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案丝路古道:单枪匹马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:与虎谋皮
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:李代桃僵
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案镇郊荒野:指鹿为马
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:小鸟依人
 干货   2019-11-12
倩女幽魂手游师徒任务情义春秋猜成语答案金陵:千金买邻
 干货   2019-11-12
 
推荐阅读
 
 
 
>>返回首頁<<
 
靜靜地坐在廢墟上,四周的荒凉一望無際,忽然覺得,淒涼也很美
© 2005- 王朝網路 版權所有