欢迎来到皮皮网网首页

【zookeeper源码下载】【内核源码阅读】【rss输出源码】Isbadcodeptr 源码

来源:查手机真伪源码 时间:2024-11-26 02:58:00

1.如何定位导致Crash的代码位置
2.屏幕取词

Isbadcodeptr 源码

如何定位导致Crash的代码位置

       1. 在开发环境下定位Crash错误

       ã€€ã€€1.1 普通的crash

       ã€€ã€€ã€€ã€€å…ˆæ¥çœ‹çœ‹æœ€æ™®é€šçš„crash

       ã€€ã€€ã€€ã€€å‚见图1(c.png)

       ã€€ã€€ã€€ã€€å½“你在debug模式下运行上面的程序就会弹出上面的框。vc就帮你定位到了错误的位置。是个对零指针的操作。非常简单,不是吗。

       ã€€ã€€1.2 较难定位的crash

       ã€€ã€€ã€€ã€€è¾ƒéš¾å®šä½çš„crash往往是由于内存错误(参见5.1 为什么程序crash时调用堆栈是乱的)。例如以下代码:

       ä»£ç 

       ã€€ã€€ã€€ã€€char *p = new char[];

       ã€€ã€€ã€€ã€€p[] = 0xfd;

       ã€€ã€€ã€€ã€€delete[] p;

       ã€€ã€€ã€€ã€€printf(p);

       ã€€ã€€ã€€ã€€ä»¥ä¸Šä»£ç æœ‰ä¸¤å¤„错误,一是第2行的内存写越界,二是第4行使用被删除的指针。

       ã€€ã€€ã€€ã€€ä½†ä»¥ä¸Šä»£ç åœ¨vc的release和debug下都不会报错。这使得这类错误很难定位。

       ã€€ã€€ã€€ã€€æ£€æµ‹è¿™ä¸€ç±»é—®é¢˜å¯ä»¥ä½¿ç”¨BoundsChecker工具的FinalCheck模式(BoundsChecker)

       ã€€ã€€ã€€ã€€ç”¨BoundsChecker检测后可得到两个错误:Write overrun(写越界) 和 Dangling pointer(使用被删除的指针)而且都精确定位到了出错的位置。是个不错的工具。

       ã€€ã€€ã€€ã€€å‚见图2(c.png)

       ã€€ã€€ã€€ã€€å‚见图3(c.png)

       ã€€ã€€1.3 注意vc的输出日志

       ã€€ã€€ã€€ã€€ç”±äºŽä¸€äº›ç›®å‰æœªçŸ¥çš„原因(有可能是程序的错误太严重或是BoundsChecker本身的bug),BoundsChekcer有时不能正常工作。

       ã€€ã€€ã€€ã€€è¿™é‡Œvc的输出日志有时能提供一些有用的信息。

       ã€€ã€€ã€€ã€€åœ¨éš¾æ‰¾çš„crash中,有很大一部分是引用了非法的指针。

       ã€€ã€€ã€€ã€€æœ‰æ—¶åœ¨vc的输出日志里可以看到类似于这样的信息

       ã€€ã€€ã€€ã€€â€œemule.exe 中的 0xb7 处最可能的异常: 0xC: 读取位置 0xfeeeff 时发生访问冲突 。”

       ã€€ã€€ã€€ã€€åœ¨ç¼ºå°‘BoundsChecker的支持时,这是一条很重要的信息。意思是说在“程序地址0xb7处”对“值为0xfeeeff的指针”进行操作。

       ã€€ã€€ã€€ã€€ï¼ˆæ€Žä¹ˆé€šè¿‡â€œç¨‹åºåœ°å€0xb7”找到对应的代码行可参照 3.1,)

       ã€€ã€€ã€€ã€€è¿™æ¡ä¿¡æ¯çš„重要性在于,这个操作只会触发一个警告,而不会导致crash,当crash真正发生时,很有可能不会在0xb7附近,

       ã€€ã€€ã€€ã€€ç”šè‡³è°ƒç”¨å †æ ˆéƒ½å·²ç»è¢«å†™ä¹±ï¼Œè®©ä½ æ— ä»Žä¸‹æ‰‹ã€‚(参见5.1 为什么程序crash时调用堆栈是乱的)

       2. 定位发布在外的版本的Crash错误

       ã€€ã€€å‘布在外的软件crash了,往往不好调试,所以目前很多软件都有“发送错误报告”这一功能。

       ã€€ã€€å®žçŽ°è¿™ä¸€åŠŸèƒ½ä¸€èˆ¬åˆ†ä»¥ä¸‹å‡ æ­¥ï¼š

       ã€€ã€€a. 使用SetUnhandledExceptionFilter函数

       ã€€ã€€ã€€ã€€ä½¿ç”¨SetUnhandledExceptionFilter设置最高一级的异常处理函数,当程序出现任何未处理的异常,都会触发你设置的函数里。具体使用可参照msdn和emule源码。

       ã€€ã€€b. 使用MiniDumpWriteDump函数

       ã€€ã€€ã€€ã€€åœ¨ä½ çš„异常处理函数里,使用MiniDumpWriteDump把错误信息存成特定格式的文件。具体使用可参照msdn和emule源码。

       ã€€ã€€c. 发送错误报告

       ã€€ã€€ã€€ã€€é€‰ç”¨ä¸€ç§å½¢å¼æŠŠç¬¬äºŒæ­¥äº§ç”Ÿçš„错误报告(.dmp)文件发送给你指定的地方。

       ã€€ã€€d. 查看错误报告

       ã€€ã€€ã€€ã€€è¿™é‡Œä»‹ç»ç”¨vc查看错误报告的方法,还可以用windows debug tools这个工具看,方法见5.2 使用windows debug tools查看.dmp文件(错误报告)

       ã€€ã€€ã€€ã€€

       ã€€ã€€ã€€ã€€æŸ¥çœ‹é”™è¯¯æŠ¥å‘Šéœ€è¦æœ‰ä¸‰æ ·ä¸œè¥¿ï¼šå¯¹åº”release版的代码,当时编译release版所产生的.exe和.pdb文件。(这两个文件都在编译的输出目录里。)所以当程序发布时,要保留下这两个文件。

       ã€€ã€€ã€€ã€€æŠŠ.dmp(错误报告文件),源码zookeeper源码下载 .pdb, .exe. 代码,在同一目录下,用vc打开.dmp 文件。

       ã€€ã€€ã€€ã€€æŒ‰F5运行,程序即到达crash时的状态,可以对其进行相应的分析。

       ã€€ã€€ã€€ã€€

       ã€€ã€€ä¸€ç‚¹è¡¥å……:当没有“发送错误报告”的功能,或是此功能失效,以致弹出了windows的“发送错误报告”的对话框。这时其实也是有错误报告的,一般在C:Documents and Settings用户名Local SettingsTemp里的一个.dmp文件(一般只有一个.dmp)

       3. 小技巧

       ã€€ã€€3.1 根据程序地址找到代码位置

       ã€€ã€€ã€€ã€€å¯æŒ‰å¦‚下步骤:

       ã€€ã€€ã€€ã€€a. 使程序处于停止状态。(比如程序运行时,在vc里按Ctrl+Alt+Break,或设断点使程序停下)

       ã€€ã€€ã€€ã€€b. 切换到汇编状态。(Ctrl+F)

       ã€€ã€€ã€€ã€€c. 在地址栏输入程序地址,回车。

       ã€€ã€€ã€€ã€€d. 可按Ctrl+F切回代码模式。

       ã€€ã€€3.2 根据消息值查看对应的windows消息

       ã€€ã€€ã€€ã€€åœ¨vc的监视窗口里输入“消息值,wm”即可看到对应的消息。

       ã€€ã€€3.3 查看GetLastError返回值

       ã€€ã€€ã€€ã€€åœ¨vc的监视窗口里输入“@err,hr”,即可看到LastError及其解释。

       ã€€ã€€3.4 在代码中暂停程序

       ã€€ã€€ã€€ã€€åœ¨debug版中可以在代码中加上“AfxDebugBreak();”以暂停程序。release版可使用 “_asm int 3;”

       4. 编程小警示

       ã€€ã€€4.1 慎用IsBadPtr系列函数

       ã€€ã€€ã€€ã€€å½“使用IsBadReadPtr, IsBadWritePtr, IsBadCodePtr一系列函数时要注意,这一类函数可能并不能达到你所想要的意图。

       ã€€ã€€ã€€ã€€æ¯”如下面的代码,两个返回都是false。

       ä»£ç 

        char *p = new char[];

        bool b;

        b = IsBadReadPtr(p+, 1);

        delete[] p;

        char *q = new char[];

        b = IsBadReadPtr(p, 1);

       ã€€ã€€ã€€ã€€æ‰€ä»¥åˆ‡å¿Œåœ¨ç¨‹åºä¸­ä»¥IsBadPtr函数来判断是否可以对这个指针进行操作。这些函数只能在调试中使用,或是你确切的知道这些函数的返回值表示的是什么意义的时候。

       ã€€ã€€4.2 慎用catch(...)

       ã€€ã€€ã€€ã€€ä¸ºäº†é˜²æ­¢ç¨‹åºcrash或是解决一个不明白的crash时,大家很容易想到一个 try{ }catch(...){ }来解决问题。

       ã€€ã€€ã€€ã€€çš„确大部分时间这样不会出问题了,但这个try-catch很有可能隐藏掉在try里面的错误,而当由此错误引起其他错误时,就很难追踪到这个问题了。

       ã€€ã€€ã€€ã€€æ‰€ä»¥å»ºè®®å°½é‡å°‘用catch(...),如果知道某一块代码会抛出异常,应该用确切的写法。比如catch(CFileException *e), catch(int e)等等。

       5. 附录

       ã€€ã€€5.1 为什么程序crash时调用堆栈是乱的

       ã€€ã€€ã€€ã€€å½“内存被写乱时程序很有可能出现很难定位的crash,比如调用堆栈是乱的,或走到不存在的代码里。这里举一例

       ä»£ç 

        class CA

        {

        public:

        CA(){ }

        ~CA(){ }

        virtual f(){ }

        };

        void show()

        {

        printf("shown");

        }

        int main()

        {

        // 对像被创建删除

        CA *p = new CA;

        delete p;

        // 一些正常的操作

        int *q = new int;

        int codeAddress = (int)show;

        *q = (int)&codeAddress;

        // 调用被删除的对像,程序有可能执行到任何地方。

        p->f();

        }

       ã€€ã€€ã€€ã€€ä¸Šé¢ä»£ç çš„结果会走到show()里显示出show(这跟编译环境有关,vc下测试结果是这样)。如果你了解c++的vtable机制就明白这是怎么回事。

       ã€€ã€€ã€€ã€€å¦‚果不明白也没关系,我下面说个大概。

       ã€€ã€€ã€€ã€€é¦–å…ˆCA对象被创建又被删除。如果此时调用p->f()多半会crash。

       ã€€ã€€ã€€ã€€ç¬¬äºŒå—代码可视为一些正常的操作,new了一个q,然后对它进行了一些赋值。

       ã€€ã€€ã€€ã€€å¦‚果你不明白vtable机制,那你只要知道,最后一行的 “p->f();”会执行变量q所指向的变量所指向的变量所标示的地址。(这里没打错字,是两个“所指向的变量”)

       ã€€ã€€ã€€ã€€è¿™é‡Œæˆ‘“心地善良”的给这个值赋上了一个合法的函数地址show。但实际程序中q所指向的变量有可能是任意值,它再指向的变量就更是任意值了。

       ã€€ã€€ã€€ã€€é‚£ç¨‹åºå°±ä¸çŸ¥é“跑哪里去了。如果这个值过于离谱,那算你运气好,程序会立即crash,而你就知道错误位置在哪了。但讨人厌的是,它有可能是一个合法地址,那程序就继续走下去,

       ã€€ã€€ã€€ã€€ä½†è¿Ÿæ—©ä¼šcrash,并且调用堆栈面目全非(原因牵涉到“调用堆栈的推导”的问题这里就不多说了),

       ã€€ã€€ã€€ã€€åˆ°æ—¶å°±æ ¹æœ¬æ— ä»ŽçŸ¥é“原来是调用了被删除的p对象而导致的。

       ã€€ã€€5.2 使用Debugging tools for windows查看.dmp文件(错误报告)

       ã€€ã€€ã€€ã€€a. 准备好程序对应的代码,exe文件,pdb文件(编译时在编译输出目录里)

       ã€€ã€€ã€€ã€€b. 安装WinDbg

       ã€€ã€€ã€€ã€€c. 在winDbg里把Symbol目录设在.pdb所在目录,Image目录设在.exe所在目录,code目录设到代码目录。

       ã€€ã€€ã€€ã€€d. 打开.dmp文件

       ã€€ã€€ã€€ã€€e. 输入命令.ecxr。(此命令使环境回到崩溃时的状态)

       ã€€ã€€ã€€ã€€f. 打开调用堆栈(ALT + F6)查看Crash的位置

       ã€€ã€€ã€€ã€€g. 进行分析

       ç®€ä»‹

       ã€€ã€€ï¼ˆFinalCheck能检测出的错误列表见附录1)

       ã€€ã€€BoundsChecker是一个很强大的调试工具。这里只简单介绍如何用它的FinalCheck模式定位比较难定位的错误。

       FinalCheck模式简单来说就是BoundsChecker在你的代码里加一些诊断代码来检查平时比较难查出的内存越界,错误的指针使用等。

       ä¸è¿‡ä»˜å‡ºçš„代价就是程序跑起来会比较慢,所以在不用时最好是把FinalCheck模式关掉。特别是发布前。

屏幕取词

       â€œé¼ æ ‡å±å¹•å–词”技术是在电子字典中得到广泛地应用的,如四通利方和金山词霸等软件,这个技术看似简单,其实在WINDOWS系统中实现却是非常复杂的,总的来说有两种实现方式:

        第一种:采用截获对部分GDI的API调用来实现,如TextOut,TextOutA等。

        第二种:对每个设备上下文(DC)做一分Copy,并跟踪所有修改上下文(DC)的操作。

        第二种方法更强大,但兼容性不好,而第一种方法使用的截获WindowsAPI的调用,这项技术的强大可能远远超出了您的想象,毫不夸张的说,利用WindowsAPI拦截技术,你可以改造整个操作系统,事实上很多外挂式Windows中文平台就是这么实现的!而这项技术也正是这篇文章的主题。

        截WindowsAPI的调用,具体的说来也可以分为两种方法:

        第一种方法通过直接改写WinAPI 在内存中的映像,嵌入汇编代码,使之被调用时跳转到指定的地址运行来截获;第二种方法则改写IAT(Import Address Table 输入地址表),重定向WinAPI函数的调用来实现对WinAPI的截获。

        第一种方法的实现较为繁琐,而且在Win、下面更有难度,这是因为虽然微软说WIN的API只是为了兼容性才保留下来,程序员应该尽可能地调用位的API,实际上根本就不是这样!WIN 9X内部的大部分位API经过变换调用了同名的位API,也就是说我们需要在拦截的函数中嵌入位汇编代码!

        我们将要介绍的是第二种拦截方法,这种方法在Win、和NT下面运行都比较稳定,兼容性较好。由于需要用到关于Windows虚拟内存的管理、打破进程边界墙、向应用程序的进程空间中注入代码、PE(Portable Executable)文件格式和IAT(输入地址表)等较底层的知识,所以我们先对涉及到的这些知识大概地做一个介绍,最后会给出拦截部分的关键代码。

        先说Windows虚拟内存的管理。Windows9X给每一个进程分配了4GB的地址空间,对于NT来说,这个数字是2GB,系统保留了2GB到 4GB之间的地址空间禁止进程访问,而在Win9X中,2GB到4GB这部分虚拟地址空间实际上是由所有的WIN进程所共享的,这部分地址空间加载了共享Win DLL、内存映射文件和VXD、内存管理器和文件系统码,Win9X中这部分对于每一个进程都是可见的,这也是Win9X操作系统不够健壮的原因。

        Win9X中为位操作系统保留了0到4MB的地址空间,而在4MB到2GB之间也就是Win进程私有的地址空间,由于 每个进程的地址空间都是相对独立的,也就是说,如果程序想截获其它进程中的API调用,就必须打破进程边界墙,向其它的进程中注入截获API调用的代码,这项工作我们交给钩子函数(SetWindowsHookEx)来完成,关于如何创建一个包含系统钩子的动态链接库,《电脑高手杂志》已经有过专题介绍了,这里就不赘述了。

        所有系统钩子的函数必须要在动态库里,这样的话,当进程隐式或显式调用一个动态库里的函数时,系统会把这个动态库映射到这个进程的虚拟地址空间里,这使得DLL成为进程的一部分,以这个进程的身份执行,使用这个进程的堆栈,也就是说动态链接库中的代码被钩子函数注入了其它GUI进程的地址空间(非GUI进程,钩子函数就无能为力了),当包含钩子的DLL注入其它进程后,就可以取得映射到这个进程虚拟内存里的各个模块(EXE和DLL)的基地址,如:

       HMODULE hmodule=GetModuleHandle(“Mypro.exe”);

        在MFC程序中,我们可以用AfxGetInstanceHandle()函数来得到模块的基地址。EXE和DLL被映射到虚拟内存空间的什么地方是由它们的基地址决定的。它们的基地址是在链接时由链接器决定的。当你新建一个Win工程时,VC++链接器使用缺省的基地址0x。可以通过链接器的BASE选项改变模块的基地址。EXE通常被映射到虚拟内存的0x处,DLL也随之有不同的基地址,通常被映射到不同进程的相同的虚拟地址空间处。

        系统将EXE和DLL原封不动映射到虚拟内存空间中,它们在内存中的结构与磁盘上的静态文件结构是一样的。即PE (Portable Executable) 文件格式。我们得到了进程模块的基地址以后,就可以根据PE文件的格式穷举这个模块的IMAGE_IMPORT_DESCRIPTOR数组,看看进程空间中是否引入了我们需要截获的函数所在的动态链接库,比如需要截获“TextOutA”,就必须检查“Gdi.dll”是否被引入了。

        说到这里,我们有必要介绍一下PE文件的格式,如右图,这是PE文件格式的大致框图,最前面是文件头,我们不必理会,从PE File Optional Header后面开始,就是文件中各个段的说明,说明后面才是真正的段数据,而实际上我们关心的只有一个段,那就是“.idata”段,这个段中包含了所有的引入函数信息,还有IAT(Import Address Table)的RVA(Relative Virtual Address)地址。

        说到这里,截获WindowsAPI的整个原理就要真相大白了。实际上所有进程对给定的API函数的调用总是通过PE文件的一个地方来转移的,这就是一个该模块(可以是EXE或DLL)的“.idata”段中的IAT输入地址表(Import Address Table)。在那里有所有本模块调用的其它DLL的函数名及地址。对其它DLL的函数调用实际上只是跳转到输入地址表,由输入地址表再跳转到DLL真正的函数入口。

        具体来说,我们将通过IMAGE_IMPORT_DESCRIPTOR数组来访问“.idata”段中引入的DLL的信息,然后通过IMAGE_THUNK_DATA数组来针对一个被引入的DLL访问该DLL中被引入的每个函数的信息,找到我们需要截获的函数的跳转地址,然后改成我们自己的函数的地址……具体的做法在后面的关键代码中会有详细的讲解。

        讲了这么多原理,现在让我们回到“鼠标屏幕取词”的专题上来。除了API函数的截获,要实现“鼠标屏幕取词”,还需要做一些其它的工作,简单的说来,可以把一个完整的取词过程归纳成以下几个步骤:

        1. 安装鼠标钩子,通过钩子函数获得鼠标消息。

        使用到的API函数:SetWindowsHookEx

        2. 得到鼠标的当前位置,向鼠标下的窗口发重画消息,让它调用系统函数重画窗口。

        使用到的API函数:WindowFromPoint,ScreenToClient,InvalidateRect

        3. 截获对系统函数的调用,取得参数,也就是我们要取的词。

        对于大多数的Windows应用程序来说,如果要取词,我们需要截获的是“Gdi.dll”中的“TextOutA”函数。

        我们先仿照TextOutA函数写一个自己的MyTextOutA函数,如:

       BOOL WINAPI MyTextOutA(HDC hdc, int nXStart, int nYStart, LPCSTR lpszString,int cbString)

       {

       // 这里进行输出lpszString的处理

       // 然后调用正版的TextOutA函数

       }

        把这个函数放在安装了钩子的动态连接库中,然后调用我们最后给出的HookImportFunction函数来截获进程对TextOutA函数的调用,跳转到我们的MyTextOutA函数,完成对输出字符串的捕捉。

        HookImportFunction的用法:

       HOOKFUNCDESC hd;

       PROC pOrigFuns;

       hd.szFunc="TextOutA";

       hd.pProc=(PROC)MyTextOutA;

       HookImportFunction (AfxGetInstanceHandle(),"gdi.dll",&hd,pOrigFuns);

        下面给出了HookImportFunction的源代码,相信详尽的注释一定不会让您觉得理解截获到底是怎么实现的很难,Ok,Let’s Go:

       ///////////////////////////////////////////// Begin ///////////////////////////////////////////////////////////////

       #include <crtdbg.h>

       // 这里定义了一个产生指针的宏

       #define MakePtr(cast, ptr, AddValue) (cast)((DWORD)(ptr)+(DWORD)(AddValue))

       // 定义了HOOKFUNCDESC结构,我们用这个结构作为参数传给HookImportFunction函数

       typedef struct tag_HOOKFUNCDESC

       {

       LPCSTR szFunc; // The name of the function to hook.

       PROC pProc; // The procedure to blast in.

       } HOOKFUNCDESC , * LPHOOKFUNCDESC;

       // 这个函数监测当前系统是否是WindowNT

       BOOL IsNT();

       // 这个函数得到hModule -- 即我们需要截获的函数所在的DLL模块的引入描述符(import descriptor)

       PIMAGE_IMPORT_DESCRIPTOR GetNamedImportDescriptor(HMODULE hModule, LPCSTR szImportModule);

       // 我们的主函数

       BOOL HookImportFunction(HMODULE hModule, LPCSTR szImportModule,

       LPHOOKFUNCDESC paHookFunc, PROC* paOrigFuncs)

       {

       /////////////////////// 下面的代码检测参数的有效性 ////////////////////////////

       _ASSERT(szImportModule);

       _ASSERT(!IsBadReadPtr(paHookFunc, sizeof(HOOKFUNCDESC)));

       #ifdef _DEBUG

       if (paOrigFuncs) _ASSERT(!IsBadWritePtr(paOrigFuncs, sizeof(PROC)));

       _ASSERT(paHookFunc.szFunc);

       _ASSERT(*paHookFunc.szFunc != '\0');

       _ASSERT(!IsBadCodePtr(paHookFunc.pProc));

       #endif

       if ((szImportModule == NULL) || (IsBadReadPtr(paHookFunc, sizeof(HOOKFUNCDESC))))

       {

       _ASSERT(FALSE);

       SetLastErrorEx(ERROR_INVALID_PARAMETER, SLE_ERROR);

       return FALSE;

       }

       //////////////////////////////////////////////////////////////////////////////

       // 监测当前模块是否是在2GB虚拟内存空间之上

       // 这部分的地址内存是属于Win进程共享的

       if (!IsNT() && ((DWORD)hModule >= 0x))

       {

       _ASSERT(FALSE);

       SetLastErrorEx(ERROR_INVALID_HANDLE, SLE_ERROR);

       return FALSE;

       }

       // 清零

       if (paOrigFuncs) memset(paOrigFuncs, NULL, sizeof(PROC));

       // 调用GetNamedImportDescriptor()函数,来得到hModule -- 即我们需要

       // 截获的函数所在的DLL模块的引入描述符(import descriptor)

       PIMAGE_IMPORT_DESCRIPTOR pImportDesc = GetNamedImportDescriptor(hModule, szImportModule);

       if (pImportDesc == NULL)

       return FALSE; // 若为空,则模块未被当前进程所引入

       // 从DLL模块中得到原始的THUNK信息,因为pImportDesc->FirstThunk数组中的原始信息已经

       // 在应用程序引入该DLL时覆盖上了所有的引入信息,所以我们需要通过取得pImportDesc->OriginalFirstThunk

       // 指针来访问引入函数名等信息

       PIMAGE_THUNK_DATA pOrigThunk = MakePtr(PIMAGE_THUNK_DATA, hModule,

       pImportDesc->OriginalFirstThunk);

       // 从pImportDesc->FirstThunk得到IMAGE_THUNK_DATA数组的指针,由于这里在DLL被引入时已经填充了

       // 所有的引入信息,所以真正的截获实际上正是在这里进行的

       PIMAGE_THUNK_DATA pRealThunk = MakePtr(PIMAGE_THUNK_DATA, hModule, pImportDesc->FirstThunk);

       // 穷举IMAGE_THUNK_DATA数组,寻找我们需要截获的函数,这是最关键的部分!

       while (pOrigThunk->u1.Function)

       {

       // 只寻找那些按函数名而不是序号引入的函数

       if (IMAGE_ORDINAL_FLAG != (pOrigThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG))

       {

       // 得到引入函数的函数名

       PIMAGE_IMPORT_BY_NAME pByName = MakePtr(PIMAGE_IMPORT_BY_NAME, hModule,

       pOrigThunk->u1.AddressOfData);

       // 如果函数名以NULL开始,跳过,继续下一个函数

       if ('\0' == pByName->Name[0])

       continue;

       // bDoHook用来检查是否截获成功

       BOOL bDoHook = FALSE;

       // 检查是否当前函数是我们需要截获的函数

       if ((paHookFunc.szFunc[0] == pByName->Name[0]) &&

       (strcmpi(paHookFunc.szFunc, (char*)pByName->Name) == 0))

       {

       // 找到了!

       if (paHookFunc.pProc)

       bDoHook = TRUE;

       }

       if (bDoHook)

       {

       // 我们已经找到了所要截获的函数,那么就开始动手吧

       // 首先要做的是改变这一块虚拟内存的内存保护状态,让我们可以自由存取

       MEMORY_BASIC_INFORMATION mbi_thunk;

       VirtualQuery(pRealThunk, &mbi_thunk, sizeof(MEMORY_BASIC_INFORMATION));

       _ASSERT(VirtualProtect(mbi_thunk.BaseAddress, mbi_thunk.RegionSize,

       PAGE_READWRITE, &mbi_thunk.Protect));

       // 保存我们所要截获的函数的正确跳转地址

       if (paOrigFuncs)

       paOrigFuncs = (PROC)pRealThunk->u1.Function;

       // 将IMAGE_THUNK_DATA数组中的函数跳转地址改写为我们自己的函数地址!

       // 以后所有进程对这个系统函数的所有调用都将成为对我们自己编写的函数的调用

       pRealThunk->u1.Function = (PDWORD)paHookFunc.pProc;

       // 操作完毕!将这一块虚拟内存改回原来的保护状态

       DWORD dwOldProtect;

       _ASSERT(VirtualProtect(mbi_thunk.BaseAddress, mbi_thunk.RegionSize,

       mbi_thunk.Protect, &dwOldProtect));

       SetLastError(ERROR_SUCCESS);

       return TRUE;

       }

       }

       // 访问IMAGE_THUNK_DATA数组中的下一个元素

       pOrigThunk++;

       pRealThunk++;

       }

       return TRUE;

       }

       // GetNamedImportDescriptor函数的实现

       PIMAGE_IMPORT_DESCRIPTOR GetNamedImportDescriptor(HMODULE hModule, LPCSTR szImportModule)

       {

       // 检测参数

       _ASSERT(szImportModule);

       _ASSERT(hModule);

       if ((szImportModule == NULL) || (hModule == NULL))

       {

       _ASSERT(FALSE);

       SetLastErrorEx(ERROR_INVALID_PARAMETER, SLE_ERROR);

       return NULL;

       }

       // 得到Dos文件头

       PIMAGE_DOS_HEADER pDOSHeader = (PIMAGE_DOS_HEADER) hModule;

       // 检测是否MZ文件头

       if (IsBadReadPtr(pDOSHeader, sizeof(IMAGE_DOS_HEADER)) ||

       (pDOSHeader->e_magic != IMAGE_DOS_SIGNATURE))

       {

       _ASSERT(FALSE);

       SetLastErrorEx(ERROR_INVALID_PARAMETER, SLE_ERROR);

       return NULL;

       }

       // 取得PE文件头

       PIMAGE_NT_HEADERS pNTHeader = MakePtr(PIMAGE_NT_HEADERS, pDOSHeader, pDOSHeader->e_lfanew);

       // 检测是否PE映像文件

       if (IsBadReadPtr(pNTHeader, sizeof(IMAGE_NT_HEADERS)) ||

       (pNTHeader->Signature != IMAGE_NT_SIGNATURE))

       {

       _ASSERT(FALSE);

       SetLastErrorEx(ERROR_INVALID_PARAMETER, SLE_ERROR);

       return NULL;

       }

       // 检查PE文件的引入段(即 .idata section)

       if (pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress == 0)

       return NULL;

       // 得到引入段(即 .idata section)的指针

       PIMAGE_IMPORT_DESCRIPTOR pImportDesc = MakePtr(PIMAGE_IMPORT_DESCRIPTOR, pDOSHeader,

       pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);

       // 穷举PIMAGE_IMPORT_DESCRIPTOR数组寻找我们需要截获的函数所在的模块

       while (pImportDesc->Name)

       {

       PSTR szCurrMod = MakePtr(PSTR, pDOSHeader, pImportDesc->Name);

       if (stricmp(szCurrMod, szImportModule) == 0)

       break; // 找到!中断循环

       // 下一个元素

       pImportDesc++;

       }

       // 如果没有找到,说明我们寻找的模块没有被当前的进程所引入!

       if (pImportDesc->Name == NULL)

       return NULL;

       // 返回函数所找到的模块描述符(import descriptor)

       return pImportDesc;

       }

       // IsNT()函数的实现

       BOOL IsNT()

       {

       OSVERSIONINFO stOSVI;

       memset(&stOSVI, NULL, sizeof(OSVERSIONINFO));

       stOSVI.dwOSVersionInfoSize = sizeof(OSVERSIONINFO);

       BOOL bRet = GetVersionEx(&stOSVI);

       _ASSERT(TRUE == bRet);

       if (FALSE == bRet) return FALSE;

       return (VER_PLATFORM_WIN_NT == stOSVI.dwPlatformId);

       }

       /////////////////////////////////////////////// End //////////////////////////////////////////////////////////////////////

       ä¸çŸ¥é“在之前,有多少朋友尝试过去实现“鼠标屏幕取词”这项充满了挑战的技术,也只有尝试过的朋友才能体会到其间的不易,尤其在探索API函数的截获时,手头的几篇资料没有一篇是涉及到关键代码的,重要的地方都是一笔代过,MSDN更是显得苍白而无力,也不知道除了IMAGE_IMPORT_DESCRIPTOR和IMAGE_THUNK_DATA,微软还隐藏了多少秘密,好在硬着头皮还是把它给攻克了,希望这篇文章对大家能有所帮助。