• C/C++安全编码-字符串


    1 字符串

       

    1.1 字符串基础

    字符串提供命令行参数、环境变量、控制台输入、文本文件及网络连

    接,提供外部输入方法来影响程序的行为和输出,这也是程序容易出错的地方。字符串是一个概念,并不是C/C++内置类型,标准C语言库支持类型为char的字符串和类型为wchar_t的宽字符串。

    字符串由一个以第一个空(null)字符作为结束的连续字符序列组成,

    包含此空字符(所以sizeofstrlen会差1)。一个指向字符串的指针实际指向该字符串的起始字符。目标大小,指sizeof(array)大小,注意与元素个数区分。

       

    数组大小。数组带来的问题之一是确定其元素数量,例如下面的例子:

    void clear(int array[])

    {

    for (size_t i = 0; i < sizeof(array) / sizeof(array[0]); ++i)

    {

    array[i] = 0;

    }

    }

       

    void dowork()

    {

    int dis[12];

     

    clear(dis);

    /* ... */

    }

    array是一个参数,所以它的类型是指针。因此,sizeof(array)等于sizeof(int*),在x86 32机中,sizeof(array) / sizeof(array[0])计算结果都是1

       

    字符串字面值:简而言之就是在双引号中的值,在C中,字符串字面值的类型是一个char数组,但在C++中,它是一个const char数组。所以在C中可以修改字面值,但是程序如果试图去修改,该行为是未定义的。不要试图修改字符串字面值,编译器有时会把多个相同的字符串字面值存储在相同位置,例如只读存储器(ROM)中,看下面例子:

    const char *s1 = "abc";

    const char *s2 = "abc";

       

    char *s3 = "abc";

    char *s4 = "abc";

       

    char s5[] = "abc";

    char s6[] = "abc";

    比较地址会发现s1,s2,s3,s4相同,用这4个指针去改变字符串字面值是会出问题的。s5,s6值不同

    字符数组初始化不要指定一个用字符串字面值初始化的字符数组的界限

    const char s[3] = "abc"; //不安全写法,少一个''

    const char s[] = "abc"; //推荐初始化方式

       

    1.2 C++中的字符串

    C++标准类模板std::basic_string。简单来说就是string(basic_string<char>)

    wstring(basic_string<wchar_t>)basic_string的类的模版特化更不容易出现错误和安全漏洞,需要强调的是大多数C++字符串对象被视为不可分割的整体(通常按值传递和引用传递),内部字符串不一定是以空字符结束(大多数实现是以空字符结尾)C的库函数都接受以空字符结尾的字符序列指针。

       

    1.3 字符类型

    char 是 signed char 还是 unsigned char 可由编译器的配置项设定

    char有符号时,由unsigned char[]转换为const char *

    char无符号时,由singned char[] 转换为const char *

    如果不强制转换会有警告,建议使用普通的char

       

    1.4 字符串的长度

    混淆概念容易在CC++中导致严重的错误,

    wchar_t wide_str1[] = L"0123456789";

    wchar_t *wide_str2 = (wchar_t*)malloc(strlen(wide_str1) + 1);

    if(wide_str2 == NULL)

    {

    /*处理错误*/

    }

    free(wide_str2);

    wide_str2 = NULL;

    对一个以空字符结尾的字节字符串,strlen()统计终止空字节前面的字符数量。然而,宽字符可以包含空字节,所以计算结果会出问题。

    使用wcslen可以计算宽字符串的大小

    wchar_t wide_str1[] = L"0123456789";

    wchar_t *wide_str2 = (wchar_t*)malloc(wcslen(wide_str1) + 1);

    if(wide_str2 == NULL)

    {

    /*处理错误*/

    }

    free(wide_str2);

    wide_str2 = NULL;

    注意此长度没有乘sizeof(wchar_t),所以还是不对,下面值最终正确写法:

    wchar_t wide_str1[] = L"0123456789";

    wchar_t *wide_str2 = (wchar_t*)malloc((wcslen(wide_str1)+1)*sizeof(wchar_t));

    if(wide_str2 == NULL)

    {

    /*处理错误*/

    }

    free(wide_str2);

    wide_str2 = NULL;

       

    2 常见的字符串操作错误

       

    2.1 无界字符串复制

    void get_y_or_n()

    {

    char response[8];

    puts("Continue? [y] n:");

    gets(response);

    if(response[0] == 'n')

    exit(0);

     

    return;

    }

    其实gets()函数在C99中以废弃并在C11中淘汰。它没有提供方法指定读入的字符数的限制。这种限制在此函数的如下一致实现中是显而易见的:

    char *gets(char *dest)

    {

    int c = getchar();

    char *p = dest;

     

    while(c != EOF && c != ' ')

    {

    *p++ = c;

    c = getchar();

    }

    *p = '';

     

    return dest;

    }

    如果输入超出8个字符,那么会导致未定义的行为。不要从一个无界源复制数据到定长数组中,禁止这种方法。

    2.1.1 复制和连接字符串

    例如strcpy(), strcat(), sprintf(), 容易执行无界操作。例如:

    int main(int argc, char *argv[])

    {

    /*argc参数个数,argv参数数组*/

    }

    argc大于0,按照惯例,argv[0]指向的字符串是程序名。若argc > 1,argv[0]~argv[argc-1]引用的就是实际程序参数。

    当分配的空间不足以复制一个程序的输入,就会产生漏洞。攻击者可以控制argv[0]的内容

    int main(int argc, char *argv[])

    {

    /*argc参数个数,argv参数数组*/

    char prog_name[128];

    strcpy(prog_name, argv[0]);

    /* ... */

    }

    输入一个大于128个字节的字符,栈溢出,即缓冲区溢出漏洞。

    标准的写法应该是:

    int main(int argc, char *argv[])

    {

    /* 不要假设argv[0]不许为空 */

    const char *const name = argv[0]? argv[0] : "";

    char *prog_name = (char*)malloc(strlen(name)+1);

    if(prog_name != NULL)

    {

    strcpy(prog_name, name);

    }

    else

    {

    /* 复原 */

    }

    }

    其实还有一种方法可以避免溢出,通过设置域宽可以消除gets()的缺陷

    char buf[12];

    std::cin::width(12);

    std::cin >> buf;

    std::cout << buf << std::endl;

       

    2.2 差一错误

    简而言之就是从源字符串拷贝内容到目的字符串,刚好最后的''没有

    拷贝到目的字符串中,在这之后对目的串调用C语言库的函数可能会出问题,即空字符结尾错误,其余的还有字符串阶截断误差,越界操作等。

       

    2.3 字符串漏洞及其利用

    大体上就是缓冲区溢出(详细的可以自己网上查,有很多资料详细介

    ),栈溢出的话,可以把目标代码或者数据覆盖到栈里面,关于栈为什么会溢出,其实是因为在编译后,栈的大小就固定了。这种攻击方式也称注入,这里涉及到汇编以及底层的结构,不做详细解释,不过解决方法也有很多,要么做边界检查,要么动态的分配内存,还有更简单的那就是直接使用std::basic_string。当然使用string也会出问题,例如迭代器失效。

    char input[];

    string email;

    string::iterator loc = email.begin();

    //复制到string对象,同时把";" 转换成" "

    for (size_t i = 0; i < strlen(input); ++i)

    {

    if(input[i] != ";")

    email.insert(loc++, input[i]);

    else

    email.insert(loc++, ' ');

    }

    第一次insert之后,loc就已经失效,后面的insert都将产生未定义行为。正确的写法应该是

    char input[];

    string email;

    string::iterator loc = email.begin();

    //复制到string对象,同时把";" 转换成" "

    for (size_t i = 0; i < strlen(input); ++i)

    {

    if(input[i] != ";")

    loc = email.insert(loc, input[i]);

    else

    loc = email.insert(loc, ' ');

    ++loc;

    }

    当然在编程的时候引用边界之外的元素会抛出一个异常std::out_of _range。另外std::string.c_str()函数可以返回一个以空字符结尾的字符,const值,所以调用free()或者delete()会出错,需要修改则只能修改副本。

  • 相关阅读:
    LeetCode Trapping Rain Water
    [Elasticsearch] 部分匹配 (四)
    SPOJ VLATTICE Visible Lattice Points (莫比乌斯反演基础题)
    SICP 习题 (1.35)解题总结
    光流(optical flow)和openCV中实现
    LZMA C# SDK 结合 UPK 打包压缩 多目录 Unity3d实例
    tabhost实现android菜单切换
    12306火车票订票失败!您的身份信息未经核验,一般人是不能订票的,我订了,可是没成功。。。
    WebService学习之旅(四)Apache Axis2的安装
    WebService学习之旅(三)JAX-WS与Spring整合发布WebService
  • 原文地址:https://www.cnblogs.com/little-sjq/p/4070537.html
Copyright © 2020-2023  润新知