没有任何C语言基础的楼主被布置了这么一道作业题(如果 gist 显示不出来,请原谅楼主…)
https://gist.github.com/acc4tb/0a554e70c80dde814975
这个程序是有问题的,我尝试以后发现 word 的长度大于等于263的时候程序就会崩溃(如果以 h 结尾则262字符即崩溃)。
看起来问题是出在 free(buf); 这一句上,但是楼主想不明白为什么是这个长度,为什么是在这里崩溃了。
更加奇怪的是如果 buf 的长度是 n+1,那么当 word 的结尾是 "h" 的时候就会被附加上 "es",那么岂不是已经挤爆了申请的空间么?为什么测试起来是正常的呢。
希望大家帮我解释一下,谢谢。
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 |
2
Andiry 2015-05-04 11:56:48 +08:00 1
栈上分配的字符数组只有256B,往里面copy多于这个长度的数据自然就崩了,因为执行栈被你写坏了
|
3
tan90ds OP |
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说到点上了. |
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` |
7
cover 2015-05-04 14:48:29 +08:00 1
这么说把 你申请的堆栈内数组在堆栈中是反向存放的。
比如你这个程序的 word_plural存放的地址 的 256下标的位置 应该是 print_plural这个函数指针和 返回后 main函数的位置 也就是说 你申请256的话 写256以上会访问到堆栈原来的数据 比如函数指针,入参等,但是报错并不是立刻的 因为你可能还没有碰到那个函数指针 或者需要访问的参数。所以一旦堆栈溢出以后,或者 你写堆栈内数组,访问到非法的下标以后,程序的情况是不可预测的。。不同的操作系统也可能表现不一致 |
8
canautumn 2015-05-04 15:05:50 +08:00 2
|
9
gleport 2015-05-04 15:24:03 +08:00 1
这是我以前的经验总结:内存越界问题的定位技巧: 越界操作的语句极有可能会成功执行, 没有任何出错提示, 但下一次申请内存或释放内存就会表现出来了, 比如越界之后进行malloc()会失败, 提示malloc.c:3096: sYSMALLOc: Assertion... 这种情况99%可能是前面发生了内存越界
|
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)) |
11
xieyudi1990 2015-05-04 15:55:43 +08:00
|
12
xieyudi1990 2015-05-04 15:58:36 +08:00
回去看了下 "没有任何C语言基础的楼主被布置了这么一道作业题".
感觉是我想多了. 不过亲手验证了下, 也好. |
13
zhicheng 2015-05-04 16:48:54 +08:00
@xieyudi1990
如果编译器能把潜在错误全都找出来,还要程序员干嘛。 |
14
xieyudi1990 2015-05-04 16:52:41 +08:00 via iPhone
@zhicheng 我感觉lz这道题是在考OS和组成原理. 这编译器有什么关系?
|
15
zhicheng 2015-05-04 17:30:40 +08:00
|
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 也是可行的。 |
17
tan90ds OP 感谢各位的解答。这课只是泛泛地讲计算机安全,前边还在讲各种加密算法和协议,脚本注入什么的,后边就无视班里基本没人懂 C ,直接来了这样的内容。题目要求只是找找这程序哪里有内存安全方面的问题,别的内容都是我自己的好奇。
看起来应该去读读 C 语言的书和 csapp 了。 |