Skip to content
LIVE FEED

Blog04 — C Nâng Cao

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*x rồi SQ(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ùng static inline function 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 static cho 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 đượcTố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ácnhanh
MSan (-fsanitize=memory)đọc bộ nhớ chưa khởi tạo~3× chậm
Valgrindtươ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ằng valgrind. 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

  1. 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ạ.
  2. Macro vs inline: viết MAX(a,b) dạng macro và dạng static inline. Gọi MAX(i++, j) và quan sát số lần i tăng ở mỗi cách. Rút ra kết luận.
  3. 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]”).
  4. 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ị.
  5. Dự án nhiều file + Makefile: tách parser Diameter header thành diameter.h/diameter.c/main.c, viết Makefile có target all, clean, asan. Build cả hai chế độ.
  6. gdb hunt: lấy một bài có lỗi UAF từ module 02, chạy dưới gdb, để nó crash, dùng bt và info registers để định vị. Sau đó build lại -fsanitize=address và so sánh thông tin ASan cho bạn so với gdb.
  7. valgrind audit: chạy valgrind --leak-check=full lên dự án bài 5 sau khi cố tình quên một free. Đọ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. 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