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文件代码如下:
首先__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文件编译,需要针对文件采自定义编译的方式进行,编译的命令行如下:
三、x64架构
XP SP1居然有64位的!虽然是鸡肋版本,但为了系统的完整兼容,我决定也进行一下尝试。虽然这样做,工程设置的“所需最低版本”至少是6.00,但我猜想它是可以在XP SP1上运行的。我的代码是这样的:
与x64架构下非常相似,我就不重复说明了。值得注意的是函数的命名规则略有变化。还有就是x64函数调用约定的情况。x64架构的子函数头四个参数通过rcx、rdx、r8、r9来传递,堆栈也至少要预留20h的空间,再加入为了使返回地址在16字节边界上对齐,需要另加上8字节空间,因此需要28h字节的空间。如果子函数参数多于4个,则空间要更多,且要在16字节边界上对齐,如果子数有5个参数,则总共需要下面这么多的空间:
最开始因为我对x64函数调用约定的误解(即使传递的参数少于4个,也要至少预留4个参数的位置),我并没有采用(1)、(2)、(3)的方式预留参数空间,而是采用了(*)处更为紧凑的方法。这种方法一般情况下是没有问题的,但在这里则不然。为了排错我耗费了很多时间,这里记录下来好让后来人不要犯同样的错误。
(*)的方法LoadLibrary和GetProcAddress函数调用完成后总是将dummy_encode的返回地址修改掉,但自信自己的代码没有问题,我逆向了一下LoadLibraryA和GetProcAddress发现,它们的实质代码第一行居然干了这个:
另外,x64下对asm文件编译的命令行为:
四、总结
这种方法可以有效地提高Visual Studio 2015编译出来的程序的兼容性,同时避免了安全性上的降低。但也有人认为费了这么大力气才解决这么小的一个问题没有必要。无所谓,通过这个试验长了点儿知识总还是好的!
一、基本方法
之所以新版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],rsirsp+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编译出来的程序的兼容性,同时避免了安全性上的降低。但也有人认为费了这么大力气才解决这么小的一个问题没有必要。无所谓,通过这个试验长了点儿知识总还是好的!
相关文章推荐
- Quartz 2D编程指南(1) - 概览
- jquery树形控件Ztree 使用
- 工作笔记之安卓开机动画与logo修改
- 实现自己的ls命令
- calibre recipe抓取中没有注意的空格
- JS实现把鼠标放到链接上出现滚动文字的方法
- memcache的介绍与操作
- Junit简单使用(一 )
- 使用JMeter对Tomcat进行压力测试与Tomcat性能调优
- iOS系列之---图片压缩
- DNS Prefetch
- HTTP Status 404 - No result defined for action com.csdhsm.struts.action.LoginAction and result error
- ADB启动相应的广播、服务、图形界面
- 在FreeBSD上安装Bugzilla
- Mysql命令大全
- 获取目录下的普通文件数目
- 基于.NET平台常用的框架整理
- 学习使用action属性来接受参数
- 一劳永逸解决UAC问题,获取超级管理员 administrator权限
- 分类汇总(按班级,可手动设置分类项)