溢出是C语言中最常见的漏洞。最常见的溢出包括数组溢出、数溢出、缓冲区溢出、指针溢出以及栈溢出,下面笔者就对这些溢出做一个总结归纳。
1 数组溢出
数组溢出是最常见的一种溢出。因为在C语言中,含N个元素的数组下标是从0开始,到N-1结束,而且C语言没有提供数组越界检查的机制。
请看如下代码:
int main (int argc, char* argv[])
{
int nSum = 0;
int nArray[] = {1, 2, 3, 4, 5};
for (int i = 0; i <= 5; ++i) // 数组含有5个元素,但是从0开始
{
nSum += nArray[i]; // 当i = 5时发生越界
}
return 0;
}
上面的循环判定应该改为: for (int i = 0; i < 5; ++i)
也就是说,一个含有n个元素的数组,其遍历元素的方式为: for (int i = 0; i < n - 1; ++i)
再看下面这个程序:
void print_array(int a[], int n)
{
for (int i = 0; i < n; ++i)
{
a[i] = a[i+1]; // 当i = n - 1时发生数组越界
printf("%d/n", a[i]);
}
}
上面的循环判断应该改为: for (int i = 0; i < n - 1; ++i)
从上面的例子看出,C语言中尤其要注意数组的溢出错误。一旦发生了数组的溢出,就会造成内存的非法访问。可能会导致程序崩溃,也可能什么都没发生。
# 2数溢出
数的溢出是指数的值超过了他的类型的表示范围。C++标准规定了每个算术类型的最小存储空间,但它并不阻止编译器使用更大的存储空间。
请看如下代码:
unsigned char ch;
for (ch = 0; ch <= UCHAR_MAX; ++ch)
{
// do something...
}
```当ch == UCHAR_MAX时,运行++ch的时候就会发生unsigned char类型的上溢,导致ch重新为0,此时程序会陷入死循环。
请看如下代码,查找内存中特定的字符位置 :
void* memchr(void *pv, unsigned char ch, size_t size{
unsigned char *pch = (unsigned char *)pv;
while (--size >= 0)
{
if (*pch == pv)
{
return (pch);
}
pch++;
}
return (NULL);
}
当无符号整型size_t(unsigned int)变量size == 0时,--size将会发生下溢,变为4294967295,导致死循环。
请看如下代码:
void IntToStr(int i, char *str)
{
if (i < 0)
{
*str++ = '-';
i = -i;
}
char *strDigits = str;
do
{
*str++ = i % 10 + '0';
} while ((i /=10) > 0);
*str='/0';
reverseStr(strDigits);
}
在上面的代码中,如果i == -2147483648时,对它求反将是2147483648,而这个值已经超过了int类型表示的最大值,即发生了上溢。应该用assert()宏来规定i的有效范围。
# 3缓冲区溢出
程序在运行过程中,为了临时存取数据的需要,一般都要分配一些内存空间,通常称这些空间为缓冲区。如果向缓冲区写入超过其本身长度的数据,以致于缓冲区无法容纳,就会造成缓冲区以外的存储单元被改写,这种现象就被称为缓冲区溢出。
请看如下代码:
#include
#include
#include
void func1(char *s)
{
char buf[10];
strcpy(buf, s);
}
void func2(void)
{
printf("Hacked by me./n");
exit(0);
}
int main (int argc, char* argv[])
{
char badCode[] = "111122223333444455556666";
DWORD* pEIP = (DWORD*)&badCode[16];
*pEIP = (DWORD)func2;
func1(badCode);
return 0;
}
运行结果:
![](https://s1.51cto.com/images/blog/201812/22/7581651fab59ce938e60d682a28eaa14.png?x-oss-process=image/watermark,size_16,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_100,g_se,x_10,y_10,shadow_90,type_ZmFuZ3poZW5naGVpdGk=)
最重要的一点是把pEIP的地址及调用函数做成一串字符串,就会导致缓冲区溢出。
字符串处理操作是缓冲区溢出的常见根源,大部分是由于C/C++语言运行时库提供的标准字符串处理函数(strcat、strcpy、sprintf等)不会阻止超出缓冲区结尾的写入操作。所以需要使用更加安全的字符串函数来消除程序中许多潜在的缓冲区溢出。比如可以使用strncat、strncpy、snprintf等函数,来控制字符串操作的实际字符数,避免溢出的问题。而在最新的微软字符串操作库中,已经提供了一些安全的字符串操作函数,如strcpy_s、strcat_s等。
# 4栈溢出
系统的内核栈大小为一个可以设置的定值。一般内核栈的大小为4KB或8KB。因此由于程序的局部变量都分配在栈上,在写程序时模块的局部变量大小之和不应该超过这个栈的大小,否则就会发生栈溢出,而导致系统崩溃,例如下面的程序在Linux系统执行就会造成内核栈溢出:
#include
int init_module(void)
{
char buf[10000];
memset(buf, 0, 10000);
printk("kernel stack./n");
return 0;
}
void cleanup_module(void)
{
printk("goodbye./n");
}
MODULE_LICENSE("GPL");
}
```
buf[10000]分配在栈上,但10000的空间超过了栈的默认大小8KB,所以发生溢出。
不仅系统栈会有溢出的问题,应用程序也可能会造成栈溢出。最常见的溢出可能就是当用递归算法写程序时,当递归嵌套过深,就会造成栈的溢出。因为递归调用的中间结果将保存在堆栈中。要防止递归造成的栈的溢出,可以跟踪递归的深度,当其大于某个深度就返回。也可以将递归算法改为非递归算法。
5指针溢出
指针的溢出可以理解为指针的错误运算,而指向了不该指向的地址。这个地址可能是NULL空间,可能是内核空间,也可能是无效的内存空间。例如:
void* memchr(void *pv, unsigned char ch, size_t size)
{
unsigned char *pch = (unsigned char *)pv;
unsigned char *pchEnd = pch + size;
while (pch < pchEnd)
{
if (*pch == pv)
{
return (pch);
}
pch++;
}
return (NULL);
}
上面的代码用于查找内存中特定的字符位置。对于其中的while()循环,平时执行似乎都没有任何问题。但是考虑一种特别情况,即pv所指的内存位置为末尾若干个字节,那么因为pchEnd = pch + size,所以pchEnd指向最后一个字符的下一个字节,那么因为pchEnd所指的位置已经不存在,所以会发生指针溢出,因此在程序中应注意内存结尾的计算方式。
正确代码如下:
void* memchr(void *pv, unsigned char ch, size_t size)
{
unsigned char *pch = (unsigned char *)pv;
while (--size >= 0)
{
if (*pch == pv)
{
return (pch);
}
pch++;
}
return (NULL);
}
原文:https://blog.csdn.net/wyunteng/article/details/6339631