经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » 程序设计 » C++ » 查看文章
【持续更新】C/C++ 踩坑记录(一)
来源:cnblogs  作者:lyazj  时间:2023/7/26 9:15:20  对本文有异议

未定义行为之 NULL dereference

下面这段代码中 is_valid() 解引用了空指针 str,我们的直觉是编译运行后将迎来 SIGSEGV,然而事情并非所期望的那样。

  1. /*
  2. * ub_null.c - 未定义行为演示 之 NULL dereference
  3. */
  4. #include <stdio.h>
  5. #include <string.h>
  6. int is_valid(const char *str)
  7. {
  8. if(*str == 0x80) return 1;
  9. if(str == NULL) return 0;
  10. return strcmp(str, "expected string") == 0;
  11. }
  12. int main(void)
  13. {
  14. const char *str = NULL;
  15. printf("%d\n", is_valid(str));
  16. return 0;
  17. }
  1. lyazj@HelloWorld:~$ gcc --version
  2. gcc (Ubuntu 11.3.0-1ubuntu1~22.04.1) 11.3.0
  3. Copyright (C) 2021 Free Software Foundation, Inc.
  4. This is free software; see the source for copying conditions. There is NO
  5. warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
  6.  
  7. lyazj@HelloWorld:~$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -O0
  8. ub_null.c: In function is_valid’:
  9. ub_null.c:6:11: warning: comparison is always false due to limited range of data type [-Wtype-limits]
  10. 6 | if(*str == 0x80) return 0;
  11. | ^~
  12. lyazj@HelloWorld:~$ ./ub_null
  13. 0

结合 GCC 发出的警告,不难推断出条件表达式 *str == 0x80 在编译期被求值且相应的 if 语句被优化掉了,而且这是在 O0 的优化等级下。以下的反汇编结果验证了这一点。

  1. lyazj@HelloWorld:~$ objdump --disassemble=is_valid -j.text ub_null
  2.  
  3. ub_null: file format elf64-x86-64
  4.  
  5.  
  6. Disassembly of section .text:
  7.  
  8. 0000000000001169 <is_valid>:
  9. 1169: f3 0f 1e fa endbr64
  10. 116d: 55 push %rbp
  11. 116e: 48 89 e5 mov %rsp,%rbp
  12. 1171: 48 83 ec 10 sub $0x10,%rsp
  13. 1175: 48 89 7d f8 mov %rdi,-0x8(%rbp)
  14. 1179: 48 83 7d f8 00 cmpq $0x0,-0x8(%rbp)
  15. 117e: 75 07 jne 1187 <is_valid+0x1e>
  16. 1180: b8 00 00 00 00 mov $0x0,%eax
  17. 1185: eb 1e jmp 11a5 <is_valid+0x3c>
  18. 1187: 48 8b 45 f8 mov -0x8(%rbp),%rax
  19. 118b: 48 8d 15 72 0e 00 00 lea 0xe72(%rip),%rdx # 2004 <_IO_stdin_used+0x4>
  20. 1192: 48 89 d6 mov %rdx,%rsi
  21. 1195: 48 89 c7 mov %rax,%rdi
  22. 1198: e8 d3 fe ff ff call 1070 <strcmp@plt>
  23. 119d: 85 c0 test %eax,%eax
  24. 119f: 0f 94 c0 sete %al
  25. 11a2: 0f b6 c0 movzbl %al,%eax
  26. 11a5: c9 leave
  27. 11a6: c3 ret

我们在同一环境对 O3 优化等级做相同的实验,得到了相同的结果:

  1. lyazj@HelloWorld:~$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -O3
  2. ub_null.c: In function is_valid’:
  3. ub_null.c:6:11: warning: comparison is always false due to limited range of data type [-Wtype-limits]
  4. 6 | if(*str == 0x80) return 0;
  5. | ^~
  6. lyazj@HelloWorld:~$ ./ub_null
  7. 0
  8. lyazj@HelloWorld:~$ objdump --disassemble=is_valid -j.text ub_null
  9.  
  10. ub_null: file format elf64-x86-64
  11.  
  12.  
  13. Disassembly of section .text:
  14.  
  15. 00000000000011a0 <is_valid>:
  16. 11a0: f3 0f 1e fa endbr64
  17. 11a4: 48 85 ff test %rdi,%rdi
  18. 11a7: 74 27 je 11d0 <is_valid+0x30>
  19. 11a9: 48 83 ec 08 sub $0x8,%rsp
  20. 11ad: 48 8d 35 50 0e 00 00 lea 0xe50(%rip),%rsi # 2004 <_IO_stdin_used+0x4>
  21. 11b4: e8 a7 fe ff ff call 1060 <strcmp@plt>
  22. 11b9: 85 c0 test %eax,%eax
  23. 11bb: 0f 94 c0 sete %al
  24. 11be: 48 83 c4 08 add $0x8,%rsp
  25. 11c2: 0f b6 c0 movzbl %al,%eax
  26. 11c5: c3 ret
  27. 11c6: 66 2e 0f 1f 84 00 00 cs nopw 0x0(%rax,%rax,1)
  28. 11cd: 00 00 00
  29. 11d0: 31 c0 xor %eax,%eax
  30. 11d2: c3 ret

接下来我们用下面的两行代码替换被优化掉的 if 语句,看看会发生什么:

  1. char head = *str;
  2. if(head == 0x80) return 0;
  1. lyazj@HelloWorld:~$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -O0
  2. ub_null.c: In function is_valid’:
  3. ub_null.c:10:11: warning: comparison is always false due to limited range of data type [-Wtype-limits]
  4. 10 | if(head == 0x80) return 0;
  5. | ^~
  6. lyazj@HelloWorld:~$ ./ub_null
  7. Segmentation fault
  8. lyazj@HelloWorld:~$ objdump --disassemble=is_valid -j.text ub_null
  9.  
  10. ub_null: file format elf64-x86-64
  11.  
  12.  
  13. Disassembly of section .text:
  14.  
  15. 0000000000001169 <is_valid>:
  16. 1169: f3 0f 1e fa endbr64
  17. 116d: 55 push %rbp
  18. 116e: 48 89 e5 mov %rsp,%rbp
  19. 1171: 48 83 ec 20 sub $0x20,%rsp
  20. 1175: 48 89 7d e8 mov %rdi,-0x18(%rbp)
  21. 1179: 48 8b 45 e8 mov -0x18(%rbp),%rax
  22. 117d: 0f b6 00 movzbl (%rax),%eax
  23. 1180: 88 45 ff mov %al,-0x1(%rbp)
  24. 1183: 48 83 7d e8 00 cmpq $0x0,-0x18(%rbp)
  25. 1188: 75 07 jne 1191 <is_valid+0x28>
  26. 118a: b8 00 00 00 00 mov $0x0,%eax
  27. 118f: eb 1e jmp 11af <is_valid+0x46>
  28. 1191: 48 8b 45 e8 mov -0x18(%rbp),%rax
  29. 1195: 48 8d 15 68 0e 00 00 lea 0xe68(%rip),%rdx # 2004 <_IO_stdin_used+0x4>
  30. 119c: 48 89 d6 mov %rdx,%rsi
  31. 119f: 48 89 c7 mov %rax,%rdi
  32. 11a2: e8 c9 fe ff ff call 1070 <strcmp@plt>
  33. 11a7: 85 c0 test %eax,%eax
  34. 11a9: 0f 94 c0 sete %al
  35. 11ac: 0f b6 c0 movzbl %al,%eax
  36. 11af: c9 leave
  37. 11b0: c3 ret

段错误如愿以偿地发生了,且是来自读取 str 处 1 字节并进行零扩展的 movzbl 指令,之前看到的编译期求值没有再次发生。

现在升高优化等级至 Og,编译期求值并优化掉第一个 if 语句的特效回归了:

  1. lyazj@HelloWorld:~$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -Og
  2. ub_null.c: In function is_valid’:
  3. ub_null.c:7:11: warning: comparison is always false due to limited range of data type [-Wtype-limits]
  4. 7 | if(head == 0x80) return 0;
  5. | ^~
  6. lyazj@HelloWorld:~$ ./ub_null
  7. 0
  8. lyazj@HelloWorld:~$ objdump --disassemble=is_valid -j.text ub_null
  9.  
  10. ub_null: file format elf64-x86-64
  11.  
  12.  
  13. Disassembly of section .text:
  14.  
  15. 0000000000001169 <is_valid>:
  16. 1169: f3 0f 1e fa endbr64
  17. 116d: 48 85 ff test %rdi,%rdi
  18. 1170: 74 1d je 118f <is_valid+0x26>
  19. 1172: 48 83 ec 08 sub $0x8,%rsp
  20. 1176: 48 8d 35 87 0e 00 00 lea 0xe87(%rip),%rsi # 2004 <_IO_stdin_used+0x4>
  21. 117d: e8 de fe ff ff call 1060 <strcmp@plt>
  22. 1182: 85 c0 test %eax,%eax
  23. 1184: 0f 94 c0 sete %al
  24. 1187: 0f b6 c0 movzbl %al,%eax
  25. 118a: 48 83 c4 08 add $0x8,%rsp
  26. 118e: c3 ret
  27. 118f: b8 00 00 00 00 mov $0x0,%eax
  28. 1194: c3 ret

GCC 如何优化,除取决于编译选项外,同样取决于程序员编写什么样的源代码,这一点不足为奇。然而,当优化等级升至 O2 时,更为不好的事情发生了:

  1. lyazj@HelloWorld:~$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -O2
  2. ub_null.c: In function is_valid’:
  3. ub_null.c:7:11: warning: comparison is always false due to limited range of data type [-Wtype-limits]
  4. 7 | if(head == 0x80) return 0;
  5. | ^~
  6. In function is_valid’,
  7. inlined from main at ub_null.c:15:3:
  8. ub_null.c:9:10: warning: argument 1 null where non-null expected [-Wnonnull]
  9. 9 | return strcmp(str, "expected string") == 0;
  10. | ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  11. In file included from ub_null.c:2:
  12. ub_null.c: In function main’:
  13. /usr/include/string.h:156:12: note: in a call to function strcmp declared nonnull
  14. 156 | extern int strcmp (const char *__s1, const char *__s2)
  15. | ^~~~~~
  16. lyazj@HelloWorld:~$ ./ub_null
  17. Segmentation fault
  18. lyazj@HelloWorld:~$ objdump --disassemble=is_valid -j.text ub_null
  19.  
  20. ub_null: file format elf64-x86-64
  21.  
  22.  
  23. Disassembly of section .text:
  24.  
  25. 00000000000011b0 <is_valid>:
  26. 11b0: f3 0f 1e fa endbr64
  27. 11b4: 48 83 ec 08 sub $0x8,%rsp
  28. 11b8: 48 8d 35 45 0e 00 00 lea 0xe45(%rip),%rsi # 2004 <_IO_stdin_used+0x4>
  29. 11bf: e8 9c fe ff ff call 1060 <strcmp@plt>
  30. 11c4: 85 c0 test %eax,%eax
  31. 11c6: 0f 94 c0 sete %al
  32. 11c9: 48 83 c4 08 add $0x8,%rsp
  33. 11cd: 0f b6 c0 movzbl %al,%eax
  34. 11d0: c3 ret

值得注意的是,现在段错误来自 strcmp() 中的 NULL dereference,且 is_valid() 的反汇编出奇地简单,GCC 同时干掉了两个 if 语句!因为我们首先访问了 str 处的 1 字节,由于 NULL dereference 是典型的 UB,编译器便假定了 str != NULL,这样第二个 if 语句也可以被优化掉!现在,我们产生了具有严重漏洞的 is_valid() 函数,当 str == NULL 时,程序将发生严重错误。

解决 bug 的方法是显然的,即将 head 转换为 unsigned char 再比较,并调换两个 if 语句的顺序。NULL dereference,这是一个曾经让 Linux 内核爆出严重漏洞的 UB,我们刚刚成功复现了这一过程。诚然,此处的程序是异常简单的,看出两个写反的 if 语句非常容易;但对于代码业务特别复杂的场景,特别是对一行代码需要数行注释的底层代码或其它核心代码而言,这个 bug 可能一致遗留下来,并成为一个长期休眠的不定时炸弹。它的出现让许多可能是至关重要的代码段,如第二个 if 语句失效,可能给程序使用者造成难以预计的后果。还好当前版本的 GCC 友好地给出了应有的警告,这也再次向我们证明,随意地忽略编译器给出的 Warning 是不可取的。

Google 等团队开发的 sanitizer 已经集成到了当前版本的 GCC 中,让我们用 sanitizer 更为有效地发现上述未定义行为:

  1. lyazj@HelloWorld:~$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -O2 -fsanitize=undefined -fno-sanitize-recover=all
  2. ub_null.c: In function is_valid’:
  3. ub_null.c:7:11: warning: comparison is always false due to limited range of data type [-Wtype-limits]
  4. 7 | if(head == 0x80) return 0;
  5. | ^~
  6. lyazj@HelloWorld:~$ ./ub_null
  7. ub_null.c:6:8: runtime error: load of null pointer of type 'const char'

看到这里,是不是很轻松就发现了两个 if 语句写反的问题?

原文链接:https://www.cnblogs.com/lyazj/p/17576220.html

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号