Cempi实战攻略(六)——如何截获到达的短消息
By 吴春雷
QQ:819543772
EMAIL:wuchunlei@163.com
1. MapiRule是什么?我从哪里能够得到它?
MapiRule是微软提供的用于演示短信拦截技术的DEMO程序,程序展示了使用COM技术为tmail.exe注册服务,实现客户端短信拦截的基本方法。您可以再SDK的安装目录中找到它,如果您使用的是PPC2003的SDK,MapiRule程序可以在下面的目录找到:
C:\Program Files\Windows CE Tools\wce420\POCKET PC 2003\Samples\Win32\mapiRule
如果您的开发平台是WM5.0则,您可以再下面目录中找到改程序:
C:\Program Files\Windows CE Tools\wce500\Windows Mobile 5.0 Pocket PC SDK\Samples\CPP\Win32\Mapirule
不知道出于什么原因,WM6.0的SDk中并没有提供MapiRule程序,因此,如果您的开发平台是WM6.0,那么就只能到网上下载该程序了。
2. 我想先部署到WM6.0平台上看看效果,我该怎么做?
a) 编译MapiRule
首先,肯定是要搞到MapiRule的源程序,源程序有两个不同的版本,分别再PPC2003 SDK和WM5.0 SDk中提供,两个版本的源程序是再不同的开发环境中编写,但内容是完全相同的,只是需要使用不同的编译器进行编译。PPC2003的版本要再EVC4.0下编译,而WM5.0的版本要再VS2003/2005/2008下编译。成功编译后可以得到mapirule.dll文件,将该文件拷贝到WM仿真器或设备上(一般放在Windows目录下),就可以进行后面的操作了。
如果您的目的仅仅是部署到设备上试一下,又不想或者没有安装开发环境无法编译,那么您可以在下面目录下找到编译好的Cab包(不一定会有这个cab包,PPC2003的版本没有该cab包),然后拷贝到设备上直接安装即可。
C:\Program Files\Windows CE Tools\wce500\Windows Mobile 5.0 Pocket PC SDK\Samples\CPP\Win32\Mapirule\Setupmapirule
b) 如何注册该Com组件
MapiRule的工作机制是基于COM技术的,但限于文章的主题,本文不会对COM技术进行详细讨论,推荐潘爱民的COM技术教材,有兴趣的朋友可以找来研究。众所周知,要想让系统知道COM组件的存在,需要首先再系统注册表中对COM组件进行注册,应用程序会通过注册表加载对应的COM组件。对于MapiRule,可以有三种方式进行注册,分别是手动修改注册表,修改安装包中的inf文件,以及调用源程序中提供的DllRegisterServer的函数。下面分别进行介绍
i. 第一种方式:手动向注册表中输入注册信息
WM设备的注册表再OS中是看不到,因此要想手动修改系统注册表,需要使用专用的注册表浏览工具,VS2005中提供了浏览手机注册表的工具,非常好用。依次选择“开始”——“Microsoft Visual Studio 2005”——“Visual Studio Remote Tools”,运行“远程注册表编辑器”,就可以打开该工具对注册表进行操作。如果您的编译环境中没有该工具,可以从网上下载相应的工具。
打开远程注册表编辑器,建立与设备的连接,再系统注册表找到如下目录:
[HKEY_CLASSES_ROOT\CLSID\]
再CLSID下添加一个Key目录(注意要有大括号),目录名为COM组件的CLSID,默认为:
{3AB4C10E-673C-494c-98A2-CC2E91A48115}
然后在该目录下建立一个名为InProcServer32的目录
最后再InProcServer32目录下添加一个字符串型的Key,名称为{3AB4C10E-673C-494c-98A2-CC2E91A48115}
值为mapirule.dll
如果mapirule.dll文件被放置再windows目录下,则key值只需要填写mapirule.dll即可,否则这里需要填写绝对路径。
然后再找到目录:
[HKEY_LOCAL_MACHINE\Software\Microsoft\Inbox\Svc\SMS\Rules]
再Rules下添加一个DWORD型key,名称为(COM组件的CLSID):
{3AB4C10E-673C-494c-98A2-CC2E91A48115}
值为1
然后刷新一下注册表编辑器,查看系统注册表中是否有如下目录和键值。
目录:
[HKEY_CLASSES_ROOT\CLSID\{3AB4C10E-673C-494c-98A2-CC2E91A48115}\InProcServer32]
键类型:字符串
键名称:{3AB4C10E-673C-494c-98A2-CC2E91A48115}
值:"mapirule.dll"
(如果mapirule.dll不再windows目录下,则填写据对路径)
目录:
[HKEY_LOCAL_MACHINE\Software\Microsoft\Inbox\Svc\SMS\Rules]
键名称:DWORD
键名称:{3AB4C10E-673C-494c-98A2-CC2E91A48115}"
键值:1
ii. 第二种方式:cab安装包中配置inf文件
WM5.0的MapiRule源程序提供了一个cab工程Setupmapirule,这个工程将帮助你建立一个cab包,用于自动安装和部署mapirule组件,再部署组件的同时,cab包会通过inf文件中的设置自动注册COM组件。如果不使用程序默认的CLSID,可以通过修改inf文件实现更改注册到注册表中的键值(源程序中对应CLSID也要修改,并且需要重新编译源程序)。
iii. 第三种方式:调用MapiRule中提供的DllRegisterServer函数
如果想在自己的程序中注册组件,可以通过掉调用序中提供DllRegisterServer函数来实现,该函数的定义如下:
STDAPI DllRegisterServer()
STDAPI是一个宏定义,进行宏替换以后,函数定义为:
extern “C” HRESULT __stdcall DllRegisterServer()
函数如果成功过调用则会返回S_OK,否则返回E_FAIL。成功调用该函数后,可以通过注册表编辑器查看上面提到的注册表目录中是否成功添加两个键值。
这里特别提一句,在程序中调用该函数前,不要忘了先从MapiRule.dll中导出该函数,导出方法是再MapiRule工程中的MapiRule.def文件中添加如下语句:
DllRegisterServer PRIVATE
再我们的程序中要通过LoadLibrary和GetProcAddress函数获取该函数的EntryPoint指针。方法如下:
LPFDLLREGISTERSERVER lpfDllRegisterServer;
typedef HRESULT (*LPFDLLREGISTERSERVER)(void); //定义函数指针类型
//初始化操作
HINSTANCE hInst;
hInst=LoadLibrary(_T("\\windows\\mapirule.dll")); //获取mapirule.dll
if(NULL==hInst) //没有找到DLL
{
//异常处理
}
//导出函数lpfDllRegisterServer=(LPFDLLREGISTERSERVER)GetProcAddress(hInst,_T("DllRegisterServer"));
//调用函数
HRESULT hr=lpfDllRegisterServer();
if(FAILED(hr))
{
//异常处理
}
c) 卸载该组件
i. 手动删除注册表信息
利用远程注册表编辑器,删除下面的Key目录和键值
目录:
[HKEY_CLASSES_ROOT\CLSID\{3AB4C10E-673C-494c-98A2-CC2E91A48115}\InProcServer32]
键类型:字符串
键名称:{3AB4C10E-673C-494c-98A2-CC2E91A48115}
目录:
[HKEY_LOCAL_MACHINE\Software\Microsoft\Inbox\Svc\SMS\Rules]
键名称:DWORD
键名称:
{3AB4C10E-673C-494c-98A2-CC2E91A48115}"
重启设备后,在使用远程注册表编辑器,确认上面键值是否被删除。
ii. 调用MapiRule中提供的DllUnregisterServer函数
MapiRule程序同样提供了用于取消注册的函数DllUnregisterServer,函数的声明如下(宏替换后):
extern “C” HRESULT __stdcall DllUnregisterServer()
同样,返回S_OK表示运行成功,返回E_FAIL则失败。调用源程序如下:
typedef HRESULT (*LPFDLLUNREGISTERSERVER)(void); //定义函数指针类型
//初始化操作
HINSTANCE hInst;
LPFUNDLLREGISTERSERVER lpfUnDllRegisterServer;
hInst=LoadLibrary(_T("\\windows\\mapirule.dll")); //获取mapirule.dll
if(NULL==hInst) //没有找到DLL
{
//异常处理
}
//导出函数lpfDllUnRegisterServer=(LPFDLUNLREGISTERSERVER)GetProcAddress(hInst,_T("DllUnregisterServer"));
//调用函数
HRESULT hr=lpfDllUnRegisterServer ();
if(FAILED(hr))
{
//异常处理
}
d) 如何确定MapiRule.dll已经被tmail进程加载?
编译MapiRule程序,拷贝MapiRule.dll文件到Windows目录,并成功进行注册表注册后,重新启动WM设备,启动tmail.exe程序(直接运行设备的“短信”功能即可)。我们还需要查看tmail.exe是否已经成功加载了该MapiRule组件。依次选择“开始”——“Microsoft Visual Studio 2005”——“Visual Studio Remote Tools”,运行“远程进程查看器”。建立与设备的连接以后,可以再process列表中找到tmail.exe进程,选中该进程,在最下面的module列表中如果看到mapirule.dll项,则COM组件加载成功。这里要注意,如果没有找到该项,也不意味着COM组件一定没有被加载。如果module中没有该项,也可以通过一个简单操作来确定mapirule.dll是否被加载,保证tmail.exe进程正在运行,使用ActiveSync同步设备,再Windows目录下找到mapirule.dll文件,尝试删除该文件,如果可以被删除则加载不成功,否则加载成功。
e) 测试该组件是否能够正常工作
确保mapiRule.dll已经被tmail.exe进程成功加载后,tmail.exe将获得截获短信的能力,如果没有做修改的话,MapiRule.dll默认会对所有接受到的包含”zzz”的短信进行拦截,并弹出MessageBox提示截获短信的内容,被截获的的短信将不会到达收件箱。而其他短信将不会被拦截。
打开Cellular Emulator工具,向仿真器发送一条包含”zzz”的短信,或者利用WM模拟器的短信功能发送一条包含”zzz”的信息到14250010001。这时会弹出一个对话框,显示被截获的短信信息。
f) 为什么我已经注册成功了,它却还不工作?
我在工作过程中遇到过,虽然注册已经成功,且组件已经嵌入到了tmail.exe进程中,但是该组件却无法工作的情况。下面三种情况是导致该问题的最常见原因。
i. 查看GUID是否与别的组件冲突
前面提到每个COM组件都需要指派一个唯一的ID,也就是GUID,这个ID是由系统开发人员来指定的,因此不可避免的会出现冲突。利用远程注册表编辑器可以查看注册表中是否有冲突的GUID。启动编辑器,连接设备,查看“HKEY_CLASSES_ROOT\CLSID”目录下对应的GUID目录键值是否为mailrule.dll,如果不是那么可能存在其它COM组件使用了该GUID。解决方法是单击VS编译器的“工具”——“创建GUID”来生成一个新的GUID,并且将MapiRuile的源程序中所有GUID的位置设置成新的GUID,然后重新编译源程序。
ii. 证书
再真实的WM设备中运行应用往往程序需要证书来确保应用程序的安全性,如果没有提供该证书,可能会导致MapiRule无法编译或不能正常执行。Mcriosoft提供了一个开发专用的证书,这个证书可以在下面的位置上找到。
D:\Program Files\Windows CE Tools\wce500\Windows Mobile 5.0 Pocket PC SDK\Tools\SDKSamplePrivDeveloper.pfx
双击运行将证书导入系统后,为MapiRule指派该证书,重新编译,部署该组件。
iii. 查看注册表中是否有其它程序已经为tmail.exe进程注册过了实现相同接口的组件
这种情况下,GUID虽然没有被占用,但是程序中有两个实现短信截获的组件,我们的MapiRule虽然被正确注册和加载,但是当短信到达的时候,首先被另一个组件所截获,从而导致我们的程序无法正常运行。这时可以通过远程注册表编辑器,记录并删除以下目录中除我们的GUID以外的所有键值。
[HKEY_LOCAL_MACHINE\Software\Microsoft\Inbox\Svc\SMS\Rules]
然后再[HKEY_CLASSES_ROOT\CLSID]这里找到并删除刚才记录下的键值所对应的目录。最后,重新启动设备或仿真器。
3. 这么多代码,我不知道该从那看起?(别急,我们只需要了解其中几个关键的地方就好)
a) 什么是GUID?如何产生新的GUID?
关于GUID的详细解释,请查看COM技术的相关书籍,这里我们只需知道GUID是使tmail.exe找到并加载我们的组件的唯一标志。单击VS编译器的“工具”——“创建GUID”可以生成一个新的GUID,生成的GUID不能保证唯一,这一点要特别注意。生成GUID以后,别忘了替换源程序中的对应位置。
b) 拦截短信的原理是什么?
短信拦截实际上是利用COM技术对tmail.exe功能进行扩展。像所有com程序一样,系统提供了一个接口(协议)来让外部程序实现,外部程序通过实现该接口来为tmail.exe增加新的功能。CMapiClient是一组接口,当短信到达本地设备的时候,系统会调用一系列的函数获取并加载COM组件,然后调用ProcessMessage方法来处理到达的短消息,这个过程由系统实现,除了ProcessMessage方法以外不需要我们来写任何程序(其实获取获取ObjectId和QueryInterface等方法也需要程序员去写,但是基本都是固定的形式,因此直接复制就OK了,MapiRule源程序中提供了这些方法)。我们可以再ProcessMessage中对收到的短信进行我们希望的操作。该方法的定义如下:
HRESULT CMailRuleClient::ProcessMessage(IMsgStore *pMsgStore, ULONG cbMsg, LPENTRYID lpMsg, ULONG cbDestFolder, LPENTRYID lpDestFolder, ULONG *pulEventType, MRCHANDLED *pHandled)
这里我们主要关心前三个参数即可,第一个参数代表指向短信(邮件)仓库的指针,后两个参数表示被截获短信的ENTRYID。参数cbDestFolder和lpDestFolder指定了该条短信将被发送到的目的信箱(Folder)的ENTRYID,这里指向的一定是收件箱,从这里我们可以隐约猜到,截获短信只是ProcessMessage方法的其中一个功能,其它功能有兴趣的朋友可以挖掘一下。倒数第二个参数pulEventType用于设置短信变化的事件,通过为它赋值,可以实现当该短信被删除等操作时我们可以通过上一篇文章提供的方法获取到对应的通知(Notify)。
MapiRule中提供的实现中有一句代码需要注意。
hr = pMsgStore->OpenEntry(cbMsg, lpMsg, NULL, 0, NULL, (LPUNKNOWN *) &pMsg);
从上面的代码中可以看出,被拦截的消息是从短信(邮件)仓库中直接获取的,而不是具体信箱。这跟我们前面使用IMAPIFolder对象获取短信的方法有所不同,这也间接的提示了我们ProcessMessage被调用的时机。即被拦截的短信并已经到达了短信邮件仓库,但并未被分配到具体的短信信箱(Folder)中时被调用。
c) 被拦截到的短信跑哪去了?
细心的朋友应该发现,被拦截的短信没有在系统收件箱中出现,通过浏览ProcessMessage中的代码,我们可以找到如下代码。
hr = DeleteMessage(pMsgStore, pMsg, cbMsg, lpMsg, cbDestFolder, lpDestFolder, pulEventType, pHandled);
这行代码帮助我们删除掉了被截获的短消息,其参数与ProcessMessage相同,这里不再赘述。
4. 我想再我自己的程序中获取被拦截到的短消息,如何实现呢?
a) 外部程序获取截获消息信息的思路
短信被mapirule截获后,我们希望在自己的程序中获取被截获的短信信息,但是由于mapirule.dll和我们的程序不在同一个进程中,因此无法直接将获取的信息反馈给我们的程序。进程间通讯的方法有很多种,比如PostMessage,xml等流文件,管道,内存映射文件等。在微软提供的ReceivingSMS中给出的方法是采用内存映射文件的方式,因此我们也使用内存映射文件的方式来实现进程间数据的传递。简单的思路是,首先创建内存映射文件,然后再ProcessMessage方法被调用时(短信被截获时)像映射文件中写入截获的内容,然后通过指针传递给我们的程序,在程序不再需要内存映射文件的时候,删除该文件。
b) 微软已经给我提供实现这个思路的部分源程序,可以直接从ReceivingSMS这个Demo中获得。
微软在ReceivingSMS中给出了实现上述思路的方法,该方法由如下3个函数组成。
typedef struct {
WCHAR g_szMessageBody[255];
WCHAR g_szPhoneNr[255];
} SMS_BUFFER;
typedef SMS_BUFFER *PSMS_BUFFER;
HANDLE g_hMMObj = NULL;
PSMS_BUFFER g_pSmsBuffer = NULL;
HANDLE g_hSmsAvailableEvent = NULL;
HANDLE g_hMutex = NULL;
EXTERN_C void CaptureSMSMessages (void)
{
g_hClientEvent = CreateEvent(NULL, FALSE, FALSE, _T("SMSAvailableEvent"));
assert(g_hClientEvent != NULL);
g_hClientMutex = CreateMutex(NULL, FALSE, _T("SMSDataMutex"));
assert(g_hClientMutex != NULL);
g_hClientMMObj = CreateFileMapping((HANDLE)-1, NULL, PAGE_READWRITE, 0, MMBUFSIZE, TEXT("SmsBuffer"));
assert(g_hClientMMObj != NULL);
g_pClientSmsBuffer = (PSMS_BUFFER)MapViewOfFile(g_hClientMMObj, FILE_MAP_WRITE, 0, 0, 0);
if (g_pClientSmsBuffer == NULL) {
CloseHandle(g_hClientMMObj);
}
assert(g_pClientSmsBuffer != NULL);
}
EXTERN_C BOOL SMSMessageAvailable (wchar_t *lpDestination, wchar_t *lpPhoneNr)
{
WaitForSingleObject(g_hClientEvent, INFINITE);
if (g_pClientSmsBuffer != NULL) {
WaitForSingleObject(g_hClientMutex, INFINITE);
lstrcpy(lpPhoneNr, g_pClientSmsBuffer->g_szPhoneNr);
lstrcpy(lpDestination, g_pClientSmsBuffer->g_szMessageBody);
ReleaseMutex(g_hClientMutex);
} else {
*lpPhoneNr = '\0';
*lpDestination = '\0';
}
return *lpPhoneNr != '\0';
}
EXTERN_C void TerminateSMSMessagePassing (void)
{
memset(g_pClientSmsBuffer, 0, sizeof(SMS_BUFFER));
SetEvent(g_hClientEvent);
CloseHandle(g_hClientEvent);
CloseHandle(g_hClientMutex);
if (g_pClientSmsBuffer) {
UnmapViewOfFile(g_pClientSmsBuffer);
g_pClientSmsBuffer = NULL;
}
if (g_hClientMMObj) {
CloseHandle(g_hClientMMObj);
g_hClientMMObj = NULL;
}
}
CaptureSMSMessages函数创建了内存映射文件、互斥资源g_hMutex、以及阻塞事件hSmsAvailableEvent。SMSMessageAvailable函数用于为外部程序返回我们所截获的短信内容,注意WaitForSingleObject这行代码,当程序运行这里会等待事件被设置(事件会在ProcessMessage截获到短信后使用SetEvent函数来设置),否则会阻塞等待,这也提示了我们,在外部程序中需要使用工作线程来调用该函数。 TerminateSMSMessagePassing用于删除内存映射文件以及事件和互斥资源。
在ProcessMessage截获短信后和删除短信前,增加了如下代码:
WaitForSingleObject(g_hMutex, INFINITE);
lstrcpy(g_pSmsBuffer->g_szPhoneNr, pspvEmail->Value.lpszW);
lstrcpy(g_pSmsBuffer->g_szMessageBody, pspvSubject->Value.lpszW);
ReleaseMutex(g_hMutex);
SetEvent(g_hSmsAvailableEvent);
这段代码将截获到的短信信息拷贝到内存映射文件中,并设置事件,取消SMSMessageAvailable函数的阻塞等待状态。
c) 如果你是CSharp的开发者,那么恭喜你ReceivingSms就是一个CSharp调用MapiRule拦截短消息的Demo,你可以直接使用了。如果你是C++的开发者,那么我们还需要做些工作。
将前面的三个函数添加到源程序中(别忘了再def文件中声明导出),重新编译并部署以后,就可以在我们的程序中调用了。使用LoadLibrary和GetProcAddress函数导出上面的三个函数,然后调用CaptureSMSMessages函数初始化内存映射文件和资源,创建一个线程,在线程中调用
SMSMessageAvailable函数。当短信到达时,短信内容会由该函数返回。当使用完毕后不要忘了调用TerminateSMSMessagePassing函数删除资源。这部分的源代码如下:
//初始化操作
HINSTANCE hInst;
HANDLE hThread;
hInst=LoadLibrary(_T("\\windows\\mapirule.dll")); //获取mapirule.dll
if(NULL==hInst) //没有找到DLL
{
return CMsgResult(_T("加载MapiRule.dll失败!没有找到DLL"),_T("CMsgControl->BeginMonitor"),ERR_LOAD_DLL);
}
//导出函数
lpfCaptureSmsMessages=(LPFCAPTURESMSMESSAGES)GetProcAddress(hInst,_T("CaptureSMSMessages"));
lpfSmsMessageAvailable=(LPFSMSMESSAGEAVAILABLE)GetProcAddress(hInst,_T("SMSMessageAvailable"));
lpfTermainateSmsMessagePassing=(LPFTERMINATESMSMESSAGEPASSING)GetProcAddress(hInst,_T("TerminateSMSMessagePassing"));
hThread=CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ThreadProc,(LPVOID)&stThreadParameter,NULL,&dwThreadId);
线程处理函数ThreadProc定义如下:
ThreadProc(LPVOID lpParam) //进程函数
{
wchar_t pswBody[360]={0};
wchar_t pswPhone[20]={0};
bool bRet=lpfSmsMessageAvailable(pswBody,pswPhone); //阻塞等待
return 0;
}