at elf64-x86-64
Disassembly of section .text:
0000000000001169 <is_valid>:
1169: f3 0f 1e fa endbr64
116d: 55 push %rbp
116e: 48 89 e5 mov %rsp,%rbp
1171: 48 83 ec 20 sub $0x20,%rsp
1175: 48 89 7d e8 mov %rdi,-0x18(%rbp)
1179: 48 8b 45 e8 mov -0x18(%rbp),%rax
117d: 0f b6 00 movzbl (%rax),%eax
1180: 88 45 ff mov %al,-0x1(%rbp)
1183: 48 83 7d e8 00 cmpq $0x0,-0x18(%rbp)
1188: 75 07 jne 1191 <is_valid+0x28>
118a: b8 00 00 00 00 mov $0x0,%eax
118f: eb 1e jmp 11af <is_valid+0x46>
1191: 48 8b 45 e8 mov -0x18(%rbp),%rax
1195: 48 8d 15 68 0e 00 00 lea 0xe68(%rip),%rdx # 2004 <_IO_stdin_used+0x4>
119c: 48 89 d6 mov %rdx,%rsi
119f: 48 89 c7 mov %rax,%rdi
11a2: e8 c9 fe ff ff call 1070 <strcmp@plt>
11a7: 85 c0 test %eax,%eax
11a9: 0f 94 c0 sete %al
11ac: 0f b6 c0 movzbl %al,%eax
11af: c9 leave
11b0: c3 ret
段错误如愿以偿地发生了,且是来自读取 str
处 1 字节并进行零扩展的 movzbl
指令,之前看到的编译期求值没有再次发生。
现在升高优化等级至 Og,编译期求值并优化掉第一个 if
语句的特效回归了:
lyazj@HelloWorld:~$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -Og
ub_null.c: In function ‘is_valid’:
ub_null.c:7:11: warning: comparison is always false due to limited range of data type [-Wtype-limits]
7 | if(head == 0x80) return 0;
| ^~
lyazj@HelloWorld:~$ ./ub_null
0
lyazj@HelloWorld:~$ objdump --disassemble=is_valid -j.text ub_null
ub_null: file format elf64-x86-64
Disassembly of section .text:
0000000000001169 <is_valid>:
1169: f3 0f 1e fa endbr64
116d: 48 85 ff test %rdi,%rdi
1170: 74 1d je 118f <is_valid+0x26>
1172: 48 83 ec 08 sub $0x8,%rsp
1176: 48 8d 35 87 0e 00 00 lea 0xe87(%rip),%rsi # 2004 <_IO_stdin_used+0x4>
117d: e8 de fe ff ff call 1060 <strcmp@plt>
1182: 85 c0 test %eax,%eax
1184: 0f 94 c0 sete %al
1187: 0f b6 c0 movzbl %al,%eax
118a: 48 83 c4 08 add $0x8,%rsp
118e: c3 ret
118f: b8 00 00 00 00 mov $0x0,%eax
1194: c3 ret
GCC 如何优化,除取决于编译选项外,同样取决于程序员编写什么样的源代码,这一点不足为奇。然而,当优化等级升至 O2 时,更为不好的事情发生了:
lyazj@HelloWorld:~$ gcc -Wall -Wshadow -Wextra ub_null.c -o ub_null -O2
ub_null.c: In function ‘is_valid’:
ub_null.c:7:11: warning: comparison is always false due to limited range of data type [-Wtype-limits]
7 | if(head == 0x80) return 0;
| ^~
In function ‘is_valid’,
inlined from ‘main’ at ub_null.c:15:3:
ub_null.c:9:10: warning: argument 1 null where non-null expected [-Wnonnull]
9 | return strcmp(str, "expected string") == 0;
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
In file included from ub_null.c:2:
ub_null.c: In function ‘main’:
/usr/include/string.h:156:12: note: in a call to function ‘strcmp’ declared ‘nonnull’
156 | extern int strcmp (const char *__s1, const char *__s2)
| ^~~~~~
lyazj@HelloWorld:~$ ./ub_null
Segmentation fault
lyazj@HelloWorld:~$ objdump --disassemble=is_valid -j.text ub_null
ub_null: file format elf64-x86-64
Disassembly of section .text:
00000000000011b0 <is_valid>:
11b0: f3 0f 1e fa endbr64
11b4: 48 83 ec 08 sub $0x8,%rsp
11b8: 48 8d 35 45 0e 00 00 lea 0xe45(%rip),%rsi # 2004 <_IO_stdin_used+0x4>
11bf: e8 9c fe ff ff call 1060 <strcmp@plt>
11c4: 85 c0 test %eax,%eax
11c6: 0f 94 c0 sete %al
11c9: 48 83 c4 08 add $0x8,%rsp
11cd: 0f b6 c0 movzbl %al,%eax
11d0: c3 ret
值得注意的是,现在段错误来自 strcmp()
中的 NULL dereference,且 is_valid()
的反汇编出奇地简单,GCC 同时干掉了两个 if
语句!因为我们首先访问了 str
处的 1 字节,由于 NULL dereference 是典型的 UB,编译器便假定了 str != NULL
,这样第二个 if
语句也可以被优化掉!现在,我们产生了具有严重漏洞的 is_v