一、思考与启发
1.对于Windows的Standard Controls,基本上大家应该是都会的:
GetWindowText,WM_GETTEXT可以获取EDIT的文本内容;
LB_GETTEXT可以获取LISTBOX列表项的文本内容;
CB_GETLBTEXT可以获取COMBOBOX下拉列表项的文本内容;
这里我就不多说了。
对于Windows的Common Controls,如LISTVIEW、TREEVIEW等,在本进程自身中获取的话可以直接用LVM_GETITEMTEXT,TVM_GETITEM消息,跨进程的话,还需要另外用到一些API函数,有兴趣的可以看看这个贴子:
http://www.delphibbs.com/delphibbs/dispq.asp?lid=3224504
DBGrid是Delphi的自写控件,不是Windows控件,没有什么消息可以利用来获取它所显示的数据记录内容。
2.运用鼠标屏幕取词的技术,可以获取到鼠标位置的显示内容,这个是采用的ApiHook技
术,截获屏幕输出函数,从而得到输出内容,其实现原理这里我不想多说,delphibbs
论坛上相关的贴子很多,大家搜索一下就能找到。不过这种方法的实用性不很强,因为
它需要在鼠标位置使输出重画,才能截获到输出内容的;而且如果DBGrid显示不完整有
滚动条的情况下,没有显示的字段、记录的内容就不能截获到。
3.在本进程自身中,是可以获得DBGrid显示的记录内容的,如
DBGrid1.Columns[1].Field.DisplayText,不过前提是要得到对象实例DBGrid1,假如
我们只知道DBGrid1的句柄。这个当然也没有什么问题,FindControl函数可以完成这个
功能。
4.上面说到,只要能够得到DBGrid的对象实例,我们就能够获得它的内容啦。前面讲的是
在本进程可以获取到DBGrid内容,而我们现在要讲的是跨进程获取其他程序的DBGrid 内容,那么是不是真的能够得到其他进程的DBGrid对象实例呢?如果能够的话,又该怎么
实现呢?另外,还有一个问题,我们获取到了这个DBGrid的对象实例,但是这个地址是
其他进程地址空间中的一个虚拟地址,我们能够DBGrid1.Columns[1].Field.DisplayText
这样在自己的进程中访问获取其内容吗?
二、必备知识基础
1.进程地址空间
Win32系统,所有32位应用程序都有4GB的进程地址空间(32位地址最多可以映射4GB的内存)。应用程序可以访问2GB的进程地址空间,称为用户模式虚拟地址空间。应用程序拥有的所有线程都共享同一个用户模式虚拟地址空间。其余2GB为操作系统保留(也称为内
核模式地址空间)。
而从Win2000 Server开始的所有操作系统版本,还有一个boot.ini
开关,可以为应用程序提供访问3GB的进程地址空间的权限,从而将内核模式地址空间
压缩为1GB。一般地,一个用户进程不可以直接访问另外一个用户进程的地址空间。进
程地址空间的描述请参考 http://msdn2.microsoft.com/zh-cn/library/ms189334.aspx
和《Windows核心编程》的第13章 Windows的内存结构。
顺便说一句,《Windows核心编程》是一本相当经典的好书,呵呵,建议大家都应该购
买收藏一本,认认真真地把它看几遍。题外话,呵呵
2.DLL基础
1)先说说为什么要使用DLL?
a.它可以动态装载,这样不必要在应用程序初始化就装载所有的代码,可以根据需要、操
作再装载DLL,这样启动速度比较快,也更节省内存;
b.便于项目管理,不同的开发人员、开发小组在不同的模块上工作;
c.有助于解决操作系统平台的差异,比如98/2000/XP等枚举进程可以用
CreateToolhelp32Snapshot,Process32First,Process32Next,而NT上就不能用,而要
用psapi.dll中的EnumProcesses,EnumProcessModules,GetModuleFileNameEx等函数;
d.可以用多种编程语言编写,开发人员可以选用自己最擅长的语言;
e.有助于资源的共享和应用程序的本地化,DLL可以包含对象框模板、图标、字符串、位
图等资源;
打错了,是对话框模板
f.可以实现一些特殊的目的,如系统范围的全局钩子,就要求写在DLL中才行。
2)EXE程序的全局变量不能被同一个EXE程序的多个运行实例所共享,DLL中的全局变量的处理方法也是一样的。也就是说,当一个进程将一个DLL映射到它的地址空间中去的时
候,系统会同时创建全局变量的实例。
也就是说不同进程间用的同一个DLL,全局变量的值可能是不一样的,不能共享
3)DLL和EXE之间的数据共享,这有很多技术,全局原子、内存映射、WM_COPYDATA消息等,
这个不是本文的重点,这里我们就不一一赘述。
4)Delphi写DLL要注意的问题:
这一点我们需要注意
a.参数和返回值为string、动态数组类型时,DLL和EXE都要把ShareMem作为.dpr工程的第一个单元引用。当然最好是不要使用string、动态数组类型,可以改用PChar、数组指
针类型,如果是混合语言编程使用的话,就一定不能用string、动态数组类型。这样做
的原因是DLL和EXE的内存管理器(MemoryManager)不是一个,而string、动态数组类型是通过引用计数由Delphi自动进行内存管理的,它何时分配何时释放,我们不能显式的
知道的,DLL分配而EXE释放的话,这样就出问题了。用ShareMem就是为了让它们统一使用一个内存管理器进行内存分配释放。
关于DLL和EXE内存管理器不同这一点,非常重要!!!
b.DLL和EXE的VCL类体系不是一个,它们各自有一套,因此,从EXE传递过去的对象,要在DLL中用is判断类型和as作类型转换,那都不能得到期望的结果。
如果是bpl,就不会有这个问题
c.DLL中应用ADO、窗体(模态、非模态、MDI子窗体)、线程等的一些相关问题与今天的主题关系不大,就不作多讲了。参考
http://www.delphibbs.com/delphibbs/dispq.asp?LID=2977902
http://www.delphibbs.com/keylife/iblog_show.asp?xid=2438
http://www.delphibbs.com/keylife/iblog_show.asp?xid=11558 等
关于DLL的详细论述请参考《Windows核心编程》第19、20章。
3.钩子(Hook)
Windows系统是建立在事件驱动的机制上的,说穿了就是整个系统都是通过消息的传递
来实现的。而钩子是Windows系统中非常重要的系统接口,用它可以截获并处理送给其
他应用程序的消息,来完成普通应用程序难以实现的功能。钩子可以监视系统或进程中
的各种事件消息,截获发往目标窗口的消息并进行处理。这样,我们就可以在系统中安
装自定义的钩子,监视系统中特定事件的发生,完成特定的功能,比如截获键盘、鼠标
的输入,屏幕取词,日志监视等等。
按事件分类,有如下的几种常用类型的钩子:
1)键盘钩子可以监视各种键盘消息。
2)鼠标钩子可以监视各种鼠标消息。
3)外壳钩子可以监视各种Shell事件消息。
4)日志钩子可以记录从系统消息队列中取出的各种事件消息。
5)窗口过程钩子监视所有从系统消息队列发往目标窗口的消息。
安装钩子:SetWindowsHookEx
卸载钩子:UnhookWindowsHookEx
钩子回调函数形式:
function GetMsgProc(Code: UINT; lParam: LPARAM; wParam: WPARAM): LRESULT; stdcall;
系统全局钩子必须在DLL中,因为它影响系统的所有应用程序,需要在消息发生时被系
统映射到其他进程的地址空间,从而调用DLL中的钩子回调函数。钩子所在的DLL被映射时,是整体映射被加载到被挂钩的进程的地址空间中,而不仅仅是钩子回调函数,这
样,被挂钩的进程就可以访问DLL中的变量和调用其他函数的。利用这个特点,在应用
中就可以做到很多特定的功能,比如屏幕取词、木马、三级跳隐藏进程等。
注意:安装了某类消息的系统全局钩子之后,在该类消息发生时钩子DLL会被系统映射到其他进程的地址空间,从而调用DLL中的钩子回调函数。
还有一点要注意:当SetWindowsHookEx调用成功后,系统会自动映射这个DLL到被挂钩
的线程,但并不是立即映射。因为所有的Windows钩子都是基于消息的,直到一个适当
的事件发生后这个DLL才被映射。同理,UnhookWindowsHookEx调用之后,也是在某个适当的事件发生之后DLL才真正地从被挂钩线程卸载。
function GetMsgProc(Code: UINT; lParam: LPARAM; wParam: WPARAM): LRESULT; stdcall;
能介绍这个回调函数什么用吗
必备的知识基础就讲这么多,有什么问题待会儿我们大家再探讨
Code:钩子代码,通常为HA_ACTION时是用户要处理的
lParam, wParam:跟具体安装的钩子类型有关的封装了缴获到的消息结构的参数
这个消息结构是怎样的?
比如GetMessage钩子,lParam是Removal flag移除标志,wParam是个TMsg结构的指针
typedef struct tagMSG { // msg
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG;
SDK中这么定义TMsg结构的
等下会有例子源代码给大家看的,不要急
继续...
三、实现
下面我们来讲如何解决一、4.中提到的问题。
1.在自己的进程中访问其他进程的对象实例
有了上面介绍的必备知识基础,那么现在这个问题对我们来说就不是很困难了,利用钩
子由系统将DLL注入目标进程,这时DLL就在目标进程的地址空间中了,这样,DLL中的访问目标进程的对象实例的代码就可以工作了。
2.得到其他进程的DBGrid对象实例
DLL注入目标进程之后,实际上DLL和目标进程就在一个进程中了,那么按理说我们用
FindControl函数应该就可以由DBGrid句柄得到DBGrid对象实例的了,但实际并非如此!
实际写代码测试一下我们可以发现它返回的是nil。
我们来看看FindControl的源代码(Controls.pas中):
{ Find a TWinControl given a window handle }
{ The global atom table is trashed when the user logs off. The extra test
below protects UI interactive services after the user logs off.
Added additional tests to enure that Handle is at least within the same
process since otherwise a bogus result can occur due to problems with
GlobalFindAtom in Windows. }
function FindControl(Handle: HWnd): TWinControl;
var
OwningProcess: DWORD;
begin
Result := nil;
if (Handle <> 0) and (GetWindowThreadProcessID(Handle, OwningProcess) <> 0) and
(OwningProcess = GetCurrentProcessId) then // 判断调用进程ID是否为Handle所在进程
begin
if GlobalFindAtom(PChar(ControlAtomString)) = ControlAtom then
Result := Pointer(GetProp(Handle, MakeIntAtom(ControlAtom)))
else
Result := ObjectFromHWnd(Handle);
end;
end;
安徽-小李(297099102) 14:23:55
function ObjectFromHWnd(Handle: HWnd): TWinControl;
var
OwningProcess: DWORD;
begin
if (GetWindowThreadProcessID(Handle, OwningProcess) <> 0) and
(OwningProcess = GetCurrentProcessID) then
Result := Pointer(SendMessage(Handle, RM_GetObjectInstance, 0, 0))
else
Result := nil;
end;
再看看其中使用到的ControlAtomString, ControlAtom, RM_GetObjectInstance的值是怎
样的(InitControls中):
不是这个问题,我们的DLL已经注入目标进程了
这是钩子帮我们完成的工作
再看看其中使用到的ControlAtomString, ControlAtom, RM_GetObjectInstance的值是怎
样的(InitControls中):
procedure InitControls;
var
UserHandle: HMODULE;
begin
WindowAtomString := Format('Delphi%.8X',[GetCurrentProcessID]);
WindowAtom := GlobalAddAtom(PChar(WindowAtomString));
ControlAtomString := Format('ControlOfs%.8X%.8X', [HInstance, GetCurrentThreadID]);
ControlAtom := GlobalAddAtom(PChar(ControlAtomString));
RM_GetObjectInstance := RegisterWindowMessage(PChar(ControlAtomString));
...
end;
不知道大家发现了没有
看到这里,我们可以发现问题之所在了。ControlAtomString是根据模块句柄(模块加载基
地址)和线程ID动态生成的,目标进程的模块基地址就是EXE基地址,一般是0x00400000,
但DLL的模块加载基地址就不是这个了,默认是0x10000000,而实际上可能因为这个地址
已经被占用(有其他DLL被加载到这个地址)而进行重定位,所以初始化时添加的
ControlAtom和目标进程的ControlAtom的值就不一样,RM_GetObjectInstance也同样是不
一样的,那FindControl当然就不能找到DBGrid对象实例啦。
OK,清楚了这一点,解决起来就简单了,我们自己写个FindControl函数,以目标进程基
地址来动态生成ControlAtomString,添加ControlAtom就可以啦。
在DLL中取EXE的基地址,用GetModuleHandle(nil)即可。
这个在DLL中做
var
ControlAtom: TAtom;
ControlAtomString: string;
RM_GetObjectInstance: DWORD; // registered window message
function FindControl(Handle: HWnd): TWinControl;
var
OwningProcess: DWORD;
begin
Result := nil;
if (Handle <> 0) and (GetWindowThreadProcessID(Handle, OwningProcess) <> 0) and
(OwningProcess = GetCurrentProcessId) then
begin
if GlobalFindAtom(PChar(ControlAtomString)) = ControlAtom then
Result := Pointer(GetProp(Handle, MakeIntAtom(ControlAtom)))
else
Result := Pointer(SendMessage(Handle, RM_GetObjectInstance, 0, 0));
end;
end;
ControlAtomString := Format('ControlOfs%.8X%.8X', [GetModuleHandle(nil), GetCurrentThreadID]);
ControlAtom := GlobalAddAtom(PChar(ControlAtomString));
RM_GetObjectInstance := RegisterWindowMessage(PChar(ControlAtomString));
finalization
GlobalDeleteAtom(ControlAtom);
ControlAtomString := '';
end.
嗯,上面的FindControl我是把Controls.pas中FindControl和ObjectFromHWND合成一个函数了
这样就可以根据句柄获取到对象实例了!
根据句柄得到对象实例,还有另外一种方法,采用MakeObjectInstance的实现机制,
和InitWndProc的实现代码,根据句柄得到ObjectInstance指针,从而得到实例对象,
本文就不说了,有兴趣的网友请自行研究MakeObjectInstance、InitWndProc的代码。
上面两个主要的问题解决了,大体上我们就解决了跨进程获取其他程序的DBGrid内容
的问题了!
3.取DBGrid的内容
句柄好找,FindWindow, FindWindowEx,关键是在于由句柄=>对象实例
是的,是“绝对句柄”
// 获取目标进程中DBGrid的数据集的记录内容,保存到文件中
procedure ProcessDataSet(hCtrl: HWND);
var
F: TextFile;
FileName: string;
Grid: TDBGrid;
DataSet: TDataSet;
I: Integer;
begin
Grid := TDBGrid(FindControl(hCtrl)); // 根据句柄取得对象实例
if (Grid <> nil) and (Grid.DataSource <> nil) and (Grid.DataSource.DataSet <> nil) then
begin
FileName := ExtractFilePath(ParamStr(0)) + 'DataSet.txt'; // 目标程序运行目录下
AssignFile(F, FileName);
if FileExists(FileName) then Append(F) else Rewrite(F);
try
DataSet := Grid.DataSource.DataSet;
DataSet.First;
while not DataSet.Eof do
begin
for I := 0 to DataSet.FieldCount - 1 do
Write(F, DataSet.Fields[I].AsString, ', ');
Writeln(F);
DataSet.Next;
end;
Writeln(F);
finally
CloseFile(F);
end;
end;
end;
大家觉得这个代码有没有问题???
这个代码,我们在实际测试时可以发现还是存在问题的,运行会出错,问题就在于
Write(F, DataSet.Fields[I].DisplayText, ', ');这一句,如果将这一句改成
Write(F, DataSet.Fields[I].Value, ', ');就不会有问题。那么这又是什么原因呢?
null?
for I := 0 to DataSet.FieldCount - 1 do
Write(F, DataSet.Fields[I].DisplayText, ', ');//这句写错了
是要取界面显示的内容
呵呵,不是这个原因
value<>displaytext
TField.AsString的属性声明:
property AsString: string read GetAsString write SetAsString;
property DisplayText: string read GetDisplayText;
function GetDisplayText: string;
procedure GetText(var Text: string; DisplayText: Boolean); virtual;
DisplayText属性声明
function TField.GetDisplayText: string;
begin
Result := '';
if Assigned(FOnGetText) then
FOnGetText(Self, Result, True) else
GetText(Result, True);
end;
procedure TField.GetText(var Text: string; DisplayText: Boolean);
begin
Text := GetAsString;
end;
TField类型的GetText的实现是直接调用GetAsString,我们看看特定类型的字段的
GetText函数的实现代码,如TIntegerField.GetText:
procedure TIntegerField.GetText(var Text: string; DisplayText: Boolean);
var
L: Longint;
FmtStr: string;
begin
if GetValue(L) then
begin
if DisplayText or (FEditFormat = '') then
FmtStr := FDisplayFormat else
FmtStr := FEditFormat;
if FmtStr = '' then Str(L, Text) else Text := FormatFloat(FmtStr, L);
end else
Text := '';
end;
这个函数也并没有什么特别的代码,那到底是怎么导致这个错误的呢?这个就需要我们
再仔细地想想了,想当初这个问题困扰了好几天而不得解!还问过刘麻子大虾的,呵呵
我们再来看看,用Field.GetText字符串类型会出错,而用Field.Value,Variant类型就
没有问题,那么应该是跟字符串类型有关了,再联想到这是在DLL中,传递字符串参数和
返回值时都要引用ShareMem作为工程的第一个单元,嗯,是不是跟字符串的自动内存管
理有关系?EXE和DLL是各自有自己的一套VCL类体系的,我们FindControl获取到的DBGrid实际上是EXE中创建的对象,而Field.GetText是个虚拟方法,它在VMT中占据一项,实际的函数地址存放在VMT中,尽管我们是在DLL中调用DisplayText属性和GetText函数的,但由于DBGrid对象实例是EXE中的,它的VMT中的实际的GetText函数是EXE中的,也就是说它是通过EXE的内存管理器,给返回字符串分配内存的,而我们的可爱的
Write(F, DataSet.Fields[I].DisplayText, ', ')在使用完这个临时字符串之后,会将其释放,这是在DLL端做的,这就是问题之所在啦!
那么我们也把ShareMem作为工程的第一个引用单元,编译生成DLL,测试,问题是否搞定了呢?没有!嘿嘿,尽管DLL我们uses ShareMem了,但目标进程并没有,而一般说来我们要用这种方法去跨进程获取其他程序的DBGrid内容,原因就在于我们没有目标程序的
源代码,也不能打开目标数据库或目标数据库的内容被加密了,那么我们当然是没法改
目标进程让它也uses ShareMem的,那就只有另谋出路的啦。解决问题的方法也是很简单,
字符串通过引用计数来进行内存管理的,当引用计数=1而这个字符串不再被使用时,引
用计数-1=0,这时就会释放字符串的内存的。嘿嘿,你不是在引用计数=0时才释放字符串
内存么,我就偏不让你释放,我用代码手工修改字符串的引用计数,让它比1大,那么
Delphi自然就会认为还有其他地方要使用这个字符串,就不会多此一举,滥做好人,帮
我们释放字符串的内容了。
但是这是解决了出错的问题,却又衍生一个新的问题:内存泄漏,不过这个倒问题不大,
反正进程结束的时候,系统总是会释放这个进程所占的所有内存资源的,没有好的解决
办法,那目前我们也就只能这么着了,哈哈!
// 获取目标进程中DBGrid的数据集的记录内容,保存到文件中
procedure ProcessDataSet(hCtrl: HWND);
var
F: TextFile;
FileName: string;
Grid: TDBGrid;
DataSet: TDataSet;
I: Integer;
S: string;
begin
Grid := TDBGrid(FindControl(hCtrl)); // 根据句柄取得对象实例
if (Grid <> nil) and (Grid.DataSource <> nil) and (Grid.DataSource.DataSet <> nil) then
begin
FileName := ExtractFilePath(ParamStr(0)) + 'DataSet.txt'; // 目标程序运行目录下
AssignFile(F, FileName);
if FileExists(FileName) then Append(F) else Rewrite(F);
try
DataSet := Grid.DataSource.DataSet;
DataSet.First;
while not DataSet.Eof do
begin
for I := 0 to DataSet.FieldCount - 1 do
begin
S := DataSet.Fields[I].AsString;
if S <> '' then
Inc(PInteger(PChar(S)-8)^); // 增加字符串引用计数,避免Delphi自
// 动管理字符串内容的释放
Write(F, S, ', ');
end;
Writeln(F);
DataSet.Next;
end;
Writeln(F);
finally
CloseFile(F);
end;
end;
end;
完整的源代码:
http://lichengbin.iii-grp.com/Source/DBGrid.rar