Blog10 — Fuzzing & Secure Coding
Bố cục bài viết
Trang này
Ngày tạo
05/08/2026
Cập nhật
Lượt xem
3
Đọc
14m
10 — Fuzzing & Secure Coding
Mục tiêu: Khép lại lộ trình. Bạn đã biết viết parser chống input thù địch (
08) và hiểu lỗ hổng vận hành thế nào (09). Giờ bạn học cách tự động tìm những lỗ hổng đó bằng fuzzing + sanitizer, viết secure code theo quy chuẩn, đọc/audit codebase telecom thật, tích hợp vào CI, và báo cáo có trách nhiệm (CVD/CVSS) — đúng tinh thần GSMA mà bạn đã quen. Đây là vòng khép kín của một security researcher viễn thông.
⚠️ Phạm vi: Fuzz và audit trên mã của bạn, dự án mã nguồn mở (để đóng góp/báo cáo), hoặc hệ thống được cấp phép. Lỗ hổng tìm thấy ở phần mềm của bên khác phải đi qua quy trình Coordinated Vulnerability Disclosure (mục 7).
1. Fuzzing là gì và vì sao nó thắng review thủ công
Fuzzing = tự động bơm hàng triệu input (ngẫu nhiên/biến dị) vào chương trình, theo dõi crash/lỗi. Với parser protocol — nơi không gian input khổng lồ và lỗi nằm ở các nhánh hiếm — fuzzing tìm bug con người bỏ sót vì nó thử những tổ hợp byte không ai nghĩ tới.
┌─────────┐ biến dị ┌──────────┐ chạy ┌──────────────┐
│ corpus │ ───────────► │ input │ ───────► │ parser + │
│ (seed) │ ◄─────────── │ mới │ │ sanitizer │
└─────────┘ giữ lại nếu └──────────┘ └──────┬───────┘
phủ code mới │ crash/UB?
(coverage-guided) ▼
lưu input gây lỗi
Điểm mạnh cốt lõi: coverage-guided (libFuzzer, AFL++) — fuzzer đo nhánh code nào được chạy, ưu tiên giữ input mở ra nhánh mới, nên nó “học” cách đi sâu vào parser thay vì gõ ngẫu nhiên mù. Kết hợp với sanitizer, một truy cập ngoài biên không gây crash ngay vẫn bị bắt → fuzzing trở thành máy dò lỗ hổng bộ nhớ tự động.
Quy tắc thép #11: Mọi parser xử lý dữ liệu mạng phải có fuzz harness và chạy fuzzing dưới ASan/UBSan trước khi tin nó an toàn. Code review thủ công là cần nhưng không đủ — bug parser nghiêm trọng nhất thường ẩn ở nhánh mà mắt người không lần tới. “Chưa fuzz” nghĩa là “chưa biết nó có an toàn không”.
2. Sanitizer — kính hiển vi bắt lỗi (ôn & mở rộng từ 02/09)
Sanitizer là instrumentation compiler chèn vào để phát hiện lỗi tại thời điểm xảy ra, kể cả khi chương trình “có vẻ chạy bình thường”.
| Sanitizer | Cờ | Bắt gì |
|---|---|---|
| ASan (Address) | -fsanitize=address | buffer overflow (stack/heap/global), UAF, double-free, leak |
| UBSan (Undefined Behavior) | -fsanitize=undefined | integer overflow, shift sai, con trỏ lệch, deref NULL |
| MSan (Memory) | -fsanitize=memory | đọc bộ nhớ chưa khởi tạo (CWE-457) |
| TSan (Thread) | -fsanitize=thread | data race (quan trọng cho server epoll/đa luồng 07) |
# Build để dò lỗi tối đa khi phát triển/fuzz:
clang -g -O1 -fsanitize=address,undefined -fno-omit-frame-pointer parser.c -o parser_asan
ASan và MSan không chạy chung (xung đột); TSan riêng. Thực tế: chạy ASan+UBSan thường xuyên khi dev/fuzz; chạy MSan và TSan thành lượt riêng. Trên production để hiệu năng, không bật sanitizer — nhưng bắt buộc bật bộ cờ “cứng” ở
09mục 7.
3. libFuzzer — viết harness trong vài dòng
libFuzzer (đi kèm Clang) là in-process, coverage-guided. Bạn chỉ cần viết một hàm nhận buffer và gọi parser của bạn lên đó — libFuzzer lo phần sinh input và biến dị.
#include <cstdint>
#include <cstddef>
#include <span>
// parser cần fuzz (từ module 08) — bọc nó lại:
std::vector<std::unique_ptr<AvpNode>> parse_avp_list(std::span<const uint8_t>, int depth);
// Điểm vào libFuzzer: được gọi hàng triệu lần với data/size khác nhau
extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) {
parse_avp_list(std::span<const uint8_t>(data, size), 0); // chỉ cần GỌI; không cần kiểm kết quả
return 0; // ASan/UBSan tự bắt mọi lỗi bộ nhớ
}
# Build harness với libFuzzer + sanitizer:
clang++ -g -O1 -std=c++20 \
-fsanitize=fuzzer,address,undefined \
fuzz_avp.cpp diameter.cpp -o fuzz_avp
# Chạy (tự sinh input, lưu crash vào file ./crash-*):
./fuzz_avp -max_len=4096 corpus/
Khi fuzzer tìm thấy lỗi, nó dừng và ghi input gây crash ra crash-<hash>. Chạy lại đúng input đó dưới ASan để xem stack trace đầy đủ:
./fuzz_avp crash-deadbeef # tái hiện 1 lần, in báo cáo ASan chi tiết
Đây chính là capstone #2 của bạn (GTP-C fuzzer harness): viết
LLVMFuzzerTestOneInputbọcparse_gtp_ies, fuzz dưới ASan, báo cáo mọi crash kèm root-cause. Vài chục dòng harness thường tìm ra lỗi mà hàng giờ review bỏ sót.
4. AFL++ — fuzzer ngoài tiến trình, mạnh cho binary thật
AFL++ (American Fuzzy Lop plus plus) fuzz chương trình qua đầu vào file/stdin, hợp khi bạn không dễ viết harness in-process (vd một tool CLI parse pcap đã build sẵn). Nó cũng coverage-guided.
# Instrument khi biên dịch (afl-clang-fast là wrapper):
afl-clang-fast++ -fsanitize=address parser_cli.cpp -o parser_cli_afl
# Chạy: -i seed đầu vào, -o thư mục kết quả; @@ = chỗ thay bằng tên file input
afl-fuzz -i seeds/ -o findings/ -- ./parser_cli_afl @@
| | libFuzzer | AFL++ |
|–|———–|——-|
| Mô hình | in-process (gọi hàm) | ngoài tiến trình (file/stdin) |
| Tốc độ | rất nhanh | nhanh (có persistent mode) |
| Hợp khi | bạn cô lập được hàm parser | fuzz cả binary/tool có sẵn |
| Harness | cần viết LLVMFuzzerTestOneInput | thường không cần sửa mã nhiều |
Hai công cụ bổ trợ, không loại trừ. Thực tế dự án lớn dùng cả hai. AFL++ còn có chế độ persistent (gần tốc độ libFuzzer) và nhiều mutator/dictionary cho protocol.
5. Corpus & dictionary — cho fuzzer khởi đầu thông minh
Fuzzer mù bắt đầu từ con số không sẽ tốn rất lâu mới tạo nổi một gói Diameter “đủ giống thật” để qua các kiểm tra đầu (Version=1, length hợp lệ…). Seed corpus và dictionary tăng tốc khám phá nhiều bậc.
- Seed corpus: tập input hợp lệ làm điểm xuất phát. Lấy từ pcap thật trong lab: dùng
tsharktách payload protocol ra file.
# Tách payload Diameter từ pcap thành từng file seed cho fuzzer:
tshark -r capture.pcap -Y diameter -T fields -e diameter \
2>/dev/null | while read hex; do
echo "$hex" | xxd -r -p > "corpus/seed_$RANDOM.bin"
done
- Dictionary: từ khóa/hằng để fuzzer chèn (Command-Code, AVP code, magic byte). Vd với SIP:
"INVITE","Content-Length:","\r\n\r\n". libFuzzer:-dict=sip.dict; AFL++:-x sip.dict.
Corpus tốt = phủ nhanh các nhánh hợp lệ để fuzzer dồn sức biến dị vào vùng sâu nơi bug ẩn. Đây là lý do pcap thật trong môi trường bạn vận hành là tài sản fuzzing quý.
6. Triage — từ crash tới root-cause tới bản vá
Tìm ra crash mới là một nửa; phân loại đúng mới có giá trị. Quy trình triage:
crash file
│
▼
1. Tái hiện ổn định? ── không ──► flaky (race? môi trường?) → điều tra riêng (TSan)
│ có
▼
2. Chạy dưới ASan → đọc loại lỗi (heap-overflow / UAF / …) + stack trace
│
▼
3. Tối giản input (libFuzzer -minimize / afl-tmin) → input nhỏ nhất còn gây lỗi
│
▼
4. Xác định ROOT CAUSE ở mã (lá chắn nào của checklist `08` thiếu?)
│
▼
5. Vá + thêm input đó vào regression test → fuzz lại để xác nhận hết
# Tối giản input gây crash (libFuzzer):
./fuzz_avp -minimize_crash=1 -runs=100000 crash-deadbeef
# (AFL++): afl-tmin -i crash_input -o crash_min -- ./parser_cli_afl @@
Bước 4 nối thẳng về
09: đọc stack trace ASan → xác định dòng → đối chiếu lớp CWE → tìm lá chắn thiếu trong checklist08→ vá tận gốc (không chỉ “thêm if để hết crash” mà sửa đúng quy ước length/biên). Mỗi crash đã vá phải thành một test hồi quy để không tái phát.
7. Secure coding — checklist tổng & quy trình CVD/CVSS
7.1. Secure coding checklist cho C/C++ parser (tổng kết toàn lộ trình)
| ✓ | Quy tắc | Module gốc |
|---|---|---|
| ☐ | Mọi hàm xử lý mạng nhận (ptr, len)/span | 02, 08 |
| ☐ | Validate length bằng phép trừ một phía trước khi đọc | 08, 09 |
| ☐ | Mỗi vòng lặp TLV/AVP/IE tiến con trỏ > 0 | 08 (#9) |
| ☐ | Giới hạn độ sâu đệ quy + số phần tử + tổng size | 08 |
| ☐ | Dùng uintN_t cho mọi field protocol; xử lý endianness | 01(#2), 03(#6) |
| ☐ | Không strcpy/strcat/sprintf/gets; dùng bản có kiểm/snprintf | 09 (#10) |
| ☐ | C++: smart pointer, vector/string, span/string_view; tránh new/delete trần | 06 (#8) |
| ☐ | C: free xong gán NULL; sở hữu rõ ràng | 09 |
| ☐ | printf("%s", data) — không bao giờ printf(data) | 09 |
| ☐ | Build production với cờ “cứng” (canary/PIE/RELRO/FORTIFY) | 09 |
| ☐ | Có fuzz harness + chạy ASan/UBSan trong CI | 10 (#11) |
| ☐ | -Wall -Wextra sạch | 00 (#1) |
7.2. Đọc & audit codebase telecom thật (kho vàng)
Giờ bạn đủ trình đọc stack thật. Cách tiếp cận: tìm biên giới input (nơi byte mạng vào parser) rồi đối chiếu checklist 7.1.
wireshark/epan/dissectors/—packet-diameter.c,packet-gtpv2.c,packet-sip.c. Học cách parse chuẩn hàng trăm protocol; chú ý cách Wireshark dùngtvb_*(tvbuff) để bounds-check mọi truy cập — chính làByteReader/spanở quy mô công nghiệp.freeDiameter— stack Diameter C thật: peer FSM (liên hệ trực tiếp case S6a của bạn — đọclibfdcorexử lý CER/CEA/DWR và chuyển trạng thái peer), parse/build message.kamailio/OpenSIPS — SIP server hiệu năng cao: parser SIP, kiến trúc xử lý, là ví dụ secure-ish C quy mô lớn.libosmocore/Osmocom — SS7/SIGTRAN/M3UA, GSM; gần với mảng signaling bạn làm.open5gs— 5GC/EPC bằng C; NGAP/NAS/Diameter; đọc để hiểu lõi hiện đại.
Mẹo audit:
grepcác hàm rủi ro (memcpy,malloc,strcpy,[i]với index từ gói) ngay tại biên parse; với mỗi chỗ hỏi “length này từ đâu, đã validate chưa?”. Đối chiếu commit lịch sử + CVE của chính dự án để học từ lỗi đã từng có.
7.3. CI — biến an toàn thành tự động
# Ý tưởng pipeline (GitHub Actions / GitLab CI):
# - build -Wall -Wextra -Werror → fail nếu có warning
# - test dưới ASan + UBSan → fail nếu sanitizer kêu
# - chạy fuzzer ngắn (vd 5 phút) trên corpus → fail nếu có crash mới
# - kiểm regression: mọi crash cũ phải pass
Dự án nghiêm túc (kể cả OSS-Fuzz của Google) fuzz liên tục mỗi commit. Bạn nên đưa fuzz vào CI ngay từ project nhỏ.
7.4. Báo cáo có trách nhiệm — CVD & CVSS (tinh thần GSMA)
Khi bạn tìm thấy lỗ hổng thật (OSS hoặc thiết bị/mạng trong phạm vi được phép), không công bố bừa. Quy trình Coordinated Vulnerability Disclosure:
1. Tìm & xác nhận (PoC tối thiểu, môi trường lab)
2. Báo RIÊNG cho nhà cung cấp/maintainer (security@ hoặc kênh CVD)
- Với mạng/thiết bị viễn thông: theo kênh GSMA CVD (GSMA FS-series bạn đã quen)
3. Thỏa thuận thời gian vá (embargo, thường 90 ngày)
4. Hỗ trợ kiểm chứng bản vá
5. Công bố phối hợp sau khi đã vá (CVE ID, advisory)
Mỗi báo cáo nên có: mô tả lỗ hổng + lớp CWE, PoC tối giản (input gây lỗi), điểm CVSS v3.1, phần spec liên quan (RFC/3GPP TS), và đề xuất vá.
CVSS v3.1 lượng hóa mức nghiêm trọng qua các metric chính:
| Metric | Ý nghĩa | Ví dụ parser từ xa |
|---|---|---|
| AV Attack Vector | đường tấn công | Network (peer gửi gói) = nặng nhất |
| AC Attack Complexity | độ khó | Low nếu chỉ cần gửi 1 gói méo |
| PR Privileges Required | quyền cần có | None nếu tiền-xác thực |
| UI User Interaction | cần tương tác? | None với server tự xử gói |
| C/I/A | tác động Bảo mật/Toàn vẹn/Khả dụng | over-read=C(leak); CFH=C/I/A cao; DoS=A |
Một lỗi over-read tiền-xác thực trên parser HSS (AV:N/AC:L/PR:N/UI:N) thường rơi vào dải High–Critical — đây là lý do
09nhấn mạnh hiểu mức nghiêm trọng thật: bạn cần xếp hạng đúng để ưu tiên vá và viết advisory thuyết phục.
8. Bài tập / Lab module 10
- Harness đầu tiên: viết
LLVMFuzzerTestOneInputbọcparse_avp_list(từ08), build với-fsanitize=fuzzer,address,undefined, chạy. Nếu parser08của bạn cứng cáp, fuzzer chạy lâu không crash; nếu có lỗ, nó tìm ra — sửa rồi fuzz lại. - Tự gài bug rồi để fuzzer bắt: cố ý bỏ một lá chắn (vd
len > data.size()), fuzz, xác nhận fuzzer tìm ra trong giây, đọc báo cáo ASan, rồi vá lại. - Capstone #2 — GTP-C fuzzer: harness cho
parse_gtp_ies, fuzz dưới ASan. Báo cáo mọi crash kèm root-cause + lớp CWE. - Corpus từ pcap: dùng
tsharktách payload Diameter/SIP từ một pcap lab thành seed corpus; chạy fuzzer có/không corpus và so sánh tốc độ phủ (số nhánh/đơn vị thời gian, libFuzzer in ra). - Dictionary: viết
sip.dict(method, header tên, CRLF) vàdiameter.dict(command/AVP code), fuzz có dictionary và quan sát khác biệt. - Triage đầy đủ: với một crash, làm trọn quy trình mục 6 — tái hiện, ASan, tối giản (
-minimize_crash/afl-tmin), root-cause, vá, thêm regression test. - AFL++: build một tool CLI parse pcap, fuzz bằng AFL++ với
@@, so sánh trải nghiệm với libFuzzer. - Audit thật: mở
wireshark/epan/dissectors/packet-diameter.c(hoặc freeDiameter), tìm biên parse, đối chiếu checklist 7.1; viết 1 trang nhận xét: chỗ nào bounds-check tốt, pattern an toàn nào học được. - CI nhỏ: dựng pipeline tối thiểu (kể cả script
make cilocal) chạy: build-Werror→ test ASan/UBSan → fuzz 60 giây → kiểm regression. Đảm bảo nó fail khi bạn cố tình gài lỗi. - Capstone #4 — báo cáo CVD: lấy một thư viện C parse protocol OSS, build ASan+UBSan, fuzz, và (nếu tìm thấy lỗi hoặc dựng kịch bản giả định trong lab) viết một báo cáo dạng CVD hoàn chỉnh: mô tả + CWE, PoC tối giản, điểm CVSS v3.1 kèm vector chuỗi, mục spec liên quan, đề xuất vá. Nếu là lỗi thật → đi đúng quy trình disclosure mục 7.4.
Cột mốc module 10 (và HOÀN THÀNH lộ trình): Bạn tự động tìm lỗ hổng bằng libFuzzer/AFL++ + sanitizer, triage crash tới root-cause, viết secure code theo checklist tổng, đọc & audit stack telecom thật (Wireshark, freeDiameter, Kamailio, Osmocom, Open5GS), đưa an toàn vào CI, và báo cáo có trách nhiệm với CVSS/CVD đúng chuẩn GSMA.
🎓 Kết thúc lộ trình — bạn đang ở đâu
Phase 0 ✅ Định hướng & môi trường
Phase 1 ✅ C: cú pháp → con trỏ/bộ nhớ → struct/endianness → nâng cao + toolchain
Phase 2 ✅ C++: OOP/RAII/STL → template/smart pointer/move/span
Phase 3 ✅ Hệ thống & mạng: socket/SCTP/epoll/thread (đưa code lên mạng thật)
Phase 4 ✅ Phân tích protocol: Diameter/GTP/SIP/ASN.1 + craft + chống hostile input
Phase 5 ✅ An ninh bộ nhớ & khai thác → fuzzing/secure coding/CVD
Bạn đã đi từ int main() tới viết và audit parser protocol chống tấn công, fuzz tìm 0-day, và báo cáo theo chuẩn GSMA — đúng năng lực của một chuyên gia an ninh viễn thông đẳng cấp. Ba việc nên làm tiếp để biến kiến thức thành đẳng cấp thực chiến:
- Hoàn thành ≥1 capstone (xem
00mục 5) — bằng chứng năng lực cầm được trên tay; capstone #3 (SCTP/Diameter peer gửi CER) còn giúp bạn đóng lại chính case S6a đang xử lý ở mức code. - Đọc đều một codebase thật — chọn freeDiameter (gần case của bạn) hoặc Wireshark dissectors; đọc 30 phút/ngày, vá thử một issue nhỏ.
- Fuzz liên tục một mục tiêu — duy trì một harness + corpus, để nó chạy nền; đây là cách 0-day thật được tìm ra.
Khép vòng với công việc hiện tại của bạn: sự cố S6a “SCTP xong nhưng không CER” mà bạn đang gỡ giờ có thể nhìn ở cả ba tầng — transport (
07: SCTP handshake vs vai trò Diameter initiator), byte/định dạng (08: CER mang App-Id 0, AVP đúng cấu trúc), và an ninh (09/10: parser hai phía có chống gói méo không). Đó chính là góc nhìn nhiều tầng mà giáo trình này được thiết kế để trao cho bạn.
➡️ Quay lại 00-README-LO-TRINH.md bất cứ lúc nào để xem bản đồ tổng thể và danh sách capstone. Chúc bạn đi xa. 🛰️