Visual Studio 2015编译EncodePointer函数的问题

来源:互联网 发布:网络摄像机方案 编辑:程序博客网 时间:2024/06/06 11:24

        在编程工具方面,我是个偏好使用最新工具的强迫症晚期患者。但对于大我数程序员来讲,采用新工具能够获得最大的编程效率,这也无可非议。Visual Studio 2015是个好东西,尤其我常用来开发驱动程序,可以节省不省时间和精力。但Visual Studio从2010开始就无法编译出可在Windows XP SP1平台上运行的程序了,现在也依然如此。作为商业软件的开发,如果不考虑Windows XP SP1以下的平台,可能有些不妥,尤其如果安全软件的话。所以,我设计了一种解决方法,供大家参考。

 

一、基本方法

        之所以新版Visual Studio编译的程序无法在XPSP1上运行,是因为C运行时和MFC框架中大量使用了EncodePointer和DecodePointer这两个新版Windows上才有的位于kernel32.dll中的API函数。网上的方法大概有三种:

  1、退回老版本的VisualStudio,至少是2008;

  2、安装两个版本的VisualStudio,在新版中采用老版本的编译工具集;

  3、自己设计一套哑函数代替出问题的这两个函数。

        前两种方法实际上是一种。而但三种方法中,在程序的安全性上是有些问题的。EncodePointer和DecodePointer之所以出现,是为了防范利用对象指针进行的攻击。在支持这两个函数的系统上不使用它们似乎也有不妥。于是我想在第三种方法上进行一下改进。

        自己在代码中实现另一套EncodePointer和DecodePointer,是基本思路。但如何让链接器知道链接时该选择哪套函数呢?如果使用C在EXE工程中实现这两个函数,我们就没有任何机会了——链接器总是先链接kernel32.lib而无情地抛弃自己定义的函数。倒是可以自己实现一个包含这两个函数的lib库,并在链接EXE时在“附加依赖项”中将自己的 lib 调到 kernel32.lib 之前(实际上只要将自己的lib放入了“附加依赖项”中,它就会在默认库之前得到链接)。还在

        我采用了网上可以查到的一种方法,在EXE工程中编译一个asm文件。因为汇编语言文件生成的obj文件链接总是先于默认库的。

对了,编译时不要忘记将工程设置中的“所需最低版本”设为5.01。

 

二、x86架构

        x86架构下的asm文件代码如下:

.model flatPUBLIC__imp__EncodePointer@4, __imp__DecodePointer@4.data__imp__EncodePointer@4dd dummy_encode__imp__DecodePointer@4dd dummy_decodeEXTERNDEF__imp__LoadLibraryA@4 : DWORDEXTERNDEF__imp__GetProcAddress@8 : DWORDEXTERNDEF __imp__ShowMsg:DWORD ; CPP 文件中定义 void ShowMsg() 函数kernel32dll db  'kernel32.dll',0encode  db 'EncodePointer',0decode  db 'DecodePointer',0.codedummy proc    mov eax, dword ptr [esp+4]    ret 4dummy endpdummy_encode proc;   call [__imp__ShowMsg]    push offset kernel32dll    call [__imp__LoadLibraryA@4]    test eax, eax    jz @F    push offset encode    push eax    call [__imp__GetProcAddress@8]    test eax, eax    jz @F    mov dword ptr [__imp__EncodePointer@4], eax    jmp eax@@:    mov eax, offset dummy    mov dword ptr [__imp__EncodePointer@4], eax    jmp eaxdummy_encode endpdummy_decode proc……dummy_decode endpend
        代码中省略了dummy_decode的内容,因为它与dummy_encode极其相似。

        首先__imp__EncodePointer@4是我伪造EncodePointer引入表的特殊变量名称,其中存储了自己实现的EncodePointer函数dummy_encode的地址。编译好的EXE的C运行时在调用EncodePointer时就会调用到dummy_encode中来。在这里首先调用LoadLibraryA装载kernel32.dll,然后调用GetProcAddress获得EncodePointer的函数地址。如果成功获得的话,就将该地址放入__imp__EncodePointer@4中并直接跳转到其上继续执行,而下一次调用将会直接转发到真正的EncodePointer上。如果没有EnocdePointer,则将__imp__EncodePointer@4置为dummy并跳转执行,这样下一次调用就不用耗费时间去查询了。通过修改__imp__EncodePointer@4改变EncodePointer调用地址的方法可以有效地适应多线程并发执行的情况。如果dummy_encode有多个线程重入,它们都将去查询真实的EncodePointer地址,“mov dword ptr [__imp__EncodePointer@4], eax”的指令会被多次执行,但因其是原子操作,多次操作不会产生任何副作用。

        调试时可能有些麻烦,所以在注释行中我调用了另一个CPP文件中的void ShowMsg()函数,而其中只有一行__debugbreak()调用。请注意名称上的特定样式,它可以很好的工作!

        还需注意的一点是, Visual Studio 2015似乎不支持应用程序的asm文件编译,需要针对文件采自定义编译的方式进行,编译的命令行如下:

cd /d $(ProjectDir)$(IntDir)ml /c "%(FullPath)"
        输出内容为:

$(ProjectDir)$(IntDir)%(Filename).obj
        另外需要将链接器的 “强制文件输出”选项设置为“仅限多次定义的符号 (/FORCE:MULTIPLE)”。

三、x64架构

  XP SP1居然有64位的!虽然是鸡肋版本,但为了系统的完整兼容,我决定也进行一下尝试。虽然这样做,工程设置的“所需最低版本”至少是6.00,但我猜想它是可以在XP SP1上运行的。我的代码是这样的:

PUBLIC__imp_EncodePointer, __imp_DecodePointer.data__imp_EncodePointerdq dummy_encode__imp_DecodePointerdq dummy_decodeEXTERNDEF__imp_LoadLibraryA : QWORDEXTERNDEF__imp_GetProcAddress : QWORDEXTERNDEF__imp_ShowMsg : QWORD ; CPP 文件中定义 void ShowMsg() 函数kernel32dll db  'kernel32.dll',0encode  db 'EncodePointer',0decode  db 'DecodePointer',0.codedummy proc    mov rax, rcx     retdummy endpdummy_encode proc    mov qword ptr [rsp+8], rcx    sub rsp, 28h                           ;(1);   mov rax, qword ptr [__imp_ShowMsg];   call rax    lea rcx, [kernel32dll];   push rcx                                (*)    mov rax, qword ptr[__imp_LoadLibraryA]    call rax    ;   poprcx                              (*)    test rax, rax    jz @F    lea rdx, [encode];   push rdx                                (*)    mov rcx, rax;   push rcx                                (*)    mov rax, qword ptr [__imp_GetProcAddress]    call rax;   pop rcx                                 (*);   pop rdx                                 (*)    test rax, rax    jz @F    mov qword ptr [__imp_EncodePointer], rax    add rsp, 28h                           ;(2)    mov rcx, qword ptr [rsp+8]    jmp rax@@:    lea rax, [dummy]    mov qword ptr [__imp_EncodePointer], rax    add rsp, 28h                           ;(3)    mov rcx, qword ptr [rsp+8]    jmp raxdummy_encode endpdummy_decode proc……dummy_decode endpend

  与x64架构下非常相似,我就不重复说明了。值得注意的是函数的命名规则略有变化。还有就是x64函数调用约定的情况。x64架构的子函数头四个参数通过rcx、rdx、r8、r9来传递,堆栈也至少要预留20h的空间,再加入为了使返回地址在16字节边界上对齐,需要另加上8字节空间,因此需要28h字节的空间。如果子函数参数多于4个,则空间要更多,且要在16字节边界上对齐,如果子数有5个参数,则总共需要下面这么多的空间:

    30h(参数) + 8h(用于返回地址对齐) = 38h

  最开始因为我对x64函数调用约定的误解(即使传递的参数少于4个,也要至少预留4个参数的位置),我并没有采用(1)、(2)、(3)的方式预留参数空间,而是采用了(*)处更为紧凑的方法。这种方法一般情况下是没有问题的,但在这里则不然。为了排错我耗费了很多时间,这里记录下来好让后来人不要犯同样的错误。

  (*)的方法LoadLibrary和GetProcAddress函数调用完成后总是将dummy_encode的返回地址修改掉,但自信自己的代码没有问题,我逆向了一下LoadLibraryA和GetProcAddress发现,它们的实质代码第一行居然干了这个:

LoadLibraryA:mov     [rsp+10h],rbxGetProcAddress:mov     [rsp+18h],rsi
        rsp+8h是第1个参数的位置,而rsp+10h和rsp+18h分别对应了第2个参数和第3个参数的位置。但LoadLibraryA哪里来的第二个参数,而GetProcAddress又哪里来的第3个呢?它们正对应了dummy_encode的返回地址!访问参数以外的堆栈空间的理由是什么,我实在猜不出,不会是为采用ROP的shellcode编写制造一些麻烦吧?

另外,x64下对asm文件编译的命令行为:

cd /d $(ProjectDir)$(IntDir)ml64 /c "%(FullPath)"


四、总结

  这种方法可以有效地提高Visual Studio 2015编译出来的程序的兼容性,同时避免了安全性上的降低。但也有人认为费了这么大力气才解决这么小的一个问题没有必要。无所谓,通过这个试验长了点儿知识总还是好的!
0 0
原创粉丝点击