前言
本文涉及 read函数
fgets函数
scanf函数
以及 gets函数
获取字符串后内存的区别,以及在pwntools中使用 sendline
和 send
的区别。实验过程有些冗长,嫌麻烦的师傅可以直接查看下面的总结
实验目标
read
fgets
scanf
这种可以限定大小的输入,如果输入量小于/等于/大于它们的输入量分别会出现什么情况scanf("%s",&buf)
这种情况是否存在溢出gets
输入是怎么处理\n
符号的
下面是我实验用的代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>int init()
{setvbuf(stdin, NULL, _IONBF, 0);setvbuf(stdout, NULL, _IONBF, 0);setvbuf(stderr, NULL, _IONBF, 0);return 0;
}
char bss[8];
int main() {init();char *ptr[13];// 初始化bss数组memset(bss, 0x22, sizeof(bss)*3);// 利用循环申请12个0x30大小的堆块,并将对应地址存储在ptr这个指针数组里面// 如果申请失败则直接退出程序for (int i = 0; i < 13; i++) {ptr[i] = (char *)malloc(0x30);if (!ptr[i]) {return 1;}}// 不用在意,只是gdb调试时方便跳转的标记sleep(0.1);// 使用memset将堆块的内容设置为\x22,为了方便我们区分哪些是我们输入的东西for (int i = 0; i < 13; i++) {memset(ptr[i], 0x22, 0x30);}sleep(0.1);// 这里free是因为free之后的堆块在pwndbg里面显示绿绿的,方便进行区分不同函数输入free(ptr[3]);free(ptr[7]);free(ptr[11]);sleep(0.1);// step 1 测试read函数,限定输入大小为8字节,查看分别输入 a*6 a*8 a*10 后内存的样子printf("read a*6\n");read(0, ptr[0], 8);while (getchar() != '\n' && getchar() != EOF); // 为了防止残留的\x0a影响后续的输入,这里采用了getchar来把多余的\x0a吃掉printf("read a*8\n");read(0, ptr[1], 8);while (getchar() != '\n' && getchar() != EOF);printf("read a*10\n");read(0, ptr[2], 8);while (getchar() != '\n' && getchar() != EOF);sleep(0.1);// step 2 测试fgets函数,限定输入大小为8字节,查看分别输入 a*6 a*8 a*10 后内存的样子printf("fgets a*6\n");fgets(ptr[4], 8, stdin);while (getchar() != '\n' && getchar() != EOF);printf("fgets a*8\n");fgets(ptr[5], 8, stdin); while (getchar() != '\n' && getchar() != EOF);printf("fgets a*10\n");fgets(ptr[6], 8, stdin);while (getchar() != '\n' && getchar() != EOF);sleep(0.1);// step 3 测试scanf函数,限定输入的字符串大小为8字节,查看分别输入 a*6 a*8 a*10 后内存的样子printf("scanf a*6\n");scanf("%8s", ptr[8]);while (getchar() != '\n' && getchar() != EOF);printf("scanf a*8\n");scanf("%8s", ptr[9]);while (getchar() != '\n' && getchar() != EOF);printf("scanf a*10\n");scanf("%8s", ptr[10]);while (getchar() != '\n' && getchar() != EOF);sleep(0.1);// step 4 测试gets函数输入之后是什么样子的,仅输入一次,输入6个字节的aprintf("gets a*6\n");gets(ptr[12]);while (getchar() != '\n' && getchar() != EOF);sleep(0.1);// 测试scanf("%s",bss)是否存在溢出scanf("%s",bss);printf("%s",bss);while (getchar() != '\n' && getchar() != EOF);sleep(0.1);return 0;
}
下面我们开始测试:
start
开头在sleep上打断点,方便我们后续使用c快速跟进,查看到bss全局变量在 0x555555558050
这个位置上
来到第一个sleep所在的地方
查看bss段里面是什么,可以看到已经都变成22了,后面输入后改变我们就可以直观的看到
来到第二个sleep所在的地方
查看堆中内存,可以看到已经都变成22了,后面输入后改变我们就可以直观的看到
来到第三个sleep所在的地方
查看堆的情况
read函数
继续跟进程序,让我们看看read,分别输入 aaaaaa
aaaaaaaa
aaaaaaaaaa
这里仔细的人会发现,read a*6
后面到 read a*8
中间除了输入还空了一行,这是因为包括 \n
在内我们输入的全部东西都被读入了内存中,getchar
没有读取到\n
所以我手动敲了一个回车来结束 getchar
来到第四个sleep所在的地方
我们已经使用read输入了,下面我们来查看一下堆内存里面是什么
可以看到输入六个a的话内存里面是 六个a和一个回车符号
输入八个a的话内存里面是 八个a,回车符号没有被读入
输入是十个a的话内存里面是 十个a,回车符号没有被读入
虽然实验很简陋但是我们可以简单得出结论,read输入的话会读取指定的字节数,除非遇到\x00
,不然输入其他东西都无法阻止read停止,直到读取完后放入指定的内存中,没读取到的部分则保持原样不动
fgets函数
继续跟进程序,让我们看看fgets,再次分别输入 aaaaaa
aaaaaaaa
aaaaaaaaaa
查看对应的堆内存
我们不难简单的得出结论,fgets和read不太一样,虽然我们设定了读取8个字节,他并不会老实读取8个字节,而是只读取7个,然后再主动添加一个\x00
来作为输入的字符串的结尾
因此像read这样的输入函数,如果buf为8字节的字符串,用户输入8字节,而read傻傻的读取八字节,那么字符串结尾的\x00
就会被省略掉,如果此时有一个 buf2
和 buf
的位置相邻,那么使用puts输出buf的时候就会连同buf2一起输出出来,因为buf失去了\x00
,puts这类输出函数就没办法判断这个字符串什么时候结束,只能无脑输出,直到遇见\x00
scanf函数
继续跟进程序,让我们看看scanf,再次分别输入 aaaaaa
aaaaaaaa
aaaaaaaaaa
查看对应的堆内存
可以发现我们的scanf函数非常的聪明,他不像fgets函数偷工减料,让他读取8字节他是真读取
并且同时他读取完八字节后,会再在后面加一个\x00
来保持字符串的独立性
继续跟进程序,让我们看看gets,输入 aaaaaa
查看堆内存
scanf函数的溢出
最后来看看scanf("%s",bss)的溢出形式
总结
read函数
:第三个参数是几就读取几个,一旦数量超过第三个参数后就直接忽略,就只读取到第三个参数所规定的数量为止,不会自动添加\x00
,如果 允许输入长度
=
字符串长度
可能导致字符串失去结尾的\x00
fgets函数
:读取的字节数为第二个参数-1
,超出的部分会被忽略,会自动在最后添加\x00
scanf("%?s", &buf)
:读取?个字节,超出的部分会被忽略,然后在最末尾添加\x00
。scanf("%s", &buf)
没有规定读取字节数,存在溢出
gets函数
:无脑读取,一直读取,直到用户输入回车才停止,输入的数据全部读取,但是最后会把读取到的回车(\x0a
)变成\x00
对于上面函数如何选择 send
和 sendline
-
对于read函数
send
和sendline
都可以,但是建议使用send
,不然屁股后面多个\x0a
,有时候打入/bin/sh
字符串的时候使用sendline没发现有个\x0a
在屁股上一直打不通也是很痛苦的 -
对于
fgets
gets
scanf
这三个函数只能使用sendline
什么?你问我上面的 send
和 sendline
是怎么总结出来的?
由于时间关系(懒得再做一遍了),直接看ZikH26师傅的博客来总结了探究pwntools中sendline的回车所造成的影响(什么时候用sendline,什么时候用send) - ZikH26 - 博客园 (cnblogs.com)
(不要喷我啊,求求了我什么都会做的Orz
)
最后感谢您的观看