windows 下实现函数打桩:拦截API方式

windows 下实现函数打桩:拦截API方式

最近因为工作需要,开始研究函数打桩的方法。由于不想对工程做过多的修改,于是放弃了使用Google gmock的想法。但是也足足困扰另外我一天一宿。经过奋战,终于有所收获。闲话少说,开始看看有什么方法。

一、基础准备

1. 函数调用的原理:通过函数名(函数的入口地址)对函数进行访问,假设我们能够改变函数首地址指向的内存的话,使其跳转到另一个函数去执行的话,那么就可以实现函数打桩了。

2. 方法:对函数首地址出写入一条汇编语言 jmp xxx (其中xxx是要跳转的相对地址)。

3. 令原函数为oldFun,新函数为newFun,那么打桩时函数跳转的相对地址 offset = newFun - oldFun - (我们制定的这条指令的大小),此处为绝对跳转指令的长度=5。  jmp xxx一共6字节。

函数:

1. VirtualQuery

WINBASEAPI
SIZE_T
WINAPI
VirtualQuery(
    __in_opt LPCVOID lpAddress,   //所查内存地址
    __out_bcount_part(dwLength, return) PMEMORY_BASIC_INFORMATION lpBuffer,   //保存内存区域的buffer
    __in     SIZE_T dwLength                                                  //信息长度
    );

该函数用于查询某一段内存区域的内存信息,事实VirtualQueryEx也可以使用。

2. VirtualProtect

WINBASEAPI
BOOL
WINAPI
VirtualProtect(
    __in  LPVOID lpAddress,
    __in  SIZE_T dwSize,
    __in  DWORD flNewProtect,
    __out PDWORD lpflOldProtect
    );

该函数用于修改指定内存区dwSize个字节的保护模式。

3. VirtualProtectEx

WINBASEAPI
BOOL
WINAPI
VirtualProtectEx(
    __in  HANDLE hProcess,   //进程句柄
    __in  LPVOID lpAddress,  //需要修改的内存首地址
    __in  SIZE_T dwSize,     //修改的字节数
    __in  DWORD flNewProtect,  //新的保护属性
    __out PDWORD lpflOldProtect  //旧的保护属性
    );

VirtualProtectEx 用于改变指定进程内存段的保护模式,默认情况下函数的内存空间不可写,这就是为什么要用改变保护属性的函数。

4. ReadProcessMemory

WINBASEAPI
BOOL
WINAPI
ReadProcessMemory(
    __in      HANDLE hProcess,
    __in      LPCVOID lpBaseAddress,
    __out_bcount_part(nSize, *lpNumberOfBytesRead) LPVOID lpBuffer,
    __in      SIZE_T nSize,
    __out_opt SIZE_T * lpNumberOfBytesRead
    );

读取进程内存,lpProcess是首地址,而lpBuffer用于保存读出的数据,nSize是需要读出的字节数。

5. WriteProcessMemory

WINBASEAPI
BOOL
WINAPI
WriteProcessMemory(
    __in      HANDLE hProcess,
    __in      LPVOID lpBaseAddress,
    __in_bcount(nSize) LPCVOID lpBuffer,
    __in      SIZE_T nSize,
    __out_opt SIZE_T * lpNumberOfBytesWritten
    );

该函数用于写进程的内存空间,可以向进程内存注入想要注入的数据,例如函数等。

6. GetCurrentProcess

WINBASEAPI
__out
HANDLE
WINAPI
GetCurrentProcess(
    VOID
    );

该函数返回一个伪进程句柄0xffffffff,任何需要进程句柄的内存都可以使用它。

二、对库中API打桩

方案一:

打桩:

#define FLATJMPCODE_LENGTH 5            //x86 平坦内存模式下,绝对跳转指令长度
#define FLATJMPCMD_LENGTH  1            //机械码0xe9长度
#define FLATJMPCMD         0xe9         //对应汇编的jmp指令

// 记录被打桩函数的内容,以便恢复
BYTE g_apiBackup[FLATJMPCODE_LENGTH+FLATJMPCMD_LENGTH];

BOOL setStub(LPVOID ApiFun,LPVOID HookFun)
{
    BOOL    IsSuccess = FALSE;
    DWORD   TempProtectVar;              //临时保护属性变量
    MEMORY_BASIC_INFORMATION MemInfo;    //内存分页属性信息

    VirtualQuery(ApiFun,&MemInfo,sizeof(MEMORY_BASIC_INFORMATION));

    if(VirtualProtect(MemInfo.BaseAddress,MemInfo.RegionSize,
        PAGE_READWRITE,&MemInfo.Protect))                            //修改页面为可写
    {
        memcpy((void*)g_apiBackup,(const void*)ApiFun, sizeof(g_apiBackup));

        *(BYTE*)ApiFun = FLATJMPCMD;                                 //拦截API,在函数代码段前面注入jmp xxx
        *(DWORD*)((BYTE*)ApiFun + FLATJMPCMD_LENGTH) = (DWORD)HookFun -
            (DWORD)ApiFun - FLATJMPCODE_LENGTH;

        VirtualProtect(MemInfo.BaseAddress,MemInfo.RegionSize,
            MemInfo.Protect,&TempProtectVar);                        //改回原属性

        IsSuccess = TRUE;
    }

    return IsSuccess;
}

清桩:

BOOL clearStub(LPVOID ApiFun)
{
    BOOL    IsSuccess = FALSE;
    DWORD   TempProtectVar;              //临时保护属性变量
    MEMORY_BASIC_INFORMATION MemInfo;    //内存分页属性信息

    VirtualQuery(ApiFun,&MemInfo,sizeof(MEMORY_BASIC_INFORMATION));

    if(VirtualProtect(MemInfo.BaseAddress,MemInfo.RegionSize,
        PAGE_READWRITE,&MemInfo.Protect))                            //修改页面为可写
    {
        memcpy((void*)ApiFun, (const void*)g_apiBackup, sizeof(g_apiBackup));  //恢复代码段

        VirtualProtect(MemInfo.BaseAddress,MemInfo.RegionSize,
            MemInfo.Protect,&TempProtectVar);                        //改回原属性

        IsSuccess = TRUE;
    }

    return IsSuccess;
}

方案二:

打桩:

bool setStub(LPVOID ApiFun,LPVOID HookFun)
{
	HANDLE file_handler = GetCurrentProcess();           //获取进程伪句柄
	DWORD oldProtect,TempProtectVar;
	char newCode[6];                                     //用于读取函数原有内存信息
	int SIZE = FLATJMPCODE_LENGTH+FLATJMPCMD_LENGTH;     //需要修改的内存大小
	if(!VirtualProtectEx(file_handler,ApiFun,SIZE,PAGE_READWRITE,&oldProtect))  //修改内存为可读写
	{
		return false;
	}
	if(!ReadProcessMemory(file_handler,ApiFun,newCode,SIZE,NULL))              //读取内存
	{
		return false;
	}
	memcpy((void*)g_apiBackup,(const void*)newCode, sizeof(g_apiBackup));      //保存被打桩函数信息
	*(BYTE*)ApiFun = FLATJMPCMD;
        *(DWORD*)((BYTE*)ApiFun + FLATJMPCMD_LENGTH) = (DWORD)HookFun - (DWORD)ApiFun - FLATJMPCODE_LENGTH;   //桩函数注入
    VirtualProtectEx(file_handler,ApiFun,SIZE,oldProtect,&TempProtectVar);  //恢复保护属性
}

清桩:

bool clearStub(LPVOID ApiFun)
{
    BOOL    IsSuccess = FALSE;
    HANDLE file_handler = GetCurrentProcess();
    DWORD oldProtect,TempProtectVar;
    int SIZE = FLATJMPCODE_LENGTH+FLATJMPCMD_LENGTH;
	if(VirtualProtectEx(file_handler,ApiFun,SIZE,PAGE_READWRITE,&oldProtect))
    {
        memcpy((void*)ApiFun, (const void*)g_apiBackup, sizeof(g_apiBackup));             //恢复被打桩函数内存
        VirtualProtectEx(file_handler,ApiFun,SIZE,oldProtect,&TempProtectVar);
        IsSuccess = TRUE;
    }

    return IsSuccess;
}

方案三:

打桩:

bool setStub(LPVOID ApiFun,LPVOID HookFun)
{
	HANDLE file_handler = GetCurrentProcess();
	DWORD oldProtect,TempProtectVar;
	char newCode[6];
	int SIZE = FLATJMPCODE_LENGTH+FLATJMPCMD_LENGTH;
	if(!ReadProcessMemory(file_handler,ApiFun,newCode,SIZE,NULL))
	{
		return false;
	}
	memcpy((void*)g_apiBackup,(const void*)newCode, sizeof(g_apiBackup));
	*(BYTE*)newCode = FLATJMPCMD;
    *(DWORD*)((BYTE*)newCode + FLATJMPCMD_LENGTH) = (DWORD)HookFun - (DWORD)ApiFun - FLATJMPCODE_LENGTH;
	if(!WriteProcessMemory(file_handler,ApiFun,newCode,FLATJMPCODE_LENGTH,NULL))
	{
		return false;
	}
}

说来也怪,这个方案没有改变读取权限,居然也可以,这里写入的方式是用WriteProcessMemory来实现,与直接用指针同理。清桩同上。但是如果直接用指针来写就会出错,暂时不知道原因。

至此我们实现了函数的打桩,但是有个小小的问题,若仅仅是如此,对类函数中成员函数打桩有点小问题,指针无法转换,这是因为类成员函数的指针不仅仅是一个普通的指针,他还包括其他信息。所有这里需要解决这个问题,网上找到了两个方法:

1. 类的普通函数成员地址转换

LPVOID GetClassFnAddress(...)
{
    LPVOID FnAddress;
    __asm
    {
        lea eax,FnAddress
        mov edx,[ebp+8]    // ebp+8 为第一个形参的地址,ebp+C 为第二个形参的地址,以此类推
        mov [eax],edx
    }
    return FnAddress;
}

2. 类的虚成员函数地址转换

LPVOID GetClassVirtualFnAddress(LPVOID pthis,int Index) //Add 2010.8.6
{
    LPVOID FnAddress;
    *(int*)&FnAddress = *(int*)pthis;                       //lpvtable
    *(int*)&FnAddress = *(int*)((int*)FnAddress + Index);
    return FnAddress;
}

至此函数打桩的介绍告一段落。

时间: 2024-10-10 21:22:39

windows 下实现函数打桩:拦截API方式的相关文章

Windows下通过写注册表的方式实现程序开机自启动

程序可以读取同目录下的config.ini文件中的配置来实现开机自启动. config.ini文件地格式实例如下: [Main] KeyName = test ProcessPath = D:\bin\test.exe 其中KeyName字段为写入注册表的表项名称(可以设置为程序名) ProcessPath为程序的完整路径 程序的完整源代码如下. regedit函数实现注册表写入,autopen实现开机自启动. 编译后的程序在此下载:程序下载 #include <stdio.h> #inclu

windows下system函数的使用

system函数 是可以调用一些DOS命令,比如system("cls");//清屏,等于在DOS上使用cls命令写可执行文件路径,可以运行它···· 下面列出常用的DOS命令,都可以用system函数调用: ASSOC 显示或修改文件扩展名关联.AT 计划在计算机上运行的命令和程序.ATTRIB 显示或更改文件属性.BREAK 设置或清除扩展式 CTRL+C 检查.CACLS 显示或修改文件的访问控制列表(ACLs).CALL 从另一个批处理程序调用这一个.CD 显示当前目录的名称或

windows下运用git bash 通过ssh方式连接到git server

在windows下进行项目开发,使用git,通过ssh方式与git库连接,而ssh方式用public key实现连接. 首先需要下载mygit,安装后使用git bash.git bash(有GUI界面,如安装TortoiseGit后就可以使用).我主要使用命令行,其命令行系统类似linux环境的基本操作命令,可以直接看到c:,如cd /d/mygitwork,进入我的D盘下的工程目录,放置开发的工程. 我的操作:在windows的git bash中用ssh -keygen ssh -keyge

Windows下让nginx以服务的方式启动运行

在windows下安装了nginx, 郁闷是发现它没有以服务方式运行, 也就是说当用户注销后,程序会终止.因此需要将nginx作为服务运行. 方法一: 假设nginx安装在c:\nginx\下: 1.下载微软服务注册工具srvany.exe, instsrv.exe, 存放到c:\nginx\目录下 http://eastedu.bl-changjiang.com/UploadFiles/2006-5/511179043.rar 2.安装NGINX服务, 将命令行切换到c:\nginx\,执行下

MongoDb在windows下的安装与以auth方式启用服务

一.下载安装 1.去官网上下载适合自己电脑的MongoDB版本  下载MongoDB 2.安装MongoDB 安装还是比较简单,按照步骤一步一步往下走就可以了. 3.启动MongodDB 安装完成之后找到自己的安装目录并打开. 在该目录下新建一个data文件夹,用于存放数据库.新建一个log目录用于存放log,并在log目录下添加log.log文件,用于存放log. 接下来打开cmd命令,注意要用管理员权限打开. 将目录定位到mongodb下的bin文件夹.输入mongod --dbpath G

Windows下createfile函数用GENERIC_READ访问模式打不开磁盘

这两天做毕设,快气死了!想读写磁盘扇区,我就百度了,都是这样写的: HANDLE hDevice = CreateFile(TEXT("\\\\.\\PhysicalDrive1", // drive,一般physicaldrive0是主磁盘,我不干,整死了咋整,虚拟机又卡 GENERIC_REAF, // no access to the drive FILE_SHARE_READ ,// share mode, NULL, // default security attribute

【CUDA】Windows 下常用函数头文件

CUDA 函数 头文件 __global__ __device__ #include <cuda_runtime.h> threadIdx #include <device_launch_parameters.h>   #include <sm_20_intrinsics.h> __shfl() #include <sm_30_intrinsics.h> tex1Dfetch() #include <texture_fetch_functions.h&

vim、gvim在windows下中文乱码的终极解决方式

測试成功,完美解决. 仅仅需改动VIM文件夹以下的这个文件_vimrc. 加油吧,骚年.非常强大的! set encoding=utf-8 set fileencodings=utf-8,chinese,latin-1 if has("win32") set fileencoding=chinese else set fileencoding=utf-8 endif "解决菜单乱码 source $VIMRUNTIME/delmenu.vim source $VIMRUNTI

QT 用Windows的API函数,调用打开方式对话框

下面的代码实现的功能是,先用Windows的ShellExecuteW函数以默认的方式打开文本文件,如果文件打开失败,则用WinExec函数调用打开方式对话框. 注意要引入头文件#include <windows.h>  和 #include <shellapi.h> QString file = "C:/新建/中文.txt"; int nRes = 0;  //以默认方式打开文件 nRes = (int)ShellExecuteW(NULL , QString