【Windows系统编程】05.内存操作与InlineHook(详解InlineHook实现)

内存相关

  • 内存信息

    头文件:#include <Psapi.h>

        //检索有关系统当前使用物理内存和虚拟内存的信息
        MEMORYSTATUSEX mst;
        GlobalMemoryStatusEx(&mst);
    
        //检索有关当前系统的信息
        SYSTEM_INFO SysInfo;
        GetSystemInfo(&SysInfo);
    
        //检索当前进程的伪句柄
        HANDLE hProcess = GetCurrentProcess();
        //检索调用进程的进程标识符。
        DWORD dwProcessId = GetCurrentProcessId();
    
        //检索有关指定进程的内存使用情况的信息
        PROCESS_MEMORY_COUNTERS pmc;
        GetProcessMemoryInfo(GetCurrentProcess(), &pmc, sizeof(PROCESS_MEMORY_COUNTERS));
    

    这里需要注意一点,上述API,后面带EX的通常可以跨进程获取信息,不带Ex的版本通常用来查询自身信息

  • 常用:

    查询虚拟内存属性,申请虚拟内存,修改属性

    本地内存读写:

        //保留、提交或更改调用进程的虚拟地址空间中页面区域的状态。 此函数分配的内存会自动初始化为零。
        LPVOID lpAddr = VirtualAlloc(
            NULL,          //要分配的内存的起始地址,如果为NULL,则由系统自动确定地址
            0x100,         //要分配的内存大小
            MEM_COMMIT,    //内存分配的类型,这里MEM_COMMIT基本上是通配了
            PAGE_READONLY  //分配内存的保护属性,这里PAGE_READONLY是只读属性,可以调用API修改的
        );
    
        //检索有关指定进程的虚拟地址空间中的页面范围的信息
        MEMORY_BASIC_INFORMATION mbi;
        VirtualQueryEx(
            GetCurrentProcess(),    //查询内存信息的进程句柄
            lpAddr,                 //指向要查询的页面区域的基址指针
            &mbi,                   //指向接收返回信息的MEMORY_BASIC_INFORMATION指针
            sizeof(MEMORY_BASIC_INFORMATION) //lpBuffer指向的缓冲区大小
        );
    
        //更改指定进程的虚拟地址空间中已提交的页面区域的保护
        DWORD OldProtect = 0;
        VirtualProtectEx(
            GetCurrentProcess(),        //要修改内存保护的进程句柄
            lpAddr,                     //要修改保护属性基址指针
            0x100,                      //要修改保护属性的内存大小
            PAGE_READWRITE,             //修改的新的保护属性
            &OldProtect                 //这里会写入原来的保护属性,为了避免被检测,通常修改之后还会修改回去
        );
    
        //给内存赋值,如果内存是只读属性,那么就会报错0x5(拒绝访问),需要修改内存属性
        memcpy(lpAddr, L"123456789", sizeof(L"123456789"));
    
        //写完内存之后,我们修改回去
        VirtualProtectEx(GetCurrentProcess(), lpAddr, 0x100, OldProtect, &OldProtect);
    
        memcpy(lpAddr, L"123456789", sizeof(L"123456789"));
    
        //释放虚拟内存
        VirtualFreeEx(
            GetCurrentProcess(),     //进程句柄
            lpAddr,                  //指向要释放内存的起始指针
            0,                       //要释放内存的大小,如果ddwFreeType属性为MEM_RELEASE,则必须为0
            MEM_RELEASE              //免费操作的类型
        );
    

    还是一样,这里所有API,Ex版本通常可以跨进程读写虚拟内存,不带Ex版本可以读写自身进程虚拟内存

  • 远程读写内存:

    ReadProcessMemory();
    WriteProcessMemory();
    
  • 堆实际上是由一种数据结构进行管理的内存

    我们用的new,malloc等都是在默认堆上申请,堆会随着我们申请空间而变大

    默认堆,我们也可以自己创建堆结构:

    	//创建可由调用进程使用的专用堆对象
    	HANDLE hHeap = HeapCreate(
    		HEAP_NO_SERIALIZE,       //堆分配选项
    		0,                       //堆的起始大小
    		1024                     //堆的最大大小
    	);
    
    	//从堆中分配内存块。分配的内存不可移动。
    	LPVOID lpHeapAddr = HeapAlloc(
    		hHeap,        //堆的句柄
    		HEAP_ZERO_MEMORY,   //堆分配选项,这里是初始化为0
    		MAX_PATH            //要分配的字节数
    	);
    
    	memcpy(lpHeapAddr, L"123456", sizeof(L"123456"));
    
    	//释放从堆中申请的内存
    	HeapFree(
    		hHeap,            //堆的句柄
    		HEAP_NO_SERIALIZE, //堆免费选项,这里是不会使用序列化访问
    		lpHeapAddr        //指向要释放内存的指针
    	);
    
    	//销毁指定堆对象
    	HeapDestroy(hHeap);
    

InlineHook

我们要HOOK的目标程序源码:

int main()
{
    
    MessageBox(NULL, L"WdIg111", L"提示", NULL);
    system("pause");
    MessageBox(NULL, L"WdIg222", L"提示", NULL);
}

程序拖进x32dbg:

0030181E | 6A 00                    | push 0                                  |
00301820 | 68 307B3000              | push <目标程序.L"\xFFD0:">                  | 307B30:"衏:y"
00301825 | 68 F87B3000              | push <目标程序.L"WdIg222">                  | 307BF8:L"WdIg222"
0030182A | 6A 00                    | push 0                                  |
0030182C | FF15 98B03000            | call dword ptr ds:[<&MessageBoxW>]      |

这是调用MessageBox的部分,有四个push是压入参数,我们来看看MessageBox实现部分:

770A8E20 | 8BFF                     | mov edi,edi                             |
770A8E22 | 55                       | push ebp                                |
770A8E23 | 8BEC                     | mov ebp,esp                             |
770A8E25 | 833D 8C8C0D77 00         | cmp dword ptr ds:[770D8C8C],0           |
770A8E2C | 74 22                    | je user32.770A8E50                      |
770A8E2E | 64:A1 18000000           | mov eax,dword ptr fs:[18]               |
770A8E34 | BA 18930D77              | mov edx,user32.770D9318                 | edx:"榍\f"
770A8E39 | 8B48 24                  | mov ecx,dword ptr ds:[eax+24]           | ecx:"榍\f"
770A8E3C | 33C0                     | xor eax,eax                             |
770A8E3E | F0:0FB10A                | lock cmpxchg dword ptr ds:[edx],ecx     | edx:"榍\f", ecx:"榍\f"
770A8E42 | 85C0                     | test eax,eax                            |
770A8E44 | 75 0A                    | jne user32.770A8E50                     |
770A8E46 | C705 288D0D77 01000000   | mov dword ptr ds:[770D8D28],1           |
770A8E50 | 6A FF                    | push FFFFFFFF                           |
770A8E52 | 6A 00                    | push 0                                  |
770A8E54 | FF75 14                  | push dword ptr ss:[ebp+14]              |
770A8E57 | FF75 10                  | push dword ptr ss:[ebp+10]              |
770A8E5A | FF75 0C                  | push dword ptr ss:[ebp+C]               |
770A8E5D | FF75 08                  | push dword ptr ss:[ebp+8]               |
770A8E60 | E8 0BFEFFFF              | call <user32.MessageBoxTimeoutW>        |
770A8E65 | 5D                       | pop ebp                                 |
770A8E66 | C2 1000                  | ret 10                                  |

这里就是MessageBox的实现了,那么我们要Hook它的实现,那么我们就要在这里做劫持,实际上就是修改前五个字节,做到jmp到我们构造的函数,

也就是:修改指令

770A8E20 | 8BFF                     | mov edi,edi                             |
770A8E22 | 55                       | push ebp                                |
770A8E23 | 8BEC                     | mov ebp,esp                             |

那么为什么是修改5个字节呢?因为我们要做到劫持,jmp到我们的地址上去,jmp指令(E9)占据一个字节,而在32位环境下,地址是四个字节长度,所以我们只需要修改五个字节就可以了

  • 实现:

    主要思想,就是发布一个dll,注入到目标程序,而我们前一讲刚学过,dll里面case DLL_PROCESS_ATTACH,实在进程加载dll的时候,就会触发,我们将Hook在触发事件里完成

完整实现代码(dll):

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "pch.h"

//全局变量

//目标函数地址(被Hook的函数)
PROC m_FuncAddr = NULL;
//保存被我们修改的5个字节数据
BYTE m_OldByte[5] = { 0 };
//我们修改的5个字节数据
BYTE m_NewByte[5] = { 0 };

//Hook(这里我们需要:目标函数所在模块名称,目标函数名,劫持流程的函数地址)
BOOL InlineHook(const WCHAR* pszModuleName, const char* pszFuncName, PROC pfnHookFunc) {
	//获取模块句柄
	HMODULE hModule = GetModuleHandleW(pszModuleName);
	//从指定的动态链接库 (DLL) 检索导出函数 (也称为过程) 或变量,返回地址
	m_FuncAddr = GetProcAddress(hModule, pszFuncName);
	if (m_FuncAddr == NULL) {
		return FALSE;
	}

	//读取指定进程内存
	SIZE_T dwReadSize = 0;
	BOOL bRet = ReadProcessMemory(
		GetCurrentProcess(),     //由于我们注入到了目标进程,这里直接获取自身的进程句柄就可以了
		m_FuncAddr,              //要读取内存的起始地址
		m_OldByte,                 //接收读取的内容指针
		5,                       //指定读取大小
		&dwReadSize              //接收实际读取大小指针
	);
	//判断是否读取成功
	if (!bRet||dwReadSize != 5) {
		return FALSE;
	}

	//构造新的5字节
	m_NewByte[0] = '\xE9';
	//这里是要跳转的地址,注意计算公式:要跳转的地址 = 要执行的地址 - 原来执行地址 -指令长度
	*(DWORD*)(m_NewByte + 1) = (DWORD)pfnHookFunc - (DWORD)m_FuncAddr - 5;
	SIZE_T dwWrittenByte = 0;
	bRet = WriteProcessMemory(
		GetCurrentProcess(),
		m_FuncAddr,
		m_NewByte,
		5,
		&dwWrittenByte
	);
	if (!bRet || dwWrittenByte != 5) {
		return FALSE;
	}
	else {
		return TRUE;
	}
}

//修改完执行后,我们还需要改回去,不然就进入了死循环
VOID UnHook() {
	if (m_FuncAddr != NULL) {
		DWORD dwWrittenByte = 0;
		WriteProcessMemory(GetCurrentProcess(), m_FuncAddr, m_OldByte, 5, &dwWrittenByte);
	}
}

VOID ReHook() {
	if (m_FuncAddr != NULL) {
		DWORD dwWrittenByte = 0;
		WriteProcessMemory(GetCurrentProcess(), m_FuncAddr, m_NewByte, 5, &dwWrittenByte);
	}
}

//这里构造我们自己的MessageBox(做函数转发)
int
WINAPI
MyMessageBox(
	_In_opt_ HWND hWnd,
	_In_opt_ LPCWSTR lpText,
	_In_opt_ LPCWSTR lpCaption,
	_In_ UINT uType) {
	//恢复
	UnHook();
	int bRet = MessageBox(hWnd, L"Hook", L"InlineHook", uType);
	//重新挂钩
	ReHook();
	return bRet;
}

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
		InlineHook(L"User32.dll", "MessageBoxW", (PROC)MyMessageBox);
		break;
    case DLL_THREAD_ATTACH:
		break;
    case DLL_THREAD_DETACH:
		break;
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

InlineHook测试:

    1. 执行目标程序:
      执行目标程序
    1. 注入dll:
      注入dll
  • Hook成功:
    Hook成功
二、API Hook的原理 这里的API既包括传统的Win32 APIs,也包括任何Module输出的函数调用。熟悉PE文件格 式的朋友都知道,PE文件将对外部Module输出函数的调用信息保存在输入表中,即.idata段。 下面首先介绍本段的结构。 输入表首先以一个IMAGE_IMPORT_DESCRIPTOR(简称IID)数组开始。每个被PE文件隐式链接 进来的DLL都有一个IID.在这个数组中的最后一个单元是NULL,可以由此计算出该数组的项数。 例如,某个PE文件从两个DLL中引入函数,就存在两个IID结构来描述这些DLL文件,并在两个 IID结构的最后由一个内容全为0的IID结构作为结束。几个结构定义如下: IMAGE_IMPORT_DESCRIPTOR struct union{ DWORD Characteristics; ;00h DWORD OriginalFirstThunk; }; TimeDateStamp DWORD ;04h ForwarderChain DWORD ;08h Name DWORD ;0Ch FirstThunk DWORD ;10h IMAGE_IMPROT_DESCRIPTOR ends typedef struct _IMAGE_THUNK_DATA{ union{ PBYTE ForwarderString; PDWORD Functions; DWORD Ordinal; PIMAGE_IMPORT_BY_NAME AddressOfData; }u1; } IMAGE_IMPORT_BY_NAME结构保存一个输入函数的相关信息: IMAGE_IMPORT_BY_NAME struct Hint WORD ? ;本函数在其所驻留DLL的输出表中的序号 Name BYTE ? ;输入函数的函数名,以NULL结尾的ASCII字符串 IMAGE_IMPORT_BY_NAME ends OriginalFirstThunk(Characteristics):这是一个IMAGE_THUNK_DATA数组的RVA(相对于PE文件 起始处)。其中每个指针都指向IMAGE_IMPORT_BY_NAME结构。 TimeDateStamp:一个32位的时间标志,可以忽略。 ForwarderChain:正向链接索引,一般为0。当程序引用一个DLL中的API,而这个API又引用别的 DLL的API时使用。 NameLL名字的指针。是个以00结尾的ASCII字符的RVA地址,如"KERNEL32.DLL"。 FirstThunk:通常也是一个IMAGE_THUNK_DATA数组的RVA。如果不是一个指针,它就是该功能在 DLL中的序号。 OriginalFirstThunk与FirstThunk指向两个本质相同的数组IMAGE_THUNK_DATA,但名称不同, 分别是输入名称表(Import Name Table,INT)和输入地址表(Import Address Table,IAT)。 IMAGE_THUNK_DATA结构是个双字,在不同时刻有不同的含义,当双字最高位为1时,表示函数以 序号输入,低位就是函数序号。当双字最高位为0时,表示函数以字符串类型的函数名 方式输入,这时它是指向IMAGE_IMPORT_BY_NAME结构的RVA。 三个结构关系如下图: IMAGE_IMPORT_DESCRIPTOR INT IMAGE_IMPORT_BY_NAME IAT -------------------- /-->---------------- ---------- ---------------- |01| 函数1 ||02| 函数2 || n| ... |"USER32.dll" | |--------------------| | | FirstThunk |---------------------------------------------------------------/ -------------------- 在PE文件中对DLL输出函数的调用,主要以这种形式出现: call dword ptr[xxxxxxxx] 或 jmp [xxxxxxxx] 其中地址xxxxxxxx就是IAT中一个IMAGE_THUNK_DATA结构的地址,[xxxxxxxx]取值为IMAGE_THUNK_DATA 的值,即IMAGE_IMPORT_BY_NAME的地址。在操作系统加载PE文件的过程中,通过IID中的Name加载相应 的DLL,然后根据INT或IAT所指向的IMAGE_IMPORT_BY_NAME中的输入函数信息,在DLL中确定函数地址, 然后将函数地址写到IAT中,此时IAT将不再指向IMAGE_IMPORT_BY_NAME数组。这样[xxxxxxxx]取到的 就是真正的API地址。 从以上分析可以看出,要拦截API的调用,可以通过改写IAT来实现,将自己函数的地址写到IAT中, 达到拦截目的。 另外一种方法的原理更简单,也更直接。我们不是要拦截吗,先在内存中定位要拦截的API的地址, 然后改写代码的前几个字节为 jmp xxxxxxxx,其中xxxxxxxx为我们的API的地址。这样对欲拦截API的 调用实际上就跳转到了咱们的API调用去了,完成了拦截。不拦截时,再改写回来就是了。 这都是自己从网上辛辛苦苦找来的,真的很好啊
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Shad0w-2023

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值