Skip to content
LIVE FEED

Blog08 — Phân Tích Giao Thức Telecom Bằng C/C++

08 — Phân Tích Giao Thức Telecom Bằng C/C++

Mục tiêu: Đây là module “trả lãi” cho toàn bộ Phase 1–3. Bạn sẽ ghép mọi thứ — struct/endianness (03), bit ops & ALIGN4 (04), ByteReader (05), parse_avp/AvpNode/span/optional (06), socket/SCTP (07) — thành bộ giải mã (decoder/dissector) protocol viễn thông thật. Trọng tâm xuyên suốt: mọi byte đến từ mạng đều là input thù địch (hostile input). Một parser tốt phải đọc đúng spec và không bao giờ crash, kể cả khi gói tin bị bóp méo có chủ đích.


1. Tư duy nền: parser là bề mặt tấn công lớn nhất

Trong một network function (HSS, MME, P-CSCF, STP, DEA…), thành phần đầu tiên chạm vào dữ liệu kẻ tấn công gửi tới chính là bộ parse gói tin. Lịch sử CVE của Wireshark, freeDiameter, Kamailio, các stack SS7… cho thấy phần lớn lỗ hổng nghiêm trọng nằm ở parser: đọc quá biên, nhân độ dài tràn số, vòng lặp vô hạn vì length=0, đệ quy không giới hạn với grouped AVP lồng nhau.

   Kẻ tấn công  ──►  [ Gói tin méo mó ]  ──►  ┌──────────────┐
   (peer SS7,                                  │   PARSER     │  ◄── nơi 80% lỗ hổng sống
    IPX, UE giả…)                              └──────┬───────┘
                                                      ▼
                                          logic nghiệp vụ (tin parser đã đúng)

Ba nguyên tắc bất di bất dịch khi viết parser protocol:

Nguyên tắcÝ nghĩa cụ thể
Validate trước, đọc sauMọi trường length phải được kiểm tra so với số byte còn lại trước khi dùng để subspan/memcpy/nhảy con trỏ.
Không tin length trong góiLength là do peer khai báo. len có thể = 0, có thể > buffer, có thể gây tràn khi cộng/nhân. Coi mọi length là kẻ địch.
Tiến độ phải dươngMỗi vòng lặp parse một phần tử (AVP/IE) bắt buộc tiến con trỏ ≥ 1. Nếu một phần tử khai báo length=0 mà code không phòng, vòng lặp treo CPU (DoS).

Quy tắc thép #9: Mỗi vòng lặp parse một danh sách phần tử có độ dài tự khai báo (TLV, AVP, IE) phải đảm bảo con trỏ tiến lên một lượng > 0 ở mỗi vòng. Một phần tử length = 0 không được phép làm vòng lặp dậm chân tại chỗ. Đây là lỗ hổng DoS kinh điển và rất dễ lọt.


2. TLV — khuôn mẫu chung của gần như mọi protocol

TLV = Type – Length – Value. Hiểu sâu một TLV parser an toàn là chìa khóa, vì Diameter AVP, GTP IE, ASN.1 BER, nhiều IE trong SS7/SCCP… đều là biến thể của TLV.

  ┌────────┬────────┬───────────────────────────┐
  │  Type  │ Length │           Value           │
  │ (n byte)│(m byte)│      (Length byte)        │
  └────────┴────────┴───────────────────────────┘
   con trỏ tiến: n + m + Length  (đôi khi + padding)

Parser TLV tổng quát, an toàn, bằng C++ (dùng span, không copy):

#include <cstdint>
#include <span>
#include <vector>
#include <optional>

struct Tlv {
    uint16_t type;
    std::span<const uint8_t> value;   // cửa sổ trỏ vào buffer gốc, KHÔNG sao chép
};

// Giả định: Type 2 byte BE, Length 2 byte BE, Value = Length byte (không padding).
std::optional<std::vector<Tlv>> parse_tlv_list(std::span<const uint8_t> buf) {
    std::vector<Tlv> out;
    size_t pos = 0;
    while (pos < buf.size()) {
        if (buf.size() - pos < 4) return std::nullopt;        // không đủ cho T+L
        uint16_t type = (uint16_t(buf[pos]) << 8) | buf[pos+1];
        uint16_t len  = (uint16_t(buf[pos+2]) << 8) | buf[pos+3];
        pos += 4;
        if (len > buf.size() - pos) return std::nullopt;       // length vượt biên → từ chối
        out.push_back({type, buf.subspan(pos, len)});
        pos += len;                                            // luôn tiến ≥ 4 (xem Quy tắc #9)
    }
    return out;
}

Điểm an toàn cốt lõi: if (len > buf.size() - pos) — luôn viết phép trừ theo hướng size - pos (cả hai là size_t, đã đảm bảo pos <= size), không bao giờ viết pos + len > buf.size() vì pos + len có thể tràn size_t và qua mặt kiểm tra (integer overflow → bypass bounds check). Đây là chính xác lớp lỗi ta sẽ mổ xẻ ở 09.


3. Diameter — header 20 byte + AVP (kể cả grouped/lồng nhau)

Diameter (RFC 6733) là trục chính của lõi 4G/IMS: S6a (DEA↔HSS, App-Id 16777251), Gx, Rx, Cx/Dx, S13… Bạn đang xử lý case S6a, nên đây là phần đáng đầu tư nhất.

3.1. Diameter header (cố định 20 byte)

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Version (1)  |          Message Length (3 byte)              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Cmd Flags (1) |          Command-Code (3 byte)                |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Application-Id (4 byte)                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    Hop-by-Hop Id (4 byte)                     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                    End-to-End Id (4 byte)                     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   AVPs ...                                                    |
  • Version = 1 (luôn). Khác 1 → từ chối.
  • Message Length (24-bit) = tổng độ dài toàn bộ message kể cả header. Bao bội số 4.
  • Command Flags (1 byte): bit R (Request), P (Proxiable), E (Error), T (reTransmit). Vd 0x80 = Request.
  • Command-Code (24-bit): CER/CEA = 257, DWR/DWA = 280, DPR/DPA = 282, ULR/ULA (S6a) = 316, AIR/AIA = 318…
  • Application-Id: 0 = base (CER, DWR, DPR dùng App-Id 0!), 16777251 = S6a/S6d.

Bẫy thực chiến liên quan case của bạn: CER/CEA, DWR/DWA, DPR/DPA luôn mang Application-Id = 0 ở header (chúng là base protocol), dù phiên này dùng để thiết lập S6a. App-Id S6a 16777251 chỉ xuất hiện trong AVP Auth-Application-Id/Vendor-Specific-Application-Id bên trong CER, và trong header của các lệnh ứng dụng (ULR…). Nhầm điều này khi tự build CER → peer trả lỗi hoặc bỏ qua. Nhưng nhớ: trong sự cố bạn gặp, vấn đề ở tầng trên cả việc này — CER không hề được gửi vì FSM role sai (xem 07). Hiểu byte layout giúp bạn xác nhận điều đó trên Wireshark: bạn sẽ thấy COOKIE-ACK rồi SHUTDOWN, không có một byte Diameter nào (Version=01) đi ra.

Parse header bằng ByteReader (đã định nghĩa ở 05):

struct DiameterHeader {
    uint8_t  version;
    uint32_t length;          // 24-bit
    uint8_t  flags;
    uint32_t command_code;    // 24-bit
    uint32_t application_id;
    uint32_t hop_by_hop;
    uint32_t end_to_end;
};

std::optional<DiameterHeader> parse_diameter_header(ByteReader& r) {
    if (r.remaining() < 20) return std::nullopt;
    DiameterHeader h;
    h.version = r.u8();
    if (h.version != 1) return std::nullopt;                 // chỉ chấp nhận v1
    h.length        = (uint32_t(r.u8()) << 16);              // đọc 24-bit length
    h.length       |= (uint32_t(r.u8()) << 8);
    h.length       |=  r.u8();
    h.flags         = r.u8();
    h.command_code  = (uint32_t(r.u8()) << 16) | (uint32_t(r.u8()) << 8) | r.u8();
    h.application_id= r.u32be();
    h.hop_by_hop    = r.u32be();
    h.end_to_end    = r.u32be();
    if (h.length < 20) return std::nullopt;                  // length không thể nhỏ hơn header
    return h;
}

3.2. AVP và grouped AVP — đệ quy có giới hạn

AVP cũng là TLV với header 8 (hoặc 12 nếu có Vendor-Id) byte:

 0                   1                   2                   3
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         AVP Code (4 byte)                     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V M P r r r r r|                 AVP Length (3 byte)          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|              Vendor-ID (4 byte, CHỈ khi cờ V = 1)            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|    Data ...                                                   |
  • Cờ V (Vendor-Specific): nếu = 1, có thêm 4 byte Vendor-Id → header thành 12 byte. Vendor-Id 3GPP = 10415.
  • AVP Length (24-bit) tính không gồm padding nhưng gồm header.
  • Mỗi AVP được đệm tới bội số 4 byte (ALIGN4 từ 04) — padding không tính vào AVP Length nhưng phải bỏ qua khi nhảy tới AVP kế tiếp.
  • Grouped AVP: Data của nó lại là một dãy AVP con (vd Vendor-Specific-Application-Id, Subscription-Data). Đây là chỗ cần đệ quy có giới hạn độ sâu.

Mở rộng parse_avp/AvpNode (từ 06) để xử lý Vendor-Id, padding ALIGN4, và grouped AVP với giới hạn độ sâu chống đệ quy bom:

#include <set>

#define ALIGN4(n) (((n) + 3u) & ~3u)          // từ module 04
constexpr int MAX_AVP_DEPTH = 16;             // chặn grouped AVP lồng vô hạn

// Tập AVP code đã biết là "grouped" (cần giải mã đệ quy)
static const std::set<uint32_t> GROUPED = {260, 279, 297, 458 /*…*/};

std::optional<std::unique_ptr<AvpNode>>
parse_avp_full(std::span<const uint8_t> data, int depth);

// Parse một DÃY avp liên tiếp (thân message, hoặc data của grouped AVP)
std::vector<std::unique_ptr<AvpNode>>
parse_avp_list(std::span<const uint8_t> buf, int depth) {
    std::vector<std::unique_ptr<AvpNode>> out;
    if (depth > MAX_AVP_DEPTH) return out;                 // chặn đệ quy bom
    size_t pos = 0;
    while (buf.size() - pos >= 8) {                        // còn đủ cho header tối thiểu
        auto avp = parse_avp_full(buf.subspan(pos), depth);
        if (!avp) break;                                   // gặp lỗi → dừng êm
        // tính bước nhảy = AVP Length đã đệm ALIGN4
        uint32_t raw_len = (*avp)->raw_len;                // length thật (chưa đệm)
        uint32_t step    = ALIGN4(raw_len);
        if (step == 0 || step > buf.size() - pos) break;   // Quy tắc thép #9 + bounds
        out.push_back(std::move(*avp));
        pos += step;
    }
    return out;
}

std::optional<std::unique_ptr<AvpNode>>
parse_avp_full(std::span<const uint8_t> data, int depth) {
    if (data.size() < 8) return std::nullopt;
    auto node = std::make_unique<AvpNode>();
    node->code  = (uint32_t(data[0])<<24)|(uint32_t(data[1])<<16)|
                  (uint32_t(data[2])<<8) | data[3];
    node->flags = data[4];
    uint32_t len = (uint32_t(data[5])<<16)|(uint32_t(data[6])<<8)|data[7];
    if (len < 8 || len > data.size()) return std::nullopt;  // validate length!
    node->raw_len = len;

    size_t hdr = 8;
    if (node->flags & 0x80) {                               // cờ V → có Vendor-Id
        if (len < 12) return std::nullopt;
        node->vendor_id = (uint32_t(data[8])<<24)|(uint32_t(data[9])<<16)|
                          (uint32_t(data[10])<<8)|data[11];
        hdr = 12;
    }
    auto value = data.subspan(hdr, len - hdr);

    if (GROUPED.count(node->code)) {                        // grouped → đệ quy
        node->children = parse_avp_list(value, depth + 1);
    } else {
        node->value.assign(value.begin(), value.end());
    }
    return node;
}

Bốn lá chắn an toàn trong đoạn trên, ánh xạ 1–1 với các lớp lỗi ở 09: (1) len < 8 || len > data.size() chặn over-read; (2) ALIGN4(raw_len) nhảy đúng để không lệch khung; (3) MAX_AVP_DEPTH chặn stack-exhaustion do đệ quy; (4) step == 0 || step > remaining chặn DoS vòng lặp & over-read. Một parser thiếu bất kỳ lá chắn nào trong số này từng là một CVE thật.


4. GTPv2-C — header + IE (TLIV)

GTPv2-C (3GPP TS 29.274) là control plane của S5/S8/S11 trên EPC. Header bạn đã gặp ở 03; giờ ta parse phần IE (Information Element) của nó. IE GTPv2 theo dạng T-L-I-V (Type, Length, spaCE+Instance, Value):

  ┌────────┬─────────────────┬──────────────┬───────────────┐
  │  Type  │   Length (2B)   │ Spare+Instance│     Value     │
  │  (1B)  │ (độ dài Value)  │     (1B)      │  (Length B)   │
  └────────┴─────────────────┴──────────────┴───────────────┘
   Header IE = 4 byte; Length CHỈ đếm Value (khác Diameter!)

Khác biệt cốt yếu so với Diameter: GTPv2 IE Length chỉ tính số byte Value, không gồm 4 byte header IE. Nhầm quy ước length giữa hai protocol là lỗi parser cực phổ biến.

struct GtpIe {
    uint8_t  type;
    uint8_t  instance;     // 4 bit thấp của byte thứ 4
    std::span<const uint8_t> value;
};

std::optional<std::vector<GtpIe>> parse_gtp_ies(std::span<const uint8_t> buf) {
    std::vector<GtpIe> out;
    size_t pos = 0;
    while (pos < buf.size()) {
        if (buf.size() - pos < 4) return std::nullopt;          // header IE = 4 byte
        uint8_t  type = buf[pos];
        uint16_t len  = (uint16_t(buf[pos+1]) << 8) | buf[pos+2]; // length CHỈ là Value
        uint8_t  inst = buf[pos+3] & 0x0F;
        pos += 4;
        if (len > buf.size() - pos) return std::nullopt;         // bounds check
        out.push_back({type, inst, buf.subspan(pos, len)});
        pos += len;                                              // tiến ≥ 4 mỗi vòng
    }
    return out;
}

GTPv2 không có padding ALIGN4 (khác Diameter). Một số IE (vd Bearer Context, type 93) là grouped — Value lại chứa IE con, xử lý đệ quy giống grouped AVP, vẫn áp MAX_DEPTH. GTP-C/GTP-U là bề mặt tấn công nổi tiếng (GTP-in-GTP, IE méo) — chuẩn GSMA FS.20 mà bạn đang học chính là về phòng thủ lớp này; parser cứng cáp là lớp bảo vệ kỹ thuật trực tiếp.


5. SIP — protocol VĂN BẢN (tư duy parse hoàn toàn khác)

Diameter/GTP là nhị phân; SIP (RFC 3261, lõi IMS/VoLTE) là text, phân tách bằng CRLF (\r\n). Nguy hiểm chuyển từ “đọc quá biên” sang “phân tách dòng sai” và injection.

INVITE sip:bob@example.com SIP/2.0\r\n      ◄── Request-Line
Via: SIP/2.0/UDP host;branch=z9hG4bK...\r\n  ◄── header (có thể lặp lại)
From: <sip:alice@example.com>;tag=1928\r\n
To: <sip:bob@example.com>\r\n
Call-ID: a84b4c76e66710\r\n
CSeq: 314159 INVITE\r\n
Content-Length: 142\r\n
\r\n                                          ◄── dòng trống = hết header
v=0...                                       ◄── body (SDP), dài đúng Content-Length

Bốn cạm bẫy an ninh đặc thù khi parse SIP (và HTTP/2 trong IMS hiện đại):

  1. Chỉ tách trên \r\n đầy đủ, đừng coi \n lẻ như nhau giữa các nơi → khác biệt diễn giải giữa proxy và endpoint tạo lỗ hổng request smuggling.
  2. Content-Length là do peer khai — đọc body đúng min(Content-Length, số byte thật còn lại); đừng tin mù để rồi over-read hoặc treo chờ byte không bao giờ tới.
  3. CRLF injection / header injection: nếu bạn ghép dữ liệu người dùng (vd số bị gọi) vào header mà không lọc \r\n, kẻ tấn công chèn header/giả mạo bản tin. Luôn loại bỏ ký tự điều khiển trước khi nội suy.
  4. Giới hạn kích thước & số lượng header — không có giới hạn = bom bộ nhớ/DoS.
#include <string_view>
#include <vector>

// Tách thành dòng trên CRLF, KHÔNG sao chép (string_view nhìn vào buffer gốc).
std::vector<std::string_view> split_crlf(std::string_view msg, size_t max_lines = 200) {
    std::vector<std::string_view> lines;
    size_t start = 0;
    while (start < msg.size() && lines.size() < max_lines) {  // chặn số dòng (DoS)
        size_t nl = msg.find("\r\n", start);
        if (nl == std::string_view::npos) { lines.push_back(msg.substr(start)); break; }
        if (nl == start) break;                               // dòng trống = hết header
        lines.push_back(msg.substr(start, nl - start));
        start = nl + 2;
    }
    return lines;
}

string_view (từ 06) là công cụ lý tưởng cho parser text: cắt/so khớp không cấp phát, không copy. Nhưng nhớ vòng đời: string_view chỉ hợp lệ khi buffer gốc còn sống — đừng trả string_view trỏ vào buffer cục bộ đã hủy (một dạng dangling ~ UAF, xem 09).


6. ASN.1 / BER — TLV ở tầng cao hơn (SS7/MAP, S1AP/NGAP)

SS7 MAP, CAP, và S1AP/NGAP (giao diện vô tuyến–lõi 4G/5G) mã hóa bằng ASN.1 với quy tắc BER/PER. BER cũng là TLV nhưng Tag và Length có thể đa byte:

  ┌──────────┬───────────────┬─────────────┐
  │  Tag(s)  │  Length(s)    │   Value     │
  └──────────┴───────────────┴─────────────┘

  Tag byte đầu:  [class:2][P/C:1][number:5]
     - number = 0x1F (11111) → tag dài, đọc tiếp các byte sau (bit 8 = "còn nữa")
  Length:
     - bit 8 = 0  → short form: 7 bit còn lại = độ dài (0..127)
     - bit 8 = 1  → long form: 7 bit còn lại = SỐ byte độ dài tiếp theo
     - 0x80       → indefinite length (kết thúc bằng 2 byte 0x00 0x00)  ◄── nguy hiểm

Đọc length BER an toàn — nơi rất dễ sai (và đầy CVE trong các thư viện ASN.1):

// Trả {length, số_byte_đã_đọc} hoặc nullopt nếu lỗi.
std::optional<std::pair<size_t,size_t>>
ber_length(std::span<const uint8_t> b) {
    if (b.empty()) return std::nullopt;
    uint8_t first = b[0];
    if ((first & 0x80) == 0) return std::make_pair(size_t(first), size_t(1)); // short
    size_t num = first & 0x7F;
    if (num == 0)   return std::nullopt;       // 0x80 = indefinite → từ chối (an toàn)
    if (num > 4)    return std::nullopt;        // > 4GB độ dài → coi là tấn công
    if (b.size() < 1 + num) return std::nullopt;
    size_t len = 0;
    for (size_t i = 0; i < num; ++i) len = (len << 8) | b[1 + i];
    return std::make_pair(len, 1 + num);
}

Hai quyết định phòng thủ đáng chú ý: từ chối indefinite length (đơn giản hóa, tránh cả lớp lỗi quét tìm 00 00) và chặn num > 4 (một length 8 byte = chiêu làm tràn số nguyên / cấp phát khổng lồ). Tinh thần: parser an ninh được phép nghiêm khắc hơn spec — từ chối cái hợp lệ-nhưng-hiếm còn hơn mở cửa cho tấn công. ASN.1 đầy đủ rất phức tạp; với audit, hiểu khung TLV này + đọc thư viện (asn1c, libtasn1) là đủ khởi đầu.


7. Packet crafting — TỰ build gói để test/fuzz

Đọc (parse) là một nửa; nửa kia là tạo (craft) gói để bơm vào parser (test/fuzz) hoặc gửi lên mạng lab. Dưới đây là builder Diameter CER tối giản bằng C++ (vector<uint8_t>, move semantics từ 06):

// Ghi 24-bit big-endian
void put_u24(std::vector<uint8_t>& v, uint32_t x) {
    v.push_back((x >> 16) & 0xFF); v.push_back((x >> 8) & 0xFF); v.push_back(x & 0xFF);
}
void put_u32(std::vector<uint8_t>& v, uint32_t x) {
    v.push_back((x>>24)&0xFF); v.push_back((x>>16)&0xFF);
    v.push_back((x>>8)&0xFF);  v.push_back(x&0xFF);
}

// AVP đơn giản (không vendor); tự đệm ALIGN4
void put_avp(std::vector<uint8_t>& v, uint32_t code, uint8_t flags,
             std::span<const uint8_t> data) {
    uint32_t len = 8 + uint32_t(data.size());        // header 8 + data (CHƯA đệm)
    put_u32(v, code);
    v.push_back(flags);
    put_u24(v, len);
    v.insert(v.end(), data.begin(), data.end());
    while (v.size() % 4 != 0) v.push_back(0x00);      // padding ALIGN4
}

std::vector<uint8_t> build_cer(std::string_view origin_host,
                               std::string_view origin_realm) {
    std::vector<uint8_t> avps;
    auto S = [](std::string_view s){
        return std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(s.data()), s.size());
    };
    put_avp(avps, 264, 0x40, S(origin_host));         // Origin-Host (M)
    put_avp(avps, 296, 0x40, S(origin_realm));        // Origin-Realm (M)
    // … thực tế thêm Host-IP-Address(257), Vendor-Id(266), Product-Name(269),
    //    Auth-Application-Id(258) hoặc Vendor-Specific-Application-Id(260)…

    std::vector<uint8_t> msg;
    msg.push_back(0x01);                              // Version
    put_u24(msg, 20 + uint32_t(avps.size()));         // Message Length = header + AVPs
    msg.push_back(0x80);                              // Flags: R=1 (Request)
    put_u24(msg, 257);                               // Command-Code = CER
    put_u32(msg, 0);                                 // Application-Id = 0 (base!) ◄── nhớ mục 3.1
    put_u32(msg, 0x11111111);                        // Hop-by-Hop
    put_u32(msg, 0x22222222);                        // End-to-End
    msg.insert(msg.end(), avps.begin(), avps.end());
    return msg;                                       // MOVE ra ngoài
}

Ghép builder này với SCTP client thô ở 07: bạn có chính xác capstone #3 (gửi CER đúng byte trên SCTP, nhận CEA) — và là công cụ để tự kiểm chứng hành vi đối tác trong sự cố S6a của bạn ở mức gói tin, không chỉ đoán qua FSM. Khi craft để fuzz, bạn sẽ cố tình làm sai length/đệm/cờ — đó là cầu nối sang 10.


8. Checklist hardening parser (in ra dán cạnh màn hình)

✓Hạng mụcVì sao
☐Mọi hàm parse nhận (con trỏ, độ dài) hoặc spanQuy tắc thép #4 — không có mảng tự biết độ dài
☐Kiểm tra len <= remaining bằng phép trừ size - posTránh integer overflow qua mặt bounds check
☐Validate Version/Type/giá trị hằng trước khi xử lýTừ chối sớm gói rác
☐Mỗi vòng lặp tiến con trỏ > 0Quy tắc thép #9 — chống DoS vòng lặp
☐Giới hạn độ sâu đệ quy (grouped AVP/IE)Chống stack exhaustion
☐Giới hạn số phần tử & tổng kích thướcChống bom bộ nhớ
☐Không memcpy/subspan trước khi validate lengthChống buffer over-read/-write
☐string_view/span không sống lâu hơn buffer gốcChống dangling/UAF
☐Build & test parser dưới ASan + UBSanBắt lỗi bộ nhớ/UB còn ẩn (xem 10)

9. Bài tập / Lab module 08

  1. TLV tổng quát: hoàn thiện parse_tlv_list (mục 2). Test: (a) danh sách hợp lệ; (b) length cuối vượt biên → nullopt; (c) buffer 3 byte → nullopt. Thêm test length khiến pos+len tràn size_t và xác nhận phiên bản size - pos vẫn an toàn.
  2. Diameter header: đọc một pcap CER thật (Wireshark → “Export Packet Bytes” lấy phần Diameter), nạp vào ByteReader, parse header, in Version/Length/Command/App-Id/Hop-by-Hop. Đối chiếu từng trường với Wireshark.
  3. Cây AVP có grouped: hoàn thiện parse_avp_full + parse_avp_list (mục 3.2), thêm field raw_len, vendor_id vào AvpNode. Parse một CER (có Vendor-Specific-Application-Id grouped) và in cây thụt lề: code, length, vendor, hex value; grouped thì in con lồng vào.
  4. Tấn công parser của chính mình: lấy parser bài 3, tự tạo input độc: (a) AVP length = 0xFFFFFF; (b) grouped AVP lồng 1000 tầng; (c) AVP length = 4 (< 8). Chạy dưới ASan. Mục tiêu: không crash, không over-read, không treo — chỉ trả lỗi êm. Đây là tiền đề trực tiếp cho capstone #1.
  5. GTPv2 IE: hiện thực parse_gtp_ies (mục 4), parse một Create Session Request từ pcap. Chú ý xử lý đúng quy ước “Length chỉ là Value”. Thử thêm xử lý grouped IE (Bearer Context).
  6. SIP splitter: hiện thực split_crlf + tách Request-Line và từng header Name: Value. Đọc đúng body theo Content-Length = min(khai_báo, thực_tế). Test với một bản tin có Content-Length lớn hơn body thật (không được treo/over-read).
  7. BER length: hoàn thiện ber_length (mục 6). Test short form, long form 2 byte, 0x80 (indefinite → nullopt), và length 8 byte (→ nullopt).
  8. Craft + round-trip: dùng build_cer (mục 7) tạo CER, rồi parse lại bằng chính parser của bạn (round-trip). Mọi trường phải khớp. Sau đó cố tình làm sai một padding và xem parser bắt lỗi thế nào.

Cột mốc module 08 (và hết Phase 4): Bạn parse được Diameter (kể cả grouped AVP), GTPv2-C IE, SIP, và khung ASN.1/BER, biết craft gói để test, và quan trọng nhất — bạn viết parser chống input thù địch theo checklist mục 8. Bạn đã có thể đọc dissector của Wireshark và stack telecom thật mà không lạc, và tự xây decoder mới. Đây là kỹ năng lõi của một chuyên gia an ninh viễn thông.

➡️ Tiếp theo: 09-An-Ninh-Bo-Nho-Va-Khai-Thac.md — khi một trong các lá chắn ở trên thiếu, lỗ hổng phát sinh ra sao, và kẻ tấn công khai thác thế nào (môi trường lab/được cấp phép).

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