Làm chủ AFL++ trên Linux: Hướng dẫn thực hành Fuzzing C/C++ để tìm lỗ hổng bảo mật

Security tutorial - IT technology blog
Security tutorial - IT technology blog

Rủi ro và thách thức trong phát triển phần mềm C/C++

C và C++ cho phép bạn kiểm soát trực tiếp phần cứng, nhưng sự tự do đó là con dao hai lưỡi. Quản lý bộ nhớ thủ công nổi tiếng là khó để thực hiện chính xác. Ngay cả tại các “ông lớn” công nghệ như Microsoft và Google, nghiên cứu cho thấy khoảng 70% tất cả các lỗ hổng bảo mật là vấn đề an toàn bộ nhớ. Một lỗi buffer overflow (tràn bộ đệm) bị bỏ sót hoặc lỗi use-after-free có thể biến một sự cố crash đơn giản thành một lỗ hổng thực thi mã từ xa (RCE) thảm khốc.

Các bài kiểm tra unit test tiêu chuẩn thường bỏ lỡ các trường hợp biên (edge cases) này. Chúng ta thường viết test cho các đầu vào mong đợi, trong khi tin tặc lại tìm kiếm những thứ mà chúng ta không nghĩ tới. Đây là lúc fuzzing phát huy tác dụng. Thay vì đoán xem điều gì có thể làm hỏng mã của bạn, fuzzing sử dụng các công cụ tự động để “dội bom” chương trình với hàng triệu đầu vào ngẫu nhiên, bị biến đổi (mutated) để xem điều gì khiến nó gục ngã.

Cách thức hoạt động kỳ diệu của AFL++

AFL++ (American Fuzzy Lop Plus Plus) là công cụ hàng đầu cho các nhà nghiên cứu bảo mật hiện đại. Nó là một bản fork hiệu suất cao của AFL gốc, được tối ưu hóa với các engine biến đổi tốt hơn và thuật toán nhanh hơn. Nhưng AFL++ không chỉ là một fuzzer “ngớ ngẩn” ném dữ liệu rác ngẫu nhiên vào một tệp thực thi.

Nó sử dụng coverage-guided fuzzing (fuzzing dựa trên độ bao phủ). Khi bạn biên dịch mã, AFL++ sẽ chèn instrumentation—hãy coi chúng là những cảm biến nhỏ—vào mọi nhánh và hàm. Khi fuzzer chạy, nó theo dõi các đường dẫn (path) mà quá trình thực thi đi qua. Nếu một đầu vào cụ thể chạm đến một nhánh mới, AFL++ sẽ lưu nó dưới dạng “seed” (hạt giống). Sau đó, it biến đổi seed đó để đào sâu hơn vào logic của bạn. Về cơ bản, đây là một thuật toán tiến hóa được thiết kế để tìm lỗi.

Thiết lập phòng thí nghiệm Fuzzing

Fuzzing là một tác vụ tiêu tốn nhiều tài nguyên. Tôi khuyên bạn nên sử dụng một phiên bản Linux riêng biệt, chẳng hạn như Ubuntu 22.04, vì bạn sẽ đẩy CPU của mình đến giới hạn. Trên một bộ vi xử lý tầm trung hiện đại, AFL++ thường có thể đạt tốc độ hơn 1.000 lần thực thi mỗi giây trên mỗi lõi.

Bảo mật là tối quan trọng khi thiết lập một máy chủ mới. Đối với các bài kiểm tra chạy dài ngày, tôi sử dụng trình tạo mật khẩu tại toolcraft.app/vi/tools/security/password-generator để tạo thông tin đăng nhập mạnh. Nó chạy cục bộ trong trình duyệt của bạn, giúp giữ các khóa của bạn không bị lộ trên mạng.

Để bắt đầu, hãy cài đặt các phụ thuộc cốt lõi và xây dựng AFL++ từ mã nguồn:

sudo apt update
sudo apt install -y build-essential python3-dev automake flex bison libglib2.0-dev libpixman-1-dev clang lld llvm llvm-dev
git clone https://github.com/AFLplusplus/AFLplusplus
cd AFLplusplus
make distrib
sudo make install

Nhân Linux của bạn cần một vài tinh chỉnh trước khi bắt đầu lần chạy đầu tiên. AFL++ xử lý các sự cố crash trực tiếp, vì vậy bạn phải ngăn hệ điều hành ghi core dump xuống đĩa. Bạn cũng nên đặt CPU sang chế độ performance để tối đa hóa thông lượng:

# Vô hiệu hóa trình báo cáo sự cố mặc định
echo core | sudo tee /proc/sys/kernel/core_pattern
# Tối ưu hóa tần số CPU để đạt tốc độ cao
cd /sys/devices/system/cpu
echo performance | sudo tee cpu*/cpufreq/scaling_governor

Thực hành: Tìm lỗi trong vài giây

Hãy xây dựng một chương trình C nhỏ có lỗi để xem AFL++ có thể tìm ra đường dẫn gây lỗi cụ thể nhanh như thế nào. Chương trình này chỉ crash nếu nó nhận được một chuỗi ký tự rất cụ thể.

1. Mã nguồn mục tiêu

Lưu tệp này với tên vulnerable.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void process_data(char *buf) {
    if (buf[0] == 'F') {
        if (buf[1] == 'U') {
            if (buf[2] == 'Z') {
                if (buf[3] == 'Z') {
                    // Đây là mục tiêu của chúng ta: một lỗi nghiêm trọng giả lập
                    abort(); 
                }
            }
        }
    }
}

int main(int argc, char *argv[]) {
    if (argc < 2) return 1;
    FILE *f = fopen(argv[1], "r");
    if (!f) return 1;

    char buffer[100];
    if (fgets(buffer, sizeof(buffer), f)) {
        process_data(buffer);
    }
    fclose(f);
    return 0;
}

2. Biên dịch với Instrumentation

Hãy tạm quên gcc tiêu chuẩn đi. Chúng ta cần các trình bao biên dịch (compiler wrappers) của AFL++ để thêm các “cảm biến” cần thiết vào tệp thực thi. Sử dụng afl-clang-fast để có hiệu suất tốt nhất:

afl-clang-fast vulnerable.c -o vulnerable_target

3. Tạo một Seed

AFL++ cần một điểm bắt đầu. Nó không cần phải phức tạp—chỉ cần một tệp đơn giản để khởi động engine:

mkdir in
echo "test" > in/seed.txt

4. Bắt đầu cuộc tấn công

Bắt đầu fuzzer bằng lệnh bên dưới. Ký hiệu @@ yêu cầu AFL++ chèn các tệp kiểm tra do nó tạo ra làm đối số dòng lệnh.

afl-fuzz -i in -o out -- ./vulnerable_target @@

Màn hình trạng thái sẽ chiếm toàn bộ terminal của bạn. Hãy theo dõi bộ đếm “unique crashes”. Đối với chương trình cụ thể này, AFL++ thường sẽ tìm thấy chuỗi “FUZZ” và kích hoạt crash trong chưa đầy 10 giây.

Phân tích kết quả tìm được

Khi bạn thấy một crash, hãy nhấn Ctrl+C. AFL++ lưu trữ mọi đầu vào gây ra lỗi trong thư mục out/default/crashes. Các tệp này chứa chính xác dữ liệu đã làm hỏng chương trình của bạn.

Để điều tra, hãy liệt kê các tệp và đọc nội dung:

ls out/default/crashes
cat out/default/crashes/id:000000... # Sử dụng tên tệp thực tế

Bây giờ bạn có thể nạp tệp này vào GDB hoặc Valgrind để xem chính xác nơi xảy ra lỗi bộ nhớ. Vòng lặp này—fuzz, crash, debug, fix—là cốt lõi của quy trình phát triển bảo mật hiện đại.

Các chiến thuật nâng cao cho phần mềm phức tạp

Các mục tiêu thực tế như trình phân tích hình ảnh hoặc giao thức mạng khó bẻ khóa hơn nhiều so với mã mẫu của chúng ta. Đây là cách để mở rộng quy mô:

  • Sử dụng Dictionary: Khi fuzzing dữ liệu có cấu trúc như JSON hoặc SQL, hãy sử dụng cờ -x để cung cấp một từ điển các từ khóa. Điều này giúp fuzzer vượt qua các kiểm tra cú pháp ngay lập tức.
  • Chế độ Persistent: Việc khởi động lại một tiến trình hàng nghìn lần mỗi giây gây ra chi phí tài nguyên rất lớn. Sử dụng __AFL_LOOP trong mã C của bạn để fuzz trong một tiến trình duy nhất, thường giúp tăng tốc độ từ 5 đến 10 lần.
  • Fuzzing song song: Nếu bạn có CPU 16 lõi, hãy tận dụng nó. Chạy một instance chính (-M master) và 15 instance phụ (-S slaveN) để khám phá không gian đường dẫn mã cùng một lúc.

Kết luận: Hãy biến Fuzzing thành một thói quen

Fuzzing không nên là một sự kiện diễn ra một lần trước khi ra mắt sản phẩm. Nó nên thuộc về quy trình CI/CD của bạn. Bằng cách để AFL++ chạy kiểm tra các tính năng mới qua đêm, bạn sẽ phát hiện ra các lỗ hổng trước khi chúng kịp tiếp cận máy chủ production.

Hãy bắt đầu từ những việc nhỏ bằng cách fuzzing các tiện ích cục bộ. Khi bạn đã thành thạo, hãy chuyển sang các thư viện khổng lồ như openssl hoặc libxml2. Bạn sẽ nhanh chóng nhận ra rằng ngay cả những phần mềm được coi là “ổn định” thường cũng chỉ cách một sự sụp đổ hoàn toàn bởi một đầu vào kỳ lạ.

Share: