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

2015-05-04 11:43:12 +08:00
 tan90ds

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

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

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

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

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


@Andiry 所以我是破坏了 print_plural 这个过程的执行栈对吧?(忘记了栈是向哪个方向增长的了)
ryd994
2015-05-04 12:59:46 +08:00
@tan90ds 不,是已经覆盖,但是一时间没人用到没人发现而已
xieyudi1990
2015-05-04 14:37:30 +08:00
@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
2015-05-04 14:40:20 +08:00
目测到几个错误,楼下继续。我觉得我比较适合做老师,呵呵。
行号:结果,原因
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
2015-05-04 14:48:29 +08:00
这么说把 你申请的堆栈内数组在堆栈中是反向存放的。
比如你这个程序的 word_plural存放的地址 的 256下标的位置 应该是 print_plural这个函数指针和 返回后 main函数的位置
也就是说 你申请256的话 写256以上会访问到堆栈原来的数据 比如函数指针,入参等,但是报错并不是立刻的 因为你可能还没有碰到那个函数指针 或者需要访问的参数。所以一旦堆栈溢出以后,或者 你写堆栈内数组,访问到非法的下标以后,程序的情况是不可预测的。。不同的操作系统也可能表现不一致
canautumn
2015-05-04 15:05:50 +08:00
gleport
2015-05-04 15:24:03 +08:00
这是我以前的经验总结:内存越界问题的定位技巧: 越界操作的语句极有可能会成功执行, 没有任何出错提示, 但下一次申请内存或释放内存就会表现出来了, 比如越界之后进行malloc()会失败, 提示malloc.c:3096: sYSMALLOc: Assertion... 这种情况99%可能是前面发生了内存越界
xieyudi1990
2015-05-04 15:52:09 +08:00
我觉得这题其实是个操作系统和体系结构相关的问题, 绝对不是什么 "内存溢出" 这种常识问题.
有点营养, 所以我实际试了下.

(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
2015-05-04 15:55:43 +08:00
@xieyudi1990 s/虽然多写了个字节 (600039468)/虽然多写了个字节 (600039467)/g


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

"@zhicheng gcc 4.9.2 没有错误."
chilledheart
2015-05-04 21:38:09 +08:00
@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
2015-05-04 23:36:32 +08:00
感谢各位的解答。这课只是泛泛地讲计算机安全,前边还在讲各种加密算法和协议,脚本注入什么的,后边就无视班里基本没人懂 C ,直接来了这样的内容。题目要求只是找找这程序哪里有内存安全方面的问题,别的内容都是我自己的好奇。
看起来应该去读读 C 语言的书和 csapp 了。

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/188289

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX