Secure Coding in C and C ++ (六)对数组/字符串的思考

0x0 前言

经过前期简单的铺垫,我们来正式学习C++的安全
首当其冲的就是简单的字符串了。
先来看一下C++中的字符串
C++的字符串来自于C语言
都是以null(\0)结尾的
而且可以理解为都存放在数组中
在这里插入图片描述
来查看以下程序

#include<iostream>
int main()
{
    int arrary[] = {1,2,3,4,5,6};
    std::cout<<&arrary[0]<<std::endl;
    std::cout<<&arrary[1]<<std::endl;
    std::cout<<&arrary[2]<<std::endl;
}

这里我们创造了一个数组 array 然后给他初始化

预期的输出结果应该是地址,
这里的arrary【】 其实就相当于 一个指针,指针的内容是&arrary[0]
这样就好理解了
不妨来直接输出一下

std::cout<<arrary<<std::endl;

结果是一样的
在这里插入图片描述
其实对于字符串也是同样的道理
我们进行猜想:是不是字符串
char st[]="hello"
也是这样的呢?
st其实是&st[0]?
答案是否定的:
在这里插入图片描述

在这里插入图片描述
这是因为在使用std::cout进行输出时,会默认将字符串指针进行解释,解释称为C语言风格的字符串的内容
我们使用以下的方法即可:
std::cout<<(void*)&st[0]<<","<<(void*)&st[1]<<std::endl;
在这里插入图片描述
即强制类型转换
就可以知道其首地址了。这里由于是char类型的,所以地址占了1
然后字符串的其他用法:
strcmp strcat strchr strstr strcpy的用法如下:

序号函数目的
1strcpy(s1, s2)复制字符串 s2 到字符串 s1
2strcat(s1, s2)连接字符串 s2 到字符串 s1 的末尾。连接字符串也可以用 + 号,例如: string str1 = "runoob"; string str2 = "google"; string str = str1 + str2;
3strlen(s1)返回字符串 s1 的长度。
4strcmp(s1, s2)如果 s1s2 是相同的,则返回 0;如果 s1 < s2 则返回值小于 0;如果 s1 > s2 则返回值大于 0。
5strchr(s1, ch)返回一个指针,指向字符串 s1 中字符 ch 的第一次出现的位置。
6strstr(s1, s2)返回一个指针,指向字符串 s1 中字符串 s2 的第一次出现的位置。

标准的C语言库中支持char 和 wchar_t
一些专业词汇

  • Bound 界限 :指数组中的元素个数
  • Lo 低位地址:指数组的首元素的地址 即 &arrary[0] 也就是 arrary
  • Hi 高位地址 : 指数组的最后一个元素,末元素的地址
  • Toofar :指Hi后面的一个地址
  • 目标大小 Tsize : 等同于 sizeof
  • 空字符结尾 Null-terminated : 在Hi或者Hi之前的位置,存在空格终结符 \0
  • 长度 :Null-terminated 之前的个数、字符数量
    好,让我们来深入的探讨下字符串和数组

0x1.关于数组的界限确定问题

先来查看以下函数:

void clear(int array[])
{
    for(size_t i =0;i<sizeof(array)/sizeof(array[0]);++i)
    {
        array[i]=0;
        
    }
}

这里的函数的作用是将目标数组进行初始化,根据我上面的结论,是否存在一些问题呢?
问题出现在对于i的范围,也就是数组的大小上。
这里将array作为参数传入函数中,当数组作为函数的参数时,会退化为指针。
因此这里的sizeof(array)其实就等价于sizeof(int*)也就是4
随后sizeof(array[0])其实就等价于sizeof(int)也是4
因此这里运行的结果显然就是1了。
如果我们将剩余的部分给出:

void clear(int array[])
{
    for(size_t i =0;i<sizeof(array)/sizeof(array[0]);++i)
    {
        array[i]=0;
        
    }
}
void dowork(void)
{
    int ary[12];
    clear(ary);
}

那么这里在执行dowork函数时,里面的clear函数会只初始化数组的Lo。
这就是值得注意的一个问题。
那么如果不作为参数传入函数中,在正常的过程中可以用sizeof(array)/sizeof(array[0])来进行数组的大小确定吗?

int main(){
    int arry[12];
    for(size_t i =0 ;i<sizeof(arry)/sizeof(arry[0]);i++)
    {
        arry[i]=6;
    }
    
}

是可以的。
首先我们要知道sizeof(arry)的返回结果是整个数组的大小,而sizeof(arry[0])返回的是数组第一个元素的大小。这样就很合理了。
我们如果想在函数中进行数组的传参,应该这样写:

void clear(int array[],size_t length)
{
    for(size_t i =0;i<length;++i)
    {
        array[i]=0;

    }
}

不直接在函数内使用sizeof,而是传入数组的大小。就可以避免这一问题。

好,让我们把关注点放到strlen()这个函数上。
在这里插入图片描述
结果是:5
为什么呢?
这是因为strlen()这个函数遇到\0就会停止
这里就是5了

0x2.字符集的概念

很多书中对于字符集的概念晦涩难懂,似乎有着防自学机制。
这里来通俗易懂的说一下:

  1. 字符集是什么?
    字符集就像一本“字典”,它定义了计算机如何将文字和符号存储为数字。例如,字母“A”在计算机中可能被存储为65,空格字符被存储为32。字符集就是规定了这些文字或符号对应哪个数字。
  2. 基本字符集 vs. 扩展字符集
    • 基本字符集:这就像字典里的“核心词汇”,包含了最常用的字母、数字和标点符号。例如,英文字母(A-Z,a-z)、数字(0-9)和一些常见的符号(如@, #, !)都在这个集合里。通常,一个字母或符号用一个字节(8位)来存储,像ASCII就是这样一个基本字符集。
    • 扩展字符集:这是字典里的“高级词汇”,包括了特定语言的特殊字符或符号。比如,西班牙语中的ñ、德语中的ß、或者中文的汉字。这些字符不在基本字符集中,所以需要用更多的字节来表示,通常称为“多字节字符集”。
  3. 多字节字符集(MBCS)
    多字节字符集就像一本大字典,因为语言中的字符非常多,不可能每个字符都用一个字节表示,所以有些字符需要用多个字节来存储。比如,汉字在很多字符集中需要2个或更多字节来表示。
  4. 本地化 (Locale)
    本地化可以理解为字典的“地区版本”。不同的地区或语言可能会使用不同的字符集,甚至同样的字符在不同地区的字典里有不同的解释。C语言中的 setlocale() 函数可以切换这种“地区版本”,从而改变程序处理字符的方式。
  5. 例子:
    • 基本字符集:想象你正在用一个基础的英语字典,它可以解释字母A-Z,数字0-9,和一些常用符号。你知道每个字母都用一个字节来表示。
    • 扩展字符集:突然你需要查找一个法语单词的字母“é”,这时候你就需要一个包含扩展字符的字典,这个字典可能需要多个字节来表示这个特殊字母。

0x3.UTF-8

对于 UTF-8来说,其本身就是一个扩展字符集,可以表示每个 Unicode 中的字符集,每个占据1-4 个 bytes
也可以兼容早期的 7-bitsUS.ASCII
是如何进行兼容的呢? 对于只需要一个字节的来说,其最高位为 0 剩余的 7bits 都为 0或 1
这样就兼容了 7-bits 的 ASCII 了
这也就是其对单字节字符的兼容了

那么对于多字节字符是如何进行处理的呢?
这里拿笑脸符号来举例:
笑脸符号的Unicode 码点是:U+263A
需要三个字节的字节符
所以其对应的编码格式为:
1110xxxx 10xxxxxx 10xxxxxx
随后将其二进制写出来;
0010 0110 0011 1010
随后补全到x里(直接填入到 x 里就好了,按顺序)
11100010 10011000 10111010
这就是其对应的 UTF-8 了
这里是用 UTF-8 指南中的一句话来解释:

Consequently, a byte with lead bit 0 is a single-byte code, a byte with multiple leading 1 bits is the first of
a multibyte sequence, and a byte with a leading 10-bit pattern is a continuation byte of a multibyte
sequence. The format of the bytes allows the beginning of each sequence to be detected without decoding
from the beginning of the string.

也就是说,是几个字符,前面的一个字节内就有几个 1 比如这里的三个字节,就是 1110
随后后面的字节都要以 10 开头.
这样在检查的时候就可以发现其是不是开头和是不是中间部分的字节了
如图标中所示
在这里插入图片描述
如果我们不构建一个严谨的 UTF-8体系的话,我们可能会收到一些攻击:
UTF-8 Smuggling

0x4. UTF-8 Smuggling

这里使用之前的理论和前几篇文章中所讲解的 C ++语法来编写一个简单的 UTF-8解码器:

bool isValidUTF8(const std::string &str) {
    int bytes = 0;
    for (unsigned char c : str) {
        if (bytes == 0) {
            if ((c >> 5) == 0b110) {
                bytes = 1;
            } else if ((c >> 4) == 0b1110) {
                bytes = 2;
            } else if ((c >> 3) == 0b11110) {
                bytes = 3;
            } else if ((c >> 7) != 0) {
                return false;
            }
        } else {
            if ((c >> 6) != 0b10) {
                return false;
            }
            --bytes;
        }
    }
    return bytes == 0;
}

道理就是对每个字符进行遍历,随后进行比较,查看其位数.
以下是一个简单的栈溢出的例子:

#include <iostream>
#include <cstring>
void unsafeUTF8Decoder(const char* input) {
    char buffer[10];  // 固定大小的缓冲区
    int index = 0;
    while (*input) {
        unsigned char c = *input;
        int bytes = 0;
        if ((c >> 5) == 0b110) {
            bytes = 2;
        } else if ((c >> 4) == 0b1110) {
            bytes = 3;
        } else if ((c >> 3) == 0b11110) {
            bytes = 4;
        } else {
            bytes = 1;
        }
        // 不安全的操作:未检查缓冲区是否溢出
        for (int i = 0; i < bytes; ++i) {
            buffer[index++] = *input++;
        }
        // 将缓冲区内容输出
        buffer[index] = '\0';
        std::cout << "Decoded character: " << buffer << std::endl;
        // 重置索引
        index = 0;
    }
}
int main() {
    const char* input = "\xF0\x90\x80\x80"  // 合法的4字节UTF-8字符 (U+10000)
                        "\xC3\x28"          // 非法的2字节UTF-8字符
                        "\xE2\x82\x28";     // 非法的3字节UTF-8字符

    std::cout << "Input string: " << input << std::endl;

    unsafeUTF8Decoder(input);

    return 0;
}

0x5.Wide Strings

WideStrings其实就是宽字符串了,
宽字符(wide character):
• 在某些情况下,为了处理一个包含大量字符的字符集(如 Unicode),程序会用“宽字符”来表示每个字符。与普通字符(通常为 8 位,即 1 字节)相比,宽字符一般占用更多的空间。
• 宽字符的典型实现为 16 位或 32 位,这意味着每个宽字符需要 2 或 4 个字节来表示。
宽字符的结构是:一组连续的宽字符序列,以一个空宽字符(null wide character)作为结束符。这个空宽字符的作用类似于普通字符串中的 \0,标识字符串的结尾。就跟数组是差不多的,个人感觉唯一的区别就是占的字节数的不同了。

#include<iostream>
#include<cwchar>
int main()
{
   wchar_t wc[] =L"hello";
   std::wcout<<L"Length: "<<wcslen(wc)<<std::endl;
   std::wcout<<L"value: "<<wc<<std::endl;
   wchar_t * p = wc;
   std::wcout<<(void*)p<<std::endl;
   std::wcout<<(void*)&wc[2]<<std::endl;
}

结果为:
在这里插入图片描述
来对比下常规的字符串,查看其长度/大小是否有变化

 const char st[] ="hello";
    std::cout<<st<<","<<(void*)&st[0]<<","<<(void*)&st[1]<<std::endl;

在这里插入图片描述
很明显,普通的字符串里面的字符是 1byte ,而我们的宽字节wchar_t占了 4 字节.
这就是所谓的宽了

0x5-1 字面量

所以,不管是宽字符或者是字符,都有字面量.
这里的字面量其实是一致的.就比如普通的 char 里面的 abc
与宽字符的 L"abc"的字面量是一样的,唯一的区别就是前面的 L
普通的字符使用的是 ASCII
而宽字符使用的是UTF-16 或 UTF-32
都是以\0结尾的

0x5-2 字面量的连接

如"a" “b” "c"为例
字符的连接是 “abc”
宽字符就可以花里胡哨了,只要有一个字符是宽字节,如:

L"a" "b" "c"
"a" L"b" "c"
"a" "b" L"c"

其结果都是L"abc"
C中的字符串是可以修改面量值的
但是 C++不可以,C++中的字符串使用的是 const char []
所以无法对值进行修改
如果想要进行修改,可以使用 string 类

#include<iostream>
#include<string>
int main()
{
    std::string st = "hello";
    st[0] = 'H';
    std::cout<<st<<std::endl;
}

在这里插入图片描述
来看下比较神奇的地方吧
在这里插入图片描述
这两个字符串的不同在于其数组的大小,abc 占了三个,但是还有结尾的\0 也占了一个,所以就多一个了.
这样写的保险起见的是可以防止\0结尾被删掉
但是更具安全性和灵活性以及简便性的写法为:

consr char st[] = "abc";

0x6. 字符类型

字符类型有:

char signed char unsigned char 

这三种类型
建议使用 普通的 char ,这样在调用 strlen 等函数的时候就不用转换为 const char *了
在这里插入图片描述
因为会爆出警告哦~

然后来看下 int
在 C 中,字符常量的类型是 int。因此,对于所有的字符常量 c,sizeof© 等于 sizeof(int)。而在 C++ 中,单字符的字符字面量有 char 类型,其大小为 1。
在处理字符数据时,特别是当字符数据可能与 EOF(一个负值)混合时,int 类型被用来存储字符数据。这是为了防止符号扩展。函数如 fgetc()、getc()、getchar() 等会返回 int 类型,以便可以表示所有可能的字符值及 EOF。

unsigned char:
这种类型对于处理可能包含任意数据的对象很有用,因为它允许你访问所有的位,并且保证以纯二进制表示法存储。它保证没有填充位(padding bits)和陷阱表示(trap representation),因此可以安全地进行逐字节的操作,比如用 memcpy() 函数复制和检查数据。
wchar_t
用于处理自然语言字符数据,通常用于支持多字节字符集(如 Unicode)的字符串。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值