函数的打桩和hook

 

刚参加工作,总是听到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

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值