編寫一個密碼驗證程式:輸入字符串與預設密碼 "1234567" 比較,相符輸出 congratulations!,否則輸出 try again!。實測發現,輸入某些 8 位字符串(如 33333333)也會誤判為「恭喜通過」。
#include <stdio.h>
#include <string.h>
#define PASSWORD "1234567"
void cmp(char* str);
int main() {
char buf[1024];
printf("please input password:\n");
scanf("%s", buf);
cmp(buf);
return 0;
}
void cmp(char* str) {
int ret; // 定義在 buf 之前
char buf[8]; // 僅 8 字節
ret = strcmp(str, PASSWORD);
strcpy(buf, str); // 冗餘語句:當 str 長度 > 7 時發生棧溢出
if (ret == 0)
printf("congratulations!\n");
else
printf("try again!\n");
}
棧溢出屬於緩衝區溢出的一種。函數運行時在棧上分配局部變量空間;若向緩衝區寫入超過其容量的數據,多餘數據會覆蓋相鄰的棧變量甚至返回地址,導致程式行為不可預測。
ret (4 字節) → buf[8] (8 字節)。strcpy(buf, str) 將輸入字符串從 buf[0] 開始寫入。若輸入長度 ≥ 8,多餘字節會向高地址溢出,覆蓋 ret 的低位字節。ret = strcmp(...) 的返回值:相等為 0 (0x00000000);str > PASSWORD 為正數(如 1 → 0x00000001);str < PASSWORD 為負數(如 -1 → 0xFFFFFFFF)。| 輸入與密碼關係 | ret 原始值 | 溢出後首字節 | ret 實際值 | 程式輸出 |
|---|---|---|---|---|
| str == PASSWORD | 0 (0x00000000) | 0x00 | 0 | congratulations! ✓ |
| str > PASSWORD | 1 (0x00000001) | 被輸入字符覆蓋為 0x33 ('3') | 0x33000000 ≠ 0 | try again! ✓ |
| str < PASSWORD | -1 (0xFFFFFFFF) | 被覆蓋為 0x33 | 0x33FFFFFF ≠ 0 | try again! ✓ |
| str > PASSWORD 且第 8 字符為 '\0' | 1 (0x00000001) | 0x00 | 0 | congratulations! ✗ 誤判 |
輸入 33333333(8 字符,無結束符)時,strcpy 會把 8 個 '3' 寫入 buf,第 9 字節隱含的 \0 剛好覆蓋 ret 的最低字節,使 ret 從 1 變為 0,繞過驗證。
strcpy(buf, str) 本無業務意義,直接移除。ret 置於 buf 之後,即使發生溢出也不會踩踏關鍵變量( defence-in-depth )。strncpy 或 snprintf 限制拷貝長度。-fstack-protector (GCC) 或 /GS (MSVC) 啟用棧金絲雀檢測。void cmp(char* str) {
char buf[8]; // 先定義緩衝區
int ret; // 再定義返回值
ret = strcmp(str, PASSWORD);
// 移除 strcpy
if (ret == 0)
printf("congratulations!\n");
else
printf("try again!\n");
}
這是一個經典的「看似無害的冗餘代碼引發安全漏洞」案例。它警示我們: