📜  C语言中的安全问题

📅  最后修改于: 2022-05-13 01:55:24.597000             🧑  作者: Mango

C语言中的安全问题

C 是一种非常强大且流行的编程语言。它最初是在 1970 年代开发的。 C语言用于编程网络驱动程序、解释器和编译器等。
尽管 C 语言广泛用于不同的系统,但它仍然存在许多与之相关的安全漏洞。本文重点讨论 C 语言中的安全漏洞。这些安全问题主要与易受攻击的库函数、数组和指针的无边界检查有关。

易受攻击的库函数:
CWE 代码:CWE – 242、CWE – 120、CWE-77

1.缓冲区和内存相关:

一种。 gets() 此函数是 C 语言标准输入输出库的一部分。它没有对缓冲区大小的任何检查,恶意输入很容易导致缓冲区溢出。
下面是演示上述概念的 C 程序 -

C
// C program to implement
// the above approach
#include 
 
// Driver code
int main()
{
    char buf[24];
    printf("Please enter your name and press \n");
    gets(buf);
    printf("%s", buf);
    return 0;
}


C
// C program to implement
// the above approach
#include 
#define MAX 15
 
// Driver code
int main()
{
    char buf[MAX];
    fgets(buf, MAX, stdin);
    printf("%s", buf);
    return 0;
}


C
// C program to implement
// the above approach
#include 
#include 
 
// Driver code
int main()
{
    char str1[2];
    char str2[] = "GeeksforGeeks";
    strcpy(str1, str2);
    printf("Copied string is: %s\n", str1);
    return 0;
}


C
// C program to implement
// the above approach
#include 
#include 
#define BUFFER_SIZE 24
 
// Driver code
int main()
{
    // 24 is the buffer size
    char str1[BUFFER_SIZE];
    char str2[] = "GeeksforGeeks";
 
    // Limits number of characters
    // to be copied
    strncpy(str1, str2, BUFFER_SIZE);
    printf("Copied string is: %s\n", str1);
    return 0;
}


C
// C program to implement
// the above approach
#include 
#include 
#include 
 
// Driver code
int main()
{
    char str[40];
    fgets(str, 39, stdin);
    system(str);
    printf("%s", str);
}


C
// C program to implement
// the above approach
#include 
 
// Driver code
int main()
{
    char str[5];
    sprintf(str, "%s",
            "GGGGGGGGGGGGGG");
    printf("%s", str);
}


C
// C program to implement
// the above approach
#include 
 
// Driver code
int main()
{
    int val = 1;
    int* p = NULL;
    *p = val;
    printf("%d", *p);
    return 0;
}


C
// C program to implement
// the above concept
#include 
 
// Driver code
int main()
{
    int val = 1;
    int* p = NULL;
    if (p == NULL) {
        printf("Pointer is NULL");
    }
    else {
        *p = val;
        printf("%d", *p);
    }
    return 0;
}


C
// C program to implement
// the above approach
#include 
int* fun()
{
    int y = 10;
    return &y;
}
 
// Driver code
int main()
{
    int* p = fun();
    printf("%d", *p);
    return 0;
}


C
// C program to implement
// the above approach
#include 
#include 
 
// Driver code
int main()
{
    int n = 5;
    int* ptr;
    ptr = (int*)malloc(n * sizeof(int));
 
    // Memory has been successfully allocated
    printf("Memory successfully allocated using malloc.\n");
 
    // Free the memory
    free(ptr);
    printf("Malloc Memory successfully freed.\n");
 
    // freed pointer set to NULL value
    ptr = NULL;
 
    return 0;
}


C
// Program to demonstrate
// accessing array out of bounds
#include 
int main()
{
    int arr[] = { 1, 2, 3, 4, 5 };
    printf("arr [0] is %d\n", arr[0]);
    printf("arr[10] is %d\n", arr[10]);
 
    // allocation memory to out of bound
    // element
    arr[10] = 11;
    printf("arr[10] is %d\n", arr[10]);
    return 0;
}


C
// C program to implement
// the above approach
#include 
int arr[5];
 
// Driver code
int main()
{
    int size = sizeof(arr) / sizeof(arr[0]);
 
    for (int i = 0; i < size; i++) {
        scanf("%d", &arr[i]);
    }
 
    // Print elements of array
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}


C
// C program to implement
// the above approach
#include 
int getValueFromArray(int* arr,
                      int size,
                      int index)
{
    int value = -1;
 
    // only maximum limit is there
    if (index < size) {
        value = arr[index];
    }
    return value;
}
 
// Driver code
int main()
{
    int arrtest[] = { 1, 2, 3, 4, 5 };
    int j = getValueFromArray(arrtest, 5, -1);
    printf("%d", j);
    return 0;
}


C
// C program to implement
// the above approach
#include 
int getValueFromArray(int* arr,
                      int size,
                      int index)
{
    int value = -1;
 
    // only maximum limit and minimum
    // limit is there
    if (index >= 0 && index < size) {
        value = arr[index];
    }
    return value;
}
 
// Driver code
int main()
{
    int arrtest[] = { 1, 2, 3, 4, 5 };
    int j = getValueFromArray(arrtest, 5, -1);
    printf("%d", j);
    return 0;
}


警告:

输出:

解释:
在上面的代码中,攻击者可以提供大量数据作为输入,gets()函数会尝试将所有数据存储到缓冲区中,而不考虑缓冲区大小,最终会导致缓冲区溢出情况,进而导致任意代码执行,信息泄露。

减轻:
用gets()函数解决这个问题。程序员可以使用 fgets()函数。它根据缓冲区大小限制输入长度。
下面是实现上述方法的C程序-

C

// C program to implement
// the above approach
#include 
#define MAX 15
 
// Driver code
int main()
{
    char buf[MAX];
    fgets(buf, MAX, stdin);
    printf("%s", buf);
    return 0;
}

输出:

解释:
在上面的代码中,fgets()函数将缓冲区大小“MAX”作为参数,它只会将可以安全存储到该大小的缓冲区中的数据写入而不会溢出。

湾。 strcpy() 这个内置函数也不检查缓冲区长度,并且可以根据恶意输入覆盖内存位置。如果 dest字符串的缓冲区大小大于 src字符串,则将 src字符串复制到 dest字符串以 NULL字符结尾。但如果 dest 缓冲区较小,则 src 将复制内容而不终止 NULL字符。字符串不能重叠,并且目标字符串必须足够大以接收副本。
下面是演示上述概念的 C 程序 -

C

// C program to implement
// the above approach
#include 
#include 
 
// Driver code
int main()
{
    char str1[2];
    char str2[] = "GeeksforGeeks";
    strcpy(str1, str2);
    printf("Copied string is: %s\n", str1);
    return 0;
}

输出:

解释:
在上面的代码中,strcpy()函数试图将 str2[] 上可用的字符串复制到 str1[],而 str1[] 在分配的缓冲区中没有足够的空间来处理最终将导致应用程序中的缓冲区溢出的缓冲区。

减轻:
使用 strncpy()函数代替 strcpy()。 strncpy() 根据可用的缓冲区大小限制输入的长度。
下面是演示上述概念的 C 程序 -

C

// C program to implement
// the above approach
#include 
#include 
#define BUFFER_SIZE 24
 
// Driver code
int main()
{
    // 24 is the buffer size
    char str1[BUFFER_SIZE];
    char str2[] = "GeeksforGeeks";
 
    // Limits number of characters
    // to be copied
    strncpy(str1, str2, BUFFER_SIZE);
    printf("Copied string is: %s\n", str1);
    return 0;
}

输出:

解释:
在上面的代码中,strncpy()函数将 BUFFER_SIZE 作为参数,它只会将可以安全存储到 str1[ ] 中的数据写入不会溢出缓冲区。使用 strcpy()函数将大字符数组复制到较小的字符数组是危险的,但如果字符串适合,则不值得冒险。如果目标字符串不足以存储源字符串,则 strcpy() 的行为未指定或未定义。

笔记:
具有相同类型漏洞的其他库函数 - calloc、malloc、realloc、strcat、memcpy。

2、命令执行漏洞:
如果攻击者可以控制命令文本或外部函数调用的参数,那么他就可以很容易地运行任意代码。
下面是演示上述概念的 C 程序 -

C

// C program to implement
// the above approach
#include 
#include 
#include 
 
// Driver code
int main()
{
    char str[40];
    fgets(str, 39, stdin);
    system(str);
    printf("%s", str);
}

运行时错误:

输出:

解释:
在上面的代码中,如果str被攻击者控制,那么他可以对系统执行任何命令,并且可以使整个系统处于危险之中。观察到的 CVE 是 CVE-1999-0067、CVE-2019-12921。

减轻:

  • 确保从程序调用的所有外部命令都是静态创建的。
  • 使用库调用而不是外部进程来重新创建所需的功能。

笔记:
其他具有相同类型漏洞的库函数:execl、execle、popen。

3.格式化字符串漏洞:
如果在代码中以不正确的方式使用诸如 %d、%s 之类的格式说明符,则可能会给攻击者带来优势,从而获得任意代码执行、信息泄漏的访问权限,并且还可以完全控制应用程序。如果成功,则返回写入的字符总数,不包括附加在字符串中的空字符,如果失败,则返回负数。
下面是演示上述概念的 C 程序 -

C

// C program to implement
// the above approach
#include 
 
// Driver code
int main()
{
    char str[5];
    sprintf(str, "%s",
            "GGGGGGGGGGGGGG");
    printf("%s", str);
}

运行时错误:

解释:
在上面的代码中,带有格式说明符 %s 的 sprintf函数可能会导致缓冲区溢出,因为输出大小为 15,大于接收到的仅大小为 5 的缓冲区的大小。
除此之外,str[] 也可以由外部代理控制并导致上述所有漏洞 (CWE – 13)。观察到的 CVE 有 CVE-2001-0717、CVE-2002-1788、CVE-2006-2480 等。

减轻:

  • 选择一种没有这种缺陷的语言。
  • 确保向所有格式字符串函数传递用户无法控制的静态字符串。
  • 在格式字符串中使用不支持 %n运算符的函数。

笔记:
其他具有相同类型漏洞的库函数:fprintf、printf、sprintf、snprintf。

指针的概念:
指针概念会导致 C 编程语言出现多个安全问题。

1. NULL 指针取消引用: CWE 代码:CWE-476。如果程序取消引用预期有效但结果为 NULL 的指针,则会导致程序崩溃,退出。对此的缓解是在执行任何操作之前检查 NULL 指针。观察到的 CVE 有 CVE-2005-3274、CVE-2005-1912、CVE-2004-0079 等。
下面是演示上述概念的 C 程序 -

C

// C program to implement
// the above approach
#include 
 
// Driver code
int main()
{
    int val = 1;
    int* p = NULL;
    *p = val;
    printf("%d", *p);
    return 0;
}

运行时错误:

解释:
在此代码中,*p 是一个 NULL 指针,这意味着它不指向内存位置,并且尝试取消引用它会导致程序中的意外行为或分段错误。

减轻:
以下是演示上述问题缓解策略的 C 程序 -

C

// C program to implement
// the above concept
#include 
 
// Driver code
int main()
{
    int val = 1;
    int* p = NULL;
    if (p == NULL) {
        printf("Pointer is NULL");
    }
    else {
        *p = val;
        printf("%d", *p);
    }
    return 0;
}

输出:

解释:
在此代码中,在对指针执行任何操作之前,对 NULL 指针执行第一次检查。这是 if 语句检查 NULL 条件,如果发现为真,则不会执行进一步的操作。

2. Free后使用(通常称为悬空指针): CWE CODE:CWE-416。如果一个引用内存被释放,然后有任何尝试再次释放它,那么它会导致这种情况。它可能导致程序崩溃、信息泄漏和数据损坏。观察到的 CVE 有 CVE-2010-4168、CVE-2010-2941、CVE-2010-2547 等。
下面是演示上述概念的 C 程序 -

C

// C program to implement
// the above approach
#include 
int* fun()
{
    int y = 10;
    return &y;
}
 
// Driver code
int main()
{
    int* p = fun();
    printf("%d", *p);
    return 0;
}

输出:

解释:
在上面的代码中,main()函数*p 包含 fun() 的返回值。随着 fun() 的调用,控件移动到 int *fun() 的上下文,fun() 返回 'y' 变量的地址,但在 main() 的上下文中,'y' 在之后不再可用程序流程返回。因此,可以说 *p 是一个悬空指针,因为它指向释放的内存。

减轻:
释放指针后将它们设置为 NULL。下面是演示上述概念的 C 程序 -

C

// C program to implement
// the above approach
#include 
#include 
 
// Driver code
int main()
{
    int n = 5;
    int* ptr;
    ptr = (int*)malloc(n * sizeof(int));
 
    // Memory has been successfully allocated
    printf("Memory successfully allocated using malloc.\n");
 
    // Free the memory
    free(ptr);
    printf("Malloc Memory successfully freed.\n");
 
    // freed pointer set to NULL value
    ptr = NULL;
 
    return 0;
}

输出:

解释:
在上面的代码中,在 free(ptr) 操作之后,ptr 指针值被设置为 NULL,这将减少悬空指针的机会。

无边界检查数组:

1. 越界写入: CWE 代码:CWE-787 在此,软件将数据写入预期缓冲区之前或之后。它可能导致执行未经授权的代码、崩溃和重新启动。观察到的 CVE 有 CVE-2020-0022、CVE-2009-0269、CVE-2009-1532 等。
下面是演示上述概念的 C 程序 -

C

// Program to demonstrate
// accessing array out of bounds
#include 
int main()
{
    int arr[] = { 1, 2, 3, 4, 5 };
    printf("arr [0] is %d\n", arr[0]);
    printf("arr[10] is %d\n", arr[10]);
 
    // allocation memory to out of bound
    // element
    arr[10] = 11;
    printf("arr[10] is %d\n", arr[10]);
    return 0;
}

运行时错误:

解释:
在上面的代码中,数组具有有效的索引 0、1、2、3 和 4,但尝试将值写入 arr[10]。由于 C 编译器不会检查数组绑定,它会在预期缓冲区之后写入该数据。但是当尝试打印索引 10 处的值时,它会显示错误。

减轻:

  • 始终确保缓冲区足够大。
  • 用于接受输入的函数应该有一个缓冲区限制实现。

下面是演示上述概念的 C 程序 -

C

// C program to implement
// the above approach
#include 
int arr[5];
 
// Driver code
int main()
{
    int size = sizeof(arr) / sizeof(arr[0]);
 
    for (int i = 0; i < size; i++) {
        scanf("%d", &arr[i]);
    }
 
    // Print elements of array
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    return 0;
}

输出:

解释:
在上面的代码中,sizeof运算符用于确定数组的大小,以便获得索引的有效范围。 sizeof(arr) 将给出数组的整个大小,而 sizeof(arr[0]) 将给出一个元素的大小,因此使用除法运算可以很容易地找到数组中元素的数量,然后是索引的范围.

2. 越界阅读: CWE 代码:CWE-125。在这种情况下,软件在预期缓冲区之前或之后读取数据。攻击者可能会使用它从其他内存位置读取敏感信息或可能导致崩溃。观察到的 CVE 是 CVE-2014-0160、CVE-2009-2523、CVE-2004-0184 等。
下面是演示上述概念的 C 程序 -

C

// C program to implement
// the above approach
#include 
int getValueFromArray(int* arr,
                      int size,
                      int index)
{
    int value = -1;
 
    // only maximum limit is there
    if (index < size) {
        value = arr[index];
    }
    return value;
}
 
// Driver code
int main()
{
    int arrtest[] = { 1, 2, 3, 4, 5 };
    int j = getValueFromArray(arrtest, 5, -1);
    printf("%d", j);
    return 0;
}

输出:

解释:
在上面的代码中,函数中的 if 语句只检查索引值是否小于数组的大小。攻击者可能会给出一个负值作为索引,即使该值无效仍然会执行读取操作,这最终会导致读取意外数据。

减轻:

  • 输入验证。
  • 确保验证参数长度、缓冲区大小等的正确计算。

下面是演示上述概念的 C 程序 -

C

// C program to implement
// the above approach
#include 
int getValueFromArray(int* arr,
                      int size,
                      int index)
{
    int value = -1;
 
    // only maximum limit and minimum
    // limit is there
    if (index >= 0 && index < size) {
        value = arr[index];
    }
    return value;
}
 
// Driver code
int main()
{
    int arrtest[] = { 1, 2, 3, 4, 5 };
    int j = getValueFromArray(arrtest, 5, -1);
    printf("%d", j);
    return 0;
}

输出:

解释:
在此代码中,在 if 语句中检查最大和最小索引范围。如果攻击者试图提供无效索引,则不会执行读取操作。