Bài 59.130 phút đọc
Tràn bộ đệm và tràn ngăn xếp
Sau bài này bạn sẽ làm được
- Giải thích cơ chế đè lên địa chỉ trả về
- Kể bốn cơ chế bảo vệ của trình biên dịch và hệ điều hành
- Thay mọi hàm chuỗi không giới hạn bằng bản có giới hạn
- Đọc báo cáo stack smashing detected
Tràn bộ đệm là lớp lỗ hổng lâu đời nhất và vẫn phổ biến nhất trong C. Bài này cho thấy một chuỗi quá dài biến thành quyền điều khiển như thế nào, bằng cách chạy từng bước, rồi bốn lớp phòng thủ chặn nó.
#Bố cục ngăn xếp
dan.c, xem cái gì nằm sau bộ đệm
static void kiem(int n) {
char dem[16];
int canh_gac = 0x41424344; /* de sau dem */
memset(dem, 'X', (size_t)n);
if (n < (int)sizeof dem) dem[n] = '\0';
printf("ghi %2d byte -> canh_gac = 0x%08X %s\n",
n, (unsigned)canh_gac,
canh_gac == 0x41424344 ? "" : "<- DA BI DE LEN");
}terminal
gcc -std=c11 -O0 -fno-stack-protector -w dan.c -o dan.exe
./dan.exe
dia chi dem = 000000000061FE30 dia chi canh gac = 000000000061FE2C khoang cach = -4 byte ghi 12 byte -> canh_gac = 0x41424344 ghi 14 byte -> canh_gac = 0x41424344 ghi 16 byte -> canh_gac = 0x41424344 ghi 28 byte -> canh_gac = 0x41424344
#Leo quyền chỉ bằng một byte
quyen.c, struct nên thứ tự trường được bảo đảm
typedef struct {
char ten[16];
int la_quan_tri; /* NAM NGAY SAU ten */
} NguoiDung;
static void dang_nhap(const char *ten_nhap) {
NguoiDung nd;
memset(&nd, 0, sizeof nd);
nd.la_quan_tri = 0; /* mac dinh: nguoi dung thuong */
strcpy(nd.ten, ten_nhap); /* KHONG kiem tra do dai */
printf(" ten = \"%s\"\n", nd.ten);
printf(" la_quan_tri = %d %s\n", nd.la_quan_tri,
nd.la_quan_tri ? "<<< DA THANH QUAN TRI VIEN" : "");
}terminal
gcc -std=c11 -O0 -fno-stack-protector -w quyen.c -o quyen.exe
./quyen.exe
offsetof(ten) = 0, offsetof(la_quan_tri) = 16, sizeof = 20 dang nhap "an": ten = "an" la_quan_tri = 0 dang nhap 15 ky tu: ten = "aaaaaaaaaaaaaaa" la_quan_tri = 0 dang nhap 16 ky tu + mot byte: ten = "aaaaaaaaaaaaaaaa\x01" la_quan_tri = 1 <<< DA THANH QUAN TRI VIEN
#Đè lên địa chỉ trả về
tran.c
static void bi_mat(void) {
printf(" >>> HAM NAY KHONG BAO GIO DUOC GOI TRONG MA <<<\n");
}
static void yeu(const char *dau_vao) {
char dem[16];
printf(" dia chi dem = %p\n", (void *)dem);
printf(" dia chi bi_mat = %p\n", (void *)bi_mat);
strcpy(dem, dau_vao); /* KHONG kiem tra do dai */
printf(" dem = %s\n", dem);
}
int main(int argc, char **argv) {
const char *s = (argc > 1) ? argv[1] : "ngan";
printf("dau vao dai %d byte\n", (int)strlen(s));
yeu(s);
printf("yeu() da tra ve binh thuong\n");
return 0;
}terminal
gcc -std=c11 -O0 -fno-stack-protector -w tran.c -o tran_khong.exe
./tran_khong.exe ngan
dau vao dai 4 byte dia chi dem = 000000000061FE00 dia chi bi_mat = 0000000000401550 dem = ngan yeu() da tra ve binh thuong
echo $?
0
./tran_khong.exe AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
dau vao dai 48 byte dia chi dem = 000000000061FE00 dia chi bi_mat = 0000000000401550 dem = AAAAAA...AAAA Segmentation fault
echo $?
139
#Bốn cơ chế bảo vệ
| Cơ chế | Chặn gì | Bật thế nào | Ai bật |
|---|---|---|---|
| Canh gác ngăn xếp | Đè lên địa chỉ trả về bị phát hiện trước khi ret | -fstack-protector-strong | Trình biên dịch |
| NX / DEP | Không cho chạy mã trên ngăn xếp hay heap | Mặc định, -z noexecstack | Trình liên kết và CPU |
| ASLR | Địa chỉ đổi mỗi lần chạy, khó đoán đích | -fPIE -pie, và hệ điều hành | Trình biên dịch và hệ điều hành |
| FORTIFY_SOURCE | Thay hàm chuỗi bằng bản có kiểm tra kích thước | -D_FORTIFY_SOURCE=2 -O2 | Thư viện C |
Canh gác ngăn xếp trong assembly
# gcc -O0 -fstack-protector-strong -S tran.c
yeu:
...
movq %fs:0, %rax # doc gia tri canh gac tu vung dac biet
movq %rax, -8(%rbp) # dat no ngay TRUOC dia chi tra ve
...
call strcpy # neu tran, no de len canh gac
...
movq -8(%rbp), %rax # doc lai canh gac
xorq %fs:0, %rax # so voi gia tri goc
je .L3 # bang nhau -> nhay qua, an toan
call __stack_chk_fail # KHAC nhau -> abort ngay
.L3:
retterminal
gcc -std=c11 -O0 -fstack-protector-strong -w tran.c -o tran_co.exe
./tran_co.exe AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
dau vao dai 48 byte *** stack smashing detected ***: terminated Segmentation fault
echo $?
134
Bật cả bốn cùng lúc
# Bo co cung hoa day du, giong cac ban phan phoi Linux dung
gcc -O2 \
-D_FORTIFY_SOURCE=2 \
-fstack-protector-strong \
-fstack-clash-protection \
-fPIE -pie \
-Wl,-z,relro,-z,now \
-Wl,-z,noexecstack \
chuong-trinh.c -o chuong-trinh
# Kiem tra mot tep nhi phan da bat nhung gi:
# Linux: checksec --file=chuong-trinh
# hardening-check chuong-trinh
# Ket qua mong doi: RELRO day du, canary co, NX co, PIE co.#Thay hàm không giới hạn
an_toan.c, bốn cách và cái bẫy của từng cái
const char *dai = "0123456789ABCDEF-THUA"; /* 21 ky tu */
/* 1. strncpy: KHONG bao dam ket thuc NUL */
char d1[16];
strncpy(d1, dai, sizeof d1);
/* 16 byte deu la du lieu, KHONG co NUL -> printf sau do doc tran */
/* 2. strncpy co sua tay */
char d2[16];
strncpy(d2, dai, sizeof d2 - 1);
d2[sizeof d2 - 1] = '\0'; /* BAT BUOC them dong nay */
/* 3. snprintf: luon ket thuc NUL, tra ve do dai CAN THIET */
char d3[16];
int n = snprintf(d3, sizeof d3, "%s", dai);
if (n >= (int)sizeof d3) { /* da bi cat, xu ly */ }
/* 4. memcpy sau khi tu kiem tra */
char d4[16];
size_t n4 = strlen(dai);
if (n4 >= sizeof d4) n4 = sizeof d4 - 1;
memcpy(d4, dai, n4);
d4[n4] = '\0';terminal
gcc -std=c11 -O2 -Wall -Wextra -D__USE_MINGW_ANSI_STDIO=1 an_toan.c -o an_toan.exe
an_toan.c:13:9: warning: 'strncpy' output truncated copying 16 bytes from a string of length 21 [-Wstringop-truncation]
./an_toan.exe
strncpy : 0123456789ABCDEF <- KHONG co NUL, 16 byte deu la du lieu strncpy+: "0123456789ABCDE" (do dai 15) snprintf: "0123456789ABCDE" tra ve 21 (can 21, co 16) <- DA BI CAT memcpy : "0123456789ABCDE" (do dai 15) strncpy nguon ngan: viet 4096 byte cho chuoi 3 ky tu
| Không dùng | Dùng | Vì sao |
|---|---|---|
| strcpy | snprintf, hoặc memcpy có kiểm tra | Không giới hạn độ dài |
| strcat | snprintf với %s%s | Không giới hạn, và O(n²) khi nối nhiều lần |
| sprintf | snprintf | Không giới hạn kích thước ra |
| gets | fgets, hoặc getline trên POSIX | Bị xoá khỏi C11, không cách nào an toàn |
| scanf("%s") | fgets rồi phân tích, hoặc %15s | Không giới hạn số ký tự đọc |
| strncpy | snprintf | Không bảo đảm NUL, và đệm phí |
| alloca | malloc, hoặc mảng cố định | Tràn ngăn xếp im lặng nếu kích thước lớn |
Tự làm thử
- Chạy
quyen.cvà tìm số ký tự nhỏ nhất biến người dùng thành quản trị. - Đảo thứ tự hai trường trong
NguoiDungvà xác nhận tràn không còn leo quyền. - Chạy
tran.cvới độ dài đầu vào tăng dần và tìm ngưỡng bắt đầu sập. - Dịch cùng chương trình có và không có
-fstack-protector-strongvà so assembly. - Tìm mọi lời gọi
strcpy,strcat,sprintftrong dự án của bạn và thay bằng bản có giới hạn. - Viết hàm
chepkiểustrlcpyvà kiểm thử với ba trường hợp biên.
Trình chấm điểm tự động sẽ được bổ sung ở giai đoạn sau. Hiện tại bạn tự chạy thử trên máy.
Tóm tắt
- Ngăn xếp lớn lên xuống địa chỉ giảm, nên tràn đè lên những gì ở trên bộ đệm, kể cả địa chỉ trả về.
- Chuẩn C bảo đảm thứ tự trường struct, nên tràn một bộ đệm cạnh một cờ quyền là một cách leo quyền đáng tin.
- Bốn cơ chế bảo vệ làm khai thác khó hơn nhưng không xoá lỗi: canh gác, NX, ASLR, FORTIFY_SOURCE.
strncpykhông bảo đảm NUL và đệm phí; nó không phảistrcpyan toàn.- Dùng
snprintfvàmemcpycó kiểm tra; tránh cả Annex K lẫn dựa vào bố cục cụ thể.