月度归档:2012年05月

向Windows进程注入自定义DLL的几种方法

将自定义的DLL注入到目标进程可以用来实现在用户态下拦截Windows api。一般的,我们把自己的函数写在dll中,然后通过修改pe文件的导入表来实现函数的重定向,也就是将pe文件中调用kernel32.dll,user32.dll,gdi32.dll的导出函数的地址改成我们dll中的自定义函数的偏移地址。这样我们就达到了拦截api,增加函数功能,监视程序行为的目的。

这里暂时不讨论API拦截,只讨论如何将自定义的dll注入到windows进程的地址空间中去。

一般来说有以下几种方法:

 一、使用注册表注入DLL

打开HKEY_LOCAL_MACHINE\SOFTWARE\MICROSOFT\WINDOWS NT\CURRENTVERSION\WINDOWS\APPINIT_DLLS,将自定义的dll的路径填入这个键对应的值域中。重启系统或explorer.exe进程,就可以用IceSword等工具看到所有进程的模块信息中多了一个自定义的dll。这种方法优点是方便,一次性可以将dll注入到所有进程,而缺点也是这个,因为我们不能对注入做任何过滤和控制。如果仅仅是想要监视某几个应用程序,这种做法略显沉重。

 二、使用钩子注入dll

首先定义钩子函数,钩子函数是一种回调函数,当钩子监视的事件发生后,系统会调用钩子函数进行处理。接着调用windows的SetWindowsHook函数来安装钩子,函数的参数包括钩子函数类型,钩子函数所在的实例的句柄,目标进程id等。这个过程中进程能访问到钩子函数的原因就是系统在调用SetWindowsHook之后就对特定进程自动调用LoadLibrary函数,这个特定进程是由传入的参数目标进程id决定的。调用LoadLibrary函数后就会加载钩子函数执行代码的模块。因此我们只要将钩子函数编译成DLL,就可以实现DLL的自动注入了。一般只要安装WH_GETMESSAGE钩子就型了,因为windows下的大部分程序要调用GetMessage函数从消息队列中获取消息。

 三、使用CreateRemoteThread函数注入DLL

CreateRemoteThread函数功能很强大,它能够在其他进程中创建线程。其调用形式如下:
hThread = CreateRemoteThread(hProcess,NULL,0,pLoadLibrary,"C:\MYDLL.DLL",0,NULL);
hProcess是目标进程句柄,pLoadLibrary是LoadLibrary函数的指针。LoadLibrary是kernel32.dll的导出函数,且这个函数被每个进程映射到地址空间中的位置都相同,这样就能保证在当前进程中得到的pLoadLibrary值在目标进程中仍然有效。CreateRemoteThread函数执行之后,目标进程中就会生成一个线程,执行LoadLibrary函数,该函数的参数就是我们刚传过去的自定义DLL的绝对路径。这样就完成了dll注入的目的。

--EOF--

Socket粘包问题处理

当初对socket函数的使用理解不深,导致socket接收方触发FD_READ事件(Windows 异步Socket)的次数远小于发送方调用send函数的次数,大量请求报文因未作处理而“丢失”。为解决这样的报文“丢失”问题,当时想到的一个治标不治本的方法是在每次send函数调用后均加一个百毫秒级的延时,这样报文“丢失”的问题虽能看似解决,但是严重影响到了通信效率。后来调试发现,此问题的关键症结在于,发送方调用多次send函数所发送的内容被接收方一次性接收了,而接收方只对这本来是多个的报文进行了一次处理。

查阅资料发现,这个问题在socket编程中属于比较常见的“粘包”问题,网上有对该问题的解决方法亦有不少。导致“粘包”问题的原因还得从send和recv函数的实现上找,send函数的原型为:int send(SOCKET s, const char *buf, int len, int flags),其作用是将buf数组中的内容复制到SOCKET s的缓冲区,复制完毕后返回所复制内容的长度,至于缓冲区的内容什么时候传输到接收方,那是TCP协议栈关心的事情,send函数并不关心。相应的,recv函数的作用仅仅是将接收缓冲区中的内容复制到参数指定的buf数组,返回值为所复制内容的长度。send和recv函数本身并不能实现其名字所隐含的数据发送和接收的功能,真正完成数据传送、保证可靠传输的是TCP协议栈。回到本文开头的报文“丢失”问题,由于默认缓冲区大小远大于每次send函数所要发送的内容,因此,多次调用send函数才触发了一次TCP协议去发送数据,接收方的也只会收到一个FD_READ事件,但是其接收到的内容长度是发送方多次send的内容的总和。

因为项目中socket传送的内容都是结构化数据,而且已经约定好每个报文的首字节表示当前报文长度,这样一来,解决“粘包”问题的逻辑就非常简单,只需将接收到的内容根据其首字节长度依次分离出来即可。伪代码实现如下:

1
2
3
4
5
6
7
8
9
case FD_READ: //Win Socket响应函数处理FD_READ事件的代码片段。
    int iter = 0; //游标,依次指示每个报文的长度。
    rlen = recv(socket, (char*)buf, MAX_BUF_LENGTH, 0);
    while(iter < rlen) {
        unsigned int mlen = buf[iter]; //mlen表示当前报文长度。
        memcpy(&message, buf+iter, mlen); //将当前报文复制到message结构体中。
        DispatchMessage(message); //将message报文交给相应模块处理。
        iter += mlen; //游标向前迭代,指示下一个报文长度。
    }

--EOF--

『伟大的逃亡』

中世纪末期,有组织且富有攻击力的丹麦实际上掌控着北欧三国(瑞典、挪威、丹麦)。就像历史上所有独立战争一样,瑞典的民族主义者和爱国主义者开始反抗,争取瑞典独立,虽然有过阶段性胜利,但最终还是负于丹麦。时间到了1520年,丹麦国王克里斯蒂安二世以胜利者的姿态来到了斯德哥尔摩,以一副不计前嫌、誓与瑞典人民友好相处的姿态邀请80+位瑞典社会名流、精英和贵族参加其在首都皇宫举行的宴会。单纯的瑞典人相信了,悉数前往,可是等待他们的不是盛宴却是一次审判和杀戮,1520年11月7日,克里斯蒂安国王以瓮中捉鳖的方式将当时的瑞典上层几乎一网打尽,他以为真正拔除了瑞典这颗心头的刺,斯堪的那维亚半岛从此可以高枕无忧。

然而一个国家的独立事业总是有人继承的,被杀的80多位瑞典名流里有个古斯塔夫家族,他们的后人瓦萨·古斯塔夫继承了继续领导瑞典独立的使命。在克里斯蒂安下令屠杀瑞典贵族的时候,当时的瓦萨实际上正以人质的身份处在丹麦境内,当然,即便遭到全国通缉,身负重任的瓦萨还是在众多命中贵人的帮助下上演伟大的逃亡,十分侥幸地在举国围堵之下逃离丹麦,回到瑞典,要不然,瑞典的历史上也就不会有瓦萨王朝了。

瓦萨号召瑞典各路独立力量起来反抗丹麦人克里斯蒂安的统治。然而,响应的人并不多,特别是瑞典的农民,这本该最易被煽动的革命军主力阶级有着自己的小算盘:其一,克里斯蒂安所做的坏事和杀死的人都是针对瑞典的贵族阶级,他并没有伤害到任何农民。事实上,反倒是克里斯蒂安给了瑞典农民一次获得自由的机会,让其免受大地主的剥削。谁是朋友谁是敌人,瑞典农民难以划出界限。其二,中世纪之前,国家的概念非常模糊,封建时代人们忠诚的只是自己的主人,区域性较强,往往以村庄或者小辖区为单位,比如,人们会自称为伦敦人而不是英国人。所以,丹麦国王的统治对瑞典人民而言没有任何思想上的负担,而现代人也不能要求那个时代的人具备一种如爱国主义这类还没有形成的情操。犹豫之后,农民领袖拒绝了瓦萨的号召,然而在瓦萨离开的不久之后,他们又主动找上瓦萨,共同反抗克里斯蒂安国王。这中间发生了什么史书没有记载,一个比较可信的原因是农民在了解了斯德哥尔摩大屠杀之后,对克里斯蒂安国王的残暴有所顾忌,从而答应瓦萨,愿意接受领导一同推翻克里斯蒂安在斯德哥尔摩的统治。

三年之后。

1523年6月6日,瓦萨被推选为瑞典国王。瑞典建国。

--EOF--

上位机轮询串口获取下位机反馈报文的算法

以前写过一篇『串口数据传输的读/写时机问题』,提到上位机(Master)发送报文给下位机(Slave)后,不能马上去读取所需信息,而需要等待一段时间,等下位机将信息采集完毕(比如温湿度值、光照,也包括上述提到的设备信息)后放在串口缓冲区,然后上位机再去取。当时采用的策略是采用固定等待时间,对于下位机可接受的所有报文均采用相同的延时。这样做有1个好处和2个坏处。好处是程序代码简单,WriteFile()/Sleep(t)/ReadFile()三行核心代码就能搞定。坏处是:一、报文功能不同,下位机的备齐时间也不同,有些简单的报文,根本无需与复杂报文一样等待t毫秒,因此这样设计造成了时间上的浪费。二、一般下位机的效率还受到所在环境的因素和折旧的影响,在其生命周期内不一定都能保证间隔t内能返回结果。t间隔内,现在能正确返回并不保证今后也能正确返回。

因此更好的方法是上位机轮询串口,将多次读取到的数据进行拼接,从而得到一个有效的下位机反馈报文。这种做法的前提是反馈报文的首字节需表示报文的长度字段,不过一般下位的传感设备的报文格式中肯定会包含报文长度字段,所以保证这样的前提不成问题。

采用轮询串口算法的伪代码实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
初始化字节数组chars[];
WriteFile(com); //将请求命令发送至串口。
记录当前时刻begin_time;
while(当前时刻-begin_time< VALID_DELYAY){//本次轮询串口的超时时间。
    ReadFile(com, nByteRead, buffer); //nByteRead表示已读到的数据长度,
					//buffer是读到的字节数据的存储缓冲区。
    if(nByteRead == 0){
        Sleep(1)//隔1毫秒再读一次。
    }else{
        strcat(chars, buffer, nByteRead);//将buffer中内容拼接到chars数组末尾。
        if(chars[0] == 当前的chars数组长度){
            break;//已取到合法的有效的反馈报文,结束循环。   
        }
    }
}

以上伪代码仅演示了正确读取反馈报文的流程,未加错误处理,譬如经过某次拼接后chars数组的长度大于chars[0]的值了,譬如chars[0]的值因线路噪音发生了跳变……等等。

--EOF--