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

AddressSanitizer

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

  • Dịch và chạy chương trình với -fsanitize=address
  • Đọc báo cáo ASan: nơi truy cập sai, nơi cấp phát, nơi giải phóng
  • Hiểu redzone và shadow memory làm việc ra sao
  • Biết bốn loại lỗi ASan không bắt được

AddressSanitizer là công cụ hiệu quả nhất trong toàn bộ phần này. Một cờ dịch, chậm gấp đôi, và nó biến mọi lỗi truy cập bộ nhớ từ chỗ "sập ở đâu đó xa lắm" thành một báo cáo chỉ đúng ba dòng mã.

#Chạy thử một lần

tran_bo_dem.c
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *a = malloc(5 * sizeof *a);

    for (int i = 0; i <= 5; ++i)      /* <= thay vi <: thua mot vong */
        a[i] = i;

    printf("%d\n", a[0]);

    free(a);
    return 0;
}
terminal
# Không có ASan: chạy êm, mã thoát 0
gcc -std=c11 -g -O1 -Wall -Wextra -o tbd tran_bo_dem.c && ./tbd
0
echo $?
0
terminal
# Có ASan
gcc -std=c11 -g -O1 -fno-omit-frame-pointer -fsanitize=address -o tbd tran_bo_dem.c && ./tbd
=================================================================
==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x602000000014 at pc 0x0000004011b6 bp 0x7ffd4f2c1e30 sp 0x7ffd4f2c1e28
WRITE of size 4 at 0x602000000014 thread T0
    #0 0x4011b5 in main tran_bo_dem.c:8
    #1 0x7f8c3f029d8f in __libc_start_call_main
    #2 0x7f8c3f029e3f in __libc_start_main
    #3 0x401104 in _start

0x602000000014 is located 0 bytes to the right of 20-byte region [0x602000000000,0x602000000014)
allocated by thread T0 here:
    #0 0x7f8c3f2b7357 in malloc
    #1 0x401186 in main tran_bo_dem.c:5

SUMMARY: AddressSanitizer: heap-buffer-overflow tran_bo_dem.c:8 in main
==12345==ABORTING

#Đọc báo cáo

DòngNói gì
ERROR: heap-buffer-overflowLoại lỗi. Có bảy loại, xem mục dưới
WRITE of size 4Ghi hay đọc, và bao nhiêu byte
#0 ... main tran_bo_dem.c:8Dòng gây lỗi. Đây là dòng bạn cần mở ra
0 bytes to the right of 20-byte regionVượt bao nhiêu byte, và về phía nào
allocated by thread T0 here: ... :5Dòng đã cấp phát khối đó
SUMMARYMột dòng tóm tắt, tiện cho việc lọc trong nhật ký

#Cơ chế: vành đỏ và bộ nhớ bóng

Vành đỏ (redzone)
Vùng đệm ASan chèn vào trước và sau mỗi khối cấp phát. Mọi truy cập rơi vào vành đỏ đều bị báo lỗi ngay lập tức.
Bộ nhớ bóng (shadow memory)
Một byte trạng thái cho mỗi tám byte bộ nhớ thật. Trước mỗi lần đọc hoặc ghi, ASan tra byte bóng tương ứng để biết vùng đó có dùng được không.
Một byte bóng cho tám byte thật. Mỗi lần truy cập chỉ tốn một phép tra.
Trình biên dịch chèn gì vào
/* Ban viet: */
a[i] = 1;

/* ASan bien no thanh, dai y: */

char *dia_chi = (char *)&a[i];
char *bong    = (char *)(((uintptr_t)dia_chi >> 3) + 0x7fff8000);

if (*bong != 0 && ((uintptr_t)dia_chi & 7) + 4 > *bong)
    __asan_report_store4(dia_chi);      /* in bao cao roi dung */

a[i] = 1;

/* Mot phep dich, mot phep cong, mot lan doc bo nho, mot phep so sanh.
   Do la ly do ASan chi cham gap doi chu khong gap hai muoi nhu Valgrind:
   Valgrind mo phong tung lenh may, con ASan chi chen them bon lenh. */
Giá trị byte bóngNghĩa
00Cả 8 byte dùng được
01 tới 07n byte đầu dùng được, phần còn lại là vành đỏ
faVành đỏ trái của khối heap
fbVành đỏ phải của khối heap
fdVùng đã giải phóng
f1Vành đỏ trái của biến ngăn xếp
f5Vùng ngăn xếp của hàm đã trả về
f9Vành đỏ của biến toàn cục

#Sáu loại lỗi nó bắt

heap-buffer-overflow

int *a = malloc(20);
a[5] = 1;                  /* 0 bytes to the right of 20-byte region */
a[-1] = 1;                 /* 4 bytes to the left */

stack-buffer-overflow

void f(void) {
    int a[5];
    a[5] = 1;              /* ASan: stack-buffer-overflow */
}

/* Valgrind KHONG bat duoc loai nay, vi no khong biet ranh gioi
   giua cac bien tren ngan xep. ASan biet vi trinh bien dich
   noi cho no. */

global-buffer-overflow

static int a[5];

int main(void) {
    a[5] = 1;              /* ASan: global-buffer-overflow */
}

heap-use-after-free

terminal
./uaf
==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x602000000010 at pc 0x000000401203 bp 0x7ffe1d3e5a10 sp 0x7ffe1d3e5a08
READ of size 4 at 0x602000000010 thread T0
    #0 0x401202 in main uaf.c:11

0x602000000010 is located 0 bytes inside of 16-byte region [0x602000000010,0x602000000020)
freed by thread T0 here:
    #0 0x7f1a2c4b8b6f in free
    #1 0x4011ec in main uaf.c:9

previously allocated by thread T0 here:
    #0 0x7f1a2c4b8357 in malloc
    #1 0x4011c6 in main uaf.c:6

SUMMARY: AddressSanitizer: heap-use-after-free uaf.c:11 in main

double-free và alloc-dealloc-mismatch

free(p);
free(p);                   /* ASan: attempting double-free */

void *p = malloc(10);
delete p;                  /* ASan: alloc-dealloc-mismatch, trong C++ */

stack-use-after-return

int *f(void) {
    int x = 42;
    return &x;
}

/* Mac dinh ASan KHONG bat loai nay vi no dat. Phai bat rieng: */

ASAN_OPTIONS=detect_stack_use_after_return=1 ./prog

/* Luc do ASan cap phat khung ngan xep tren heap, nen giu duoc
   vanh do sau khi ham tra ve. Cham hon dang ke. */

#Kiểm tra rò rỉ

ro_ri.c
#include <stdlib.h>
#include <string.h>

static char *tao_ten(const char *s) {
    char *p = malloc(strlen(s) + 1);
    strcpy(p, s);
    return p;
}

int main(void) {
    char *a = tao_ten("Nguyen Van A");
    char *b = tao_ten("Tran Thi B");

    free(a);
    /* quen free(b) */

    return 0;
}
terminal
gcc -std=c11 -g -O1 -fsanitize=address -o rr ro_ri.c && ./rr
=================================================================
==12345==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 11 bytes in 1 object(s) allocated from:
    #0 0x7f2b1c8b7357 in malloc
    #1 0x401196 in tao_ten ro_ri.c:5
    #2 0x4011f8 in main ro_ri.c:12

SUMMARY: AddressSanitizer: 11 byte(s) leaked in 1 allocation(s).
Tùy chọnTác dụng
ASAN_OPTIONS=detect_leaks=1Bật kiểm tra rò rỉ, mặc định đã bật trên Linux
ASAN_OPTIONS=detect_leaks=0Tắt, khi bạn cố ý không giải phóng lúc thoát
LSAN_OPTIONS=suppressions=bo_qua.txtBỏ qua rò rỉ khớp một danh sách
ASAN_OPTIONS=fast_unwind_on_malloc=0Vết gọi chính xác hơn, chậm hơn
-fsanitize=leakChỉ chạy LeakSanitizer, không kèm ASan, rất nhẹ

#Bốn thứ nó không bắt

Trường hợp ASan bỏ qua
typedef struct {
    char ten[8];
    int  diem;
} SinhVien;

SinhVien *sv = malloc(sizeof *sv);

strcpy(sv->ten, "NguyenVanA");    /* 11 byte vao mang 8 byte */

/* ASan KHONG bao, vi ca 11 byte van nam TRONG khoi 16 byte
   ma malloc cap. Vanh do nam sau khoi, khong nam giua cac truong.

   Chinh vi du nay o Bai 35.5 se duoc GDB bat bang watchpoint.

   Ba cach chua:
     - dung strncpy hoac snprintf thay strcpy
     - bat -D_FORTIFY_SOURCE=2, no bat duoc strcpy voi kich thuoc biet truoc
     - dung -fsanitize=address kem -fsanitize-address-use-after-scope */
terminal
# _FORTIFY_SOURCE bắt được nhiều lỗi chuỗi ngay lúc dịch
gcc -std=c11 -O2 -D_FORTIFY_SOURCE=2 -Wall -c sv.c
sv.c:9:5: warning: '__builtin___strcpy_chk' writing 11 bytes into a region of size 8 overflows the destination [-Wstringop-overflow=]

Tự làm thử

  1. Viết chương trình tràn bộ đệm lệch một và chạy nó có và không có ASan.
  2. Đọc báo cáo và chỉ ra ba thông tin: dòng ghi, dòng cấp phát, số byte vượt.
  3. Viết ba biến thể tràn: trên heap, trên ngăn xếp, trên biến toàn cục.
  4. Viết chương trình rò rỉ trong một hàm tiện ích được gọi ba lần, rồi xác định lời gọi nào rò rỉ.
  5. Chứng minh ASan không bắt được ghi tràn giữa hai trường của một struct.
  6. Bật -D_FORTIFY_SOURCE=2 và xem nó bắt được thêm gì.

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

  • Một cờ dịch, chậm gấp đôi, và báo cáo chỉ đúng dòng truy cập sai, dòng cấp phát và số byte vượt.
  • Cơ chế là một byte bóng cho mỗi tám byte thật, cộng với vành đỏ quanh mỗi khối.
  • Hàng đợi quarantine giữ khối đã giải phóng một thời gian, nhờ đó bắt được dùng sau khi giải phóng.
  • Vết gọi rò rỉ có hai tầng, và tầng gọi mới cho biết lời gọi nào bị rò rỉ.
  • Nó không bắt biến chưa khởi tạo, không bắt tràn số, và không bắt ghi tràn giữa hai trường của cùng một struct.