Skip to content
LIVE FEED

Blog02 — Con Trỏ & Bộ Nhớ

02 — Con Trỏ & Bộ Nhớ

Mục tiêu: Đây là module quan trọng nhất của cả lộ trình. Con trỏ và mô hình bộ nhớ là bản chất của C, và cũng là bản chất của mọi lỗ hổng memory trong telecom stack. Nắm chắc chương này = bạn đã có 50% tư duy của một chuyên gia khai thác/audit.


1. Bộ nhớ là gì — mô hình tinh thần đúng

Hãy hình dung RAM là một dãy ô liên tiếp, mỗi ô 1 byte, mỗi ô có một địa chỉ (số nguyên):

Địa chỉ:  0x7000  0x7001  0x7002  0x7003  0x7004 ...
          ┌──────┬──────┬──────┬──────┬──────┐
Giá trị:  │ 0x45 │ 0x00 │ 0x00 │ 0x3c │ 0x1a │ ...
          └──────┴──────┴──────┴──────┴──────┘
  • Biến = một cái tên gợi nhớ cho một (hoặc vài) ô nhớ.
  • Con trỏ (pointer) = một biến mà giá trị của nó là một địa chỉ.

Đó là tất cả. Mọi thứ phức tạp về sau chỉ là hệ quả của ý tưởng đơn giản này.


2. Con trỏ cơ bản: & và *

int x = 65;
int *p = &x;     // p giữ ĐỊA CHỈ của x.  '&' = "địa chỉ của"
                 // int * = "con trỏ tới int"

printf("%d\n", x);    // 65   — giá trị của x
printf("%p\n", (void*)p);   // 0x7ffe...  — địa chỉ (giá trị của p)
printf("%d\n", *p);   // 65   — '*p' = "giá trị TẠI địa chỉ p" (dereference)

*p = 66;         // ghi 66 vào ô nhớ mà p trỏ tới → x trở thành 66
printf("%d\n", x);    // 66
Ký hiệuĐọc làÝ nghĩa
&x“địa chỉ của x”Lấy địa chỉ của biến x
*p“giá trị tại p” / “dereference p”Truy cập ô nhớ mà p trỏ tới
int *p“p là con trỏ tới int”Khai báo con trỏ

Mẹo đọc khai báo: đọc từ tên ra ngoài. int *p → *p là int → p là con trỏ tới int.


3. Tại sao con trỏ là TẤT CẢ trong C

3.1. Sửa biến gốc từ trong hàm (output parameter)

void set_teid(uint32_t *out) {   // nhận địa chỉ
    *out = 0xDEADBEEF;           // ghi vào ô gốc
}

uint32_t teid = 0;
set_teid(&teid);                 // truyền địa chỉ của teid
// teid bây giờ = 0xDEADBEEF

Đây là lý do parser C thường có chữ ký int parse(const uint8_t *in, size_t len, struct result *out).

3.2. Tránh sao chép dữ liệu lớn

Truyền một buffer 1500 byte theo giá trị = copy 1500 byte. Truyền con trỏ = copy 8 byte (địa chỉ). Toàn bộ packet processing dựa vào điều này.

3.3. Cấu trúc dữ liệu động (linked list, cây AVP lồng nhau) — bất khả thi nếu không có con trỏ.


4. Con trỏ void*, NULL, và const

void *raw;            // con trỏ "vô kiểu" — trỏ tới bất cứ gì, KHÔNG dereference trực tiếp được
int *p = NULL;        // NULL = "không trỏ tới đâu cả" (địa chỉ 0)

if (p != NULL) *p = 5;   // LUÔN kiểm tra NULL trước khi dereference

const uint8_t *r;     // CON TRỎ tới dữ liệu KHÔNG ĐƯỢC SỬA (đọc-only buffer)
uint8_t *const c = &b;// con trỏ CỐ ĐỊNH (không đổi địa chỉ) tới dữ liệu sửa được
Khai báoSửa được dữ liệu?Đổi được địa chỉ con trỏ?
uint8_t *p✅✅
const uint8_t *p❌✅
uint8_t *const p✅❌
const uint8_t *const p❌❌

Quy ước: parser nhận const uint8_t *buf để cam kết “tôi không sửa gói tin đầu vào”.

Bẫy NULL deref: dereference NULL → crash (SIGSEGV). Trong code mạng, nếu malloc thất bại trả NULL mà bạn không kiểm tra → DoS.


5. STACK vs HEAP — phải phân biệt rạch ròi

Đây là kiến thức nền cho toàn bộ phần khai thác bộ nhớ sau này.

Bố cục bộ nhớ một tiến trình (đơn giản hóa):

   Địa chỉ cao
   ┌─────────────────────┐
   │       STACK         │  ← biến cục bộ, tham số hàm, địa chỉ trả về
   │         ↓ lớn xuống │     (mọi lần gọi hàm tạo 1 "stack frame")
   │                     │
   │      (khoảng trống) │
   │                     │
   │         ↑ lớn lên   │
   │        HEAP         │  ← vùng malloc/free (động)
   ├─────────────────────┤
   │   BSS / DATA        │  ← biến toàn cục & static
   ├─────────────────────┤
   │       TEXT          │  ← mã máy (chỉ đọc)
   └─────────────────────┘
   Địa chỉ thấp
Đặc điểmSTACKHEAP
Cấp phátTự động (vào/ra hàm)Thủ công (malloc/free)
Tốc độRất nhanhChậm hơn
Kích thướcNhỏ (vài MB), cố địnhLớn
LifetimeTheo scope hàmĐến khi free
Lỗi điển hìnhStack buffer overflow → ghi đè địa chỉ trả vềHeap overflow, use-after-free, double-free

Liên hệ khai thác (đào sâu ở 09):

  • Tràn buffer trên stack có thể ghi đè return address → chiếm luồng điều khiển (ret2libc, ROP).
  • Tràn/UAF trên heap thao túng metadata của allocator (tcache, fastbin) → ghi tùy ý. Cả hai đều bắt đầu từ việc bạn hiểu hai vùng này khác nhau thế nào.

6. Cấp phát động trên heap: malloc, calloc, realloc, free

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

// Cấp phát buffer để chứa 1 AVP có độ dài lấy từ gói
uint8_t *buf = malloc(len);          // cấp len byte (nội dung RÁC, chưa khởi tạo)
if (buf == NULL) return -1;          // LUÔN kiểm tra — malloc có thể thất bại

memset(buf, 0, len);                 // hoặc dùng calloc để vừa cấp vừa zero
// ... dùng buf ...
free(buf);                           // trả lại bộ nhớ
buf = NULL;                          // tốt: tránh dùng nhầm con trỏ đã free
HàmCông dụng
malloc(n)Cấp n byte, không khởi tạo (rác)
calloc(k, n)Cấp k×n byte, zero hóa; còn kiểm tra tràn nhân
realloc(p, n)Đổi kích thước vùng đã cấp (có thể di chuyển!)
free(p)Giải phóng. free(NULL) an toàn (không làm gì)

6.1. Quy tắc vàng của quản lý bộ nhớ thủ công

  1. Mỗi malloc có đúng một free. Không free → memory leak (DoS dài hạn cho probe chạy lâu). Free hai lần → double-free (lỗ hổng).
  2. Sau free, đặt con trỏ = NULL. Tránh use-after-free.
  3. Không bao giờ dùng con trỏ sau khi free. Đó là use-after-free (UAF) — lớp lỗ hổng nguy hiểm nhất hiện nay.
  4. realloc có thể đổi địa chỉ. Luôn gán lại: p = realloc(p, n); (nhưng dùng biến tạm để không mất con trỏ cũ nếu thất bại).

6.2. Năm tội lỗi bộ nhớ kinh điển (CWE) — bạn sẽ săn chúng cả đời

// (A) Memory leak — quên free
uint8_t *p = malloc(100);
return;                     // p mất, 100 byte rò rỉ vĩnh viễn   [CWE-401]

// (B) Use-after-free
free(p);
p[0] = 1;                   // dùng sau khi free                  [CWE-416]

// (C) Double-free
free(p);
free(p);                    // free hai lần                       [CWE-415]

// (D) Heap buffer overflow
uint8_t *p = malloc(8);
memcpy(p, src, 16);         // ghi 16 byte vào vùng 8 byte         [CWE-122]

// (E) Đọc bộ nhớ chưa khởi tạo
uint8_t *p = malloc(8);
printf("%d", p[0]);         // p[0] là rác — rò rỉ thông tin?      [CWE-457]

Cách phát hiện: chạy chương trình với valgrind ./prog hoặc biên dịch -fsanitize=address. ASan/Valgrind sẽ chỉ chính xác dòng và loại lỗi. Đây là kỹ năng audit bắt buộc — ta luyện ở 10.


7. Mảng và quan hệ mật thiết với con trỏ

uint8_t packet[16] = {0x45, 0x00, 0x00, 0x3c};

packet[0];      // phần tử đầu = 0x45
*packet;        // GIỐNG packet[0] — tên mảng "phân rã" thành con trỏ tới phần tử đầu
*(packet + 3);  // GIỐNG packet[3] — số học con trỏ
&packet[3];     // địa chỉ phần tử thứ 3

7.1. Số học con trỏ (pointer arithmetic) — đơn vị là phần tử, không phải byte!

uint32_t arr[4];
uint32_t *p = arr;
p + 1;          // tiến 4 BYTE (vì sizeof(uint32_t)=4), trỏ tới arr[1]

uint8_t *b = (uint8_t*)arr;
b + 1;          // tiến 1 BYTE

Khi parse packet, ta thường dùng uint8_t* để cộng theo byte chính xác:

const uint8_t *cur = buf;
uint8_t  type   = cur[0];
uint16_t length = (cur[2] << 8) | cur[3];   // big-endian
cur += length;                              // nhảy tới field kế tiếp

7.2. CẠM BẪY LỚN NHẤT: C không kiểm tra biên mảng

uint8_t a[4];
a[10] = 0xFF;        // KHÔNG LỖI BIÊN DỊCH. Ghi ra ngoài mảng → UB, corruption.
a[-1] = 0xFF;        // cũng "hợp lệ" về cú pháp — ghi đè ô nhớ trước mảng

Đây chính là buffer overflow. C tin tưởng tuyệt đối vào lập trình viên. Trong viễn thông, nếu chỉ số đến từ length field trong gói của kẻ tấn công → thảm họa.

Quy tắc thép #5: Mọi truy cập mảng/buffer bằng chỉ số có nguồn gốc từ dữ liệu mạng phải được kiểm tra idx < len trước. Không có ngoại lệ.


8. Chuỗi C — chỉ là mảng char kết thúc bằng \0

char s[] = "INVITE";   // thực chất: {'I','N','V','I','T','E','\0'}  → 7 byte

Chuỗi C không lưu độ dài; nó dựa vào byte \0 (NUL terminator) để biết điểm kết thúc. Đây là nguồn gốc vô số lỗ hổng.

8.1. Hàm chuỗi NGUY HIỂM vs AN TOÀN

#include <string.h>

char dst[8];
strcpy(dst, src);            // NGUY HIỂM — không giới hạn, tràn nếu src dài  [tránh]
strncpy(dst, src, sizeof(dst));  // giới hạn, NHƯNG có thể không kết thúc \0  [cẩn thận]
snprintf(dst, sizeof(dst), "%s", src);  // AN TOÀN — luôn kết thúc \0         [ưu tiên]

size_t n = strlen(s);        // đếm tới \0 — nếu thiếu \0 → đọc tràn (over-read)
Hàm nguy hiểmVấn đềThay bằng
gets()Không giới hạn (đã bị xóa khỏi chuẩn)fgets
strcpyKhông giới hạnsnprintf / strlcpy
strcatKhông giới hạnsnprintf
sprintfKhông giới hạnsnprintf
scanf("%s")Không giới hạnfgets

Bài học SIP: SIP là protocol text (chuỗi). Vô số CVE trong Kamailio/Asterisk đến từ xử lý header SIP bằng hàm chuỗi không giới hạn. Khi parse SIP bằng C, dữ liệu nhị phân (binary) và chuỗi NUL-terminated là hai thế giới khác nhau — gói mạng có thể chứa byte \0 ở giữa, làm strlen/strcpy hiểu sai. Với protocol nhị phân, luôn dùng cặp (ptr, len), không dùng hàm chuỗi.


9. Con trỏ tới con trỏ (**) — dùng khi nào?

// Hàm tự cấp phát buffer và "trả" con trỏ ra ngoài
int alloc_buf(uint8_t **out, size_t n) {
    *out = malloc(n);
    return (*out != NULL) ? 0 : -1;
}

uint8_t *buf = NULL;
alloc_buf(&buf, 128);    // truyền địa chỉ của con trỏ → hàm sửa được chính con trỏ
free(buf);

Dùng cho: hàm cấp phát giúp caller, mảng động của con trỏ (vd. mảng các AVP), char *argv[].


10. Soi bộ nhớ thật bằng gdb (kỹ năng cốt lõi của security)

gcc -g -O0 prog.c -o prog
gdb ./prog

Trong gdb:

(gdb) break main
(gdb) run
(gdb) next                      # chạy từng dòng
(gdb) print x                   # in giá trị biến
(gdb) print &x                  # in địa chỉ
(gdb) print *p                  # dereference
(gdb) x/16xb buf                # xem 16 byte (hex) tại buf  ← SOI PACKET TRONG RAM
(gdb) x/4xw arr                 # xem 4 word (4 byte) hex
(gdb) info locals               # các biến cục bộ
(gdb) bt                        # backtrace — chuỗi gọi hàm (xem được sau crash)

Thói quen: với mỗi struct/buffer bạn tạo, dùng x/Nxb để nhìn tận mắt layout byte. Khi bạn có thể đối chiếu byte trong gdb ↔ hex trong Wireshark ↔ định nghĩa trong RFC, bạn đã tư duy như một chuyên gia.


11. Bài tập / Lab module 02

  1. Swap bằng con trỏ: viết void swap(uint32_t *a, uint32_t *b) tráo giá trị hai biến. Chứng minh nó hoạt động (khác với swap pass-by-value không hoạt động).
  2. Dynamic buffer: viết hàm đọc toàn bộ một file vào buffer cấp phát động (dùng malloc/realloc), trả về (con trỏ, độ dài). Nhớ free. Chạy qua valgrind để xác nhận không leak.
  3. Tự cố tình tạo lỗi rồi bắt nó: viết 4 chương trình nhỏ tái hiện (B) UAF, (C) double-free, (D) heap overflow, (E) đọc chưa khởi tạo ở mục 6.2. Biên dịch -fsanitize=address và đọc kỹ báo cáo của ASan. Đây là bài tập quan trọng nhất module — học cách lỗi “trông như thế nào”.
  4. Pointer walker: cho một buffer mô phỏng chuỗi TLV (type[1] len[1] value[len] ...), viết vòng lặp dùng con trỏ cur duyệt và in từng record. Phải kiểm tra biên để không đọc tràn khi len xấu (vd. len lớn hơn phần còn lại của buffer). Thử cho một buffer độc hại (len = 0xFF nhưng chỉ còn 3 byte) và chứng minh code của bạn từ chối an toàn thay vì crash.
  5. gdb hexdump: lấy chương trình bài 4, đặt breakpoint, dùng x/16xb buf xem byte, đối chiếu với những gì in ra.

Cột mốc module 02: Bạn (a) hiểu stack vs heap, (b) quản lý malloc/free không leak/UAF, (c) duyệt buffer bằng con trỏ có kiểm tra biên, (d) nhận diện được 5 lỗi bộ nhớ kinh điển qua báo cáo ASan/Valgrind. Đây là năng lực phân biệt một coder C bình thường với một người sắp trở thành chuyên gia security.

➡️ Tiếp theo: 03-C-Struct-Union-Bitfield.md — mô tả header protocol bằng struct.

Lộ trình học tập

Lập Trình C/C++

  1. 1 01 — C Cơ Bản
  2. 2 02 — Con Trỏ & Bộ Nhớ
  3. 3 03 — Struct, Union, Bitfield & Bố Cục Bộ Nhớ
  4. 4 04 — C Nâng Cao
  5. 5 05 — C++ Cốt Lõi
  6. 6 06 — C++ Hiện Đại
  7. 7 07 — Lập Trình Hệ Thống & Mạng
  8. 8 08 — Phân Tích Giao Thức Telecom Bằng C/C++
  9. 9 09 — An Ninh Bộ Nhớ & Khai Thác Cơ Bản
  10. 10 11 — Lập Trình Nhúng & Real-time (SDR-first)
  11. 11 Bài tập & Pet Project — Lộ trình C/C++ chuyên sâu
  12. 12 Tổng hợp các dạng printf để in giá trị trong C
  13. 13 Bitwise & Mask, Octet, Endian: Hướng dẫn parse protocol byte cho người mới
  14. 14 Tổng hợp: Con trỏ, kích thước kiểu dữ liệu và byte trong bộ nhớ
Contact
Facebook
TOP