ตัวอย่างการทำซ้ำที่น้อยที่สุดพร้อมการวิเคราะห์การแยกชิ้นส่วน
main.c
void myfunc(char *const src, int len) {
int i;
for (i = 0; i < len; ++i) {
src[i] = 42;
}
}
int main(void) {
char arr[] = {'a', 'b', 'c', 'd'};
int len = sizeof(arr);
myfunc(arr, len + 1);
return 0;
}
GitHub ต้นน้ำ
รวบรวมและเรียกใช้:
gcc -fstack-protector -g -O0 -std=c99 main.c
ulimit -c unlimited && rm -f core
./a.out
ล้มเหลวตามต้องการ:
*** stack smashing detected ***: ./a.out terminated
Aborted (core dumped)
ทดสอบบน Ubuntu 16.04, GCC 6.4.0
ถอดชิ้นส่วน
ตอนนี้เรามองไปที่การถอดชิ้นส่วน:
objdump -D a.out
ซึ่งประกอบด้วย:
int main (void){
400579: 55 push %rbp
40057a: 48 89 e5 mov %rsp,%rbp
# Allocate 0x10 of stack space.
40057d: 48 83 ec 10 sub $0x10,%rsp
# Put the 8 byte canary from %fs:0x28 to -0x8(%rbp),
# which is right at the bottom of the stack.
400581: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
400588: 00 00
40058a: 48 89 45 f8 mov %rax,-0x8(%rbp)
40058e: 31 c0 xor %eax,%eax
char arr[] = {'a', 'b', 'c', 'd'};
400590: c6 45 f4 61 movb $0x61,-0xc(%rbp)
400594: c6 45 f5 62 movb $0x62,-0xb(%rbp)
400598: c6 45 f6 63 movb $0x63,-0xa(%rbp)
40059c: c6 45 f7 64 movb $0x64,-0x9(%rbp)
int len = sizeof(arr);
4005a0: c7 45 f0 04 00 00 00 movl $0x4,-0x10(%rbp)
myfunc(arr, len + 1);
4005a7: 8b 45 f0 mov -0x10(%rbp),%eax
4005aa: 8d 50 01 lea 0x1(%rax),%edx
4005ad: 48 8d 45 f4 lea -0xc(%rbp),%rax
4005b1: 89 d6 mov %edx,%esi
4005b3: 48 89 c7 mov %rax,%rdi
4005b6: e8 8b ff ff ff callq 400546 <myfunc>
return 0;
4005bb: b8 00 00 00 00 mov $0x0,%eax
}
# Check that the canary at -0x8(%rbp) hasn't changed after calling myfunc.
# If it has, jump to the failure point __stack_chk_fail.
4005c0: 48 8b 4d f8 mov -0x8(%rbp),%rcx
4005c4: 64 48 33 0c 25 28 00 xor %fs:0x28,%rcx
4005cb: 00 00
4005cd: 74 05 je 4005d4 <main+0x5b>
4005cf: e8 4c fe ff ff callq 400420 <__stack_chk_fail@plt>
# Otherwise, exit normally.
4005d4: c9 leaveq
4005d5: c3 retq
4005d6: 66 2e 0f 1f 84 00 00 nopw %cs:0x0(%rax,%rax,1)
4005dd: 00 00 00
ขอให้สังเกตความคิดเห็นที่มีประโยชน์เพิ่มโดยอัตโนมัติโดยobjdump
's โมดูลปัญญาประดิษฐ์
หากคุณเรียกใช้โปรแกรมนี้หลายครั้งผ่าน GDB คุณจะเห็นว่า:
- นกขมิ้นจะได้รับค่าสุ่มที่แตกต่างกันทุกครั้ง
- ห่วงสุดท้ายของ
myfunc
สิ่งที่แก้ไขที่อยู่ของนกขมิ้น
นกขมิ้นสุ่มโดยการตั้งค่าด้วย%fs:0x28
ซึ่งมีค่าสุ่มตามที่อธิบายไว้ที่:
แก้ไขข้อผิดพลาด
จากนี้ไปเราจะแก้ไขโค้ด:
myfunc(arr, len + 1);
เป็นแทน:
myfunc(arr, len);
myfunc(arr, len + 1); /* line 12 */
myfunc(arr, len);
น่าสนใจมากขึ้น
จากนั้นเราจะพยายามดูว่าเราสามารถระบุการ+ 1
เรียกผู้ร้ายด้วยวิธีอัตโนมัติได้มากกว่าการอ่านและทำความเข้าใจซอร์สโค้ดทั้งหมดหรือไม่
gcc -fsanitize=address
เพื่อเปิดใช้งานการล้างที่อยู่ของ Google (ASan)
หากคุณคอมไพล์อีกครั้งด้วยแฟล็กนี้และรันโปรแกรมมันจะเอาต์พุต:
#0 0x4008bf in myfunc /home/ciro/test/main.c:4
#1 0x40099b in main /home/ciro/test/main.c:12
#2 0x7fcd2e13d82f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x2082f)
#3 0x400798 in _start (/home/ciro/test/a.out+0x40079
ตามด้วยเอาท์พุทสีเพิ่มเติม
สิ่งนี้ชี้ให้เห็นอย่างชัดเจนถึงบรรทัดที่มีปัญหา 12
ซอร์สโค้ดสำหรับสิ่งนี้อยู่ที่: https://github.com/google/sanitizersแต่ตามที่เราเห็นจากตัวอย่างมันอัปสตรีมเป็น GCC แล้ว
ASan ยังสามารถตรวจพบปัญหาหน่วยความจำอื่นเช่นการรั่วไหลของหน่วยความจำ: จะค้นหาหน่วยความจำรั่วในรหัส C ++ ได้อย่างไร
Valgrind SGCheck
ในฐานะที่เป็นที่กล่าวถึงโดยคนอื่น , Valgrind ไม่ดีที่การแก้ปัญหาชนิดนี้
มันมีเครื่องมือทดลองที่เรียกว่า SGCheck :
SGCheck เป็นเครื่องมือในการค้นหา overruns ของ stack และอาร์เรย์ระดับโลก มันทำงานโดยใช้วิธีการแก้ปัญหาที่ได้มาจากการสังเกตเกี่ยวกับรูปแบบที่น่าจะเป็นของสแต็กและการเข้าถึงอาร์เรย์ทั่วโลก
ดังนั้นฉันไม่แปลกใจมากเมื่อไม่พบข้อผิดพลาด:
valgrind --tool=exp-sgcheck ./a.out
ข้อความแสดงข้อผิดพลาดควรมีลักษณะดังนี้: Valgrind error error
GDB
ข้อสังเกตที่สำคัญคือถ้าคุณเรียกใช้โปรแกรมผ่าน GDB หรือตรวจสอบcore
ไฟล์หลังจากข้อเท็จจริง:
gdb -nh -q a.out core
จากนั้นตามที่เราเห็นในชุดประกอบ GDB ควรนำคุณไปยังจุดสิ้นสุดของฟังก์ชั่นที่ทำการตรวจสอบนกขมิ้น:
(gdb) bt
#0 0x00007f0f66e20428 in __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:54
#1 0x00007f0f66e2202a in __GI_abort () at abort.c:89
#2 0x00007f0f66e627ea in __libc_message (do_abort=do_abort@entry=1, fmt=fmt@entry=0x7f0f66f7a49f "*** %s ***: %s terminated\n") at ../sysdeps/posix/libc_fatal.c:175
#3 0x00007f0f66f0415c in __GI___fortify_fail (msg=<optimized out>, msg@entry=0x7f0f66f7a481 "stack smashing detected") at fortify_fail.c:37
#4 0x00007f0f66f04100 in __stack_chk_fail () at stack_chk_fail.c:28
#5 0x00000000004005f6 in main () at main.c:15
(gdb) f 5
#5 0x00000000004005f6 in main () at main.c:15
15 }
(gdb)
และดังนั้นจึงมีปัญหาน่าจะเกิดจากหนึ่งในสายที่ฟังก์ชั่นนี้ทำ
ต่อไปเราพยายามที่จะระบุการเรียกที่ล้มเหลวโดยการก้าวขึ้นครั้งแรกหลังจากที่นกขมิ้นถูกตั้งค่า:
400581: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
400588: 00 00
40058a: 48 89 45 f8 mov %rax,-0x8(%rbp)
และดูที่อยู่:
(gdb) p $rbp - 0x8
$1 = (void *) 0x7fffffffcf18
(gdb) watch 0x7fffffffcf18
Hardware watchpoint 2: *0x7fffffffcf18
(gdb) c
Continuing.
Hardware watchpoint 2: *0x7fffffffcf18
Old value = 1800814336
New value = 1800814378
myfunc (src=0x7fffffffcf14 "*****?Vk\266", <incomplete sequence \355\216>, len=5) at main.c:3
3 for (i = 0; i < len; ++i) {
(gdb) p len
$2 = 5
(gdb) p i
$3 = 4
(gdb) bt
#0 myfunc (src=0x7fffffffcf14 "*****?Vk\266", <incomplete sequence \355\216>, len=5) at main.c:3
#1 0x00000000004005cc in main () at main.c:12
ตอนนี้ไม่ใบเราได้ที่การเรียนการสอนที่กระทำผิดขวา: len = 5
และi = 4
และในกรณีนี้โดยเฉพาะได้จุดที่เราจะกระทำผิดเส้น 12
อย่างไรก็ตาม backtrace เสียหายและมีถังขยะอยู่บ้าง backtrace ที่ถูกต้องจะมีลักษณะดังนี้:
#0 myfunc (src=0x7fffffffcf14 "abcd", len=4) at main.c:3
#1 0x00000000004005b8 in main () at main.c:11
ดังนั้นนี่อาจทำให้สแต็กเสียหายและป้องกันไม่ให้คุณเห็นร่องรอย
นอกจากนี้วิธีนี้ต้องรู้ว่าสิ่งที่เป็นสายสุดท้ายของฟังก์ชั่นการตรวจสอบนกขมิ้นมิฉะนั้นคุณจะมีผลบวกปลอมซึ่งจะไม่เคยเป็นไปได้ถ้าคุณใช้การแก้จุดบกพร่องกลับ