[Lab M1] Module 1 — Luật cứng, log honeypot thật, email phishing #2

Open
opened 2026-10-11 11:25:46 +07:00 by mikrotik · 1 comment
Owner

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.

Cách làm mỗi bước: copy nguyên khối lệnh trong ô xám → dán vào terminal (chuột phải, hoặc Ctrl+Shift+V) → Enter → so với Kết quả. Dòng bắt đầu bằng # là chú thích, máy bỏ qua. Terminal hiện dấu > và đứng im = dán thiếu: bấm Ctrl+C rồi dán lại cả khối (tới dòng EOF cuối).

Bước 0 — Vào máy

Đăng nhập theo bình luận mới nhất ở issue #1: ssh -p <PORT> userNN@<HOST>. Thấy dấu nhắc userNN@...:~$ là vào được.


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

# 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

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?

cd ~/lab/day1_labs
python -B - <<'EOF'
# 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)
EOF

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ì?

# 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?

# 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

# (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):

python -B - <<'EOF'
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))
EOF

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?

python -B - <<'EOF'
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())
EOF

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)?

python -B - <<'EOF'
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")
EOF

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?

python -B - <<'EOF'
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())
EOF

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ì?

python -B - <<'EOF'
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")
EOF

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?

python -B - <<'EOF'
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())
EOF

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

python -B - <<'EOF'
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())
EOF

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ì?

python -B - <<'EOF'
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())
EOF

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ì?

python -B - <<'EOF'
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())
EOF

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ì?

python -B - <<'EOF'
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))
EOF

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ủ"?

python -B - <<'EOF'
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())
EOF

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

python -B - <<'EOF'
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())
EOF

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ể

python -B - <<'EOF'
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())
EOF

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

cd ~/lab/day1_phishing
python -B - <<'EOF'
# 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)
EOF

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

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

cd ~/lab/day1_phishing
python -B - <<'EOF'
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")
EOF

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.

**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. > **Cách làm mỗi bước:** copy nguyên khối lệnh trong ô xám → dán vào terminal (chuột phải, hoặc Ctrl+Shift+V) → Enter → so với **Kết quả**. Dòng bắt đầu bằng `#` là chú thích, máy bỏ qua. Terminal hiện dấu `>` và đứng im = dán thiếu: bấm **Ctrl+C** rồi dán lại cả khối (tới dòng `EOF` cuối). ## Bước 0 — Vào máy Đăng nhập theo **bình luận mới nhất ở [issue #1](https://gitearepo.ddns.net/mikrotik/labs/issues/1)**: `ssh -p <PORT> userNN@<HOST>`. Thấy dấu nhắc `userNN@...:~$` là vào được. --- ## 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 ```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`) ```python 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 ```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? ```bash cd ~/lab/day1_labs python -B - <<'EOF' # 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) EOF ``` **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ì? ```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? ```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 ```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): ```bash python -B - <<'EOF' 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)) EOF ``` **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?** ```bash python -B - <<'EOF' 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()) EOF ``` **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)?** ```bash python -B - <<'EOF' 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") EOF ``` **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?** ```bash python -B - <<'EOF' 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()) EOF ``` **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ì?** ```bash python -B - <<'EOF' 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") EOF ``` **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?** ```bash python -B - <<'EOF' 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()) EOF ``` **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** ```bash python -B - <<'EOF' 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()) EOF ``` **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ì?** ```bash python -B - <<'EOF' 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()) EOF ``` **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ì?** ```bash python -B - <<'EOF' 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()) EOF ``` **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ì?** ```bash python -B - <<'EOF' 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)) EOF ``` **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ủ"?** ```bash python -B - <<'EOF' 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()) EOF ``` **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** ```bash python -B - <<'EOF' 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()) EOF ``` **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ể** ```bash python -B - <<'EOF' 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()) EOF ``` **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 ```bash cd ~/lab/day1_phishing python -B - <<'EOF' # 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) EOF ``` **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 ```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. ```bash cd ~/lab/day1_phishing python -B - <<'EOF' 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") EOF ``` **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.
Author
Owner

Thực hành bằng Jupyter — chạy từng ô, dễ nhìn hơn

Cùng nội dung với hướng dẫn ở trên, nhưng mở trong trình duyệt: mỗi bước là một ô, bấm chạy là thấy kết quả ngay dưới ô, sửa số rồi chạy lại để thử. Chọn cách nào cũng được (gõ lệnh trong terminal như ở trên, hoặc Jupyter) — hai cách dùng chung dữ liệu.

Bước 1 — Vào máy bằng SSH có "đường hầm" tới Jupyter của bạn

Cổng Jupyter của bạn = 8800 + số tài khoản: user05 → 8805, user12 → 8812, user20 → 8820.

Trên laptop, mở Windows Terminal / PowerShell (hoặc Terminal trên macOS/Linux), gõ (thay NN bằng số tài khoản; <PORT> và <HOST> lấy theo bình luận mới nhất ở issue #1):

ssh -L 8888:127.0.0.1:88NN -p <PORT> userNN@<HOST>

Ví dụ máy hôm nay, tài khoản user05:

ssh -L 8888:127.0.0.1:8805 -p 21722 user05@38.64.63.43

Dùng PuTTY: trước khi bấm Open, vào Connection → SSH → Tunnels: Source port 8888, Destination 127.0.0.1:88NN → bấm Add → rồi Open và đăng nhập như bình thường.

Bước 2 — Bật Jupyter của bạn

Trong cửa sổ SSH vừa vào, gõ:

lab-jupyter

Máy in ra một đường link dạng http://localhost:8888/lab?token=... — giữ nguyên cửa sổ này trong lúc làm bài.

Bước 3 — Mở trình duyệt

Copy nguyên đường link đó, dán vào trình duyệt (Chrome / Edge / Firefox) trên laptop của bạn.

Bước 4 — Làm bài

  • Bên trái: mở thư mục notebooks → bấm đúp Lab_M1.ipynb.
  • Góc trên bên phải phải ghi Python (lớp CSAI).
  • Bấm vào một ô code → nhấn Shift + Enter: ô đó chạy, kết quả hiện ngay bên dưới, con trỏ nhảy sang ô kế tiếp.
  • Chạy lần lượt từ trên xuống (bước sau dùng kết quả của bước trước). Ô đang chạy hiện [*], chạy xong hiện số [1], [2]…
  • Muốn thử khác: sửa số / chữ trong ô (ví dụ đổi IP, đổi ngày ở các ô có dòng SỬA DÒNG NÀY) rồi Shift + Enter lại.

Gặp lỗi?

Hiện tượng Cách xử lý
Trình duyệt báo không kết nối được localhost:8888 cửa sổ SSH đã đóng, hoặc lệnh ssh thiếu phần -L 8888:127.0.0.1:88NN → vào lại SSH đúng lệnh ở Bước 1
lab-jupyter báo "đã chạy rồi" đóng cửa sổ SSH cũ, vào lại theo Bước 1 rồi chạy lab-jupyter
Laptop báo cổng 8888 đang bận đổi 8888 thành 8889 ở cả lệnh ssh và đường link
Muốn chạy lại từ đầu cho sạch menu Kernel → Restart Kernel and Run All Cells

Làm xong: bấm Ctrl + C trong cửa sổ SSH để tắt Jupyter.

## Thực hành bằng Jupyter — chạy từng ô, dễ nhìn hơn Cùng nội dung với hướng dẫn ở trên, nhưng mở trong trình duyệt: mỗi bước là **một ô**, bấm chạy là thấy kết quả ngay dưới ô, sửa số rồi chạy lại để thử. **Chọn cách nào cũng được** (gõ lệnh trong terminal như ở trên, hoặc Jupyter) — hai cách dùng chung dữ liệu. ### Bước 1 — Vào máy bằng SSH có "đường hầm" tới Jupyter của bạn Cổng Jupyter của bạn = **8800 + số tài khoản**: `user05` → `8805`, `user12` → `8812`, `user20` → `8820`. Trên laptop, mở **Windows Terminal / PowerShell** (hoặc Terminal trên macOS/Linux), gõ (thay `NN` bằng số tài khoản; `<PORT>` và `<HOST>` lấy theo **bình luận mới nhất ở [issue #1](https://gitearepo.ddns.net/mikrotik/labs/issues/1)**): ``` ssh -L 8888:127.0.0.1:88NN -p <PORT> userNN@<HOST> ``` Ví dụ máy hôm nay, tài khoản `user05`: ``` ssh -L 8888:127.0.0.1:8805 -p 21722 user05@38.64.63.43 ``` Dùng **PuTTY**: trước khi bấm Open, vào *Connection → SSH → Tunnels*: **Source port** `8888`, **Destination** `127.0.0.1:88NN` → bấm **Add** → rồi Open và đăng nhập như bình thường. ### Bước 2 — Bật Jupyter của bạn Trong cửa sổ SSH vừa vào, gõ: ``` lab-jupyter ``` Máy in ra một đường link dạng `http://localhost:8888/lab?token=...` — **giữ nguyên cửa sổ này** trong lúc làm bài. ### Bước 3 — Mở trình duyệt Copy **nguyên đường link** đó, dán vào trình duyệt (Chrome / Edge / Firefox) **trên laptop của bạn**. ### Bước 4 — Làm bài - Bên trái: mở thư mục **`notebooks`** → bấm đúp **`Lab_M1.ipynb`**. - Góc trên bên phải phải ghi **Python (lớp CSAI)**. - Bấm vào một ô code → nhấn **Shift + Enter**: ô đó chạy, kết quả hiện ngay bên dưới, con trỏ nhảy sang ô kế tiếp. - Chạy **lần lượt từ trên xuống** (bước sau dùng kết quả của bước trước). Ô đang chạy hiện `[*]`, chạy xong hiện số `[1]`, `[2]`… - Muốn thử khác: sửa số / chữ trong ô (ví dụ đổi IP, đổi ngày ở các ô có dòng `SỬA DÒNG NÀY`) rồi Shift + Enter lại. ### Gặp lỗi? | Hiện tượng | Cách xử lý | |---|---| | Trình duyệt báo không kết nối được `localhost:8888` | cửa sổ SSH đã đóng, hoặc lệnh `ssh` thiếu phần `-L 8888:127.0.0.1:88NN` → vào lại SSH đúng lệnh ở Bước 1 | | `lab-jupyter` báo "đã chạy rồi" | đóng cửa sổ SSH cũ, vào lại theo Bước 1 rồi chạy `lab-jupyter` | | Laptop báo cổng 8888 đang bận | đổi `8888` thành `8889` ở **cả** lệnh `ssh` và đường link | | Muốn chạy lại từ đầu cho sạch | menu **Kernel → Restart Kernel and Run All Cells** | Làm xong: bấm **Ctrl + C** trong cửa sổ SSH để tắt Jupyter.
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mikrotik/labs#2