分类归档: Programming

编程编程编程。。。

向其他进程注入代码的三种方法[2]

     这是什么意思?每个进程都有一个特殊的寄存器,这个寄存器指向下一条要执行的指令的内存地址,即32位Intel和AMD处理器上所谓的EIP寄存器。因为EIP是个特殊的寄存器,所以你不能像访问通用寄存器(EAX,EBX等)那样来访问它。换句话说,你找不到一个可以用来寻址EIP并且对它进行读写的操作码(OpCode)。然而,EIP同样可以被JMP,CALL,RET等指令隐含地改变(事实上它一直都在改变)。让我们举例说明32位的Intel和AMD处理器上CALL/RET是如何工作的吧:

     当我们用CALL调用一个子程序时,这个子程序的地址被加载进EIP。同时,在EIP被改变之前,它以前的值会被自动压栈(在后来被用作返回指令指针[return instruction-pointer])。在子程序的最后RET指令自动把这个值从栈中弹出到EIP。

     现在我们知道了如何通过CALL和RET来修改EIP的值了,但是如何得到他的当前值?
还记得CALL把EIP的值压栈了吗?所以为了得到EIP的值我们调用了一个“假(dummy)函数”然后弹出栈顶值。看一下编译过的NewProc:

Address    OpCode/Params    Decoded instruction
————————————————–
:00401000   55          push ebp             ; entry point of
                                                ; NewProc
:00401001   8BEC             mov ebp, esp
:00401003   51               push ecx
:00401004   E800000000       call 00401009        ; *a*     call dummy
:00401009   59          pop ecx             ; *b*
:0040100A   83E909           sub ecx, 00000009    ; *c*
:0040100D   894DFC           mov [ebp-04], ecx    ; mov pData, ECX
:00401010   8B45FC           mov eax, [ebp-04]
:00401013   83E814           sub eax, 00000014    ; pData–;
…..
…..
:0040102D   8BE5             mov esp, ebp
:0040102F   5D               pop ebp
:00401030   C21000           ret 0010

     a. 一个假的函数调用;仅仅跳到下一条指令并且(译者注:更重要的是)把EIP压栈。
     b. 弹出栈顶值到ECX。ECX就保存的EIP的值;这也就是那条“pop ECX”指令的地址。
     c. 注意从NewProc的入口点到“pop ECX”指令的“距离”为9字节;因此把ECX减去9就得到的NewProc的地址了。

     这样一来,不管被复制到什么地方,NewProc总能正确计算自身的地址了!然而,要注意从NewProc的入口点到“pop ECX”的距离可能会因为你的编译器/链接选项的不同而不同,而且在Release和Degub版本中也是不一样的。但是,不管怎样,你仍然可以在编译期知道这个距离的具体值。
     1. 首先,编译你的函数。
     2. 在反汇编器(disassembler)中查出正确的距离值。
     3. 最后,使用正确的距离值重新编译你的程序。

     这也是InjectEx中使用的解决方案。InjectEx和HookInjEx类似,交换开始按钮上的鼠标左右键点击事件。

解决方案2

     在远程进程中把INJDATA放在NewProc的前面并不是唯一的解决方案。看一下下面的NewProc:
static LRESULT CALLBACK NewProc(
   HWND hwnd,       // handle to window
   UINT uMsg,       // message identifier
   WPARAM wParam,   // first message parameter
   LPARAM lParam ) // second message parameter
{
     INJDATA* pData = 0xA0B0C0D0;     // 一个假值

     //—————————–
     // 子类代码
     // ……..
     //—————————–

     // 调用以前的窗口过程
     return pData->fnCallWindowProc( pData->fnOldProc,
                                     hwnd,uMsg,wParam,lParam );
}

     这里,0XA0B0C0D0仅仅是INJDATA在远程进程中的地址的占位符(placeholder)。你无法在编译期得到这个值,然而你在调用VirtualAllocEx(为INJDATA分配内存时)后确实知道INJDATA的地址!(译者注:就是VirtualAllocEx的返回值)

     我们的NewProc编译后大概是这个样子:
Address    OpCode/Params      Decoded instruction
————————————————–
:00401000   55                 push ebp
:00401001   8BEC               mov ebp, esp
:00401003   C745FCD0C0B0A0     mov [ebp-04], A0B0C0D0
:0040100A   …
….
….
:0040102D   8BE5               mov esp, ebp
:0040102F   5D                 pop ebp
:00401030   C21000             ret 0010

     编译后的机器码应该为:558BECC745FCD0C0B0A0……8BE55DC21000。

     现在,你这么做:
     1. 把INJDATA,ThreadFunc和NewFunc复制到目的进程。
     2. 改变NewPoc的机器码,让pData指向INJDATA的真实地址。
     比如,假设INJDATA的的真实地址(VirtualAllocEx的返回值)为0x008a0000,你把NewProc的机器码改为:

558BECC745FCD0C0B0A0……8BE55DC21000   <- 修改前的 NewProc 1    
558BECC745FC00008A00……8BE55DC21000   <- 修改后的 NewProc  

     也就是说,你把假值 A0B0C0D0改为INJDATA的真实地址2
     3. 开始指向远程的ThreadFunc,它子类了远程进程中的控件。

     &sup1; 你可能会问,为什么A0B0C0D0和008a0000在编译后的机器码中为逆序的。这时因为Intel和AMD处理器使用littl-endian标记法(little-endian notation)来表示它们的(多字节)数据。换句话说:一个数的低字节(low-order byte)在内存中被存放在最低位,高字节(high-order byte)存放在最高位。
想像一下,存放在四个字节中的单词“UNIX”,在big-endia系统中被存储为“UNIX”,在little-endian系统中被存储为“XINU”。

     &sup2; 一些蹩脚的破解者用类似的方法来修改可执行文件的机器码,但是一个程序一旦载入内存,就不能再更改自身的机器码(一个可执行文件的.text段是写保护的)。我们能修改远程进程中的NewProc是因为它所处的那块内存在分配时给予了PAGE_EXECUTE_READWRITE属性。

     何时使用CreateRemoteThread和WriteProcessMemory技术

    通过CreateRemoteThread和WriteProcessMemory来注入代码的技术,和其他两种方法相比,不需要一个额外的DLL文件,因此更灵活,但也更复杂更危险。一旦你的ThreadFunc中有错误,远程线程会立即崩溃(看附录F)。调试一个远程的ThreadFunc也是场恶梦,所以你应该在仅仅注入若干条指令时才使用这个方法。要注入大量的代码还是使用另外两种方法吧。

     再说一次,你可以在文章的开头部分下载到WinSpy,InjectEx和它们的源代码。

     写在最后的话

     最后,我们总结一些目前还没有提到的东西:

     方法 适用的操作系统 可操作的进程进程    
     I. Windows钩子 Win9x 和WinNT 仅限链接了USER32.DLL的进程1    
     II. CreateRemoteThread & LoadLibrary 仅WinNT2 所有进程3,包括系统服务4    
     III. CreateRemoteThread & WriteProcessMemory 近WinNT 所有进程,包括系统服务  

     1. 很明显,你不能给一个没有消息队列的线程挂钩。同样SetWindowsHookEx也对系统服务不起作用(就算它们连接了USER32)。
     2. 在Win9x下没有CreateRemoteThread和VirtualAllocEx(事实上可以在9x上模拟它们,但是到目前为止还只是个神话)
    3. 所有进程 = 所有的Win32进程 + csrss.exe
     本地程序(native application)比如smss.exe, os2ss.exe, autochk.exe,不使用Win32 APIs,也没有连接到kernel32.dll。唯一的例外是csrss.exe,win32子系统自身。它是一个本地程序,但是它的一些库(比如winsrv.dll)需要Win32 DLL包括kernel32.dll.
     4.如果你向注入代码到系统服务或csrss.exe,在打开远程进程的句柄(OpenProcess)之前把你的进程的优先级调整为“SeDebugprovilege”(AdjustTokenPrivileges)。

     大概就这些了吧。还有一点你需要牢记在心:你注入的代码(特别是存在错误时)很容易就会把目的进程拖垮。记住:责任随权利而来(Power comes with responsibility)!

     这篇文章中的很多例子都和密码有关,看过这篇文章后你可能也会对Zhefu Zhang(译者注:大概是一位中国人,张哲夫??)写的Supper Password Spy++感兴趣。他讲解了如何从IE的密码框中得到密码,也说了如何保护你的密码不被这种攻击。

     最后一点:读者的反馈是文章作者的唯一报酬,所以如果你认为这篇文章有作用,请留下你的评论或给它投票。更重要的是,如果你发现有错误或bug;或你认为什么地方做得还不够好,有需要改进的地方;或有不清楚的地方也都请告诉我。

感谢
     首先,我要感谢我在CodeGuru(这篇文章最早是在那儿发表的)的读者,正是由于你们的鼓励和支持这篇文章才得以从最初的1200单词发展到今天这样6000单词的“庞然大物”。如果说有一个人我要特别感谢的话,他就是Rado Picha。这篇文章的一部分很大程度上得益于他对我的建议和帮助。最后,但也不能算是最后,感谢Susan Moore,他帮助我跨越了那个叫做“英语”的雷区,让这篇文章更加通顺达意。
――――――――――――――――――――――――――――――――――――
附录
A) 为什么kernel32.dll和user32.dll中是被映射到相同的内存地址?
我的假定:以为微软的程序员认为这么做可以优化速度。让我们来解释一下这是为什么。
一般来说,一个可执行文件包含几个段,其中一个为“.reloc”段。

当链接器生成EXE或DLL时,它假定这个文件会被加载到一个特定的地址,也就是所谓的假定/首选加载/基地址(assumed/preferred load/base address)。内存映像(image)中的所有绝对地址都时基于该“链接器假定加载地址”的。如果由于某些原因,映像没有加载到这个地址,那么PE加载器(PE loader)就不得不修正该映像中的所有绝对地址。这就是“.reloc”段存在的原因:它包含了一个该映像中所有的“链接器假定地址”与真正加载到的地址之间的差异的列表(注意:编译器产生的大部分指令都使用一种相对寻址模式,所以,真正需要重定位[relocation]的地方并没有你想像的那么多)。如果,从另一方面说,加载器可以把映像加载到链接器首选地址,那么“.reloc”段就会被彻底忽略。

但是,因为每一个Win32程序都需要kernel32.dll,大部分需要user32.dll,所以如果总是把它们两个映射到其首选地址,那么加载器就不用修正kernel32.dll和user32.dll中的任何(绝对)地址,加载时间就可以缩短。

让我们用下面的例子来结束这个讨论:
把一个APP.exe的加载地址改为kernel32的(/base:”0x77e80000″)或user32的(/base:”0x77e10000″)首选地址。如果App.exe没有引入UESE32,就强制LoadLibrary。然后编译App.exe,并运行它。你会得到一个错误框(“非法的系统DLL重定位”),App.exe无法被加载。

为什么?当一个进程被创建时,Win2000和WinXP的加载器会检查kernel32.dll和user32.dll是否被映射到它们的首选地址(它们的名称是被硬编码进加载器的),如果没有,就会报错。在WinNT4 中ole32.dll也会被检查。在WinNT3.51或更低版本中,则不会有任何检查,kernel32.dll和user32.dll可以被加载到任何地方。唯一一个总是被加载到首选地址的模块是ntdll.dll,加载器并不检查它,但是如果它不在它的首选地址,进程根本无法创建。

总结一下:在WinNT4或更高版本的操作系统中:
●总被加载到它们的首选地址的DLL有:kernel32.dll,user32.dll和ntdll.dll。
●Win32程序(连同csrss.exe)中一定存在的DLL:kernel32.dll和ntdll.dll。
●所有进程中都存在的dll:ntdll.dll。

B) /GZ编译开关
在Debug时,/GZ开关默认是打开的。它可以帮你捕捉一些错误(详细内容参考文档)。但是它对我们的可执行文件有什么影响呢?

当/GZ被使用时,编译器会在每个函数,包含函数调用中添加额外的代码(添加到每个函数的最后面)来检查ESP栈指针是否被我们的函数更改过。但是,等等,ThreadFunc中被添加了一个函数调用?这就是通往灾难的道路。因为,被复制到远程进程中的ThreadFunc将调用一个在远程进程中不存在的函数。

C) static函数和增量连接(Incremental linking)
增量连接可以缩短连接的时间,在增量编译时,每个函数调用都是通过一个额外的JMP指令来实现的(一个例外就是被声明为static的函数!)这些JMP允许连接器移动函数在内存中的位置而不用更新调用该函数的CALL。但是就是这个JMP给我们带来了麻烦:现在ThreadFunc和AfterThreadFunc将指向JMP指令而不是它们的真实代码。所以,当计算ThreadFunc的大小时:
const int cbCodeSize = ((LPBYTE) AfterThreadFunc – (LPBYTE) ThreadFunc);
你实际得到的将是指向ThreadFunc和AfterThreadFunc的JMP指令之间的“距离”。现在假设我们的ThreadFunc在004014C0,和其对应的JMP指令在00401020
:00401020    jmp   004014C0

:004014C0    push EBP           ; ThreadFunc的真实地址
:004014C1    mov   EBP, ESP

然后,
WriteProcessMemory( .., &ThreadFunc, cbCodeSize, ..);
将把“JMP 004014C0”和其后的cbCodeSize范围内的代码而不是ThreadFunc复制到远程进程。远程线程首先会执行“JMP 004010C0”,然后一直执行到这个进程代码的最后一条指令(译者注:这当然不是我们想要的结果)。

然而,如果一个函数被声明为static,就算使用增量连接,也不会被替换为JMP指令。这就是为什么我在规则#4中说把ThreadFunc和AfterThreadFunc声明为static或禁止增量连接的原因了。(关于增量连接的其他方面请参看Matt Pietrek写的“Remove Fatty Deposits from Your Applications Using Our 32-bit Liposuction Tools”)

D) 为什么ThreadFunc只能有4K的局部变量?
局部变量总是保存在栈上的。假设一个函数有256字节的局部变量,当进入该函数时(更确切地说是在functions prologue中),栈指针会被减去256。像下面的函数:
void Dummy(void) {
     BYTE var[256];
     var[0] = 0;
     var[1] = 1;
     var[255] = 255;
}
会被编译为类似下面的指令:
:00401000    push ebp
:00401001    mov   ebp, esp
:00401003    sub   esp, 00000100            ; change ESP as storage for
                                          ; local variables is needed
:00401006    mov   byte ptr [esp], 00       ; var[0] = 0;
:0040100A    mov   byte ptr [esp+01], 01    ; var[1] = 1;
:0040100F    mov   byte ptr [esp+FF], FF    ; var[255] = 255;
:00401017    mov   esp, ebp                 ; restore stack pointer
:00401019    pop   ebp
:0040101A    ret

请注意在上面的例子中ESP(栈指针)是如何被改变的。但是如果一个函数有多于4K的局部变量该怎么办?这种情况下,栈指针不会被直接改变,而是通过一个函数调用来正确实现ESP的改变。但是就是这个“函数调用”导致了ThreadFunc的崩溃,因为它在远程进程中的拷贝将会调用一个不存在的函数。

让我们来看看文档关于栈探针(stack probes)和/Gs编译选项的说明:
“/Gssize选项是一个允许你控制栈探针的高级特性。栈探针是编译器插入到每个函数调用中的一系列代码。当被激活时,栈探针将温和地按照存储函数局部变量所需要的空间大小来移动

如果一个函数需要大于size指定的局部变量空间,它的栈探针将被激活。默认的size为一个页的大小(在80×86上为4k)。这个值可以使一个Win32程序和Windows NT的虚拟内存管理程序和谐地交互,在运行期间向程序栈增加已提交的内存总数。

我能确定你们对上面的叙述(“栈探针将温和地按照存储函数局部变量所需要的空间大小来移动”)感到奇怪。这些编译选项(他们的描述!)有时候真的让人很恼火,特别是当你想真的了解它们是怎么工作的时候。打个比方,如果一个函数需要12kb的空间来存放局部变量,栈上的内存是这样“分配”的
sub     esp, 0x1000     ; 先“分配”4 Kb
test   [esp], eax       ; touches memory in order to commit a
                       ; new page (if not already committed)
sub     esp, 0x1000     ; “分配”第二个 4 Kb
test   [esp], eax       ; …
sub     esp, 0x1000
test   [esp], eax

注意栈指针是如何以4Kb为单位移动的,更重要的是每移动一步后使用test对栈底的处理(more importantly, how the bottom of the stack is “touched” after each step)。这可以确保了在“分配”下一个页之前,包含栈底的页已经被提交。

继续阅读文档的说明:
“每一个新的线程会拥有(receives)自己的栈空间,这包括已经提交的内存和保留的内存。默认情况下每个线程使用1MB的保留内存和一个页大小的以提交内存。如果有必要,系统将从保留内存中提交一个页。”(看MSDN中GreateThread > dwStackSize   > “Thread Stack Size”)

..现在为什么文档中说“这个值可以使一个Win32程序和Windows NT的虚拟内存管理程序和谐地交互”也很清楚了。

E) 为什么我要把多于3个case分支的swith分割开来呢?
同样,用例子来说明会简单些:
int Dummy( int arg1 )
{
     int ret =0;

     switch( arg1 ) {
     case 1: ret = 1; break;
     case 2: ret = 2; break;
     case 3: ret = 3; break;
     case 4: ret = 0xA0B0; break;
     }
     return ret;
}
将会被编译为类似下面的代码:
Address    OpCode/Params     Decoded instruction
————————————————–
                                              ; arg1 -> ECX
:00401000   8B4C2404          mov ecx, dword ptr [esp+04]
:00401004   33C0              xor eax, eax      ; EAX = 0
:00401006   49                dec ecx           ; ECX —
:00401007   83F903            cmp ecx, 00000003
:0040100A   771E              ja 0040102A

; JMP to one of the addresses in table ***
; note that ECX contains the offset
:0040100C   FF248D2C104000    jmp dword ptr [4*ecx+0040102C]

:00401013   B801000000        mov eax, 00000001    ; case 1: eax = 1;
:00401018   C3                 ret
:00401019   B802000000        mov eax, 00000002    ; case 2: eax = 2;
:0040101E   C3                 ret
:0040101F   B803000000        mov eax, 00000003    ; case 3: eax = 3;
:00401024   C3                 ret
:00401025   B8B0A00000        mov eax, 0000A0B0    ; case 4: eax = 0xA0B0;
:0040102A   C3                 ret
:0040102B   90                 nop

; 地址表 ***
:0040102C   13104000          DWORD 00401013    ; jump to case 1
:00401030   19104000          DWORD 00401019    ; jump to case 2
:00401034   1F104000          DWORD 0040101F    ; jump to case 3
:00401038   25104000          DWORD 00401025    ; jump to case 4

看到switch-case是如何实现的了吗?
它没有去测试每个case分支,而是创建了一个地址表(address table)。我们简单地计算出在地址表中偏移就可以跳到正确的case分支。想想吧,这真是一个进步,假设你有一个50个分支的switch语句,假如没有这个技巧,你不的不执行50次CMP和JMP才能到达最后一个case,而使用地址表,你可以通过一次查表即跳到正确的case。使用算法的时间复杂度来衡量:我们把O(2n)的算法替换成了O(5)的算法,其中:
1. O代表最坏情况下的时间复杂度。
2. 我们假设计算偏移(即查表)并跳到正确的地址需要5个指令。

现在,你可能认为上面的情况仅仅是因为case常量选择得比较好,(1,2,3,4,5)。幸运的是,现实生活中的大多数例子都可以应用这个方案,只是偏移的计算复杂了一点而已。但是,有两个例外:
●如果少于3个case分支,或
●如果case常量是完全相互无关的。(比如 1, 13, 50, 1000)。
最终的结果和你使用普通的if-else if是一样的。

有趣的地方:如果你曾经为case后面只能跟常量而迷惑的话,现在你应该知道为什么了吧。这个值必须在编译期间就确定下来,这样才能创建地址表。

回到我们的问题!
注意到0040100C处的JMP指令了吗?我们来看看Intel的文档对十六进制操作码FF的说明:
Opcode    Instruction   Description
FF /4     JMP r/m32    Jump near, absolute indirect, address given in r/m32

JMP使用了绝对地址!也就是说,它的其中一个操作数(在这里是0040102C)代表一个绝对地址。还用多说吗?现在远程的ThreadFunc会盲目第在地址表中004101C然后跳到这个错误的地方,马上使远程进程挂掉了。

F) 到底是什么原因使远程进程崩溃了?
如果你的远程进程崩溃了,原因可能为下列之一:
1. 你引用了ThreadFunc中一个不存在的字符串。
2. ThreadFunc中一个或多个指令使用了绝对寻址(看附录E中的例子)
3. ThreadFunc调用了一个不存在的函数(这个函数调用可能是编译器或连接器添加的)。这时候你需要在反汇编器中寻找类似下面的代码:
:004014C0     push EBP          ; entry point of ThreadFunc
:004014C1     mov EBP, ESP

:004014C5     call 0041550      ; 在这里崩溃了
                               ; remote process

:00401502     ret
如果这个有争议的CALL是编译器添加的(因为一些不该打开的编译开关比如/GZ打开了),它要么在ThreadFunc的开头要么在ThreadFunc接近结尾的地方

不管在什么情况下,你使用CreateRemoteThread & WriteProcessMemory技术时必须万分的小心,特别是编译器/连接器的设置,它们很可能会给你的ThreadFunc添加一些带来麻烦的东西。

参考(省略)
文章历史(省略)

<结束>

Read: 774

向其他进程注入代码的三种方法[1]

     本文章翻译自Robet Kuster的Three Ways to Inject Your Code into Another Process一文,原版地址见下面。本文章版权归原作者所有。
     如果转载该译文,请保证文章的完整性,并注明来自www.farproc.com
袁晓辉   
2005/5/20

原版地址:http://www.codeproject.com/threads/winspy.asp?df=100&forumid=16291&select=1025152&msg=1025152

下载整个压缩包
下载WinSpy

作者:Robert Kuster
翻译:袁晓辉 (www.farproc.com  hyzs@sina.com)
摘要:如何向其他线程的地址空间中注入代码并在这个线程的上下文中执行之。

目录:
●导言
●Windows 钩子(Hooks)
●CreateRemoteThread 和LoadLibrary 技术
○进程间通讯
●CreateRemoteThread 和 WriteProcessmemory 技术
○如何使用该技术子类(SubClass)其他进程中的控件
○什么情况下适合使用该技术
●写在最后的话
●附录
●参考
●文章历史

导言:
     我们在Code project(www.codeproject.com)上可以找到许多密码间谍程序(译者注:那些可以看到别的程序中密码框内容的软件),他们都依赖于Windows钩子技术。要实现这个还有其他的方法吗?有!但是,首先,让我们简单回顾一下我们要实现的目标,以便你能弄清楚我在说什么。
要读取一个控件的内容,不管它是否属于你自己的程序,一般来说需要发送 WM_GETTEXT 消息到那个控件。这对edit控件也有效,但是有一种情况例外。如果这个edit控件属于其他进程并且具有 ES_PASSWORD 风格的话,这种方法就不会成功。只有“拥有(OWNS)”这个密码控件的进程才可以用 WM_GETTEXT 取得它的内容。所以,我们的问题就是:如何让下面这句代码在其他进程的地址空间中运行起来:
::SendMessage( hPwdEdit, WM_GETTEXT, nMaxChars, psBuffer );

一般来说,这个问题有三种可能的解决方案:
1. 把你的代码放到一个DLL中;然后用 windows 钩子把它映射到远程进程。
2. 把你的代码放到一个DLL中;然后用 CreateRemoteThread 和 LoadLibrary 把它映射到远程进程。
3. 不用DLL,直接复制你的代码到远程进程(使用WriteProcessMemory)并且用CreateRemoteThread执行之。在这里有详细的说明:

Ⅰ. Windows 钩子

示例程序:HookSpy 和 HookInjEx

Windows钩子的主要作用就是监视某个线程的消息流动。一般可分为:
1. 局部钩子,只监视你自己进程中某个线程的消息流动。
2. 远程钩子,又可以分为:
a. 特定线程的,监视别的进程中某个线程的消息;
b. 系统级的,监视整个系统中正在运行的所有线程的消息。

     如果被挂钩(监视)的线程属于别的进程(情况2a和2b),你的钩子过程(hook procedure)必须放在一个动态连接库(DLL)中。系统把这包含了钩子过程的DLL映射到被挂钩的线程的地址空间。Windows会映射整个DLL而不仅仅是你的钩子过程。这就是为什么windows钩子可以用来向其他线程的地址空间注入代码的原因了。

     在这里我不想深入讨论钩子的问题(请看MSDN中对SetWindowsHookEx的说明),让我再告诉你两个文档中找不到的诀窍,可能会有用:
1. 当SetWindowHookEx调用成功后,系统会自动映射这个DLL到被挂钩的线程,但并不是立即映射。因为所有的Windows钩子都是基于消息的,直到一个适当的事件发生后这个DLL才被映射。比如:
如果你安装了一个监视所有未排队的(nonqueued)的消息的钩子(WH_CALLWNDPROC),只有一个消息发送到被挂钩线程(的某个窗口)后这个DLL才被映射。也就是说,如果在消息发送到被挂钩线程之前调用了UnhookWindowsHookEx那么这个DLL就永远不会被映射到该线程(虽然SetWindowsHookEx调用成功了)。为了强制映射,可以在调用SetWindowsHookEx后立即发送一个适当的消息到那个线程。

     同理,调用UnhookWindowsHookEx之后,只有特定的事件发生后DLL才真正地从被挂钩线程卸载。

2. 当你安装了钩子后,系统的性能会受到影响(特别是系统级的钩子)。然而如果你只是使用的特定线程的钩子来映射DLL而且不截获如何消息的话,这个缺陷也可以轻易地避免。看一下下面的代码片段:
BOOL APIENTRY DllMain( HANDLE hModule,
                        DWORD   ul_reason_for_call,
                        LPVOID lpReserved )
{
     if( ul_reason_for_call == DLL_PROCESS_ATTACH )
     {
         //用 LoadLibrary增加引用次数
         char lib_name[MAX_PATH];
         ::GetModuleFileName( hModule, lib_name, MAX_PATH );
         ::LoadLibrary( lib_name );

         // 安全卸载钩子
         ::UnhookWindowsHookEx( g_hHook );
     }    
     return TRUE;
}

     我们来看一下。首先,我们用钩子映射这个DLL到远程线程,然后,在DLL被真正映射进去后,我们立即卸载挂钩(unhook)。一般来说当第一个消息到达被挂钩线程后,这DLL会被卸载,然而我们通过LoadLibrary来增加这个DLL的引用次数,避免了DLL被卸载。

     剩下的问题是:使用完毕后如何卸载这个DLL?UnhookWindowsHookEx不行了,因为我们已经对那个线程取消挂钩(unhook)了。你可以这么做:
○在你想要卸载这个DLL之前再安装一个钩子;
○发送一个“特殊”的消息到远程线程;
○在你的新钩子的钩子过程(hook procedure)中截获该消息,调用FreeLibrary 和 (译者注:对新钩子调用)UnhookwindowsHookEx。
现在,钩子只在映射DLL到远程进程和从远程进程卸载DLL时使用,对被挂钩线程的性能没有影响。也就是说,我们找到了一种(相比第二部分讨论的LoadLibrary技术)WinNT和Win9x下都可以使用的,不影响目的进程性能的DLL映射机制。

     但是,我们应该在何种情况下使用该技巧呢?通常是在DLL需要在远程进程中驻留较长时间(比如你要子类[subclass]另一个进程中的控件)并且你不想过于干涉目的进程时比较适合使用这种技巧。我在HookSpy中并没有使用它,因为那个DLL只是短暂地注入一段时间――只要能取得密码就足够了。我在另一个例子HookInjEx中演示了这种方法。HookInjEx把一个DLL映射进“explorer.exe”(当然,最后又从其中卸载),子类了其中的开始按钮,更确切地说我是把开始按钮的鼠标左右键点击事件颠倒了一下。

     你可以在本文章的开头部分找到HookSpy和HookInjEx及其源代码的下载包链接。

Ⅱ. CreateRemoteThread 和 LoadLibrary 技术
示例程序:LibSpy
     通常,任何进程都可以通过LoadLibrary动态地加载DLL,但是我们如何强制一个外部进程调用该函数呢?答案是CreateRemoteThread。
让我们先来看看LoadLibrary和FreeLibrary的函数声明:

HINSTANCE LoadLibrary(
   LPCTSTR lpLibFileName    // address of filename of library module
);

BOOL FreeLibrary(
   HMODULE hLibModule       // handle to loaded library module
);

再和CreateRemoteThread的线程过程(thread procedure)ThreadProc比较一下:
DWORD WINAPI ThreadProc(
   LPVOID lpParameter    // thread data
);

     你会发现所有的函数都有同样的调用约定(calling convention)、都接受一个32位的参数并且返回值类型的大小也一样。也就是说,我们可以把LoadLibrary/FreeLibrary的指针作为参数传递给CrateRemoteThread。

     然而,还有两个问题(参考下面对CreateRemoteThread的说明)

     1. 传递给ThreadProc的lpStartAddress 参数必须为远程进程中的线程过程的起始地址。
     2. 如果把ThreadProc的lpParameter参数当做一个普通的32位整数(FreeLibrary把它当做HMODULE)那么没有如何问题,但是如果把它当做一个指针(LoadLibrary把它当做一个char*),它就必须指向远程进程中的内存数据。

     第一个问题其实已经迎刃而解了,因为LoadLibrary和FreeLibrary都是存在于kernel32.dll中的函数,而kernel32可以保证任何“正常”进程中都存在,且其加载地址都是一样的。(参看附录A)于是LoadLibrary/FreeLibrary在任何进程中的地址都是一样的,这就保证了传递给远程进程的指针是个有效的指针。

     第二个问题也很简单:把DLL的文件名(LodLibrary的参数)用WriteProcessMemory复制到远程进程。

     所以,使用CreateRemoteThread和LoadLibrary技术的步骤如下:
     1. 得到远程进程的HANDLE(使用OpenProcess)。
     2. 在远程进程中为DLL文件名分配内存(VirtualAllocEx)。
     3. 把DLL的文件名(全路径)写到分配的内存中(WriteProcessMemory)
     4. 使用CreateRemoteThread和LoadLibrary把你的DLL映射近远程进程。
     5. 等待远程线程结束(WaitForSingleObject),即等待LoadLibrary返回。也就是说当我们的DllMain(是以DLL_PROCESS_ATTACH为参数调用的)返回时远程线程也就立即结束了。
     6. 取回远程线程的结束码(GetExitCodeThtread),即LoadLibrary的返回值――我们DLL加载后的基地址(HMODULE)。
     7. 释放第2步分配的内存(VirtualFreeEx)。
     8. 用CreateRemoteThread和FreeLibrary把DLL从远程进程中卸载。调用时传递第6步取得的HMODULE给FreeLibrary(通过CreateRemoteThread的lpParameter参数)。
     9. 等待线程的结束(WaitSingleObject)。

     同时,别忘了在最后关闭所有的句柄:第4、8步得到的线程句柄,第1步得到的远程进程句柄。

     现在我们看看LibSpy的部分代码,分析一下以上的步骤是任何实现的。为了简单起见,没有包含错误处理和支持Unicode的代码。
HANDLE hThread;
char     szLibPath[_MAX_PATH];   // “LibSpy.dll”的文件名
                                // (包含全路径!);
void*    pLibRemote;              // szLibPath 将要复制到地址
DWORD    hLibModule;    //已加载的DLL的基地址(HMODULE);
HMODULE hKernel32 = ::GetModuleHandle(“Kernel32”);

//初始化 szLibPath
//…

// 1. 在远程进程中为szLibPath 分配内存
// 2. 写szLibPath到分配的内存
pLibRemote = ::VirtualAllocEx( hProcess, NULL, sizeof(szLibPath),
                                MEM_COMMIT, PAGE_READWRITE );
::WriteProcessMemory( hProcess, pLibRemote, (void*)szLibPath,
                       sizeof(szLibPath), NULL );

// 加载 “LibSpy.dll” 到远程进程
// (通过 CreateRemoteThread & LoadLibrary)
hThread = ::CreateRemoteThread( hProcess, NULL, 0,
             (LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,
                                        “LoadLibraryA” ),
              pLibRemote, 0, NULL );
::WaitForSingleObject( hThread, INFINITE );

//取得DLL的基地址
::GetExitCodeThread( hThread, &hLibModule );

//扫尾工作
::CloseHandle( hThread );
::VirtualFreeEx( hProcess, pLibRemote, sizeof(szLibPath), MEM_RELEASE );

我们放在DllMain中的真正要注入的代码(比如为SendMessage)现在已经被执行了(由于DLL_PROCESS_ATTACH),所以现在可以把DLL从目的进程中卸载了。

// 从目标进程卸载LibSpu.dll
// (通过 CreateRemoteThread & FreeLibrary)
hThread = ::CreateRemoteThread( hProcess, NULL, 0,
             (LPTHREAD_START_ROUTINE) ::GetProcAddress( hKernel32,
                                        “FreeLibrary” ),
             (void*)hLibModule, 0, NULL );
::WaitForSingleObject( hThread, INFINITE );

// 扫尾工作
::CloseHandle( hThread );

进程间通讯
     到目前为止,我们仅仅讨论了任何向远程进程注入DLL,然而,在多数情况下被注入的DLL需要和你的程序以某种方式通讯(记住,那个DLL是被映射到远程进程中的,而不是在你的本地程序中!)。以密码间谍为例:那个DLL需要知道包含了密码的的控件的句柄。很明显,这个句柄是不能在编译期间硬编码(hardcoded)进去的。同样,当DLL得到密码后,它也需要把密码发回我们的程序。

     幸运的是,这个问题有很多种解决方案:文件映射(Mapping),WM_COPYDATA,剪贴板等。还有一种非常便利的方法#pragma data_seg。这里我不想深入讨论因为它们在MSDN(看一下Interprocess Communications部分)或其他资料中都有很好的说明。我在LibSpy中使用的是#pragma data_seg。

     你可以在本文章的开头找到LibSpy及源代码的下载链接。

Ⅲ.CreateRemoteThread和WriteProcessMemory技术
示例程序:WinSpy

     另一种注入代码到其他进程地址空间的方法是使用WriteProcessMemory API。这次你不用编写一个独立的DLL而是直接复制你的代码到远程进程(WriteProcessMemory)并用CreateRemoteThread执行之。

     让我们看一下CreateRemoteThread的声明:
HANDLE CreateRemoteThread(
   HANDLE hProcess,         // handle to process to create thread in
   LPSECURITY_ATTRIBUTES lpThreadAttributes,   // pointer to security
                                              // attributes
   DWORD dwStackSize,       // initial thread stack size, in bytes
   LPTHREAD_START_ROUTINE lpStartAddress,      // pointer to thread
                                              // function
   LPVOID lpParameter,      // argument for new thread
   DWORD dwCreationFlags,   // creation flags
   LPDWORD lpThreadId       // pointer to returned thread identifier
);

和CreateThread相比,有一下不同:

●增加了hProcess参数。这是要在其中创建线程的进程的句柄。
●CreateRemoteThread的lpStartAddress参数必须指向远程进程的地址空间中的函数。这个函数必须存在于远程进程中,所以我们不能简单地传递一个本地ThreadFucn的地址,我们必须把代码复制到远程进程。
●同样,lpParameter参数指向的数据也必须存在于远程进程中,我们也必须复制它。

     现在,我们总结一下使用该技术的步骤:

     1. 得到远程进程的HANDLE(OpenProcess)。
     2. 在远程进程中为要注入的数据分配内存(VirtualAllocEx)、
     3. 把初始化后的INJDATA结构复制到分配的内存中(WriteProcessMemory)。
     4. 在远程进程中为要注入的数据分配内存(VirtualAllocEx)。
     5. 把ThreadFunc复制到分配的内存中(WriteProcessMemory)。
     6. 用CreateRemoteThread启动远程的ThreadFunc。
     7. 等待远程线程的结束(WaitForSingleObject)。
     8. 从远程进程取回指执行结果(ReadProcessMemory 或 GetExitCodeThread)。
     9. 释放第2、4步分配的内存(VirtualFreeEx)。
     10. 关闭第6、1步打开打开的句柄。

     另外,编写ThreadFunc时必须遵守以下规则:
     1. ThreadFunc不能调用除kernel32.dll和user32.dll之外动态库中的API函数。只有kernel32.dll和user32.dll(如果被加载)可以保证在本地和目的进程中的加载地址是一样的。(注意:user32并不一定被所有的Win32进程加载!)参考附录A。如果你需要调用其他库中的函数,在注入的代码中使用LoadLibrary和GetProcessAddress强制加载。如果由于某种原因,你需要的动态库已经被映射进了目的进程,你也可以使用GetMoudleHandle代替LoadLibrary。同样,如果你想在ThreadFunc中调用你自己的函数,那么就分别复制这些函数到远程进程并通过INJDATA把地址提供给ThreadFunc。
     2. 不要使用static字符串。把所有的字符串提供INJDATA传递。为什么?编译器会把所有的静态字符串放在可执行文件的“.data”段,而仅仅在代码中保留它们的引用(即指针)。这样,远程进程中的ThreadFunc就会执行不存在的内存数据(至少没有在它自己的内存空间中)。
     3. 去掉编译器的/GZ编译选项。这个选项是默认的(看附录B)。
     4. 要么把ThreadFunc和AfterThreadFunc声明为static,要么关闭编译器的“增量连接(incremental linking)”(看附录C)。
     5. ThreadFunc中的局部变量总大小必须小于4k字节(看附录D)。注意,当degug编译时,这4k中大约有10个字节会被事先占用。
     6. 如果有多于3个switch分支的case语句,必须像下面这样分割开,或用if-else if代替:

switch( expression ) {
     case constant1: statement1; goto END;
     case constant2: statement2; goto END;
     case constant3: statement2; goto END;
}
switch( expression ) {
     case constant4: statement4; goto END;
     case constant5: statement5; goto END;
     case constant6: statement6; goto END;
}
END:
(参考附录E)

     如果你不按照这些游戏规则玩的话,你注定会使目的进程挂掉!记住,不要妄想远程进程中的任何数据会和你本地进程中的数据存放在相同内存地址!(参看附录F)
(原话如此:You will almost certainly crash the target process if you don’t play by those rules. Just remember: Don’t assume anything in the target process is at the same address as it is in your process.)

GetWindowTextRemote(A/W)

     所有取得远程edit中文本的工作都被封装进这个函数:GetWindowTextRemote(A/W):
int GetWindowTextRemoteA( HANDLE hProcess, HWND hWnd, LPSTR   lpString );
int GetWindowTextRemoteW( HANDLE hProcess, HWND hWnd, LPWSTR lpString );

参数:
hProcess
目的edit所在的进程句柄
hWnd
目的edit的句柄
lpString
接收字符串的缓冲

返回值:
成功复制的字符数。

     让我们看以下它的部分代码,特别是注入的数据和代码。为了简单起见,没有包含支持Unicode的代码。

INJDATA

typedef LRESULT      (WINAPI *SENDMESSAGE)(HWND,UINT,WPARAM,LPARAM);

typedef struct {    
     HWND hwnd;                     // handle to edit control
     SENDMESSAGE   fnSendMessage;    // pointer to user32!SendMessageA

     char psText[128];     // buffer that is to receive the password
} INJDATA;

     INJDATA是要注入远程进程的数据。在把它的地址传递给SendMessageA之前,我们要先对它进行初始化。幸运的是unse32.dll在所有的进程中(如果被映射)总是被映射到相同的地址,所以SendMessageA的地址也总是相同的,这也保证了传递给远程进程的地址是有效的。

ThreadFunc

static DWORD WINAPI ThreadFunc (INJDATA *pData)
{
     pData->fnSendMessage( pData->hwnd, WM_GETTEXT,     // 得到密码
                           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)
{
}

ThreadFunc是远程线程实际执行的代码。
     ●注意AfterThreadFunc是如何计算ThreadFunc的代码大小的。一般地,这不是最好的办法,因为编译器会改变你的函数中代码的顺序(比如它会把ThreadFunc放在AfterThreadFunc之后)。然而,你至少可以确定在同一个工程中,比如在我们的WinSpy工程中,你函数的顺序是固定的。如果有必要,你可以使用/ORDER连接选项,或者,用反汇编工具确定ThreadFunc的大小,这个也许会更好。

如何用该技术子类(subclass)一个远程控件
示例程序:InjectEx

     让我们来讨论一个更复杂的问题:如何子类属于其他进程的一个控件?

     首先,要完成这个任务,你必须复制两个函数到远程进程:
     1. ThreadFunc,这个函数通过调用SetWindowLong API来子类远程进程中的控件,
     2. NewProc, 那个控件的新窗口过程(Window Procedure)。

     然而,最主要的问题是如何传递数据到远程的NewProc。因为NewProc是一个回调(callback)函数,它必须符合特定的要求(译者注:这里指的主要是参数个数和类型),我们不能再简单地传递一个INJDATA的指针作为它的参数。幸运的我已经找到解决这个问题的方法,而且是两个,但是都要借助于汇编语言。我一直都努力避免使用汇编,但是这一次,我们逃不掉了,没有汇编不行的。

解决方案1
看下面的图片:

不知道你是否注意到了,INJDATA紧挨着NewProc放在NewProc的前面?这样的话在编译期间NewProc就可以知道INJDATA的内存地址。更精确地说,它知道INJDATA相对于它自身地址的相对偏移,但是这并不是我们真正想要的。现在,NewProc看起来是这个样子:
static LRESULT CALLBACK NewProc(
   HWND hwnd,        // handle to window
   UINT uMsg,        // message identifier
   WPARAM wParam,    // first message parameter
   LPARAM lParam )   // second message parameter
{
     INJDATA* pData = (INJDATA*) NewProc;   // pData 指向
                                           // NewProc;
     pData–;               // 现在pData指向INJDATA;
                           // 记住,INJDATA 在远程进程中刚好位于
                           // NewProc的紧前面;

     //—————————–
     // 子类代码
     // ……..
     //—————————–

     //调用用来的的窗口过程;
     // fnOldProc (由SetWindowLong返回) 是被ThreadFunc(远程进程中的)初始化
     // 并且存储在远程进程中的INJDATA里的;
     return pData->fnCallWindowProc( pData->fnOldProc,
                                     hwnd,uMsg,wParam,lParam );
}

     然而,还有一个问题,看第一行:
INJDATA* pData = (INJDATA*) NewProc;

     pData被硬编码为我们进程中NewProc的地址,但这是不对的。因为NewProc会被复制到远程进程,那样的话,这个地址就错了。

     用C/C++没有办法解决这个问题,可以用内联的汇编来解决。看修改后的NewProc:

static LRESULT CALLBACK NewProc(
   HWND hwnd,       // handle to window
   UINT uMsg,       // message identifier
   WPARAM wParam,   // first message parameter
   LPARAM lParam ) // second message parameter
{
     // 计算INJDATA 的地址;
     // 在远程进程中,INJDATA刚好在
     //NewProc的前面;
     INJDATA* pData;
     _asm {
         call     dummy
dummy:
         pop      ecx          // <- ECX 中存放当前的EIP
         sub      ecx, 9       // <- ECX 中存放NewProc的地址
         mov      pData, ecx
     }
     pData–;

     //—————————–
     // 子类代码
     // ……..
     //—————————–

     // 调用原来的窗口过程
     return pData->fnCallWindowProc( pData->fnOldProc,
                                     hwnd,uMsg,wParam,lParam );
}

Read: 768

windows C语言网络编程快速入门

Windows下C语言网络编程快速入门

  
C语言的学习,一般的方式是,先学C,然后是C++,最好还要有汇编语言和微机原理基础,然后才是Visual C++。这样的方式,对学习者来说,要花费很多时间和耐力。而在学校教学中,也没有时间深入学习Windows编程的实用技术了。

其实,具有了C语言基础后,再有一些基本的C++类的概念,就可以直接学习Windows C编程了。

一、走近Windows C语言

很多语言都把显示一个“Hello,World!”做为第一个入门程序, C语言的第一个程序是这样的:

#include<stdio.h>
main()
{
printf(“Hello,World!”);
}

如果把main函数写成带参数的main函数,应该是:

#include<stdio.h>
main(int arge,char *argv[])
{
printf(“Hello,World!”);
}

Windows C的第一个程序和这个程序在形式和原理上都是一致的,只是有两点不同:

1. 主函数接收的形参不只是命令行中的字符串的个数和字符串的首地址。

2. C语言的很多函数在Windows C中都可以继续使用,但象printf()屏幕显示等函数就不能继续使用了。因为Windows是多任务操作系统,屏幕已不再为某一个应用程序所独有,Windows C应用程序要显示字符串,需要使用Windows提供的API函数,开自己的窗口

下面是一个最简单的,显示“Hello,World!”的Windows C程序:

#include<windows.h>
APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,
LPSTR lpCmdLine,int nCmdShow)
{
MessageBox(NULL,"Hello,World!","第一个Windows C程序",MB_OK|MB_ICONASTERISK);
}

主函数的形参有四个:

1) Hinstance:接收程序运行时当前实例的句柄;
2) HprivInstance:前一个实例的句柄;
3) LpCmdLine:程序命令行指针;
4) NcmdShow:一个用来指定窗口显示方式的整数。

这几个参数的使用我们会在深入的学习中介绍的。

显示Hello,Word!字符串,我们使用了一个MessageBox函数,这个函数会在屏幕上显示一个对话框,它的原型是:

int MessageBox(HWND hWnd,LPCTSTR lpText,LPCTSTR lpCaption,UNIT uType)

四个参数分别是:

1) HWnd:父窗口的句柄;
2) LpText:要显示字符串的指针;
3) LpCaption:对话框标题字符串的指针;
4) UType:显示在对话框上的小图标的类型。

使用这个函数要包含windows.h头文件。

调试一下,怎么样?窗口上弹出了一个“第一个Windows C程序”对话框,上面有一行字:“Hello,World!”。

世界真的很美好啊!!

深入编程:

在C语言中,函数的声明,如果没有指明返回值类型,缺省值为void,这个程序的主函数就没有返回值。不过,在Windows编程时,我们最好养成个好习惯,指明函数的返回值类型,因为在C++中,函数返回值类型是不可以缺省的。而我们在Windows C编程时,还是会用到C++的一些概念,这样做,有利于以后深入地学习。

规范一点的程序应该是这样的:

#include<windows.h>
int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,
LPSTR lpCmdLine,int nCmdShow)
{
MessageBox(NULL,"Hello,World!","第一个Windows C程序",MB_OK|MB_ICONASTERISK);
return 0;
}

这里,我们声明的类型为int型,并且返回一个值0,这样的函数就可以使用在复杂一点的函数调用中了。

在这一节中,我们有几处都提到了句柄的概念,句柄和指针的概念不同,它是作为操作系统内部索引表中的一个值来使用的,这样可以防止应用程序直接访问名对象的内部结构,体现了Windows资源管理的优越性。譬如说,一个窗口找开之后,好对应内存中的一个内存块,这个窗口所在的内存快地址往往会由操作系统做动态的调整,但其却不会随之变化。不过,通过它可以访问这个窗口,所以在使用的时候,可以把它当做指针一样看待。
二、 获取本地计算机的主机名和IP地址

和C语言一样,函数是Windows C编程的最基本的单位。不过,Windows C主要使用API函数,而网络编程则主要使用Winsock提供的API函数。

Winsock是90年代初,为了方便网络编程,由Microsoft联合了其他几家公司共同制定的一套WINDOWS下的网络编程接口,它是通过C语言的动态链接库方式提供给用户及软件开发者的,主要由winsock.h头文件和动态链接库winsock.dll组成,目前有两个版本:Winsock1.1和Winsock2.0。

在Win32平台上,访问众多的基层网络协议,Winsock是首选接口。

用Visual C++6.0编译Windows C程序,使用Winsock API函数时,首先要把wsock32.lib添加到它的库模块中,否刚在链接的时候,会出现“error LNK2001”错误。添加wsock32.lib的具体步骤是:打开工程菜单,选择设置,在弹出的Project settings对话框中,点击link选项卡,然后在对象/库模块文本框中添加wsock32.lib。

最简单的网络编程是获取本机的主机名和IP地址,这个程序使用了WSAStart()、WSAClenaup()、gethostname()、gethostbyname()四个winsock API函数,这四个函数的功能和使用方法介绍如下:

1. WSAStartup():

【函数原型】

int PASCAL FAR WSAStartup(WORD wVersionRequired, LPWSADATA lpWSAData);

【使用说明】

每一个使用winsock的应用程序,都必须进行WSAStart函数调用,并且只有在调用成功之后才能使用其它的winsock网络操作函数。

WVersionRequired:<输入>表示欲使用的Winsock版本,这是一个WORD类型的整数,它的高位字节定义的是次版本号,低位字节定义的是主版本号。

LpWSAData:<输出>是一个指向WSADATA资料的指针。这个资料我们一般不使用。

返回值:调用成功返回0;否则,返回出错信息。

2. WSAClenaup():

【函数原型】

int PASCAL FAR WSACleanup(void);

【使用说明】

winsock使用后,要调用WSACleanup函数关闭网络设备,以便释放其占用的资源。

3.gethostname()

【函数原型】

int PASCAL FAR gethostname (char FAR * name, int namelen);

【使用说明】

该函数可以获取本地主机的主机名,其中:

name:<输出>用于指向所获取的主机名的缓冲区的指针。

Namelen:<输入>缓冲区的大小,以字节为单位。

返回值:若无错误,返回0;否则,返回错误代吗。

4.gethostbyname()

【函数原型】

struct hostent FAR * PASCAL FAR gethostbyname(const char FAR * name);

【使用说明】

该函数可以从主机名数据库中得到对应的“主机”。

该函数唯一的参数name就是前面调用函数gethostname()得到的主机名。若无错误,刚返回一个指向hostent结构的批针,它可以标识一个“主机”列表。

Hostent结构定义如下:

Struct hostent
{
char FAR * h_name;
char FAR FAR ** h_aliases;
short h_addrtype;
char FAR FAR ** h_addr_list;
}

其中:

h_name:<输入>主机名地址(PC)。
h_aliases:一个由主机备用名组成的空中止数组。
H_addrtype:返回地址的类型,对于Winsock,这个域总是PF_INET。
H_lenth:每个地址的长度(字节数),对应于PF_INET域应该为4。
H_addr_list:应该以空指针结尾的主机地址的列表,返回的地址是以网络顺序排列的。

其中,h_addr_list[0]存放的就是本地主机的4个字节的IP地址,即:

h_addr_list[0][0].h_addr_list[0][1].h_addr_list[0][2].h_addr_list[0][3]

一个简单的用消息框显示主机名和IP地址的源程序如下:

#include<winsock.h>

int WSA_return;
WSADATA WSAData;

HOSTENT *host_entry;
char host_name[256];
char host_address[256];

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,
LPSTR lpCmdLine,int nCmdShow)
{
WSA_return=WSAStartup(0x0101,&WSAData);

if(WSA_return==0)
{
gethostname(host_name,256);
host_entry=gethostbyname(host_name);
if(host_entry!=0)
{
wsprintf(host_address,"%d.%d.%d.%d",
(host_entry->h_addr_list[0][0]&0x00ff),
(host_entry->h_addr_list[0][1]&0x00ff),
(host_entry->h_addr_list[0][2]&0x00ff),
(host_entry->h_addr_list[0][3]&0x00ff));

MessageBox(NULL,host_address,host_name,MB_OK);
}
}
WSACleanup();
return 0;
}

深入编程:

前面显示IP地址的时候,我们使用的是消息框,规范一点的编程应该使用对话框,如何编辑一个对话框,很多书中都有介绍,编辑的对话框可参考图5的运行界面。

头文件Get_IP.h如下:

BOOL APIENTRY Hostname_ipDlgPro(HWND hDlg,UINT message,WPARAM wParam,LPARAM lParam);

这个程序只使用了一个对话框过程,一般把这个过程的声明放在头文件中。

源程序Get_IP.c:

#include<winsock2.h>
#include"Get_IP.h"
#include"resource.h" //这个头文件在创建资源的时候会自动生成,
//并会在插入资源时自动生成控件标识号.
int WSA_return;
WSADATA WSAData;

HOSTENT *host_entry;
char host_name[256];
char host_address[256];

int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,
LPSTR lpCmdLine,int nCmdShow)
{
WSA_return=WSAStartup(0x0101,&WSAData);
if(WSA_return==0)
{
gethostname(host_name,256);
host_entry=gethostbyname(host_name);
if(host_entry!=0)
{
wsprintf(host_address,"%d.%d.%d.%d",
(host_entry->h_addr_list[0][0]&0x00ff),
(host_entry->h_addr_list[0][1]&0x00ff),
(host_entry->h_addr_list[0][2]&0x00ff),
(host_entry->h_addr_list[0][3]&0x00ff));
}
}
WSACleanup();
DialogBox(hInstance,"DIALOG1",NULL,(DLGPROC)Hostname_ipDlgPro);
return 0;
}

BOOL APIENTRY Hostname_ipDlgPro(HWND hDlg,UINT message,
WPARAM wParam,LPARAM lParam)
{
switch(message)
{
case WM_INITDIALOG:
return(TRUE);
case WM_COMMAND:
if(LOWORD(wParam)==IDOK)
{
SetDlgItemText(hDlg,IDC_EDIT1,host_name);
SetDlgItemText(hDlg,IDC_EDIT2,host_address);
SetDlgItemText(hDlg,IDCANCEL,"确定");
}
if(LOWORD(wParam)==IDCANCEL)
EndDialog(hDlg,TRUE);
return(TRUE);
break;
}
return(FALSE);
}

Read: 701

全力提升 TIniFile 的读取速度

全力提升 TIniFile 的读取速度

软件保存设置一般是写入本地目录,或者写入注册表。前者使用的文件格式很多,多为 IniFile (现在 XML 也开始流行)。后者是将信息储存在注册表中,并通过的 TRegistry 或者标准的 WindowsAPI 来读写。后者有个很大的好处:不需要对多用户系统进行考虑,Windows 可以很好的控制多用户的切换。但缺点是往往卸载软件不将注册表中生成的记录信息删除干净。而制作绿色软件的作者多采用 IniFile 方式保存信息。

IniFile 本来也没有什么好多说的,有现成的工具 TIniFile,也有标准的 WindowsAPI。不过偶然发现当读取 IniFile 的一整个记录较多的 Section 时,性能很差。根据我的测试,当一个 Section 有超过 61 条记录的时候,比直接用 TStringList 打开一个文件,并分析出 Section 的记录要慢。问题出在哪里呢?我估计可能是大量的时间被耗费在打开文件和记录定位上了。

设想一下:如果我们将带有 600 条记录的语言文件保存在 IniFile 中。用上述2种方法在速度上的差异甚大。我在 GOSURF 浏览器中做了比较。使用标准的 TIniFile 读取需要 2.78 秒,而使用了利用 TStringList 之后,时间缩短为 0.102秒。这 2 秒的时间对于一个软件的启动来说,算是相当长的了。

随后我对 TIniFile 的各种操作进行了测试,发现 ReadSectionValues 是有性能明显性能瓶颈的。于是动手改造了一番:

procedure TTntIniFile.ReadSectionValuesEx(const Section: WideString; Strings: TTntStrings);

var

BeginIdx, EndIdx: Integer;

KeyValues: TTntStringList;

I: Integer;

S: WideString;

begin

KeyValues := TTntStringList.Create;

try

    KeyValues.LoadFromFile(FileName);

    BeginIdx := 0;

    while (BeginIdx <= KeyValues.Count – 1) do

    begin

      if Trim(KeyValues[BeginIdx]) = ‘[‘ + Section + ‘]’ then Break;

      Inc(BeginIdx);

    end;

    EndIdx := BeginIdx + 1;

    while (EndIdx <= KeyValues.Count – 1) do

    begin

      S := Trim(KeyValues[EndIdx]);

      if (S <> ”) and (S[1] = ‘[‘) and (S[Length(S)] = ‘]’) then Break;

      Inc(EndIdx);

    end;

    Strings.BeginUpdate;

    try

      for I := BeginIdx + 1 to EndIdx – 1 do

        if KeyValues[I] <> ” then

           Strings.Add(KeyValues[I]);

    finally

      Strings.EndUpdate;

    end;

finally

    KeyValues.Free;

end;

end;

注1:如果没有安装过 TntControls 的朋友,只要将 WideString 改成 string,将 TTnt 开头的对象都改成 T 即可。

注2:第二个参数 Strings: TTntStrings 是抽象类。你可以使用 TStringList,不过我推荐使用 THashedStringList。当记录数量很大的时候搜索某条记录 (如:Strings.Values[‘LastRecode’]) 可能也相当耗时。不使用 THashedStringList 那 ReadSectionValuesEx 的优势无法完全体现。

另外,DELPHI 中还提供了一种 TMemIniFile。原理是一次性将整个 IniFile 文件读入内存,以提高响应速度。但灵活性和稳定性不如使用 TIniFile + ReadSectionValuesEx 来的好。当然将常用的调用单条记录制作成函数会更加灵活方便。如 IniReadString(const FileName, Section, Ident, Default: WideString): WideString; 这些函数就请你自己扩充了 :D

最后说一下 XML。它的性质和 IniFile 差不多。不过他的结构更多样化,你可以轻松把一个 Object 保存在文件中,阅读也比较直观。我把 IniFile 文件看成简单的一层目录结构,那 XML 是多层目录结构。当然分析一个 XML 也相对会稍慢一些。待有时间,再向大家推荐好用高效的 TXMLParser。

Read: 1270

VC小技巧15则

一、 一次只运行一个程序实例
下列两种方式都可以实现,建议采用第二种方式:
1、 if( FindWindow(NULL,"程序标题"))
         exit(0);
2、BOOL CDemoTBarEApp::InstanceIsRun()
{
HANDLE m_hMutex;
m_hMutex = ::CreateMutex(NULL, TRUE, _T("YourApplication"));
ASSERT(m_hMutex);
if (GetLastError() == ERROR_ALREADY_EXISTS)
{
m_hMutex = NULL;
return TRUE;//实例已经运行
}
return FALSE;//实例未运行
}

二、 装载光标
SetCursor(AfxGetApp()->LoadStandardCursor(IDC_WAIT));
其中::SetCursor()是全局函数,用来设置整个例程的光标参数是宏定义光标句柄。AfxGetApp ()是一个系统函数,它返回当前的一个CWinApp对象。其成员函数LoadStandardCursor()用来读取一个系统指针,每一种系统指针的具体宏定义如下:
IDC_APPSTARTING 带小沙漏的标准箭头
IDC_ARROW 标准箭头
IDC_CROSS 十字光标(用于定位)
IDC_HAND Windows 2000:手型
IDC_HELP 带问号的箭头
IDC_IBEAM I型标
IDC_ICON Obsolete for applications marked version 4.0 or later.
IDC_NO   禁止符号
IDC_SIZE Obsolete for applications marked version 4.0 or later. Use IDC_SIZEALL.
IDC_SIZEALL 十字箭头
IDC_SIZENESW 指向东北和西南的双向箭头
IDC_SIZENS 指向南和北的双向箭头
IDC_SIZENWSE 指向西北和东南的双向箭头
IDC_SIZEWE 指向东西的双向箭头
IDC_UPARROW 上箭头
IDC_WAIT 沙漏

三、获得主框架:
CMainFrame * pMainframe = (CMainFrame *) AfxGetApp()->m_pMainWnd;
.获取应用程序的实例句柄:
      Example: HANDLE hInstance=AfxGetInstanceHandle();

获得应用程序主窗口的指针:
      Example: AfxGetMainWnd() ->ShowWindow(SW_SHOWMAXMIZED); //使程序最大化

四、重新建立字体的代码
if(m_fontLogo.m_hObject)
m_fontLogo.Detach();

m_fontLogo.CreateFont(nHeight, 0, 0, 0, nWeight, bItalic, bUnderline,0,0,0,0,0,0, Name);

五、用指定颜色填充区域
dc.FillSolidRect(rect, ::GetSysColor(COLOR_3DFACE));

六、绘制立体字体效果的字体,很值得一看
void CTestView::OnPaint()
{
CPaintDC dc(this); // device context for painting

CRect rect;
GetWindowRect(rect);

CFont m_fontLogo;
m_fontLogo.CreateFont(24, 0, 0, 0, FW_BOLD, true,
FALSE,0,0,0,0,0,0, "Arial");
CString m_LogoText;
m_LogoText=_T("Benlux Pro3D System");
dc.SetBkMode(TRANSPARENT);

CFont * OldFont = dc.SelectObject(&m_fontLogo);

// draw text in DC
COLORREF OldColor = dc.SetTextColor( ::GetSysColor( COLOR_3DHILIGHT));

rect.right = rect.Width();
rect.bottom = rect.Height();
rect.left = rect.top = 0;
dc.FillSolidRect(rect, ::GetSysColor(COLOR_3DFACE));

dc.DrawText( m_LogoText, rect + CPoint(1,1), DT_SINGLELINE | DT_LEFT | DT_VCENTER);
dc.SetTextColor( ::GetSysColor( COLOR_3DSHADOW));
dc.DrawText( m_LogoText, rect, DT_SINGLELINE | DT_LEFT | DT_VCENTER);

// restore old text color
dc.SetTextColor( OldColor);
// restore old font
dc.SelectObject(OldFont);
// Do not call CView::OnPaint() for painting messages
}

七、简单的消息检索和抽取函数,能够让系统响应其它操作
BOOL PeekAndPump()
{
static MSG msg;

while (::PeekMessage(&msg,NULL,0,0,PM_NOREMOVE)) {
if (!AfxGetApp()->PumpMessage()) {
   ::PostQuitMessage(0);
   return FALSE;
}
}
return TRUE;
}

八、在你的程序中用动画光标替换默认的等待光标 (ANI光标的使用)
HCURSOR m_hAniCursor=NULL;
BeginWaitCursor();   //begin wait cursor for api function

//load ani cursor from file in current path
TCHAR cursorPath[MAX_PATH]; GetModuleFileName(NULL,cursorPath,MAX_PATH);
char drive[_MAX_DRIVE];
char dir[_MAX_DIR];
char fname[_MAX_FNAME];
char ext[_MAX_EXT];
_splitpath(cursorPath, drive, dir, fname, ext );
sprintf(cursorPath,"%s%swait.ani",drive,dir); //ani cursor file name is wait.ani

m_hAniCursor= LoadCursorFromFile(cursorPath);
HCURSOR oldCursor;
if(m_hAniCursor != NULL)
oldCursor=SetCursor(m_hAniCursor);

for(long i=0;i<1000;i++)
Sleep(5);

oldCursor=NULL;
m_hAniCursor=NULL;
EndWaitCursor(); //end wait cursor for api function

九、如何限制编辑框中的准许字符
     如果用户在编辑控件中只允许接收数字,可以使用一个标准的编辑控件并指
定新的创建标志ES_NUMBERS,它是Windows 95新增加的标志,该标志限制 编辑控
件只按收数字字符。
如果用户需要复杂的编辑控件,可以使用Microsoft 的屏蔽编辑控件,它是一个很有用的OLE定制控件。
    如果希望不使用OLE 定制控件自己处理字符,可以派生一个CEdit 类并处理WM_CHAR消息,然后从编辑控件中过滤出特定的字符。首先,使用ClassWizard 建立一个 CEdit的派生类,其次,在对话类中指定一个成员变量将编辑控件分类在OnInitdialog 中调用CWnd: : SubclassDlgItem .

//In your dialog class declaration (.H file )
private :
    CMyEdit m_wndEdit ; // Instance of your new edit control .

//In you dialog class implementation (.CPP file )
BOOL CSampleDialog : : OnInitDialog ( )
{

    //Subclass the edit lontrod .
    m_wndEdit .SubclassDlgItem (IDC_EDIT,this );
    …
}
    使用ClassWizard处理WM_CHAR消息,计算nChar参量并决定所执行的操作,用户可以确定是否修改、传送字符。下例说明了如何显示字母字符,如果字符是字母字符,则调用CWnd ; OnChar,否则不调用OnChar.
//Only display alphabetic dharacters .
void CMyEdit : : OnChar (UINT nChar , UINT nRepCnt , UITN nFlags )
{
    //Determine if nChar is an alphabetic character .
    if (: : IsCharAlpha ( ( TCHAR) nChar ) )
        CEdit : : OnChar (nChar, nRepCnt , nFlags );
}
    如果要修改字符,则不能仅仅简单地用修改过的nChar调用CEdit : : OnChar。要修改一个字符,需要首先修改nChar,然后用修改过的nChar调用CWnd: : DefWindowProc。下例说明了如何将字符转变为大写:
//Make all characters uppercase
void CMyEdit : : OnChar (UINT nChar , UINT nRepCnt , UINT nFlags )
{
    //Make sure character is uppercase .
    if (: : IsCharAlpha ( .( TCHAR) nChar)
         nChar=: : CharUpper (nChar ) ;
    //Bypass default OnChar processing and directly call default window proc.
    DefWindProc (WM_CHAR, nChar , MAKELPARAM (nRepCnt , nFlags )) ;
}

十、串太长时如何在其末尾显示一个省略号
    调用CDC:: DrawText并指定DT_END_ELLIPSIS标志,这样就可以用小略号取代串末尾的字符使其适合于指定的边界矩形。如果要显示路径信息,指定DT_END_ELLIPSIS标志并省略号取代串中间的字符。
void CSampleView:: OnDraw (CDC* pDC)
{
     CTestDoc* pDoc=GetDocument ();
     ASSERT_VALID (pDoc);
                                                                                   
     //Add ellpsis to end of string if it does not fit
     pDC->Drawtext (CString ("This is a long string"),
         CRect (10, 10, 80, 30), DT_LEFT | DT_END_ELLIPSIS);

     //Add ellpsis to middle of string if it does not fit
     pDC->DrawText (AfxgetApp () ->m_pszhelpfilePath,
         CRect (10, 40, 200, 60), DT_LEFT | DT_PATH_ELLIPSIS);
}

十一、如何实现一个橡皮区矩形(具有踪迹矩形并可移动、缩放的矩形)
     CRectTracker是一个很有用的类,可以通过调用CRectTracker:: TrackRubberBand响应WM_LBUTTONDOWN消息来创建一个橡皮区矩形。下例表明使用CRectTracker移动和重置视窗中的蓝色椭圆的大小是很容易的事情。
    首先,在文档类中声明一个CRectTracker数据成员:
class CTestDoc: Public CDocument
{…
public:
CRectTracker m_tracker;

};
     其次,在文档类的构造函数中初始化CRectTracker 对象:
CTestDoc::CTestDoc()
{
m_tracker.m_rect.SetRect (10, 10, 300, 300);
m_tracker.m_nStyle=CRectTracker:: resizeInside |
CRectTracker:: dottedLine;
}
     然后,在视图类的OnDraw函数中画椭圆和踪迹矩形:
void CTestView::OnDraw(CDC* pDC)
{
CTestDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);

//Select blue brush into device context.
     CBrush brush (RGB (0, 0, 255));
     CBrush* pOldBrush=pDC->SelectObject (&brush);

     //draw ellipse in tracking rectangle.
     CRect rcEllipse;
     pDoc->m_tracker.GetTrueRect (rcEllipse);
     pDC->Ellipse (rcEllipse);

     //Draw tracking rectangle.
     pDoc->m_tracker.Draw (pDC);
     //Select blue brush out of device context.
     pDC->SelectObject(pOldBrush);
}
    最后,视图类中处理WM_LBUTTONDOWN消息,并增加下述代码。该段代码根据鼠标击键情况可以拖放、移动或者重置椭圆的大小。

void CTestView::OnLButtonDown(UINT nFlags, CPoint point)
{
   //Get pointer to document.
     CTestDoc* pDoc=GetDocument();
     ASSERT_VALID (pDoc);

     //If clicked on ellipse, drag or resize it. Otherwise create a
     //rubber-band rectangle nd create a new ellipse.
     BOOL bResult=pDoc->m_tracker.HitTest (point)!=
         CRectTracker::hitNothing;

     //Tracker rectangle changed so update views.
     if (bResult)
     {
        pDoc->m_tracker.Track (this,point,TRUE);
        pDoc->SetModifiedFlag ();
        pDoc->UpdateAllViews (NULL);
     }
     else
        pDoc->m_tracker.TrackRubberBand (this,point,TRUE);

CView::OnLButtonDown(nFlags, point);
}

十二、如何在临时目录创建一个临时文件
如果你要在临时目录下创建临时文件,下面的代码能帮到你的忙。
bool GetuniqueTempName (CString& strTempName)
{
strTempName="";
     //Get the temporary files directory.
     TCHAR szTempPath [MAX_PATH];
     DWORD dwResult=:: GetTempPath (MAX_PATH, szTempPath);
     if (dwResult==0)
   return false;

     //Create a unique temporary file.
     TCHAR szTempFile[MAX_PATH];
     UINT nResult=GetTempFileName (szTempPath, _T ("~ex"),0,szTempFile);
     if (dwResult==0)
   return false;

     strTempName=szTempFile;
return true;
}

十三、如何限制窗口的最小范围
要限制窗体的大小,下面的代码能帮到你的忙。
在CMainFrame中增加WM_GETMAXMININFO消息的处理函数,然后在这个函数中写代码如下:
//限制主窗体的最小高度和宽度
void CMainFrame::OnGetMinMaxInfo(MINMAXINFO FAR* lpMMI)
{
lpMMI->ptMinTrackSize.x=600;
lpMMI->ptMinTrackSize.y=400;
CNewFrameWnd::OnGetMinMaxInfo(lpMMI);
}

十四、怎样删除文件到回收站中
要删除文件到回收站,很简单。只要用SHFileOperation函数就行了,下面的代码我将为你演示了这一个函数的用法。当然你可以直接拷贝到你的项目中。
//删除文件到回收站中
//pszPath : 待删除的全路径文件名
//bDelete : TRUE 删除,不移到回收站,FALSE:移到回收站
一、 //返回    : TRUE 删除成功     FALSE 删除失败
BOOL CDelFileToRecycleDlg::Recycle(LPCTSTR pszPath, BOOL bDelete/*=FALSE*/)
{
SHFILEOPSTRUCT shDelFile;
memset(&shDelFile,0,sizeof(SHFILEOPSTRUCT));
shDelFile.fFlags |= FOF_SILENT;      // don’t report progress
shDelFile.fFlags |= FOF_NOERRORUI;     // don’t report errors
shDelFile.fFlags |= FOF_NOCONFIRMATION;    // don’t confirm delete
// Copy pathname to double-NULL-terminated string.
//
TCHAR buf[_MAX_PATH + 1]; // allow one more character
_tcscpy(buf, pszPath);   // copy caller’s pathname
buf[_tcslen(buf)+1]=0;   // need two NULLs at end

// Set SHFILEOPSTRUCT params for delete operation
shDelFile.wFunc = FO_DELETE;       // REQUIRED: delete operation
shDelFile.pFrom = buf;         // REQUIRED: which file(s)
shDelFile.pTo = NULL;          // MUST be NULL
if (bDelete)
{         // if delete requested..
shDelFile.fFlags &= ~FOF_ALLOWUNDO;    // ..don’t use Recycle Bin
}
else
{           // otherwise..
shDelFile.fFlags |= FOF_ALLOWUNDO;    // ..send to Recycle Bin
}
     return SHFileOperation(&shDelFile);    // do it!
}

十五、内存泄漏检查
    也许你已经知道,在C++和C语言中指针问题也就是内存申请与释放是一个令人头疼的事情,假如你申请了内存,但没有释放,并且你的程序需要长时间地运行,那么,系统的资源将逐渐减少,当系统的资源全部被用完时,系统将会崩溃。所以在开发程序的过程中一定要保证资源的完全释放。下面我们来介绍内存漏洞的检查。
示例如下:
// do your memory allocations and deallocations…
CString s = "This is a frame variable";
#ifdef _DEBUG
CMemoryState oldMemState, newMemState, diffMemState;
oldMemState.Checkpoint();
#endif
// the next object is a heap object
CString* p = new CString( "Smith Alan 581_0215" );
delete p;
p=NULL;
#ifdef _DEBUG
newMemState.Checkpoint();
BOOL b=diffMemState.Difference(oldMemState, newMemState);
if (b)
{
AfxMessageBox( "Memory leaked!n" );
}
#endif
     根据试验,由于我们无法释放掉象int CString char 申请的变量。只能释放指针型的变量。而检测内存时,照样会出现内存泄漏现象。所以,这种内存检测方式局限性还是很大。因为我们无法释放非指针型变量。

Read: 835