一个 C 语言缓冲区溢出的问题 - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
tan90ds
V2EX    C

一个 C 语言缓冲区溢出的问题

  •  
  •   tan90ds 2015-05-04 11:43:12 +08:00 2199 次点击
    这是一个创建于 3864 天前的主题,其中的信息可能已经有所发展或是发生改变。

    没有任何C语言基础的楼主被布置了这么一道作业题(如果 gist 显示不出来,请原谅楼主…)

    https://gist.github.com/acc4tb/0a554e70c80dde814975

    这个程序是有问题的,我尝试以后发现 word 的长度大于等于263的时候程序就会崩溃(如果以 h 结尾则262字符即崩溃)。
    看起来问题是在 free(buf); 这一句上,但是楼主想不明白为什么是这个长度,为什么是在这里崩溃了。
    更加奇怪的是如果 buf 的长度是 n+1,那么当 word 的结尾是 "h" 的时候就会被附加上 "es",那么岂不是已经挤爆了申请的空间么?为什么测试起来是正常的呢。
    希望大家帮我解释一下,谢谢。

    17 条回复    2015-05-04 23:36:32 +08:00
    Valyrian
        1
    Valyrian  
       2015-05-04 11:56:04 +08:00   1
    把申请空间挤爆了的话,程序的行为属于undefined behavior,可能是正常也可能是崩溃。一句话就是不要挤爆。。

    具体为何在free崩溃属于malloc家族的原理。简单的讲就是overwrite了他管理用户申请内存使用的数据结构。想研究的话楼主可以看看libc的malloc: http://sourceware.org/git/?p=glibc.git;a=blob_plain;f=malloc/malloc.c;hb=HEAD
    Andiry
        2
    Andiry  
       2015-05-04 11:56:48 +08:00   1
    栈上分配的字符数组只有256B,往里面copy多于这个长度的数据自然就崩了,因为执行栈被你写坏了
    tan90ds
        3
    tan90ds  
    OP
       2015-05-04 12:11:02 +08:00
    @Valyrian 也就是说,没有一超过256就立即崩溃只是因为它暂时还没有把重要的数据覆盖是吧?


    @Andiry 所以我是破坏了 print_plural 这个过程的执行栈对吧?(忘记了栈是向哪个方向增长的了)
    ryd994
        4
    ryd994  
       2015-05-04 12:59:46 +08:00
    @tan90ds 不,是已经覆盖,但是一时间没人用到没人发现而已
    xieyudi1990
        5
    xieyudi1990  
       2015-05-04 14:37:30 +08:00   1
    @tan90ds @ryd994
    LZ已经说了是free的时候就出问题了, 所以栈帧被破坏只是个间接问题.

    buf引用的是用malloc分配的一段空间, 其大小是str引用的字符串的大小 (不包括末尾的0) + 1.
    也就是说, buf引用的空间最多只能放得下原字符串. 根据你下面的逻辑, 如果只要你的字符串不是以s结尾的, 都可能覆盖掉堆上的数据结构. 先把问题搞清楚.

    我想LZ的疑问是程序挂掉的规律.

    比如这种情况, "word 的长度大于等于263的时候程序就会崩溃"
    假设word字符串的长度就是263. 这是malloc传入的参数是264.
    buf[0:262] 是字符串, buf[263]被写入附加的's', 结尾的NUL刚好写到了buf[264]上, 破坏了堆的数据结构.

    另一种情况也类似.

    也就是说, 只有对buf[264]和以后的越界写才会出问题.
    考虑到这个地址刚好在4和8个边界上, 嗯, 建议你参考下malloc的实现.

    所以只有1L说到点上了.
    zhicheng
        6
    zhicheng  
       2015-05-04 14:40:20 +08:00   1
    目测到几个错误,楼下继续。我觉得我比较适合做老师,呵呵。
    行号:结果,原因
    10: Wrong,`plural` maybe NULL
    12: Wrong,`str` is pointer
    14: Wrong,`str` no guarantee always has '\0'
    17: Maybe Wrong,Integer Overflow
    18: Wrong,`buf` is pointer
    20: Wrong,`n` is not guarantee always `str` length
    25: Wrong,`buf` hasn't enough space
    27: Wrong,`buf` hasn't enough space
    29: Wrong,`plural` maybe NULL or hasn't enough space
    37: Wrong,`word_plural` no guarantee always has `\0`
    cover
        7
    cover  
       2015-05-04 14:48:29 +08:00   1
    这么说把 你申请的堆栈内数组在堆栈中是反向存放的。
    比如你这个程序的 word_plural存放的地址 的 256下标的位置 应该是 print_plural这个函数指针和 返回后 main函数的位置
    也就是说 你申请256的话 写256以上会访问到堆栈原来的数据 比如函数指针,入参等,但是报错并不是立刻的 因为你可能还没有碰到那个函数指针 或者需要访问的参数。所以一旦堆栈溢出以后,或者 你写堆栈内数组,访问到非法的下标以后,程序的情况是不可预测的。。不同的操作系统也可能表现不一致
    canautumn
        8
    canautumn  
       2015-05-04 15:05:50 +08:00   2
    gleport
        9
    gleport  
       2015-05-04 15:24:03 +08:00   1
    这是我以前的经验总结:内存越界问题的定位技巧: 越界操作的语句极有可能会成功执行, 没有任何出错提示, 但下一次申请内存或释放内存就会表现出来了, 比如越界之后进行malloc()会失败, 提示malloc.c:3096: sYSMALLOc: Assertion... 这种情况99%可能是前面发生了内存越界
    xieyudi1990
        10
    xieyudi1990  
       2015-05-04 15:52:09 +08:00   1
    我觉得这题其实是个操作系统和体系结构相关的问题, 绝对不是什么 "内存溢出" 这种常识问题.
    有点营养, 所以我实际试了下.

    (gdb) p n
    $2 = 263

    free前一刻堆上的情况:

    调用sprintf(buf, "%ss", str)前:
    (gdb) x/32xb buf+256
    0x600039460: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
    0x600039468: 0x51 0x6b 0x01 0x00 0x00 0x00 0x00 0x00
    0x600039470: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
    0x600039478: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

    调用sprintf(buf, "%ss", str)后:
    (gdb) x/32xb buf+256
    0x600039460: 0x36 0x37 0x38 0x39 0x31 0x32 0x33 0x73
    0x600039468: 0x00 0x6b 0x01 0x00 0x00 0x00 0x00 0x00
    0x600039470: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
    0x600039478: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

    猜测结尾的那个00016b51 (姑且算32位, 不过可能更长) 是malloc维护的堆的数据结构.
    堆上的数据结构被你末尾的那个0给破坏了. 这个被覆盖的数据多半是个整数 (我x86_64, 只有小端模式), 整数的低8位被清零, free时SIGABRT, 这就是为什么会出问题.

    剩下的问题就是是为什么依然越界时不会出问题.

    ------------------------------
    如果将字符串的长度改为262:
    (gdb) p n
    $1 = 262

    调用sprintf(buf, "%ss", str)前:
    (gdb) x/32xb buf+256
    0x600039460: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
    0x600039468: 0x51 0x6b 0x01 0x00 0x00 0x00 0x00 0x00
    0x600039470: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
    0x600039478: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

    调用sprintf(buf, "%ss", str)后:
    (gdb) x/32xb buf+256
    0x600039460: 0x36 0x37 0x38 0x39 0x31 0x32 0x73 0x00
    0x600039468: 0x51 0x6b 0x01 0x00 0x00 0x00 0x00 0x00
    0x600039470: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
    0x600039478: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

    虽然多写了个字节 (600039468), 但因为malloc至少按机器字对齐, 所以实际上还是分配了264个字节, 所以这次虽然越界了, 但没出问题, free正常返回.

    -----------------------------
    如果将字符串的长度改为259:
    (gdb) p n
    $1 = 259

    调用sprintf(buf, "%ss", str)前:
    (gdb) x/32xb buf+256
    0x600039460: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
    0x600039468: 0x51 0x6b 0x01 0x00 0x00 0x00 0x00 0x00
    0x600039470: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
    0x600039478: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

    按照这个规律, 我机器上是至少按照8自己对齐的 (不然结尾的那个00016b51就会是在600039464处而不是依然还在600039468)
    调用sprintf(buf, "%ss", str)后: 当然更不会崩溃.

    ------------------------------
    如果将字符串的长度改为255:
    (gdb) p n
    $1 = 255

    调用sprintf(buf, "%ss", str)后:
    (gdb) x/32xb buf+240
    0x600039450: 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37
    0x600039458: 0x38 0x39 0x30 0x31 0x32 0x33 0x34 0x73
    0x600039460: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
    0x600039468: 0x51 0x6b 0x01 0x00 0x00 0x00 0x00 0x00

    显然是越界了. 因为只分配了256个字节, 从+256的地方开始都是越界访问.
    从这里和前面的几个实验, 证实我机器上至少是16对齐, (不然结尾的那个00016b51就会是在600039460处而不是依然还在600039468) 所以也没问题.

    ------------------------------
    摘自glibc里的malloc.c:

    INTERNAL_SIZE_T size_t
    MALLOC_ALIGNMENT MAX (2 * sizeof(INTERNAL_SIZE_T), __alignof__ (long double))
    xieyudi1990
        11
    xieyudi1990  
       2015-05-04 15:55:43 +08:00
    @xieyudi1990 s/虽然多写了个字节 (600039468)/虽然多写了个字节 (600039467)/g


    @zhicheng gcc 4.9.2 没有错误.
    xieyudi1990
        12
    xieyudi1990  
       2015-05-04 15:58:36 +08:00
    回去看了下 "没有任何C语言基础的楼主被布置了这么一道作业题".
    感觉是我想多了. 不过亲手验证了下, 也好.
    zhicheng
        13
    zhicheng  
       2015-05-04 16:48:54 +08:00
    @xieyudi1990
    如果编译器能把潜在错误全都找出来,还要程序员干嘛。
    xieyudi1990
        14
    xieyudi1990  
       2015-05-04 16:52:41 +08:00 via iPhone
    @zhicheng 我感觉lz这道题是在考OS和组成原理. 这编译器有什么关系?
    zhicheng
        15
    zhicheng  
       2015-05-04 17:30:40 +08:00
    @xieyudi1990

    "@zhicheng gcc 4.9.2 没有错误."
    j16ZgMV9cs6ZB23n
        16
    j16ZgMV9cs6ZB23n  
       2015-05-04 21:38:09 +08:00   1
    @zhicheng 如有用gcc 4.9的话, 建议编译时候带上address-sanitizer,这类问题很容易查出。
    比如 假设源文件是 test.c, 那么用gcc -o test test.c -fsanitize=address 编译成test,然后运行./test。

    输出结果是 heap-buffer-overflow 堆区域overflow,如下:

    SUMMARY: AddressSanitizer: heap-buffer-overflow ??:0 wrap_strlen
    Shadow bytes around the buggy address:
    0x1c24000017a0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
    0x1c24000017b0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
    0x1c24000017c0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
    0x1c24000017d0: fa fa fa fa fa fa fa fa 00 00 00 00 00 00 00 00
    0x1c24000017e0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
    =>0x1c24000017f0: 00 00 00 00 00 00 00 00 00[fa]fa fa fa fa fa fa
    0x1c2400001800: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
    0x1c2400001810: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
    0x1c2400001820: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
    0x1c2400001830: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
    0x1c2400001840: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
    Shadow byte legend (one shadow byte represents 8 application bytes):
    Addressable: 00
    Partially addressable: 01 02 03 04 05 06 07
    Heap left redzone: fa
    Heap right redzone: fb
    Freed heap region: fd
    Stack left redzone: f1
    Stack mid redzone: f2
    Stack right redzone: f3
    Stack partial redzone: f4
    Stack after return: f5
    Stack use after scope: f8
    Global redzone: f9
    Global init order: f6
    Poisoned by user: f7
    Contiguous container OOB:fc
    ASan internal: fe
    ==43896==ABORTING
    Abort trap: 6

    如果没有高版本的gcc (至少4.8),或者clang,那用valgrind 也是可行的。
    tan90ds
        17
    tan90ds  
    OP
       2015-05-04 23:36:32 +08:00
    感谢各位的解答。这课只是泛泛地讲计算机安全,前边还在讲各种加密算法和协议,脚本注入什么的,后边就无视班里基本没人懂 C ,直接来了这样的内容。题目要求只是找找这程序哪里有内存安全方面的问题,别的内容都是我自己的好奇。
    看起来应该去读读 C 语言的书和 csapp 了。
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     5275 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 29ms UTC 07:13 PVG 15:13 LAX 23:13 JFK 02:13
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86