Blog08 — Phân Tích Giao Thức Telecom Bằng C/C++
Bố cục bài viết
Trang này
Ngày tạo
05/08/2026
Cập nhật
Lượt xem
6
Đọc
18m
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 sau | Mọ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ói | Length 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ương | Mỗ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 = 0khô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
16777251chỉ xuất hiện trong AVPAuth-Application-Id/Vendor-Specific-Application-Idbê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 (xem07). 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 (
ALIGN4từ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_DEPTHchặn stack-exhaustion do đệ quy; (4)step == 0 || step > remainingchặ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):
- Chỉ tách trên
\r\nđầy đủ, đừng coi\nlẻ 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. - 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.
- 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. - 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_viewchỉ hợp lệ khi buffer gốc còn sống — đừng trảstring_viewtrỏ vào buffer cục bộ đã hủy (một dạng dangling ~ UAF, xem09).
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ặnnum > 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 sang10.
8. Checklist hardening parser (in ra dán cạnh màn hình)
| ✓ | Hạng mục | Vì sao |
|---|---|---|
| ☐ | Mọi hàm parse nhận (con trỏ, độ dài) hoặc span | Quy 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 - pos | Trá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ỏ > 0 | Quy 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ước | Chống bom bộ nhớ |
| ☐ | Không memcpy/subspan trước khi validate length | Chống buffer over-read/-write |
| ☐ | string_view/span không sống lâu hơn buffer gốc | Chống dangling/UAF |
| ☐ | Build & test parser dưới ASan + UBSan | Bắt lỗi bộ nhớ/UB còn ẩn (xem 10) |
9. Bài tập / Lab module 08
- 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ếnpos+lentrànsize_tvà xác nhận phiên bảnsize - posvẫn an toàn. - 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. - Cây AVP có grouped: hoàn thiện
parse_avp_full+parse_avp_list(mục 3.2), thêm fieldraw_len,vendor_idvàoAvpNode. Parse một CER (cóVendor-Specific-Application-Idgrouped) và in cây thụt lề: code, length, vendor, hex value; grouped thì in con lồng vào. - 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.
- 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). - SIP splitter: hiện thực
split_crlf+ tách Request-Line và từng headerName: Value. Đọc đúng body theoContent-Length=min(khai_báo, thực_tế). Test với một bản tin cóContent-Lengthlớn hơn body thật (không được treo/over-read). - 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). - 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 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ớ