技术领域
本发明涉及测试领域,尤其涉及一种为脚本接口增加API钩子测试的方法。
技术背景
API钩子是一种监视函数调用的装置,当被钩中的API函数执行时,会自动调用钩子函数,从而使得测试工具能够监视重点函数的执行以及修改被钩中的函数的行为。API钩子在测试中有很重要的作用,通过该装置能够方便的获得重要的被测试函数的行为,以及改变被测试函数的行为,从而为测试自动化提供很多方便。
在测试中,可执行程序(包括动态库等形式)头部记录了大量有用信息,这其中包含了与本专利有关的内容为,应用程序的引入函数列表,该应用程序从哪些动态库引入了哪些函数都记录在这个表格中。这种表格的结构很简单,表格基本可以分为两部分,一部分是动态库名称以及动态库,另一部分是跳转指令列表,拿之类的工具查看一个可执行程序时,可以看到程序头部有一段包含了动态库名称和函数名称的文本数据,这一段就是函数名称列表;在这样的表格后面,能够看到很多由‘’组成的序列,这些序列实际包含了两组指令,,是一个JMP命令,8BC0是一个MOV指令(实际运行中该指令不会发挥任何作用),比如‘’对应的汇编指令为[]和,EAX两条;上述的两组表格(函数名和JMP指令)的关系:一个函数名对应一个JMP指令,函数名按所属的动态库分组,JMP指令也分组,函数在函数名列表中的顺序与JMP指令表中的顺序相反;举例讲,如果应用程序引入了.dll以及.dll两个动态库,在.dll中引入了3个函数比如叫a、b、c,在中引入了2个函数d、e;那么有5条对应的jmp指令,第一jmp指令表示系统调用函数c时跳转,第二jmp指令表示系统调用函数b时跳转,第三jmp指令表示系统调用函数a时跳转,第四jmp指令表示系统调用函数e时跳转,第五jmp指令表示系统调用函数d时跳转。
jmp加一个目标地址实际上也构成了一个最简单的函数,应用程序中调用该动态库的函数时,实际就是调用的该由jmp指令构成的最简单的函数,由该jmp指令完成对真实函数的调用。
可通过提供的API函数、以及几个函数获得该jmp函数地址。
API钩子的原理就是,通过修改上述表格的jmp的参数列,使其指向钩子函数,在钩子函数中记录输入参数等信息,再调用原来的函数,然后可以获得函数返回结果等信息。
通过高级语言按照上述原理实现一个API钩子比较容易,在类似实时翻译软件等工具中有广泛应用。但是通过高级语言完成的这些钩子,只能完成定死的任务,无法为其它程序所共享,且很不灵活,因此应用范围有很大限制。
目前有很多脚本引擎如TCL、、perl等,若工具能够通过脚本引擎设置这样的钩子,根据设置条件实现指定的工作,则钩子将可以十分灵活,也就能够发挥更大的作用。
现有方案局限于通过高级语言按照上述原理实现定死的钩子。这种钩子只能实现单一的固定功能,具有这种钩子的应用程序,若想修改钩子的功能则需要修改源代码,重新编译。
基于上述限制,这种固定的模式只能对指定的对象设置这种钩子,若需要针对不同的目标设置不同的钩子,则无法做到。若作为测试工具,面对的被测试应用程序很多,若为每个应用程序定制具有指定钩子的测试工具,则不经济也不好管理。
发明内容
本发明的目的是提供一种钩子设置方法,通过该方法可以灵活地设置钩子,对不同的目标可以灵活地设置不同的钩子,以方便测试的实现。
为此,本发明采用如下方案:
一种在中实现API钩子测试的方法,其包括以下步骤:
a、根据脚本请求动态生成一个可执行函数,该可执行函数可以依不同的测试要求将测试所需参数传递给公共主处理函数;
b、根据脚本设置接口提供的参数,设置API钩子函数;
c、当系统调用被勾挂的API时,直接触发对上述动态生成的函数的调用;
d、动态生成的函数根据生成该函数时记录在该函数内的信息,把参数传递给主处理函数;
e、主处理函数执行并根据传入的参数确定是否执行以及执行的脚本;
f、主处理函数执行完成之后,返回到动态生成的函数,动态生成的函数恢复堆栈,返回到系统调用钩子API的地方。
所述的步骤a,是通过编写机器码来实现的。
所述的步骤a中,所述的参数至少包括被钩挂的函数的地址、需要执行的脚本、参数个数及是否需要执行原来的API等信息中的一个或几个的组合。
所述的步骤b中,所述的脚本设置接口提供的参数为:被勾挂的动态库,被勾挂的函数名,被勾挂函数的参数个数,钩子被触发时执行的脚本字符串。
所述的步骤b中,设置API钩子的步骤,具体包括:
b1、获得被勾挂函数在内存中的位置;
b2、动态构造一个可执行的函数;
b3、记录应用程序引入函数列表中jmp指令的参数列的值;
b4、替换应用程序引入函数列表中jmp指定的参数列的值,使其跳向b2步构造出来的新函数;
b5、结束并返回执行结果。
所述的步骤b1,是通过及函数实现的。
所述的步骤b2进一步包括:
b21、申请一段内存,用于存放函数的执行代码;
b22、填入机器码,依次把通过手册查好的各种汇编命令对应的二进制机器码写入上述申请的内存。
所述的在中实现API钩子测试的方法,还包括生成一个数据记录,该数据记录记载进入钩子函数时需要执行的脚本和退出钩子函数前需要执行的脚本。
本发明技术方案带来的有益效果:
可以通过脚本实现对API函数执行过程的监视和控制,比如测试过程中经常有要求验证一些API函数执行失败或者失效时系统的处理;比如验证申请内存失败后系统应该做的处理,构造内存申请失败是很困难的,通过设置钩子,则很容易完成此项工作。
再如测试过程中希望记录和比较一些API的执行效率,若不通过加入钩子的手段则无法实现。
对于监视一些重要API执行过程,此法则更加奏效,比如监视接口函数收发消息的具体值,监视消息处理函数收到的消息内容等。
通过脚本提供API钩子接口,无需修改措施工具的代码,即可修改钩子内容。
可以灵活地取消钩子。
能够方便地共享钩子函数。
附图说明
图1是本发明的测试流程图。
具体实施方式
下面结合说明书附图来说明本发明的具体实施方式。
由于需要设置的钩子目标函数以及设置钩子的数目是未知的,因此无法通过固定有限个数的预定好的函数作为钩子函数,而必须能够动态的根据脚本请求动态生成可执行的函数。运行阶段生成新的函数,需要通过编写机器码实现。动态生成的函数实现功能仅仅是把正确的参数(包括被勾挂的函数的地址,钩子函数的地址,需要执行的脚本,参数个数,是否需要执行老的API等等信息)传给公共的主处理函数,由公共的主处理函数完成诸如脚本调用以及老的API的调用参数以及返回值的获取等工作。
由脚本设置接口提供以下参数:被勾挂的动态库,被勾挂的函数名,被勾挂函数的参数个数,钩子被触发时执行的脚本字符串,格式为:(动态库名,函数名,触发时执行的脚本,参数个数,执行结束前需要执行的脚本,是否需要执行被勾挂的函数本身);根据这些参数,按照以下步骤完成钩子实现设置:
1、通过以及函数获得被勾挂函数在内存中的位置,若未引入该函数则不进行勾挂;
2、动态构造一个可执行的函数(具体构造方法后面详细描述);
3、记录jmp指令的参数列的值;
4、替换jmp指定的参数列的值,使其跳向上述第二步动态构造出来的新函数;
5、结束返回执行结果。
动态构造一个可执行的函数至少需要被勾挂的函数地址、被勾挂的函数的参数个数两个参数,为了方便处理和易于扩展,我们把其它的相关信息放在一个数据记录中,该记录可以记载进入钩子函数时需要执行的脚本,记录退出钩子函数前需要执行的脚本等内容。
生成函数的步骤:
1、申请一段内存,用于存放函数的执行代码(二进制机器码);
2、填入机器码,生成机器码的方法可以通过查阅汇编手册完成,填入机器码的方法就是依次把通过手册查好的各种汇编命令对应的二进制机器码写入上述申请的内存即可,动态生成的函数的功能如下:
1)保存esp,目的为了在函数执行之后能正确恢复堆栈指针,正确返回调用位置正确执行。
2)把记录有脚本内容的数据记录地址传给并执行主处理函数;主处理函数根据传入参数中记录的和脚本有关的信息进行处理,若定义了参数个数,则根据参数个数从堆栈中取出各参数的值;根据设置,若需要执行触发时的脚本,则把取出的参数值提供给脚本接口,执行触发时需执行的脚本;根据设置,若需要执行原来的函数,则把取得的参数按照正确的顺序压入堆栈并执行记录中记载的老的API函数。根据设置,若需要在退出前执行指定脚本,则执行对应的脚本。主处理程序结束。
3)执行完成之后根据前面记录的堆栈位置信息,以及参数个数信息正确设置堆栈内容,并返回。
已经成功填写了相应信息的这段内存就是一个合法的可执行的函数。
钩子函数被调用的过程:
1、若系统调用被勾挂的API,该调用将直接触发对上述动态生成的函数的调用;
2、动态生成的函数根据生成该函数时记录在该函数内的信息(相当于一些常数),把参数传递给主处理函数;
3、主处理函数执行并根据传入的参数确定是否以及执行那些脚本;
4、主处理函数执行完成之后,退到动态生成的函数,动态生成的函数恢复堆栈,返回。
获取参数以及调用原函数。
动态库中函数的参数传递一般都是通过堆栈来完成的,因此传给函数的参数需要从堆栈中获取,堆栈中除了存有参数信息之外还有其他一些信息,比如的编译器,在编译一个函数时,会固定保存一些寄存器,在函数调用结束后恢复这些寄存器,另外大家都知道的,当产生一个函数调用时,call指令会在堆栈中保存esp和eip两个寄存器,用于在执行ret指令时能够根据该信息正确返回到调用者调用该函数的位置,以便正确的继续执行。因此要获得参数的内容,需要能够正确计算出参数在堆栈中的位置。编译一个函数时,会自动把进入函数时的堆栈指针位置esp保存到ebp中,因此可以根据esp以及参数的相对位置获得参数的值,参数的相对位置根据调用层次不难算出相差为16个字节(2层调用,每层保存两个寄存器各占用8个字节共计16字节),每个参数占四个字节,由此若需要计算某个具体的参数位置为ebp的值加16再加参数编号乘以4即可,参数编号从左到右从0开始。
获取返回值。
返回值记录在eax中,把该值取出即可。
将函数封装成接口的方法,属于通用的脚本接口扩展方法,其具体执行可参照技术参考大全。
下面我们看一个具体的例子:
比如测试某版本时,我期望在界面出现中文字符时,测试工具自动给出提示,此时我们可以通过监视所有文本输出函数,取出传给文本输出函数的文字参数,并判断是否为中文的方式来完全自动实现。当有中文字符出现时给出警告,这样可以避免人工逐个检查之苦。针对这个例子具体一点:为了实现对文字输出函数的监控,假设在这里我们只监控函数,首先我们要通过脚本设置对该API的钩子,设置方法(′gdi32.dll′,′′,′′,5,‘’,true)。
其中第一个参数′gdi32.dll′表示被勾挂的API函数所属的动态库,第二个参数是被勾挂的函数名称,第三个参数是表示函数被触发时需要执行的脚本函数名称,第四个参数是被勾挂函数的参数个数,第五个参数是勾挂函数执行完成退出前需要执行的脚本函数名称,第六个参数是指勾挂函数中是否需要执行老的API函数。
经过上述设置后,也就是上述脚本被执行后,首先是完成钩子的设置,程序执行过程包括以下步骤:
1)首先根据请求动态生成一个可执行的函数,其中又包括以下步骤:
a)申请一段内存,记录该内存的起始地址;
b)填入目标机器码;
c)生成一个记录包括(′gdi32.dll′,′′,′′,5,‘’,true)所有参数以及在内数据记录;
d)将c)生成的数据记录的地址记录在a)申请的内存的指定位置,以便本节动态生成的函数执行时可以访问到。
2)找到指定动态库gdi32.dll中指定函数在内存中的地址,也就是对应JMP项的参数值,记录该参数值到上述c)的数据记录中。
3)替换2)中Jmp的参数为1)中动态生成的函数的起始地址。
当系统有文本输出,涉及到API函数调用时,将触发1)中动态生成的钩子函数。钩子函数根据记录函数内部的数据记录的地址,找到存放有参数信息的数据记录,调用一个通用的主处理函数。
在主处理函数中,首先取出进入时需要执行的脚本记录,执行该脚本,根据是否要执行老的函数的设置,如果要执行老的函数,则根据参数个数的记录,从堆栈中取出调用钩子函数时传递进来的参数的值(但是不改变堆栈的结构),把这些值按顺序压入堆栈,然后调用数据记录中记录的JMP参数所对应的函数;再根据钩子函数执行结束前需要执行的脚本记录,执行结束前的脚本。返回到动态创建的函数。
动态创建的函数根据参数个数退栈,返回到系统调用对应的位置继续向下执行。
比如这个例子中,我们可以在‘’函数中对传入的参数进行分析,的第4个参数是输出的文本字符串。判断其中是否有中文等双字节字符,只要把该字符串转换成,判断转换前后的字符串长度是否一样。如果转换后字符串变短了,表明存在双字节字符。此时给出警告即可。本发明可在普PC机(win98、、)环境下提供实现,但是方法在其它平台是适用的,只是具体的代码不适用而已。
以上所述,仅为本发明较佳的具体实施方式,但本发明的保护范围并不局限于此,任何熟悉本技术领域的技术人员在本发明揭露的技术范围内,可轻易想到的变化或替换,都应涵盖在本发明的保护范围之内。因此,本发明的保护范围应该以权利要求书的保护范围为准。