Files

42 KiB

Lab Module 1 — Bộ lọc từ khóa · Log honeypot thật · Phân loại email

Cách làm trong Jupyter: bấm vào một ô code rồi nhấn Shift + Enter để chạy ô đó và sang ô kế tiếp. Kết quả hiện ngay dưới ô. Muốn thử khác: sửa số / chữ trong ô rồi chạy lại. Chạy lần lượt từ trên xuống (có bước dùng kết quả của bước trước). Kernel phải là Python (lớp CSAI) (góc trên bên phải).

Lab Module 1 — 3 bài: M1 (bộ lọc từ khóa) · M1.2 (log honeypot thật) · M1.3 (phân loại email). Làm lần lượt từ trên xuống.


Bài M1 — Bộ lọc từ khóa (luật cứng IF/ELSE) và cú "lách luật"

M1-1. Xem dữ liệu: 5 tin nhắn đầu tiên

In [ ]:
%%bash
# Vào thư mục bài M1
cd ~/lab/day1_labs
# In 5 tin nhắn đầu của bộ 50 tin (tail -n +2 = bỏ dòng tiêu đề; head -5 = lấy 5 dòng)
tail -n +2 Manual_SOC_Dataset.csv | head -5

Kết quả:

MSG-001,Zalo,Hưng IT,Ê ông tối nay mấy giờ đá bóng nhỉ? Sân cũ hay sân mới thế?,An toàn (Benign)
MSG-002,Email,hr@company.com,Nhắc nhở: Cuộc họp toàn công ty sẽ diễn ra vào lúc 3h chiều nay ...,An toàn (Benign)
MSG-003,Email,no-reply@vneid.gov.vn,Thông báo: Tài khoản định danh điện tử của bạn đã được đăng nhập ...,An toàn (Benign)
MSG-004,Facebook,Shopee - Khuyến Mãi,Duy nhất hôm nay! Mua iPhone 15 Pro Max chỉ với 15 triệu. ...,Lừa đảo (Phishing)
MSG-005,Email,admin@sec-alert.com,Mật khẩu 'honghanh123' của bạn đã bị lộ. ...,Lừa đảo (Phishing)

👉 Hiểu: mỗi dòng là 1 tin nhắn, 5 cột cách nhau bằng dấu phẩy: mã tin · kênh · người gửi · nội dung · nhãn đúng (Lừa đảo / An toàn).

M1-2. Luật của bộ lọc (trong file rule_based_classifier.py)

SUSPICIOUS_KEYWORDS = ["thanh toán gấp", "chuyển gấp", "trúng thưởng", "bitcoin", ...]   # danh sách từ khóa "đáng ngờ"
SUSPICIOUS_DOMAINS  = ["gmali", "micros0ft", "webmail-verify", ...]                    # danh sách tên miền giả

def is_phishing(text, sender):
    for keyword in SUSPICIOUS_KEYWORDS:      # lần lượt từng từ khóa trong danh sách...
        if keyword in text.lower():          # ...NẾU từ khóa nằm trong nội dung tin nhắn
            return True                      # ...THÌ kết luận: LỪA ĐẢO
    for domain in SUSPICIOUS_DOMAINS:        # tương tự với tên miền giả
        if domain in sender.lower() or domain in text.lower():
            return True
    return False                             # không dính luật nào -> AN TOÀN

👉 Hiểu: máy không hiểu nội dung — nó chỉ so khớp chữ với danh sách. (Xem cả file: less rule_based_classifier.py, thoát bằng phím q.)

M1-3. Chạy bộ lọc 3 lần

In [ ]:
%%bash
cd ~/lab/day1_labs
# Lần 1: chạy bộ lọc trên 50 tin nhắn quen thuộc
python rule_based_classifier.py
# Lần 2: nhân 50 tin lên 10.000 tin để đo tốc độ
python rule_based_classifier.py --simulate-mass
# Lần 3: chạy trên 10 tin kẻ tấn công đã viết lại để "lách luật"
python rule_based_classifier.py --dataset Evasion_Dataset.csv

Kết quả (dòng "Thời gian thực thi" mỗi lần một khác là bình thường):

Lần Dòng cần thấy
1 - Độ chính xác: 48/50 (96.00%)
2 - Độ chính xác: 9600/10000 (96.00%), thời gian khoảng vài phần trăm giây
3 - Độ chính xác: 2/10 (20.00%) + dòng ❌ CẢNH BÁO ...

👉 Hiểu: trên tin quen thuộc luật cứng đúng 96% và quét 10.000 tin trong nháy mắt. Nhưng khi kẻ tấn công đổi cách viết, nó chỉ bắt được 2/10.

M1-4. Tin nào bị lọt, vì sao?

In [ ]:
%cd ~/lab/day1_labs
# Mượn công cụ đọc file CSV, và mượn hàm is_phishing + 2 danh sách luật từ file bài M1
import csv
from rule_based_classifier import is_phishing, SUSPICIOUS_KEYWORDS, SUSPICIOUS_DOMAINS

# In 2 danh sách luật để so
print("Từ khóa trong luật :", SUSPICIOUS_KEYWORDS)
print("Tên miền trong luật:", SUSPICIOUS_DOMAINS)
print()
# Đọc từng dòng của file 10 tin lách luật; mỗi dòng có 5 cột
for ma, kenh, nguoi_gui, noi_dung, nhan in csv.reader(open("Evasion_Dataset.csv", encoding="utf-8-sig")):
    # Bỏ qua dòng tiêu đề, chỉ xét các dòng có mã bắt đầu bằng EVA
    if ma.startswith("EVA"):
        # Hỏi bộ lọc: tin này có dính luật không?
        if is_phishing(noi_dung, nguoi_gui):
            ket_qua = "BẮT ĐƯỢC"
        else:
            ket_qua = "BỎ LỌT  "
        # In kết quả + mã tin + người gửi + nội dung
        print(ket_qua, "|", ma, "|", nguoi_gui, "|", noi_dung)

Kết quả: 2 danh sách luật, sau đó 10 dòng — 2 dòng BẮT ĐƯỢC (EVA-004 có webmail-verify, EVA-007 có .apk) và 8 dòng BỎ LỌT, ví dụ:

BỎ LỌT   | EVA-001 | ketoan@doitac-uytin.com | Hệ thống bank đang cập nhật nha. Cậu chuyển khoản vào tk cập nhật 09128374 giúp tớ nhé.
BỎ LỌT   | EVA-003 | Sếp Tùng | Ông ơi tớ bị tông xe trên đường cao tốc, bắn tớ 5 củ vào tk này để tớ đền người ta.

👉 Hiểu: so tin lọt với danh sách luật: luật có doitac-uytln nhưng kẻ tấn công viết doitac-uytin; luật có tai nạn giao thông nhưng tin viết "bị tông xe"; luật có chuyển gấp nhưng tin viết "bắn tớ 5 củ". Đổi chữ là lọt — luật cứng phải thêm từ khóa mãi không hết.


Bài M1.2 — 30 ngày log honeypot THẬT: xem → thống kê → làm sạch → đặc trưng

Honeypot = máy chủ SSH "mồi" cố tình để kẻ tấn công vào. Log ở /opt/csai/data/honeypot_m1 là dữ liệu tấn công thật thu từ máy chủ của mình, 30 ngày 24/08–22/09/2026.

M1.2-① Xem log: mỗi dòng log là gì?

In [ ]:
%%bash
# Xem tên 3 file log đầu tiên (mỗi ngày 1 file, tên có ngày)
ls /opt/csai/data/honeypot_m1 | head -3
# Lấy 3 dòng đầu của file ngày 22/09 và trình bày lại cho dễ đọc (python -m json.tool)
head -3 /opt/csai/data/honeypot_m1/cowrie.json.2026-09-22 | python -m json.tool --json-lines

Kết quả:

cowrie.json.2026-08-24
cowrie.json.2026-08-25
cowrie.json.2026-08-26
{
    "eventid": "cowrie.session.connect",
    "timestamp": "2026-09-22T00:00:02.779416Z",
    "src_ip": "109.160.32.67",
    "src_port": 36330,
    "session": "54613b5cec67"
}
{
    "eventid": "cowrie.client.version",
    "timestamp": "2026-09-22T00:00:02.793738Z",
    "src_ip": "109.160.32.67",
    "session": "54613b5cec67",
    "version": "SSH-2.0-Go"
}
{
    "eventid": "cowrie.login.failed",
    "timestamp": "2026-09-22T00:00:03.650371Z",
    "src_ip": "109.160.32.67",
    "session": "54613b5cec67",
    "username": "ftpuser1",
    "password": "123456"
}

👉 Hiểu: mỗi dòng là một sự kiện; trường eventid cho biết loại:

eventid Nghĩa Trường quan trọng
cowrie.session.connect một máy lạ kết nối vào src_ip = IP kẻ tấn công · timestamp = thời điểm, giờ UTC (chữ Z)
cowrie.client.version kẻ tấn công khai tên phần mềm SSH version (vd SSH-2.0-Go = chương trình viết bằng ngôn ngữ Go)
cowrie.login.failed / .success kẻ tấn công thử đăng nhập (sai / đúng) username, password đã thử

3 dòng mẫu là một lần tấn công (cùng session): kết nối → khai SSH-2.0-Go → thử ftpuser1 / 123456, tất cả trong 1 giây.

M1.2-② Log lớn cỡ nào?

In [ ]:
%%bash
# Có bao nhiêu file log? (ls liệt kê, wc -l đếm số dòng = số file)
ls /opt/csai/data/honeypot_m1 | wc -l
# Cả thư mục nặng bao nhiêu? (du -sh = dung lượng, dạng dễ đọc)
du -sh /opt/csai/data/honeypot_m1
# Tổng cộng bao nhiêu dòng log? (cat ghép 30 file, wc -l đếm dòng)
cat /opt/csai/data/honeypot_m1/cowrie.json.* | wc -l

Kết quả:

30
63M	/opt/csai/data/honeypot_m1
395124

👉 Hiểu: 30 file · 63 MB · 395.124 dòng. Đọc tay mỗi dòng 1 giây = hơn 4 ngày không ngủ.

M1.2-③ Thống kê sơ bộ bằng lệnh Linux

In [ ]:
%%bash
# (1) Mỗi LOẠI sự kiện có bao nhiêu dòng?
#     grep -oh = chỉ lấy phần khớp mẫu "eventid": "..." ; sort | uniq -c = gom giống nhau và đếm ; sort -rn = nhiều nhất lên đầu
grep -oh '"eventid": "[^"]*"' /opt/csai/data/honeypot_m1/cowrie.json.* | sort | uniq -c | sort -rn
# (2) Có bao nhiêu IP KHÁC NHAU đã kết nối?
#     lấy dòng kết nối -> cắt riêng phần IP -> sort -u = bỏ trùng -> đếm dòng
grep -h '"cowrie.session.connect"' /opt/csai/data/honeypot_m1/cowrie.json.* | grep -o '"src_ip": "[^"]*"' | sort -u | wc -l
# (3) Top 5 IP kết nối nhiều nhất
grep -h '"cowrie.session.connect"' /opt/csai/data/honeypot_m1/cowrie.json.* | grep -o '"src_ip": "[^"]*"' | sort | uniq -c | sort -rn | head -5

Kết quả:

 136063 "eventid": "cowrie.session.connect"
 129951 "eventid": "cowrie.client.version"
  65772 "eventid": "cowrie.login.success"
  63338 "eventid": "cowrie.login.failed"
2906
   5569 "src_ip": "154.18.197.29"
   3757 "src_ip": "130.12.182.196"
   2774 "src_ip": "109.160.32.31"
   2350 "src_ip": "103.245.237.17"
   2291 "src_ip": "85.11.167.127"

👉 Hiểu: 136.063 lần kết nối từ 2.906 IP, 4 loại sự kiện trộn lẫn. grep đếm được vài thứ đơn giản, nhưng đổi giờ UTC → giờ Việt Nam hay đếm cặp username/password thì rất khó → cần làm sạch log thành bảng.

M1.2-④ Làm sạch: log hỗn độn → 3 bảng gọn

Dán nguyên khối (~5 giây):

In [ ]:
import glob, os
import pandas as pd

# Đọc cả 30 file, giữ NGUYÊN dạng chữ như trong log (chưa đổi kiểu dữ liệu)
cac_file = sorted(glob.glob("/opt/csai/data/honeypot_m1/cowrie.json.*"))
df = pd.concat(pd.read_json(f, lines=True, dtype=False, convert_dates=False) for f in cac_file)
print("TRƯỚC khi làm sạch:", len(df), "dòng, trộn", df.eventid.nunique(), "loại sự kiện,", len(df.columns), "cột:", list(df.columns))

# (1) Tách theo loại sự kiện + chỉ giữ cột cần dùng
ket_noi   = df[df.eventid == "cowrie.session.connect"][["timestamp", "src_ip"]]
dang_nhap = df[df.eventid.isin(["cowrie.login.failed", "cowrie.login.success"])][["src_ip", "username", "password"]]
phan_mem  = df[df.eventid == "cowrie.client.version"][["src_ip", "version"]]

# (2) Xử lý ô trống: lần thử đăng nhập KHÔNG gửi mật khẩu -> ghi rõ "(không có)", không xóa dòng
print("Số lần thử đăng nhập không có mật khẩu:", dang_nhap.password.isna().sum())
dang_nhap = dang_nhap.fillna({"password": "(không có)"})

# (3) Đổi giờ: chuỗi giờ quốc tế (UTC, chữ Z) -> giờ Việt Nam
ket_noi["gio_vn"] = pd.to_datetime(ket_noi.timestamp, utc=True).dt.tz_convert("Asia/Ho_Chi_Minh")
ket_noi = ket_noi[["gio_vn", "src_ip"]]

# (4) Lưu 3 bảng sạch vào thư mục ~/m12_sach
thu_muc = os.path.expanduser("~/m12_sach")
os.makedirs(thu_muc, exist_ok=True)
ket_noi.to_csv(thu_muc + "/ket_noi.csv", index=False)
dang_nhap.to_csv(thu_muc + "/dang_nhap.csv", index=False)
phan_mem.to_csv(thu_muc + "/phan_mem.csv", index=False)

print("\nSAU khi làm sạch: 3 bảng gọn trong ~/m12_sach")
print("ket_noi.csv  :", len(ket_noi), "dòng\n" + ket_noi.head(3).to_string(index=False))
print("\ndang_nhap.csv:", len(dang_nhap), "dòng\n" + dang_nhap.head(3).to_string(index=False))
print("\nphan_mem.csv :", len(phan_mem), "dòng\n" + phan_mem.head(3).to_string(index=False))

Kết quả:

TRƯỚC khi làm sạch: 395124 dòng, trộn 4 loại sự kiện, 8 cột: ['eventid', 'timestamp', 'src_ip', 'src_port', 'session', 'version', 'username', 'password']
Số lần thử đăng nhập không có mật khẩu: 244

SAU khi làm sạch: 3 bảng gọn trong ~/m12_sach
ket_noi.csv  : 136063 dòng
                          gio_vn          src_ip
2026-08-24 07:01:03.700755+07:00   217.154.13.82
2026-08-24 07:01:24.563780+07:00 195.178.110.227
2026-08-24 07:03:00.934683+07:00 195.178.110.227

dang_nhap.csv: 129110 dòng
         src_ip username password
  217.154.13.82   ubuntu zaq1xsw2
195.178.110.227     root     1234
195.178.110.227     root    12345

phan_mem.csv : 129951 dòng
         src_ip    version
  217.154.13.82 SSH-2.0-Go
195.178.110.227 SSH-2.0-Go
195.178.110.227 SSH-2.0-Go

👉 Hiểu: 395.124 dòng trộn lẫn → 3 bảng, mỗi bảng chỉ còn 2–3 cột cần dùng, giờ đã đổi sang giờ Việt Nam. 244 lần thử không gửi mật khẩu được ghi "(không có)" chứ không xóa — xóa thì mất luôn username của các lần thử đó.

M1.2-⑤ Đặc trưng: trả lời 4 câu hỏi, mỗi câu một khối

⑤a — Có bao nhiêu IP tấn công? IP nào nhiều nhất?

In [ ]:
import pandas as pd
# Mở bảng kết nối đã làm sạch ở bước ④
ket_noi = pd.read_csv("~/m12_sach/ket_noi.csv")
# Đếm mỗi IP xuất hiện bao nhiêu lần, xếp nhiều nhất lên đầu
so_lan = ket_noi.src_ip.value_counts()
# len(ket_noi) = số kết nối; len(so_lan) = số IP khác nhau
print("Số lần kết nối:", len(ket_noi), "| Số IP khác nhau:", len(so_lan))
# .head(3) = 3 IP đầu bảng
print("Top 3 IP kết nối nhiều nhất:\n" + so_lan.head(3).to_string())
# (so_lan == 1).sum() = đếm IP chỉ xuất hiện đúng 1 lần
print("Số IP chỉ ghé 1 lần:", (so_lan == 1).sum())

Kết quả: Số lần kết nối: 136063 | Số IP khác nhau: 2906 · top 3: 154.18.197.29 5569, 130.12.182.196 3757, 109.160.32.31 2774 · Số IP chỉ ghé 1 lần: 1103

👉 Hiểu: giống hệt kết quả grep ở bước ③ — nhưng giờ chỉ cần công thức TOP: bảng.cột.value_counts().head(N).

⑤b — Giờ nào bị tấn công nhiều nhất (giờ Việt Nam)?

In [ ]:
import pandas as pd
ket_noi = pd.read_csv("~/m12_sach/ket_noi.csv")
# Đọc lại cột giờ Việt Nam thành thời gian thật, rồi lấy riêng GIỜ (0-23)
gio = pd.to_datetime(ket_noi.gio_vn, utc=True).dt.tz_convert("Asia/Ho_Chi_Minh").dt.hour
# Đếm số kết nối mỗi giờ, xếp theo giờ từ 0h đến 23h
dem = gio.value_counts().sort_index()
# Mỗi giờ in 1 dòng: giờ | số kết nối | thanh # (giờ đông nhất = 40 dấu #)
for h, n in dem.items():
    print(f"{h:02d}h | {n:5d} | " + "#" * int(n * 40 / dem.max()))
# idxmax() = giờ đông nhất; dem.max() / dem.min() = giờ đông gấp mấy lần giờ vắng
print("Giờ đông nhất:", dem.idxmax(), "h (giờ VN) | gấp", round(dem.max() / dem.min(), 1), "lần giờ vắng nhất")

Kết quả: 24 dòng 00h … 23h kèm thanh #, rồi Giờ đông nhất: 0 h (giờ VN) | gấp 1.8 lần giờ vắng nhất

👉 Hiểu: giờ đông nhất chỉ gấp 1,8 lần giờ vắng nhất → tấn công gần như đều suốt 24 giờ: đây là máy chạy tự động, không phải người ngồi gõ theo giờ làm việc.

⑤c — Kẻ tấn công thử username / password nào nhất?

In [ ]:
import pandas as pd
# dtype=str: đọc mọi thứ là chữ (mật khẩu "123456" không bị biến thành số); keep_default_na=False: giữ mật khẩu rỗng
dang_nhap = pd.read_csv("~/m12_sach/dang_nhap.csv", dtype=str, keep_default_na=False)
print("Tổng số lần thử đăng nhập:", len(dang_nhap))
# 5 username và 5 password bị thử nhiều nhất
print("Top 5 username:\n" + dang_nhap.username.value_counts().head(5).to_string())
print("Top 5 password:\n" + dang_nhap.password.value_counts().head(5).to_string())

Kết quả: Tổng số lần thử đăng nhập: 129110 · username: root 74861, ubuntu 5265, admin 4027, user 2125, test 1376 · password: 123456 6247, Aa112211 2476, 123 2418, 1234 2050, root 1593

👉 Hiểu: kẻ tấn công thử theo danh sách mật khẩu yếu có sẵn — máy chủ nào dùng các mật khẩu này sẽ bị đoán trúng ngay.

⑤d — Kẻ tấn công dùng phần mềm gì?

In [ ]:
import pandas as pd
phan_mem = pd.read_csv("~/m12_sach/phan_mem.csv")
# Đếm từng tên phần mềm SSH mà kẻ tấn công khai báo
dem = phan_mem.version.value_counts()
print("Top 5 phần mềm:\n" + dem.head(5).to_string())
# dem.index[0] = tên đứng đầu; dem.iloc[0] / len(phan_mem) = tỉ lệ phần trăm của nó
print(f"Riêng {dem.index[0]} chiếm {dem.iloc[0] / len(phan_mem):.0%} số lần khai báo")

Kết quả: SSH-2.0-Go 120382, SSH-2.0-libssh_0.9.6 2722, SSH-2.0-phpseclib_1.0 (openssl) 738, SSH-2.0-PuTTY_Release_0.84 719, SSH-2.0-libssh2_1.8.1 716 · Riêng SSH-2.0-Go chiếm 93% số lần khai báo

👉 Hiểu: 93% là chương trình tự động viết bằng Go — không phải phần mềm người dùng hằng ngày.

M1.2-⑥ Tra cứu sâu: mỗi câu hỏi một khối

Khối nào có dòng SỬA DÒNG NÀY thì được đổi giá trị trong ngoặc kép (IP, username, ngày) rồi dán lại để hỏi câu khác.

⑥a — Ngày nào, thứ mấy bị tấn công nhiều nhất?

In [ ]:
import pandas as pd
ket_noi = pd.read_csv("~/m12_sach/ket_noi.csv")
# File CSV lưu thời gian dưới dạng chữ -> đổi lại thành thời gian thật (giờ Việt Nam)
thoi_gian = pd.to_datetime(ket_noi.gio_vn, utc=True).dt.tz_convert("Asia/Ho_Chi_Minh")
# .dt.date = lấy NGÀY -> đếm -> lấy 5 ngày đông nhất
print("TOP 5 NGÀY bị tấn công nhiều nhất:")
print(thoi_gian.dt.date.rename("ngay").value_counts().head(5).to_string())
# .dt.day_name() = lấy THỨ trong tuần (tiếng Anh: Monday, Tuesday...) -> đếm
print("\nTấn công theo THỨ trong tuần:")
print(thoi_gian.dt.day_name().rename("thu").value_counts().to_string())

Kết quả: top ngày 2026-09-20 10275 · 2026-09-13 9183 · 2026-09-15 8014 · 2026-09-18 7720 · 2026-09-16 7326 · theo thứ: Sunday 26130 đứng đầu … Monday 16841 cuối

👉 Hiểu: 30 ngày này chỉ có 4 Chủ nhật (thứ Hai, thứ Ba có 5) mà Chủ nhật vẫn đứng đầu — vì 2 ngày đông nhất 20/09 và 13/09 đều là Chủ nhật. Vài ngày bùng nổ có thể kéo lệch cả bảng thống kê.

⑥b — Top cặp username / password

In [ ]:
import pandas as pd
# Mở bảng THỬ ĐĂNG NHẬP. dtype=str: đọc mọi thứ là chữ (mật khẩu "123456" không bị biến thành số)
# keep_default_na=False: giữ nguyên mật khẩu rỗng, không coi là ô trống
dang_nhap = pd.read_csv("~/m12_sach/dang_nhap.csv", dtype=str, keep_default_na=False)
# Ghép 2 cột thành 1 chuỗi "username / password" rồi đếm -> top theo CẶP
print("TOP 10 CẶP username / password:")
print((dang_nhap.username + " / " + dang_nhap.password).value_counts().head(10).to_string())

Kết quả: root / Aa112211 2474 · root / root 837 · root / abcd1234 790 · root / 3245gs5662d34 604 · 345gs5662d34 / 345gs5662d34 601 · root / 123456 600 · ubuntu / 123456 436 · ubuntu / ubuntu 398 · …

👉 Hiểu: kẻ tấn công thử theo cặp có sẵn, vd tên đăng nhập trùng mật khẩu (ubuntu / ubuntu).

⑥c — Một username bị thử những mật khẩu gì?

In [ ]:
import pandas as pd
# ===== SỬA DÒNG NÀY: username muốn soi (thử "admin", "ubuntu", "postgres"...) =====
USER_CAN_SOI = "root"
dang_nhap = pd.read_csv("~/m12_sach/dang_nhap.csv", dtype=str, keep_default_na=False)
# Lọc: chỉ giữ các lần thử có username đúng bằng USER_CAN_SOI (== nghĩa là "bằng")
loc = dang_nhap[dang_nhap.username == USER_CAN_SOI]
print("Username", USER_CAN_SOI, "bị thử", len(loc), "lần, từ", loc.src_ip.nunique(), "IP khác nhau")
print("TOP 10 mật khẩu kẻ tấn công thử với", USER_CAN_SOI + ":")
print(loc.password.value_counts().head(10).to_string())

Kết quả: Username root bị thử 74861 lần, từ 706 IP khác nhau · top: Aa112211 2474, root 837, abcd1234 790, 3245gs5662d34 604, 123456 600, … P@ssw0rd 255

👉 Hiểu: thử đổi "root" thành "postgres" hoặc "oracle" (tài khoản dịch vụ hay có trên máy chủ) — danh sách mật khẩu thử khác hẳn.

⑥d — Kiểu tấn công: chỉ quét hay dò mật khẩu? Dùng công cụ gì?

In [ ]:
import pandas as pd
# Mở cả 3 bảng sạch
ket_noi   = pd.read_csv("~/m12_sach/ket_noi.csv")
dang_nhap = pd.read_csv("~/m12_sach/dang_nhap.csv", dtype=str, keep_default_na=False)
phan_mem  = pd.read_csv("~/m12_sach/phan_mem.csv")
# set(...) = tập hợp các IP KHÁC NHAU của mỗi bảng
ip_ket_noi   = set(ket_noi.src_ip)
ip_dang_nhap = set(dang_nhap.src_ip)
# Phép trừ tập hợp: IP có kết nối nhưng KHÔNG hề thử mật khẩu -> chỉ "gõ cửa" rồi đi (quét, dò dịch vụ)
print("KIỂU 1 - Chỉ kết nối rồi đi (quét / dò dịch vụ):", len(ip_ket_noi - ip_dang_nhap), "IP")
# Phép giao tập hợp: IP vừa kết nối VỪA thử mật khẩu -> dò mật khẩu (brute-force)
print("KIỂU 2 - Có thử mật khẩu (brute-force):          ", len(ip_ket_noi & ip_dang_nhap), "IP")
# Rút gọn tên phần mềm: "SSH-2.0-Go" -> "Go", "SSH-2.0-PuTTY_Release_0.84" -> "PuTTY"
ho_phan_mem = phan_mem.version.str.extract(r"^SSH-[\d.]+-([A-Za-z]+)")[0].fillna("khác").rename("cong_cu")
print("\nTOP 5 CÔNG CỤ tấn công (theo số lần khai báo):")
print(ho_phan_mem.value_counts().head(5).to_string())

Kết quả:

KIỂU 1 - Chỉ kết nối rồi đi (quét / dò dịch vụ): 2049 IP
KIỂU 2 - Có thử mật khẩu (brute-force):           857 IP

công cụ: Go 120382 · libssh 4269 · PuTTY 1396 · phpseclib 947 · khác 739

👉 Hiểu: hơn 2/3 số IP chỉ "gõ cửa" rồi đi (máy quét dò khắp Internet); gần 1/3 mới thật sự dò mật khẩu.

⑥e — Hồ sơ một IP: nó đã làm gì?

In [ ]:
import pandas as pd
# ===== SỬA DÒNG NÀY: IP muốn soi (lấy từ kết quả TOP 10 IP ở trên) =====
IP_CAN_SOI = "154.18.197.29"
ket_noi   = pd.read_csv("~/m12_sach/ket_noi.csv")
dang_nhap = pd.read_csv("~/m12_sach/dang_nhap.csv", dtype=str, keep_default_na=False)
phan_mem  = pd.read_csv("~/m12_sach/phan_mem.csv")
# Lọc từng bảng: chỉ giữ dòng của đúng IP này
kn = ket_noi[ket_noi.src_ip == IP_CAN_SOI]
dn = dang_nhap[dang_nhap.src_ip == IP_CAN_SOI].replace("", "(trống)")  # ô rỗng -> ghi "(trống)" cho dễ đọc
pm = phan_mem[phan_mem.src_ip == IP_CAN_SOI]
thoi_gian = pd.to_datetime(kn.gio_vn, utc=True).dt.tz_convert("Asia/Ho_Chi_Minh")
print("HỒ SƠ IP", IP_CAN_SOI)
# .min() = sớm nhất, .max() = muộn nhất, .dt.date.nunique() = số ngày khác nhau, .dt.hour.nunique() = số giờ khác nhau
print("  Lần đầu:", thoi_gian.min(), "| lần cuối:", thoi_gian.max())
print("  Xuất hiện", thoi_gian.dt.date.nunique(), "ngày, trong", thoi_gian.dt.hour.nunique(), "khung giờ khác nhau của ngày")
print("  Số lần kết nối:", len(kn), "| số lần thử đăng nhập:", len(dn))
print("  Phần mềm đã khai:", ", ".join(pm.version.unique()))
print("  TOP 5 username:", ", ".join(dn.username.value_counts().head(5).index))
print("  TOP 5 password:", ", ".join(dn.password.value_counts().head(5).index))

Kết quả:

HỒ SƠ IP 154.18.197.29
  Lần đầu: 2026-08-29 13:00:09.623421+07:00 | lần cuối: 2026-09-21 00:59:17.779344+07:00
  Xuất hiện 12 ngày, trong 24 khung giờ khác nhau của ngày
  Số lần kết nối: 5569 | số lần thử đăng nhập: 4590
  Phần mềm đã khai: SSH-2.0-Go
  TOP 5 username: root, ubuntu, toor, (trống)
  TOP 5 password: root@1234, root@123, root12345, root1, root123

👉 Hiểu: hoạt động ở đủ 24 giờ, chỉ một chương trình Go → máy tự động. Thử đổi IP thành 109.160.32.31.

⑥f — Top dải mạng: nhiều IP nhưng thật ra là một "chủ"?

In [ ]:
import pandas as pd
ket_noi = pd.read_csv("~/m12_sach/ket_noi.csv")
# Cắt số cuối của IP: "109.160.32.18" -> tách ở dấu chấm CUỐI -> "109.160.32" -> thêm ".0/24"
ket_noi["dai_mang"] = ket_noi.src_ip.str.rsplit(".", n=1).str[0] + ".0/24"
# Gom theo dải: đếm số kết nối và số IP khác nhau trong mỗi dải
bang = ket_noi.groupby("dai_mang").agg(so_ket_noi=("src_ip", "size"), so_ip=("src_ip", "nunique"))
print("TOP 10 DẢI MẠNG /24 (cùng 3 số đầu của IP):")
print(bang.sort_values("so_ket_noi", ascending=False).head(10).to_string())

Kết quả: đứng đầu 109.160.32.0/24 41899 42, rồi 91.92.42.0/24 6805 11, 160.187.229.0/24 6429 14, 154.18.197.0/24 5569 1, …

👉 Hiểu: 42 IP cùng dải 109.160.32.x tạo ra 41.899 kết nối — gấp hơn 7 lần IP đứng đầu ⑤a. Nhìn từng IP không thấy; nhìn cả dải mới thấy nguồn tấn công lớn nhất. Chặn từng IP là không đủ.

⑥g — IP "kiên trì": quay lại nhiều ngày nhất

In [ ]:
import pandas as pd
ket_noi = pd.read_csv("~/m12_sach/ket_noi.csv")
ket_noi["ngay"] = pd.to_datetime(ket_noi.gio_vn, utc=True).dt.tz_convert("Asia/Ho_Chi_Minh").dt.date
# Với mỗi IP: đếm số NGÀY khác nhau nó xuất hiện -> xếp giảm dần -> lấy 10
so_ngay = ket_noi.groupby("src_ip").ngay.nunique().sort_values(ascending=False)
print("TOP 10 IP 'kiên trì' nhất (xuất hiện nhiều ngày nhất trong 30 ngày):")
print(so_ngay.head(10).to_string())
# (so_ngay == 30).sum() = có bao nhiêu IP xuất hiện đủ 30/30 ngày
print("Số IP xuất hiện đủ 30/30 ngày:", (so_ngay == 30).sum())
print("Số IP chỉ xuất hiện 1 ngày:   ", (so_ngay == 1).sum())

Kết quả: 94.154.43.56 30 · 185.246.128.133 30 · 176.53.159.196 29 · 94.154.43.234 29 · 185.246.130.20 27 · … · Số IP xuất hiện đủ 30/30 ngày: 2 · Số IP chỉ xuất hiện 1 ngày: 2487

👉 Hiểu: đa số IP (2.487 / 2.906) chỉ ghé 1 ngày rồi biến mất — chặn sau khi phát hiện thì đã muộn; vài IP thì ngày nào cũng có mặt.

⑥h — Soi một ngày cụ thể

In [ ]:
import pandas as pd
# ===== SỬA DÒNG NÀY: ngày muốn soi (giờ Việt Nam), dạng năm-tháng-ngày, trong khoảng 2026-08-24 .. 2026-09-22 =====
NGAY_CAN_SOI = "2026-09-20"
ket_noi = pd.read_csv("~/m12_sach/ket_noi.csv")
thoi_gian = pd.to_datetime(ket_noi.gio_vn, utc=True).dt.tz_convert("Asia/Ho_Chi_Minh")
# Lọc các kết nối có NGÀY đúng bằng ngày cần soi (.astype(str) đổi ngày thành chữ để so sánh)
mot_ngay = ket_noi[thoi_gian.dt.date.astype(str) == NGAY_CAN_SOI]
gio_mot_ngay = thoi_gian[thoi_gian.dt.date.astype(str) == NGAY_CAN_SOI].dt.hour.rename("gio")
print("Ngày", NGAY_CAN_SOI, ":", len(mot_ngay), "kết nối từ", mot_ngay.src_ip.nunique(), "IP")
print("TOP 5 IP trong ngày:")
print(mot_ngay.src_ip.value_counts().head(5).to_string())
print("TOP 3 giờ trong ngày:")
print(gio_mot_ngay.value_counts().head(3).to_string())

Kết quả: Ngày 2026-09-20 : 10275 kết nối từ 178 IP · top IP: 5 IP 109.160.32.x, mỗi IP đúng 1099 · top giờ: 12 1498, 23 1425, 16 937

👉 Hiểu: nhiều IP cùng dải, cùng đúng một số kết nối trong ngày → một hệ thống điều khiển chung. Thử đổi ngày thành "2026-09-05" (ngày vắng nhất).

M1.2-⑦ Tổng kết

Bước Làm gì Thu được
① Xem log head, json.tool mỗi dòng = một sự kiện, 4 loại
② Đếm dữ liệu ls, du, wc -l 30 file, 63 MB, 395.124 dòng
③ Thống kê sơ bộ grep | sort | uniq -c số kết nối, số IP, top IP
④ Làm sạch Python + pandas 3 bảng gọn, giờ Việt Nam
⑤ Đặc trưng value_counts() top IP, giờ, username/password, phần mềm
⑥ Tra cứu sâu lọc + đếm ngày/thứ, cặp user/pass, kiểu tấn công, hồ sơ IP, dải mạng, IP kiên trì

Bài M1.3 — Phân loại email lừa đảo bằng máy học: cái bẫy "100%"

M1.3-1. Xem dữ liệu email

In [ ]:
%cd ~/lab/day1_phishing
# io, contextlib: để "nuốt" dòng chữ mà hàm tạo dữ liệu tự in ra
import io, contextlib
# Mượn file bài M1.3 (đặt tên tắt là m)
import phishing_classifier as m
# Lấy đúng 100 dòng email mà bài dùng (không in gì ra)
with contextlib.redirect_stdout(io.StringIO()):
    df = m.generate_mock_data()
# Đếm tổng số dòng và số email KHÁC NHAU (nunique = đếm giá trị khác nhau)
print("Tổng số dòng:", len(df), "| số email KHÁC NHAU:", df.text.nunique())
# Bỏ bản sao (drop_duplicates), in mỗi email 1 lần kèm nhãn: spam = lừa đảo, ham = thật
for text, nhan in df.drop_duplicates("text").values:
    print(nhan, "|", text)

Kết quả: Tổng số dòng: 100 | số email KHÁC NHAU: 10, rồi 10 dòng: 5 dòng spam | ... (lừa đảo) và 5 dòng ham | ... (thật).

👉 Hiểu: 100 dòng nhưng chỉ có 10 email — mỗi email lặp lại 10 lần.

M1.3-2. File phishing_classifier.py làm gì?

Phần Code Ý nghĩa
Part 1 suspicious_keywords = ["urgent", "verify", "free", "reset", "password"] chấm điểm mỗi email = số từ khóa nó chứa (giống bài M1)
Part 2 GroupShuffleSplit(...) chia 10 email: 7 để học, 3 để thi — mỗi email (cả 10 bản sao) chỉ nằm ở một bên
TfidfVectorizer(...) TF-IDF: biến mỗi email thành dãy số (từ hiếm, đặc trưng → điểm cao)
MultinomialNB() · .fit(...) Naive Bayes học từ 7 email: từ nào hay đi với spam, từ nào với ham
.predict(...) làm bài thi trên 3 email chưa từng thấy
Accuracy / Precision / Recall đúng bao nhiêu phần · báo spam thì đúng bao nhiêu · bắt được bao nhiêu spam thật

M1.3-3. Chạy

In [ ]:
%%bash
# Chú ý: thư mục day1_phishing
cd ~/lab/day1_phishing
python phishing_classifier.py

Kết quả:

Phần Dòng cần thấy
1 5 dòng Email: '…' | Keyword Score: N | Actual: … với N lần lượt 3, 0, 1, 0, 1
2 [*] Split by unique e-mail: 7 train / 3 test e-mails, overlap = 0
2 Accuracy: 0.67 · Precision: 0.50 · Recall: 1.00
2 2 dòng OK … và 1 dòng WRONG predicted=spam actual=ham | Don't forget to submit your timesheets by Friday.

👉 Hiểu: với email chưa từng thấy, mô hình đúng 2/3 (67%) và gắn nhầm một email công việc bình thường là lừa đảo.

M1.3-4. Tái hiện cái bẫy "100% ảo"

Đây là cách chia dữ liệu cũ của bài: chia ngẫu nhiên 100 dòng, không để ý các dòng là bản sao.

In [ ]:
%cd ~/lab/day1_phishing
import io, contextlib
import phishing_classifier as m
# Công cụ chia dữ liệu ngẫu nhiên thành phần học / phần thi
from sklearn.model_selection import train_test_split
# Lấy lại 100 dòng email của bài
with contextlib.redirect_stdout(io.StringIO()):
    df = m.generate_mock_data()
# Cách chia CŨ: chia ngẫu nhiên 70% học / 30% thi, không để ý các dòng là bản sao của nhau
tr, te = train_test_split(df.index, test_size=0.3, random_state=42)
# Tìm email vừa có trong phần thi, vừa có trong phần học (& = phần chung của 2 tập hợp)
trung = set(df.text[te]) & set(df.text[tr])
print(f"Chia ngẫu nhiên: {df.text[te].nunique()} email khác nhau trong đề thi, {len(trung)} email trong số đó ĐÃ CÓ trong lúc học")

Kết quả: Chia ngẫu nhiên: 10 email khác nhau trong đề thi, 10 email trong số đó ĐÃ CÓ trong lúc học

👉 Hiểu: chia ngẫu nhiên thì bản sao của mọi email đề thi đã nằm sẵn trong phần học → mô hình chỉ cần nhớ là được 100%. Con số thật về khả năng bắt email mới là 67% ở bước 3.