小Pwn手杂谈之不同输入函数之间的区别

前言

本文涉及 read函数 fgets函数 scanf函数 以及 gets函数 获取字符串后内存的区别,以及在pwntools中使用 sendlinesend 的区别。实验过程有些冗长,嫌麻烦的师傅可以直接查看下面的总结

实验目标

  1. read fgets scanf 这种可以限定大小的输入,如果输入量小于/等于/大于它们的输入量分别会出现什么情况
  2. scanf("%s",&buf)这种情况是否存在溢出
  3. gets 输入是怎么处理\n符号的

下面是我实验用的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#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个字节的a
    printf("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;
}
C

下面我们开始测试:

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就会被省略掉,如果此时有一个 buf2buf 的位置相邻,那么使用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):读取?个字节,超出的部分会被忽略,然后在最末尾添加\x00scanf("%s", &buf)没有规定读取字节数,存在溢出

gets函数:无脑读取,一直读取,直到用户输入回车才停止,输入的数据全部读取,但是最后会把读取到的回车(\x0a)变成\x00

对于上面函数如何选择 sendsendline

  1. 对于read函数 sendsendline 都可以,但是建议使用send,不然屁股后面多个\x0a,有时候打入/bin/sh字符串的时候使用sendline没发现有个\x0a在屁股上一直打不通也是很痛苦的

  2. 对于 fgets gets scanf 这三个函数只能使用sendline

什么?你问我上面的 sendsendline 是怎么总结出来的?
由于时间关系(懒得再做一遍了),直接看ZikH26师傅的博客来总结了探究pwntools中sendline的回车所造成的影响(什么时候用sendline,什么时候用send) - ZikH26 - 博客园 (cnblogs.com)
(不要喷我啊,求求了我什么都会做的Orz)

最后感谢您的观看


小Pwn手杂谈之不同输入函数之间的区别
http://example.com/2024/11/19/小Pwn手杂谈之不同输入函数之间的区别/
作者
XiDP
发布于
2024年11月19日
许可协议