函数的打桩和hook

转载 2012年03月27日 23:59:12

 

刚参加工作,总是听到PM分配任务的时候提到打桩的概念。查看项目代码的时候又碰到打桩函数中的HOOK技术。对于这两种技术我以前都没有接触过,在网上查了下资料,备份如下,希望对自己和他人都能有所帮助。

 

在软件测试的时候经常需要动态的替换被测函数调用的其它函数,而这个替换就叫做打桩。比如现在要测试函数A,但是函数A调用了还没有实现的函数B,那么我们给函数B创造打桩函数STUB_B,STUB_B可以直接返回一个值,由此便可以测试函数A的功能。这便是打桩。

以下为函数HOOK技术的两篇文章:

多任务下的数据结构与算法:函数的HOOK

HOOK功能的应用简介

    HOOK功能是一项非常重要的功能,是软件工程师必须掌握的技术。HOOK功能在实际中应用非常广泛,如词典软件、安全软件及各种系统软件中均会用到;还有一些C语言语法不能实现的功能也可以用HOOK来实现,如C++标准模板库中就用到了内存配置器的概念,即用户可以自己指定内存分配方法,可以使用自己的内存分配函数来进行内存管理。在C语言中,这个功能从语法上要求用函数指针作为回调函数等来实现,但是有很多已经写好的模块是不能随便修改接口的,如果要让这些模块在不修改的基础上使用自定义内存管理算法,就必须使用 HOOK功能。可以对malloc(),free()等内存管理函数设置 HOOK,使它们跳转到自定义的内存管理函数即可。

    HOOK功能还有一项非常重要的用途就是在白盒测试方面,像单元测试需要打桩,传统的打桩方法是将源文件中需要打桩的函数用桩函数替代,这个工作虽然可以通过工具来实现,但还是比较麻烦;如果使用HOOK来打桩则可以避免修改源文件,只要将桩函数设置成钩子函数,比传统的方法简洁多了。

    单个函数的HOOK实现

    很多读者可能用过金山词霸之类的软件,鼠标只要往单词上一指,就可以弹出对应单词的解释,这种屏幕取词便是使用了HOOK功能拦截Windows API的调用来实现的。本节要用数组来实现对函数HOOK的管理功能,首先讨论如何实现对单个函数的HOOK功能。



    对某个函数的HOOK就是通过某种方法使得调用这个函数时调用到指定的钩子函数。实现HOOK的方法很多,最简单的一种方法便是将调用函数的开头位置的代码改成一条跳转到钩子函数的指令即可。

    譬如我们自己写了一个MyMalloc()函数,以实现所有对malloc()函数的调用都调用MyMalloc()函数,假设 malloc函数的起始地址是0x00421540,那么只要将0x00421540开始的几个字节改成jmp DWORD PTR MyMalloc指令即可,假设MyMalloc()函数执行代码的起始地址为00401830,在INTEL32位芯片系列的机器中,jmp DWORD PTR MyMalloc这条指令的机器码为0xff2500401830,共6个字节,只要将这6个字节写入malloc()函数的开头6个字节中,即可实现调用malloc()函数时跳转到MyMalloc()函数。

    以下代码便能实现将跳转到MyMalloc()函数的指令写入malloc函数起始位置。
    DWORD lpSrcFunc = (DWORD)malloc;
    DWORD dwNewFunc = (DWROD)MyMalloc;
    DWORD lppNewFunc = &dwNewFunc;
    *(unsigned char *)lpSrcFunc = (unsigned char)0xff;
    *(((unsigned char *)lpSrcFunc)+1) = (unsigned char)0x25;


    memcpy( (void *)(lpSrcFunc+2),(const void *)&lppNewFunc,sizeof(DWORD) );
    如果是像Windows NT系列的操作系统,对执行代码的内存做了保护,是不能直接调用以上代码的,还需要使用VirtualProtect()函数将lpSrcFunc指向的地址处的内存属性改成可以写的,才可以调用以上代码。

    还要考虑将malloc()的调用改掉后再如何恢复的问题,否则程序运行很容易出现问题。恢复的做法便是将malloc()起始处的6个字节先保存起来,当要恢复时,将保存的6个字节写回原来位置即可。

    还需说明的是,以上介绍的是同一进程内的HOOK实现方法,如果要HOOK其他进程中的函数调用,还需要将上述代码注入到目标进程中去执行才可以实现HOOK功能,将代码注入到其他进程中的方法有很多种,Windows系统下常用的方法有以下几种。
    ① 使用Windows API函数 SetWindowsHookEx()将程序注入到其他进程中执行;
    ② 使用修改DLL IMPORT表的方法来实现对DLL中函数的HOOK;
    ③ 使用CreateRemoteThread()函数将代码注入到其他进程中执行;
    ④ 对于Windows NT系列操作系统,还可以使用注册表设置来实现将代码注入到其他进程中执行。



    以上几种方法在很多书籍中都有详细介绍,这里就不作进一步介绍。

    HOOK使用的注意事项

    使用HOOK时要特别小心,使用不当很容易导致程序崩溃。比如要HOOK malloc()和free()函数,如果HOOK的时机不当,会导致HOOK了的free()函数去释放未被HOOK的malloc()的内存,引起程序崩溃。

原文地址:http://www.mscto.com/shujujiegou/25533203.html

数据结构与算法:多个函数的HOOK实现

 采用前面的方法对多个函数实现HOOK,当设置HOOK时,如何保存那些函数的起始6 个字节?当取消HOOK时,如何从保存的数据中找到某个函数的起始6个字节?解决这个问题,可以用一个结构体数组来实现,结构体中包含源函数地址、源函数起始位置的6个字节数据等内容。HOOK管理结构体设计如下。
    typedef struct APIHOOKDATA_st {
    DWORD dwSrcFuncAddr; /* 源函数的地址 */
    DWORD dwNewFuncAddr; /* 钩子函数的地址 */
    BYTE byHeaderCode[6]; /* 源函数起始6字节 */
    WORD wFlag; /* 用来表示是否设置了HOOK的标志 */
    } APIHOOKDATA;
    typedef struct APIHOOK_st {
    APIHOOKDATA *pHookData;
    UINT uMaxFunctions;
    } APIHOOK;

    APIHOOKDATA结构体用来记录单个函数的HOOK数据,APIHOOK结构体用来管理多个函数的HOOK,里面包含一个APIHOOKDATA结构体数组,uMaxFunctions是数组的长度,表示最多可以HOOK的函数个数。

    1. 多个函数的HOOK管理 软件开发网 www.mscto.com

    对多个函数的HOOK管理可以分为以下四个操作步骤。
    ① 初始化;
    ② 设置某个函数的HOOK;
    ③ 取消某个函数的HOOK;
    ④ 关闭。

    下面以Windows NT系列操作系统为例来实现以上四个操作的代码。初始化操作主要是为结构体分配内存,将整个数组清零。编码如下。
    /** ApiHook 模块初始化函数
    @param INT nMaxHookFuncCounts——最大可设置的钩子数量
    @return INT (by default) ——返回NULL 表示失败
    */
    HANDLE ApiHook_Init(UINT uMaxFunctions)
    {
        APIHOOK * pApiHook = (APIHOOK *)malloc(sizeof(APIHOOK));
        if ( pApiHook )
        {
            pApiHook->uMaxFunctions = uMaxFunctions;


            pApiHook->pHookData =
            (APIHOOKDATA *)malloc(sizeof(APIHOOKDATA) *uMaxFunctions);
            if ( NULL != pApiHook->pHookData)
            {
                memset(pApiHook->pHookData,0,sizeof(APIHOOKDATA) *uMaxFunctions );
                return (HANDLE)pApiHook;
            }
            free(pApiHook);
        }
    return NULL;
    }

    2. 设置HOOK操作

    设置某个函数的HOOK操作主要是在数组中查找未使用的节点,将源函数相关信息保存到节点中,修改源函数起始位置的几个字节为一条跳转到钩子函数的指令。
    /** 通过地址来设置某个函数的钩子函数
    @param HANDLE hApiHook——由ApiHook_Init()函数生成的句柄
    @param DWORD dwSrcFuncAddr——源函数地址
    @param DWORD dwNewFuncAddr——钩子函数地址
    @return INT (by default) ——返回-1表示失败;返回0表示在HOOK数组中的序号
    */
  INT ApiHook_SetByAddr(HANDLE hApiHook,DWORD dwSrcFuncAddr,DWORD dwNewFuncAddr)
    {
        DWORD dwOldProtect;
        DWORD dwNewProtect;
        DWORD lpSrcFunc;
        DWORD lppNewFunc;
        UINT i;


        INT nAlreadyFlag = 0;
        if ( NULL == hApiHook )
        {
            return-1;
        }
        APIHOOK *pApiHook = (APIHOOK *)hApiHook;
        lpSrcFunc = dwSrcFuncAddr;
        /* 查找是否已设置了钩子 */
        for ( i = 0; i < pApiHook->uMaxFunctions; i++ )
        {
            if ( pApiHook->pHookData[i].dwSrcFuncAddr == lpSrcFunc )
            {
                /* 如果已经设置了钩子,仅仅改变 */


                nAlreadyFlag = 1;
                break;
            }
        }
        /* 如果没有设置源函数的钩子函数,在表中找出一个可供记录的位置 */
        if ( i == pApiHook->uMaxFunctions )
        {
            for ( i = 0; i < pApiHook->uMaxFunctions; i++ )
            {
                if (pApiHook->pHookData[i].wFlag == 0 )
                {


                    break;
                }
            }
        if ( i == pApiHook->uMaxFunctions )
        {
            return-1;
        }
    }
    /* 将新的钩子函数地址记录到表中 */
    pApiHook->pHookData[i].dwNewFuncAddr = dwNewFuncAddr;
    /* 以下这段代码将源函数头部6个字节保存到表中 */
    lppNewFunc = (DWORD)(&(pApiHook->pHookData[i].dwNewFuncAddr) );
    if ( !nAlreadyFlag )
    {
        /* 将源函数起始处6个字节保存到 byHeaderCode.中 */


        memcpy( pApiHook->pHookData[i].byHeaderCode,(const void *)lpSrcFunc,6);
    }
    /* 以下这段代码将源函数首部6个字节改成为一条跳转到新函数地址的指令 */
    if ( VirtualProtect( (LPVOID)lpSrcFunc,6, PAGE_EXECUTE_READWRITE,&dwOldProtect ) == 0 )
    {
        return-1;
    }
    *(unsigned char *)lpSrcFunc = (unsigned char)0xff;
    *(((unsigned char *)lpSrcFunc)+1) = (unsigned char)0x25;
    memcpy( (void *)(lpSrcFunc+2),(const void *)&lppNewFunc,4 );
    if ( VirtualProtect( (LPVOID)lpSrcFunc,6,dwOldProtect, &dwNewProtect) == 0 )
    {
        return-1;
    };
    pApiHook->pHookData[i].wFlag = 1;
    pApiHook->pHookData[i].dwSrcFuncAddr = lpSrcFunc;


    return (INT)i;
    }
 3. 取消HOOK操作

    取消某个函数的HOOK操作和设置操作相反,主要是在数组中查到要操作的函数的相关记录信息,将源函数起始位置处的6个字节恢复,清除数组中的对应记录信息。编码如下。

    /** 取消某个函数的钩子函数设置
    @param HANDLE hApiHook——由ApiHook_Init()函数生成的句柄
    @param DWORD dwSrcFuncAddr——源函数地址
    @return INT (by default) ——返回-1表示失败;返回0表示在HOOK数组中的序号
    */
    INT ApiHook_UnSetByAddr(HANDLE hApiHook,DWORD dwSrcFuncAddr)
    {
        UINT i;
        DWORD dwOldProtect;
        DWORD dwNewProtect;
        DWORD lpSrcFunc;
        APIHOOK *pApiHook = (APIHOOK *)hApiHook; 软件开发网 www.mscto.com
        if ( NULL == hApiHook )
        {
            return-1;
        }
        for ( i = 0; i < pApiHook->uMaxFunctions; i++ )
        {
            if ( pApiHook->pHookData[i].dwSrcFuncAddr == dwSrcFuncAddr )
            {
                break;
            }
        }
        if ( i == pApiHook->uMaxFunctions )
        {
            return-1;
        }
        lpSrcFunc = pApiHook->pHookData[i].dwSrcFuncAddr;
        /* 将lpSrcFunc指向的内存属性修改为可以读写的 */
        if ( VirtualProtect( ( LPVOID)lpSrcFunc,6,PAGE_EXECUTE_READWRITE,&dwOldProtect ) == 0 )
        {
            return-1;
        }
        memcpy( (void *)lpSrcFunc,pApiHook->pHookData[i].byHeaderCode,6 );
        /* 恢复lpSrcFunc指向的内存属性 */
        if ( VirtualProtect( (LPVOID)lpSrcFunc,6,dwOldProtect,&dwNewProtect ) == 0 )
        {
            return-1;


        }
        pApiHook->pHookData[i].wFlag = 0;
        if ( pApiHook->pHookData[i].pszDllName )
        {
            free(pApiHook->pHookData[i].pszDllName);
            pApiHook->pHookData[i].pszDllName = NULL;
        }
        if ( pApiHook->pHookData[i].pszFuncName )
        {
            free( pApiHook->pHookData[i].pszFuncName );
            pApiHook->pHookData[i].pszFuncName = NULL;
        }
        return (INT)i;


    }

原文地址:http://www.mscto.com/shujujiegou/25533303.html

一种C语言打桩函数的源码实现

实际工作中,在我们写好源代码后,通常需要对代码进行UT、FT测试,这个时候我们经常需要“打桩”。...

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

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

简单的hookapi C语言版

什么叫Hook API?所谓Hook就是钩子的意思,而API是指Windows开放给程序员的编程接口,使得在用户级别下可以对操作系统进行控制,也就是一般的应用程 序都需要调用API来完成某些功能,Ho...

对于HOOK函数的一点认识

这种函数是Windows消息处理机制的一部分,通过设置“钩子”,应用程序可以在系统级对所有消息、事件进行过滤,访问在正常情况下无法访问的消息。当然,这么做也是需要付出一定的代价的。由于多了这么一道处理...

C++单元测试--打桩测试

一 问题的产生 当构造测试用例的数据,是在函数内部被另一个外部函数所使用时,我们需要忽略外部函数所带来的影响。 需要进行“打桩”,举一个具体的例子 class DataGette...

打桩 转

打桩是一种用定制的函数替换链接库函数且不需重新编译的技术。甚至可用此技术替换系统调用(更确切地说,库函数包装系统调用)。可能的应用是沙盒、调试或性能优化库。为演示过程,此处给出一个简单库,以记录GNU...

浅谈白盒测试中桩函数的灵活应用

浅谈白盒测试中桩函数的灵活应用作者:张元礼http://blog.csdn.net/vincetest首先我们得了解下什么是桩函数,如下有个定义,更具体可以查阅白盒测试相关文章了解。测试桩:代替被测模...

认识单元测试中的打桩

什么是桩        桩,或称桩代码,是指用来代替关联代码或者未实现代码的代码。如果函数B用B1来代替,那么,B称为原函数,B1称为桩函数。打桩就是编写或生成桩代码。        打桩的目的 ...

node测试框架:mocha+should.js

对于任何一个项目来说,单元测试都是必不可少的一项工作。nodejs也是一样。   关于js的测试框架有很多,最终我选择了mocha。   Mocha项目地址:http://visionmedia....

stub函数

stub 函数,叫它桩函数,存根函数都可以,用一个桩函数替换一些接口函数,用于测试当前函数的特性。譬如说,要测试一个函数 f()void f(){var = g(...);}f()函数中调用了函数 g...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:函数的打桩和hook
举报原因:
原因补充:

(最多只允许输入30个字)