Bỏ qua điều hướng, tới nội dung chính
Học C
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àoAi 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-strongTrình biên dịch
NX / DEPKhông cho chạy mã trên ngăn xếp hay heapMặc định, -z noexecstackTrì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ànhTrình biên dịch và hệ điều hành
FORTIFY_SOURCEThay hàm chuỗi bằng bản có kiểm tra kích thước-D_FORTIFY_SOURCE=2 -O2Thư 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:
    ret
terminal
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ùngDùngVì sao
strcpysnprintf, hoặc memcpy có kiểm traKhông giới hạn độ dài
strcatsnprintf với %s%sKhông giới hạn, và O(n²) khi nối nhiều lần
sprintfsnprintfKhông giới hạn kích thước ra
getsfgets, hoặc getline trên POSIXBị xoá khỏi C11, không cách nào an toàn
scanf("%s")fgets rồi phân tích, hoặc %15sKhông giới hạn số ký tự đọc
strncpysnprintfKhông bảo đảm NUL, và đệm phí
allocamalloc, hoặc mảng cố địnhTràn ngăn xếp im lặng nếu kích thước lớn

Tự làm thử

  1. Chạy quyen.c và tìm số ký tự nhỏ nhất biến người dùng thành quản trị.
  2. Đảo thứ tự hai trường trong NguoiDung và xác nhận tràn không còn leo quyền.
  3. Chạy tran.c với độ dài đầu vào tăng dần và tìm ngưỡng bắt đầu sập.
  4. Dịch cùng chương trình có và không có -fstack-protector-strong và so assembly.
  5. Tìm mọi lời gọi strcpy, strcat, sprintf trong dự án của bạn và thay bằng bản có giới hạn.
  6. Viết hàm chep kiểu strlcpy và 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.
  • strncpy không bảo đảm NUL và đệm phí; nó không phải strcpy an toàn.
  • Dùng snprintf và memcpy có kiểm tra; tránh cả Annex K lẫn dựa vào bố cục cụ thể.