【windows核心编程】一个API拦截的例子

API拦截

修改PE文件导入段中的导入函数地址 为 新的函数地址

这涉及PE文件格式中的导入表和IAT,PE文件中每个隐式链接的DLL对应一个IMAGE_IMPORT_DESCRIPTOR描述符结构,而每个IMAGE_IMPORT_DESCRIPTOR结构中的FirstThunk指向一个IMAGE_THUNK_DATA结构数组的首地址。

在这个IAMGE_THUNK_DATA数组中,每一项对应一个该DLL模块的导入函数(对使用该DLL模块的PE文件来说是 导入)。

 结构大致如下

拦截某DLL模块中的API时,先在PE文件的导入段中查找该DLL,然后再查找该API的地址,将其地址替换为新的地址。

即先遍历IMAGE_IMPORT_DESCRIPTOR数组,找到其Name和给定DLL名字相同的模块,然后再遍历其FirstThunk指向的IAMGE_THUNK_DATA数组,找到待拦截的API,然后将新地址替换其地址。

本例中,LanjieAPI.exe中使用了DllForLanjie.dll模块,该模块导出了一个计算两个数的和的函数Add(int a, int b),本文实例将该Add函数拦截,替换为show函数来显示信息。

上代码

/************************************************************************/
/* PE文件中,每个隐式链接的DLL都对应一个IMAGE_IMPORT_DESCRIPTOR结构(winnt.h)
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD   Characteristics;            // 0 for terminating null import descriptor
DWORD   OriginalFirstThunk;         //指向IMAGE_THUNK_DATA结构数组的指针,RVA to original unbound IAT (PIMAGE_THUNK_DATA)
};
DWORD   TimeDateStamp;                  // 0 if not bound,
// -1 if bound, and real date\time stamp
//     in IMAGE_DIRECTORY_ENTRY_BOUND_IMPORT (new BIND)
// O.W. date/time stamp of DLL bound to (Old BIND)

DWORD   ForwarderChain;                 // -1 if no forwarders
DWORD   Name;                           //该模块的名字
DWORD   FirstThunk;                     //指向IMAGE_THUNK_DATA结构数组的指针,RVA to IAT (if bound this IAT has actual addresses)
} IMAGE_IMPORT_DESCRIPTOR;

PE文件的某个导入模块中的 每个函数 对应一个IMAGE_THUNK_DATA结构
typedef struct _IMAGE_THUNK_DATA32 {
union {
DWORD ForwarderString;      // PBYTE
DWORD Function;             // PDWORD
DWORD Ordinal;
DWORD AddressOfData;        //指向IMAGE_IMPORT_BY_NAME结构的指针, PIMAGE_IMPORT_BY_NAME
} u1;
} IMAGE_THUNK_DATA32;

typedef struct _IMAGE_IMPORT_BY_NAME {
WORD    Hint;
BYTE    Name[1];
} IMAGE_IMPORT_BY_NAME, *PIMAGE_IMPORT_BY_NAME;

其中OriginalFirstThunk和FirstThunk均各自一个IMAGE_THUNK_DATA结构类型数组
所不同的是:OriginalFirstThunk指向的IAMGE_THUNK_DATA结构数组是单独的一项,
并且不可更改,成为INT,有时也成为提示名表。
FirstThunk所指向的是IMAGE_THUNK_DATA结构类型数组是又PE装载器重写的,PE装载器
首先搜索OriginalFirstThunk,如果找到则加载程序迭代搜索数组中的每个指针,找到
每个IMAGE_IMPORT_BY_NAME结构所指向的输入函数的地址,然后加载器用函数真正入口
地址来替代由FirstThunk数组中的每个函数入口,因此他成为【输入地址表IAT】。
/************************************************************************/

DLL导出函数

//DllForLanjie.dll导出函数
extern "C" __declspec(dllexport) int __stdcall Add(int a, int b)
{
    return a + b;
}

拦截代码

BOOL ReplaceIATEntryInOneMod(
         PCSTR pszCalleeModName,  //被调模块,要拦截的API所在的模块
         PROC pfnCurrent,          //被拦截的API,其所在模块中的地址
         PROC pfnNew,              //用来替换的新函数的地址
         HMODULE hmodCaller        //调用新函数的模块
         )
{
    ULONG ulSize = 0UL;
    PIMAGE_IMPORT_DESCRIPTOR pImportDesc = NULL;  //导入描述符
    __try
    {
        pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)(ImageDirectoryEntryToData(hmodCaller, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT, &ulSize));
    }
    //__except(InvalidReadExceptionFilter(GetExceptionInformation))
    __except(1)
    {
        cerr<<"Exception !"<<endl;
    }

    if(NULL == pImportDesc) return FALSE;

    //结束条件为 当前该IMAGE_IMPORT_DESCRIPTOR结构(Name字段)指针为NULL
    for (; pImportDesc->Name; pImportDesc++)
    {
        //查找导入段中每个模块的地址,转为PBYTE计算是为了指针+1时是加了一个字节
        PSTR pszModName = (PSTR)((PBYTE)hmodCaller + pImportDesc->Name);

        //如果找到同名模块
        if(lstrcmpiA(pszModName, pszCalleeModName) == 0)
        {
            //获取调用程序中的该pImageDesc模块的IMAGE_THUNK_DATA数组的首地址(在PE文件中)
            PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)((PBYTE)hmodCaller + pImportDesc->FirstThunk);

            //在模块中查找要拦截的【函数】
            for (; pThunk->u1.Function; pThunk++)
            {
                PROC* ppfn = (PROC*)(&pThunk->u1.Function);

                //如果找到了需要的【函数】
                BOOL bFound = (*ppfn == pfnCurrent);
                if (bFound)
                {
                    //如果往被拦截函数的地址写新函数的地址 【失败】
                    if (! WriteProcessMemory(GetCurrentProcess(), ppfn, &pfnNew, sizeof(pfnNew), NULL)
                        && (ERROR_NOACCESS == GetLastError())   )
                    {
                        DWORD dwOldProtect = 0;

                        //则改变页面的保护属性为 【写时复制】
                        if(VirtualProtect(ppfn, sizeof(pfnNew), PAGE_WRITECOPY, &dwOldProtect))
                        {
                            BOOL b1 = WriteProcessMemory(GetCurrentProcess(), ppfn, &pfnNew, sizeof(pfnNew), NULL);
                            BOOL b2 = VirtualProtect(ppfn, sizeof(pfnNew), dwOldProtect, &dwOldProtect);

                            return b1 && b2;
                        } //if
                        else
                        {
                            return FALSE;
                        }

                    } //if

                    //如果执行到这里了,则肯定是成功的
                    return TRUE;

                } //if

            } //for

        } //if
    } //for

    return FALSE;
}

用来替换的新函数, 在当前exe中定义

int __stdcall show()
{
    cout<<endl<<"如果看到此行信息 说明 你的函数 被 当前函数 拦截了  呵呵"<<endl;
    return 0;
}

示意怎么去拦截

头文件 和 库

#include "stdafx.h"
#include <iostream>
#include <Windows.h>
#include <Dbghelp.h>

#include <Psapi.h>
using namespace std;

#pragma comment(lib, "psapi.lib")
#pragma comment(lib, "Dbghelp.lib")
#pragma comment(lib, "DllForLanjie.lib")
extern "C" __declspec(dllimport) int __stdcall Add(int a, int b);
int _tmain(int argc, _TCHAR* argv[])
{   //本例中获取ExitProess地址为NULL,用depends查看该exe的使用kernel32.dll的导入函数中没有ExitProcess,    //可能是因为这个原因所以地址为NULL
    //PROC pfnCurrent = (PROC)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "ExitProcess");
    //PROC pfnCurrent = (PROC)GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "Sleep");

    cout<<"拦截之前 调用Add函数 计算Add(10, 20)"<<endl;
    cout<<"Add(10, 20) = "<<Add(10, 20)<<endl<<endl;

    PROC pfnCurrent = (PROC)GetProcAddress(GetModuleHandle(_T("DllForLanjie.dll")), "[email protected]");

    if(NULL == pfnCurrent) return -1;

    TCHAR chImageName[MAX_PATH + 1] = {0};
    ZeroMemory(chImageName, sizeof(chImageName));
    DWORD dwRet = GetProcessImageFileName(GetCurrentProcess(), chImageName, _countof(chImageName));
    if(0 == dwRet)
    {
        cerr<<"some error"<<endl;
        return -2;
    }

    cout<<endl<<"开始 将当前exe的导入段中的模块DllForLanjie.dll中的函数Add的地址替换为show函数"<<endl;
    BOOL bRet = ReplaceIATEntryInOneMod("DllForLanjie.dll", pfnCurrent, show, GetModuleHandle(NULL));
    if(FALSE == bRet)
    {
        cout<<"替换失败 退出!"<<endl;
        return -3;
    }
    cout<<"替换成功 继续!"<<endl;

    cout<<endl<<endl<<"下面调用三次Add函数 计算100 和 200 的和"<<endl;
    for (int i = 0; i < 3; ++ i)
    {
        Add(100, 200);
    }

    cout<<endl<<endl;
    return 0;
}

执行结果

执行过程中,VS2005一直报运行时异常

每次点击【忽略】才能继续,为何?

该实例已经成功的拦截了DllForLanjie.dll中的API Add函数,替换为了exe中的show函数。

【windows核心编程】一个API拦截的例子

时间: 2024-11-05 19:30:24

【windows核心编程】一个API拦截的例子的相关文章

【转】《windows核心编程》读书笔记

这篇笔记是我在读<Windows核心编程>第5版时做的记录和总结(部分章节是第4版的书),没有摘抄原句,包含了很多我个人的思考和对实现的推断,因此不少条款和Windows实际机制可能有出入,但应该是合理的.开头几章由于我追求简洁,往往是很多单独的字句,后面的内容更为连贯. 海量细节. 第1章    错误处理 1.         GetLastError返回的是最后的错误码,即更早的错误码可能被覆盖. 2.         GetLastError可能用于描述成功的原因(CreatEvent)

windows核心编程 DLL技术 【转】

注:本文章转载于网络,源地址为:http://blog.csdn.net/ithzhang/article/details/7051558 本篇文章将介绍DLL显式链接的过程和模块基地址重定位及模块绑定的技术. 第一种将DLL映射到进程地址空间的方式是直接在源代码中引用DLL中所包含的函数或是变量,DLL在程序运行后由加载程序隐式的载入,此种方式被称为隐式链接. 第二种方式是在程序运行时,通过调用API显式的载入所需要的DLL,并显式的链接所想要链接的符号.换句话说,程序在运行时,其中的一个线程

C++Windows核心编程读书笔记

转自:http://www.makaidong.com/%E5%8D%9A%E5%AE%A2%E5%9B%AD%E6%96%87/71405.shtml "C++Windows核心编程读书笔记": 关键词:c++windows 核心 编程 读书笔记 这篇笔记是我在读<windows核心编程>第5版时做的记录和总结(部分章节是第4版的书),没有摘抄原句,包含了很多我个人的思考和对实现的推断,因此不少条款和windows实际机制可能有出入,但应该是合理的.开头几章由于我追求简洁

【windows核心编程】IO完成端口(IOCP)复制文件小例前简单说明

1.关于IOCP IOCP即IO完成端口,是一种高伸缩高效率的异步IO方式,一个设备或文件与一个IO完成端口相关联,当文件或设备的异步IO操作完成的时候,去IO完成端口的[完成队列]取一项,根据完成键(Complete Key)来判断是哪个设备或文件的操作完成,然后再根据实际情况进行处理. 2.相关API 和 数据结构   将一个已完成的IO通知追加到IOCP的[完成队列]中 BOOL   PostQueuedCompletionStatus( HANDLE    hCompletionPort

Windows核心编程第二章,字符串的表示以及宽窄字符的转换

目录 Windows核心编程,字符串的表示以及宽窄字符的转换 1.字符集 1.1.双字节字符集DBCS 1.2 Unicode字符集 1.3 UTF-8编码 1.4 UTF - 32编码. 1.5 Unicode标准包含的对照表. 2.Ansi字符与Unicode字符的字符串数据类型 2.1.Ansi 与 Unicode数据类型 2.2添加的新的数据类型 2.3 TEXT()宏的使用 3.Windows中的Unicode与Ansi函数 3.1 对于Com接口的移植 4.C 运行库中的Unicod

《windows核心编程系列》十八谈谈windows钩子

windows应用程序是基于消息驱动的.各种应用程序对各种消息作出响应从而实现各种功能. windows钩子是windows消息处理机制的一个监视点,通过安装钩子能够达到监视指定窗体某种类型的消息的功能.所谓的指定窗体并不局限于当前进程的窗体,也能够是其它进程的窗体.当监视的某一消息到达指定的窗体时,在指定的窗体处理消息之前,钩子函数将截获此消息,钩子函数既能够加工处理该消息,也能够不作不论什么处理继续传递该消息.使用钩子是实现dll注入的方法之中的一个.其它经常使用的方法有:注冊表注入,远程线

【windows核心编程】使用远程线程注入DLL

前言 该技术是指通过在[目标进程]中创建一个[远程线程]来达到注入的目的. 创建的[远程线程]函数为LoadLibrary, 线程函数的参数为DLL名字, 想要做的工作在DLL中编写.  示意图如下:  相关API 1.创建远程线程 //该函数除了第一个参数为目标进程句柄外 //其他参数均和CreateThread一样 HANDLE hThread = CreateRemoteThread( __in HANDLE hProcess, //目标进程句柄 __in_opt LPSECURITY_A

【windows核心编程】DLL相关(3)

DLL重定向 因为DLL的搜索路径有先后次序,假设有这样的场景:App1.exe使用MyDll1.0.dll, App2.exe使用MyDll2.0.dll, MyDll1.0 和 MyDll2.0是同一个DLL的两个版本,1.0为旧版本,2.0为新版本. 而如果MyDll2.0.dll的存放路径的优先次序比较靠前时,那么App1.exe就会去加载MyDll2.0.dll,这就可能引发 DLL地狱问题,因此DLL重定向可解决这个问题. 加载程序总是先检查应用程序目录,我们所要做的就是如下: ①在

《Windows核心编程》读书笔记 上

[C++]<Windows核心编程>读书笔记 这篇笔记是我在读<Windows核心编程>第5版时做的记录和总结(部分章节是第4版的书),没有摘抄原句,包含了很多我个人的思考和对实现的推断,因此不少条款和Windows实际机制可能有出入,但应该是合理的.开头几章由于我追求简洁,往往是很多单独的字句,后面的内容更为连贯. 海量细节. 第1章    错误处理 1.         GetLastError返回的是最后的错误码,即更早的错误码可能被覆盖. 2.         GetLas