Chris Wellons mem-fuzz sekitar 1.000 proyek selama enam tahun, dan menurutnya setelan bawaan AFL++ meninggalkan bug yang seharusnya tertangkap. Masalah paling konkret ada di Address Sanitizer: defaultnya hanya mengisi 4 KiB pertama setiap alokasi dengan pola, sehingga bug use-after-free pada alokasi besar tidak pernah terdeteksi. Artikel Tips for more effective fuzz testing with AFL++ merangkum enam kebiasaan yang mengubah hasil fuzzing dari "tidak ada temuan" menjadi "ada temuan".
TL;DR
- Setel ASAN_OPTIONS dengan abort_on_error, halt_on_error, dan symbolize=0. Tanpa itu AFL++ tidak bisa menangkap crash dengan benar.
- Naikkan cakupan pengisian pola dari default 4 KiB menjadi 1 GiB (1<<30). Bug pada alokasi besar baru terlihat setelah ini.
- Pakai persistent mode karena biasanya satu orde besaran lebih cepat, tapi perhatikan isolasi state antar tes.
- Jangan pernah meneruskan buffer fuzz langsung ke target: buffer AFL++ lebih besar dari panjang input, jadi ASan tidak bisa mendeteksi overflow baca.
Kenapa dua sanitizer lebih baik daripada satu?
Karena sanitizer berfungsi seperti assertion yang dipasang otomatis oleh compiler, dan makin banyak assertion makin banyak peluang crash yang bisa ditemukan. Wellons menyarankan selalu memakai Address Sanitizer dan Undefined Behavior Sanitizer bersamaan lewat satu flag.
afl-gcc-fast -g3 -fsanitize=address,undefined ...
Flag -g3 bukan hiasan: tanpa informasi debug, crash report akan berisi alamat memori tanpa petunjuk baris kode, dan biaya menyelidikinya jauh lebih besar daripada biaya kompilasi ulang. Dokumentasi lengkap opsi sanitizer ada di halaman resmi AddressSanitizer.
Setelan ASAN_OPTIONS mana yang wajib diubah?
Minimal tiga opsi wajib disetel eksplisit, dan satu opsi tambahan sangat disarankan. Tanpa tiga yang wajib, AFL++ tidak mendapat sinyal yang ia butuhkan untuk mengenali proses sebagai crash.
export ASAN_OPTIONS="abort_on_error=1:halt_on_error=1:symbolize=0"
ASAN_OPTIONS+=":detect_stack_use_after_return=1"
ASAN_OPTIONS+=":max_malloc_fill_size=$((1<<30))"
Opsi symbolize=0 sengaja dimatikan saat fuzzing karena simbolisasi memakan waktu dan baru dibutuhkan ketika crash akan dianalisis ulang. Opsi detect_stack_use_after_return=1 memperlambat fuzzing sedikit, tapi menurut artikel itu sepadan dengan bug tambahan yang tertangkap.
| Opsi | Default ASan | Disarankan untuk fuzzing |
|---|---|---|
| abort_on_error | Mati | Aktif |
| halt_on_error | Mati | Aktif |
| symbolize | Aktif | Mati saat fuzzing |
| detect_stack_use_after_return | Mati | Aktif |
| max_malloc_fill_size | 4 KiB | 1 GiB |
Baris terakhir adalah temuan dengan dampak terbesar. ASan mengisi 4 KiB pertama alokasi baru dengan pola tertentu supaya bug use-after-free cepat terlihat. Kalau program mengalokasikan lebih dari 4 KiB lalu memakai memori itu tanpa menginisialisasi, kemungkinan besar ada byte nol setelah batas 4 KiB, sehingga strlen berhenti normal dan bug-nya lolos. Menurut Wellons, batas 4 KiB ini menyembunyikan sejumlah bug sampai ia menyadarinya.
Apa itu persistent mode dan kenapa lebih cepat?
Persistent mode menjalankan banyak tes di dalam satu proses, alih-alih meluncurkan proses baru untuk setiap input. Wellons menyebutnya biasanya satu orde besaran lebih cepat dibanding mode biasa yang hanya mengganti compiler, dan itu cukup alasan untuk menulis harness sendiri.
#include <unistd.h>
__AFL_FUZZ_INIT();
int main(void) {
__AFL_INIT();
unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
while (__AFL_LOOP(10000)) {
int len = __AFL_FUZZ_TESTCASE_LEN;
/* olah buf dengan panjang len */
}
}
Karena prosesnya tidak di-restart, mode ini punya jebakan: semua state global terbawa dari tes sebelumnya. Wellons menyarankan menyiapkan template sendiri, bukan memakai contoh resmi apa adanya, karena template resmi punya cacat di bagian isolasi.
Kenapa source file lebih baik daripada header?
Karena menyertakan file sumber langsung ke harness memungkinkan pengujian fungsi static dan menghilangkan konfigurasi build yang rumit. Dengan cara ini hanya dibutuhkan dua translation unit: satu untuk kode yang diinstrumentasi, satu untuk pustaka yang tidak.
#include "src/utils.c"
#include "src/parser.c"
#include <unistd.h>
Trik ini terlihat aneh bagi orang yang terbiasa satu file sumber satu translation unit, tapi efeknya besar: tidak ada lagi pengelolaan object file terpisah antara build fuzzing dan build normal, dan fungsi internal yang tidak pernah diuji bisa diuji. Untuk program yang punya main sendiri, nama fungsi itu bisa diganti sementara sebelum di-include:
#define main oldmain
#include "src/utils.c"
#include "src/parser.c"
#undef main
Pendekatan ini bertentangan dengan praktik rekayasa perangkat lunak yang rapi, dan Wellons mengakuinya. Alasannya pragmatis: sebagian besar proyek yang ia fuzz adalah proyek open source yang ia periksa sambil lewat, dan menulis ulang struktur build-nya bukan pilihan.
Kenapa buffer fuzz tidak boleh dipakai langsung?
Karena buffer yang dialokasikan AFL++ lebih besar daripada panjang input, sehingga ASan tidak akan pernah mendeteksi pembacaan melewati batas. Ini kesalahan paling umum dan paling halus, terutama karena template resmi justru mendorongnya.
myprogram(buf, len); // salah, overflow tidak terdeteksi
src = realloc(src, len);
memcpy(src, buf, len);
myprogram(src, len); // benar, ukuran pas
Kalau kode yang sedang diuji membaca satu byte lebih jauh dari len, pada buffer asli byte itu masih berada di dalam alokasi sehingga tidak ada yang dilanggar. Setelah disalin ke buffer berukuran pas, pembacaan itu langsung menjadi out-of-bounds. Wellons menyebut teknik ini berulang kali memunculkan temuan baru pada proyek yang test harness-nya sudah ada.
💬 Komentar (0)
Belum ada komentar. Jadilah yang pertama! 💬