Blog09 — An Ninh Bộ Nhớ & Khai Thác Cơ Bản
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
09 — An Ninh Bộ Nhớ & Khai Thác Cơ Bản
Mục tiêu: Hiểu tại sao và bằng cách nào các lỗ hổng bộ nhớ trong C/C++ phát sinh từ chính những parser bạn vừa viết ở
08, và kẻ tấn công biến chúng thành chiếm quyền điều khiển ra sao. Mục đích là hiểu để vá và phòng thủ — bạn không thể bảo vệ stack viễn thông nếu không hiểu lỗ hổng vận hành thế nào ở mức bộ nhớ.
⚠️ Phạm vi & đạo đức: Toàn bộ module này dành cho môi trường được cấp phép: lab của bạn, CTF, nghiên cứu phòng thủ, pentest có hợp đồng. Đây là kiến thức nền của red team và blue team — bạn học tư duy tấn công để xây phòng thủ tốt hơn và để báo cáo lỗ hổng có trách nhiệm (CVD, xem
10). Không khai thác hệ thống không thuộc quyền bạn.
1. Vì sao C/C++ “không an toàn bộ nhớ”
C/C++ cho bạn kiểm soát bộ nhớ trực tiếp — sức mạnh để viết stack telecom hiệu năng cao — nhưng không có rào chắn tự động: không kiểm tra biên mảng, không quản lý vòng đời tự động, con trỏ thô tự do. Lập trình viên là hệ thống an toàn. Khi một lá chắn ở checklist 08 thiếu, hậu quả không chỉ là “kết quả sai” mà là kẻ tấn công ghi/đọc bộ nhớ ngoài ý muốn — bước đầu của chiếm quyền.
Bộ nhớ một tiến trình (đơn giản hóa)
địa chỉ thấp ──────────────────────────────────► địa chỉ cao
┌──────┬──────┬───────────┬───────────────┬───────────┐
│ .text│.data │ HEAP │ → ... ← │ STACK │
│ (mã) │(biến)│ (malloc) │ (vùng trống) │(hàm,biến) │
└──────┴──────┴───────────┴───────────────┴───────────┘
▲ ▲
over-read/write return address ở đây
làm hỏng metadata → mục tiêu kinh điển
Bảng các lớp lỗ hổng (đối chiếu CWE đã nêu ở 02):
| Lớp lỗ hổng | CWE | Gốc rễ trong parser | Hậu quả |
|---|---|---|---|
| Stack buffer overflow | CWE-787/121 | memcpy length không kiểm vào buffer stack | Ghi đè return address → CFH |
| Heap buffer overflow | CWE-122 | ghi quá vùng malloc | Hỏng heap metadata → CFH |
| Use-after-free (UAF) | CWE-416 | dùng con trỏ sau free | Đọc/ghi vùng bị tái cấp phát |
| Double-free | CWE-415 | free hai lần | Hỏng allocator → ghi tùy ý |
| Integer overflow → overflow | CWE-190 | pos+len tràn, len*n tràn | Bypass bounds → over-read/write |
| Off-by-one | CWE-193 | <= thay vì <, quên byte NUL | Ghi 1 byte ngoài biên |
| Out-of-bounds read | CWE-125 | đọc trước khi validate length | Rò rỉ bộ nhớ (info leak) |
| Format string | CWE-134 | printf(user_input) | Đọc/ghi qua %x/%n |
| Uninitialized memory | CWE-457 | dùng biến/buffer chưa gán | Rò rỉ dữ liệu, hành vi bất định |
2. Stack buffer overflow — lỗ hổng kinh điển nhất
Khi một hàm chạy, stack frame chứa biến cục bộ và địa chỉ trở về (return address). Ghi quá một buffer cục bộ sẽ đè lên return address.
Stack frame của hàm parse (địa chỉ cao ở trên):
┌────────────────────────┐ ← cao
│ return address │ ← nếu đè được cái này = điều khiển luồng thực thi
├────────────────────────┤
│ saved frame pointer │
├────────────────────────┤
│ char buf[64] │ ← memcpy quá 64 byte tràn LÊN TRÊN
└────────────────────────┘ ← thấp
ghi tràn ───────────────────►
Parser tồi điển hình (mô phỏng một lỗi thật trong xử lý IE/AVP):
// LỖI: tin tưởng len từ gói, buffer cố định trên stack
void handle_avp_value(const uint8_t *data, size_t len) {
char value[64];
memcpy(value, data, len); // len do PEER khai → có thể 5000 → tràn stack!
// ... xử lý value ...
}
Kẻ tấn công gửi AVP có Value dài 200 byte: 64 byte đầu vào value, phần còn lại đè saved frame pointer + return address. Nếu chương trình không có mitigation, khi hàm return, CPU nhảy tới địa chỉ kẻ tấn công chọn.
Cách vá (đúng tinh thần 08):
void handle_avp_value(const uint8_t *data, size_t len) {
char value[64];
if (len > sizeof(value)) return; // ◄── validate TRƯỚC khi copy (Quy tắc thép #5)
memcpy(value, data, len);
}
Quy tắc thép #10: Trước mọi
memcpy/memmove/strcpy/sprintfvào buffer kích thước cố định, phải có một dòng kiểm traif (len > sizeof(dst)) <từ chối>.strcpy/strcat/sprintf/getskhông kiểm độ dài — coi như cấm dùng trong code xử lý mạng; thay bằngmemcpycó kiểm,snprintf, hoặcstd::string/std::vector.
3. Integer overflow → buffer overflow (lớp lỗi tinh vi nhất của parser)
Đây là cầu nối giữa 08 và 09, và là dạng lỗi qua mặt được code trông có vẻ an toàn. Quay lại checklist mục 2 của 08:
// LỖI: kiểm tra biên SAI vì pos + len có thể tràn
if (pos + len > buf_size) return -1; // nếu pos+len tràn size_t → kết quả NHỎ → "qua"
memcpy(dst, buf + pos, len); // over-read khổng lồ
Nếu pos = 0xFFFFFFF0 và len = 0x20 trên hệ 32-bit (hoặc các giá trị tương ứng làm tràn size_t), pos + len wrap về một số nhỏ, vượt qua kiểm tra, rồi memcpy đọc/ghi ngoài biên. Cách đúng luôn là phép trừ một phía (cả hai vế size_t, đã đảm bảo pos <= buf_size):
if (len > buf_size - pos) return -1; // an toàn: không có phép cộng để tràn
Một biến thể: nhân tràn khi cấp phát theo số phần tử do peer khai:
// LỖI: count * sizeof(elem) có thể tràn → cấp phát nhỏ → ghi tràn heap
T *arr = malloc(count * sizeof(T)); // count = 0x20000000 → tràn → cấp phát bé tí
for (size_t i = 0; i < count; i++) arr[i] = parse_one(); // heap overflow
Phòng: kiểm tra count so với trần hợp lý trước, hoặc dùng cấp phát có kiểm tràn (calloc kiểm nmemb*size, hoặc __builtin_mul_overflow). Trong C++, ưu tiên std::vector + reserve với trần rõ ràng.
Lý do dạng lỗi này hay lọt: code trông như đã bounds-check. Khi audit, mọi biểu thức
a + b,a * btrên giá trị gốc từ gói tin dùng để cấp phát hoặc so biên đều là điểm cần soi.
4. Heap, Use-After-Free và Double-Free
Heap chứa các khối malloc xen với metadata allocator (kích thước, con trỏ free-list). Hỏng metadata → kẻ tấn công lái được các malloc/free sau đó để ghi vào địa chỉ tùy ý.
Use-After-Free (CWE-416) — lớp lỗ hổng số 1 của phần mềm C/C++ hiện đại (trình duyệt, kernel, protocol stack):
struct session *s = lookup(id);
free_session(s); // s được free
// ... đường code khác ...
s->state = ACTIVE; // UAF: ghi vào vùng đã free (có thể đã bị cấp lại)
Nếu giữa free và lần dùng lại, kẻ tấn công khiến chương trình malloc một đối tượng khác trùng vùng (heap grooming), thì việc ghi s->state sửa đối tượng mới — vd ghi đè một con trỏ hàm/vtable trong đó.
Double-free (CWE-415): free(p); …; free(p); làm hỏng free-list; với một số allocator dẫn tới khả năng “ghi gần như tùy ý” (tcache/fastbin poisoning — khái niệm).
UAF khai thác (ý tưởng):
free(obj) → vùng vào free-list
malloc(same size) → kẻ tấn công chiếm lại vùng đó, đặt dữ liệu mình
dùng con trỏ cũ → thao tác trên dữ liệu kẻ tấn công (vd vtable giả)
Phòng — và đây là lý do 06 nhấn mạnh smart pointer:
- C++:
unique_ptr/shared_ptrloại bỏ phần lớn UAF/double-free ở mức thiết kế; saufree/reset con trỏ tự vô hiệu. Đừng.get()rồi giữ con trỏ thô qua thời điểm object hủy. - C: sau
free(p), đặtp = NULLngay (free NULL là vô hại; UAF/double-free trên NULL crash sạch thay vì khai thác được). Thiết kế “ai sở hữu, ai giải phóng” rõ ràng.
5. Off-by-one, OOB-read (info leak) và Format string
Off-by-one (CWE-193): kinh điển với chuỗi C cần byte NUL kết thúc:
char buf[64];
// LỖI: chép 64 ký tự rồi thêm '\0' → ghi buf[64], tràn 1 byte
strncpy(buf, src, 64); buf[64] = '\0'; // buf[64] NGOÀI biên (chỉ 0..63 hợp lệ)
Một byte tràn (đặc biệt là byte NUL đè LSB của con trỏ/length kế bên) đủ thành lỗ hổng khai thác được.
OOB-read / info leak (CWE-125): đọc quá biên thường rò rỉ bộ nhớ ra ngoài (như Heartbleed) — lộ khóa, con trỏ (đánh bại ASLR), dữ liệu thuê bao. Trong telecom, rò rỉ bộ nhớ HSS/MME có thể lộ thông tin nhạy cảm. Phòng: validate length trước mọi lần đọc (Quy tắc thép #4, #5).
Format string (CWE-134):
printf(user_controlled); // LỖI: nếu chuỗi chứa %x%x%x → đọc stack; %n → GHI bộ nhớ
printf("%s", user_controlled); // ĐÚNG: dữ liệu là tham số, không phải format
%n ghi số ký tự đã in vào một địa chỉ trên stack → ghi tùy ý. Luôn dùng format hằng; dữ liệu động đi qua %s/%d. Trong logging stack telecom, log thẳng một field do peer khai (log(avp_string)) là lỗi format-string điển hình.
6. Từ “ghi ngoài biên” tới “chiếm quyền điều khiển” (control-flow hijack)
Vì sao ghi sai bộ nhớ lại thành thực thi mã của kẻ tấn công? Vì một số ô nhớ chứa địa chỉ CPU sẽ nhảy tới:
| Mục tiêu ghi đè | Khi nào dùng | Kết quả |
|---|---|---|
| Return address (stack) | stack overflow | hàm return nhảy tới địa chỉ kẻ tấn công |
| Con trỏ hàm / vtable (heap) | heap overflow, UAF | lần gọi gián tiếp tiếp theo nhảy sai |
| GOT entry (.got.plt) | ghi tùy ý | gọi hàm thư viện thực ra nhảy chỗ khác |
Chuỗi khai thác cổ điển (mức khái niệm, để hiểu phòng thủ):
1. Tràn buffer → ghi đè return address
2. ASLR? NX? → quyết định kỹ thuật:
- NX bật (stack không thực thi) → KHÔNG chạy shellcode trên stack
- → dùng ROP: xâu chuỗi các "gadget" (đoạn mã sẵn có kết thúc bằng `ret`)
để gọi vd mprotect/execve mà không cần inject mã mới
- ret2libc: nhảy thẳng tới hàm libc (system("/bin/sh"))
3. Heap: tcache/fastbin poisoning → lái malloc trả về địa chỉ ta chọn
→ ghi đè con trỏ hàm → CFH
Bạn không cần viết được ROP chain phức tạp ngay; cần hiểu chuỗi nhân-quả: input thù địch → lỗi bộ nhớ → ghi đè con trỏ điều khiển → chuyển hướng thực thi. Hiểu điều này khiến bạn thấy mức nghiêm trọng thật của một “bug parser nhỏ” khi audit/triage (mục then chốt cho báo cáo CVSS ở 10).
7. Mitigation — lớp phòng thủ của hệ điều hành/compiler
Phần mềm hiện đại không “trần trụi” — có nhiều lớp chống khai thác. Hiểu chúng để (a) biết một lỗ hổng thực sự khai thác được tới đâu, (b) bật đủ cờ khi build stack của bạn.
| Cơ chế | Chống gì | Bật thế nào |
|---|---|---|
Stack canary (-fstack-protector-strong) | stack overflow ghi đè return addr | giá trị canh gác đặt trước return addr, kiểm trước ret |
| NX / DEP | chạy shellcode trên stack/heap | mặc định; đánh dấu vùng dữ liệu không thực thi |
ASLR (-fPIE -pie) | đoán địa chỉ (ROP/ret2libc) | ngẫu nhiên hóa base của stack/heap/lib/exe |
RELRO (-Wl,-z,relro,-z,now) | ghi đè GOT | GOT thành read-only sau khi nạp |
FORTIFY_SOURCE (-D_FORTIFY_SOURCE=2 -O2) | overflow trong memcpy/strcpy… | compiler chèn kiểm tra độ dài khi biết kích thước |
CFI (Clang -fsanitize=cfi) | gọi gián tiếp sai (vtable/con trỏ hàm) | kiểm tra đích lời gọi gián tiếp hợp lệ |
Cờ build “cứng” nên dùng cho mọi network function trong lab/production:
gcc -O2 -Wall -Wextra \
-fstack-protector-strong -D_FORTIFY_SOURCE=2 \
-fPIE -pie -Wl,-z,relro,-z,now \
parser.c -o parser
Quan trọng: Mitigation làm khó khai thác, không loại bỏ lỗ hổng. Một lỗ hổng + đủ điều kiện vẫn vượt được mọi lớp. Đừng coi mitigation là lý do bỏ qua bug. Cách triệt để vẫn là không tạo lỗi bộ nhớ ngay từ đầu (checklist
08+ smart pointer06+ sanitizer10).
8. Lab an toàn — quan sát lỗ hổng tận mắt
Cách học chắc nhất là thấy chương trình hỏng dưới điều kiện kiểm soát. Vô hiệu hóa mitigation chỉ trong lab để quan sát cơ chế:
# Build CỐ TÌNH yếu để quan sát (CHỈ trong VM lab, không bao giờ thật):
gcc -g -O0 -fno-stack-protector -z execstack vuln.c -o vuln
# Tắt ASLR toàn máy (lab): echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
Quy trình quan sát:
- Viết chương trình
vuln.cđọc một “AVP” từstdin/file vàmemcpyvàochar buf[64]không kiểm (mục 2). - Cấp input 200 byte. Chạy dưới
gdb: đặt breakpoint, xem stack frame, quan sát return address bị đè (so byte input với giá trị$rip/$pckhi crash). - Build lại phiên bản đã vá (
if (len > sizeof buf) return;) và xác nhận cùng input giờ bị từ chối êm. - Build lại nữa với ASan (
-fsanitize=address) bản chưa vá → ASan in chính xác “stack-buffer-overflow”, dòng code, độ lệch. So sánh trải nghiệm gdb thủ công vs ASan tự động.
Đây là cách red và blue gặp nhau: bạn gây lỗi để hiểu nó, rồi vá và kiểm chứng bằng công cụ. Toàn bộ làm trong VM cách ly, trên mã của chính bạn. Picoctf / pwn.college / các CTF “pwn” là sân tập hợp pháp lý tưởng để luyện sâu hơn.
9. Bài tập / Lab module 09
- Tái hiện stack overflow: viết
vuln.c(mục 2), cấp input dài, chạy gdb, định vị thời điểm return address bị đè bằngbt/info registers. Ghi lại offset từ đầu buffer tới return address. - Vá & kiểm chứng: thêm
if (len > sizeof buf) return;. Xác nhận input cũ bị từ chối. Build cả hai bản dưới ASan và so sánh báo cáo. - Integer-overflow bypass: viết hàm có kiểm biên sai kiểu
pos + len > size, dựng giá trị làm trànsize_tđể qua mặt; rồi sửa thànhlen > size - posvà xác nhận đã chặn. (Liên hệ thẳng08mục 2.) - Nhân tràn khi cấp phát: viết code
malloc(count * sizeof(T))vớicounttừ input; chocountlớn để tràn; chạy ASan thấy heap-overflow; vá bằng kiểm trần +__builtin_mul_overflow. - UAF tối giản: dựng struct session,
freerồi dùng lại; chạy ASan thấy “heap-use-after-free”. Vá bằng (a) C: gánNULLsau free + kiểm NULL; (b) C++: viết lại bằngunique_ptrvà chứng minh lỗi không biên dịch được hoặc bị loại ở thiết kế. - Format string: viết
log(const char*)gọiprintf(s)trực tiếp; cấp"%x %x %x %n"quan sát (chỉ in/đọc trong lab). Vá thànhprintf("%s", s). Rút ra quy tắc cho logging stack telecom. - Off-by-one NUL: tái hiện
buf[64]='\0'trênchar buf[64]; chạy ASan thấy global/stack-buffer-overflow 1 byte; vá. - Hardening flags: lấy parser từ
08, build với bộ cờ “cứng” (mục 7), rồi không cờ; so sánh (vd thử overflow nhỏ và xem stack canary bắt). Viết Makefile targetharden. - Đọc một CVE thật: chọn một CVE parser telecom/Wireshark (vd một CVE dissector), đọc mô tả + commit vá, phân loại nó vào bảng CWE mục 1, và chỉ ra lá chắn nào ở checklist
08đã thiếu.
Cột mốc module 09: Bạn nhận diện được tất cả các lớp lỗ hổng bộ nhớ từ mã nguồn, hiểu chuỗi input thù địch → lỗi bộ nhớ → control-flow hijack, biết các mitigation và giới hạn của chúng, và quan trọng nhất — bạn thấy mức nghiêm trọng thật của một bug parser để vá đúng và xếp hạng đúng. Bạn tư duy được cả hai mũ: tấn công để hiểu, phòng thủ để bảo vệ.
➡️ Tiếp theo: 10-Fuzzing-Va-Secure-Coding.md — tự động tìm các lỗ hổng này bằng fuzzing + sanitizer, viết secure code, đọc codebase thật, và báo cáo có trách nhiệm (CVD/CVSS). Chương cuối khép lại lộ trình.
Lộ trình học tập
Lập Trình C/C++
- 1 01 — C Cơ Bản
- 2 02 — Con Trỏ & Bộ Nhớ
- 3 03 — Struct, Union, Bitfield & Bố Cục Bộ Nhớ
- 4 04 — C Nâng Cao
- 5 05 — C++ Cốt Lõi
- 6 06 — C++ Hiện Đại
- 7 07 — Lập Trình Hệ Thống & Mạng
- 8 08 — Phân Tích Giao Thức Telecom Bằng C/C++
- 9 09 — An Ninh Bộ Nhớ & Khai Thác Cơ Bản
- 10 11 — Lập Trình Nhúng & Real-time (SDR-first)
- 11 Bài tập & Pet Project — Lộ trình C/C++ chuyên sâu
- 12 Tổng hợp các dạng printf để in giá trị trong C
- 13 Bitwise & Mask, Octet, Endian: Hướng dẫn parse protocol byte cho người mới
- 14 Tổng hợp: Con trỏ, kích thước kiểu dữ liệu và byte trong bộ nhớ