Bỏ qua điều hướng, tới nội dung chính
Học C
Bài 59.228 phút đọc

Chuỗi định dạng và tràn số

Sau bài này bạn sẽ làm được

  • Nhận ra lỗ hổng chuỗi định dạng và sửa nó
  • Giải thích %n làm được gì
  • Chỉ ra cách tràn số dẫn tới tràn bộ đệm
  • Viết mọi phép tính kích thước một cách an toàn

Hai lớp lỗ hổng của bài này có chung một gốc: tin vào dữ liệu bên ngoài ở một chỗ mà ngôn ngữ không kiểm tra hộ bạn. printf(s) là một lỗ hổng, và một phép nhân có thể vượt qua chính cái kiểm tra kích thước bảo vệ nó.

#Lỗ hổng chuỗi định dạng

dinhdang.c
static void an_toan(const char *nguoi_dung) {
    printf("%s\n", nguoi_dung);      /* dung */
}

static void nguy_hiem(const char *nguoi_dung) {
    printf(nguoi_dung);              /* SAI: nguoi dung dieu khien dinh dang */
    printf("\n");
}
terminal
gcc -std=c11 -Wall -Wextra -Wformat=2 -c dinhdang.c -o /dev/null
dinhdang.c:10:5: warning: format not a string literal and no format arguments [-Wformat-security]
     printf(nguoi_dung);
     ^~~~~~
./dd.exe 'xin chao'
=== an toan ===
xin chao
=== nguy hiem ===
xin chao
# Dau vao chua cac dinh dang %x
./dd.exe '%x.%x.%x.%x.%x.%x'
=== an toan ===
%x.%x.%x.%x.%x.%x
=== nguy hiem ===
10000.fffffffc.35ad0500.10.12.657bc0

#%n làm được gì

Đặc tả %n
Một đặc tả định dạng của printf không in gì cả, mà ghi số ký tự đã in ra cho tới điểm đó vào địa chỉ mà đối số con trỏ trỏ tới. Nó biến một lỗ hổng chuỗi định dạng từ đọc bộ nhớ thành ghi bộ nhớ tuỳ ý.
%n dùng hợp pháp, và vì sao nó nguy hiểm
/* Dung hop phap: dem so ky tu da in */
int da_in;
printf("xin chao%n th14 gioi", &da_in);
/* da_in = 8, so ky tu truoc %n. Hiem khi dung den. */

/* Voi lo hong chuoi dinh dang, %n cho phep GHI:
   Neu ke tan cong dieu khien chuoi dinh dang, ho viet:

     "%100x%n"

   %100x in 100 ky tu (mot so, dem cho du 100 cot).
   %n ghi so 100 vao dia chi lay tu ngan xep.

   Ket hop voi %hhn (ghi 1 byte) va cac ky thuat dinh vi,
   ke tan cong ghi duoc GIA TRI TUY Y vao DIA CHI TUY Y.
   Do la buoc cuoi de bien doc bo nho thanh chiem dieu khien:
   ghi de len mot con tro ham, hoac len dia chi tra ve. */

#Tràn số vượt kiểm tra

transo.c
/* Cap phat mot mang n phan tu, moi phan tu 16 byte. SAI. */
static void *cap_sai(size_t n) {
    size_t tong = n * 16;               /* CO THE TRAN */
    printf("  cap_sai: n=%zu, n*16=%zu\n", n, tong);
    void *p = malloc(tong);
    if (p) memset(p, 0, 16);
    return p;
}

/* Kiem tra tran TRUOC khi nhan. */
static void *cap_dung(size_t n) {
    if (n > SIZE_MAX / 16) {
        printf("  cap_dung: n=%zu -> TU CHOI (se tran)\n", n);
        return NULL;
    }
    size_t tong = n * 16;
    printf("  cap_dung: n=%zu, n*16=%zu -> OK\n", n, tong);
    void *p = malloc(tong);
    if (p) memset(p, 0, 16);
    return p;
}
terminal
./transo.exe
=== nhan tran (n*16) ===
  cap_sai: n=1152921504606846976, n*16=0
  cap_dung: n=1152921504606846976 -> TU CHOI (se tran)

#Lỗi dấu

transo.c, phần kiểm tra có dấu
/* Kiem tra kich thuoc kieu "cong them tieu de", loi lech dau. */
static int kiem_tra_do_dai(int do_dai_bao) {
    char bo_dem[256];
    if (do_dai_bao > (int)sizeof bo_dem) {   /* int co dau! */
        printf("  do_dai=%d -> tu choi\n", do_dai_bao);
        return -1;
    }
    printf("  do_dai=%d -> chap nhan, se memcpy %d byte\n",
           do_dai_bao, do_dai_bao);
    return 0;
}
terminal
./transo.exe
=== do dai am vuot kiem tra co dau ===
  do_dai=100 -> chap nhan, se memcpy 100 byte
  do_dai=-1 -> chap nhan, se memcpy -1 byte
Kiểm tra có dấu, một phía
int kiem_tra_do_dai(int do_dai_bao) { char bo_dem[256]; if (do_dai_bao > (int)sizeof bo_dem) return -1; memcpy(bo_dem, nguon, do_dai_bao); return 0; } /* -1 qua duoc, thanh SIZE_MAX trong memcpy. */
size_t, hai phía
int kiem_tra_do_dai(size_t do_dai_bao) { char bo_dem[256]; if (do_dai_bao > sizeof bo_dem) /* size_t, khong the am */ return -1; memcpy(bo_dem, nguon, do_dai_bao); return 0; } /* do_dai_bao khong dau, nen "am" khong ton tai. Neu du lieu vao la int, chuyen VA kiem tra tuong minh: */ int tho = doc_int_tu_mang(); if (tho < 0) return -1; /* can duoi */ size_t do_dai = (size_t)tho; if (do_dai > sizeof bo_dem) return -1; /* can tren */

#Tính kích thước an toàn

Hàm đọc khung tin an toàn, gộp mọi quy tắc
#include <stdint.h>

#define KHUNG_TOI_DA (16u * 1024u * 1024u)      /* 16 MB, gioi han cung */

int doc_khung(int fd, uint8_t **ra, size_t *ra_co) {
    /* Doc do dai 4 byte, thu tu mang */
    uint32_t do_dai_mang;
    if (doc_du(fd, &do_dai_mang, 4) != 0) return -1;
    uint32_t do_dai = ntohl(do_dai_mang);       /* khong dau, tot */

    /* Quy tac 1: kiem tra ca hai can */
    if (do_dai == 0) return -1;                 /* can duoi */
    if (do_dai > KHUNG_TOI_DA) return -1;       /* can tren cung */

    /* Quy tac 2: kiem tra tran khi cong 1 cho NUL.
       do_dai <= 16 MB nen +1 khong the tran uint32_t o day,
       nhung viet ro de an toan khi ai do doi KHUNG_TOI_DA. */
    size_t cap;
    if (__builtin_add_overflow((size_t)do_dai, 1, &cap)) return -1;

    /* Cap phat va doc */
    uint8_t *dem = malloc(cap);
    if (dem == NULL) return -1;
    if (doc_du(fd, dem, do_dai) != 0) { free(dem); return -1; }
    dem[do_dai] = 0;                            /* an toan: cap = do_dai+1 */

    *ra    = dem;
    *ra_co = do_dai;
    return 0;
}

/* Ham nay chiu duoc moi gia tri 4 byte ma ke tan cong gui:
     0            -> tu choi (can duoi)
     0xFFFFFFFF   -> tu choi (can tren)
     bat ky > 16M -> tu choi
   Khong co gia tri nao dan toi cap phat sai hay tran. */

Tự làm thử

  1. Chạy dinhdang.c với đầu vào chứa nhiều %x và quan sát nó đọc ngăn xếp.
  2. Thêm thuộc tính format(printf, ...) vào một hàm nhật ký của bạn và tìm lỗi mới.
  3. Chạy transo.c và tìm giá trị n nhỏ nhất làm n * 16 tràn về một số nhỏ hơn 256.
  4. Đổi mọi int dùng làm độ dài trong một module của bạn thành size_t và sửa các cảnh báo.
  5. Cài doc_khung an toàn và thử mọi giá trị độ dài đặc biệt: 0,0xFFFFFFFF, và một số lớn hơn giới hạn.
  6. Thay mọi malloc(n * kich_thuoc) trong dự án của bạn bằng calloc hoặc bằng phép nhân có kiểm tra trà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

  • printf(bien) cho phép đọc ngăn xếp; đối số định dạng phải luôn là hằng chuỗi.
  • %n biến lỗ hổng chuỗi định dạng từ đọc thành ghi bộ nhớ tuỳ ý; đừng dựa vào việc nó bị tắt.
  • Một phép nhân có thể tràn về số nhỏ và vượt qua chính cái kiểm tra kích thước; dùng calloc hoặc __builtin_mul_overflow.
  • int âm vượt qua kiểm tra cận trên rồi thành SIZE_MAX trong memcpy; dùng size_t và kiểm tra cả hai cận.
  • Độ dài trong dữ liệu là một lời hứa, không phải sự thật; luôn giới hạn bằng một hằng cứng.