GDIWatch 是Virgo Software 开发的一个for Visual Studio的插件,支持2005/2008/2010,它的功能主要是在一个类似watch的窗口上显示被调试程序的GDI对象的当前状态,比如HBRUSH的颜色,大小,图片等等,并且它还能在调试过程中高亮显示有变化的项目,方便程序员跟踪调试画图函数。
下载地址: http://www.gdiwatch.com/GDIWatch.msi
(小声说一下,crack在文中提供了)
这是官方的截图:
顺便再贴一个 GDIWatch 在 VS2010上使用的效果图:
感觉还不赖,使用起来也挺方便的,就是拽个变量到它上面就可以了。
GDIWatch 不是免费软件,作者给了15天的试用期,如果需要继续使用就要到官网 www.gdiwatch.com 联系作者获取注册码。
P.S. 话说前天我在公司正好想上他的网站看看价钱如何,结果发现他的主页不知出现神马问题没法显示了,囧啊。
P.P.S. 印象中貌似是要100多美刀的样子。
P.P.P.S. 在15天后我偶尔还想继续使用,但是中国国情告诉我,花100多美刀买个插件是稍微有点贵了的说,而且目前在公司还没用上VS2010,所以便可耻地尝试crack,没想到很好crack的说,稍微改动一下居然就搞定了,主要是该作者的防范意识不够啊,犯了很多防破解的大忌,给了人家很多线索,有需要的童鞋请猛击此处下载,适用于1.5.1.254版本,替换原版之前请自行备份以防万一!
好了, 言归正传,我当初之所以找到这个软件是因为前阵子一直在写画图的代码,本来是想说在网上找个VC6的插件的(没办法,公司还是在用),先是在 CodeProject 上找到一篇某位国人很久以前发表的文章,可是他居然不是开源的(这不坑爹吗),而且远没有 GDIWatch 那么方便好用(不给力啊),最奇怪的是CodeProject 居然让他把文章给发表上去了(我勒个去),真是无奈。
不过该作者倒是简单提到了一下他实现的方法:
The steps to do watch Image is :
(1)get the selection text by ISelectionText interface
(2)get the value of selection text by IDebugger interface
(3)Read the memeory or bitmap data from the debugged process memory space
(4)show it
最后只找到这个支持VS2005+的 GDIWatch,于是开始寻思这玩意怎么实现,我想如果不是很复杂的话说不定可以在闲暇时间做一个for VC6的版本出来的说。
我首先思考的是要实现这样的插件最重要是要解决哪些问题:
1、最最重要的是,必须能够跨进程“访问”被调试进程的GDI objects,这是当然的;
2、必须能跟VS协调运作,响应调试动作并及时更新GUI,要像VS自己的watch那么好用;
3、必须有界面能显示GDI objects,这......必须的;
当然要完善这个插件的话,还需要尽量满足下列条件:
1、避免使用undocumented trick,保证兼容性;
2、如GDIWatch那样支持拖放变量名到GUI上;
3、高亮有变化的内容,方便跟踪;
在定下上面这些条件后,下一步就是逐个解决问题了。
首先,要获取GDI对象的属性,基本是要走这条路:
DWORD GetObjectType(__in HGDIOBJ h);
HGDIOBJ GetCurrentObject(__in HDC hdc,__in UINT uObjectType);
int GetObject(__in HGDIOBJ hgdiobj, __in int cbBuffer, __out LPVOID lpvObject);
然而,GDI对象是基于进程的,GDIWatch作为一个插件,也就是VS的一个DLL,它如果要拿被调试进程的GDI对象句柄来直接用必然是不行的,
GDI objects 也不在 DuiplicateHandle 这个API支持的 object handle 的范畴之内。
当然了,GDI对象毕竟也是数据,在用户模式不能做到的,在内核模式肯定有奇淫巧计可以做到,比如说访问GDI对象表:
http://topic.csdn.net/t/20031009/14/2337150.html
http://hi.baidu.com/qzccan/blog/item/154b542375171440ac34de08.html
说起来有一款软件很可能就是这么实现的,叫做 GDIView,它可以查看指定进程当前打开的所有GDI objects并显示其属性:
不过这些都属于tricks,不是标准的做法,而且我也不熟悉具体实现方法,所以只能放弃。
其实,毕竟目标进程是在被调试的状态下,这还是给了插件解决这个问题的环境,或者说至少有一些条件可以被利用。
调试器是可以有办法读写被调试进程的内存的,可以在被调试进程的运行空间插入一段代码让它执行,只要上面提到的 GetObjectType 等API是在被调试进程的领域执行的,那么句柄就是有效的,自然能得到所需的结果。
要读写内存,必然是这条路:
接下来的事情大概是这样:
设计一段代码,主要做的事情是接受指定的GDI句柄,然后通过 GetObjectType/GetCurrentObject/GetObject 等API去获取 GDI object 的相关信息,然后将结果保存在某个buffer。
假设这段代码是一个C函数,那么代码大致是:
typedef struct tagBrushInfo { HBRUSH hBrush; LOGBRUSH logBrush; }BrushInfo, *PBrushInfo; typedef struct tagPenInfo { HPEN hPen; LOGPEN logPen; }PenInfo, *PPenInfo; typedef struct tagDCInfo { HDC hDC; BrushInfo brushInfo; PenInfo penInfo; }DCInfo, *PDCInfo; LPVOID GetGDIObjectInfo(HGDIOBJ hGDIObjects) { LPVOID pInfo = NULL; DWORD dwObjType = GetObjectType(hGDIObjects); switch ( dwObjType ) { case OBJ_DC: { PDCInfo pDCInfo = new DCInfo; pDCInfo->hDC = (HDC)hGDIObjects; // retrieve the brush info pDCInfo->brushInfo.hBrush = (HBRUSH)GetCurrentObject(pDCInfo->hDC, OBJ_BRUSH); if ( pDCInfo->brushInfo.hBrush ) { GetObject(pDCInfo->brushInfo.hBrush, sizeof(LOGBRUSH), &pDCInfo->brushInfo.logBrush); } // retrieve the pen info pDCInfo->penInfo.hPen = (HPEN)GetCurrentObject(pDCInfo->hDC, OBJ_PEN); if ( pDCInfo->penInfo.hPen ) { GetObject(pDCInfo->penInfo.hPen, sizeof(LOGPEN), &pDCInfo->penInfo.logPen); } pInfo = pDCInfo; } break; case OBJ_BRUSH: if ( hGDIObjects ) { PBrushInfo pBrushInfo = new BrushInfo; GetObject(hGDIObjects, sizeof(LOGBRUSH), &pBrushInfo->logBrush); pInfo = pBrushInfo; } break; } return pInfo; }
接下来就是要把 GetGDIObjectInfo 这个函数的代码通过某种方式拷贝到被调试进程中,方法很多,其中一种方法是通过插件内实现一份该函数,然后设法计算出函数体的二进制代码长度,从而将函数代码拷贝,一个具体的例子是 CodeProject 上非常著名的文章 Three Ways to Inject Your Code into Another Process 中:
static DWORD WINAPI ThreadFunc (INJDATA *pData) { pData->fnSendMessage( pData->hwnd, WM_GETTEXT, // Get password sizeof(pData->psText), (LPARAM)pData->psText ); return 0; } // This function marks the memory address after ThreadFunc. // int cbCodeSize = (PBYTE) AfterThreadFunc - (PBYTE) ThreadFunc. static void AfterThreadFunc (void) { }
可以看出是利用编译器生成代码的习惯,通过一个额外的空函数 AfterThreadFunc 得到 ThreadFunc 的可能大小(即 nCodeSize = AfterThreadFunc - ThreadFunc)。
此外也可以尝试基于X86汇编指令自行组装 GetGDIObjectInfo 的二进制代码,不过不是很容易阅读和维护代码。
不过这里还有一个需要注意的地方,CodeProject 的那篇文章提到了,就是同一个API的地址在不同进程中可能会被映射到不同的地址上,所以要拷贝的代码中肯定是不能直接那样调用的,LoadLibrary 和 GetProcAddress 就是很好的一个能得到正确的地址的方法。前面的 GetGDIObjectInfo 函数还使用了 new operator,也要对应修改为API函数如 VirtualAlloc 等。
在终于把这个GetGDIObjectInfo函数的代码拷贝到目标进程后,下一步最为重要,就是要设法让被调试进程执行该函数。
既然插件已经是调试器的小弟,那么当然可以利用debug API来实现,而不必用到 CreateRemoteThread 这样感觉稍微猥琐的方法。
VS 应该是通过 WaitForDebugEvent 等一系列API来进行调试的,所以可以拦截它,比如在先调用 SuspendThread 把当前进程中所有非插件模块所在线程给暂停掉,然后它的函数头部加个 jmp,让它先跳转到自己的一个函数,在这个函数里,要先进行一些逻辑判断,在适合的时机利用 GetThreadContext/SetThreadContext 来操作被调试进程,比如修改eip,然后 ContinueDebugEvent 让被调试进程执行 GetGDIObjectInfo 函数,在取得GDI对象的信息buffer后,拷贝到插件自己的内存空间上,调用 ResumeThread 恢复所有之前被暂停的线程,最后不要忘了还要跳转回 WaitForDebugEvent 的函数里。
关于运用debug API的,最近的 Writing Windows Debugger 系列文章貌似不错,我有时间要看看。
做完上面这些事情后,可以给插件的窗口post 一个消息,让它读取 GetGDIObjectInfo 返回的结果并更新GUI。
至于BITMAP这个比较特殊的对象,可以用 CreateDIBSection 这个API。
可是事情到此还没完,因为还要写VC6插件的代码,还好这个问题已经有一篇非常棒的文章可以参考:Undocumented Visual C++。
最后就是那个类似watch窗口的属性列表控件,我没找到现成的,不过倒是有一个还不错的封装类 CPropTree,只是还需要在它的基础上加不少代码进行增强。
P.S. 终于把这几天的想法记录下来,感觉真是说起来容易做起来难啊,这个小小的插件要真正实现起来还是相当麻烦的,有大量的工作要做,难怪人家要卖 100 多美刀的说......