【持续更新】C/C++ 踩坑记录(一)

[复制链接]
发表于 2023-7-26 00:04:57 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
未定义行为之 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. <font color="#26A269"><b>lyazj@HelloWorld</b></font>:<font color="#3268AB"><b>~</b></font>$ 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. <font color="#26A269"><b>lyazj@HelloWorld</b></font>:<font color="#3268AB"><b>~</b></font>$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -O0
  7. <b>ub_null.c:</b> In function ‘<b>is_valid</b>’:
  8. <b>ub_null.c:6:11:</b> <font color="#A347BA"><b>warning: </b></font>comparison is always false due to limited range of data type [<font color="#A347BA"><b>-Wtype-limits</b></font>]
  9.     6 |   if(*str <font color="#A347BA"><b>==</b></font> 0x80) return 0;
  10.       |           <font color="#A347BA"><b>^~</b></font>
  11. <font color="#26A269"><b>lyazj@HelloWorld</b></font>:<font color="#3268AB"><b>~</b></font>$ ./ub_null
  12. 0
复制代码
结合 GCC 发出的警告,不难推断出条件表达式 *str == 0x80 在编译期被求值且相应的 if 语句被优化掉了,而且这是在 O0 的优化等级下。以下的反汇编结果验证了这一点。
  1. <font color="#26A269"><b>lyazj@HelloWorld</b></font>:<font color="#3268AB"><b>~</b></font>$ objdump --disassemble=is_valid -j.text ub_null
  2. ub_null:     file format elf64-x86-64
  3. Disassembly of section .text:
  4. 0000000000001169 <is_valid>:
  5.     1169:        f3 0f 1e fa                  endbr64
  6.     116d:        55                           push   %rbp
  7.     116e:        48 89 e5                     mov    %rsp,%rbp
  8.     1171:        48 83 ec 10                  sub    $0x10,%rsp
  9.     1175:        48 89 7d f8                  mov    %rdi,-0x8(%rbp)
  10.     1179:        48 83 7d f8 00               cmpq   $0x0,-0x8(%rbp)
  11.     117e:        75 07                        jne    1187 <is_valid+0x1e>
  12.     1180:        b8 00 00 00 00               mov    $0x0,%eax
  13.     1185:        eb 1e                        jmp    11a5 <is_valid+0x3c>
  14.     1187:        48 8b 45 f8                  mov    -0x8(%rbp),%rax
  15.     118b:        48 8d 15 72 0e 00 00         lea    0xe72(%rip),%rdx        # 2004 <_IO_stdin_used+0x4>
  16.     1192:        48 89 d6                     mov    %rdx,%rsi
  17.     1195:        48 89 c7                     mov    %rax,%rdi
  18.     1198:        e8 d3 fe ff ff               call   1070 <strcmp@plt>
  19.     119d:        85 c0                        test   %eax,%eax
  20.     119f:        0f 94 c0                     sete   %al
  21.     11a2:        0f b6 c0                     movzbl %al,%eax
  22.     11a5:        c9                           leave  
  23.     11a6:        c3                           ret   
复制代码
我们在同一环境对 O3 优化等级做相同的实验,得到了相同的结果:
  1. <font color="#26A269"><b>lyazj@HelloWorld</b></font>:<font color="#3268AB"><b>~</b></font>$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -O3
  2. <b>ub_null.c:</b> In function ‘<b>is_valid</b>’:
  3. <b>ub_null.c:6:11:</b> <font color="#A347BA"><b>warning: </b></font>comparison is always false due to limited range of data type [<font color="#A347BA"><b>-Wtype-limits</b></font>]
  4.     6 |   if(*str <font color="#A347BA"><b>==</b></font> 0x80) return 0;
  5.       |           <font color="#A347BA"><b>^~</b></font>
  6. <font color="#26A269"><b>lyazj@HelloWorld</b></font>:<font color="#3268AB"><b>~</b></font>$ ./ub_null
  7. 0
  8. <font color="#26A269"><b>lyazj@HelloWorld</b></font>:<font color="#3268AB"><b>~</b></font>$ objdump --disassemble=is_valid -j.text ub_null
  9. ub_null:     file format elf64-x86-64
  10. Disassembly of section .text:
  11. 00000000000011a0 <is_valid>:
  12.     11a0:        f3 0f 1e fa                  endbr64
  13.     11a4:        48 85 ff                     test   %rdi,%rdi
  14.     11a7:        74 27                        je     11d0 <is_valid+0x30>
  15.     11a9:        48 83 ec 08                  sub    $0x8,%rsp
  16.     11ad:        48 8d 35 50 0e 00 00         lea    0xe50(%rip),%rsi        # 2004 <_IO_stdin_used+0x4>
  17.     11b4:        e8 a7 fe ff ff               call   1060 <strcmp@plt>
  18.     11b9:        85 c0                        test   %eax,%eax
  19.     11bb:        0f 94 c0                     sete   %al
  20.     11be:        48 83 c4 08                  add    $0x8,%rsp
  21.     11c2:        0f b6 c0                     movzbl %al,%eax
  22.     11c5:        c3                           ret   
  23.     11c6:        66 2e 0f 1f 84 00 00         cs nopw 0x0(%rax,%rax,1)
  24.     11cd:        00 00 00
  25.     11d0:        31 c0                        xor    %eax,%eax
  26.     11d2:        c3                           ret   
复制代码
接下来我们用下面的两行代码替换被优化掉的 if 语句,看看会发生什么:
  1. char head = *str;
  2. if(head == 0x80) return 0;
复制代码
  1. <font color="#26A269"><b>lyazj@HelloWorld</b></font>:<font color="#3268AB"><b>~</b></font>$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -O0
  2. <b>ub_null.c:</b> In function ‘<b>is_valid</b>’:
  3. <b>ub_null.c:10:11:</b> <font color="#A347BA"><b>warning: </b></font>comparison is always false due to limited range of data type [<font color="#A347BA"><b>-Wtype-limits</b></font>]
  4.    10 |   if(head <font color="#A347BA"><b>==</b></font> 0x80) return 0;
  5.       |           <font color="#A347BA"><b>^~</b></font>
  6. <font color="#26A269"><b>lyazj@HelloWorld</b></font>:<font color="#3268AB"><b>~</b></font>$ ./ub_null
  7. Segmentation fault
  8. <font color="#26A269"><b>lyazj@HelloWorld</b></font>:<font color="#3268AB"><b>~</b></font>$ objdump --disassemble=is_valid -j.text ub_null
  9. ub_null:     file format elf64-x86-64
  10. Disassembly of section .text:
  11. 0000000000001169 <is_valid>:
  12.     1169:        f3 0f 1e fa                  endbr64
  13.     116d:        55                           push   %rbp
  14.     116e:        48 89 e5                     mov    %rsp,%rbp
  15.     1171:        48 83 ec 20                  sub    $0x20,%rsp
  16.     1175:        48 89 7d e8                  mov    %rdi,-0x18(%rbp)
  17.     1179:        48 8b 45 e8                  mov    -0x18(%rbp),%rax
  18.     117d:        0f b6 00                     movzbl (%rax),%eax
  19.     1180:        88 45 ff                     mov    %al,-0x1(%rbp)
  20.     1183:        48 83 7d e8 00               cmpq   $0x0,-0x18(%rbp)
  21.     1188:        75 07                        jne    1191 <is_valid+0x28>
  22.     118a:        b8 00 00 00 00               mov    $0x0,%eax
  23.     118f:        eb 1e                        jmp    11af <is_valid+0x46>
  24.     1191:        48 8b 45 e8                  mov    -0x18(%rbp),%rax
  25.     1195:        48 8d 15 68 0e 00 00         lea    0xe68(%rip),%rdx        # 2004 <_IO_stdin_used+0x4>
  26.     119c:        48 89 d6                     mov    %rdx,%rsi
  27.     119f:        48 89 c7                     mov    %rax,%rdi
  28.     11a2:        e8 c9 fe ff ff               call   1070 <strcmp@plt>
  29.     11a7:        85 c0                        test   %eax,%eax
  30.     11a9:        0f 94 c0                     sete   %al
  31.     11ac:        0f b6 c0                     movzbl %al,%eax
  32.     11af:        c9                           leave  
  33.     11b0:        c3                           ret   
复制代码
段错误如愿以偿地发生了,且是来自读取 str 处 1 字节并进行零扩展的 movzbl 指令,之前看到的编译期求值没有再次发生。
现在升高优化等级至 Og,编译期求值并优化掉第一个 if 语句的特效回归了:
  1. <font color="#26A269"><b>lyazj@HelloWorld</b></font>:<font color="#3268AB"><b>~</b></font>$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -Og
  2. <b>ub_null.c:</b> In function ‘<b>is_valid</b>’:
  3. <b>ub_null.c:7:11:</b> <font color="#A347BA"><b>warning: </b></font>comparison is always false due to limited range of data type [<font color="#A347BA"><b>-Wtype-limits</b></font>]
  4.     7 |   if(head <font color="#A347BA"><b>==</b></font> 0x80) return 0;
  5.       |           <font color="#A347BA"><b>^~</b></font>
  6. <font color="#26A269"><b>lyazj@HelloWorld</b></font>:<font color="#3268AB"><b>~</b></font>$ ./ub_null
  7. 0
  8. <font color="#26A269"><b>lyazj@HelloWorld</b></font>:<font color="#3268AB"><b>~</b></font>$ objdump --disassemble=is_valid -j.text ub_null
  9. ub_null:     file format elf64-x86-64
  10. Disassembly of section .text:
  11. 0000000000001169 <is_valid>:
  12.     1169:        f3 0f 1e fa                  endbr64
  13.     116d:        48 85 ff                     test   %rdi,%rdi
  14.     1170:        74 1d                        je     118f <is_valid+0x26>
  15.     1172:        48 83 ec 08                  sub    $0x8,%rsp
  16.     1176:        48 8d 35 87 0e 00 00         lea    0xe87(%rip),%rsi        # 2004 <_IO_stdin_used+0x4>
  17.     117d:        e8 de fe ff ff               call   1060 <strcmp@plt>
  18.     1182:        85 c0                        test   %eax,%eax
  19.     1184:        0f 94 c0                     sete   %al
  20.     1187:        0f b6 c0                     movzbl %al,%eax
  21.     118a:        48 83 c4 08                  add    $0x8,%rsp
  22.     118e:        c3                           ret   
  23.     118f:        b8 00 00 00 00               mov    $0x0,%eax
  24.     1194:        c3                           ret   
复制代码
GCC 如何优化,除取决于编译选项外,同样取决于程序员编写什么样的源代码,这一点不足为奇。然而,当优化等级升至 O2 时,更为不好的事情发生了:
  1. <font color="#26A269"><b>lyazj@HelloWorld</b></font>:<font color="#3268AB"><b>~</b></font>$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -O2
  2. <b>ub_null.c:</b> In function ‘<b>is_valid</b>’:
  3. <b>ub_null.c:7:11:</b> <font color="#A347BA"><b>warning: </b></font>comparison is always false due to limited range of data type [<font color="#A347BA"><b>-Wtype-limits</b></font>]
  4.     7 |   if(head <font color="#A347BA"><b>==</b></font> 0x80) return 0;
  5.       |           <font color="#A347BA"><b>^~</b></font>
  6. In function ‘<b>is_valid</b>’,
  7.     inlined from ‘<b>main</b>’ at <b>ub_null.c:15:3</b>:
  8. <b>ub_null.c:9:10:</b> <font color="#A347BA"><b>warning: </b></font>argument 1 null where non-null expected [<font color="#A347BA"><b>-Wnonnull</b></font>]
  9.     9 |   return <font color="#A347BA"><b>strcmp(str, "expected string")</b></font> == 0;
  10.       |          <font color="#A347BA"><b>^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~</b></font>
  11. In file included from <b>ub_null.c:2</b>:
  12. <b>ub_null.c:</b> In function ‘<b>main</b>’:
  13. <b>/usr/include/string.h:156:12:</b> <font color="#2AA1B3"><b>note: </b></font>in a call to function ‘<b>strcmp</b>’ declared ‘<b>nonnull</b>’
  14.   156 | extern int <font color="#2AA1B3"><b>strcmp</b></font> (const char *__s1, const char *__s2)
  15.       |            <font color="#2AA1B3"><b>^~~~~~</b></font>
  16. <font color="#26A269"><b>lyazj@HelloWorld</b></font>:<font color="#3268AB"><b>~</b></font>$ ./ub_null
  17. Segmentation fault
  18. <font color="#26A269"><b>lyazj@HelloWorld</b></font>:<font color="#3268AB"><b>~</b></font>$ objdump --disassemble=is_valid -j.text ub_null
  19. ub_null:     file format elf64-x86-64
  20. Disassembly of section .text:
  21. 00000000000011b0 <is_valid>:
  22.     11b0:        f3 0f 1e fa                  endbr64
  23.     11b4:        48 83 ec 08                  sub    $0x8,%rsp
  24.     11b8:        48 8d 35 45 0e 00 00         lea    0xe45(%rip),%rsi        # 2004 <_IO_stdin_used+0x4>
  25.     11bf:        e8 9c fe ff ff               call   1060 <strcmp@plt>
  26.     11c4:        85 c0                        test   %eax,%eax
  27.     11c6:        0f 94 c0                     sete   %al
  28.     11c9:        48 83 c4 08                  add    $0x8,%rsp
  29.     11cd:        0f b6 c0                     movzbl %al,%eax
  30.     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. <font color="#26A269"><b>lyazj@HelloWorld</b></font>:<font color="#3268AB"><b>~</b></font>$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -O2 -fsanitize=undefined -fno-sanitize-recover=all
  2. <b>ub_null.c:</b> In function ‘<b>is_valid</b>’:
  3. <b>ub_null.c:7:11:</b> <font color="#A347BA"><b>warning: </b></font>comparison is always false due to limited range of data type [<font color="#A347BA"><b>-Wtype-limits</b></font>]
  4.     7 |   if(head <font color="#A347BA"><b>==</b></font> 0x80) return 0;
  5.       |           <font color="#A347BA"><b>^~</b></font>
  6. <font color="#26A269"><b>lyazj@HelloWorld</b></font>:<font color="#3268AB"><b>~</b></font>$ ./ub_null
  7. <b>ub_null.c:6:8:</b><font color="#C01C28"><b> runtime error: </b></font><b>load of null pointer of type 'const char'</b>
复制代码
看到这里,是不是很轻松就发现了两个 if 语句写反的问题?

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
继续阅读请点击广告
回复

使用道具 举报

×
登录参与点评抽奖,加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表