Blog04 — C Nâng Cao
Bố cục bài viết
Trang này
Ngày tạo
05/08/2026
Cập nhật
Lượt xem
10
Đọc
10m
04 — C Nâng Cao
Mục tiêu: Hoàn thiện phần C với các công cụ “trình độ cao”: con trỏ hàm (dispatch table cho mã lệnh protocol), preprocessor, các kỹ thuật bitwise nâng cao (checksum, bitmap), tổ chức dự án nhiều file, build system (Makefile/CMake), và quy trình debug bằng gdb/valgrind. Sau module này bạn đủ C để đọc telecom stack thật.
1. Con trỏ hàm (function pointer) — dispatch table cho protocol
Một con trỏ hàm lưu địa chỉ của một hàm. Cực mạnh để xây bảng phân nhánh theo mã lệnh (thay cho switch khổng lồ) — chính xác cách stack thật xử lý command code.
// Kiểu: con trỏ tới hàm nhận (const uint8_t*, size_t), trả int
typedef int (*handler_fn)(const uint8_t *buf, size_t len);
int handle_cer(const uint8_t *b, size_t n) { /* ... */ return 0; }
int handle_dwr(const uint8_t *b, size_t n) { /* ... */ return 0; }
int handle_dpr(const uint8_t *b, size_t n) { /* ... */ return 0; }
// Bảng dispatch theo Diameter Command Code
struct { uint32_t code; handler_fn fn; } dispatch[] = {
{ 257, handle_cer },
{ 280, handle_dwr },
{ 282, handle_dpr },
};
int dispatch_command(uint32_t code, const uint8_t *b, size_t n) {
for (size_t i = 0; i < sizeof(dispatch)/sizeof(dispatch[0]); i++)
if (dispatch[i].code == code)
return dispatch[i].fn(b, n); // GỌI qua con trỏ hàm
return -1; // unknown command
}
Cú pháp đọc con trỏ hàm (khó nhìn lúc đầu):
int (*fp)(const uint8_t*, size_t); // fp là con trỏ tới hàm(...)→int
fp = handle_cer; // gán (tên hàm tự decay thành địa chỉ)
fp(buf, len); // gọi (cũng viết được (*fp)(buf,len))
Liên hệ an ninh: Bảng con trỏ hàm (vtable, dispatch table) là mục tiêu khai thác hấp dẫn. Nếu kẻ tấn công ghi đè được một con trỏ hàm (qua heap overflow/UAF) → control-flow hijack. Hiểu cơ chế này giúp bạn vừa khai thác (lab) vừa phòng thủ (CFI, RELRO). Cũng là lý do callback trong Kamailio/FreeDiameter cần được bảo vệ.
Callback — truyền hành vi vào hàm
// Duyệt mọi AVP, gọi cb cho từng cái (mô hình "visitor")
void foreach_avp(const uint8_t *b, size_t n,
void (*cb)(uint32_t code, const uint8_t *val, uint32_t vlen)) {
/* ... với mỗi AVP: cb(code, val, vlen); ... */
}
2. Preprocessor — macro, điều kiện biên dịch
2.1. Macro hằng & macro hàm
#define DIAMETER_PORT 3868
#define MIN(a, b) ((a) < (b) ? (a) : (b)) // LUÔN đóng ngoặc từng tham số!
Bẫy macro:
#define SQ(x) x*xrồiSQ(a+1)→a+1*a+1(sai!). Phải#define SQ(x) ((x)*(x)). VàMIN(i++, j)đánh giái++hai lần → tác dụng phụ. Khi nghi ngờ, dùngstatic inlinefunction thay macro:
static inline int imin(int a, int b) { return a < b ? a : b; } // an toàn hơn macro
2.2. Include guard (bắt buộc cho file .h)
#ifndef DIAMETER_H
#define DIAMETER_H
/* nội dung header */
#endif
// hoặc gọn (gần như mọi compiler hỗ trợ):
#pragma once
2.3. Biên dịch điều kiện — bật/tắt code theo nền tảng hoặc debug
#ifdef DEBUG
#define DBG(...) fprintf(stderr, __VA_ARGS__)
#else
#define DBG(...) ((void)0) // không làm gì khi không debug
#endif
#if defined(__linux__)
#include <endian.h>
#elif defined(__APPLE__)
#include <libkern/OSByteOrder.h>
#endif
Bật macro khi biên dịch: gcc -DDEBUG file.c.
2.4. Macro hữu ích cho parser
#define ARRAY_LEN(a) (sizeof(a) / sizeof((a)[0]))
#define ALIGN4(n) (((n) + 3u) & ~3u) // làm tròn lên bội số 4 — Diameter AVP padding!
Diameter AVP được đệm tới bội số 4 byte.
ALIGN4(avp_len)cho bạn độ dài kể cả padding — bạn sẽ dùng nó liên tục ở08.
3. Bitwise nâng cao — checksum, bitmap, cờ
3.1. Tập cờ (flags) bằng bit
enum {
F_PROXYABLE = 1 << 0, // 0x01
F_ERROR = 1 << 1, // 0x02
F_RETRANS = 1 << 2, // 0x04
};
uint8_t flags = 0;
flags |= F_ERROR; // bật cờ
flags &= ~F_ERROR; // tắt cờ
if (flags & F_PROXYABLE) { ... } // kiểm tra cờ
flags ^= F_RETRANS; // đảo cờ
Diameter Command Flags (byte sau Command Code): R(equest), P(roxyable), E(rror), T. Bạn bóc/đặt chúng đúng bằng kỹ thuật này.
3.2. Internet checksum (16-bit one’s complement) — dùng trong IP/UDP/ICMP
uint16_t inet_checksum(const uint8_t *data, size_t len) {
uint32_t sum = 0;
for (size_t i = 0; i + 1 < len; i += 2)
sum += ((uint32_t)data[i] << 8) | data[i+1]; // cộng từng word 16-bit
if (len & 1) sum += (uint32_t)data[len-1] << 8; // byte lẻ cuối
while (sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16);// gập carry
return (uint16_t)~sum; // bù 1
}
Bạn cần checksum khi craft packet thô (raw socket) — nếu checksum sai, kernel/đích sẽ vứt gói. Đây là kỹ năng nền cho fuzzing tầng mạng.
3.3. Đếm bit, thao tác bit thông dụng
__builtin_popcount(x); // đếm số bit 1 (GCC/Clang)
__builtin_clz(x); // số bit 0 dẫn đầu
x & (x - 1); // xóa bit 1 thấp nhất (mẹo kinh điển)
x & -x; // tách bit 1 thấp nhất
4. Tổ chức dự án nhiều file
Dự án thật chia thành .h (khai báo) + .c (định nghĩa).
diameter.h:
#ifndef DIAMETER_H
#define DIAMETER_H
#include <stdint.h>
#include <stddef.h>
int parse_diameter_header(const uint8_t *buf, size_t len, uint32_t *out_cmd);
#endif
diameter.c:
#include "diameter.h" // ngoặc kép = header của dự án mình
int parse_diameter_header(const uint8_t *buf, size_t len, uint32_t *out_cmd) {
if (len < 20) return -1;
*out_cmd = ((uint32_t)buf[5] << 16) | ((uint32_t)buf[6] << 8) | buf[7]; // 24-bit
return 0;
}
main.c:
#include "diameter.h"
#include <stdio.h>
int main(void) { /* dùng parse_diameter_header */ return 0; }
Biên dịch nhiều file:
gcc -std=c17 -Wall -Wextra -c diameter.c -o diameter.o
gcc -std=c17 -Wall -Wextra -c main.c -o main.o
gcc diameter.o main.o -o app
static & extern (linkage) — kiểm soát phạm vi giữa các file
static int helper(void) { ... } // CHỈ thấy trong file này (internal linkage) — đóng gói
extern int g_counter; // khai báo "biến này định nghĩa ở file khác"
Dùng
staticcho hàm/biến nội bộ là thói quen tốt: thu nhỏ bề mặt tấn công và tránh xung đột tên.
5. Build system
5.1. Makefile (chuẩn mực, hầu hết telecom stack dùng)
CC := gcc
CFLAGS := -std=c17 -Wall -Wextra -Wpedantic -g -O0
LDFLAGS := -lsctp # link libsctp khi cần SCTP
SRCS := main.c diameter.c avp.c
OBJS := $(SRCS:.c=.o)
TARGET := app
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(OBJS) -o $@ $(LDFLAGS)
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
# build phiên bản có sanitizer để săn lỗi bộ nhớ
asan: CFLAGS += -fsanitize=address,undefined
asan: clean $(TARGET)
clean:
rm -f $(OBJS) $(TARGET)
.PHONY: all clean asan
Chạy: make, make asan, make clean.
5.2. CMake (dự án C++ hiện đại, cross-platform)
CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)
project(diameter_tool C CXX)
set(CMAKE_C_STANDARD 17)
set(CMAKE_CXX_STANDARD 20)
add_compile_options(-Wall -Wextra -g)
add_executable(app main.c diameter.c avp.c)
target_link_libraries(app PRIVATE sctp)
Build:
cmake -B build -DCMAKE_BUILD_TYPE=Debug
cmake --build build
./build/app
6. Quy trình debug — gdb & valgrind (kỹ năng audit cốt lõi)
6.1. gdb — mổ xẻ lúc chạy
gcc -g -O0 app.c -o app # -g: kèm symbol debug; -O0: không tối ưu
gdb ./app
(gdb) break parse_diameter_header # đặt breakpoint tại hàm
(gdb) run arg1 arg2
(gdb) next / step # qua dòng / vào hàm
(gdb) print *out_cmd
(gdb) x/20xb buf # xem 20 byte gói tin trong RAM
(gdb) info registers # xem thanh ghi (quan trọng khi debug crash/exploit)
(gdb) bt # backtrace khi crash → tìm hàm gây lỗi
(gdb) frame 2 # nhảy tới frame thứ 2 trong stack
(gdb) watch length # dừng khi 'length' thay đổi
(gdb) continue
Sau một crash (SIGSEGV), bt cho bạn biết chính xác chuỗi gọi dẫn tới lỗi — bước đầu của mọi root-cause analysis.
6.2. valgrind — săn lỗi bộ nhớ tự động
valgrind --leak-check=full --track-origins=yes ./app
Valgrind báo: đọc/ghi ngoài vùng cấp phát, dùng bộ nhớ chưa khởi tạo, memory leak (và nơi cấp phát), invalid free… — không cần sửa code. Đây là vũ khí #1 khi audit một thư viện parse protocol viết bằng C.
6.3. Sanitizers (nhanh hơn valgrind, dùng khi dev/fuzz)
gcc -g -O1 -fsanitize=address,undefined app.c -o app
./app # crash NGAY tại điểm lỗi, in stack trace + loại lỗi
| Công cụ | Bắt được | Tốc độ |
|---|---|---|
ASan (-fsanitize=address) | heap/stack overflow, UAF, double-free, leak | ~2× chậm |
UBSan (-fsanitize=undefined) | integer overflow, shift quá mức, misaligned, UB khác | nhanh |
MSan (-fsanitize=memory) | đọc bộ nhớ chưa khởi tạo | ~3× chậm |
| Valgrind | tương tự ASan, không cần biên dịch lại | ~10–30× chậm |
Workflow chuẩn của bạn từ giờ: dev với
-Wall -Wextra -fsanitize=address,undefined, audit codebase người khác bằngvalgrind. Hai công cụ này sẽ tìm ra phần lớn lỗ hổng memory của telecom stack.
7. Một số chủ đề C còn lại cần biết
7.1. errno & xử lý lỗi hệ thống
#include <errno.h>
#include <string.h>
int fd = open("/dev/x", O_RDONLY);
if (fd < 0) fprintf(stderr, "open lỗi: %s\n", strerror(errno));
7.2. volatile, restrict, inline
volatile: cấm trình biên dịch tối ưu việc đọc biến (cho memory-mapped I/O, biến thay đổi bởi tín hiệu).restrict: cam kết con trỏ không trùng vùng (giúp tối ưu, vd.memcpy).inline/static inline: gợi ý nội tuyến hàm nhỏ.
7.3. Biến tham số (variadic) — như printf
#include <stdarg.h>
void log_msg(const char *fmt, ...) {
va_list ap; va_start(ap, fmt);
vfprintf(stderr, fmt, ap);
va_end(ap);
}
Bẫy format string (xem
09):printf(user_input)thay vìprintf("%s", user_input)là lỗ hổng format-string kinh điển — kẻ tấn công nhồi%nđể ghi bộ nhớ. Trong logging của telecom stack, lỗi này từng gây RCE.
8. Bài tập / Lab module 04
- Dispatch table: mở rộng
dispatch_command(mục 1) cho đủ CER/CEA/DWR/DWA/DPR/DPA, mỗi handler chỉ in tên. Test bằng cách gọi với nhiều command code, kể cả code lạ. - Macro vs inline: viết
MAX(a,b)dạng macro và dạngstatic inline. GọiMAX(i++, j)và quan sát số lầnităng ở mỗi cách. Rút ra kết luận. - Internet checksum: hiện thực
inet_checksum(mục 3.2), kiểm tra trên một header UDP/IP thật lấy từ pcap — so khớp với giá trị checksum trong gói (Wireshark hiển thị “checksum: 0x… [correct]”). - ALIGN4: viết hàm dùng
ALIGN4để tính, với một AVP độ dài 13 byte, độ dài thực tế kể cả padding (= 16). Kiểm tra vài giá trị. - Dự án nhiều file + Makefile: tách parser Diameter header thành
diameter.h/diameter.c/main.c, viết Makefile có targetall,clean,asan. Build cả hai chế độ. - gdb hunt: lấy một bài có lỗi UAF từ module 02, chạy dưới gdb, để nó crash, dùng
btvàinfo registersđể định vị. Sau đó build lại-fsanitize=addressvà so sánh thông tin ASan cho bạn so với gdb. - valgrind audit: chạy
valgrind --leak-check=fulllên dự án bài 5 sau khi cố tình quên mộtfree. Đọc và diễn giải báo cáo (số byte leak, stack cấp phát).
Cột mốc module 04: Bạn xây được dispatch table bằng con trỏ hàm, tổ chức dự án nhiều file với Makefile/CMake, và thành thạo gdb + valgrind + sanitizer để săn lỗi bộ nhớ. Đây là bộ công cụ làm việc hằng ngày của một security researcher. Bạn đã đủ C để bước sang đọc telecom stack thật.
➡️ Tiếp theo: 05-Cpp-Co-Ban.md — chuyển sang C++ (OOP, RAII, STL).
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ớ