V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
tan90ds
V2EX  ›  C

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

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

    没有任何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 没有错误."
    chilledheart
        16
    chilledheart  
       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   ·   我们的愿景   ·   实用小工具   ·   1065 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 19:11 · PVG 03:11 · LAX 12:11 · JFK 15:11
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.