Files
labs/Notebooks-CSAI/Lab9_URL_Detection.ipynb

39 KiB

Module 9 — URL độc hại: luật regex đối đầu đặc trưng + Random Forest (và cách "tấn công" mô hình)

Bài thực hành Module 9 (Kiểm thử xâm nhập với AI). Chạy lần lượt (Shift + Enter), kernel csai. Mỗi ô là một bước nhỏ, chạy đâu hiểu đấy; chỗ # ✏️ SỬA: là đổi số rồi chạy lại.

Module 9 nói về dùng AI để kiểm thử/tấn công và phòng thủ lại. Bài này đi đủ một vòng: xây một bộ chặn URL bằng luật tay, rồi bằng học máy, đo xem học máy đạt bao nhiêu — và cuối cùng tự tay lừa chính mô hình đó để hiểu một con số rất cao có thể che giấu điều gì. (Bài kế — Lab 9B — làm lại trên log web THẬT của máy chủ để thấy đời thực khác hẳn.)

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

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

  1. Đặt được bài toán chặn URL độc hại: quyết định gì, sai thì mất gì (Phần 0).
  2. Đọc bộ sinh URL để biết dữ liệu từ đâu ra, và nhìn URL thô từng loại (Phần 1).
  3. Tách URL thành phần đo được bằng urlparse (Phần 2).
  4. Viết luật regex và thấy chỗ báo nhầm / bỏ lọt (Phần 3).
  5. Biến URL thành con số, huấn luyện Random Forest, đọc đặc trưng quan trọng (Phần 4, 5).
  6. Tự "tấn công" mô hình để thấy một con số rất cao che giấu điều gì (Phần 6).

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

  • Bài toán: mỗi URL → an toàn hay độc hại: phân loại 2 lớp; bỏ lọt nguy hiểm hơn báo nhầm.
  • Dữ liệu: chuỗi URL có nhãn (tự sinh, Phần 1) → phải biến thành bảng số trước.
  • Vì vậy chọn: mốc = luật regex; có nhãn + bảng số → Random Forest. Bảng ① ② ở mục Trước khi bắt đầu ngay dưới đây.
  • Xử lý gì trước: tách URL thành phần (host, đường dẫn) để đo được (Phần 2).
  • Thước đo: bắt được bao nhiêu URL độc, báo nhầm bao nhiêu — rồi thử tấn công xem con số đó có giữ được không.

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ì, từ đâu ra, có nhãn hay không nhãn? Phần 1 (5 câu hỏi, bộ sinh, URL thô)
2. Làm sạch dữ liệu ô trống, trùng; tách URL thành phần đo được Phần 2
3. Chọn mô hình phù hợp với bài toán bài toán gì; cách cũ (luật regex) làm được đến đâu? Phần 0, 3
4. Coding biến URL thành con số, huấn luyện Random Forest Phần 4
5. Lưu ý khi coding lỗi nào làm điểm sai mà không báo lỗi? Phần 4 (chia học / kiểm tra giữ đúng tỉ lệ)
6. Đánh giá các mô hình mô hình dựa vào đâu; lừa được không? Phần 4, 5, 6
7. Quy tắc bàn tay phải ✋** giải pháp, mang gì theo khi đi làm Phần 7

📌 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.

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 là chuỗi URL có nhãn → biến mỗi URL thành bảng số rồi dùng Random Forest; mốc là luật regex.

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 ✋
In [ ]:
import numpy as np, pandas as pd, matplotlib.pyplot as plt
import random, string, base64, math, re
from collections import Counter
from urllib.parse import urlparse
%matplotlib inline
pd.set_option("display.width", 170); pd.set_option("display.max_colwidth", 90)
print("sẵn sàng | pandas", pd.__version__)

Phần 0 — Đặt vấn đề

📍 Bước 3 (đặt bài toán): quyết định gì, sai thì mất gì.

Trung tâm an ninh (SOC) nhận hàng nghìn URL mỗi ngày (từ email, chat, log proxy) và phải quyết định mỗi URL: an toàn (cho qua) hay độc hại (chặn, cảnh báo).

  • Bỏ lọt (false negative): một URL lừa đảo được cho qua → nhân viên bấm vào, mất mật khẩu. Nguy hiểm.
  • Báo nhầm (false positive): một trang mua sắm thật bị chặn → phiền, mất việc, nhưng không mất an ninh.

Kẻ tấn công cố làm URL trông vô hại: dùng IP trần, tên miền giả (vietc0mbank), nhiều subdomain, rút gọn (bit.ly), mã hoá (%xx, base64). Câu hỏi: luật regex do người viết có theo kịp không, hay cần học máy? Và khi học máy báo chính xác gần như tuyệt đối — tin được tới đâu?


Phần 1 — Dữ liệu — tự sinh 5.000 URL, đọc bộ sinh để biết từ đâu ra

📍 Bước 1 — Thu thập dữ liệu.

5 câu hỏi về dữ liệu — trả lời trước khi viết dòng code nào:

Câu hỏi Trả lời cho dữ liệu bài này
Dữ liệu gì? chuỗi URL, mỗi URL một hàng + nhãn label (0 = an toàn, 1 = độc hại) + loại kind
Lấy ở đâu? tự sinh bằng bộ sinh ở Phần 1 (hạt giống cố định) — không có kho URL thật gán nhãn sẵn cho lớp
Thu thập thế nào? bộ sinh ghép tên miền và đường dẫn theo từng kiểu: sạch, lừa đảo lộ liễu, che giấu (rút gọn, %xx, base64)
Có nhãn chưa? có, do bộ sinh đặt → học có giám sát. Ngoài đời nhãn đến từ danh sách đen / người phân tích, và có nhiễu (Lab 9B)
Có cần làm sạch? không có ô trống / trùng; "làm sạch" ở đây là tách URL thành phần đo được (Phần 2)

Không có kho URL thật gán nhãn sẵn, nên ta tự sinh: 3.000 sạch + 1.250 phishing lộ liễu + 750 "che giấu". seed=42 để cả lớp giống nhau. (Bài 9B dùng log thật.) Nhãn: label 0 = an toàn, 1 = độc hại; kind = loại ẩn.

In [ ]:
# ===== BỘ SINH URL (giữ nguyên thuật toán của bộ kit; đọc để hiểu các kiểu URL độc) =====
BRANDS = ["paypal","microsoft","apple","vietcombank","google","facebook","amazon","zalo","shopee","netflix"]
GOOD_DOMAINS = ["shop-example.com","news-daily.vn","tintuc24h.vn","bookstore.org","travel-blog.net","university.edu.vn",
                "cooking-recipes.com","techreview.io","sportsnow.vn","community-forum.org"]
WORDS = ["login","secure","update","verify","account","billing","support","signin","confirm","wallet"]
TLDS  = [".com",".net",".xyz",".top",".info",".click"]

def rnd(rng, n, alphabet=string.ascii_lowercase+string.digits):
    return "".join(rng.choice(alphabet) for _ in range(n))            # chuỗi ngẫu nhiên n ký tự

def benign(rng):                                                       # URL thật: tên miền tốt, đường dẫn hợp lý, có thể có query dài
    d = rng.choice(GOOD_DOMAINS)
    path = "/".join(rng.choice(["products","category","article","2026","page","item","search"]) for _ in range(rng.randint(1,4)))
    q = ""
    if rng.random() < 0.6:
        q = "?" + "&".join("%s=%s" % (rng.choice(["id","ref","utm_source","sort","page","color","size"]), rnd(rng, rng.randint(2,10)))
                           for _ in range(rng.randint(1,6)))
    return "%s://www.%s/%s%s" % (rng.choice(["https","https","http"]), d, path, q)

def phishing(rng):                                                     # 4 chiêu lừa "lộ liễu"
    brand, w = rng.choice(BRANDS), rng.choice(WORDS)
    style = rng.randint(0,3)
    if style == 0:                                                     # IP trần làm host
        host = ".".join(str(rng.randint(1,254)) for _ in range(4))
        return "http://%s/%s/%s.php?user=%s" % (host, w, brand, rnd(rng,6))
    if style == 1:                                                     # rất nhiều subdomain
        return "http://%s.%s.%s.%s%s/%s" % (w, brand, "secure-"+rnd(rng,4), rnd(rng,7), rng.choice(TLDS), w)
    if style == 2:                                                     # giả chính tả: paypal -> paypa1
        typo = brand.replace("o","0").replace("l","1") if rng.random() < 0.5 else brand+"-"+w
        return "http://%s%s/%s-%s?session=%s" % (typo, rng.choice(TLDS), w, brand, rnd(rng,12))
    return "http://%s@%s%s/%s" % (brand+".com", rnd(rng,9), rng.choice(TLDS), w)   # chiêu user@host

def obfuscated(rng):                                                   # 4 chiêu "che giấu" (khó cho regex)
    brand, w = rng.choice(BRANDS), rng.choice(WORDS)
    style = rng.randint(0,3)
    if style == 0:                                                     # rút gọn: không đọc được gì
        return "https://%s/%s" % (rng.choice(["bit.ly","tinyurl.com","t.co","s.id"]), rnd(rng,7,string.ascii_letters+string.digits))
    if style == 1:                                                     # mã hoá %xx
        s = "/%s/%s" % (w, brand)
        return "http://%s%s/%s" % (rnd(rng,8), rng.choice(TLDS), "".join("%%%02x" % ord(c) for c in s))
    if style == 2:                                                     # nhúng base64 trong data URI
        payload = base64.b64encode(("<form action=http://%s%s>" % (rnd(rng,6), rng.choice(TLDS))).encode()).decode()
        return "data:text/html;base64," + payload
    return "https://www.%s/redirect?u=%s" % (rng.choice(GOOD_DOMAINS),   # trang thật + payload giấu trong tham số redirect
            base64.urlsafe_b64encode(("http://%s%s/%s" % (brand, rng.choice(TLDS), w)).encode()).decode().rstrip("="))

SEED = 42                                                              # ✏️ SỬA: đổi seed để ra bộ URL khác
rng = random.Random(SEED)
N = 5000                                                               # ✏️ SỬA: đổi số lượng URL
n_b, n_p = int(N*0.60), int(N*0.25); n_o = N - n_b - n_p
rows = [(benign(rng),0,"benign") for _ in range(n_b)] + [(phishing(rng),1,"phishing") for _ in range(n_p)]      + [(obfuscated(rng),1,"obfuscated") for _ in range(n_o)]
rng.shuffle(rows)
urls = pd.DataFrame(rows, columns=["url","label","kind"])              # 📦 urls = DataFrame 5000×3
print("📦 urls =", urls.shape, "| an toàn:", int((urls.label==0).sum()), "| độc hại:", int((urls.label==1).sum()))
print(urls.kind.value_counts().to_string())

Kết quả (seed 42): 5.000 URL = 3.000 benign + 1.250 phishing + 750 obfuscated (độc hại = 2.000 = 40%).

1.1 Xem URL thô từng loại

In [ ]:
for k in ["benign","phishing","obfuscated"]:
    print("### %s" % k)
    for u in urls[urls.kind==k].url.head(3):
        print("   ", u[:110])

👉 Hiểu: nhìn bằng mắt, URL benign dài nhưng có tên miền quen; phishing có IP trần / @ / paypa1; obfuscated thì không đọc được (bit.ly, %xx, base64). Chính loại "che giấu" này sẽ làm luật regex bó tay.


Phần 2 — Làm sạch / chuẩn hoá

📍 Bước 2 — Làm sạch / chuẩn hoá.

URL là chuỗi, không có ô trống như bảng số. "Làm sạch" ở đây là tách URL thành phần (giao thức, host, đường dẫn) để đo được. Dùng urlparse của Python.

In [ ]:
print("Ô trống trong dữ liệu:", int(urls.isna().sum().sum()), "| URL trùng:", int(urls.url.duplicated().sum()))
vd = urls.url.iloc[0]
p = urlparse(vd if "://" in vd else "http://"+vd)
print("\nVí dụ tách 1 URL:")
print("  url   :", vd[:90])
print("  host  :", p.hostname, "| path:", p.path[:50], "| query:", (p.query[:40] or "(trống)"))

👉 Hiểu: dữ liệu tự sinh không có ô trống, không trùng. urlparse cho ta hostname (để đếm subdomain, bắt IP) và path. Ở log thật (Lab 9B) còn phải bỏ query riêng tư, ẩn danh IP, xử lý dòng hỏng — nặng hơn nhiều.


Phần 3 — Thuật toán 1 — 4 luật regex (cách làm đầu tiên ai cũng nghĩ ra)

📍 Bước 3 — Chọn mô hình (mốc so sánh): cách cũ — luật regex — làm được đến đâu?

Luật người viết: chặn URL nếu có @, hoặc ≥ 3 dấu -, hoặc host là IP trần, hoặc dài > 75 ký tự.

In [ ]:
IP_RE = re.compile(r"^https?://\d{1,3}(\.\d{1,3}){3}")               # bắt đầu bằng http(s):// rồi 4 cụm số
def luat_regex(u):
    return bool("@" in u or u.count("-") >= 3 or IP_RE.match(u) or len(u) > 75)

urls["co_chan"] = urls.url.map(luat_regex)                             # 📦 co_chan = Series True/False cho từng URL
doc = urls.label == 1
tp = int((urls.co_chan & doc).sum());  fp = int((urls.co_chan & ~doc).sum())
fn = int((~urls.co_chan & doc).sum())
print("Luật regex trên 5000 URL:")
print("  bắt đúng độc hại : %d / %d  (recall %.1f%%)" % (tp, int(doc.sum()), 100*tp/int(doc.sum())))
print("  BỎ LỌT           : %d  <- kẻ tấn công đi thẳng qua" % fn)
print("  BÁO NHẦM         : %d  <- khách thật bị chặn (%.1f%% số URL an toàn)" % (fp, 100*fp/int((~doc).sum())))
print("\nTheo loại ẩn:")
print(urls.groupby("kind").co_chan.agg(flagged="sum", total="count").to_string())

Kết quả (seed 42): recall chỉ 36.4% (bỏ lọt 1.272 URL độc), đồng thời báo nhầm 1.185 URL sạch (39.5% số URL an toàn!). Theo loại: obfuscated lọt gần hết (chỉ chặn 65/750), còn benign bị chặn oan 1.185/3.000.

👉 Hiểu: luật "dài > 75" chặn nhầm hàng loạt URL mua sắm thật (query dài); còn bit.ly/%xx/base64 ngắn và không có @/-/IP nên lọt sạch. Kẻ tấn công đổi hình dạng URL nhanh hơn ta viết regex.


Phần 4 — Thuật toán 2 — biến URL thành 9 con số rồi cho Random Forest học

📍 Bước 4 — Coding (đặc trưng + Random Forest), Bước 5 — Lưu ý (chia giữ tỉ lệ) và Bước 6 — Đánh giá.

Ý tưởng: thay vì bắt từng ký tự bằng tay, đo mỗi URL thành vài con số rồi để máy tự tìm ranh giới. Thư viện: scikit-learn. Mỗi URL → 9 đặc trưng:

Đặc trưng Nghĩa
url_length độ dài
num_special_chars số ký tự @-_?=
entropy độ "hỗn loạn" ký tự (chuỗi mã hoá/ngẫu nhiên cao)
has_ip host có phải IP trần
num_subdomains số subdomain (dấu . trong host trừ 1)
is_shortener có phải dịch vụ rút gọn
has_percent_encoding có % (mã hoá hex)
is_data_uri có phải data:
digit_ratio tỉ lệ chữ số
In [ ]:
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, precision_score, recall_score, confusion_matrix
import time

IP_HOST = re.compile(r"^\d{1,3}(\.\d{1,3}){3}$")
SHORTENERS = {"bit.ly","tinyurl.com","t.co","s.id","goo.gl","ow.ly"}
def shannon(s):                                                        # entropy Shannon: đo độ hỗn loạn ký tự
    if not s: return 0.0
    c = Counter(s); return -sum((v/len(s))*math.log2(v/len(s)) for v in c.values())
def dac_trung(u):
    p = urlparse(u if "://" in u or u.startswith("data:") else "http://"+u)
    host = p.hostname or ""
    return {"url_length": len(u), "num_special_chars": sum(u.count(ch) for ch in "@-_?="),
            "entropy": round(shannon(u),4), "has_ip": int(bool(IP_HOST.match(host))),
            "num_subdomains": max(0, host.count(".")-1), "is_shortener": int(host in SHORTENERS),
            "has_percent_encoding": int("%" in u), "is_data_uri": int(u.startswith("data:")),
            "digit_ratio": round(sum(ch.isdigit() for ch in u)/max(1,len(u)),4)}

t0 = time.perf_counter()
X = pd.DataFrame([dac_trung(u) for u in urls.url])                      # 📦 X = bảng 5000×9 con số
y = urls.label.values
print("rút %d đặc trưng cho %d URL trong %.2f giây" % (X.shape[1], len(X), time.perf_counter()-t0))
X.head(3)
In [ ]:
# chia 80% học / 20% kiểm tra, giữ đúng tỉ lệ độc hại (stratify)
Xtr, Xte, ytr, yte, ktr, kte = train_test_split(X, y, urls.kind.values, test_size=0.2, stratify=y, random_state=42)
rf = RandomForestClassifier(n_estimators=200, n_jobs=2, random_state=42)   # ✏️ SỬA: thử n_estimators=20 hoặc 500
rf.fit(Xtr, ytr)
pred = rf.predict(Xte)
tn, fp, fn, tp = confusion_matrix(yte, pred, labels=[0,1]).ravel()
print("RANDOM FOREST trên %d URL kiểm tra:" % len(Xte))
print("  acc=%.3f  precision=%.3f  recall=%.3f | báo nhầm=%d  bỏ lọt=%d" % (
      accuracy_score(yte,pred), precision_score(yte,pred), recall_score(yte,pred), fp, fn))
for k in ["phishing","obfuscated"]:
    m = kte==k; print("  %-10s bắt %d/%d" % (k, int(((pred==1)&m).sum()), int(m.sum())))

Kết quả (seed 42): acc 0.999, precision 1.000, recall 0.998 — chỉ bỏ lọt 1, báo nhầm 0. phishing bắt 235/235, obfuscated 164/165. So với regex (recall 36.4%, báo nhầm 39.5%) là một trời một vực.

👉 Hiểu: máy tìm được rằng "che giấu" cũng để lại dấu đo được — entropy cao, nhiều subdomain, có %. Điều regex làm không nổi vì ta không nghĩ ra hết luật.


Phần 5 — Đặc trưng nào quyết định?

📍 Bước 6 — Đánh giá các mô hình: mô hình dựa vào đặc trưng nào?

In [ ]:
tqt = pd.Series(rf.feature_importances_, index=X.columns).sort_values()
ax = tqt.plot.barh(figsize=(8,4), color="tab:green"); ax.set_xlabel("độ quan trọng (tổng=1)")
ax.set_title("URL: đặc trưng nào tố cáo độc hại")
for i,v in enumerate(tqt): ax.text(v+.004, i, "%.3f"%v, va="center", fontsize=9)
plt.tight_layout(); plt.show()
print(tqt.sort_values(ascending=False).round(3).to_string())

Kết quả (seed 42): num_subdomains 0.514 — mô hình dựa hơn một nửa vào số subdomain, rồi tới số ký tự đặc biệt và entropy. Hãy nhớ con số 0.514 này — Phần 6 sẽ cho thấy nó là điểm yếu.


Phần 6 — "Tấn công" mô hình — vì sao 99.9% dễ gây lầm tưởng (AI pentesting)

📍 Bước 6 — Đánh giá (thử tấn công): con số rất cao có giữ được khi bị lừa không?

Mô hình 99.9% nghe như xong việc. Nhưng nó học từ dữ liệu tự sinh, nơi phishing luôn có nhiều subdomain còn trang thật thì không. Kẻ tấn công biết điều đó sẽ cố tình đổi đúng đặc trưng mô hình tin nhất. Thử tự tay:

In [ ]:
def diem(u):
    x = pd.DataFrame([dac_trung(u)])
    return rf.predict(x)[0], rf.predict_proba(x)[0,1]            # (0/1, xác suất độc hại)

# ✏️ SỬA: thử các URL của riêng anh/chị
thu = [
    ("trang thật + thêm nhiều subdomain",       "https://www.a.b.c.d.shop-example.com/products"),
    ("giả trang ngân hàng, trông hợp lý",       "https://login.vietcombank.com.vn/"),
    ("phishing IP trần (giống dữ liệu học)",    "http://185.23.11.9/login/paypal.php?user=abc123"),
]
for ten, u in thu:
    nhan, xs = diem(u)
    print("%-38s -> %-8s (xác suất độc %.2f) | %s" % (ten, "ĐỘC HẠI" if nhan else "AN TOÀN", xs, u[:55]))

👉 Hiểu (lệ thuộc kết quả ô trên):

  • URL 1 là trang thật nhưng thêm nhiều subdomain → mô hình gọi ĐỘC HẠI: báo nhầm, vì nó tin num_subdomains tới 51%.
  • URL 2 là kiểu giả trang ngân hàng (login.vietcombank.com.vn) nhưng trông hợp lý (https, ngắn, cấu trúc .com.vn) → mô hình gọi AN TOÀN: bỏ lọt, vì nó không giống hình dạng phishing trong dữ liệu học (vốn luôn có IP trần / rất nhiều subdomain / lỗi chính tả).
  • URL 3 giống hệt dữ liệu học → bắt đúng.

Cả hai kiểu sai đều có thể dựng ra chỉ bằng cách nắn đúng đặc trưng mô hình tin nhất — không cần phá mô hình. Đó là tấn công đối kháng (adversarial). Con số 99.9% chỉ đúng với phân bố dữ liệu lúc học; kẻ tấn công thật không chơi theo phân bố đó.

Phòng thủ (Blue Team): đừng để mô hình phụ thuộc gần hết vào một đặc trưng; thêm đặc trưng khó giả (tuổi tên miền, danh tiếng IP); theo dõi phân bố đầu vào để phát hiện khi có người đang "dò" mô hình; và kiểm thử mô hình bằng URL đối kháng trước khi tin nó.


Phần 7 — Giải pháp & kết luận

📍 Bước 7 — Quy tắc bàn tay phải: giải pháp và kết luận.

Luật regex Random Forest (9 đặc trưng)
Bắt độc hại (recall) 36.4% 99.8%
Báo nhầm khách thật 39.5% 0%
Bắt loại "che giấu" 65/750 164/165
Hiểu/giải thích dễ xem độ quan trọng
Bị lừa khi đổi hình dạng URL rất dễ cũng bị, nếu chỉ dựa 1 đặc trưng (Phần 6)

Giải pháp: dùng ML làm lớp chính (bao phủ rộng hơn regex nhiều), nhưng không tin mù con số 99.9%: nó đo trên dữ liệu tự sinh. Luôn kiểm thử đối kháng, dùng nhiều đặc trưng, và giữ người thẩm định cho ca nghi.

Nối sang Lab 9B: con số đẹp 99.9% ở đây là vì dữ liệu tự sinh quá ngăn nắp. Bài 9B làm lại trên log web THẬT của máy chủ DietPi, nhãn lấy từ fail2ban thật — và anh/chị sẽ thấy precision rơi về ~0.46, không phải vì mô hình kém mà vì nhãn thật có nhiễu. Đó mới là bộ mặt thật của bài toán.

Bài học:

  1. Luật regex chặt thì lọt, lỏng thì oan trên thứ biến hình như URL; ML đo nhiều đặc trưng nên bao phủ rộng hơn.
  2. Độ chính xác cao trên dữ liệu đẹp ≠ an toàn trước kẻ tấn công thật — phải kiểm thử đối kháng.
  3. Biết mô hình tin đặc trưng nào là con dao hai lưỡi: giúp ta hiểu nó, cũng giúp kẻ tấn công lừa nó.

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 Ô trống, trùng? URL đã tách thành phần đo được chưa? 0 ô trống, 0 trùng; tách bằng urlparse (Phần 2) đo trên chuỗi thô, không tách được host / đường dẫn
☝️ 2. Ngón trỏ ĐẶC TRƯNG đúng Biến URL thành những con số nào? 9 con số đo từ host và đường dẫn (Phần 4) chỉ có regex: bắt 36,4% URL độc và báo nhầm 39,5% URL sạch (Phần 3)
✋ 3. Ngón giữa CHỌN CÔNG CỤ Có nhãn, dữ liệu là chuỗi → dùng gì? regex làm mốc (Phần 3) → Random Forest trên 9 con số (Phần 4) sửa regex mãi: chặt thì lọt, lỏng thì oan
💍 4. Ngón áp út ĐO trung thực Con số cao đo trên dữ liệu nào? acc 0,999, bỏ lọt 1, báo nhầm 0 (Phần 4) — nhưng trên dữ liệu tự sinh cùng kiểu với lúc học (Phần 6) tin 99,9% là an toàn
🤙 5. Ngón út GIẢI THÍCH & CẢNH GIÁC Mô hình tin đặc trưng nào? Lừa được không? num_subdomains 0,514 (Phần 5) → thêm subdomain là bị báo nhầm, trang ngân hàng giả "trông hợp lý" là lọt (Phần 6) bị tấn công đối kháng mà không biết