您的位置:首页 > 其它

Visual Studio 2015编译EncodePointer函数的问题

2016-04-06 09:59 573 查看
在编程工具方面,我是个偏好使用最新工具的强迫症晚期患者。但对于大我数程序员来讲,采用新工具能够获得最大的编程效率,这也无可非议。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 flat
PUBLIC__imp__EncodePointer@4, __imp__DecodePointer@4
.data
__imp__EncodePointer@4dd dummy_encode
__imp__DecodePointer@4dd dummy_decode
EXTERNDEF__imp__LoadLibraryA@4 : DWORD
EXTERNDEF__imp__GetProcAddress@8 : DWORD
EXTERNDEF __imp__ShowMsg:DWORD ; CPP 文件中定义 void ShowMsg() 函数
kernel32dll db  'kernel32.dll',0
encode  db 'EncodePointer',0
decode  db 'DecodePointer',0
.code
dummy proc
mov eax, dword ptr [esp+4]
ret 4
dummy endp
dummy_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 eax
dummy_encode endp
dummy_decode proc
……
dummy_decode endp
end
代码中省略了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_decode
EXTERNDEF__imp_LoadLibraryA : QWORD
EXTERNDEF__imp_GetProcAddress : QWORD
EXTERNDEF__imp_ShowMsg : QWORD ; CPP 文件中定义 void ShowMsg() 函数
kernel32dll db  'kernel32.dll',0
encode  db 'EncodePointer',0
decode  db 'DecodePointer',0
.code
dummy proc
mov rax, rcx
ret
dummy endp
dummy_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 rax
dummy_encode endp
dummy_decode proc
……
dummy_decode endp
end

  与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],rbx
GetProcAddress:
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编译出来的程序的兼容性,同时避免了安全性上的降低。但也有人认为费了这么大力气才解决这么小的一个问题没有必要。无所谓,通过这个试验长了点儿知识总还是好的!
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: