详解C语言之缓冲区溢出

一、缓冲区溢出原理

栈帧结构的引入为高级语言中实现函数或过程调用提供直接的硬件支持,但由于将函数返回地址这样的重要数据保存在程序员可见的堆栈中,因此也给系统安全带来隐患。若将函数返回地址修改为指向一段精心安排的恶意代码,则可达到危害系统安全的目的。此外,堆栈的正确恢复依赖于压栈的EBP值的正确性,但EBP域邻近局部变量,若编程中有意无意地通过局部变量的地址偏移窜改EBP值,则程序的行为将变得非常危险。

由于C/C++语言没有数组越界检查机制,当向局部数组缓冲区里写入的数据超过为其分配的大小时,就会发生缓冲区溢出。攻击者可利用缓冲区溢出来窜改进程运行时栈,从而改变程序正常流向,轻则导致程序崩溃,重则系统特权被窃取。

例如,对于下图的栈结构:

若将长度为16字节的字符串赋给acArrBuf数组,则系统会从acArrBuf[0]开始向高地址填充栈空间,导致覆盖EBP值和函数返回地址。若攻击者用一个有意义的地址(否则会出现段错误)覆盖返回地址的内容,函数返回时就会去执行该地址处事先安排好的攻击代码。最常见的手段是通过制造缓冲区溢出使程序运行一个用户shell,再通过shell执行其它命令。若该程序有root或suid执行权限,则攻击者就获得一个有root权限的shell,进而可对系统进行任意操作。

除通过使堆栈缓冲区溢出而更改返回地址外,还可改写局部变量(尤其函数指针)以利用缓冲区溢出缺陷。

注意,本文描述的堆栈缓冲区溢出不同于广义的“堆栈溢出(Stack OverFlow)”,后者除局部数组越界和内存覆盖外,还可能由于调用层次太多(尤其应注意递归函数)或过大的局部变量所导致。

二、缓冲区溢出实例

本节给出若干缓冲区溢出相关的示例性程序。前三个示例为手工修改返回地址或实参,后两个示例为局部数组越界访问和缓冲区溢出。更加深入的缓冲区溢出攻击参见相关资料。

示例函数必须包含stdio.h头文件,并按需包含string.h头文件(如strcpy函数)。

【示例1】改变函数的返回地址,使其返回后跳转到某个指定的指令位置,而不是函数调用后紧跟的位置。实现原理是在函数体中修改返回地址,即找到返回地址的位置并修改它。代码如下:

//foo.c
void foo(void){
    int a, *p;
    p = (int*)((char *)&a + 12);  //让p指向main函数调用foo时入栈的返回地址,等效于p = (int*)(&a + 3);
    *p += 12;    //修改该地址的值,使其指向一条指令的起始地址
}
int main(void){
    foo();
    printf("First printf call\n");
    printf("Second printf call\n");
    return 0;
}

编译运行,结果输出Second printf call,未输出First printf call。

下面详细介绍代码中两个12的由来。

编译(gcc main.c –g)和反汇编(objdump a.out –d)后,得到汇编代码片段如下:

从上述汇编代码可知,foo后面的指令地址(即调用foo时压入的返回地址)是0x80483b8,而进入调用printf("Second printf call“)的指令地址是0x80483c4。两者相差12,故将返回地址的值加12即可(*p += 12)。

指令<804838a>将-8(%ebp)的地址赋值给%eax寄存器(p = &a)。可知foo()函数中的变量a存储在-8(%ebp)地址上,该地址向上8+4=12个单位就是返回地址((char *)&a + 12)。修改该地址内容(*p += 12)即可实现函数调用结束后跳转到第二个printf函数调用的位置。

用gdb查看汇编指令刚进入foo时栈顶的值(%esp),如下所示:

可见%esp值的确是调用foo后main中下条待执行指令的地址,而代码所修改的也正是该值。%eip则指向当前程序(foo)的指令地址。

【示例2】暂存RunAway函数的返回地址后修改其值,使函数返回后跳转到Detour函数的地址;Detour函数内尝试通过之前保存的返回地址重回main函数内。代码如下:

//RunAway.c
int gPrevRet = 0; //保存函数的返回地址
void Detour(void){
    int *p = (int*)&p + 2;  //p指向函数的返回地址
    *p = gPrevRet;
    printf("Run Away!\n"); //需要回车,或打印后fflush(stdout);刷新缓冲区,否则可能在段错误时无法输出
}
int RunAway(void){
    int *p = (int*)&p + 2;
    gPrevRet = *p;
    *p = (int)Detour;
    return 0;
}
int main(void){
    RunAway();
    printf("Come Home!\n");
    return 0;
}

编译运行后输出:

Run Away!

Come Home!

Run Away!

Come Home!

Segmentation fault

运行后出现段错误?There must be something wrong!错误原因留待读者思考,下面给出上述代码的另一版本,借助汇编获取返回地址(而不是根据栈帧结构估算)。

register void *gEbp __asm__ ("%ebp");
void Detour(void){
    *((int *)gEbp + 1) = gPrevRet;
    printf("Run Away!\n");
}
int RunAway(void){
    gPrevRet = *((int *)gEbp + 1);
    *((int *)gEbp + 1) = Detour;
    return 0;
}

【示例3】在被调函数内修改主调函数指针变量,造成后续访问该指针时程序崩溃。代码如下:

//Crasher.c
typedef struct{
    int member1;
    int member2;
}T_STRT;
T_STRT gtTestStrt = {0};
register void *gEbp __asm__ ("%ebp");

void Crasher(T_STRT *ptStrt){
    printf("[%s]: ebp    = %p(0x%08x)\n", __FUNCTION__, gEbp, *((int*)gEbp));
    printf("[%s]: ptStrt = %p(%p)\n", __FUNCTION__, &ptStrt, ptStrt);
    printf("[%s]: (1)    = %p(0x%08x)\n", __FUNCTION__, ((int*)&ptStrt-2), *((int*)&ptStrt-2));
    printf("[%s]: (2)    = %p(0x%08x)\n", __FUNCTION__, (int*)(*((int*)&ptStrt-2)-4), *(int*)(*((int*)&ptStrt-2)-4));
    printf("[%s]: (3)    = %p(0x%08x)\n", __FUNCTION__, (int*)(*((int*)&ptStrt-2)-8), *(int*)(*((int*)&ptStrt-2)-8));
    *(int*)( *( (int*)&ptStrt - 2 ) - 8 ) = 0;  //A:此句将导致代码B处发生段错误
}

int main(void){
    printf("[%s]: ebp    = %p(0x%08x)\n", __FUNCTION__, gEbp, *((int*)gEbp));
    T_STRT *ptStrt = &gtTestStrt;
    printf("[%s]: ptStrt = %p(%p)\n", __FUNCTION__, &ptStrt, ptStrt);

    Crasher(ptStrt);
    printf("[%s]: ptStrt = %p(%p)\n", __FUNCTION__, &ptStrt, ptStrt);
    ptStrt->member1 = 5;  //B:需要在此处崩溃
    printf("Try to come here!\n");
    return 0;
}

运行结果如下所示:

根据打印出的地址及其存储内容,可得到以下堆栈布局:

&ptStrt为形参地址0xbff8f090,该地址处在main函数栈帧中。(int*)&ptStrt - 2地址存储主调函数的EBP值,根据该值可直接定位到main函数栈帧底部。(*((int*)&ptStrt - 2) - 8)为主调函数中实参ptStrt的地址,而*(int*) (*((int*)&ptStrt - 2) - 4) = 0将该地址内容置零,即实参指针ptStrt设置为NULL(不再指向全局结构gtTestStrt)。这样,访问ptStrt->member1时就会发生段错误。

注意,虽然本例代码结构简单,但不能轻率地推断main函数中局部变量ptStrt位于帧基指针EBP-4处(实际上本例为EBP-8处)。以下改进版本用于自动计算该偏移量:

static int gOffset = 0;
void Crasher(T_STRT *ptStrt){
   *(int*)( *(int*)gEbp - gOffset ) = 0;
}

int main(void){
    T_STRT *ptStrt = &gtTestStrt;
    gOffset = (char*)gEbp - (char*)(&ptStrt);
    Crasher(ptStrt);
    ptStrt->member1 = 5;  //在此处崩溃
    printf("Try to come here!\n");
    return 0;
}

当然,该版本已失去原有意义(不借助寄存器层面手段),纯为示例。

【示例4】越界访问造成死循环。代码如下:

//InfinteLoop.c
void InfinteLoop(void){
    unsigned char ucIdx, aucArr[10];
    for(ucIdx = 0; ucIdx <= 10; ucIdx++)
        aucArr[ucIdx] = 1;
}

在循环内部,当访问不存在的数组元素aucArr[10]时,实际上在访问数组aucArr所在地址之后的那个位置,而该位置存放着变量ucIdx。因此aucArr[10] = 1将ucIdx重置为1,然后继续循环的条件仍然成立,最终将导致死循环。

【示例5】缓冲区溢出。代码如下:

//CarelessPapa.c
register int *gEbp __asm__ ("%ebp");
void NaughtyBoy(void){
    printf("[2]EBP=%p(%#x), EIP=%p(%#x)\n", gEbp, *gEbp, gEbp+1, *(gEbp+1));
    printf("Catch Me!\n");
}
void CarelessPapa(const char *pszStr){
    printf("[1]EBP=%p(%#x)\n", gEbp, *gEbp);
    printf("[1]EIP=%p(%#x)\n", gEbp+1, *(gEbp+1));
    char szBuf[8];
    strcpy(szBuf, pszStr);
}
int main(void){
    printf("[0]EBP=%p(%#x)\n", gEbp, *gEbp);
    printf("Addr: CarelessPapa=%p, NaughtyBoy=%p\n", CarelessPapa, NaughtyBoy);
    char szArr[]="0123456789AB\xe4\x83\x4\x8\x23\x85\x4\x8";
    CarelessPapa(szArr);
    printf("Come Home!\n");
    printf("[3]EBP=%p\n", gEbp);
    return 0;
}

编译运行结果如下:

可见,当CarelessPapa函数调用结束后,并未直接执行Come Home的输出,而是转而执行NaughtyBoy函数(输出Catch Me),然后回头输出Come Home。该过程重复一次后发生段错误(具体原因留待读者思考)。

结合下图所示的栈帧布局,详细分析本示例缓冲区溢出过程。注意,本示例中地址及其内容由内嵌汇编和打印输出获得,正常情况下应通过gdb调试器获得。

首先,main函数将字符数组szArr的地址作为参数(即pszStr)传递给函数CarelessPapa。该数组内容为"0123456789AB\xe4\x83\x4\x8\x23\x85\x4\x8",其中转义字符串"\xe4\x83\x4\x8"对应NaughtyBoy函数入口地址0x080483e4(小字节序),而"\x23\x85\x4\x8"对应调用CarelessPapa函数时的返回地址0x8048523(小字节序)。CarelessPapa函数内部调用strcpy库函数,将pszStr所指字符串内容拷贝至szBuf数组。因为strcpy函数不进行越界检查,会逐字节拷贝直到遇见'\0'结束符。故pszStr字符串将从szBuf数组起始地址开始向高地址覆盖,原返回地址0x8048523被覆盖为NaughtyBoy函数地址0x080483e4。

这样,当CarelessPapa函数返回时,修改后的返回地址从栈中弹出到EIP寄存器中,此时栈顶指针ESP指向返回地址上方的空间(esp+4),程序跳转到EIP所指地址(NaughtyBoy函数入口)开始执行,首先就是EBP入栈——并未像正常调用那样先压入返回地址,故NaughtyBoy函数栈帧中EBP位置相对CarelessPapa函数上移4个字节!此时,"\x23\x85\x4\x8"可将EBP上方的EIP修改为CarelessPapa函数的返回地址(0x8048523),从而保证正确返回main函数内。

注意,返回main函数并输出Come Home后,main函数栈帧的EBP地址被改为0x42413938("89AB"),该地址已非堆栈空间,最终产生段错误。EBP地址会随每次程序执行而改变,故试图在szArr字符串中恢复EBP是非常困难的。

从main函数return时将返回到调用它的启动例程(_start函数)中,返回值被启动例程获得并用其作为参数调用exit函数。exit函数首先做一些清理工作,然后调用_exit系统调用终止进程。main函数的返回值最终传给_exit系统调用,成为进程的退出状态。以下代码在main函数中直接调用exit函数终止进程而不返回到启动例程:

//CarelessPapa.c
register int *gEbp __asm__ ("%ebp");
void NaughtyBoy(void){
    printf("[2]EBP=%p(%#x), EIP=%p(%#x)\n", gEbp, *gEbp, gEbp+1, *(gEbp+1));
    printf("Catch Me!\n");
}
void CarelessPapa(const char *pszStr){
    printf("[1]EBP=%p(%#x)\n", gEbp, *gEbp);
    printf("[1]EIP=%p(%#x)\n", gEbp+1, *(gEbp+1));
    char szBuf[8];
    strcpy(szBuf, pszStr);
}
int main(void){
    printf("[0]EBP=%p(%#x)\n", gEbp, *gEbp);
    printf("Addr: CarelessPapa=%p, NaughtyBoy=%p\n", CarelessPapa, NaughtyBoy);
    char szArr[]="0123456789AB\x14\x84\x4\x8\x33\x85\x4\x8"; //转义字符串稍有变化
    CarelessPapa(szArr);
    printf("Come Home!\n");
    printf("[3]EBP=%p\n", gEbp);
    exit(0); //#include <stdlib.h>
}

编译运行结果如下:

这次没有重复执行,也未出现段错误。

三、缓冲区溢出防范

防范缓冲区溢出问题的准则是:确保做边界检查(通常不必担心影响程序效率)。不要为接收数据预留相对过小的缓冲区,大的数组应通过malloc/new分配堆空间来解决;在将数据读入或复制到目标缓冲区前,检查数据长度是否超过缓冲区空间。同样,检查以确保不会将过大的数据传递给别的程序,尤其是第三方COTS(Commercial-off-the-shelf)商用软件库——不要设想关于其他人软件行为的任何事情。

若有可能,改用具备防止缓冲区溢出内置机制的高级语言(Java、C#等)。但许多语言依赖于C库,或具有关闭该保护特性的机制(为速度而牺牲安全性)。其次,可以借助某些底层系统机制或检测工具(如对C数组进行边界检查的编译器)。许多操作系统(包括Linux和Solaris)提供非可执行堆栈补丁,但该方式不适于这种情况:攻击者利用堆栈溢出使程序跳转到放置在堆上的执行代码。此外,存在一些侦测和去除缓冲区溢出漏洞的静态工具(检查代码但并不运行)和动态工具(执行代码以确定行为),甚至采用grep命令自动搜索源代码中每个有问题函数的实例。

但即使采用这些保护手段,程序员自身也可能犯其他许多错误,从而引入缺陷。例如,当使用有符号数存储缓冲区长度或某个待读取内容长度时,攻击者可将其变为负值,从而使该长度被解释为很大的正值。经验丰富的程序员还容易过于自信地"把玩"某些危险的库函数,如对其添加自己总结编写的检查,或错误地推论出使用潜在危险的函数在某些特殊情况下是"安全"的。

本节将主要讨论一些已被证明危险的C库函数。通过在C/C++程序中禁用或慎用危险的函数,可有效降低在代码中引入安全漏洞的可能性。在考虑性能和可移植性的前提下,强烈建议在开发过程中使用相应的安全函数来替代危险的库函数调用。

以下分析某些危险的库函数,较完整的列表参见表3-1。

3.1、gets

该函数从标准输入读入用户输入的一行文本,在遇到EOF字符或换行字符前,不会停止读入文本。即该函数不执行越界检查,故几乎总有可能使任何缓冲区溢出(应禁用)。

gcc编译器下会对gets调用发出警告(the `gets' function is dangerous and should not be used)。

3.2、strcpy

该函数将源字符串复制到目标缓冲区,但并未指定要复制字符的数目。若源字符串来自用户输入且未限制其长度,则可能引发危险。规避的方法如下:

1) 若知道目标缓冲区大小,则可添加明确的检查(不建议该法):

if(strlen(szSrc) >= dwDstSize){
    /* Do something appropriate, such as throw an error. */
}
else{
    strcpy(szDst, szSrc);
}

2) 改用strncpy函数:

strncpy(szDst, szSrc, dwDstSize-1);
szDst[dwDstSize-1] = '\0';  //Always do this to be safe!

若szSrc比szDst大,则该函数不会返回错误;当达到指定长度(dwDstSize-1)时,停止复制字符。第二句将字符串结束符放在szDst数组的末尾。

3) 在源字符串上调用strlen()来为其分配足够的堆空间:

pszDst = (char *)malloc(strlen(szSrc));
strcpy(pszDst, szSrc);

4) 某些情况下使用strcpy不会带来潜在的安全性问题:

strcpy(szDst, "Hello!");  //Usually by initialization, such as char szDst[] = “Hello!”;

即使该操作造成szDst溢出,但这几个字符显然不会造成危害——除非用其它方式覆盖字符串“Hello”所在的静态存储区。

安全的字符串处理函数通常体现在如下几个方面:

  • 显式指明目标缓冲区大小
  • 动态校验
  • 返回码(以指明成功或失败原因)

与strcpy函数具有相同问题的还有strcat函数。

3.3、 strncpy/strncat

该对函数是strcpy/strcat调用的“安全”版本,但仍存在一些问题:

1) strncpy和strncat要求程序员给出剩余的空间,而不是给出缓冲区的总大小。缓冲区大小一经分配就不再变化,但缓冲区中剩余的空间量会在每次添加或删除数据时发生变化。这意味着程序员需始终跟踪或重新计算剩余的空间,而这种跟踪或重新计算很容易出错。

2) 在发生溢出(和数据丢失)时,strncpy和strncat返回结果字符串的起始地址(而不是其长度)。虽然这有利于链式表达,但却无法报告缓冲区溢出。

3) 若源字符串长度至少和目标缓冲区相同,则strncpy不会使用NUL来结束字符串;这可能会在以后导致严重破坏。因此,在执行strncpy后通常需要手工终止目标字符串。

4) strncpy还可复制源字符串的一部分到目标缓冲区,要复制的字符数目通常基于源字符串的相关信息来计算。这种操作也会产生未终止字符串。

5) strncpy会在源字符串结束时使用NUL来填充整个目标缓冲区,这在源字符串较短时存在性能问题。

3.4、sprintf

该函数使用控制字符串来指定输出格式,该字符串通常包括"%s"(字符串输出)。若指定字符串输出的精确指定符,则可通过指定输出的最大长度来防止缓冲区溢出(如%.10s将复制不超过10个字符)。也可以使用"*"作为精确指定符(如"%.*s"),这样就可传入一个最大长度值。精确字段仅指定一个参数的最大长度,但缓冲区需要针对组合起来的数据的最大尺寸调整大小。

注意,"字段宽度"(如"%10s",无点号)仅指定最小长度——而非最大长度,从而留下缓冲区溢出隐患。

3.5、scanf

scanf系列函数具有一个最大宽度值,函数不能读取超过最大宽度的数据。但并非所有规范都规定了这点,也不确定是否所有实现都能正确执行这些限制。若要使用这一特性,建议在安装或初始化期间运行小测试来确保它能正确工作。

3.6、streadd/strecpy

这对函数可将含有不可读字符的字符串转换成可打印的表示。其原型包含在libgen.h头文件内,编译时需加-lgen [library ...]选项。

char *strecpy(char *pszOut, const char *pszIn, const char *pszExcept);

char *streadd(char *pszOut, const char *pszIn, const char *pszExcept);

strecpy将输入字符串pszIn(连同结束符)拷贝到输出字符串pszOut中,并将非图形字符展开为C语言中相应的转义字符序列(如Control-A转为“\001”)。参数pszOut指向的缓冲区大小必须足够容纳结果字符串;输出缓冲区大小应为输入缓冲区大小的四倍(单个字符可能转换为\abc共四个字符)。出现在参数pszExcept字符串内的字符不被展开。该参数可设为空串,表示扩展所有非图形字符。strecpy函数返回指向pszOut字符串的指针。

streadd函数与strecpy相同,只不过返回指向pszOut字符串结束符的指针。

考虑以下代码:

#include <libgen.h>
int main(void){
    char szBuf[20] = {0};
    streadd(szBuf, "\t\n", "");
    printf(%s\n", szBuf);
    return 0;
}

打印输出\t\n,而不是所有空白。

3.7、strtrns

该函数将pszStr字符串中的字符转换后复制到结果缓冲区pszResult。其原型包含在libgen.h头文件内:

char * strtrns(const char *pszStr, const char *pszOld, const char *pszNew, char *pszResult);

出现在pszOld字符串中的字符被pszNew字符串中相同位置的字符替换。函数返回新的结果字符串。

如下示例将小写字符转换成大写字符:

#include <libgen.h>
int main(int argc,char *argv[]){
    char szLower[] = "abcdefghijklmnopqrstuvwxyz";
    char szUpper[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    if(argc < 2){
        printf("USAGE: %s arg\n", argv[0]);
        exit(0);
    }
    char *pszBuf = (char *)malloc(strlen(argv[1]));
    strtrns(argv[1], szLower, szUpper, pszBuf);
    printf("%s\n", pszBuf);
    return 0;
}

以上代码使用malloc分配足够空间来复制argv[1],因此不会引起缓冲区溢出。

3.8、realpath

该函数在libc 4.5.21及以后版本中提供,使用时需要limits.h和stdlib.h头文件。其原型为:

char *realpath(const char *pszPath, char *pszResolvedPath);

该函数展开pszPath字符串中的所有符号链接,并解析pszPath中所引用的/./、/../和'/'字符(相对路径),最终生成规范化的绝对路径名。该路径名作为带结束符的字符串存入pszResolvedPath指向的缓冲区,长度最大为PATH_MAX字节。结果路径中不含符号链接、/./或/../。

若pszResolvedPath为空指针,则realpath函数使用malloc来分配PATH_MAX字节的缓冲区以存储解析后的路径名,并返回指向该缓冲区的指针。调用者应使用free函数去释放该该缓冲区。

若执行成功,realpath函数返回指向pszResolvedPath(规范化绝对路径)的指针;否则返回空指针并设置errno以指示该错误,此时pszResolvedPath的内容未定义。

调用者需要确保结果缓冲区足够大(但不应超过PATH_MAX),以处理任何大小的路径。此外,不可能为输出缓冲区确定合适的长度,因此POSIX.1-2001规定,PATH_MAX字节的缓冲区足够,但PATH_MAX不必定义为常量,且可以通过pathconf函数获得。然而,pathconf输出的结果可能超大,以致不适合动态分配内存;另一方面,pathconf函数可返回-1表明结果路径名超出PATH_MAX限制。pszResolvedPath为空指针的特性被POSIX.1-2008标准化,以避免输出缓冲区长度难以静态确定的缺陷。

应禁用或慎用的库函数如下表所示:

表3-1


函数


危险性


解决方案


gets


最高


禁用gets(buf),改用fgets(buf, size, stdin)


strcpy



检查目标缓冲区大小,或改用strncpy,或动态分配目标缓冲区


strcat



改用strncat


sprintf



改用snprintf,或使用精度说明符


scanf



使用精度说明符,或自己进行解析


sscanf



使用精度说明符,或自己进行解析


fscanf



使用精度说明符,或自己进行解析


vfscanf



使用精度说明符,或自己进行解析


vsprintf



改为使用vsnprintf,或使用精度说明符


vscanf



使用精度说明符,或自己进行解析


vsscanf



使用精度说明符,或自己进行解析


streadd



确保分配的目标参数缓冲区大小是源参数大小的四倍


strecpy



确保分配的目标参数缓冲区大小是源参数大小的四倍


strtrns



手工检查目标缓冲区大小是否至少与源字符串相等


getenv



不可假定特殊环境变量的长度


realpath


高(或稍低,实现依赖)


分配缓冲区大小为PATH_MAX字节,并手工检查参数以确保输入参数和输出参数均不超过PATH_MAX


syslog


高(或稍低,实现依赖)


将字符串输入传递给该函数之前,将所有字符串输入截成合理大小


getopt


高(或稍低,实现依赖)


将字符串输入传递给该函数之前,将所有字符串输入截成合理大小


getopt_long


高(或稍低,实现依赖)


将字符串输入传递给该函数之前,将所有字符串输入截成合理大小


getpass


高(或稍低,实现依赖)


将字符串输入传递给该函数之前,将所有字符串输入截成合理大小


getchar



若在循环中使用该函数,确保检查缓冲区边界


fgetc



若在循环中使用该函数,确保检查缓冲区边界


getc



若在循环中使用该函数,确保检查缓冲区边界


read



若在循环中使用该函数,确保检查缓冲区边界


bcopy



确保目标缓冲区不小于指定长度


fgets



确保目标缓冲区不小于指定长度


memcpy



确保目标缓冲区不小于指定长度


snprintf



确保目标缓冲区不小于指定长度


strccpy



确保目标缓冲区不小于指定长度


strcadd



确保目标缓冲区不小于指定长度


strncpy



确保目标缓冲区不小于指定长度


vsnprintf



确保目标缓冲区不小于指定长度

以上就是详解C语言之缓冲区溢出的详细内容,更多关于C语言 缓冲区溢出的资料请关注我们其它相关文章!

(0)

相关推荐

  • PHP5.0 TIDY_PARSE_FILE缓冲区溢出漏洞的解决方案

    漏洞说明 不得不再次吐槽一下exploit-db对exp审核的质量,这个exp仍然不能触发漏洞,修改第一个参数则可以触发,我给出的poc是一个可以触发php漏洞的,问题出现在php_tidy.dll扩展中,对tidy_parse_file的第二个参数,也就是文件绝对路径没有进行长度控制和内容校验,导致在fopen失败后进入失败处理逻辑引发缓冲区溢出,下面对此漏洞进行详细分析. 软件下载: https://www.exploit-db.com/apps/f8fb5676b6a32f7be1c8d8

  • PHP基础之输出缓冲区基本概念、原理分析

    本文实例讲述了PHP基础之输出缓冲区.分享给大家供大家参考,具体如下: 一.概念 在PHP运行的过程中,可以将会产生输出的函数或操作结果暂时保存在PHP的缓冲区,只有当缓冲区满了.或者PHP运行完毕.或者在必要时候进行输出,才会将数据输出到浏览器,此缓冲数据的区域称为PHP的输出缓冲区(OB). 二.原理 ①使用了缓冲区之后,当执行PHP的时候,如果碰到了echo.print_r之类的会输出数据的代码(实际上许多函数都会产生输出),PHP就会将要输出的数据放到PHP自身的缓冲区,等待输出: ②当

  • C#环形缓冲区(队列)完全实现

    公司项目中经常设计到串口通信,TCP通信,而且大多都是实时的大数据的传输,然后大家都知道协议通讯肯定涉及到什么,封包.拆包.粘包.校验--什么鬼的概念一大堆,说简单点儿就是要一个高效率可复用的缓存区.按照码农的惯性思维就是去百度.谷歌搜索看有没有现成的东西可以直接拿来用,然而我并没有找到,好吧不是很难的东西自己实现一个呗.开扯-- 为什么要用环形队列? 环形队列是在实际编程极为有用的数据结构,它有如下特点: 它是一个首尾相连的FIFO的数据结构,采用数组的线性空间,数据组织简单.能很快知道队列是

  • C 语言中实现环形缓冲区

    1.实现代码: #include #include #include #include #include #define BUFFSIZE 1024 * 1024 #define min(x, y) ((x) < (y) ? (x) : (y)) pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; struct cycle_buffer { unsigned char *buf; unsigned int size; unsigned int in

  • 缓冲区溢出:十年来攻击和防卫的弱点

    摘要: 在过去的十年中,以缓冲区溢出为类型的安全漏洞占是最为 常见的一种形式了.更为严重的是,缓冲区溢出漏洞占了远程网 络攻击的绝大多数,这种攻击可以使得一个匿名的Internet用户 有机会获得一台主机的部分或全部的控制权!如果能有效地消除 缓冲区溢出的漏洞,则很大一部分的安全威胁可以得到缓解.在 本文中,我们研究了各种类型的缓冲区溢出漏洞和攻击手段,同 时我们也研究了各种的防御手段,这些手段用来消除这些漏洞所 造成的影响,其中包括我们自己的堆栈保护方法.然后我们要考 虑如何在保证现有系统功能

  • python运行时强制刷新缓冲区的方法

    需求:打印一颗"*"休息1s 代码如下: #!/usr/bin/python #coding=utf-8 ''' 暂停1s输出 ''' import time def printStar(n): for i in range(n): print " * ", time.sleep(1) if __name__ == '__main__': printStar(10) 输出结果(等待10s后一次性输出): [root@miner_k test]# python sle

  • 详解C语言之缓冲区溢出

    一.缓冲区溢出原理 栈帧结构的引入为高级语言中实现函数或过程调用提供直接的硬件支持,但由于将函数返回地址这样的重要数据保存在程序员可见的堆栈中,因此也给系统安全带来隐患.若将函数返回地址修改为指向一段精心安排的恶意代码,则可达到危害系统安全的目的.此外,堆栈的正确恢复依赖于压栈的EBP值的正确性,但EBP域邻近局部变量,若编程中有意无意地通过局部变量的地址偏移窜改EBP值,则程序的行为将变得非常危险. 由于C/C++语言没有数组越界检查机制,当向局部数组缓冲区里写入的数据超过为其分配的大小时,就

  • 详解C语言数组越界及其避免方法

    所谓的数组越界,简单地讲就是指数组下标变量的取值超过了初始定义时的大小,导致对数组元素的访问出现在数组的范围之外,这类错误也是 C 语言程序中最常见的错误之一. 在 C 语言中,数组必须是静态的.换而言之,数组的大小必须在程序运行前就确定下来.由于 C 语言并不具有类似 Java 等语言中现有的静态分析工具的功能,可以对程序中数组下标取值范围进行严格检查,一旦发现数组上溢或下溢,都会因抛出异常而终止程序.也就是说,C 语言并不检验数组边界,数组的两端都有可能越界,从而使其他变量的数据甚至程序代码

  • 详解go语言 make(chan int, 1) 和 make (chan int) 的区别

    遇到golang channel 的一个问题:发现go 协程读取channel 数据 并没有按照预期进行协作执行. 经过查资料: 使用channel 操作不当导致,channel分 有缓冲区 和 无缓冲区 , 以下是两者的区别. 无缓冲区channel 用make(chan int) 创建的chan, 是无缓冲区的, send 数据到chan 时,在没有协程取出数据的情况下, 会阻塞当前协程的运行.ch <- 后面的代码就不会再运行,直到channel 的数据被接收,当前协程才会继续往下执行.

  • 一文详解C语言中文件相关函数的使用

    目录 一.文件和流 1.程序文件 2.数据文件 3.流 二.文件组成 三.文件的打开和关闭 1.文件的打开fopen 2.文件关闭fclose 四.文件的顺序读写 1.使用fputc和fgetc写入/读取单个字符 2.使用fputs和fgets写入/读取一串字符 3.使用fprintf和fscanf按照指定的格式写入/读取 4.使用fwrite和fread按照二进制的方式写入/读取 5.使用sprintf和sscanf将格式化数据和字符串互相转换(文件无关) 五.文件的随机读写 1.fseek(

  • 详解C语言内核字符串拷贝与比较

    在上一篇文章<驱动开发:内核字符串转换方法>中简单介绍了内核是如何使用字符串以及字符串之间的转换方法,本章将继续探索字符串的拷贝与比较,与应用层不同内核字符串拷贝与比较也需要使用内核专用的API函数,字符串的拷贝往往伴随有内核内存分配,我们将首先简单介绍内核如何分配堆空间,然后再以此为契机简介字符串的拷贝与比较. 首先内核中的堆栈分配可以使用ExAllocatePool()这个内核函数实现,此外还可以使用ExAllocatePoolWithTag()函数,两者的区别是,第一个函数可以直接分配内

  • 详解C语言函数返回值解析

    详解C语言函数返回值解析 程序一: int main() { int *p; int i; int*fun(void); p=fun(); for(i=0;i<3;i++) { printf("%d\n",*p); p++; } return 0; }; int* fun(void) { static int str[]={1,2,3,4,5}; int*q=str; return q; } //不能正确返回 虽然str是在动态变量区,而该动态变量是局部的,函数结束时不保留的.

  • 详解C语言 三大循环 四大跳转 和判断语句

    三大循环for while 和 do{ }while; 四大跳转 : 无条件跳转语句 go to; 跳出循环语句 break; 继续跳出循环语句 continue; 返回值语句 return 判断语句 if,if else,if else if else if...else ifelse 组合 if(0 == x) if(0 == y) error(): else{ //program code } else到底与那个if配对 C语言有这样的规定: else 始终与同一括号内最近的未匹配的if语

  • 详解C语言gets()函数与它的替代者fgets()函数

    在c语言中读取字符串有多种方法,比如scanf() 配合%s使用,但是这种方法只能获取一个单词,即遇到空格等空字符就会返回.如果要读取一行字符串,比如: I love BIT 这种情况,scanf()就无能为力了.这时我们最先想到的是用gets()读取. gets()函数从标准输入(键盘)读入一行数据,所谓读取一行,就是遇到换行符就返回.gets()函数并不读取换行符'\n',它会吧换行符替换成空字符'\0',作为c语言字符串结束的标志. gets()函数经常和puts()函数配对使用,puts

  • 详解C 语言项目中.h文件和.c文件的关系

    详解C 语言项目中.h文件和.c文件的关系 在编译器只认识.c(.cpp))文件,而不知道.h是何物的年代,那时的人们写了很多的.c(.cpp)文件,渐渐地,人们发现在很多.c(.cpp)文件中的声明语句就是相同的,但他们却不得不一个字一个字地重复地将这些内容敲入每个.c(.cpp)文件.但更为恐怖的是,当其中一个声明有变更时,就需要检查所有的.c(.cpp)文件. 于是人们将重复的部分提取出来,放在一个新文件里,然后在需要的.c(.cpp)文件中敲入#include XXXX这样的语句.这样即

  • 详解C语言用malloc函数申请二维动态数组的实例

    详解C语言用malloc函数申请二维动态数组的实例 C语言在程序运行中动态的申请及释放内存十分方便,一维数组的申请及释放比较简单. Sample one #include <stdio.h> int main() { char * p=(char *)malloc(sizeof(char)*5);//申请包含5个字符型的数组 free(p); return 0; } 是否申请二维动态内存也如此简单呢?答案是否定的.申请二维数组有一下几种方法 Sample two /* 申请一个5行3列的字符型

随机推荐