当前位置:主页>销售管理软件> 列表

300分求思路,截取其他程序输入框中的字符串,钩子程序高手

仓库管理软件版1楼: 情况是这样的:在一个公司的CallCenter里头,已经有一个windows(可能是98,2k,xp)下的软件可以录入客户的基本信息,比如姓名,电话。现在我要开发另外一个后台监控程序,监控程序必须能够截取CallCenter程序输入框中的姓名,或者电话什么的。此外,这个监控程序必须在多家不同公司的不同平台不同的CallCenter程序中,当用户输入姓名等信息时,同时采集相关信息:
查了很多关于屏幕取词的帖子,有几个疑问想请教:
1,是否可能通过截取“姓名”输入框的句柄,从而获得需要的字符串信息;但是是否被监控程序的“姓名”输入框的句柄参数在每次启动后都是相同的?而且如果用户启动了两个被监控程序,又该怎么办?
2,如果结合用户手工按键提示,比如,用户进入“姓名”输入框时,按F1,输入完毕,按F12提示监控程序采集姓名信息;依此类推,是否可以更好的解决问题?
3,不同公司的CallCenter程序可能非常不同,有可能是VC开发的CS程序,也可能是IE界面的,更可能是用NetTerm连接Unix主机,请帮忙指出可能造成的各种麻烦。

小弟在windows编程方面是个新手,请各位大侠不吝指教,有相关的资料也请mail给我,小弟一定把所有分数拿来感谢大家!
willxue@163.com
QQ: 93354

2楼: 1.先获取程序窗体的句柄
2.在此句柄上读取所有的控件句柄.
3.得到输入框句柄后.用XXX函数得取输入框的字符 如进销存管理软件

3楼: 难,尤其是IE和NetTerm,看来得用键盘钩子

4楼: .先枚举所有窗口 获取句柄

得到句柄后 创建键盘钩子

安装。。。试试

5楼: 转载

鼠标屏幕取词技术的原理和实现

白瑜


“鼠标屏幕取词”技术是在电子字典中得到广泛地应用的 ,如四通利方和金山词霸等软件 ,这个技术看似简单 ,其实在 windows 系统中实现却是非常复杂的 ,总的来说有两种实现方式 :

第一种 : 采用截获对部分 gdi 的 api 调用来实现 ,如 textout,textouta 等。

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

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

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

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

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

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

先说 windows 虚拟内存的管理。 windows9x 给每一个进程分配了 4gb 的地址空间 ,对于 nt 来说 ,这个数字是 2gb,系统保留了 2gb 到 4gb 之间的地址空间禁止进程访问 ,而在 win9x 中 ,2gb 到 4gb 这部分虚拟地址空间实际上是由所有的 win32 进程所共享的 ,这部分地址空间加载了共享 win32 dll 、内存映射文件和 vxd 、内存管理器和文件系统码 ,win9x 中这部分对于每一个进程都是可见的 ,这也是 win9x 操作系统不够健壮的原因。 win9x 中为 16 位操作系统保留了 0 到 4mb 的地址空间 ,而在 4mb 到 2gb 之间也就是 win32 进程私有的地址空间 ,由于 每个进程的地址空间都是相对独立的 ,也就是说 ,如果程序想截获其它进程中的 api 调用 ,就必须打破进程边界墙 ,向其它的进程中注入截获 api 调用的代码 ,这项工作我们交给钩子函数 (setwindowshookex) 来完成 ,关于如何创建一个包含系统钩子的动态链接库 ,《电脑高手杂志》在第?期已经有过专题介绍了 ,这里就不赘述了。所有系统钩子的函数必须要在动态库里 ,这样的话 ,当进程隐式或显式调用一个动态库里的函数时 ,系统会把这个动态库映射到这个进程的虚拟地址空间里 ,这使得 dll 成为进程的一部分 ,以这个进程的身份执行 ,使用这个进程的堆栈 ,也就是说动态链接库中的代码被钩子函数注入了其它 gui 进程的地址空间 ( 非 gui 进程 ,钩子函数就无能为力了 ),



当包含钩子的 dll 注入其它进程后 ,就可以取得映射到这个进程虚拟内存里的各个模块 (exe 和 dll) 的基地址 ,如 :

hmodule hmodule=getmodulehandle( “ mypro.exe ” );

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

的相同的虚拟地址空间处。

系统将 exe 和 dll 原封不动映射到虚拟内存空间中 ,它们在内存中的结构与磁盘上的静态文件结构是一样的。即 pe (portable executable) 文件格式。我们得到了进程模块的基地址以后 ,就可以根据 pe 文件的格式穷举这个模块的 image_import_descriptor 数组 ,看看进程空间中是否引入了我们需要截获的函数所在的动态链接库 ,比如需要截获“ textouta ” ,就必须检查“ gdi32.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 应用程序来说 ,如果要取词 ,我们需要截获的是“ gdi32.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(),"gdi32.dll",&hd,porigfuns);

下面给出了 hookimportfunction 的源代码 ,相信详尽的注释一定不会让您觉得理解截获到底是怎么实现的

很难 ,ok,let ’ s go:


///////////////////////////////////////////// begin ///////////////////////////////////////////////////////////////

#include


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

#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 虚拟内存空间之上

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

if (!isnt() && ((dword)hmodule >= 0x80000000))

{

_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_win32_nt == stosvi.dwplatformid);

}

/////////////////////////////////////////////// end //////////////////////////////////////////////////////////////////////


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

6楼: 高人......

仓库管理软件版7楼: 1.可以监控姓名输入框的字符串,输入框句柄每次运行程序都可能不同的,用户启动多个被控程序可以用SHELL钩子获取每个应用程序的参数,在获得字符串时查一下应用程序的句柄参数就行了。
2.方法一查焦点是否是姓名输入框再捕获所需的窗口消息,方法二查焦点再捕获键盘消息
3.对应各类WINDOWS应用程序都没问题的,如果是浏览器中的网页就只能获取键盘输入但无法知道焦点在什么地方,NetTeam估计你只能去分析输入再判断在什么输入状态了
用到的SHELL钩子、键盘钩子、消息钩子DFW上资料很多的自己查一下,鼠标屏幕取词好象不适合用在这。

8楼: 难点就在如何获取网页中的输入信息

9楼: IE好像不好搞,很多SENDMESSAGE的代码都失效了

10楼: 用hook实时监控键盘,当发现当对窗体为你的那个程序时记下输入的内容,行不?

11楼: 大家说得都很不错,启发很大,还有其他看法么?