Fuzzing in Binary exploitation: American Fuzzing Lop

Chào những bông hoa nhỏ ham hack !

 

Hôm nay chúng ta sẽ nói về một kỹ thuật/công cụ khá quan trọng trong việc kiểm thử xâm nhập, tìm kiếm các lỗ hổng bảo mật đó là fuzzing.

 

Fuzzing, còn được gọi là kiểm thử fuzz, là một cách tiếp cận mạnh mẽ được sử dụng trong an ninh mạng để phát hiện lỗ hổng an ninh trong phần mềm, hệ điều hành hoặc mạng bằng cách nhập vào một lượng lớn dữ liệu ngẫu nhiên, được gọi là fuzz, vào hệ thống với mục đích làm cho nó bị lỗi, xảy ra crash, … Blog này sẽ đi sâu vào kỹ thuật fuzzing và fuzzer rất phổ biến đó là AFL.

 

FBI warning: Bài viết có thể gây ngủ gật, quý độc giả vui lòng không đọc khi buồn ngủ

 

Fuzzing Là Gì?

Fuzzing là một kỹ thuật kiểm thử phần mềm tự động, mục tiêu chính của nó là phát hiện ra những điểm yếu trong một hệ thống, ứng dụng, hoặc mạng bằng cách tạo ra và nhập vào dữ liệu không hợp lệ hoặc ngẫu nhiên. Nếu hệ thống bị lỗi hoặc bị sập, đó có một lỗ hổng an ninh tiềm ẩn có thể bị hacker khai thác. Fuzzing giúp xác định những lỗ hổng này để có thể sửa chữa trước khi bị kẻ tấn công xác định và khai thác chúng.

Tại sao lại cần Fuzzing?

Khi xây dựng một chương trình, developers sẽ tự viết các test case để kiểm tra code của mình. Do đó, các test case này sẽ hơi chủ quan vì họ đã biết trước các luồng xử lí.

 

Vì vậy, developers lại cần đến testers. Testers sẽ đưa ra các test case khách quan nhất, nhưng vẫn chưa đảm bảo được tất cả các nhánh khi chạy chương trình.

 

Trong khi đó, fuzzing sẽ giúp phát hiện ra các lỗi mà khó có thể tìm thấy thông qua việc viết các test case bình thường của tester hay developer. Fuzzing cũng giúp tiết kiệm thời gian và tăng hiệu quả bởi việc kiểm thử được diễn ra tự động, chúng ta có thể tìm ra lỗi ngay cả khi đang ngủ!

Fuzzer

Fuzzer là các công cụ (tool) thực hiện việc fuzzing một các tự động. Fuzzer có thể được chia thành 3 loại:

  • Mutational: fuzzer sẽ ngẫu nhiên hóa dữ liệu đầu vào bằng cách biến đổi hoặc sử dụng các dữ liệu khác nhau
  • Grammar: người sử dụng sẽ quyết định các quy luật biến đổi dữ liệu đầu vào
  • Feedback-based: đây còn được gọi là smart fuzzer bởi fuzzer sẽ giám sát ảnh hưởng của dữ liệu đầu vào tới chương trình, sau đó đưa ra các quyết định tới việc thay đổi dữ liệu đầu vào sao cho việc fuzzing được tối ưu nhất có thể

Một Feedback-based fuzzer rất phổ biến và đã có nhiều ứng dụng đó là American Fuzzing Lop (AFL)

American Fuzzing Lop (AFL)

AFL là một fuzzer mã nguồn mở, được phát triển bởi Michal Zalewski vào năm 2013. Thời điểm đó, AFL được coi là một fuzzer rất “hiện đại” và cũng đã phát hiện ra rất nhiều lỗ hổng trong các phần mềm mã nguồn mở như X.Org Server, PHP, OpenSSL, bash, firefox, Qt, SQLite,…

 

Khi thực hiện fuzzing, có 2 tiêu chí rất quan trọng nhằm đánh giá hiệu quả của quá trình fuzzing, đó là độ bao phủ mã nguồn (code coverage) và độ bao phủ các nhánh của chương trình (path coverage). Code coverage được dùng để chỉ lượng mã nguồn được thực thi đối với 1 test case cụ thể. Còn path coverage được dùng để đánh giá về các luồng được thực thi đối với 1 test case cụ thể.

 

Để hiểu rõ về 2 tiêu chí trên, ta hãy cùng xét ví dụ sau:

if (condition 1):
// statements
if (condition 2):
// statements

Trong đoạn mã giả trên, giả sử có 3 test case lần lượt thoả mãn condition 1, condition 2 và cả condition 1 lẫn condition 2. Dẫn tới chương trình có thể có tới 3 nhánh. Giả sử chỉ có test case 1 và test case 2 được kiểm tra, dẫn đến độ bao phủ mã nguồn sẽ là 100%, nhưng 2 test case đó mới kiểm tra được 2 nhánh, do đó độ bao phủ nhánh sẽ là 2/3.

 

AFL được đánh giá là khá thông minh bởi khi bắt đầu fuzzing, AFL quan sát trạng thái của chương trình khi các test case được xử lí, đồng thời cũng lưu lại các nhánh mã nguồn được kiểm tra. Dựa vào các nhánh đó, AFL sẽ đưa ra các quyết định tới dữ liệu đầu vào nhằm tìm kiếm những nhánh mới, qua đó tăng độ bao phủ nhánh.

 

Tác giả của AFL cũng đã có một thử nghiệm, khi fuzzing một chương trình mà chương trình ấy chỉ chấp nhận dữ liệu đầu vào là một file ảnh JPEG. Tác giả đã dùng test case ban đầu là 1 file text với nội dung “hello”. Chi tiết về bài viết tại đây.

 

Workflow sử dụng AFL có thể được mô tả ngắn gọn như sau:

  • Biên dịch mã nguồn sang file thực thi bằng trình biên dịch của AFL để AFL có thể dễ dàng quan sát được trạng thái của chương trình.
  • Xây dựng (các) test case
  • Chạy chương trình đã được biên dịch bằng (các) test case đó
  • Phân tích kết quả thu được

Thuật toán của AFL

Có 2 bước chính trong việc xây dựng, biến đổi các dữ liệu đầu vào của AFL:

  • Mutation: AFL sẽ thực hiện các biến đổi trên cùng 1 test case với các vị trí khác nhau. các biến đổi này bao gồm:
    • Đảo bit: thực hiện từ 1 cho tới 32 bits
    • Tăng/giảm các số nguyên dạng 8, 16, 32 bits dưới dạng little/big endian
    • Sử dụng các số đặc biệt: số 0, số nguyên có dấu/ không dấu lớn nhất/ nhỏ nhất với độ dài khác nhau (cả little/big endian)
    • Sử dụng các byte đặc biệt được cung cấp bởi người dùng hoặc tự phát hiện
  • Havoc:
    • Sử dụng các biến đổi như mutation
    • Ghi đè các byte bằng byte bất kì
    • Xoá, lặp, gán giá trị cho mỗi byte trong các block nhiều byte

Nếu qua mutation và havoc AFL không phát hiện thêm test case nào thu được nhánh chương trình mới thì AFL sẽ lấy 2 test case, kết hợp với nhau ở vị trí bất kì rồi thực hiện lại từ đầu.

AFL cũng là fuzzer đầu tiên sử dụng “binned hitcounts” trong việc thống kê độ bao phủ của mã nguồn.

 

Các nhánh của chương trình được AFL lưu lại và thống kê theo số lần đã thực thi. AFL sẽ lưu trữ thông tin về nhánh – số lần thực thi và chia các nhánh đó vào trong các buckets theo số lần thực thi. Các buckets được chia theo: 1, 2, 4-7, 8-15, 16-31, 32-127, và 128 lần thực thi trở lên. Một test case được xem là thú vị nếu test case đó được đưa vào 1 bucket mới.

 

Tác giả của AFL khuyên rằng nên sử dụng test case ban đầu có độ lớn nhỏ bởi test case càng nhỏ thì thời gian thực thi cũng sẽ ngắn hơn. AFL cũng ưu tiên việc sử dụng các test case nhỏ. Khi một test case mới có độ dài ngắn hơn test case khác nhưng cũng cho ra cùng một kết quả thì test case cũ sẽ được loại bỏ, thay thế bằng test case mới.

Trải nghiệm sử dụng AFL

Ta cùng thử sử dụng AFL với một chương trình mẫu FuzzGoat.

 

Trước tiên, ta cần phải cài đặt AFL. Việc cài đặt diễn ra khá đơn giản. Cũng như nhiều project khác trên github, ta chỉ cần clone repo về và chạy câu lệnh “make” và sau đó là “make install” (chạy với quyền root). 

install AFL
install AFL-2

Sau đó, tiếp tục cài đặt chương trình thử nghiêm FuzzGoat. Ta cũng chỉ cần clone repo về và chạy câu lệnh “make”

Chương trình đã được biên dịch bằng trình biên dịch của AFL afl-cc. Chúng ta chỉ cần chạy AFL bằng câu lệnh sau: “afl-fuzz -i in -o out ./fuzzgoat @@”

Trong đó tham số -i in là thư mục chứa các tệp test case, -o out là thư mục chứa kết quả.

 

Màn hình khi đang chạy AFL hiển thị như sau:

Trong lúc đợi AFL fuzzing, chúng ta có thể ngủ một giấc, dậy đi ăn sáng phở Thìn bờ Hồ rồi nhâm nhi một cốc cà phê trên Nhà thờ.

 

Kiểm tra kết quả sau khi fuzzing trong thư mục output, ta nhận được kết quả là một file với nội dung “{}”. 

 

Tài liệu tham khảo

423 lượt xem