Files
labs/Notebooks-CSAI/Lab5_Malware_Workflow.ipynb

84 KiB

Module 5 — Quy trình phân tích mã độc: entropy quyết định đường đi, và cây quyết định học lại luật đó

Cách làm: bấm vào một ô code rồi nhấn Shift + Enter để chạy ô đó và sang ô kế tiếp. Chạy lần lượt từ trên xuống. Kernel phải là Python (lớp CSAI). Ô có dòng ✏️ SỬA là chỗ mời bạn đổi số / chữ rồi chạy lại. Sửa hỏng? Menu Kernel → Restart Kernel and Run All Cells.

Bài này làm gì? Đi đúng quy trình phân tích mã độc thật mà honeypot của mình đang dùng (phiên bản V3.2): mẫu mã độc tới → thu thập → tra cứu → đo entropy để chọn đường phân tích → phân tích tĩnh / chạy thử trong máy cách ly → báo cáo. Sau đó cho cây quyết định tự học luật chọn đường từ dữ liệu, và đọc 2 ca phân tích thật.

Dữ liệu là gì? các mẫu mã độc thật do honeypot của mình bắt được (bao nhiêu mẫu thì ô 2.2 sẽ đếm) (kẻ tấn công tải lên sau khi đăng nhập được vào máy "mồi"). ⚠️ Trên máy lớp không có file mã độc nào: chỉ có các con số của từng mẫu (kích thước, entropy, loại file, kiến trúc CPU, có dấu UPX không) trong /opt/csai/data/malware_meta/malware_meta.csv. Phía sạch là các chương trình thật trong /usr/bin, /usr/sbin của chính máy lớp. Phần "nén / mã hoá" ta tự tạo từ /usr/bin/ls bằng zlib và phép XOR — vô hại.

🎯 Mục tiêu của bài

Học xong bài này, bạn:

  1. Hiểu quy trình phân tích mã độc 5 cổng: mỗi cổng hỏi gì, làm gì, ra gì (Phần 1).
  2. Đo và đọc được entropy của một file: chữ, chương trình, file nén, file mã hoá khác nhau thế nào (Phần 2).
  3. Chạy luật chọn đường theo entropy trên mã độc thật và tìm lỗ hổng của chính quy trình (Phần 3).
  4. Nhìn file thành ảnh để thấy "có kết cấu" và "nhiễu trắng" (Phần 4).
  5. Cho cây quyết định học lại luật, và biết khi nào một kết quả "quá đẹp" chưa nói được gì (Phần 5).
  6. Đọc 2 ca phân tích thật và biến kết quả thành hành động SOC (Phần 6, 7).

🗺️ Bài này: bài toán gì, mô hình nào, vì sao?

  • Bài toán: một file lạ bị bắt → đưa vào đường phân tích nào (đọc tĩnh, giải nén trước, hay chạy thử trong máy cách ly)? Đây là bài phân luồng bằng luật trên một đặc trưng (entropy), rồi để máy học lại luật đó.
  • Dữ liệu: bảng số, mỗi mẫu một hàng (kích thước, entropy, loại file, CPU, dấu UPX); file mã độc không có trên máy lớp. Phía sạch là chương trình của chính máy lớp.
  • Vì vậy chọn: mốc = luật ngưỡng entropy của quy trình hiện có; mô hình = cây quyết định (hộp trắng: đọc được ngưỡng nó chọn, so được với luật của người).
  • Xử lý gì trước: so cùng loại file (chương trình Linux với chương trình Linux), và biết mẫu nào bị nén trước khi kết luận theo entropy.
  • Thước đo: đường đi của luật có đúng không; với cây — độ chính xác, nhưng cảnh giác khi kết quả quá đẹp trên dữ liệu quá dễ.

🧭 Ký hiệu trong bài — cái gì BẮT BUỘC, cái gì TUỲ CHỌN

Ký hiệu Nghĩa Bắt buộc?
ô code bình thường phải chạy, lần lượt từ trên xuống: ô sau dùng các biến mà ô trước tạo ra. Bỏ một ô → các ô sau báo lỗi NameError (không biết tên biến) BẮT BUỘC
🎯 Mục tiêu · 📥 Đầu vào · 📤 Đầu ra đặt ngay trên mỗi ô code: ô này làm gì, dùng dữ liệu / biến gì có sẵn, tạo ra gì cho các bước sau đọc trước khi chạy
✏️ SỬA chỗ mời đổi số / chữ để thử. Giữ nguyên cũng chạy đúng. Dòng Các lựa chọn ngay trên ô cho biết nên thử giá trị nào và sẽ thấy gì (đo thật) tuỳ chọn
🧰 tham số một hàm nhận những gì: tham số bắt buộc (thiếu là lỗi) và tuỳ chọn (không ghi thì dùng giá trị mặc định) để tra khi cần
(TUỲ CHỌN) trong tên mục đọc / chạy thêm nếu còn thời gian; bỏ qua thì các ô sau vẫn chạy tuỳ chọn
Kết quả · 👉 Hiểu · 📦 số đo thật trên máy lớp · ý nghĩa cần nhớ · bên trong một biến có gì (biến sẽ được dùng lại) —
💼 Dùng khi nào · ⚖️ Ảnh hưởng tới ngoài bài này, khi đi làm bạn sẽ cần kỹ thuật này lúc nào · nó quyết định điều gì (làm sai thì hỏng cái gì) đọc để nhớ lâu

Tham số bắt buộc / tuỳ chọn trông thế nào? Hàm def ham(a, b=1): a bắt buộc, b tuỳ chọn (không ghi thì b = 1). Gọi được ham(5) hoặc ham(5, b=3); gọi ham() thì lỗi vì thiếu a.

Pipeline — cách làm mọi bài toán AI trong khoá, nhớ thuộc lòng 7 bước. Mỗi phần ghi 📍 Bước ở đầu:

Bước Câu hỏi phải trả lời Phần
1. Thu thập dữ liệu dữ liệu gì, lấy ở đâu, thu thập thế nào, có nhãn hay không nhãn? Phần 0 · nạp dữ liệu 2.2
2. Làm sạch dữ liệu có gì lẫn vào: script, file rỗng, lối tắt? so có cùng loại không? Phần 0 · 2.2
3. Chọn mô hình phù hợp với bài toán bài toán gì; quy trình hiện có (luật ngưỡng) làm được đến đâu? Phần 0, 1, 3
4. Coding đo entropy, vẽ file thành ảnh, huấn luyện cây quyết định Phần 2, 4, 5.1
5. Lưu ý khi coding lỗi nào làm sai mà không báo lỗi? 3.3 (mẫu nén bị đưa nhầm đường), 5.2 (kết quả quá đẹp trên dữ liệu dễ)
6. Đánh giá các mô hình luật / cây đúng đến đâu trên mẫu thật và ca thật? 3.1, 5.2, Phần 6
7. Quy tắc bàn tay phải ✋** từ kết quả đến hành động, mang gì theo khi đi làm Phần 7

Thực tế các bước lặp lại, không đi một mạch: ví dụ quy trình (bước 3) được nêu ở Phần 1, nhưng phải đo entropy (bước 4) rồi mới chạy được luật ở Phần 3.

📌 Tra cứu nhanh: Chọn mô hình nào? Khi nào dùng gì, đánh đổi gì — mục Trước khi bắt đầu, ngay dưới đây.

📖 Thuật ngữ dùng trong bài

Thuật ngữ Nghĩa Ở bài này
mẫu (sample) một file nghi là mã độc mỗi mẫu một hàng trong CSV
byte đơn vị nhỏ nhất của file, giá trị từ 0 đến 255 một chương trình như ls có hàng trăm nghìn byte
entropy độ "hỗn loạn" của các byte: 0 = toàn một giá trị lặp lại, 8 = 256 giá trị xuất hiện đều nhau như ngẫu nhiên Phần 2
ELF định dạng file chương trình của Linux (4 byte đầu là \x7fELF) phần lớn mẫu là ELF (đếm ở 2.2)
nén / packed file chương trình bị nén lại, khi chạy mới tự giải nén ra bộ nhớ → nhìn từ ngoài không đọc được mã bên trong Phần 3
UPX công cụ nén chương trình phổ biến nhất, để lại dấu UPX! trong file cột has_upx
phân tích tĩnh đọc file mà không chạy (strings, Ghidra) G3B
phân tích động chạy mẫu trong máy ảo cách ly, ghi lại nó làm gì G4
IOC dấu hiệu nhận biết bị nhiễm: tên file, đường dẫn, cổng, tên miền… Phần 6
sha12 12 ký tự đầu của mã băm SHA-256 — "chứng minh thư" của một file f14937a19cf6

Trước khi bắt đầu — Chọn mô hình nào? Khi nào dùng gì, đánh đổi gì

(Đọc thêm — để tra lại khi cần. Lần đầu có thể đọc lướt, chạy xong bài rồi quay lại.)

Không có mô hình "tốt nhất" cho mọi bài toán (No Free Lunch). Chọn mô hình = trả lời lần lượt 4 câu hỏi:

① Có đáp án (nhãn) không?

  • Có nhãn từ quá khứ (email đã biết sạch / lừa đảo, file đã biết là mã độc) → học có giám sát: Naïve Bayes, Logistic Regression, cây quyết định, Random Forest, XGBoost, SVM.
  • Không có nhãn, hoặc phải tìm cái chưa từng thấy (zero-day) → học không giám sát: Isolation Forest tìm điểm lạ, K-Means gom nhóm hành vi (ví dụ UBA: tách bot tự động khỏi thao tác của người mà không cần ai gắn nhãn). ⚠️ Mô hình không giám sát chỉ nói "khác đám đông", không nói "xấu". Trên honeypot, đám đông chính là kẻ tấn công (Lab 3, 5.0).
  • Có rất ít nhãn → bắt đầu bằng luật + không giám sát, rồi gom nhãn dần từ các cảnh báo đã được người phân tích xác nhận.

② Dữ liệu dạng gì?

Dạng dữ liệu Thường dùng Trong khoá
Bảng số (mỗi đối tượng = một hàng số) mô hình cây: cây quyết định, Random Forest, XGBoost · Logistic Regression làm mốc so sánh Lab 3 (mỗi IP 7 số), M5, Lab 7 (hồ sơ hành vi), M8–M10
Văn bản (email, tin nhắn, log dạng chữ) Naïve Bayes / Logistic Regression trên số đếm từ (TF-IDF) · mô hình ngôn ngữ (BERT, LLM) khi cần hiểu ngữ cảnh M1–M2 (Naïve Bayes), Lab 4 (Perceptron, SVM, Logistic Regression, Naive Bayes, Voting), Lab 10
Chuỗi theo thời gian (số kết nối mỗi giờ, CPU mỗi phút, chuỗi lời gọi API của một chương trình) phân rã chuỗi thời gian (xu hướng + nhịp ngày + phần dư) · HMM, LSTM khi cần học thứ tự sự kiện M4.2, Lab 6 Phần 3
Tệp nhị phân (mã độc) trích đặc trưng (kích thước, entropy…) rồi dùng mô hình cây · đổi file thành ảnh + CNN là hướng nghiên cứu, cần nhiều dữ liệu và GPU M5

③ Sợ bỏ lọt hay sợ báo nhầm?

  • Sợ bỏ lọt (mã độc, xâm nhập, lỗ hổng khi pentest) → ưu tiên recall. Sợ báo nhầm (chặn email khách hàng, khoá tài khoản nhân viên) → ưu tiên precision.
  • ⚠️ Hiểu lầm hay gặp: "muốn precision cao thì chọn Naïve Bayes / SVM". Thực ra gần như mọi mô hình đều ra một điểm / xác suất, và ngưỡng cắt điểm đó mới quyết định đánh đổi: hạ ngưỡng → bắt thêm nhưng báo nhầm thêm (Lab 3 9E.3; Lab 4 9.3). Chọn mô hình tốt rồi chọn ngưỡng theo sức đội SOC.
  • Xác suất của Naïve Bayes thường quá tự tin (sát 0 hoặc 1); SVM mặc định không ra xác suất → muốn đặt ngưỡng theo xác suất phải hiệu chỉnh (calibration) trước.
  • Tìm bất thường / gom nhóm: không có đáp án để tính precision, recall → dùng điểm bất thường, độ tách nhóm (Silhouette), và quan trọng nhất là người phân tích xem lại top N (Lab 3, 5.6).

④ Ràng buộc khi chạy thật

  • Giải thích được: cần báo cáo kiểm toán / ban giám đốc → hộp trắng (luật, cây quyết định nông, Logistic Regression, Naïve Bayes: đọc được nhánh IF/ELSE hoặc trọng số từ). Cần bắt kẻ tinh vi (APT, tấn công "chậm mà chắc") → chấp nhận hộp đen (Random Forest nhiều cây, XGBoost, deep learning) rồi giải trình bằng SHAP (Lab 3 Phần 7, Lab 6 4.2).
  • Tốc độ / phần cứng: router, IoT, máy trạm (RAM < 1 GB, CPU yếu) → luật, cây nông, Logistic Regression, Isolation Forest nhỏ. Trung tâm dữ liệu / cloud có GPU → deep learning, LLM.
  • Mất cân bằng lớp (tấn công hiếm): không mô hình nào tự xử lý — phải chỉnh class_weight hoặc ngưỡng. Lab 3 đo ở 9E: tấn công càng hiếm, mô hình mặc định càng bỏ lọt nhiều; class_weight="balanced" bắt thêm được, đổi lại báo nhầm tăng.
  • Bị né: mô hình dựa vào đặc trưng mà kẻ tấn công tự đổi được (tên phần mềm, tốc độ thử) thì dễ bị né (Lab 3 Phần 8, Lab 4 Phần 10).

Bảng tra nhanh: khi nào dùng gì, đánh đổi gì

Công cụ Dùng khi Được Phải chấp nhận Trong khoá
Luật / ngưỡng (IF/ELSE) kiểu tấn công đã biết rõ nhanh, không cần dữ liệu học, ai cũng hiểu chỉ bắt đúng cái đã viết; dễ bị né (thử chậm, Lab 3 1.2) M1, Lab 3 Phần 1
Naïve Bayes văn bản, ít dữ liệu, cần nhanh học rất nhanh, chạy được trên máy yếu, xem được từ nào "nặng ký" coi các từ độc lập với nhau; xác suất quá tự tin; bị lách bằng cách đổi từ M1–M2, Lab 4 Phần 7
Logistic Regression cần mốc so sánh đơn giản, cần giải thích bằng trọng số nhanh, ổn định, xác suất dùng được chỉ vẽ được ranh giới "thẳng"; quan hệ phức tạp thì kém Lab 4 Phần 6
SVM dữ liệu vừa, nhiều chiều (văn bản) tách tốt khi ít mẫu chậm khi dữ liệu lớn; mặc định không ra xác suất; khó giải thích Lab 4 Phần 4–5
Perceptron mốc đơn giản nhất: "cộng điểm từng đặc trưng" cực đơn giản, nhanh, tự tính tay được chỉ ranh giới thẳng; không ra xác suất; dễ bị lách Lab 4 Phần 3
Hội đồng (Voting) đã có vài mô hình khá, muốn bù chỗ sai cho nhau thường ổn định hơn từng mô hình chậm hơn, khó giải thích hơn; chia phiếu đều không tự động hơn mô hình giỏi nhất Lab 4 Phần 8
Cây quyết định cần luật đọc được cho người không chuyên ra đúng dạng IF/ELSE cây sâu dễ "học thuộc lòng" (overfit); dữ liệu đổi chút là cây đổi Lab M2 (M2.3), M5
Random Forest dữ liệu bảng có nhãn, cần ổn định chính xác, ít phải tinh chỉnh, ít overfit hơn một cây khó đọc từng quyết định (cần SHAP / độ quan trọng đặc trưng); nặng hơn một cây Lab 3 Phần 9, M8–M10
XGBoost dữ liệu bảng lớn, cần chính xác nhất thường đứng đầu trên dữ liệu bảng; chạy được GPU nhiều tham số phải chỉnh, chỉnh ẩu dễ overfit; khó giải thích M5, Lab 10 5.1
Isolation Forest không có nhãn, tìm cái lạ không cần ai gắn nhãn; ra thứ hạng ưu tiên "lạ" ≠ "xấu"; dữ liệu bẩn làm lệch (Lab 3 Phần 6); phải chọn contamination nếu dùng predict (Lab 3, 5.6) Lab 3 Phần 5, Lab 6 Phần 4, Lab 7 Phần 7
K-Means gom nhóm hành vi (người dùng, IP) thấy được "các kiểu" hành vi phải chọn trước số nhóm k; phải chuẩn hoá; chỉ hợp với nhóm "tròn", cỡ gần nhau Lab 6 Phần 6, Lab 7
Phân rã chuỗi thời gian số liệu theo giờ / ngày có nhịp lặp tách nhịp bình thường khỏi đột biến cần chuỗi đủ dài; nhịp thay đổi thì phải tính lại M4.2, Lab 6 Phần 3
Deep learning (LSTM, CNN) dữ liệu rất lớn, dạng chuỗi / ảnh học được mẫu phức tạp cần rất nhiều dữ liệu và GPU; hộp đen Lab 10 5.1 (mạng nơ-ron MLP nhỏ)
LLM đọc, tóm tắt, giải thích log / cảnh báo viết báo cáo nhanh có thể bịa; tốn tiền; không để LLM tự quyết định chặn Lab 3 Phần 10

Bài này dùng gì? dữ liệu bảng số (mỗi mẫu một hàng: entropy, kích thước…) → mốc là luật ngưỡng của quy trình, mô hình là cây quyết định (hộp trắng: đọc được ngưỡng nó tự chọn). Không có nhãn "độc / sạch" do người gắn: nhãn theo nguồn gốc file (Phần 0).

Tóm lại: chọn mô hình là bài toán cân đối (thiết kế cả hệ thống, không chỉ chọn một thuật toán): biết mình có dữ liệu gì (có nhãn không, dạng bảng / chữ / chuỗi) và cần tối ưu điều gì (bỏ lọt hay báo nhầm, giải thích hay tốc độ). Hệ thống thật hầu như luôn kết hợp nhiều lớp (luật + không giám sát + có giám sát) — quy tắc bàn tay ở Lab 3 Phần 11.

📌 Pipeline xử lý luôn là 7 bước (chi tiết từng bước ở bảng đầu bài):

  1. Thu thập dữ liệu — có nhãn hay không nhãn?
  2. Làm sạch dữ liệu
  3. Chọn mô hình phù hợp với bài toán (4 câu hỏi ① – ④ ở trên)
  4. Coding
  5. Lưu ý khi coding — những lỗi không báo lỗi
  6. Đánh giá các mô hình
  7. Quy tắc bàn tay phải ✋

Phần 0 — Thu thập dữ liệu: dữ liệu gì, lấy ở đâu, có nhãn chưa, có cần làm sạch?

📍 Bước 1 + 2 + 3. Trước khi chọn mô hình, phải trả lời được 5 câu hỏi này về dữ liệu (dữ liệu được nạp và đếm ở 2.2):

Câu hỏi Trả lời cho dữ liệu bài này
Dữ liệu gì? bảng số, mỗi mẫu mã độc một hàng: sha12, kích thước, entropy, loại file, CPU nhắm tới, có dấu UPX không (/opt/csai/data/malware_meta/malware_meta.csv). Phía sạch: các chương trình trong /usr/bin, /usr/sbin của máy lớp (tự đo ở 2.2). File nén / "mã hoá" mẫu: tự tạo từ /usr/bin/ls (2.1)
Lấy ở đâu? các file kẻ tấn công tải lên honeypot SSH của mình sau khi đăng nhập được, lưu trong kho mẫu trên máy chủ giám sát
Thu thập thế nào? trên máy chủ giám sát, một chương trình chỉ đọc từng mẫu — không chạy, không giải nén, không sao chép đi đâu — và tính ra các con số. Chỉ các con số được đưa lên máy lớp
Có nhãn chưa? nhãn theo nguồn gốc: mọi mẫu bắt từ honeypot = mã độc; chương trình đi kèm hệ điều hành = sạch. Nhãn "đi đường nào" thì không có sẵn: luật của quy trình tự đặt (Phần 1, 3)
Có cần làm sạch? Có: kho mẫu lẫn cả script, file chữ, file rỗng → chỉ so chương trình Linux (ELF) với ELF (2.2); phía sạch bỏ lối tắt (symlink) để không đo trùng (2.2). Mẫu chỉ hiện bằng 12 ký tự đầu mã băm, không kèm IP hay địa chỉ của kẻ tấn công

Vậy đây là bài toán gì? Chọn đường phân tích cho một file từ vài con số → luật ngưỡng trên một đặc trưng (entropy), rồi xem cây quyết định có học lại được luật đó không. Không cần mô hình phức tạp: cần hiểu đặc trưng và lỗ hổng của luật.

👉 Hiểu: chưa trả lời được 5 câu này thì chưa nên viết dòng code mô hình nào.


Phần 1 — Quy trình phân tích mã độc V3.2: 5 cổng, mỗi cổng một câu hỏi

📍 Bước 3 — Chọn mô hình (mốc so sánh): quy trình mà đội phân tích đang dùng — mô hình nào cũng phải hơn nó hoặc giúp nó.

Khi honeypot bắt được một file lạ, đội phân tích không "mở ra xem" ngay. Mẫu đi qua 5 cổng theo thứ tự; cổng nào trả lời được thì dừng sớm:

Mẫu tới
  ├─ đã phân tích rồi? ............................. XONG
  ▼
G1  Thu thập     mã băm, loại file, MỌI file đi kèm (script cài đặt, script dọn dẹp, khoá SSH, cron), phiên đăng nhập
  ▼
G2  Tra cứu      cơ sở dữ liệu mã độc, danh sách đen, nguồn tình báo
  ├─ mã độc đã biết .................................. CHẶN + XONG
  ▼
G3A Phân loại nhanh  loại file → ENTROPY (quyết định đường đi) → cấu trúc ELF → dấu UPX
  ├─ entropy < 7,0 ......... THƯỜNG   → G3B: phân tích tĩnh đầy đủ (Ghidra)
  ├─ 7,0 – 7,5 ............. NÉN      → thử giải nén UPX → G3B
  └─ entropy > 7,5 ......... MÃ HOÁ   → G3B-ALT: bỏ qua strings + Ghidra, đọc các script đi kèm → G4 BẮT BUỘC
  ▼
G3B Phân tích tĩnh   đọc chuỗi ký tự, dịch ngược mã (Ghidra), kiểm tra file có chạy được không
  ▼
G4  Phân tích động   chạy trong máy ảo CÁCH LY hoàn toàn (không ra Internet, không vào mạng nội bộ), ghi lại mọi hành vi, xong thì xoá sạch
  ▼
G5  Báo cáo      IOC, bảng MITRE ATT&CK, luật YARA để quét các máy khác
Cổng Câu hỏi Ra gì
G1 Thu thập Chuyện gì đã xảy ra? Kẻ tấn công còn để lại gì? mã băm, loại file, các file đi kèm, phiên đăng nhập
G2 Tra cứu Thế giới đã biết mẫu này chưa? "đã biết → chặn" hoặc "mới → phân tích tiếp"
G3A Phân loại nhanh Có đọc được bên trong không, đọc bằng cách nào? THƯỜNG / NÉN / MÃ HOÁ → chọn đường
G3B Tĩnh Bên trong có gì (chuỗi, hàm, địa chỉ C2)? danh sách hàm, chuỗi đáng ngờ
G4 Động Khi chạy thì nó làm gì? hành vi thật: cài đặt, mở cổng, quét, gửi dữ liệu
G5 Báo cáo SOC phải làm gì tiếp? IOC, MITRE, YARA

Quy tắc vàng của quy trình: entropy là bước thứ 2 của G3A, trước khi đọc chuỗi ký tự. Lý do: với file bị nén / mã hoá, chuỗi ký tự đọc ra chỉ là rác — mất công mà còn dễ kết luận sai. Phần 2–3 kiểm tra quy tắc này trên dữ liệu thật.

💼 Dùng khi nào: viết quy trình xử lý sự cố (playbook) cho SOC: mỗi bước có câu hỏi rõ ràng và điểm dừng sớm, người mới cũng làm đúng thứ tự. ⚖️ Ảnh hưởng tới: thời gian phân tích mỗi mẫu — đi nhầm đường (đọc chuỗi của file mã hoá) là mất hàng giờ mà không ra gì.


Phần 2 — Entropy: đo độ "hỗn loạn" của một file

📍 Bước 4 — Coding: tính đặc trưng chính của bài (entropy); 2.2 nạp dữ liệu thật (bước 1 + 2).

2.1 Entropy là gì? Đo thử trên 6 loại dữ liệu

Entropy Shannon của một file: đếm xem mỗi giá trị byte (0…255) xuất hiện bao nhiêu lần, đổi thành tỉ lệ p, rồi tính H = −Σ p · log₂(p). Kết quả nằm trong khoảng 0 đến 8 (bit trên mỗi byte):

Entropy Nghĩa là So sánh đời thường
0 cả file chỉ có một giá trị byte lặp lại trang giấy chỉ viết đúng một chữ "A"
4 – 5,5 văn bản: chỉ dùng chữ cái, dấu cách — vài chục giá trị byte, có cái rất hay gặp trang sách: chữ "a", "n", dấu cách gặp liên tục, chữ "z" hiếm
khoảng 1 – 6,5 chương trình bình thường: mã máy + chuỗi + nhiều vùng byte 0 đệm (đo trên máy lớp ở 2.2)
gần 8 256 giá trị xuất hiện gần đều nhau — file nén hoặc mã hoá xúc xắc 256 mặt tung ngẫu nhiên

Vì sao file nén có entropy cao? Nén = bỏ hết phần lặp lại: cái gì đoán trước được thì đã bị bỏ, phần còn lại trông như ngẫu nhiên. Mã hoá tốt cũng vậy: không để lộ quy luật nào.

🎯 Mục tiêu: viết hàm entropy() và đo nó trên 6 loại dữ liệu: lặp lại, văn bản, chương trình, chương trình bị nén, chương trình bị "mã hoá", byte ngẫu nhiên 📥 Đầu vào: file /usr/bin/ls (chương trình liệt kê thư mục) và /etc/services (file chữ) trên máy lớp 📤 Đầu ra: hàm entropy (cả bài dùng) · ls_bytes (nội dung file ls) · mau (6 mẫu byte, Phần 4 dùng lại) · rng (máy sinh số ngẫu nhiên)

🧰 np.bincount(mang, minlength=256): đếm mỗi số nguyên 0, 1, 2… xuất hiện bao nhiêu lần trong mang (bắt buộc); minlength (tuỳ chọn) = độ dài tối thiểu của kết quả, ghi 256 để luôn đủ 256 ô kể cả giá trị không xuất hiện. np.frombuffer(b, dtype=np.uint8) = xem dãy byte như một mảng số 0…255. zlib.compress(du_lieu, 9): nén, mức 9 = nén mạnh nhất (tuỳ chọn, mặc định -1 ≈ mức 6).

In [ ]:
import glob
import os
import zlib
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
pd.set_option("display.width", 200)
pd.set_option("display.max_colwidth", 60)
RANDOM_STATE = 42                                  # "hạt giống" ngẫu nhiên: lần nào chạy cũng ra cùng kết quả
rng = np.random.default_rng(RANDOM_STATE)          # máy sinh số ngẫu nhiên có hạt giống cố định


def entropy(du_lieu):
    # Entropy Shannon của một dãy byte, đơn vị bit/byte (0 đến 8)
    if len(du_lieu) == 0:
        return 0.0
    so_lan = np.bincount(np.frombuffer(du_lieu, dtype=np.uint8), minlength=256)  # 256 ô: giá trị byte k xuất hiện bao nhiêu lần
    p = so_lan[so_lan > 0] / so_lan.sum()          # tỉ lệ của mỗi giá trị byte CÓ xuất hiện (bỏ ô 0 vì log2(0) không xác định)
    return max(0.0, float(-(p * np.log2(p)).sum()))  # H = -Σ p·log2(p); max(0, ...) để không in "-0.000"


with open("/usr/bin/ls", "rb") as f:               # "rb" = đọc dạng byte (không phải chữ)
    ls_bytes = f.read()
with open("/etc/services", "rb") as f:
    van_ban = f.read()
khoa = rng.integers(0, 256, len(ls_bytes), dtype=np.uint8)            # một "khoá" ngẫu nhiên dài bằng file ls
mau = {
    "1.000 chữ A giống nhau": b"A" * 1000,
    "văn bản (/etc/services)": van_ban,
    "chương trình /usr/bin/ls": ls_bytes,
    "ls bị NÉN (zlib)": zlib.compress(ls_bytes, 9),
    "ls bị 'MÃ HOÁ' (XOR khoá ngẫu nhiên)": (np.frombuffer(ls_bytes, dtype=np.uint8) ^ khoa).tobytes(),  # ^ = phép XOR từng byte
    "byte ngẫu nhiên": rng.integers(0, 256, len(ls_bytes), dtype=np.uint8).tobytes(),
}
# 📦 mau = dict: tên mẫu -> dãy byte
bang_mau = pd.DataFrame([{"mẫu": ten, "kích thước (byte)": len(b), "số giá trị byte khác nhau": len(set(b)),
                          "entropy": round(entropy(b), 3)} for ten, b in mau.items()])
print(bang_mau.to_string(index=False))
print("\n10 byte đầu của ls          :", ls_bytes[:10])
print("10 byte đầu của ls bị nén   :", mau["ls bị NÉN (zlib)"][:10])

Kết quả:

mẫu kích thước (byte) số giá trị byte khác nhau entropy
1.000 chữ A giống nhau 1.000 1 0,000
văn bản (/etc/services) 12.813 80 5,198
chương trình /usr/bin/ls 142.312 256 5,935
ls bị nén (zlib) 62.313 256 7,990
ls bị "mã hoá" (XOR) 142.312 256 7,999
byte ngẫu nhiên 142.312 256 7,999

10 byte đầu của ls: b'\x7fELF\x02\x01\x01\x00\x00\x00' · 10 byte đầu của ls bị nén: b'x\xda\xec}\t|\x14U\xf2\xff'

📦 entropy = hàm đo entropy · ls_bytes = 142.312 byte của /usr/bin/ls · mau = 6 mẫu byte ở bảng trên · rng = máy sinh số ngẫu nhiên.

👉 Hiểu: ls, ls bị nén và byte ngẫu nhiên đều dùng đủ 256 giá trị byte — khác nhau ở chỗ các giá trị xuất hiện đều hay lệch. Chương trình thường (5,9) có nhiều byte 0 và các mã lệnh hay gặp; nén / mã hoá xong thì gần 8, không phân biệt được với byte ngẫu nhiên. Nhìn 10 byte đầu: ls mở đầu bằng chữ ký \x7fELF, bản nén thì không còn gì đọc được.

💼 Dùng khi nào: phân loại nhanh một file lạ trước khi mở nó ra; phát hiện dữ liệu bị mã hoá hàng loạt (ransomware làm entropy file của người dùng nhảy lên gần 8).
⚖️ Ảnh hưởng tới: chọn công cụ phân tích — đọc chuỗi ký tự của file entropy ~8 chỉ ra rác.

2.2 Mã độc thật và chương trình sạch: entropy phân bố thế nào?

📍 Bước 1 + 2 — nạp dữ liệu, chỉ giữ cùng loại file (ELF), rồi so phân bố.

🎯 Mục tiêu: nạp số liệu của các mẫu mã độc thật; đo entropy của mọi chương trình ELF trên máy lớp (phía sạch); vẽ chung lên một biểu đồ 📥 Đầu vào: /opt/csai/data/malware_meta/malware_meta.csv · thư mục /usr/bin, /usr/sbin · hàm entropy (2.1) 📤 Đầu ra: mal (mọi mẫu) · mal_elf (chỉ mẫu ELF) · lanh (chương trình sạch trên máy lớp: tên, kích thước, entropy, has_upx)

Cột của malware_meta.csv: sha12 (12 ký tự đầu mã băm) · size_bytes (kích thước) · entropy · file_type (loại file, đoán từ vài byte đầu) · elf_arch (CPU mà chương trình nhắm tới: x86-64 cho máy chủ, ARM/MIPS cho camera, router…) · has_upx (1 = trong file có dấu UPX!).

🧰 glob.glob("/usr/bin/*"): danh sách mọi đường dẫn khớp mẫu * · os.path.islink(p) = là lối tắt (symlink) → bỏ, để không đo trùng · b[:4] == b"\x7fELF" = 4 byte đầu là "chữ ký" của chương trình Linux.

In [ ]:
mal = pd.read_csv("/opt/csai/data/malware_meta/malware_meta.csv")
print("mal:", mal.shape, "(hàng = mẫu, cột = thông tin)")
print(mal.head(3).to_string(index=False))
print("\nLoại file:", mal.file_type.value_counts().to_dict())
mal_elf = mal[mal.file_type == "ELF"].copy()          # chỉ giữ chương trình Linux (ELF) để so cùng loại với phía sạch
print("Kiến trúc CPU của mẫu ELF:", mal_elf.elf_arch.value_counts().to_dict())
print("Mẫu ELF có dấu UPX:", int(mal_elf.has_upx.sum()), "/", len(mal_elf))

# Phía SẠCH: mọi chương trình ELF trên chính máy lớp (đi kèm hệ điều hành, coi là sạch)
hang = []
for duong_dan in sorted(glob.glob("/usr/bin/*") + glob.glob("/usr/sbin/*")):
    if os.path.islink(duong_dan) or not os.path.isfile(duong_dan):
        continue                                      # bỏ lối tắt và thư mục
    try:
        with open(duong_dan, "rb") as f:
            b = f.read()
    except OSError:
        continue                                      # file không có quyền đọc -> bỏ qua
    if b[:4] == b"\x7fELF":
        hang.append({"ten": duong_dan, "size_bytes": len(b), "entropy": round(entropy(b), 4), "has_upx": int(b"UPX!" in b)})
lanh = pd.DataFrame(hang)
# 📦 lanh = bảng chương trình sạch: ten, size_bytes, entropy, has_upx
print(f"\nlanh: {len(lanh)} chương trình ELF sạch, có dấu UPX: {int(lanh.has_upx.sum())}")
print(lanh.sort_values("entropy", ascending=False).head(3).to_string(index=False))

nhom = {"sạch (máy lớp)": lanh.entropy, "mã độc KHÔNG UPX": mal_elf[mal_elf.has_upx == 0].entropy,
        "mã độc CÓ UPX": mal_elf[mal_elf.has_upx == 1].entropy}
print("\n" + pd.DataFrame({ten: s.describe()[["count", "min", "50%", "max"]] for ten, s in nhom.items()}).round(3).to_string())

Kết quả: mal: (219, 6) · loại file ELF 205 · shell-script 10 · text 3 · empty 1 · CPU x86-64 150 · x86 17 · AArch64 16 · ARM 12 · other 10 · Mẫu ELF có dấu UPX: 74 / 205 · lanh: 653 chương trình ELF sạch, có dấu UPX: 0

sạch (máy lớp) mã độc KHÔNG UPX mã độc CÓ UPX
số file 653 131 74
entropy thấp nhất 1,299 5,564 7,989
trung vị 4,831 6,247 8,000
cao nhất 6,531 (x86_64-linux-gnu-lto-dump-13) 6,420 8,000

📦 mal = 219 mẫu × 6 cột · mal_elf = 205 mẫu ELF · lanh = 653 chương trình sạch (ten, size_bytes, entropy, has_upx).

👉 Hiểu: mã độc không chỉ nhắm máy chủ x86-64: 38/205 mẫu nhắm chip ARM, AArch64 hoặc loại khác (thiết bị nhỏ như camera, router), 17 mẫu nhắm x86 32-bit. 36% mẫu ELF (74/205) bị nén UPX, trong khi không chương trình sạch nào của máy lớp bị nén. Mã độc không nén (6,25) có entropy ngang chương trình sạch: chương trình sạch cao nhất là 6,53 — cao hơn cả mã độc không nén cao nhất.

Cách đọc biểu đồ dưới đây:

Thành phần Nghĩa
trục ngang entropy của file (0 → 8)
trục dọc số file có entropy trong khoảng đó
màu xanh / cam / đỏ chương trình sạch / mã độc không UPX / mã độc có UPX
2 vạch đứt ngưỡng 7,0 và 7,5 của quy trình (Phần 3)
In [ ]:
fig, ax = plt.subplots(figsize=(11, 4))
khoang = np.arange(1.0, 8.05, 0.1)                     # các "thùng" rộng 0,1
for (ten, s), mau_ve in zip(nhom.items(), ["tab:blue", "tab:orange", "tab:red"]):
    ax.hist(s, bins=khoang, alpha=0.6, label=f"{ten} ({len(s)})", color=mau_ve)
for x in (7.0, 7.5):
    ax.axvline(x, ls="--", color="gray")
ax.set_xlabel("entropy (bit/byte)")
ax.set_ylabel("số file")
ax.set_title("Entropy: chương trình sạch trên máy lớp vs 205 mã độc ELF thật")
ax.legend()
plt.show()

Kết quả: biểu đồ có 3 cụm: chương trình sạch (xanh) trải rộng 1,3 – 6,5 · mã độc không UPX (cam) dồn vào 5,6 – 6,4, nằm lẫn trong cụm sạch · mã độc UPX (đỏ) là một cột duy nhất ở 8,0. Giữa 6,53 và 7,99 không có file nào — 2 vạch ngưỡng 7,0 và 7,5 rơi đúng vào khoảng trống.

👉 Hiểu: entropy tách rất rõ "bị nén" và "không bị nén", nhưng không tách được "độc" và "sạch": cột cam nằm giữa đám xanh. Đó là lý do quy trình dùng entropy để chọn đường phân tích, không dùng để kết luận độc hay sạch (bài M5.2 đo kỹ điều này).

💼 Dùng khi nào: trước khi đặt một ngưỡng, vẽ phân bố của cả hai phía (cần bắt và không được bắt nhầm) trên dữ liệu thật.
⚖️ Ảnh hưởng tới: số file sạch bị báo nhầm nếu ai đó dùng luật entropy để chặn file.


Phần 3 — Luật chọn đường theo entropy, và lỗ hổng của chính quy trình

📍 Bước 3 + 6 — Chạy luật của quy trình trên mẫu thật, đánh giá nó đi đúng đường đến đâu.

3.1 Chạy luật chọn đường trên mã độc thật

Luật ở cổng G3A của quy trình V3.2: entropy < 7,0 → THƯỜNG (phân tích tĩnh đầy đủ) · 7,0 – 7,5 → NÉN (thử giải nén UPX rồi phân tích) · > 7,5 → MÃ HOÁ (bỏ qua đọc chuỗi, đọc script đi kèm, bắt buộc chạy thử).

🎯 Mục tiêu: viết luật thành hàm chon_duong, chạy trên 205 mẫu ELF và trên các chương trình sạch, đếm mỗi đường bao nhiêu file 📥 Đầu vào: mal_elf, lanh (2.2) 📤 Đầu ra: hàm chon_duong · cột duong trong mal_elf và lanh

🧰 chon_duong(e, nguong_nen=7.0, nguong_ma_hoa=7.5): e bắt buộc (entropy của một file); 2 ngưỡng tuỳ chọn. .apply(ham) = chạy hàm trên từng giá trị của một cột. pd.crosstab(a, b) = bảng đếm chéo: mỗi ô là số file có giá trị a ở hàng và b ở cột.

Các lựa chọn ✏️ (đo thật):

NGUONG_NEN, NGUONG_MA_HOA mã độc: THƯỜNG / NÉN / MÃ HOÁ sạch: THƯỜNG / NÉN / MÃ HOÁ ghi chú
7.0, 7.5 (có sẵn, quy trình V3.2) 131 / 0 / 74 653 / 0 / 0
6.5, 7.5 (pipeline vẽ ảnh dùng) 131 / 0 / 74 652 / 1 / 0 x86_64-linux-gnu-lto-dump-13 (6,53) bị coi là "NÉN"
6.0, 7.0 5 / 126 / 74 603 / 50 / 0 ngưỡng quá thấp: 126 mẫu và 50 chương trình sạch bị coi là "NÉN"
5.5, 7.0 0 / 131 / 74 491 / 162 / 0 càng thấp càng tệ: mọi mẫu không UPX thành "NÉN"
7.2, 7.8 131 / 0 / 74 653 / 0 / 0 không đổi gì
In [ ]:
# ✏️ SỬA: 2 ngưỡng của luật chọn đường
NGUONG_NEN, NGUONG_MA_HOA = 7.0, 7.5


def chon_duong(e, nguong_nen=7.0, nguong_ma_hoa=7.5):
    # Luật G3A của quy trình V3.2: entropy -> đường phân tích
    if e < nguong_nen:
        return "1 THƯỜNG -> Ghidra"
    if e <= nguong_ma_hoa:
        return "2 NÉN -> thử giải nén UPX"
    return "3 MÃ HOÁ -> bỏ Ghidra, chạy thử"


mal_elf["duong"] = mal_elf.entropy.apply(lambda e: chon_duong(e, NGUONG_NEN, NGUONG_MA_HOA))   # lambda = hàm 1 dòng không tên
lanh["duong"] = lanh.entropy.apply(lambda e: chon_duong(e, NGUONG_NEN, NGUONG_MA_HOA))
print("Mã độc ELF (205):")
print(pd.crosstab(mal_elf.duong, mal_elf.has_upx.map({0: "không UPX", 1: "có UPX"})).to_string())
print("\nChương trình sạch trên máy lớp:")
print(lanh.duong.value_counts().sort_index().to_string())
print("\nFile sạch KHÔNG đi đường THƯỜNG:")
print(lanh[~lanh.duong.str.startswith("1")][["ten", "entropy"]].to_string(index=False) or "(không có)")

Kết quả:

đường mã độc có UPX mã độc không UPX chương trình sạch
1 THƯỜNG → Ghidra 0 131 653
2 NÉN → thử giải nén UPX 0 0 0
3 MÃ HOÁ → bỏ Ghidra, chạy thử 74 0 0

File sạch KHÔNG đi đường THƯỜNG: (không có)

📦 chon_duong = hàm luật G3A · cột duong trong mal_elf và lanh = đường mà luật chọn cho từng file.

👉 Hiểu: luật chia gọn 205 mẫu thành 2 đường, và đường giữa (NÉN) không có ai. Đổi ngưỡng trong khoảng trống (7,2 / 7,8) thì không đổi gì; hạ xuống 6,5 thì bắt đầu kéo nhầm 1 chương trình sạch; xuống 6,0 thì 126 mẫu và 50 chương trình sạch bị coi là "nén" (xem bảng lựa chọn).

💼 Dùng khi nào: viết luật phân luồng tự động (sandbox nào, ai xử lý) cho hàng trăm mẫu mỗi ngày.
⚖️ Ảnh hưởng tới: mỗi mẫu được phân tích bằng cách nào và tốn bao nhiêu thời gian của người phân tích.

3.2 Dải "NÉN" 7,0 – 7,5 dành cho cái gì? File nén một phần

Ở 3.1 không mẫu nào rơi vào dải 7,0 – 7,5. Dải này dành cho file nén phần lớn, chưa phải toàn bộ — ví dụ trình nén để lại một đoạn mã thường (đoạn tự giải nén) ở đầu file, hoặc chương trình chỉ nén phần dữ liệu. Entropy cả file là trung bình (theo số byte) của các đoạn. Để thấy rõ, ta đo entropy từng đoạn 1 KB dọc theo file (hồ sơ entropy).

🎯 Mục tiêu: tạo file "nén phần lớn" từ ls (giữ nguyên phần đầu, nén phần cuối), đo entropy cả file, vẽ hồ sơ entropy từng đoạn của 3 file 📥 Đầu vào: ls_bytes, mau (2.1) · hàm entropy 📤 Đầu ra: nen_mot_phan (byte của file nén một phần) · ho_so (hàm tính hồ sơ entropy)

🧰 ho_so(du_lieu, buoc=1024): du_lieu bắt buộc; buoc (tuỳ chọn) = độ dài mỗi đoạn, mặc định 1.024 byte. Đoạn nhỏ quá (≤ 256 byte) thì entropy bị thấp giả vì chưa đủ chỗ cho 256 giá trị.

Các lựa chọn ✏️ TI_LE_NEN (đo thật, entropy cả file → đường): 0.0 → 5,935 (chính là ls) THƯỜNG · 0.5 → 6,712 THƯỜNG (nén một nửa chưa đủ!) · 0.7 → 7,049 NÉN · 0.8 (có sẵn) → 7,181 NÉN · 0.9 → 7,447 NÉN · 0.95 → 7,790 MÃ HOÁ · 1.0 → 7,990 MÃ HOÁ.

Vì sao nén một nửa vẫn chỉ 6,71? Phần bị nén co lại (zlib nén ls còn 44%), nên trong file mới nó chiếm ít byte hơn phần để nguyên → kéo trung bình lên không nhiều.

In [ ]:
# ✏️ SỬA: tỉ lệ phần cuối file bị nén (0 = không nén, 1 = nén cả file)
TI_LE_NEN = 0.8
cat = int(len(ls_bytes) * (1 - TI_LE_NEN))                       # vị trí cắt: trước = để nguyên, sau = nén
nen_mot_phan = ls_bytes[:cat] + zlib.compress(ls_bytes[cat:], 9)
# 📦 nen_mot_phan = phần đầu ls để nguyên + phần cuối đã nén
print(f"File nén {TI_LE_NEN:.0%} phần cuối: {len(nen_mot_phan):,} byte, entropy CẢ FILE = {entropy(nen_mot_phan):.3f}"
      f" -> {chon_duong(entropy(nen_mot_phan))}")


def ho_so(du_lieu, buoc=1024):
    # entropy của từng đoạn 'buoc' byte liên tiếp
    return [entropy(du_lieu[i:i + buoc]) for i in range(0, len(du_lieu) - buoc + 1, buoc)]


fig, ax = plt.subplots(figsize=(11, 3.5))
for ten, b in [("ls (thường)", ls_bytes), (f"ls nén {TI_LE_NEN:.0%} phần cuối", nen_mot_phan), ("ls nén cả file", mau["ls bị NÉN (zlib)"])]:
    ax.plot(np.arange(len(ho_so(b))), ho_so(b), label=f"{ten}: cả file {entropy(b):.2f}")
ax.axhline(7.5, ls="--", color="gray")
ax.set_xlabel("vị trí trong file (đoạn 1 KB thứ ...)")
ax.set_ylabel("entropy của đoạn")
ax.set_ylim(0, 8.2)
ax.legend(loc="lower right")
ax.set_title("Hồ sơ entropy: entropy cả file là TRUNG BÌNH của các đoạn")
plt.show()

Kết quả: File nén 80% phần cuối: 80,843 byte, entropy CẢ FILE = 7.181 -> 2 NÉN -> thử giải nén UPX

Hồ sơ entropy: đường xanh (ls) lên xuống từ 0 (vùng toàn byte 0) đến khoảng 6 · đường cam (nén 80%) đi giống hệt đường xanh ở phần đầu, rồi nhảy lên ~7,8 và nằm phẳng ở phần đã nén · đường lục (nén cả file) phẳng ở ~7,8 từ đầu đến cuối (mỗi đoạn 1 KB chỉ có 1.024 byte nên không đạt hẳn 8).

📦 nen_mot_phan = phần đầu ls + phần cuối đã nén · ho_so = hàm tính entropy từng đoạn.

👉 Hiểu: dải 7,0 – 7,5 là dải của file nén phần lớn nhưng còn một phần thường — đúng kiểu file mà trình nén tạo ra (đoạn tự giải nén ở đầu + phần nén). Nhưng entropy cả file che mất chỗ bị nén: file nén một nửa vẫn chỉ 6,71 → luật gọi là "THƯỜNG". Hồ sơ entropy nhìn thấy ngay: có một vùng phẳng ~7,8.

💼 Dùng khi nào: khi entropy cả file "bình thường" mà công cụ vẫn không đọc được gì → vẽ hồ sơ entropy để tìm vùng bị nén / mã hoá giấu bên trong.
⚖️ Ảnh hưởng tới: bỏ sót mã độc giấu phần độc trong một vùng nén nhỏ của file trông "bình thường".

3.3 Lỗ hổng: mẫu UPX bị đưa thẳng sang "MÃ HOÁ", bỏ qua bước giải nén

📍 Bước 5 — Lưu ý: luật chạy trơn tru, không báo lỗi, nhưng đưa cả một nhóm mẫu vào sai đường.

Bước "thử giải nén UPX" trong quy trình chỉ chạy khi entropy ở dải 7,0 – 7,5 (3.1). Nhưng trên dữ liệu thật cả 74 mẫu có dấu UPX đều có entropy ≥ 7,98 (2.2) → đi đường MÃ HOÁ → không ai thử upx -d (lệnh giải nén UPX). Trong khi UPX là trình nén công khai: nếu giải nén được thì quay lại phân tích tĩnh đầy đủ, khỏi phải chạy thử mẫu.

Cải tiến: kiểm tra dấu UPX trước, không phụ thuộc entropy. Đây là đúng việc SOC nên làm với mọi luật: chạy luật trên dữ liệu thật để tìm chỗ luật viết sai.

🎯 Mục tiêu: viết luật chọn đường bản 2 (kiểm tra UPX trước), so số mẫu mỗi đường với bản 1 📥 Đầu vào: mal_elf (có cột duong từ 3.1) · hàm chon_duong 📤 Đầu ra: hàm chon_duong_v2 · cột duong_v2 trong mal_elf

In [ ]:
def chon_duong_v2(e, co_upx, nguong_nen=7.0, nguong_ma_hoa=7.5):
    # Bản 2: thấy dấu UPX thì THỬ GIẢI NÉN trước, bất kể entropy; không thấy thì dùng luật cũ
    if co_upx == 1:
        return "2 NÉN -> thử giải nén UPX"
    return chon_duong(e, nguong_nen, nguong_ma_hoa)


mal_elf["duong_v2"] = [chon_duong_v2(e, u) for e, u in zip(mal_elf.entropy, mal_elf.has_upx)]   # zip = đi song song 2 cột
so_sanh = pd.DataFrame({"luật V3.2": mal_elf.duong.value_counts(), "luật bản 2": mal_elf.duong_v2.value_counts()}).fillna(0).astype(int)
print(so_sanh.sort_index().to_string())
print("\nKiến trúc CPU của 74 mẫu UPX:", mal_elf[mal_elf.has_upx == 1].elf_arch.value_counts().to_dict())

Kết quả:

đường luật V3.2 luật bản 2
1 THƯỜNG → Ghidra 131 131
2 NÉN → thử giải nén UPX 0 74
3 MÃ HOÁ → bỏ Ghidra, chạy thử 74 0

Kiến trúc CPU của 74 mẫu UPX: x86-64 23 · x86 17 · AArch64 16 · ARM 9 · other 9

📦 chon_duong_v2 = luật bản 2 · cột duong_v2 trong mal_elf.

👉 Hiểu: chỉ thêm 1 dòng ("có dấu UPX thì thử giải nén trước"), 74 mẫu được thử giải nén thay vì bị đưa thẳng sang "chạy thử bắt buộc". Điều này càng quan trọng vì 51/74 mẫu UPX nhắm CPU không phải x86-64 (x86, ARM, AArch64, khác): máy ảo phân tích x86-64 không chạy được chúng (ca B ở Phần 6) → nếu không giải nén được thì gần như không phân tích được gì.

💼 Dùng khi nào: sau khi viết bất kỳ luật nào (cảnh báo, phân luồng, chặn): chạy nó trên dữ liệu thật và đếm xem có nhánh nào rỗng hay quá đông — dấu hiệu luật viết sai.
⚖️ Ảnh hưởng tới: 36% số mẫu ELF có được phân tích tĩnh đầy đủ hay không.


Phần 4 — Biến file thành ảnh: nhìn tận mắt "có kết cấu" và "nhiễu trắng"

📍 Bước 4 — Coding: một cách nhìn khác cho cùng dữ liệu.

4.1 Mỗi byte là một điểm ảnh xám

Một hướng nghiên cứu (Nataraj, 2011): đặt các byte của file thành hàng ngang, mỗi byte = một điểm ảnh xám (0 = đen, 255 = trắng), hết một hàng thì xuống hàng → được một ảnh xám. Chương trình thường có vùng rõ rệt (mã máy, bảng chuỗi, vùng toàn byte 0 = dải đen); file nén / mã hoá thì đều như nhiễu trắng trên TV. Mô hình nhận dạng ảnh (CNN) có thể học các "kết cấu" này — nhưng với file nén, không còn kết cấu nào để học: đó là lý do pipeline vẽ ảnh không vẽ file có entropy > 7,5.

Chiều rộng ảnh chọn theo kích thước file (bảng Nataraj 2011):

Kích thước file < 10 KB 10–30 KB 30–60 KB 60–100 KB 100–200 KB 200–500 KB 500 KB–1 MB > 1 MB
Chiều rộng (điểm ảnh) 32 64 128 256 384 512 768 1024

Cách đọc 3 ảnh dưới đây: mỗi ảnh là một file, đọc từ trên xuống như đọc sách. Dải đen = vùng toàn byte 0 · dải mịn, có hoa văn lặp = bảng dữ liệu / chuỗi · vùng lấm tấm đều khắp = mã máy hoặc dữ liệu nén. Tiêu đề mỗi ảnh ghi entropy và độ lệch histogram (4.2).

🎯 Mục tiêu: viết hàm anh_tu_bytes, vẽ ảnh của ls, ls bị nén, ls bị "mã hoá" 📥 Đầu vào: mau (2.1) · hàm entropy 📤 Đầu ra: hàm chieu_rong, anh_tu_bytes, do_lech_histogram (4.2 dùng lại)

🧰 np.pad(mang, (0, n)): thêm n số 0 vào cuối mảng (để chia hết cho chiều rộng) · .reshape(-1, w) = xếp mảng thành bảng w cột, số hàng (-1) tự tính · ax.imshow(anh, cmap="gray") = vẽ bảng số thành ảnh xám.

In [ ]:
BANG_NATARAJ = [(10_240, 32), (30_720, 64), (61_440, 128), (102_400, 256), (204_800, 384), (512_000, 512), (1_048_576, 768)]


def chieu_rong(kich_thuoc):
    # chiều rộng ảnh theo bảng Nataraj 2011: file càng lớn ảnh càng rộng
    for nguong, rong in BANG_NATARAJ:
        if kich_thuoc < nguong:
            return rong
    return 1024


def anh_tu_bytes(du_lieu):
    mang = np.frombuffer(du_lieu, dtype=np.uint8)
    w = chieu_rong(len(mang))
    mang = np.pad(mang, (0, (w - len(mang) % w) % w))   # đệm 0 cho tròn hàng cuối
    return mang.reshape(-1, w)                          # bảng (số hàng, w): mỗi ô = 1 byte = 1 điểm ảnh


def do_lech_histogram(du_lieu):
    # phương sai của 256 số đếm: byte phân bố càng ĐỀU (nhiễu) thì càng NHỎ
    return float(np.var(np.bincount(np.frombuffer(du_lieu, dtype=np.uint8), minlength=256)))


ba_file = ["chương trình /usr/bin/ls", "ls bị NÉN (zlib)", "ls bị 'MÃ HOÁ' (XOR khoá ngẫu nhiên)"]
fig, axes = plt.subplots(1, 3, figsize=(13, 6))
for ax, ten in zip(axes, ba_file):
    anh = anh_tu_bytes(mau[ten])
    ax.imshow(anh, cmap="gray", aspect="auto", interpolation="nearest")
    ax.set_title(f"{ten}\n{anh.shape[1]}×{anh.shape[0]} điểm · entropy {entropy(mau[ten]):.2f}\n"
                 f"độ lệch histogram {do_lech_histogram(mau[ten]):,.0f}", fontsize=9)
    ax.axis("off")
plt.tight_layout()
plt.show()

Kết quả: 3 ảnh: ls 384×371 điểm, entropy 5,93, độ lệch histogram 5.739.217 · ls bị nén 256×244 điểm, entropy 7,99, độ lệch 845 · ls bị "mã hoá" 384×371 điểm, entropy 8,00, độ lệch 553.

Ảnh ls: có các dải ngang rõ rệt — dải đen (vùng byte 0), dải sọc đều ở đầu (bảng tiêu đề, bảng ký hiệu), một vùng xám mịn gần cuối (bảng chuỗi ký tự), phần giữa lấm tấm (mã máy). Hai ảnh còn lại: nhiễu đều từ trên xuống dưới, không thấy cấu trúc nào; ảnh nén nhỏ hơn vì file nén chỉ còn 44%.

📦 chieu_rong, anh_tu_bytes, do_lech_histogram = 3 hàm của pipeline vẽ ảnh.

👉 Hiểu: cùng một chương trình, sau khi nén / mã hoá thì mọi "kết cấu" biến mất — mắt người và mô hình nhận dạng ảnh đều không còn gì để học. Độ lệch histogram giảm từ 5,7 triệu xuống vài trăm: con số này đo được đúng điều mắt thấy.

💼 Dùng khi nào: trình bày cho người không chuyên vì sao "file này không phân tích được" — một ảnh dễ hiểu hơn con số 7,99.
⚖️ Ảnh hưởng tới: có nên đầu tư mô hình nhận dạng ảnh cho mã độc không: với 36% mẫu bị nén, ảnh không mang thông tin.

4.2 (TUỲ CHỌN) Kiểm tra chất lượng ảnh: một ngưỡng phụ thuộc vào kích thước file

Pipeline vẽ ảnh có bước 3 "kiểm tra chất lượng": tính độ lệch histogram (phương sai của 256 số đếm byte); nếu < 800.000 thì coi ảnh là "gần như nhiễu" (NEAR-NOISE). Ý tưởng đúng: byte càng đều thì phương sai càng nhỏ. Nhưng phương sai của số đếm tăng theo bình phương kích thước file → cùng một ngưỡng cố định sẽ cư xử khác hẳn với file nhỏ và file lớn. Ta đo thử.

🎯 Mục tiêu: đo độ lệch histogram của đoạn đầu ls (có kết cấu) và byte ngẫu nhiên (nhiễu) ở nhiều kích thước, so với ngưỡng 800.000 📥 Đầu vào: ls_bytes, rng, hàm do_lech_histogram, entropy 📤 Đầu ra: bang_ql (bảng kết quả)

In [ ]:
NGUONG_NHIEU = 800_000      # ngưỡng của pipeline vẽ ảnh
hang = []
for kb in (10, 50, 140):
    doan = ls_bytes[:kb * 1024]                                               # đoạn đầu của ls: CÓ kết cấu
    hang.append({"file": f"{kb} KB đầu của ls", "entropy": round(entropy(doan), 2),
                 "độ lệch histogram": round(do_lech_histogram(doan)), "pipeline kết luận": "nhiễu" if do_lech_histogram(doan) < NGUONG_NHIEU else "có kết cấu"})
for mb in (1, 10, 30):
    ngau_nhien = rng.integers(0, 256, mb * 1024 * 1024, dtype=np.uint8).tobytes()   # nhiễu thật
    hang.append({"file": f"{mb} MB byte ngẫu nhiên", "entropy": round(entropy(ngau_nhien), 2),
                 "độ lệch histogram": round(do_lech_histogram(ngau_nhien)), "pipeline kết luận": "nhiễu" if do_lech_histogram(ngau_nhien) < NGUONG_NHIEU else "có kết cấu"})
bang_ql = pd.DataFrame(hang)
print(bang_ql.to_string(index=False))

Kết quả:

file entropy độ lệch histogram pipeline kết luận
10 KB đầu của ls 3,06 178.073 nhiễu ❌ (sai: đây là vùng có cấu trúc, entropy chỉ 3)
50 KB đầu của ls 5,43 1.210.872 có kết cấu
140 KB đầu của ls 5,93 5.739.217 có kết cấu
1 MB byte ngẫu nhiên 8,00 4.316 nhiễu
10 MB byte ngẫu nhiên 8,00 47.647 nhiễu
30 MB byte ngẫu nhiên 8,00 121.861 nhiễu

👉 Hiểu: độ lệch histogram tính trên số đếm, nên nó lớn lên theo kích thước file: file nhỏ có cấu trúc rõ (10 KB, entropy 3) vẫn bị kết luận "nhiễu"; nhiễu thật thì độ lệch tăng dần theo kích thước (4 nghìn → 122 nghìn), file đủ lớn sẽ vượt ngưỡng. Một ngưỡng cố định chỉ đúng cho một khoảng kích thước. Cách sửa: chia số đếm cho kích thước file trước khi tính (dùng tỉ lệ thay cho số đếm), hoặc dùng luôn entropy.

💼 Dùng khi nào: mỗi khi thấy một ngưỡng cố định áp lên một con số phụ thuộc kích thước (số byte, số kết nối, số dòng log) — kiểm tra ở file / ngày nhỏ và lớn.
⚖️ Ảnh hưởng tới: file nhỏ bị loại oan khỏi bước vẽ ảnh; nhiễu ở file rất lớn lọt qua.


Phần 5 — Cây quyết định học lại luật chọn đường

📍 Bước 4 — Coding (5.1) rồi Bước 5 + 6 — Lưu ý và đánh giá (5.2: kết quả quá đẹp chưa chắc đã tốt).

5.1 Cho cây tự tìm ngưỡng entropy

Luật 7,0 / 7,5 do người đặt. Cây quyết định (đã học ở M5) có tự tìm ra ngưỡng từ dữ liệu không? Để so được với luật, nhãn phải đúng là câu hỏi luật trả lời: "file có bị nén không?" → dùng cột has_upx làm nhãn. Đặc trưng: entropy và log10(kích thước).

🎯 Mục tiêu: huấn luyện cây quyết định đoán has_upx từ entropy + kích thước trên 205 mẫu ELF; in luật cây học được 📥 Đầu vào: mal_elf (2.2) 📤 Đầu ra: X, y (bảng đặc trưng và nhãn) · cay (cây đã học)

🧰 DecisionTreeClassifier(max_depth=None, random_state=None): mọi tham số tuỳ chọn; max_depth = số tầng câu hỏi tối đa (mặc định không giới hạn), random_state = hạt giống để kết quả lặp lại được. .fit(X, y) = học · .score(X, y) = tỉ lệ đoán đúng · export_text(cay, feature_names=[...]) = in cây thành các dòng IF/ELSE.

Các lựa chọn ✏️ DO_SAU (đo thật): 1, 2, 3 → cùng một cây entropy <= 7.20, đúng 100%: dữ liệu tách được chỉ bằng 1 câu hỏi nên cây không cần hỏi thêm.

In [ ]:
from sklearn.tree import DecisionTreeClassifier, export_text

# ✏️ SỬA: số tầng tối đa của cây
DO_SAU = 2
X = pd.DataFrame({"entropy": mal_elf.entropy, "log10_kich_thuoc": np.log10(mal_elf.size_bytes)})   # log10: 1 MB -> 6, 30 MB -> 7.5
y = mal_elf.has_upx
# 📦 X = 205 hàng × 2 cột số · y = 205 nhãn (1 = có UPX)
print("X:", X.shape, "| y: có UPX", int(y.sum()), "/ không UPX", int((y == 0).sum()))
print(f"Mốc 'đoán bừa': luôn đoán 'không UPX' đã đúng {(y == 0).mean():.1%}")
cay = DecisionTreeClassifier(max_depth=DO_SAU, random_state=RANDOM_STATE).fit(X, y)
print(f"Cây đoán đúng {cay.score(X, y):.1%} số mẫu")
print(export_text(cay, feature_names=list(X.columns)))

Kết quả: X: (205, 2) | y: có UPX 74 / không UPX 131 · Mốc 'đoán bừa': luôn đoán 'không UPX' đã đúng 63.9% · Cây đoán đúng 100.0% số mẫu

|--- entropy <= 7.20
|   |--- class: 0
|--- entropy >  7.20
|   |--- class: 1

📦 X = 205 hàng × 2 cột (entropy, log10_kich_thuoc) · y = 205 nhãn (1 = có UPX) · cay = cây đã học.

👉 Hiểu: cây chỉ cần một câu hỏi — entropy <= 7.20? — và không dùng tới kích thước. Nó tự tìm ra một ngưỡng nằm giữa 7,0 và 7,5 của quy trình. Cho DO_SAU = 1, 2 hay 3 đều ra đúng cây này.

💼 Dùng khi nào: kiểm tra một luật do chuyên gia đặt: cho cây học trên dữ liệu thật, xem nó chọn đặc trưng nào, ngưỡng nào.
⚖️ Ảnh hưởng tới: niềm tin vào luật — nhưng xem 5.2 trước khi kết luận.

5.2 Vì sao cây chọn đúng 7,20? Và vì sao "đúng 100%" chưa nói được gì

🎯 Mục tiêu: tìm khoảng trống giữa 2 nhóm; học lại cây khi thêm cả chương trình sạch 📥 Đầu vào: mal_elf, lanh, X, y 📤 Đầu ra: cay2 (cây học trên mã độc + chương trình sạch)

In [ ]:
cao_nhat_khong_upx = mal_elf[mal_elf.has_upx == 0].entropy.max()
thap_nhat_co_upx = mal_elf[mal_elf.has_upx == 1].entropy.min()
print(f"Mã độc KHÔNG UPX: entropy cao nhất = {cao_nhat_khong_upx:.4f}")
print(f"Mã độc CÓ UPX   : entropy thấp nhất = {thap_nhat_co_upx:.4f}")
print(f"Điểm giữa khoảng trống = {(cao_nhat_khong_upx + thap_nhat_co_upx) / 2:.4f}")
for nguong in (6.5, 7.0, 7.2, 7.5, 7.9):
    dung = ((mal_elf.entropy > nguong).astype(int) == mal_elf.has_upx).mean()
    print(f"  luật 'entropy > {nguong}' đoán đúng {dung:.1%}")

# Thêm cả chương trình sạch của máy lớp (đều không UPX)
X2 = pd.concat([X[["entropy"]], lanh[["entropy"]]], ignore_index=True)
y2 = pd.concat([y, lanh.has_upx], ignore_index=True)
cay2 = DecisionTreeClassifier(max_depth=1, random_state=RANDOM_STATE).fit(X2, y2)
print(f"\nThêm {len(lanh)} chương trình sạch -> cây mới:")
print(export_text(cay2, feature_names=["entropy"]))

Kết quả: Mã độc KHÔNG UPX: entropy cao nhất = 6.4205 · Mã độc CÓ UPX: entropy thấp nhất = 7.9892 · Điểm giữa khoảng trống = 7.2049 · luật entropy > 6.5 / 7.0 / 7.2 / 7.5 / 7.9 đều đúng 100,0% · thêm 653 chương trình sạch → cây mới: entropy <= 7.26

📦 cay2 = cây học trên 205 mã độc + 653 chương trình sạch.

👉 Hiểu: 7,20 không phải ngưỡng "đúng" mà cây khám phá ra — nó chỉ là điểm giữa của khoảng trống 6,42 → 7,99 (cây quyết định luôn đặt ngưỡng ở giữa hai giá trị kề nhau). Mọi ngưỡng trong khoảng trống đều đúng 100%, nên dữ liệu này không thể cho biết nên chọn 7,0 hay 7,5. Thêm dữ liệu sạch (cao nhất 6,53) chỉ dịch điểm giữa lên 7,26. Và "đúng 100%" ở đây gần như hiển nhiên: nén gây ra entropy cao, nên nhãn "có UPX" và đặc trưng "entropy" là hai mặt của cùng một sự việc — câu hỏi khó thật sự ("độc hay sạch?") thì entropy không trả lời được (2.2).

💼 Dùng khi nào: mỗi khi mô hình đạt 100%: hỏi "nhãn có phải là hệ quả trực tiếp của đặc trưng không?" và "có mẫu nào nằm gần ranh giới không?".
⚖️ Ảnh hưởng tới: đừng dùng con số 100% này để thuyết phục ai rằng ngưỡng của mình là tối ưu — cần thêm mẫu nén một phần (3.2) mới phân định được.


Phần 6 — Hai ca phân tích thật: một mẫu đọc được, một mẫu bị "khoá"

📍 Bước 6 — Đánh giá: quy trình chạy thế nào trên 2 mẫu thật.

Hai báo cáo dưới đây do máy phân tích của mình viết theo đúng quy trình V3.2 (tháng 5/2026). Bản trong bài đã lược bớt: bỏ địa chỉ IP kẻ tấn công, bỏ địa chỉ kênh điều khiển (C2) kèm mã truy cập, bỏ đường dẫn nội bộ; mã băm chỉ giữ 12 ký tự đầu.

6.1 Hai mẫu này nằm ở đâu trong dữ liệu?

🎯 Mục tiêu: tìm 2 mẫu trong mal_elf và cho 2 luật chọn đường (3.1, 3.3) chọn đường cho chúng 📥 Đầu vào: mal_elf · hàm chon_duong, chon_duong_v2 📤 Đầu ra: hai_ca (2 hàng)

In [ ]:
hai_ca = mal_elf[mal_elf.sha12.isin(["94f2e4d8d443", "f14937a19cf6"])].copy()
hai_ca["MB"] = (hai_ca.size_bytes / 1e6).round(1)
print(hai_ca[["sha12", "MB", "entropy", "elf_arch", "has_upx", "duong", "duong_v2"]].to_string(index=False))

Kết quả:

sha12 MB entropy CPU has_upx luật V3.2 luật bản 2
94f2e4d8d443 30,3 6,3601 x86-64 0 1 THƯỜNG → Ghidra 1 THƯỜNG → Ghidra
f14937a19cf6 1,1 7,9998 ARM 1 3 MÃ HOÁ → bỏ Ghidra, chạy thử 2 NÉN → thử giải nén UPX

📦 hai_ca = 2 hàng của 2 mẫu này trong mal_elf.

👉 Hiểu: số đo của notebook khớp với báo cáo (6,36 và 8,00). Luật V3.2 đưa ca B sang "chạy thử bắt buộc" — nhưng ca B là chip ARM, máy ảo x86-64 không chạy được → đó là lý do báo cáo phải bỏ qua G4. Luật bản 2 thì cho thử giải nén UPX trước (và ở ca B, kẻ tấn công đã sửa phần đầu UPX để lệnh chuẩn thất bại — đọc tiếp 6.3).

6.2 Ca A — 94f2e4d8: backdoor viết bằng Go, đi đủ G1 → G4

Cổng Kết quả (trích báo cáo)
G1 Kẻ tấn công đăng nhập honeypot bằng root / mật khẩu yếu, tải lên file 30,3 MB đặt tên sshd (giả làm dịch vụ SSH) trong thư mục ẩn tên là một dãy số, rồi gõ chmod +x … ; nohup … & (cho chạy ngầm). Honeypot chặn lệnh chạy. Phần mềm SSH của kẻ tấn công khai SSH-2.0-Go.
G2 Thuộc một chiến dịch đã theo dõi (backdoor Go + PAM). 9 bản trước của chiến dịch đều là file bị cắt cụt ở các kích thước khác nhau và không chạy được (báo cáo đặt giả thuyết: mỗi lần cắt một kiểu → mã băm khác → lách phần mềm diệt virus); đây là bản đầy đủ đầu tiên.
G3A ELF x86-64, entropy 6,36 → THƯỜNG → phân tích tĩnh đầy đủ. Có 35 section (các bản bị cắt có 0).
G3B Ghidra khôi phục 5.980 hàm. Đáng ngờ: hàm cbPAMConv (móc vào PAM — bộ phận kiểm tra mật khẩu của Linux — để đánh cắp mật khẩu mỗi lần ai đó đăng nhập SSH), các hàm đổi quyền người dùng, chuỗi cấu hình đào tiền ảo Xmrig_*, 7 địa chỉ web "hỏi IP công khai của tôi là gì", một địa chỉ webhook Discord làm kênh gửi dữ liệu ra ngoài.
G4 Chạy 60 giây trong máy ảo cách ly (kiểm tra 7/7: không ra được Internet, không vào được mạng nội bộ, tên miền Discord bị chuyển hướng vào hố đen). Hành vi đo được: nạp thư viện PAM · thử đọc khoá SSH riêng /root/.ssh/id_rsa · đọc /etc/os-release (dò hệ điều hành) · quét SSH 65.955 địa chỉ khác nhau trong 60 giây (~1.100 địa chỉ/giây, để lây lan) · khi có quyền root: tự chép mình thành /bin/systemd-worker và mở cổng 1919 làm cửa hậu. Không thấy đào tiền ảo (có lẽ cần cấu hình từ C2, mà C2 đã bị chặn). Xong: xoá mẫu, khôi phục máy ảo, tắt máy ảo.

Vì sao chạy G4 dù entropy chỉ 6,36? Quy trình chỉ bắt buộc G4 khi entropy > 7,5, nhưng khuyến nghị G4 khi thấy webhook Discord / Telegram hoặc hành vi chưa rõ sau G3B — ca này có cả hai.

MITRE ATT&CK (bảng tra kỹ thuật tấn công dùng chung toàn ngành):

Kỹ thuật Mã Bằng chứng
Sửa quy trình xác thực: PAM T1556.003 hàm cbPAMConv
Giả danh tiện ích hệ thống T1036.005 tên sshd, cài thành systemd-worker
Tạo dịch vụ systemd để tự chạy lại T1543.002 chuỗi systemd-worker.service
Chiếm tài nguyên để đào tiền ảo T1496 cấu hình Xmrig_*
Gửi dữ liệu ra ngoài qua dịch vụ web (Discord) T1567 địa chỉ webhook (đã che)
Lây lan qua SSH T1021.004 quét 65.955 địa chỉ / 60 giây

Luật YARA (đã lược phần webhook): quét được mọi máy khác để tìm đúng loại file này.

rule GoPAM_Backdoor {
    strings:
        $pam_fn      = "cbPAMConv" ascii
        $persistence = "systemd-worker.service" ascii
        $miner       = "Xmrig_poolAddr" ascii
        $cred        = "*main.credential" ascii
    condition:
        uint32(0) == 0x464c457f and 3 of them      // file ELF và có ít nhất 3/4 chuỗi
}

6.3 Ca B — f14937a1: RedTail cho ARM, bị "khoá" nên chỉ phân tích tĩnh được

Cổng Kết quả (trích báo cáo)
G1 File redtail.arm7 (1,1 MB) tải lên qua SFTP, kèm script clean.sh. Phiên chỉ tải lên, không gõ lệnh.
G2 Họ RedTail — mạng máy ma đào tiền ảo, nhắm thiết bị IoT chạy chip ARM.
G3A ELF ARM 32-bit, 0 section, entropy 8,00 → MÃ HOÁ. Có dấu UPX! nhưng phần đầu UPX đã bị sửa (2 trường phiên bản / định dạng bị đổi) → upx -d báo "không phải UPX", sửa tay 2 trường vẫn không giải nén được.
G3B-ALT Đọc chuỗi: 1.835 chuỗi, 0 chuỗi có ích (toàn rác nén). Thông tin thật lấy từ clean.sh: tắt và gỡ dịch vụ đào tiền ảo của đối thủ (c3pool_miner) · xoá khỏi crontab mọi dòng có wget/curl//tmp… (xoá dấu vết của kẻ khác) · chattr -ia (gỡ thuộc tính "không được sửa") · xoá sạch /tmp, /var/tmp, /dev/shm.
G4 Bỏ qua: máy ảo phân tích là x86-64, không chạy được chương trình ARM; họ RedTail đã được mô tả nhiều; mức đe doạ trung bình.

MITRE ATT&CK: đào tiền ảo T1496 · sửa cron T1053.003 · xoá file để xoá dấu vết T1070.004 · vô hiệu hoá phòng thủ T1562 (chattr -ia) · làm rối file T1027 (sửa phần đầu UPX).

Luật YARA không dùng được chuỗi (toàn rác nén) → dùng cấu trúc: file ELF 32-bit, CPU ARM, kích thước 0,9–1,2 MB, và đúng 8 byte UPX! + 2 trường bị sửa tại vị trí 0x98.

6.4 So hai ca

Ca A 94f2e4d8 Ca B f14937a1
Entropy · đường 6,36 · THƯỜNG 8,00 · MÃ HOÁ
Phân tích tĩnh đọc được 5.980 hàm, ra IOC và YARA theo chuỗi 0 chuỗi có ích; YARA theo cấu trúc
Thông tin chính đến từ chính file (Ghidra) + chạy thử (G4) file đi kèm (clean.sh) — vì vậy G1 phải thu mọi file của phiên
Chạy thử G4 có: thấy cài đặt, cửa hậu, lây lan không: sai kiến trúc CPU
Bài học entropy thấp không có nghĩa là vô hại — báo cáo xếp mẫu này mức CRITICAL kẻ tấn công cố ý sửa UPX để công cụ chuẩn bó tay → luật bản 2 (3.3) thử UPX trước vẫn đúng, nhưng phải chuẩn bị cả cách giải nén thủ công / chạy thử trên máy ảo ARM

Phần 7 — Từ kết quả phân tích đến hành động SOC

📍 Bước 7 — Quy tắc bàn tay phải: từ kết quả đến hành động.

Kết quả Hành động Ai / công cụ
IP kẻ tải mã độc lên chặn trên tường lửa, đưa vào danh sách chặn quản trị mạng
Luật YARA quét mọi máy chủ để tìm file cùng loại EDR / script quét
IOC trên máy (/bin/systemd-worker, dịch vụ systemd-worker, cổng 1919 đang mở, thư mục ẩn tên số) kiểm tra từng máy: ls -la /bin/systemd-worker · systemctl status systemd-worker · ss -tlnp | grep 1919 quản trị hệ thống
Địa chỉ C2 (webhook, tên miền) chặn ở DNS / proxy, theo dõi máy nào cố gọi tới SOC
Kỹ thuật MITRE đối chiếu với các luật giám sát đang có: kỹ thuật nào chưa có luật bắt? SOC
Mật khẩu yếu bị dùng (root / mật khẩu mặc định) tắt đăng nhập root qua SSH, bắt dùng khoá SSH quản trị hệ thống

Đánh đổi cần nhớ:

Lựa chọn Được Mất
Luật entropy (Phần 3) 1 dòng code, chạy ngay, không cần dữ liệu học chỉ nói "đọc được hay không", không nói "độc hay sạch"; luật viết sai thì cả loạt mẫu đi nhầm đường (3.3)
Mô hình học từ đặc trưng tĩnh (Phần 5, M5) tự tìm ngưỡng với file bị nén, đặc trưng tĩnh gần như chỉ còn entropy → mô hình chỉ học lại "có nén không"
Ảnh + CNN (Phần 4) học được kết cấu mà người khó mô tả vô dụng với file nén / mã hoá; ngưỡng kiểm tra chất lượng phải tính theo kích thước (4.2)
Phân tích tĩnh (G3B) an toàn (không chạy mẫu), ra nhiều IOC bó tay trước trình nén bị sửa (ca B)
Phân tích động (G4) thấy hành vi thật (ca A: cài đặt, cửa hậu, quét) cần máy ảo cách ly đúng kiến trúc CPU, mất thời gian, mẫu có thể "giả chết" khi biết đang trong máy ảo

👉 Đúc kết: entropy là cổng chọn đường, không phải máy phát hiện mã độc. Nó trả lời "đọc được bằng cách nào", và chính câu trả lời đó phải được kiểm tra lại trên dữ liệu thật (3.3). Hệ thống thật luôn kết hợp: luật nhanh để chọn đường → tĩnh để lấy IOC → động khi tĩnh bó tay → phát hiện theo hành vi (EDR) trên máy thật, vì kẻ tấn công luôn có cách làm file "trông bình thường".

Quy tắc bàn tay ✋ áp vào bài này

Xòe bàn tay, đếm từ ngón cái: 5 điều không được bỏ của mọi bài toán phát hiện tấn công bằng AI (đầy đủ ở Lab 3 Phần 11). Bài này đã làm đủ 5 điều:

Ngón Điều Câu hỏi phải tự trả lời Trong bài này Quên điều này thì…
👍 1. Ngón cái DỮ LIỆU sạch Có lẫn file khác loại không? So có cùng loại không? chỉ so chương trình Linux (ELF) với ELF, bỏ lối tắt (2.2) so lẫn script, file rỗng với chương trình → entropy nói sai
☝️ 2. Ngón trỏ ĐẶC TRƯNG đúng Đo cái gì của file? entropy (Phần 2), dấu UPX, nhìn file thành ảnh (Phần 4) tưởng entropy tách được "độc / sạch": nó chỉ tách "nén / không nén" (2.2)
✋ 3. Ngón giữa CHỌN CÔNG CỤ Luật hay mô hình? Cần phức tạp tới đâu? luật ngưỡng của quy trình (Phần 3) → cây quyết định học lại đúng một câu hỏi entropy <= 7.20 (5.1) dựng mô hình phức tạp cho bài chỉ cần một câu hỏi
💍 4. Ngón áp út ĐO trung thực "Đúng 100%" nghĩa là gì? Luật có đưa mẫu vào đúng đường không? mốc đoán bừa 63,9% (5.1) · mọi ngưỡng 6,5 – 7,9 đều đúng 100% vì có khoảng trống 6,42 – 7,99 (5.2) · 74 mẫu UPX bị đưa thẳng sang "mã hoá" (3.3) tưởng 7,20 là ngưỡng "đúng"; mẫu nén bỏ qua bước giải nén
🤙 5. Ngón út GIẢI THÍCH & CẢNH GIÁC Ca thật đi đường nào? Ai quyết định? 2 ca thật (Phần 6): ca B (ARM, có UPX) — luật bản 2 đưa sang giải nén thay vì bỏ Ghidra bỏ phân tích tĩnh khi chỉ cần giải nén trước

💬 Câu hỏi thảo luận

  1. (3.1) Vì sao không có mẫu mã độc nào rơi vào dải 7,0 – 7,5? Dải này dành cho loại file nào (3.2)?
  2. (3.3) Luật bản 2 thêm 1 dòng "có dấu UPX thì thử giải nén trước". Có cách nào kẻ tấn công né được luật bản 2 không? (gợi ý: ca B)
  3. (5.2) Cây đúng 100%. Vì sao điều đó không chứng minh ngưỡng 7,0 hay 7,5 là đúng?
  4. (6.4) Ca A có entropy "bình thường" mà lại nguy hiểm nhất. Vậy có nên dùng "entropy cao = mã độc" làm luật chặn file không? (xem thêm bài M5.2)
  5. (Phần 7) Trong hệ thống của bạn, nếu một máy chủ có file /bin/systemd-worker và cổng 1919 đang mở, bạn làm gì đầu tiên?