diff --git a/README.md b/README.md index 297679c..dbe32be 100644 --- a/README.md +++ b/README.md @@ -121,3 +121,16 @@ + +### 第六次课————c风格字符串 + +
+点击展开 +
+ +#### 讲义 + +[C语言进阶——C风格字符串](https://ucas-ctf.github.io/posts/2024/2_advanced_C/string) + +
+
diff --git a/posts/2024/2_advanced_C/code/string/strcat.c b/posts/2024/2_advanced_C/code/string/strcat.c new file mode 100644 index 0000000..d16898e --- /dev/null +++ b/posts/2024/2_advanced_C/code/string/strcat.c @@ -0,0 +1,25 @@ +#include +#include + +void read(char *str) { // 将字符串读入到从str开始的一段内存中 + int i = 0; + char c; + while ((c = getchar()) != '\n') { // 检查读入的字符是否等于约定的字符串结尾字符,这里是'\n' + str[i++] = c; + } + str[i] = '\0'; // 注意字符串总是以'\0'作为结束 +} + +int main() { + char str2[10], str1[10]; + printf("str1=%p\nstr2=%p\n", str1, str2); + puts("Please input the first string:"); + read(str1); + puts("Please input the second string:"); + read(str2); + printf("Content of the second string: %s\n", str2); + strcat(str1, str2); + printf("Result: %s\n", str1); + printf("Content of the second string: %s\n", str2); + return 0; +} \ No newline at end of file diff --git a/posts/2024/2_advanced_C/code/string/strcmp.c b/posts/2024/2_advanced_C/code/string/strcmp.c new file mode 100644 index 0000000..a7ce94d --- /dev/null +++ b/posts/2024/2_advanced_C/code/string/strcmp.c @@ -0,0 +1,32 @@ +#include +#include + +void read(char *str) { // 将字符串读入到从str开始的一段内存中 + int i = 0; + char c; + while ((c = getchar()) != '\n') { // 检查读入的字符是否等于约定的字符串结尾字符,这里是'\n' + str[i++] = c; + if (i >= 99) { // 检查字符串长度是否超出了可接受的范围 + while ((c = getchar()) != '\n') + ; // 空循环,清空输入缓存,防止影响下一次读入 + break; + } + } + str[i] = '\0'; // 注意字符串总是以'\0'作为结束 +} + +int main() { + char str1[100], str2[100]; + puts("Please input the first string:"); + read(str1); + puts("Please input the second string:"); + read(str2); + int ret = strcmp(str1, str2); + if (ret < 0) + puts("Compare Result: str1 < str2"); + else if (ret > 0) + puts("Compare Result: str1 > str2"); + else + puts("Compare Result: str1 = str2"); + return 0; +} \ No newline at end of file diff --git a/posts/2024/2_advanced_C/code/string/strcpy.c b/posts/2024/2_advanced_C/code/string/strcpy.c new file mode 100644 index 0000000..a44da4d --- /dev/null +++ b/posts/2024/2_advanced_C/code/string/strcpy.c @@ -0,0 +1,25 @@ +#include +#include + +void read(char *str) { // 将字符串读入到从str开始的一段内存中 + int i = 0; + char c; + while ((c = getchar()) != '\n') { // 检查读入的字符是否等于约定的字符串结尾字符,这里是'\n' + str[i++] = c; + } + str[i] = '\0'; // 注意字符串总是以'\0'作为结束 +} + +int main() { + char str2[10], str1[10]; + printf("str1=%p\nstr2=%p\n", str1, str2); + puts("Please input the first string:"); + read(str1); + puts("Please input the second string:"); + read(str2); + printf("Content of the second string: %s\n", str2); + strcpy(str1, str2); + printf("Result: %s\n", str1); + printf("Content of the second string: %s\n", str2); + return 0; +} \ No newline at end of file diff --git a/posts/2024/2_advanced_C/code/string/strlen.c b/posts/2024/2_advanced_C/code/string/strlen.c new file mode 100644 index 0000000..11d1d1b --- /dev/null +++ b/posts/2024/2_advanced_C/code/string/strlen.c @@ -0,0 +1,25 @@ +#include +#include + +void read(char *str) { // 将字符串读入到从str开始的一段内存中 + int i = 0; + char c; + while ((c = getchar()) != '\n') { // 检查读入的字符是否等于约定的字符串结尾字符,这里是'\n' + str[i++] = c; + if (i >= 99) { // 检查字符串长度是否超出了可接受的范围 + while ((c = getchar()) != '\n') + ; // 空循环,清空输入缓存,防止影响下一次读入 + break; + } + } + str[i] = '\0'; // 注意字符串总是以'\0'作为结束 +} + +int main() { + char str[100]; + puts("Please input a string:"); + read(str); + int len = strlen(str); + printf("The length of str is: %d\n", len); + return 0; +} \ No newline at end of file diff --git a/posts/2024/2_advanced_C/code/string/strstr.c b/posts/2024/2_advanced_C/code/string/strstr.c new file mode 100644 index 0000000..b425629 --- /dev/null +++ b/posts/2024/2_advanced_C/code/string/strstr.c @@ -0,0 +1,26 @@ +#include +#include + +void read(char *str) { // 将字符串读入到从str开始的一段内存中 + int i = 0; + char c; + while ((c = getchar()) != '\n') { // 检查读入的字符是否等于约定的字符串结尾字符,这里是'\n' + str[i++] = c; + } + str[i] = '\0'; // 注意字符串总是以'\0'作为结束 +} + +int main() { + char str2[10], str1[10]; + puts("Please input the first string:"); + read(str1); + puts("Please input the second string:"); + read(str2); + char *res = strstr(str1, str2); + if (res == NULL) { + puts("Not Found."); + } else { + printf("Found at %d.\n", (int)(res - str1)); + } + return 0; +} \ No newline at end of file diff --git a/posts/2024/2_advanced_C/string.md b/posts/2024/2_advanced_C/string.md new file mode 100644 index 0000000..c115c6a --- /dev/null +++ b/posts/2024/2_advanced_C/string.md @@ -0,0 +1,170 @@ +# C语言进阶:字符串 + +Author: [doyo](https://github.com/doyo2024) + +## C风格字符串 + +所谓字符串,顾名思义,就是一连串不间断的字符(注意,不间断不意味着不能有空格,事实上空格也是一种特殊的字符)。如果你学过OI,你可能经常使用一种“string”类型的字符串。但是,string是C++中才有的,在C语言中用不了。 + +在C语言中,我们使用的是一种被称之为“C风格字符串”的形式来描述字符串,即使用一个以“`\0`”结尾的`char`类型数组来存放字符串,例如: + +```c +char str[] = "Hello World!\0"; +``` + +`char`类型用于定义字符变量。一个`char`类型变量占用一个字节(即1B,8bit)的内存空间,可以存放一个ASCII字符。这里的“`\0`”是一个字符,前面的“`\`”是一个转义符,表示它后面的一个字符(例如这里的“`0`”)不表示其原本的意思,而是一个特殊含义(例如“`\n`”不表示小写字母n而是换行)。所以,“`\0`”表示的不是一个ASCII码为0x30的字符“0”,而是一个ASCII码为0x00的空字符。所以,我们也可以说“C风格字符串”是以空字符为结尾的字符串。 + +一般情况下,我们也常常会省略字符串末尾的这个“`\0`”,因为在计算机内部存储字符串常量时会自动为其添加“`\0`”,所以,我们更常使用的是下面这种等价的定义方式: + +```c +char str[] = "Hello World!"; +``` + +这个字符串长度为12(10个英文字母+1个空格+1个叹号),但实际需要占用13B的内存空间,因为“`\0`”也要占用1B的字节。 + +### 字符串的读入 + +C语言中,比较常见的有两种方式:`scanf()`函数和`getchar()`函数(我知道有同学会用`cin`,但那是C++才有的东西)。 + +`scanf()`函数的用法与读入整数时类似: + +```c +char str[100]; // 给要读入的字符串留够空间! +scanf("%s", str); +``` + +我们使用占位符“`%s`”来表示字符串。此处我们传入的第二个参数`str`已经是一个指针了,所以我们不需要再对它取一次地址。 + +使用上述方法时,默认读入的字符串以空格为结尾,毕竟你不可能在终端中输入空字符。但这也导致它在处理字符串读入时可能不够灵活(例如它必须用两个“`%s`”占位符才能读入“Hello World!”),所以,我们有时也会使用`getchar()`函数作为替代,例如: + +```c +void read(char *str) { // 将字符串读入到从str开始的一段内存中 + int i = 0; + char c; + while ((c = getchar()) != '\n') { // 检查读入的字符是否等于约定的字符串结尾字符,这里是'\n' + str[i++] = c; + if (i >= 99) { // 检查字符串长度是否超出了可接受的范围 + while ((c = getchar()) != '\n') + ; // 空循环,清空输入缓存,防止影响下一次读入 + break; + } + } + str[i] = '\0'; // 注意字符串总是以'\0'作为结束 +} +``` + +上面这种方法要灵活的多,因为它可以以你想要的字符来区分输入中的不同字符串,例如上例中展示的便是以“`\n`”来进行区分。你也可以使用其它字符,但注意,使用的字符必须是你能通过键盘输入的字符。 + +### 字符串的输出 + +字符串的输出要简单的多,使用`printf()`函数即可,程序会在遇到“`\0`”时自动停止输出: + +```c +printf("%s", str); +``` + +也可以使用`puts()`函数,这个函数的不同之处在于它会自动在字符串输出结束后进行换行: + +```c +puts(str); +``` + +## string.h与常见字符串处理函数 + +常见的字符串处理函数几乎都可以在string.h中找到。 + +### 获取字符串长度 + +字符串最基本的操作之一。 + +函数原型: + +```c +size_t strlen(const char *str) +``` + +该函数传入一个参数`str`,为目标字符串;返回一个整数值,表示`str`的长度(不计入字符串结尾的“`\0`”)。 + +[点击此处下载示例代码strlen.c。](/posts/2024/2_advanced_C/code/string/strlen.c) + +### 比较字符串 + +字符串也是可以比较的,`strcmp()`将两个字符串从左至右依次比较ASCII值大小,直至出现不同字符或到达某一字符串结尾(“`\0`”)。 + +函数原型: + +```c +int strcmp(const char *str1, const char *str2) +``` + +传入的两个参数是我们要比较的两个字符串;返回值是一个有符号整数,满足: + +* `str1 == str2` 时,返回0; +* `str1 < str2` 时,返回一个负数(不一定是-1); +* `str1 > str2` 时,返回一个整数(不一定是1)。 + +[点击此处下载示例代码strcmp.c。](/posts/2024/2_advanced_C/code/string/strcmp.c) + +### 连接字符串 + +连接字符串是指将两个字符串拼接在一起,例如将“Hello ”(注意结尾有空格)和“World!”拼成“Hello World!”。 + +函数原型: + +```c +char *strcat(char *dest, const char *src) +``` + +该函数将字符串src拼接在dest字符串的末尾;返回指向拼接得到的字符串的指针(其实就是dest)。 + +[点击此处下载示例代码strcat.c。](/posts/2024/2_advanced_C/code/string/strcat.c) + +如果你足够仔细和谨慎,你或许会发现这个函数有个致命的漏洞:没有限制字符串的长度。这可能导致拼接后字符串长度超出了为它预设的内存空间大小。 + +例如,`dest`字符串为“Hello ”,但我们只为它预留了7B的内存空间(刚好放下这个6个字符和作为结尾的“`\0`”);`src`字符串为“World!”;显然,此时将src拼接到dest之后,`dest`的长度便超出了我们实际允许它使用的内存空间大小。 + +上述这类问题就是大名鼎鼎的**缓冲区溢出(buffer overflow)**,缓冲区就是指我们在内存中划定的一块特定大小的区域(你也可以简单地理解成一个数组)。缓冲区溢出可以造成非常严重的危害。借由这一漏洞,攻击者可以非法覆写内存(不要忘记,任何数据在内存都以0和1的形式存在),从而破坏数据乃至改变程序行为。 + +在上述实例代码中,我们特意将字符串占用空间声明得很小,并且移除了对读入字符串长度的检查,大家可以尝试通过溢出攻击,通过strcat修改第二个字符串的数据。 + +在CTF中,缓冲区溢出问题也是PWN方向重点研究的问题之一,我们会在相关课程中对这类问题作更深入的探讨,敬请期待。 + +介于缓冲区溢出问题的存在,我们更推荐采用下面这个函数来进行字符串拼接: + +```c +char *strncat(char *dest, const char *src, size_t n) +``` + +这个函数引入了一个新的参数`n`,用来限制要追加的最大字符数。通过合理设置这个参数,可以有效应对缓冲区溢出的问题。 + +### 复制字符串 + +函数原型: + +```c +char *strcpy(char *dest, const char *src) +``` + +将`src`处的字符串复制到`dest`处;返回一个指向`dest`的指针。 + +[点击此处下载示例代码strcpy.c。](/posts/2024/2_advanced_C/code/string/strcpy.c) + +这个函数与前文所述的`strcat()`类似,也存在缓冲区溢出问题,因此,我们更推荐采用下面这个函数来进行字符串复制: + +```c +char *strncpy(char *dest, const char *src, size_t n) +``` + +类似地,第三个参数`n`用来限制能复制的最大字符数。 + +### 字符串查找 + +函数原型: + +```c +char *strstr(const char *haystack, const char *needle) +``` + +在`haystack`中查找`needle`是否出现,若是,返回指向第一次找到`needle`时的位置的指针;否则,返回空指针。 + +[点击此处下载示例代码strstr.c。](/posts/2024/2_advanced_C/code/string/strstr.c) \ No newline at end of file