Phân tích tĩnh malware 94f2e4d8 #6

Open
opened 2026-10-14 09:22:19 +07:00 by mikrotik · 0 comments
Owner

Mục lục

  • Step 0: Quy tắc an toàn và mở hồ sơ vụ việc
  • Step 1: Nhận dạng file
  • Step 2: Cấu trúc ELF và thư viện
  • Step 3: Entropy và chọn hướng phân tích
  • Step 4: Săn strings và bảng IOC
  • Step 5: Bảng ký hiệu (symbols)
  • Step 6: Ghidra, kiểm chứng giả thuyết
  • Step 7: Rule YARA
  • Step 8: MITRE ATT&CK và hành động SOC
  • Step 9: Đối chiếu với phân tích động (G4)
  • Step 10: Đối chứng với mẫu bị pack (f14937a1)

STEP 0/10: Quy tắc an toàn và mở hồ sơ vụ việc

📁 Hồ sơ vụ việc

Ngày 25/05/2026, honeypot SSH của chúng ta nhận được một file 30 MB tên sshd. Kẻ tấn công upload nó vào một thư mục ẩn có tên toàn chữ số (.3060536107357439887/).
Nhiệm vụ: xác định file này có nguy hiểm không và nó làm gì, mà không chạy nó lần nào. Đó là phân tích tĩnh (static analysis).

🎯 Mục tiêu step này

Coi malware như vật chứng: chưa phân tích gì cả, việc đầu tiên là xác lập an toàn và danh tính của file.

💻 Lệnh và kết quả

$ ls -l 94f2e4d8.elf
-r-------- 1 dell dell 30304472 Oct 14 08:29 94f2e4d8.elf

$ sha256sum 94f2e4d8.elf
94f2e4d8d443…  94f2e4d8.elf
  ✓ SHA-256 khớp với hồ sơ thu thập (G1)

🔍 Diễn giải

Quan sát Ý nghĩa
Quyền -r-------- (400) Chỉ chủ sở hữu được đọc. Không ai có quyền ghi hay chạy, nên không thể lỡ tay kích hoạt file.
SHA-256 94f2e4d8d443… Là "dấu vân tay" của file: chỉ cần đổi 1 byte là hash đổi hoàn toàn. Hash khớp với lúc thu thập ngày 25/05 nghĩa là ta đang phân tích đúng file đó, chưa bị sửa.
Trên màn chiếu chỉ hiện 12 ký tự đầu Hash đầy đủ là thông tin tình báo nội bộ nên được che. 12 ký tự đã đủ để gọi tên mẫu.

Vì sao hash là việc đầu tiên:

  • Tên file do kẻ tấn công đặt, nó có thể là sshd, invoice.pdf hay gì cũng được. Hash mới là danh tính thật.
  • Chain of custody: phải chứng minh được file phân tích chính là file đã thu được.
  • Hash là chìa khóa tra cứu: dùng để tìm trên VirusTotal, trong kho mẫu nội bộ, và ghi vào ticket.

5 quy tắc vàng của phân tích tĩnh:

  1. Không bao giờ chạy file mẫu.
  2. Chỉ làm việc trên bản sao read-only.
  3. Hash trước và sau khi phân tích.
  4. Để mẫu trong thư mục riêng, không lẫn với file làm việc.
  5. Mẫu không rời khỏi máy phân tích (Kali), không copy sang máy học viên.

⚠️ Hiểu lầm hay gặp

"Đã chmod 400 thì file an toàn rồi."

Sai. chmod 400 chỉ chặn việc lỡ tay chạy file. An toàn thật sự đến từ hai việc: không chạy và cách ly. Nếu cần quan sát hành vi khi chạy, ta làm ở G4 trong máy ảo cô lập (phân tích động).

🛡️ Ý nghĩa với SOC

Khi phát hiện một file lạ trên server, việc đầu tiên là sha256sum, ghi vào ticket, rồi tra cứu. Không mở, không chạy, không gửi file qua chat.


STEP 1/10: Nhận dạng file — thực chất nó là gì?

🎯 Mục tiêu step này

Chứng minh rằng tên file không phải bằng chứng. Loại file được quyết định bởi nội dung, cụ thể là vài byte đầu tiên (magic bytes).

💻 Lệnh và kết quả

$ file 94f2e4d8.elf
94f2e4d8.elf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
              dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,
              BuildID[sha1]=300bf5c7e304…, for GNU/Linux 3.2.0, stripped

$ xxd 94f2e4d8.elf | head -1
00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000  .ELF............

$ ls -lh 94f2e4d8.elf
29M 94f2e4d8.elf

So sánh với sshd thật trên chính máy Kali này:

$ ls -l /usr/sbin/sshd
-rwxr-xr-x 1 root root 580512 Feb 19 2026 /usr/sbin/sshd     ← 0,58 MB
$ file /usr/sbin/sshd
/usr/sbin/sshd: ELF 64-bit LSB pie executable, x86-64, ...

🔍 Diễn giải

Quan sát Ý nghĩa
7f 45 4c 46 = .ELF Magic bytes: 4 byte đầu file cho biết đây là chương trình chạy được trên Linux (định dạng ELF). Linux không dựa vào đuôi file.
ELF 64-bit … x86-64 Chạy được trên máy chủ Intel/AMD 64-bit, loại server phổ biến nhất. Mục tiêu là các server thông thường.
dynamically linked Chương trình gọi thêm thư viện của hệ thống khi chạy. Step 2 sẽ xem nó cần những thư viện nào, và đó là manh mối quan trọng.
stripped Kẻ tấn công đã xóa bảng tên hàm nội bộ để gây khó cho người phân tích.
29 MB vs 0,58 MB File giả lớn gấp 50 lần sshd thật. Lý do: viết bằng ngôn ngữ Go, vốn đóng gói toàn bộ runtime và thư viện vào một file. Kích thước bất thường là dấu hiệu nhận dạng đầu tiên.
executable vs pie executable sshd thật được biên dịch kiểu PIE (có cơ chế bảo vệ bộ nhớ ASLR) theo chuẩn của bản phân phối. File giả không phải PIE, nên không phải do Debian/Kali build ra.

Kết luận step 1: đây không phải sshd. Đây là một chương trình Go 64-bit giả danh sshd.
→ Ghi lên bảng IOC: Masquerading (MITRE T1036): tên sshd, nằm trong thư mục ẩn toàn chữ số.

⚠️ Hiểu lầm hay gặp

"Nhìn tên và đuôi file là biết loại file."

Sai. Trên Linux, tên và đuôi file là do người tạo đặt tùy ý. Kẻ tấn công cố tình đặt tên sshd để người quản trị liếc qua ps hay ls mà tưởng là dịch vụ SSH bình thường. Chỉ có nội dung file (magic bytes, cấu trúc) mới đáng tin.

🛡️ Ý nghĩa với SOC

  • sshd thật chỉ nằm ở /usr/sbin/sshd. Một tiến trình tên sshd chạy từ /tmp, /dev/shm, thư mục home hay thư mục ẩn là dấu hiệu đỏ.
  • Lệnh kiểm tra nhanh trên server: ls -l /proc/<PID>/exe (xem file thật đằng sau tiến trình), và so sánh hash với gói chính chủ (debsums, rpm -V).

STEP 2/10: Cấu trúc ELF — nó cần gì để chạy?

🎯 Mục tiêu step này

Một chương trình luôn phải khai báo những thư viện nó cần thì hệ điều hành mới nạp được. Danh sách đó là lời tự khai về khả năng của chương trình, đọc được mà không cần chạy và không cần biết assembly.

💻 Lệnh và kết quả

Phần đầu file (ELF header):

$ readelf -h 94f2e4d8.elf
  Class:                     ELF64
  Type:                      EXEC (Executable file)
  Machine:                   Advanced Micro Devices X86-64
  Entry point address:       0x403470
  Number of section headers: 35

Các thư viện nó khai báo cần dùng:

$ readelf -d 94f2e4d8.elf | grep NEEDED
 (NEEDED)  Shared library: [libpam.so.0]      ← ⚠️
 (NEEDED)  Shared library: [libpthread.so.0]
 (NEEDED)  Shared library: [libc.so.6]

So sánh với sshd thật trên Kali:

$ readelf -d /usr/sbin/sshd | grep NEEDED
 (NEEDED)  Shared library: [libselinux.so.1]
 (NEEDED)  Shared library: [libcrypto.so.3]   ← thư viện mã hóa (SSH cần)
 (NEEDED)  Shared library: [libc.so.6]

🔍 Diễn giải

Quan sát Ý nghĩa
Entry point 0x403470 Địa chỉ lệnh đầu tiên chạy khi chương trình khởi động. Ghi lại để dùng ở step 6 (Ghidra).
35 section headers File còn nguyên vẹn. Trong cùng chiến dịch này, ta đã thu được 9 biến thể khác có 0 section header: đó là các bản bị cắt cụt, chạy là crash ngay. Mẫu này là bản đầy đủ duy nhất, nên đáng phân tích sâu.
libc.so.6, libpthread.so.0 Thư viện nền tảng mà gần như mọi chương trình đều dùng, không có gì lạ.
libpam.so.0 PAM (Pluggable Authentication Modules) là thư viện xác thực đăng nhập của Linux. Nó là cơ chế kiểm tra user/mật khẩu mà SSH, login, sudo đều dùng.
Thiếu libcrypto Một sshd thật cần thư viện mã hóa. File này không cần, vì Go tự mang theo bộ mã hóa riêng. Thêm một bằng chứng nó không phải sshd.

Câu hỏi then chốt của buổi học:

🔎 Một chương trình lạ, giả danh sshd, tại sao lại cần đến thư viện xác thực đăng nhập?

Ta ghi lên bảng IOC một giả thuyết, chưa phải kết luận:

Loại Bằng chứng Step Độ tin cậy
Masquerading (T1036) tên sshd, thư mục ẩn 1 ✅ Chắc chắn
Liên quan đến đăng nhập / mật khẩu? liên kết libpam.so.0 2 🟡 Giả thuyết

Có vài khả năng: (a) đánh cắp mật khẩu khi người dùng đăng nhập, (b) kiểm tra user/mật khẩu trên chính máy nạn nhân, (c) phục vụ một cửa hậu có màn đăng nhập riêng. Chưa đủ bằng chứng để chọn. Ta sẽ kiểm chứng ở step 5 và step 6.

⚠️ Hiểu lầm hay gặp

"Phân tích tĩnh nghĩa là phải đọc assembly."

Sai. Chỉ đọc metadata (header, danh sách thư viện) trong vài giây ta đã biết: file nguyên vẹn, 64-bit, viết bằng Go, và dính đến xác thực đăng nhập. Phần lớn câu trả lời ban đầu đến từ metadata. Assembly và decompiler để dành cho bước sau, khi đã biết cần tìm gì.

"Thấy thư viện X là kết luận được nó làm Y."

Chưa được. Thư viện cho biết khả năng có thể có, chưa chứng minh ý đồ. Vì thế ở đây ta ghi là giả thuyết.

🛡️ Ý nghĩa với SOC

  • Một binary lạ nằm ngoài đường dẫn hệ thống mà lại liên kết libpam là cảnh báo ưu tiên cao. Lệnh rà nhanh trên server: ldd <file> | grep pam.
  • Ghi chú vào ticket: "binary lạ có liên kết thư viện xác thực, cần điều tra khả năng liên quan đến thông tin đăng nhập".

STEP 3/10: Entropy và chọn hướng phân tích

🎯 Mục tiêu step này

Trước khi đọc nội dung, phải biết nội dung có đọc được không. Malware thường được nén hoặc mã hóa (pack) để che giấu. Entropy đo độ "ngẫu nhiên" của dữ liệu và cho biết nên đi theo hướng phân tích nào. Đây chính là chỉ số học viên đã tính trong Lab 5.

💻 Lệnh và kết quả

$ python3 -c '<shannon entropy trên toàn bộ byte>' 94f2e4d8.elf
entropy = 6.3601 bits/byte

Thang so sánh, đo ngay trên máy này:

File                         Entropy    Bản chất
---------------------------  -------    -----------------------------
/usr/sbin/sshd (thật)         5.66      chương trình bình thường
/usr/bin/ls                   5.97      chương trình bình thường
94f2e4d8.elf (mẫu)          ► 6.36 ◄    ?
ls nén bằng gzip              7.99      dữ liệu nén
2 MB byte ngẫu nhiên          8.00      ngẫu nhiên hoàn toàn (mức tối đa)

Bảng định tuyến trong quy trình của chúng ta (SOP V3.2):

 entropy < 7.0   → G3B     : phân tích đầy đủ bằng Ghidra
 7.0 – 7.5       → thử giải nén UPX rồi quay lại G3B
 entropy > 7.5   → G3B-ALT : chỉ dùng script (Ghidra sẽ không đọc được)

🔍 Diễn giải

Quan sát Ý nghĩa
Entropy là gì Đo trung bình cần bao nhiêu bit để mã hóa 1 byte. Tối đa là 8, khi mọi giá trị byte xuất hiện đều nhau như dữ liệu ngẫu nhiên. Code máy và chuỗi chữ có quy luật lặp lại nên thấp hơn, khoảng 5–6,5.
Mẫu = 6,36 Nằm trong vùng chương trình bình thường, cao hơn sshd một chút vì Go nhúng nhiều dữ liệu. Không bị pack hay mã hóa.
Đi theo nhánh G3B Chuỗi ký tự, tên hàm, code đều đọc được. Step 4 (strings) và step 6 (Ghidra) sẽ cho kết quả tốt.
So với file nén (7,99) Nếu malware bị pack thì entropy sẽ sát mức 8 như vậy, và strings chỉ ra rác. Ta sẽ thấy tận mắt ở step 10 với một mẫu khác.

Một hạn chế có thật của SOP, nói rõ để thấy quy trình nào cũng cần kiểm chứng bằng dữ liệu:
Khi đo 205 mẫu ELF trong kho, nhóm DietPi thấy chúng chia thành 2 cụm rõ rệt: 131 mẫu ở 5,56–6,42 và 74 mẫu nén UPX ở 7,99–8,00. Không mẫu nào rơi vào dải 7,0–7,5. Nghĩa là bước "thử giải nén UPX" trong SOP chưa bao giờ được kích hoạt. Đề xuất sửa: kiểm tra dấu hiệu UPX trước, rồi mới xét entropy. Đây cũng là cầu nối sang phần Decision Tree của Module 5: khi máy học từ dữ liệu, ngưỡng nó học được là ≈7,2, đúng điểm giữa khoảng trống giữa hai cụm.

⚠️ Hiểu lầm hay gặp

"Entropy cao nghĩa là malware."

Sai. File ZIP, ảnh JPEG, video, bản backup mã hóa đều có entropy khoảng 8. Entropy cho biết phân tích cách nào, không kết luận có độc hay không.

"Entropy thấp nghĩa là an toàn."

Sai. Mẫu này entropy chỉ 6,36, trông rất "bình thường", nhưng như các step sau sẽ cho thấy, nó là một malware nguy hiểm.

🛡️ Ý nghĩa với SOC

  • Entropy là bước phân loại (triage) nhanh nhất: vài giây là biết nên mở Ghidra hay phải tìm cách giải nén trước, đỡ lãng phí thời gian.
  • Gặp một file thực thi có entropy sát 8 nằm ngoài thư mục hệ thống: khả năng cao bị pack, nên tăng mức nghi ngờ và ưu tiên đưa vào sandbox.

STEP 4/10: Săn chuỗi ký tự (strings) và điền bảng IOC

🎯 Mục tiêu step này

Rèn kỹ năng cốt lõi của người phân tích: biến hàng nghìn chuỗi ký tự thành một bảng bằng chứng gọn, có phân loại. Bài học chính: tìm theo giả thuyết nhanh hơn rất nhiều so với đọc từng dòng.

💻 Lệnh và kết quả

Bước 1: xem khối lượng.

$ strings -n 8 94f2e4d8.elf | wc -l
13789 strings (>= 8 chars)

Gần 14.000 chuỗi, không ai đọc hết được. Ta tìm theo từng nhóm hành vi:

📌 Lưu ý khi đọc kết quả: Go lưu các chuỗi nối liền nhau, không có ký tự phân cách, nên mỗi kết quả dính thêm một ít chữ của chuỗi bên cạnh. Chỉ cần nhìn vào phần ta đang tìm.

[1] Đào tiền ảo

Xmrig_algo   Xmrig_enabled   Xmrig_nicehash
Xmrig_poolAddr   Xmrig_poolPass   Xmrig_poolUser
json:"nbminer_poolAddr"   json:"nbminer_poolPass"   json:"nbminer_poolUser"

[2] Bám trụ trên máy (persistence)

ExecStart=/bin/systemd-worker
service systemd-worker enable || systemctl enable systemd-worker.service

[3] Kênh liên lạc với kẻ tấn công (C2)

https://discord.com/api/webhooks/<ID>/<TOKEN-MASKED>        ← đã che

[4] Do thám nạn nhân: "IP công khai của tôi là gì?"

http://ipgrab.io/   https://ident.me/   http://inet-ip.info/ip   https://api.ipify.org/
http://ipinfo.io/ip   https://checkip.amazonaws.com/   https://ip.seeip.org/

[5] Truy cập khóa SSH của máy chủ

/etc/ssh/ssh_host_rsa_key   /etc/ssh/ssh_host_ecdsa_key   /etc/ssh/ssh_host_ed25519_key

[6] Chữ ký của tác giả

pan-chan's mining island hi!

[7] Lệnh lạ (giả thuyết)

^(sharepeer)(\s)(.*)$

🔍 Diễn giải: bảng IOC sau step 4

# Loại hành vi Bằng chứng MITRE ATT&CK Độ tin cậy
— Giả danh tên sshd, thư mục ẩn T1036 ✅ Chắc chắn
— Liên quan đăng nhập? liên kết libpam — 🟡 Giả thuyết (step 2)
1 Đào coin bằng 2 phần mềm: XMRig và NBMiner biến cấu hình Xmrig_*, nbminer_* T1496 Resource Hijacking 🟠 Có khả năng
2 Tự cài thành dịch vụ hệ thống, giả tên systemd để trông như thành phần của Linux /bin/systemd-worker, systemctl enable T1543.002 Systemd Service 🟠 Có khả năng
3 Gửi dữ liệu về qua Discord: dùng dịch vụ hợp pháp nên tường lửa khó chặn URL webhook Discord T1102 / T1041 🟠 Có khả năng
4 Lấy IP công khai của nạn nhân, dùng tới 7 dịch vụ dự phòng để chắc chắn lấy được 7 URL tra IP T1016 🟠 Có khả năng
5 Đọc khóa SSH của máy chủ /etc/ssh/ssh_host_*_key T1552.004 Private Keys 🟡 Giả thuyết
6 Chữ ký tác giả, gợi ý để truy vết nhóm tấn công pan-chan's mining island — ✅ Có trong file
7 Có thể là lệnh giao tiếp ngang hàng (P2P) giữa các máy bị nhiễm regex sharepeer — 🟡 Giả thuyết

Điều thú vị: không tìm thấy địa chỉ pool đào coin hay ví tiền. Chỉ có tên biến (Xmrig_poolAddr…), không có giá trị. Kết luận: cấu hình được gửi xuống từ kẻ tấn công lúc chạy. Phân tích tĩnh thấy được "ô trống" chứ không thấy "nội dung điền vào". Đây là giới hạn đầu tiên của phân tích tĩnh, và ta sẽ quay lại ở step 9.

⚠️ Hiểu lầm hay gặp

"Có chuỗi ký tự trong file nghĩa là chương trình chắc chắn làm việc đó."

Chưa chắc. Chuỗi cho thấy khả năng và ý đồ, chưa chứng minh hành vi thực tế: có thể là code chết, mồi nhử, hoặc nằm trong thư viện không được gọi tới. Vì thế các dòng trên chỉ ở mức 🟠 có khả năng. Muốn lên mức ✅ phải có bằng chứng từ code (step 6) hoặc từ chạy thử trong sandbox (G4).

🛡️ Ý nghĩa với SOC

Mỗi dòng trong bảng IOC thành một quy tắc phát hiện:

  • Proxy/Firewall: cảnh báo khi server (không phải máy người dùng) gọi tới discord.com/api/webhooks hoặc các dịch vụ tra IP như ipify, ident.me, checkip.amazonaws.com.
  • EDR/giám sát file: cảnh báo khi xuất hiện /bin/systemd-worker hoặc dịch vụ systemd-worker.service.
  • Giám sát hiệu năng: CPU server tăng vọt kéo dài, kèm kết nối ra ngoài lạ, là dấu hiệu bị đào coin trộm.

STEP 5/10: Bảng ký hiệu (symbols) — nó gọi những hàm nào?

🎯 Mục tiêu step này

Ở step 1 ta thấy file đã bị stripped (xóa tên hàm). Nhưng có một loại tên không thể xóa: tên các hàm mà chương trình trao đổi với thư viện bên ngoài, vì hệ điều hành cần chúng để nối chương trình với thư viện. Đọc bảng này để làm rõ giả thuyết PAM từ step 2.

💻 Lệnh và kết quả

Hai hàm có tên liên quan PAM nằm ngay trong chương trình:

$ readelf --dyn-syms -W 94f2e4d8.elf | grep -E ' (cbPAMConv|init_pam_conv)$'
    83: 00000000006c0470    19 FUNC    GLOBAL DEFAULT   14 init_pam_conv
    86: 00000000006bfed0   168 FUNC    GLOBAL DEFAULT   14 cbPAMConv

Các hàm PAM mà nó gọi từ thư viện libpam:

pam_start  pam_authenticate  pam_acct_mgmt  pam_setcred  pam_chauthtok
pam_open_session  pam_close_session  pam_get_item  pam_set_item
pam_getenv  pam_getenvlist  pam_putenv  pam_strerror  pam_end  (pam_conv)

Các hàm đổi danh tính người dùng/nhóm:

_cgo_libc_setuid  _cgo_libc_seteuid  _cgo_libc_setreuid  _cgo_libc_setresuid
_cgo_libc_setgid  _cgo_libc_setegid  _cgo_libc_setregid  _cgo_libc_setresgid
_cgo_libc_setgroups

🔍 Diễn giải

Quan sát Ý nghĩa
cbPAMConv tại 0x6bfed0, dài 168 byte "cb" = callback, "PAMConv" = PAM conversation. Trong PAM, hàm hội thoại (conversation function) là chỗ hỏi người dùng "Mật khẩu?" rồi chuyển câu trả lời cho hệ thống kiểm tra. Ai viết hàm này thì nhìn thấy mật khẩu.
Gọi gần như toàn bộ API PAM pam_start mở phiên, pam_authenticate kiểm tra user/mật khẩu, pam_open_session mở phiên đăng nhập, pam_chauthtok đổi mật khẩu. Chương trình có đủ công cụ để tự thực hiện một quy trình đăng nhập hoàn chỉnh.
_cgo_libc_set*uid/gid "cgo" là cơ chế để code Go gọi sang thư viện C. Các hàm setuid/setgid dùng để đổi danh tính tiến trình, ví dụ chuyển sang chạy dưới quyền một user khác sau khi đăng nhập. Một chương trình có màn đăng nhập riêng sẽ cần đúng các hàm này.
Tiền tố _cgo_ khắp nơi Xác nhận lại: đây là Go dùng cgo để gọi thư viện C (libpam). Ta cần điều này để hiểu step 6.

Cập nhật bảng IOC:

Loại Bằng chứng Độ tin cậy
Có khả năng xử lý đăng nhập và mật khẩu cbPAMConv + bộ API PAM đầy đủ + setuid/setgid 🟠 Có khả năng có khả năng này

Tới đây có 2 cách giải thích, và phải chọn bằng bằng chứng:

  • Giả thuyết A, "cửa hậu ăn cắp mật khẩu": kẻ tấn công tự viết cbPAMConv để chép lại mật khẩu mỗi khi có người đăng nhập (MITRE T1556.003). Đây cũng là kết luận ban đầu trong báo cáo tháng 5 của chúng ta.
  • Giả thuyết B, "dùng thư viện có sẵn": cbPAMConv là một phần của thư viện mã nguồn mở mà kẻ tấn công nhúng vào, để chương trình tự kiểm tra user/mật khẩu trên máy nạn nhân.

Cả hai đều khớp với những gì thấy ở step 5. Chỉ nhìn tên hàm thì không phân biệt được. Cần đọc code thật, đó là việc của Ghidra ở step 6.

⚠️ Hiểu lầm hay gặp

"File đã bị strip thì mất hết tên hàm."

Sai. Strip chỉ xóa tên các hàm nội bộ. Các tên dùng để nối với thư viện ngoài (dynamic symbols) bắt buộc phải giữ lại, nếu không chương trình không chạy được. Đây là "điểm yếu" mà kẻ tấn công không xóa được.

"Tên hàm nói lên ý đồ."

Chưa chắc. Tên hàm cho biết chức năng, không cho biết ai viết và dùng vào việc gì. Một hàm tên cbPAMConv có thể do kẻ tấn công viết, nhưng cũng có thể là của một thư viện hợp pháp. Đừng kết luận chỉ từ tên, kể cả khi tên nghe rất "đáng sợ".

🛡️ Ý nghĩa với SOC

  • Một binary ngoài đường dẫn hệ thống mà gọi pam_authenticate là đáng điều tra: trên một server bình thường, chỉ các chương trình hệ thống như sshd, login, sudo, su mới làm việc này.
  • Kiểm tra nhanh: ldd <file> | grep pam, rồi nm -D <file> | grep pam_.
  • Khi viết báo cáo: ghi rõ mức độ tin cậy ("có khả năng" hay "đã xác nhận"). Báo cáo nói quá sẽ dẫn đến phản ứng sai, ví dụ đi tìm một PAM module bị cài trộm mà thật ra không tồn tại.

STEP 6/10: Ghidra — đọc code để kiểm chứng giả thuyết

🎯 Mục tiêu step này

Step 5 để lại hai giả thuyết về PAM, và chỉ nhìn tên hàm thì không phân biệt được. Giờ ta dùng Ghidra, công cụ dịch ngược mã nguồn mở của NSA, để đọc code thật và trả lời 3 câu: ai viết cbPAMConv, ai gọi PAM, và code của chính kẻ tấn công kể câu chuyện gì.

(Project Ghidra đã được phân tích sẵn trước buổi học, vì bước tự động phân tích mất khoảng 4 phút.)

💻 Kết quả từ Ghidra

① Go giữ lại tên hàm dù file đã bị strip

Tổng số hàm có tên khôi phục được: 5.978

Go lưu một bảng tên hàm riêng (pclntab) để in thông báo lỗi. Lệnh strip không xóa bảng này. Bất lợi cho kẻ tấn công, có lợi cho ta.

② cbPAMConv là của ai? Trả lời: của một thư viện mã nguồn mở

Code C tại 0x6bfed0 (cbPAMConv), dịch ngược:
    crosscall2(..., _cgoexp_eba3282b571c_cbPAMConv, ...);   ← chỉ là cầu nối C → Go

Hàm Go thật nằm trong gói:
    0066d480  github.com/msteinert/pam.cbPAMConv             ← thư viện công khai trên GitHub

github.com/msteinert/pam là thư viện Go hợp pháp để gọi PAM. cbPAMConv là callback chuẩn của thư viện đó, không phải code kẻ tấn công tự viết.

③ Ai thật sự gọi PAM, và để làm gì?

pam.Start                       ← chỉ được gọi bởi:  main.protector.func1
pam.(*Transaction).Authenticate ← chỉ được gọi bởi:  main.protector.func1

main.protector.func1 gọi:  os/user.Lookup → pam.Start → pam.Authenticate → time.Sleep
main.protector       gọi:  os.ReadFile → ssh.ParsePrivateKey → ssh.AddHostKey → net.Listen → Accept
main.protectorHandleClient.func1 gọi:  pty.open → term.ReadLine → exec.Command → Cmd.Start

④ Code của chính kẻ tấn công (76 hàm main.*), xếp theo chức năng

Đào coin        : main.miner, main.killxmrig, main.killnbminer
Lây lan (worm)  : main.spreader → main.randomIP → main.sshtry
Cửa hậu SSH     : main.protector, main.protectorHandleClient
Tự bảo vệ       : main.antikill, main.antitaskmanager, main.killdup, main.kill_by_port
Mạng ngang hàng : main.p2p, main.p2ptry, main.p2phandleconnection
Khác            : main.updater, main.getMyIP, main.loadpayload, main.startupmanager

⑤ Hàm lây lan main.sshtry gọi gì?

net.DialTimeout → ssh.NewClient → sftp.NewClient → sftp.Mkdir → sftp.Create → File.Write
→ os.executable → ssh.Session.Start          (+ shuffleStrings, generate_seed: xáo trộn danh sách mật khẩu)

⑥ Cấu trúc dữ liệu main.credential

+0x00  username : string
+0x10  password : string
+0x20  ip       : string

🔍 Diễn giải: kiểm chứng giả thuyết

Giả thuyết (step 5) Bằng chứng từ Ghidra Kết luận
A. Kẻ tấn công tự viết cbPAMConv để chép mật khẩu khi người dùng đăng nhập SSH thật (T1556.003) cbPAMConv thuộc thư viện công khai. Không có PAM module nào bị cài, và không có code nào móc vào sshd thật. ❌ Bác bỏ: không có bằng chứng
B. Dùng thư viện PAM để tự kiểm tra user/mật khẩu PAM chỉ được gọi từ main.protector.func1, tức là hàm kiểm tra mật khẩu của một SSH server riêng do malware dựng lên ✅ Xác nhận

Câu chuyện thật mà code kể lại:

  1. 🚪 Cửa hậu SSH riêng (main.protector): malware đọc khóa SSH thật của máy nạn nhân (chính các file /etc/ssh/ssh_host_*_key ở step 4), rồi mở một SSH server riêng. Server này dùng khóa đó nên dấu vân tay giống hệt server thật và không gây cảnh báo. Khi có người đăng nhập, nó hỏi PAM của hệ thống xem user/mật khẩu có đúng không, rồi cấp shell (pty + exec.Command). Kẻ tấn công chỉ cần biết một mật khẩu thật là vào được bằng cửa riêng, kể cả khi admin đã đổi cấu hình sshd.
  2. 🐛 Worm SSH (main.spreader → main.sshtry): sinh IP ngẫu nhiên, thử đăng nhập SSH bằng danh sách user/mật khẩu đã xáo trộn (main.credential = {username, password, ip}). Khi vào được thì upload chính nó qua SFTP (os.executable + sftp.Create/Write) và chạy nó. 👉 Đây chính là cách file này đến được honeypot của chúng ta: một máy bị nhiễm khác đã brute-force rồi SFTP nó vào thư mục ẩn!
  3. ⛏️ Đào coin và diệt đối thủ: main.killxmrig / main.killnbminer diệt các miner khác đang chạy trên máy để độc chiếm CPU.
  4. 🛡️ Tự bảo vệ: antikill, antitaskmanager, killdup chống bị tắt và chống chạy trùng.

Cập nhật bảng IOC:

Loại Bằng chứng MITRE Độ tin cậy
Đánh cắp mật khẩu bằng PAM hook cbPAMConv T1556.003 ❌ Bác bỏ
Cửa hậu SSH riêng, dùng khóa thật của host + PAM kiểm tra login main.protector*, AddHostKey, pam.Authenticate T1021.004 (kẻ tấn công dùng để vào lại) ✅ Code xác nhận
Worm: brute-force SSH rồi tự copy qua SFTP main.sshtry, randomIP, sftp.Create T1110.001, T1570 ✅ Code xác nhận
Đọc khóa SSH của host os.ReadFile → ParsePrivateKey T1552.004 🟡 → ✅
Diệt miner đối thủ main.killxmrig, main.killnbminer T1489 ✅ Code xác nhận

⚠️ Hiểu lầm hay gặp

"Code dịch ngược chính là mã nguồn gốc."

Sai. Đó là bản tái dựng: tên biến được đoán (param_1, local_58…), cấu trúc có thể sai, và luôn cần người diễn giải.

"Tên hàm nghe đáng sợ thì hàm đó là của kẻ tấn công."

Đây là bài học chính của step này. Một malware nhúng rất nhiều thư viện hợp pháp: ở đây là net/http, crypto/ssh, sftp, msteinert/pam… Muốn biết ý đồ, phải đọc code main.* của kẻ tấn công và xem ai gọi ai, không dừng lại ở tên hàm. Chính báo cáo tháng 5 của chúng ta từng kết luận vội từ tên cbPAMConv, và hôm nay bằng chứng đã sửa lại kết luận đó.

🛡️ Ý nghĩa với SOC

  • Đừng đi tìm sai thứ: nếu tin vào "PAM hook", SOC sẽ mất thời gian rà /etc/pam.d/ và các PAM module. Việc đúng phải làm là: tìm cổng SSH lạ đang lắng nghe (không phải cổng 22), và đổi mật khẩu mọi tài khoản, vì cửa hậu chấp nhận mật khẩu thật của hệ thống.
  • Server dùng khóa host thật, nên kiểm tra dấu vân tay SSH không phát hiện được. Phải nhìn vào tiến trình nào đang mở cổng (ss -ltnp).
  • Phòng chống worm: tắt đăng nhập SSH bằng mật khẩu, chỉ dùng key; dùng fail2ban; chặn SSH ra Internet từ server không cần thiết.

STEP 7/10: Từ bằng chứng thành rule phát hiện (YARA)

🎯 Mục tiêu step này

Phân tích chỉ có giá trị khi nó bắt được mẫu tiếp theo. YARA là "ngôn ngữ chân dung" cho file độc: mô tả các đặc điểm nhận dạng, rồi quét hàng nghìn file trong vài giây. Ở step này ta tập tư duy về báo nhầm (false positive), và sửa rule khi hiểu biết thay đổi.

💻 Lệnh và kết quả

Rule v1, viết theo báo cáo ban đầu:

rule Demo_GoPAM_Backdoor {
    strings:
        $pam   = "cbPAMConv"              // (theo báo cáo cũ: hàm đánh cắp mật khẩu)
        $svc   = "systemd-worker.service" // bám trụ
        $miner = "Xmrig_poolAddr"         // cấu hình đào coin
        $cred  = "*main.credential"       // cấu trúc user/pass/ip
    condition:
        uint32(0) == 0x464c457f and 3 of them   // phải là ELF + khớp ≥ 3/4 chuỗi
}
$ yara demo_rule.yar 94f2e4d8.elf
Demo_GoPAM_Backdoor 94f2e4d8.elf                 ← ✅ bắt được mẫu

$ yara -r demo_rule.yar /usr/bin                 ← thử trên file hệ thống sạch
  0 match(es) in 2812 clean files                ← ✅ không báo nhầm

Rule v2, sửa lại theo phát hiện ở step 6:

rule Demo_GoPAM_Backdoor_v2 {
    strings:
        $prot  = "main.protectorHandleClient"     // cửa hậu SSH: code CỦA KẺ TẤN CÔNG
        $kill  = "main.killnbminer"               // diệt miner đối thủ: code CỦA KẺ TẤN CÔNG
        $svc   = "systemd-worker.service"
        $miner = "Xmrig_poolAddr"
        $sig   = "pan-chan's mining island hi!"   // chữ ký tác giả
    condition:
        uint32(0) == 0x464c457f and 4 of them
}
$ yara v2.yar 94f2e4d8.elf
Demo_GoPAM_Backdoor_v2 94f2e4d8.elf              ← ✅ bắt được mẫu
$ yara -r v2.yar /usr/bin
  0 match(es)                                    ← ✅ không báo nhầm

🔍 Diễn giải

Yếu tố của rule Vì sao chọn như vậy
uint32(0) == 0x464c457f 4 byte đầu phải là .ELF (magic bytes ở step 1). Rule bỏ qua ngay mọi file không phải chương trình Linux, nên quét nhanh hơn và ít báo nhầm hơn.
3 of them / 4 of them Không phụ thuộc vào một chuỗi duy nhất. Kẻ tấn công sửa một chuỗi thì rule vẫn bắt được nhờ các chuỗi còn lại.
Chọn chuỗi cụ thể, gắn với hành vi systemd-worker.service, Xmrig_poolAddr: chương trình sạch gần như không bao giờ chứa.
Thử trên 2.812 file sạch trong /usr/bin Một rule chưa test báo nhầm thì chưa được triển khai. 0 match là điều kiện tối thiểu.

Vì sao phải có v2? Bài học từ step 6:

  • Ở v1, $pam = "cbPAMConv" là chuỗi của thư viện công khai msteinert/pam. Bất kỳ chương trình Go hợp pháp nào dùng thư viện này (ví dụ một công cụ đăng nhập nội bộ) cũng chứa chuỗi đó. Nó là chỉ báo yếu, dễ gây báo nhầm ở môi trường thật, dù hôm nay /usr/bin cho 0 match.
  • Ở v2, ta thay bằng tên hàm do chính kẻ tấn công viết (main.protectorHandleClient, main.killnbminer) và chữ ký tác giả. Đây là những thứ chỉ malware này mới có.
  • 👉 Rule tốt nhất được dựng từ code của kẻ tấn công, không phải từ code thư viện. Khi hiểu biết thay đổi, rule cũng phải được sửa theo.

⚠️ Hiểu lầm hay gặp

"Rule YARA khớp nghĩa là máy bị nhiễm."

Sai. Khớp rule nghĩa là "file này giống chân dung". Vẫn cần người xác minh (triage), đặc biệt khi rule dùng chuỗi phổ biến.

"Rule càng nhiều chuỗi càng tốt" / "chọn chuỗi đặc biệt nhất là được, ví dụ URL webhook".

Chưa đủ. URL webhook là duy nhất, nhưng kẻ tấn công đổi nó chỉ trong 1 phút. Chuỗi gắn với cấu trúc code (tên hàm, cấu trúc dữ liệu) khó đổi hơn nhiều, vì muốn đổi phải sửa và build lại cả chương trình.

Quy tắc nội bộ của chúng ta: rule có dưới 3 chuỗi đặc trưng = LOW_QUALITY, không triển khai.

🛡️ Ý nghĩa với SOC

  • Rule này được đưa vào pipeline quét YARA hằng ngày trên kho mẫu honeypot, và có thể dùng để rà soát server nội bộ: yara -r rule.yar / 2>/dev/null.
  • Vòng đời của một rule: viết → test báo nhầm → triển khai → xem lại khi có hiểu biết mới, như ta vừa làm từ v1 sang v2.

STEP 8/10: MITRE ATT&CK và hành động cho đội phòng thủ

🎯 Mục tiêu step này

Chuyển bằng chứng kỹ thuật sang ngôn ngữ chung của ngành, tức MITRE ATT&CK. Mọi SOC, mọi nhà cung cấp và mọi báo cáo tình báo đều dùng chung bộ mã này. Sau đó biến nó thành việc cụ thể mà đội vận hành làm được ngay.

💻 Bảng ATT&CK tổng hợp, đã cập nhật theo step 6

Giai đoạn tấn công Mã MITRE Kỹ thuật Bằng chứng trong mẫu Nguồn
Lẩn tránh T1036.005 Giả danh tên hợp lệ file tên sshd; dịch vụ systemd-worker Step 1, 4
T1564.001 Thư mục ẩn thư mục .3060536107357439887/ Hồ sơ vụ việc
Bám trụ T1543.002 Tạo dịch vụ systemd /bin/systemd-worker, systemctl enable Step 4
Truy cập thông tin đăng nhập T1552.004 Đọc khóa riêng đọc /etc/ssh/ssh_host_*_key → ParsePrivateKey Step 4, 6
T1110.001 Đoán mật khẩu (brute-force) main.sshtry + main.credential {username, password, ip} Step 6
Do thám T1016 Lấy cấu hình mạng 7 dịch vụ tra IP công khai Step 4
T1046 Quét dịch vụ mạng main.randomIP → thử SSH Step 6
Lây lan T1021.004 Dịch vụ từ xa: SSH worm SSH; cửa hậu SSH riêng main.protector Step 6
T1570 Chuyển công cụ sang máy khác tự upload chính nó qua SFTP (os.executable → sftp.Create) Step 6
Gửi dữ liệu ra ngoài T1567 Qua dịch vụ web hợp pháp Discord webhook Step 4
Tác động T1496 Chiếm dụng tài nguyên XMRig + NBMiner Step 4, 6
T1489 Dừng tiến trình main.killxmrig, main.killnbminer (diệt miner đối thủ) Step 6
Truy cập thông tin đăng nhập T1556.003 Sửa đổi PAM cbPAMConv ❌ Bác bỏ ở step 6

Kết luận phân loại: đây là botnet đào coin kiểu worm, lây qua SSH. Nó có cửa hậu SSH riêng dùng khóa thật của máy và mật khẩu thật của hệ thống. Đây không phải công cụ móc PAM để đánh cắp mật khẩu.

🛡️ Danh sách hành động cho SOC (chỉ trình bày, không chạy)

① Phát hiện: rà soát server

ls -l /bin/systemd-worker                          # file bám trụ
systemctl status systemd-worker                     # dịch vụ giả danh
ss -ltnp | grep -v ':22 '                           # cổng lắng nghe lạ: cửa hậu SSH riêng
ps -eo pid,comm,args | grep sshd                    # tiến trình tên "sshd" ...
ls -l /proc/<PID>/exe                               # ... nhưng file thật KHÔNG phải /usr/sbin/sshd
find / -xdev -type d -regex '.*/\.[0-9]+' 2>/dev/null   # thư mục ẩn toàn chữ số
yara -r Demo_GoPAM_Backdoor_v2.yar / 2>/dev/null    # quét bằng rule ở step 7

② Ngăn chặn: khi phát hiện máy nhiễm

Thứ tự Hành động Lý do (từ phân tích)
1 Cô lập máy khỏi mạng Worm đang quét và lây sang máy khác (T1046, T1021.004)
2 Đổi mật khẩu MỌI tài khoản trên máy, và trên mọi máy dùng chung mật khẩu đó Cửa hậu chấp nhận mật khẩu thật của hệ thống qua PAM
3 Tạo lại khóa SSH của host (ssh-keygen -A) Malware đã đọc khóa host. Kẻ tấn công có thể giả danh server của ta
4 Gỡ dịch vụ systemd-worker và file /bin/systemd-worker, rồi diệt tiến trình Bám trụ (T1543.002)
5 Chặn ra ngoài: discord.com/api/webhooks và các dịch vụ tra IP, từ server Cắt kênh C2 / gửi dữ liệu (T1567, T1016)

③ Phòng ngừa: cho toàn hệ thống

  • Tắt đăng nhập SSH bằng mật khẩu (PasswordAuthentication no), chỉ dùng key. Biện pháp này vô hiệu hóa hoàn toàn cơ chế lây lan của worm.
  • Bật fail2ban hoặc giới hạn tốc độ kết nối SSH; không mở SSH ra Internet nếu không cần.
  • Server không cần gọi ra Discord hay các dịch vụ tra IP, nên chặn mặc định ở proxy.
  • Giám sát CPU tăng vọt kéo dài trên server: dấu hiệu đào coin trộm.

⚠️ Hiểu lầm hay gặp

"Diệt tiến trình và xóa file là xong."

Sai. Với malware này, kẻ tấn công đã có mật khẩu thật và khóa SSH của host. Xóa file mà không đổi mật khẩu và tạo lại khóa thì kẻ tấn công quay lại ngay, hoặc worm từ máy khác lây lại.

"Mã MITRE chỉ để trang trí báo cáo."

Sai. Mỗi mã nối thẳng tới danh sách cách phát hiện và cách giảm thiểu có sẵn trên trang MITRE. Mã đúng giúp SOC tra ngay được biện pháp. Mã sai, như T1556.003 nếu ta không sửa, sẽ dẫn đội phòng thủ đi kiểm tra sai chỗ.


STEP 9/10: Đối chiếu kết quả — phân tích tĩnh đoán đúng bao nhiêu?

🎯 Mục tiêu step này

Kết thúc bằng sự đánh giá, không bằng công cụ. Chúng ta đã từng chạy thật mẫu này trong máy ảo cô lập (giai đoạn G4, ngày 26/05):

  • Trước khi chạy, kiểm tra cách ly đạt 7/7: không ra được LAN, không ra được Internet, DNS bị chuyển hướng vào sinkhole.
  • Cho chạy 60 giây, ghi lại mọi lời gọi hệ thống bằng strace.
  • Xong thì khôi phục snapshot sạch.

Giờ đặt dự đoán tĩnh cạnh quan sát động để thấy phương pháp nào trả lời được câu hỏi nào.

💻 Bảng đối chiếu

Dự đoán từ phân tích tĩnh (step 1–8) Kết quả khi chạy trong máy ảo (G4) Đánh giá
Liên kết libpam, libpthread, libc (step 2) Nạp đúng 3 thư viện đó khi khởi động ✅ Đúng
Tự cài vào /bin/systemd-worker (step 4) Tự copy 30,3 MB sang /bin/systemd-worker, chỉ khi có quyền root ✅ Đúng + biết thêm điều kiện
Tạo dịch vụ systemd-worker.service (step 4) Chưa thấy trong 60 giây 🟡 Chưa xác nhận
Cửa hậu SSH riêng main.protector (step 6) Mở cổng lắng nghe 1919 ✅ Đúng, nhưng số cổng chỉ G4 mới thấy
Worm SSH main.sshtry + randomIP (step 6) 65.955 IP khác nhau / 60 giây (≈1.100 IP/giây), không cần root ✅ Đúng, nhưng tốc độ chỉ G4 mới đo được
Gửi về Discord webhook (step 4) Chỉ thấy truy vấn DNS (bị sinkhole), không kết nối được 🟡 Có ý đồ, chưa thấy gửi
Đào coin XMRig / NBMiner (step 4) Không thấy chạy miner 🟡 Đúng như dự đoán ở step 4: cấu hình phải nhận từ C2, mà C2 đã bị chặn
Đọc khóa host /etc/ssh/ssh_host_*_key (step 4, 6) Thấy thử đọc /root/.ssh/id_rsa và ~/.ssh/id_rsa của người dùng ⚠️ Tĩnh bỏ sót một phần, xem bên dưới
PAM để kiểm tra login vào cửa hậu (step 6) libpam được nạp, nhưng không có ai đăng nhập vào cổng 1919 nên không quan sát được 🟡 Code xác nhận, hành vi chưa quan sát
(không đoán được) Dùng 148 MB RAM, ~42% CPU chỉ cho việc quét ➕ Chỉ G4 mới đo được

Về id_rsa, một bài học trung thực: chuỗi id_rsa có trong file (strings | grep -c id_rsa → 1). Nhưng ở step 4 ta grep theo mẫu \.ssh/id_rsa, mà Go lưu tên file và đường dẫn tách rời, nên ta bỏ sót. Phân tích tĩnh chỉ tốt bằng câu hỏi mà người phân tích đặt ra.

Về cổng 1919: số 1919 không nằm dưới dạng chuỗi ký tự trong file. Nó là một hằng số trong code máy, nên strings không thể thấy. Muốn tìm bằng phân tích tĩnh, phải lần trong Ghidra tới tham số của net.Listen bên trong main.protector, tốn công hơn nhiều so với chạy thử 60 giây.

🔍 Tổng kết

Phân tích tĩnh Phân tích động (G4)
Trả lời được Nó có thể làm gì: toàn bộ khả năng, kể cả nhánh code chưa chạy tới Nó thực sự làm gì: số liệu, tốc độ, điều kiện kích hoạt
Ưu điểm An toàn tuyệt đối, không cần môi trường đặc biệt, thấy cả code "ngủ" Có bằng chứng hành vi; thấy hằng số và cấu hình lúc chạy
Hạn chế Không thấy giá trị nhận lúc chạy (pool, cổng); phụ thuộc vào câu hỏi của người phân tích Chỉ thấy những gì xảy ra trong 60 giây đó; malware có thể phát hiện sandbox; cần hạ tầng cách ly
Chi phí Thấp Cao: máy ảo, mạng cách ly, kiểm tra 7 điểm, khôi phục snapshot

Hai phương pháp bổ sung cho nhau: phân tích tĩnh cho bản đồ khả năng và cho biết cần quan sát gì khi chạy; phân tích động xác nhận và đo lường trên bản đồ đó.

📌 Điều cần nhớ

Phân tích tĩnh cho biết nó CÓ THỂ làm gì.
Phân tích động cho biết nó THỰC SỰ làm gì.
Entropy cho biết nên BẮT ĐẦU từ đâu.

⚠️ Hiểu lầm hay gặp

"Chạy thử trong sandbox là đủ, không cần phân tích tĩnh."

Sai. Trong 60 giây G4, malware không đào coin, không gửi dữ liệu, không tạo service. Nếu chỉ dựa vào sandbox, ta sẽ đánh giá thấp nó. Chính phân tích tĩnh mới cho biết nó có cả cửa hậu SSH, worm, miner và cơ chế diệt đối thủ.

"Báo cáo đã viết là đúng."

Không phải lúc nào cũng vậy. Hôm nay ta đã sửa hai điểm trong báo cáo cũ: địa chỉ cbPAMConv và kết luận "PAM hook". Báo cáo cũng là giả thuyết, và cần được kiểm chứng lại khi có bằng chứng mới.

🛡️ Ý nghĩa với SOC: khi nào cần phân tích động?

  • Cần G4 khi: phân tích tĩnh thấy có kết nối mạng hoặc cấu hình nhận từ C2; mẫu bị pack hoặc mã hóa (entropy > 7,5); hoặc cần số liệu để đánh giá mức độ ảnh hưởng (tốc độ lây, tài nguyên).
  • Phân tích tĩnh là đủ khi: mẫu thuộc họ đã biết, khớp rule YARA tin cậy, và chỉ cần IOC để chặn.

STEP 10/10: Đối chứng — khi phân tích tĩnh "bị bịt mắt"

🎯 Mục tiêu step này

Chạy đúng các lệnh hôm nay trên một mẫu khác, f14937a1, cũng thu được từ honeypot của chúng ta, để thấy vì sao quy trình phải chọn hướng phân tích theo entropy. Hiểu một quy tắc qua chỗ nó thất bại thường nhớ lâu hơn qua chỗ nó thành công.

💻 Lệnh và kết quả, so sánh với mẫu chính

$ file f14937a19cf6…
ELF 32-bit LSB executable, ARM, EABI5 version 1 (GNU/Linux)

$ python3 -c '<shannon entropy>' f14937a19cf6…
entropy = 7.9998 bits/byte

$ readelf -h  →  Number of section headers: 0
$ readelf -d  →  There is no dynamic section in this file.

$ strings -n 8 f14937a19cf6… | wc -l        →  245
$ strings -n 8 f14937a19cf6… | head
Zl#B9iI4C   <4y?rGg^   `kgIlj)S   u2D2A/]i   4nl+oWx%      ← toàn rác
$ strings ... | grep -cE 'Xmrig|systemd|http|/etc/'   →  0 chuỗi hữu ích

$ upx -t f14937a19cf6…
upx: NotPackedException: not packed by UPX                  ← công cụ nói "không pack"
$ grep -c 'UPX!' f14937a19cf6…
1                                                           ← nhưng bên trong có dấu UPX!
Câu hỏi 94f2e4d8 (mẫu chính) f14937a1 (mẫu đối chứng)
Kiến trúc x86-64 (server) ARM 32-bit (router, camera, thiết bị IoT)
Entropy 6,36 → đọc được 7,9998 → gần như ngẫu nhiên hoàn toàn
Section headers 35 0
Thư viện khai báo (step 2) libpam, libc… → ra manh mối Không có: không còn manh mối
Strings hữu ích (step 4) hàng chục IOC 0
Tên hàm (step 5–6) 5.978 hàm, 76 hàm main.* Ghidra không đọc được: code đang bị nén
Hướng xử lý theo SOP G3B: Ghidra đầy đủ G3B-ALT: chỉ dùng script, rồi tìm cách giải nén

🔍 Diễn giải

  • Entropy 7,9998 nghĩa là toàn bộ code thật đã bị nén (pack). File chỉ còn một đoạn nhỏ để tự giải nén trong bộ nhớ khi chạy. Trên đĩa, ta chỉ thấy dữ liệu nén, giống hệt file ls.gz (7,99) ở step 3.
  • Kẻ tấn công còn chủ động gây khó: file được nén bằng UPX, một công cụ nén phổ biến có sẵn lệnh giải nén upx -d. Nhưng header đã bị sửa nên chính công cụ UPX báo "not packed" và từ chối giải nén, dù dấu UPX! vẫn nằm trong file.
  • Kiến trúc ARM nhắm vào thiết bị IoT. Sandbox x86-64 của chúng ta không chạy được nó, nên G4 cũng bị chặn.
  • Vậy làm sao ta vẫn biết đây là botnet RedTail đào coin? Thông tin không đến từ file binary. Nó đến từ ngữ cảnh: log honeypot ghi lại kẻ tấn công upload file với tên redtail.arm7, kèm một script clean.sh đọc được, có lệnh diệt miner đối thủ, dọn crontab, chattr -ia, xóa thư mục tạm. 👉 Khi binary "câm", ngữ cảnh lên tiếng.

Các lựa chọn khi gặp mẫu như thế này:

  1. Sửa header UPX bằng tay rồi giải nén, sau đó quay lại G3B.
  2. Chạy trong trình giả lập ARM (QEMU) đã cách ly, để code tự giải nén rồi trích bộ nhớ.
  3. Dựa vào ngữ cảnh: log phiên tấn công, script đi kèm, tên file, tình báo về họ malware đã biết.

Đây cũng là lý do của đề xuất sửa SOP ở step 3: kiểm tra dấu UPX! trước, rồi mới xét entropy. Với mẫu này, entropy 8,0 đẩy thẳng sang G3B-ALT, và bước "thử giải nén UPX" không bao giờ được thử.

⚠️ Hiểu lầm hay gặp

"Công cụ nói 'not packed' thì nghĩa là không bị pack."

Sai. Công cụ chỉ kiểm tra định dạng chuẩn. Kẻ tấn công biết điều đó và cố tình làm hỏng định dạng. Luôn đối chiếu nhiều dấu hiệu: entropy 8,0 + dấu UPX! + 0 section header cùng nói một điều.

"Không đọc được binary thì không phân tích được."

Sai. Binary chỉ là một nguồn bằng chứng. Log honeypot, script đi kèm, hạ tầng mạng và tình báo về họ malware thường đủ để phân loại và phòng thủ.

🛡️ Ý nghĩa với SOC

  • Mẫu ARM nghĩa là thiết bị IoT (router, camera, NAS) cũng là mục tiêu. Hãy đưa chúng vào phạm vi giám sát, đổi mật khẩu mặc định, và không mở SSH/Telnet ra Internet.
  • Khi gặp file entropy ≈ 8 + có dấu UPX!: ghi vào ticket "packed, header UPX bị sửa", chuyển cho bộ phận phân tích chuyên sâu, và trong lúc chờ thì dựa vào IOC ngữ cảnh (IP upload, tên file, script đi kèm) để chặn ngay.

🎓 TỔNG KẾT BUỔI PHÂN TÍCH

Hành trình hôm nay, chỉ đọc file mà không chạy lần nào:

Step 0  Hash và an toàn          → định danh vật chứng
Step 1  Magic bytes              → "sshd" là đồ giả, viết bằng Go
Step 2  Thư viện                 → libpam: giả thuyết về đăng nhập
Step 3  Entropy 6,36             → đọc được → đi nhánh Ghidra
Step 4  Strings                  → miner, persistence, C2 Discord, do thám IP
Step 5  Symbols                  → cbPAMConv + API PAM: 2 giả thuyết
Step 6  Ghidra                   → bác bỏ "PAM hook"; xác nhận cửa hậu SSH + worm
Step 7  YARA                     → rule v2 dựng từ code của kẻ tấn công, 0 báo nhầm
Step 8  MITRE ATT&CK             → 12 kỹ thuật + kế hoạch ứng phó
Step 9  Đối chiếu với G4         → tĩnh = khả năng; động = hành vi thực
Step 10 Mẫu bị pack              → khi binary "câm", ngữ cảnh lên tiếng

Phân tích tĩnh cho biết nó CÓ THỂ làm gì · Phân tích động cho biết nó THỰC SỰ làm gì · Entropy cho biết BẮT ĐẦU từ đâu · Và mọi kết luận, kể cả trong báo cáo của chính mình, đều phải được kiểm chứng bằng bằng chứng.

## Mục lục - Step 0: Quy tắc an toàn và mở hồ sơ vụ việc - Step 1: Nhận dạng file - Step 2: Cấu trúc ELF và thư viện - Step 3: Entropy và chọn hướng phân tích - Step 4: Săn strings và bảng IOC - Step 5: Bảng ký hiệu (symbols) - Step 6: Ghidra, kiểm chứng giả thuyết - Step 7: Rule YARA - Step 8: MITRE ATT&CK và hành động SOC - Step 9: Đối chiếu với phân tích động (G4) - Step 10: Đối chứng với mẫu bị pack (f14937a1) --- # STEP 0/10: Quy tắc an toàn và mở hồ sơ vụ việc ## 📁 Hồ sơ vụ việc > Ngày **25/05/2026**, honeypot SSH của chúng ta nhận được một file **30 MB tên `sshd`**. Kẻ tấn công upload nó vào một thư mục ẩn có tên toàn chữ số (`.3060536107357439887/`). > **Nhiệm vụ:** xác định file này có nguy hiểm không và nó làm gì, mà **không chạy nó lần nào**. Đó là **phân tích tĩnh (static analysis)**. ## 🎯 Mục tiêu step này Coi malware như **vật chứng**: chưa phân tích gì cả, việc đầu tiên là xác lập an toàn và danh tính của file. ## 💻 Lệnh và kết quả ``` $ ls -l 94f2e4d8.elf -r-------- 1 dell dell 30304472 Oct 14 08:29 94f2e4d8.elf $ sha256sum 94f2e4d8.elf 94f2e4d8d443… 94f2e4d8.elf ✓ SHA-256 khớp với hồ sơ thu thập (G1) ``` ## 🔍 Diễn giải | Quan sát | Ý nghĩa | |---|---| | Quyền `-r--------` (400) | Chỉ chủ sở hữu được **đọc**. Không ai có quyền **ghi** hay **chạy**, nên không thể lỡ tay kích hoạt file. | | SHA-256 `94f2e4d8d443…` | Là "dấu vân tay" của file: chỉ cần đổi 1 byte là hash đổi hoàn toàn. Hash khớp với lúc thu thập ngày 25/05 nghĩa là ta đang phân tích **đúng file đó**, chưa bị sửa. | | Trên màn chiếu chỉ hiện 12 ký tự đầu | Hash đầy đủ là thông tin tình báo nội bộ nên được che. 12 ký tự đã đủ để gọi tên mẫu. | **Vì sao hash là việc đầu tiên:** - **Tên file do kẻ tấn công đặt**, nó có thể là `sshd`, `invoice.pdf` hay gì cũng được. Hash mới là danh tính thật. - **Chain of custody:** phải chứng minh được file phân tích chính là file đã thu được. - Hash là **chìa khóa tra cứu**: dùng để tìm trên VirusTotal, trong kho mẫu nội bộ, và ghi vào ticket. **5 quy tắc vàng của phân tích tĩnh:** 1. **Không bao giờ chạy** file mẫu. 2. Chỉ làm việc trên **bản sao read-only**. 3. **Hash trước và sau** khi phân tích. 4. Để mẫu trong **thư mục riêng**, không lẫn với file làm việc. 5. Mẫu **không rời khỏi máy phân tích** (Kali), không copy sang máy học viên. ## ⚠️ Hiểu lầm hay gặp > *"Đã `chmod 400` thì file an toàn rồi."* **Sai.** `chmod 400` chỉ chặn việc **lỡ tay chạy** file. An toàn thật sự đến từ hai việc: **không chạy** và **cách ly**. Nếu cần quan sát hành vi khi chạy, ta làm ở G4 trong máy ảo cô lập (phân tích động). ## 🛡️ Ý nghĩa với SOC Khi phát hiện một file lạ trên server, việc đầu tiên là **`sha256sum`, ghi vào ticket, rồi tra cứu**. Không mở, không chạy, không gửi file qua chat. --- # STEP 1/10: Nhận dạng file — thực chất nó là gì? ## 🎯 Mục tiêu step này Chứng minh rằng **tên file không phải bằng chứng**. Loại file được quyết định bởi **nội dung**, cụ thể là vài byte đầu tiên (magic bytes). ## 💻 Lệnh và kết quả ``` $ file 94f2e4d8.elf 94f2e4d8.elf: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=300bf5c7e304…, for GNU/Linux 3.2.0, stripped $ xxd 94f2e4d8.elf | head -1 00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............ $ ls -lh 94f2e4d8.elf 29M 94f2e4d8.elf ``` **So sánh với `sshd` thật trên chính máy Kali này:** ``` $ ls -l /usr/sbin/sshd -rwxr-xr-x 1 root root 580512 Feb 19 2026 /usr/sbin/sshd ← 0,58 MB $ file /usr/sbin/sshd /usr/sbin/sshd: ELF 64-bit LSB pie executable, x86-64, ... ``` ## 🔍 Diễn giải | Quan sát | Ý nghĩa | |---|---| | `7f 45 4c 46` = `.ELF` | **Magic bytes**: 4 byte đầu file cho biết đây là chương trình chạy được trên Linux (định dạng ELF). Linux không dựa vào đuôi file. | | `ELF 64-bit … x86-64` | Chạy được trên máy chủ Intel/AMD 64-bit, loại server phổ biến nhất. Mục tiêu là các server thông thường. | | `dynamically linked` | Chương trình gọi thêm thư viện của hệ thống khi chạy. Step 2 sẽ xem nó cần những thư viện nào, và đó là manh mối quan trọng. | | `stripped` | Kẻ tấn công đã **xóa bảng tên hàm nội bộ** để gây khó cho người phân tích. | | **29 MB vs 0,58 MB** | File giả **lớn gấp 50 lần** `sshd` thật. Lý do: viết bằng **ngôn ngữ Go**, vốn đóng gói toàn bộ runtime và thư viện vào một file. Kích thước bất thường là dấu hiệu nhận dạng đầu tiên. | | `executable` vs `pie executable` | `sshd` thật được biên dịch kiểu PIE (có cơ chế bảo vệ bộ nhớ ASLR) theo chuẩn của bản phân phối. File giả **không phải PIE**, nên không phải do Debian/Kali build ra. | **Kết luận step 1:** đây **không phải** `sshd`. Đây là một chương trình Go 64-bit **giả danh** `sshd`. → Ghi lên bảng IOC: **Masquerading (MITRE T1036)**: tên `sshd`, nằm trong thư mục ẩn toàn chữ số. ## ⚠️ Hiểu lầm hay gặp > *"Nhìn tên và đuôi file là biết loại file."* **Sai.** Trên Linux, tên và đuôi file là do người tạo đặt tùy ý. Kẻ tấn công cố tình đặt tên `sshd` để người quản trị liếc qua `ps` hay `ls` mà tưởng là dịch vụ SSH bình thường. Chỉ có **nội dung file** (magic bytes, cấu trúc) mới đáng tin. ## 🛡️ Ý nghĩa với SOC - `sshd` thật **chỉ nằm ở `/usr/sbin/sshd`**. Một tiến trình tên `sshd` chạy từ `/tmp`, `/dev/shm`, thư mục home hay thư mục ẩn là **dấu hiệu đỏ**. - Lệnh kiểm tra nhanh trên server: `ls -l /proc/<PID>/exe` (xem file thật đằng sau tiến trình), và so sánh hash với gói chính chủ (`debsums`, `rpm -V`). --- # STEP 2/10: Cấu trúc ELF — nó cần gì để chạy? ## 🎯 Mục tiêu step này Một chương trình luôn phải **khai báo những thư viện nó cần** thì hệ điều hành mới nạp được. Danh sách đó là **lời tự khai** về khả năng của chương trình, đọc được mà không cần chạy và không cần biết assembly. ## 💻 Lệnh và kết quả **Phần đầu file (ELF header):** ``` $ readelf -h 94f2e4d8.elf Class: ELF64 Type: EXEC (Executable file) Machine: Advanced Micro Devices X86-64 Entry point address: 0x403470 Number of section headers: 35 ``` **Các thư viện nó khai báo cần dùng:** ``` $ readelf -d 94f2e4d8.elf | grep NEEDED (NEEDED) Shared library: [libpam.so.0] ← ⚠️ (NEEDED) Shared library: [libpthread.so.0] (NEEDED) Shared library: [libc.so.6] ``` **So sánh với `sshd` thật trên Kali:** ``` $ readelf -d /usr/sbin/sshd | grep NEEDED (NEEDED) Shared library: [libselinux.so.1] (NEEDED) Shared library: [libcrypto.so.3] ← thư viện mã hóa (SSH cần) (NEEDED) Shared library: [libc.so.6] ``` ## 🔍 Diễn giải | Quan sát | Ý nghĩa | |---|---| | `Entry point 0x403470` | Địa chỉ lệnh đầu tiên chạy khi chương trình khởi động. Ghi lại để dùng ở step 6 (Ghidra). | | `35 section headers` | File **còn nguyên vẹn**. Trong cùng chiến dịch này, ta đã thu được 9 biến thể khác có **0** section header: đó là các bản **bị cắt cụt**, chạy là crash ngay. Mẫu này là **bản đầy đủ duy nhất**, nên đáng phân tích sâu. | | `libc.so.6`, `libpthread.so.0` | Thư viện nền tảng mà gần như mọi chương trình đều dùng, không có gì lạ. | | **`libpam.so.0`** | **PAM (Pluggable Authentication Modules)** là thư viện **xác thực đăng nhập** của Linux. Nó là cơ chế kiểm tra user/mật khẩu mà SSH, `login`, `sudo` đều dùng. | | Thiếu `libcrypto` | Một `sshd` thật cần thư viện mã hóa. File này không cần, vì Go **tự mang theo** bộ mã hóa riêng. Thêm một bằng chứng nó không phải `sshd`. | **Câu hỏi then chốt của buổi học:** > 🔎 Một chương trình lạ, giả danh `sshd`, **tại sao lại cần đến thư viện xác thực đăng nhập?** Ta ghi lên bảng IOC một **giả thuyết**, chưa phải kết luận: | Loại | Bằng chứng | Step | Độ tin cậy | |---|---|---|---| | Masquerading (T1036) | tên `sshd`, thư mục ẩn | 1 | ✅ Chắc chắn | | **Liên quan đến đăng nhập / mật khẩu?** | liên kết `libpam.so.0` | 2 | 🟡 **Giả thuyết** | Có vài khả năng: (a) đánh cắp mật khẩu khi người dùng đăng nhập, (b) **kiểm tra** user/mật khẩu trên chính máy nạn nhân, (c) phục vụ một cửa hậu có màn đăng nhập riêng. **Chưa đủ bằng chứng để chọn.** Ta sẽ kiểm chứng ở step 5 và step 6. ## ⚠️ Hiểu lầm hay gặp > *"Phân tích tĩnh nghĩa là phải đọc assembly."* **Sai.** Chỉ đọc **metadata** (header, danh sách thư viện) trong vài giây ta đã biết: file nguyên vẹn, 64-bit, viết bằng Go, và dính đến xác thực đăng nhập. Phần lớn câu trả lời ban đầu đến từ metadata. Assembly và decompiler để dành cho bước sau, khi đã biết cần tìm gì. > *"Thấy thư viện X là kết luận được nó làm Y."* **Chưa được.** Thư viện cho biết **khả năng có thể có**, chưa chứng minh **ý đồ**. Vì thế ở đây ta ghi là *giả thuyết*. ## 🛡️ Ý nghĩa với SOC - Một binary lạ **nằm ngoài đường dẫn hệ thống** mà lại liên kết `libpam` là **cảnh báo ưu tiên cao**. Lệnh rà nhanh trên server: `ldd <file> | grep pam`. - Ghi chú vào ticket: *"binary lạ có liên kết thư viện xác thực, cần điều tra khả năng liên quan đến thông tin đăng nhập"*. --- # STEP 3/10: Entropy và chọn hướng phân tích ## 🎯 Mục tiêu step này Trước khi đọc nội dung, phải biết **nội dung có đọc được không**. Malware thường được **nén hoặc mã hóa (pack)** để che giấu. **Entropy** đo độ "ngẫu nhiên" của dữ liệu và cho biết nên đi theo **hướng phân tích nào**. Đây chính là chỉ số học viên đã tính trong Lab 5. ## 💻 Lệnh và kết quả ``` $ python3 -c '<shannon entropy trên toàn bộ byte>' 94f2e4d8.elf entropy = 6.3601 bits/byte ``` **Thang so sánh, đo ngay trên máy này:** ``` File Entropy Bản chất --------------------------- ------- ----------------------------- /usr/sbin/sshd (thật) 5.66 chương trình bình thường /usr/bin/ls 5.97 chương trình bình thường 94f2e4d8.elf (mẫu) ► 6.36 ◄ ? ls nén bằng gzip 7.99 dữ liệu nén 2 MB byte ngẫu nhiên 8.00 ngẫu nhiên hoàn toàn (mức tối đa) ``` **Bảng định tuyến trong quy trình của chúng ta (SOP V3.2):** ``` entropy < 7.0 → G3B : phân tích đầy đủ bằng Ghidra 7.0 – 7.5 → thử giải nén UPX rồi quay lại G3B entropy > 7.5 → G3B-ALT : chỉ dùng script (Ghidra sẽ không đọc được) ``` ## 🔍 Diễn giải | Quan sát | Ý nghĩa | |---|---| | **Entropy là gì** | Đo trung bình cần bao nhiêu bit để mã hóa 1 byte. Tối đa là **8**, khi mọi giá trị byte xuất hiện đều nhau như dữ liệu ngẫu nhiên. Code máy và chuỗi chữ có quy luật lặp lại nên thấp hơn, khoảng 5–6,5. | | Mẫu = **6,36** | Nằm trong vùng **chương trình bình thường**, cao hơn `sshd` một chút vì Go nhúng nhiều dữ liệu. **Không bị pack hay mã hóa.** | | Đi theo nhánh **G3B** | Chuỗi ký tự, tên hàm, code đều **đọc được**. Step 4 (strings) và step 6 (Ghidra) sẽ cho kết quả tốt. | | So với file nén (7,99) | Nếu malware bị pack thì entropy sẽ sát mức 8 như vậy, và `strings` chỉ ra rác. Ta sẽ thấy tận mắt ở step 10 với một mẫu khác. | **Một hạn chế có thật của SOP, nói rõ để thấy quy trình nào cũng cần kiểm chứng bằng dữ liệu:** Khi đo 205 mẫu ELF trong kho, nhóm DietPi thấy chúng chia thành 2 cụm rõ rệt: **131 mẫu ở 5,56–6,42** và **74 mẫu nén UPX ở 7,99–8,00**. **Không mẫu nào rơi vào dải 7,0–7,5.** Nghĩa là bước "thử giải nén UPX" trong SOP **chưa bao giờ được kích hoạt**. Đề xuất sửa: **kiểm tra dấu hiệu UPX trước**, rồi mới xét entropy. Đây cũng là cầu nối sang phần **Decision Tree** của Module 5: khi máy học từ dữ liệu, ngưỡng nó học được là **≈7,2**, đúng điểm giữa khoảng trống giữa hai cụm. ## ⚠️ Hiểu lầm hay gặp > *"Entropy cao nghĩa là malware."* **Sai.** File ZIP, ảnh JPEG, video, bản backup mã hóa đều có entropy khoảng 8. Entropy cho biết **phân tích cách nào**, không kết luận **có độc hay không**. > *"Entropy thấp nghĩa là an toàn."* **Sai.** Mẫu này entropy chỉ 6,36, trông rất "bình thường", nhưng như các step sau sẽ cho thấy, nó là một malware nguy hiểm. ## 🛡️ Ý nghĩa với SOC - Entropy là bước **phân loại (triage) nhanh nhất**: vài giây là biết nên mở Ghidra hay phải tìm cách giải nén trước, đỡ lãng phí thời gian. - Gặp một file thực thi có entropy sát 8 nằm ngoài thư mục hệ thống: khả năng cao bị pack, nên **tăng mức nghi ngờ** và ưu tiên đưa vào sandbox. --- # STEP 4/10: Săn chuỗi ký tự (strings) và điền bảng IOC ## 🎯 Mục tiêu step này Rèn kỹ năng cốt lõi của người phân tích: biến **hàng nghìn chuỗi ký tự** thành **một bảng bằng chứng gọn, có phân loại**. Bài học chính: **tìm theo giả thuyết** nhanh hơn rất nhiều so với đọc từng dòng. ## 💻 Lệnh và kết quả **Bước 1: xem khối lượng.** ``` $ strings -n 8 94f2e4d8.elf | wc -l 13789 strings (>= 8 chars) ``` Gần **14.000 chuỗi**, không ai đọc hết được. Ta tìm theo **từng nhóm hành vi**: > 📌 Lưu ý khi đọc kết quả: Go lưu các chuỗi **nối liền nhau, không có ký tự phân cách**, nên mỗi kết quả dính thêm một ít chữ của chuỗi bên cạnh. Chỉ cần nhìn vào phần ta đang tìm. **[1] Đào tiền ảo** ``` Xmrig_algo Xmrig_enabled Xmrig_nicehash Xmrig_poolAddr Xmrig_poolPass Xmrig_poolUser json:"nbminer_poolAddr" json:"nbminer_poolPass" json:"nbminer_poolUser" ``` **[2] Bám trụ trên máy (persistence)** ``` ExecStart=/bin/systemd-worker service systemd-worker enable || systemctl enable systemd-worker.service ``` **[3] Kênh liên lạc với kẻ tấn công (C2)** ``` https://discord.com/api/webhooks/<ID>/<TOKEN-MASKED> ← đã che ``` **[4] Do thám nạn nhân: "IP công khai của tôi là gì?"** ``` http://ipgrab.io/ https://ident.me/ http://inet-ip.info/ip https://api.ipify.org/ http://ipinfo.io/ip https://checkip.amazonaws.com/ https://ip.seeip.org/ ``` **[5] Truy cập khóa SSH của máy chủ** ``` /etc/ssh/ssh_host_rsa_key /etc/ssh/ssh_host_ecdsa_key /etc/ssh/ssh_host_ed25519_key ``` **[6] Chữ ký của tác giả** ``` pan-chan's mining island hi! ``` **[7] Lệnh lạ (giả thuyết)** ``` ^(sharepeer)(\s)(.*)$ ``` ## 🔍 Diễn giải: bảng IOC sau step 4 | # | Loại hành vi | Bằng chứng | MITRE ATT&CK | Độ tin cậy | |---|---|---|---|---| | — | Giả danh | tên `sshd`, thư mục ẩn | T1036 | ✅ Chắc chắn | | — | Liên quan đăng nhập? | liên kết `libpam` | — | 🟡 Giả thuyết (step 2) | | 1 | **Đào coin bằng 2 phần mềm**: XMRig và NBMiner | biến cấu hình `Xmrig_*`, `nbminer_*` | T1496 Resource Hijacking | 🟠 Có khả năng | | 2 | **Tự cài thành dịch vụ hệ thống**, giả tên `systemd` để trông như thành phần của Linux | `/bin/systemd-worker`, `systemctl enable` | T1543.002 Systemd Service | 🟠 Có khả năng | | 3 | **Gửi dữ liệu về qua Discord**: dùng dịch vụ hợp pháp nên tường lửa khó chặn | URL webhook Discord | T1102 / T1041 | 🟠 Có khả năng | | 4 | **Lấy IP công khai của nạn nhân**, dùng tới **7 dịch vụ** dự phòng để chắc chắn lấy được | 7 URL tra IP | T1016 | 🟠 Có khả năng | | 5 | **Đọc khóa SSH của máy chủ** | `/etc/ssh/ssh_host_*_key` | T1552.004 Private Keys | 🟡 Giả thuyết | | 6 | Chữ ký tác giả, gợi ý để **truy vết nhóm tấn công** | `pan-chan's mining island` | — | ✅ Có trong file | | 7 | Có thể là **lệnh giao tiếp ngang hàng (P2P)** giữa các máy bị nhiễm | regex `sharepeer` | — | 🟡 Giả thuyết | **Điều thú vị: không tìm thấy địa chỉ pool đào coin hay ví tiền.** Chỉ có **tên biến** (`Xmrig_poolAddr`…), không có **giá trị**. Kết luận: cấu hình được **gửi xuống từ kẻ tấn công lúc chạy**. Phân tích tĩnh thấy được "**ô trống**" chứ không thấy "**nội dung điền vào**". Đây là giới hạn đầu tiên của phân tích tĩnh, và ta sẽ quay lại ở step 9. ## ⚠️ Hiểu lầm hay gặp > *"Có chuỗi ký tự trong file nghĩa là chương trình chắc chắn làm việc đó."* **Chưa chắc.** Chuỗi cho thấy **khả năng** và **ý đồ**, chưa chứng minh **hành vi thực tế**: có thể là code chết, mồi nhử, hoặc nằm trong thư viện không được gọi tới. Vì thế các dòng trên chỉ ở mức 🟠 *có khả năng*. Muốn lên mức ✅ phải có bằng chứng từ code (step 6) hoặc từ chạy thử trong sandbox (G4). ## 🛡️ Ý nghĩa với SOC Mỗi dòng trong bảng IOC thành **một quy tắc phát hiện**: - **Proxy/Firewall:** cảnh báo khi **server** (không phải máy người dùng) gọi tới `discord.com/api/webhooks` hoặc các dịch vụ tra IP như `ipify`, `ident.me`, `checkip.amazonaws.com`. - **EDR/giám sát file:** cảnh báo khi xuất hiện `/bin/systemd-worker` hoặc dịch vụ `systemd-worker.service`. - **Giám sát hiệu năng:** CPU server tăng vọt kéo dài, kèm kết nối ra ngoài lạ, là dấu hiệu bị đào coin trộm. --- # STEP 5/10: Bảng ký hiệu (symbols) — nó gọi những hàm nào? ## 🎯 Mục tiêu step này Ở step 1 ta thấy file đã bị `stripped` (xóa tên hàm). Nhưng có một loại tên **không thể xóa**: tên các hàm mà chương trình **trao đổi với thư viện bên ngoài**, vì hệ điều hành cần chúng để nối chương trình với thư viện. Đọc bảng này để **làm rõ giả thuyết PAM** từ step 2. ## 💻 Lệnh và kết quả **Hai hàm có tên liên quan PAM nằm ngay trong chương trình:** ``` $ readelf --dyn-syms -W 94f2e4d8.elf | grep -E ' (cbPAMConv|init_pam_conv)$' 83: 00000000006c0470 19 FUNC GLOBAL DEFAULT 14 init_pam_conv 86: 00000000006bfed0 168 FUNC GLOBAL DEFAULT 14 cbPAMConv ``` **Các hàm PAM mà nó gọi từ thư viện `libpam`:** ``` pam_start pam_authenticate pam_acct_mgmt pam_setcred pam_chauthtok pam_open_session pam_close_session pam_get_item pam_set_item pam_getenv pam_getenvlist pam_putenv pam_strerror pam_end (pam_conv) ``` **Các hàm đổi danh tính người dùng/nhóm:** ``` _cgo_libc_setuid _cgo_libc_seteuid _cgo_libc_setreuid _cgo_libc_setresuid _cgo_libc_setgid _cgo_libc_setegid _cgo_libc_setregid _cgo_libc_setresgid _cgo_libc_setgroups ``` ## 🔍 Diễn giải | Quan sát | Ý nghĩa | |---|---| | `cbPAMConv` tại `0x6bfed0`, dài 168 byte | "**cb**" = *callback*, "**PAMConv**" = *PAM conversation*. Trong PAM, **hàm hội thoại (conversation function)** là chỗ hỏi người dùng *"Mật khẩu?"* rồi chuyển câu trả lời cho hệ thống kiểm tra. Ai viết hàm này thì **nhìn thấy mật khẩu**. | | Gọi gần như **toàn bộ** API PAM | `pam_start` mở phiên, `pam_authenticate` **kiểm tra user/mật khẩu**, `pam_open_session` mở phiên đăng nhập, `pam_chauthtok` **đổi mật khẩu**. Chương trình có đủ công cụ để **tự thực hiện một quy trình đăng nhập hoàn chỉnh**. | | `_cgo_libc_set*uid/gid` | "cgo" là cơ chế để code **Go gọi sang thư viện C**. Các hàm `setuid`/`setgid` dùng để **đổi danh tính tiến trình**, ví dụ chuyển sang chạy dưới quyền một user khác sau khi đăng nhập. Một chương trình có màn đăng nhập riêng sẽ cần đúng các hàm này. | | Tiền tố `_cgo_` khắp nơi | Xác nhận lại: đây là **Go dùng cgo** để gọi thư viện C (`libpam`). Ta cần điều này để hiểu step 6. | **Cập nhật bảng IOC:** | Loại | Bằng chứng | Độ tin cậy | |---|---|---| | Có khả năng xử lý đăng nhập và mật khẩu | `cbPAMConv` + bộ API PAM đầy đủ + `setuid/setgid` | 🟠 **Có khả năng có khả năng này** | **Tới đây có 2 cách giải thích, và phải chọn bằng bằng chứng:** - **Giả thuyết A, "cửa hậu ăn cắp mật khẩu":** kẻ tấn công **tự viết** `cbPAMConv` để chép lại mật khẩu mỗi khi có người đăng nhập (MITRE T1556.003). Đây cũng là kết luận ban đầu trong báo cáo tháng 5 của chúng ta. - **Giả thuyết B, "dùng thư viện có sẵn":** `cbPAMConv` là **một phần của thư viện mã nguồn mở** mà kẻ tấn công nhúng vào, để chương trình **tự kiểm tra** user/mật khẩu trên máy nạn nhân. Cả hai đều khớp với những gì thấy ở step 5. **Chỉ nhìn tên hàm thì không phân biệt được.** Cần đọc code thật, đó là việc của Ghidra ở step 6. ## ⚠️ Hiểu lầm hay gặp > *"File đã bị strip thì mất hết tên hàm."* **Sai.** Strip chỉ xóa tên các hàm **nội bộ**. Các tên **dùng để nối với thư viện ngoài** (dynamic symbols) **bắt buộc phải giữ lại**, nếu không chương trình không chạy được. Đây là "điểm yếu" mà kẻ tấn công không xóa được. > *"Tên hàm nói lên ý đồ."* **Chưa chắc.** Tên hàm cho biết **chức năng**, không cho biết **ai viết** và **dùng vào việc gì**. Một hàm tên `cbPAMConv` có thể do kẻ tấn công viết, nhưng cũng có thể là của một thư viện hợp pháp. **Đừng kết luận chỉ từ tên**, kể cả khi tên nghe rất "đáng sợ". ## 🛡️ Ý nghĩa với SOC - Một binary **ngoài đường dẫn hệ thống** mà gọi `pam_authenticate` là đáng điều tra: trên một server bình thường, chỉ các chương trình hệ thống như `sshd`, `login`, `sudo`, `su` mới làm việc này. - **Kiểm tra nhanh:** `ldd <file> | grep pam`, rồi `nm -D <file> | grep pam_`. - **Khi viết báo cáo:** ghi rõ mức độ tin cậy (*"có khả năng"* hay *"đã xác nhận"*). Báo cáo nói quá sẽ dẫn đến phản ứng sai, ví dụ đi tìm một PAM module bị cài trộm mà thật ra không tồn tại. --- # STEP 6/10: Ghidra — đọc code để kiểm chứng giả thuyết ## 🎯 Mục tiêu step này Step 5 để lại hai giả thuyết về PAM, và chỉ nhìn tên hàm thì không phân biệt được. Giờ ta dùng **Ghidra**, công cụ dịch ngược mã nguồn mở của NSA, để đọc code thật và trả lời 3 câu: **ai viết** `cbPAMConv`, **ai gọi** PAM, và **code của chính kẻ tấn công** kể câu chuyện gì. *(Project Ghidra đã được phân tích sẵn trước buổi học, vì bước tự động phân tích mất khoảng 4 phút.)* ## 💻 Kết quả từ Ghidra **① Go giữ lại tên hàm dù file đã bị strip** ``` Tổng số hàm có tên khôi phục được: 5.978 ``` Go lưu một bảng tên hàm riêng (**pclntab**) để in thông báo lỗi. Lệnh `strip` không xóa bảng này. Bất lợi cho kẻ tấn công, có lợi cho ta. **② `cbPAMConv` là của ai? Trả lời: của một thư viện mã nguồn mở** ``` Code C tại 0x6bfed0 (cbPAMConv), dịch ngược: crosscall2(..., _cgoexp_eba3282b571c_cbPAMConv, ...); ← chỉ là cầu nối C → Go Hàm Go thật nằm trong gói: 0066d480 github.com/msteinert/pam.cbPAMConv ← thư viện công khai trên GitHub ``` `github.com/msteinert/pam` là **thư viện Go hợp pháp** để gọi PAM. `cbPAMConv` là **callback chuẩn của thư viện đó**, không phải code kẻ tấn công tự viết. **③ Ai thật sự gọi PAM, và để làm gì?** ``` pam.Start ← chỉ được gọi bởi: main.protector.func1 pam.(*Transaction).Authenticate ← chỉ được gọi bởi: main.protector.func1 main.protector.func1 gọi: os/user.Lookup → pam.Start → pam.Authenticate → time.Sleep main.protector gọi: os.ReadFile → ssh.ParsePrivateKey → ssh.AddHostKey → net.Listen → Accept main.protectorHandleClient.func1 gọi: pty.open → term.ReadLine → exec.Command → Cmd.Start ``` **④ Code của chính kẻ tấn công (76 hàm `main.*`), xếp theo chức năng** ``` Đào coin : main.miner, main.killxmrig, main.killnbminer Lây lan (worm) : main.spreader → main.randomIP → main.sshtry Cửa hậu SSH : main.protector, main.protectorHandleClient Tự bảo vệ : main.antikill, main.antitaskmanager, main.killdup, main.kill_by_port Mạng ngang hàng : main.p2p, main.p2ptry, main.p2phandleconnection Khác : main.updater, main.getMyIP, main.loadpayload, main.startupmanager ``` **⑤ Hàm lây lan `main.sshtry` gọi gì?** ``` net.DialTimeout → ssh.NewClient → sftp.NewClient → sftp.Mkdir → sftp.Create → File.Write → os.executable → ssh.Session.Start (+ shuffleStrings, generate_seed: xáo trộn danh sách mật khẩu) ``` **⑥ Cấu trúc dữ liệu `main.credential`** ``` +0x00 username : string +0x10 password : string +0x20 ip : string ``` ## 🔍 Diễn giải: kiểm chứng giả thuyết | Giả thuyết (step 5) | Bằng chứng từ Ghidra | Kết luận | |---|---|---| | **A.** Kẻ tấn công tự viết `cbPAMConv` để **chép mật khẩu** khi người dùng đăng nhập SSH thật (T1556.003) | `cbPAMConv` thuộc **thư viện công khai**. Không có PAM module nào bị cài, và không có code nào móc vào `sshd` thật. | ❌ **Bác bỏ**: không có bằng chứng | | **B.** Dùng thư viện PAM để **tự kiểm tra** user/mật khẩu | PAM chỉ được gọi từ `main.protector.func1`, tức là **hàm kiểm tra mật khẩu của một SSH server riêng** do malware dựng lên | ✅ **Xác nhận** | **Câu chuyện thật mà code kể lại:** 1. 🚪 **Cửa hậu SSH riêng (`main.protector`):** malware **đọc khóa SSH thật của máy nạn nhân** (chính các file `/etc/ssh/ssh_host_*_key` ở step 4), rồi mở một **SSH server riêng**. Server này dùng khóa đó nên **dấu vân tay giống hệt server thật** và không gây cảnh báo. Khi có người đăng nhập, nó hỏi **PAM của hệ thống** xem user/mật khẩu có đúng không, rồi **cấp shell** (`pty` + `exec.Command`). Kẻ tấn công chỉ cần biết một mật khẩu thật là vào được bằng cửa riêng, kể cả khi admin đã đổi cấu hình `sshd`. 2. 🐛 **Worm SSH (`main.spreader` → `main.sshtry`):** sinh **IP ngẫu nhiên**, thử đăng nhập SSH bằng danh sách user/mật khẩu đã xáo trộn (`main.credential` = {username, password, ip}). Khi vào được thì **upload chính nó qua SFTP** (`os.executable` + `sftp.Create/Write`) và **chạy nó**. 👉 **Đây chính là cách file này đến được honeypot của chúng ta**: một máy bị nhiễm khác đã brute-force rồi SFTP nó vào thư mục ẩn! 3. ⛏️ **Đào coin và diệt đối thủ:** `main.killxmrig` / `main.killnbminer` **diệt các miner khác** đang chạy trên máy để độc chiếm CPU. 4. 🛡️ **Tự bảo vệ:** `antikill`, `antitaskmanager`, `killdup` chống bị tắt và chống chạy trùng. **Cập nhật bảng IOC:** | Loại | Bằng chứng | MITRE | Độ tin cậy | |---|---|---|---| | ~~Đánh cắp mật khẩu bằng PAM hook~~ | ~~`cbPAMConv`~~ | ~~T1556.003~~ | ❌ **Bác bỏ** | | **Cửa hậu SSH riêng, dùng khóa thật của host + PAM kiểm tra login** | `main.protector*`, `AddHostKey`, `pam.Authenticate` | T1021.004 (kẻ tấn công dùng để vào lại) | ✅ **Code xác nhận** | | **Worm: brute-force SSH rồi tự copy qua SFTP** | `main.sshtry`, `randomIP`, `sftp.Create` | T1110.001, T1570 | ✅ **Code xác nhận** | | Đọc khóa SSH của host | `os.ReadFile` → `ParsePrivateKey` | T1552.004 | 🟡 → ✅ | | Diệt miner đối thủ | `main.killxmrig`, `main.killnbminer` | T1489 | ✅ **Code xác nhận** | ## ⚠️ Hiểu lầm hay gặp > *"Code dịch ngược chính là mã nguồn gốc."* **Sai.** Đó là bản **tái dựng**: tên biến được đoán (`param_1`, `local_58`…), cấu trúc có thể sai, và luôn cần người diễn giải. > *"Tên hàm nghe đáng sợ thì hàm đó là của kẻ tấn công."* **Đây là bài học chính của step này.** Một malware **nhúng rất nhiều thư viện hợp pháp**: ở đây là `net/http`, `crypto/ssh`, `sftp`, `msteinert/pam`… Muốn biết **ý đồ**, phải đọc **code `main.*` của kẻ tấn công** và xem **ai gọi ai**, không dừng lại ở tên hàm. Chính báo cáo tháng 5 của chúng ta từng kết luận vội từ tên `cbPAMConv`, và hôm nay bằng chứng đã sửa lại kết luận đó. ## 🛡️ Ý nghĩa với SOC - **Đừng đi tìm sai thứ:** nếu tin vào "PAM hook", SOC sẽ mất thời gian rà `/etc/pam.d/` và các PAM module. Việc đúng phải làm là: **tìm cổng SSH lạ đang lắng nghe** (không phải cổng 22), và **đổi mật khẩu mọi tài khoản**, vì cửa hậu chấp nhận mật khẩu thật của hệ thống. - Server dùng khóa host **thật**, nên kiểm tra dấu vân tay SSH **không phát hiện được**. Phải nhìn vào **tiến trình nào đang mở cổng** (`ss -ltnp`). - **Phòng chống worm:** tắt đăng nhập SSH bằng mật khẩu, chỉ dùng key; dùng fail2ban; chặn SSH ra Internet từ server không cần thiết. --- # STEP 7/10: Từ bằng chứng thành rule phát hiện (YARA) ## 🎯 Mục tiêu step này Phân tích chỉ có giá trị khi nó **bắt được mẫu tiếp theo**. **YARA** là "ngôn ngữ chân dung" cho file độc: mô tả các đặc điểm nhận dạng, rồi quét hàng nghìn file trong vài giây. Ở step này ta tập **tư duy về báo nhầm (false positive)**, và **sửa rule** khi hiểu biết thay đổi. ## 💻 Lệnh và kết quả **Rule v1, viết theo báo cáo ban đầu:** ``` rule Demo_GoPAM_Backdoor { strings: $pam = "cbPAMConv" // (theo báo cáo cũ: hàm đánh cắp mật khẩu) $svc = "systemd-worker.service" // bám trụ $miner = "Xmrig_poolAddr" // cấu hình đào coin $cred = "*main.credential" // cấu trúc user/pass/ip condition: uint32(0) == 0x464c457f and 3 of them // phải là ELF + khớp ≥ 3/4 chuỗi } ``` ``` $ yara demo_rule.yar 94f2e4d8.elf Demo_GoPAM_Backdoor 94f2e4d8.elf ← ✅ bắt được mẫu $ yara -r demo_rule.yar /usr/bin ← thử trên file hệ thống sạch 0 match(es) in 2812 clean files ← ✅ không báo nhầm ``` **Rule v2, sửa lại theo phát hiện ở step 6:** ``` rule Demo_GoPAM_Backdoor_v2 { strings: $prot = "main.protectorHandleClient" // cửa hậu SSH: code CỦA KẺ TẤN CÔNG $kill = "main.killnbminer" // diệt miner đối thủ: code CỦA KẺ TẤN CÔNG $svc = "systemd-worker.service" $miner = "Xmrig_poolAddr" $sig = "pan-chan's mining island hi!" // chữ ký tác giả condition: uint32(0) == 0x464c457f and 4 of them } ``` ``` $ yara v2.yar 94f2e4d8.elf Demo_GoPAM_Backdoor_v2 94f2e4d8.elf ← ✅ bắt được mẫu $ yara -r v2.yar /usr/bin 0 match(es) ← ✅ không báo nhầm ``` ## 🔍 Diễn giải | Yếu tố của rule | Vì sao chọn như vậy | |---|---| | `uint32(0) == 0x464c457f` | 4 byte đầu phải là `.ELF` (magic bytes ở step 1). Rule bỏ qua ngay mọi file không phải chương trình Linux, nên quét nhanh hơn và ít báo nhầm hơn. | | `3 of them` / `4 of them` | **Không phụ thuộc vào một chuỗi duy nhất.** Kẻ tấn công sửa một chuỗi thì rule vẫn bắt được nhờ các chuỗi còn lại. | | Chọn chuỗi **cụ thể, gắn với hành vi** | `systemd-worker.service`, `Xmrig_poolAddr`: chương trình sạch gần như không bao giờ chứa. | | Thử trên **2.812 file sạch** trong `/usr/bin` | Một rule chưa test báo nhầm thì **chưa được triển khai**. 0 match là điều kiện tối thiểu. | **Vì sao phải có v2? Bài học từ step 6:** - Ở v1, `$pam = "cbPAMConv"` là chuỗi của **thư viện công khai `msteinert/pam`**. **Bất kỳ chương trình Go hợp pháp nào** dùng thư viện này (ví dụ một công cụ đăng nhập nội bộ) cũng chứa chuỗi đó. Nó là **chỉ báo yếu**, dễ gây báo nhầm ở môi trường thật, dù hôm nay `/usr/bin` cho 0 match. - Ở v2, ta thay bằng **tên hàm do chính kẻ tấn công viết** (`main.protectorHandleClient`, `main.killnbminer`) và **chữ ký tác giả**. Đây là những thứ **chỉ malware này mới có**. - 👉 **Rule tốt nhất được dựng từ code của kẻ tấn công, không phải từ code thư viện.** Khi hiểu biết thay đổi, **rule cũng phải được sửa theo**. ## ⚠️ Hiểu lầm hay gặp > *"Rule YARA khớp nghĩa là máy bị nhiễm."* **Sai.** Khớp rule nghĩa là *"file này giống chân dung"*. Vẫn cần người **xác minh** (triage), đặc biệt khi rule dùng chuỗi phổ biến. > *"Rule càng nhiều chuỗi càng tốt" / "chọn chuỗi đặc biệt nhất là được, ví dụ URL webhook".* **Chưa đủ.** URL webhook là **duy nhất**, nhưng kẻ tấn công **đổi nó chỉ trong 1 phút**. Chuỗi gắn với **cấu trúc code** (tên hàm, cấu trúc dữ liệu) khó đổi hơn nhiều, vì muốn đổi phải sửa và build lại cả chương trình. **Quy tắc nội bộ của chúng ta:** rule có **dưới 3 chuỗi đặc trưng** = `LOW_QUALITY`, **không triển khai**. ## 🛡️ Ý nghĩa với SOC - Rule này được đưa vào **pipeline quét YARA hằng ngày** trên kho mẫu honeypot, và có thể dùng để **rà soát server nội bộ**: `yara -r rule.yar / 2>/dev/null`. - **Vòng đời của một rule:** viết → test báo nhầm → triển khai → **xem lại khi có hiểu biết mới**, như ta vừa làm từ v1 sang v2. --- # STEP 8/10: MITRE ATT&CK và hành động cho đội phòng thủ ## 🎯 Mục tiêu step này Chuyển bằng chứng kỹ thuật sang **ngôn ngữ chung của ngành**, tức MITRE ATT&CK. Mọi SOC, mọi nhà cung cấp và mọi báo cáo tình báo đều dùng chung bộ mã này. Sau đó biến nó thành **việc cụ thể** mà đội vận hành làm được ngay. ## 💻 Bảng ATT&CK tổng hợp, đã cập nhật theo step 6 | Giai đoạn tấn công | Mã MITRE | Kỹ thuật | Bằng chứng trong mẫu | Nguồn | |---|---|---|---|---| | **Lẩn tránh** | T1036.005 | Giả danh tên hợp lệ | file tên `sshd`; dịch vụ `systemd-worker` | Step 1, 4 | | | T1564.001 | Thư mục ẩn | thư mục `.3060536107357439887/` | Hồ sơ vụ việc | | **Bám trụ** | T1543.002 | Tạo dịch vụ systemd | `/bin/systemd-worker`, `systemctl enable` | Step 4 | | **Truy cập thông tin đăng nhập** | T1552.004 | Đọc khóa riêng | đọc `/etc/ssh/ssh_host_*_key` → `ParsePrivateKey` | Step 4, 6 | | | T1110.001 | Đoán mật khẩu (brute-force) | `main.sshtry` + `main.credential` {username, password, ip} | Step 6 | | **Do thám** | T1016 | Lấy cấu hình mạng | 7 dịch vụ tra IP công khai | Step 4 | | | T1046 | Quét dịch vụ mạng | `main.randomIP` → thử SSH | Step 6 | | **Lây lan** | T1021.004 | Dịch vụ từ xa: SSH | worm SSH; cửa hậu SSH riêng `main.protector` | Step 6 | | | T1570 | Chuyển công cụ sang máy khác | tự upload chính nó qua SFTP (`os.executable` → `sftp.Create`) | Step 6 | | **Gửi dữ liệu ra ngoài** | T1567 | Qua dịch vụ web hợp pháp | Discord webhook | Step 4 | | **Tác động** | T1496 | Chiếm dụng tài nguyên | XMRig + NBMiner | Step 4, 6 | | | T1489 | Dừng tiến trình | `main.killxmrig`, `main.killnbminer` (diệt miner đối thủ) | Step 6 | | ~~Truy cập thông tin đăng nhập~~ | ~~T1556.003~~ | ~~Sửa đổi PAM~~ | ~~`cbPAMConv`~~ | ❌ **Bác bỏ ở step 6** | **Kết luận phân loại:** đây là **botnet đào coin kiểu worm**, lây qua SSH. Nó có **cửa hậu SSH riêng** dùng khóa thật của máy và mật khẩu thật của hệ thống. Đây **không phải** công cụ móc PAM để đánh cắp mật khẩu. ## 🛡️ Danh sách hành động cho SOC (chỉ trình bày, không chạy) **① Phát hiện: rà soát server** ```bash ls -l /bin/systemd-worker # file bám trụ systemctl status systemd-worker # dịch vụ giả danh ss -ltnp | grep -v ':22 ' # cổng lắng nghe lạ: cửa hậu SSH riêng ps -eo pid,comm,args | grep sshd # tiến trình tên "sshd" ... ls -l /proc/<PID>/exe # ... nhưng file thật KHÔNG phải /usr/sbin/sshd find / -xdev -type d -regex '.*/\.[0-9]+' 2>/dev/null # thư mục ẩn toàn chữ số yara -r Demo_GoPAM_Backdoor_v2.yar / 2>/dev/null # quét bằng rule ở step 7 ``` **② Ngăn chặn: khi phát hiện máy nhiễm** | Thứ tự | Hành động | Lý do (từ phân tích) | |---|---|---| | 1 | **Cô lập máy** khỏi mạng | Worm đang quét và lây sang máy khác (T1046, T1021.004) | | 2 | **Đổi mật khẩu MỌI tài khoản** trên máy, và trên mọi máy dùng chung mật khẩu đó | Cửa hậu chấp nhận **mật khẩu thật của hệ thống** qua PAM | | 3 | **Tạo lại khóa SSH của host** (`ssh-keygen -A`) | Malware đã đọc khóa host. Kẻ tấn công có thể **giả danh server** của ta | | 4 | Gỡ dịch vụ `systemd-worker` và file `/bin/systemd-worker`, rồi diệt tiến trình | Bám trụ (T1543.002) | | 5 | Chặn ra ngoài: `discord.com/api/webhooks` và các dịch vụ tra IP, từ server | Cắt kênh C2 / gửi dữ liệu (T1567, T1016) | **③ Phòng ngừa: cho toàn hệ thống** - **Tắt đăng nhập SSH bằng mật khẩu** (`PasswordAuthentication no`), chỉ dùng key. Biện pháp này **vô hiệu hóa hoàn toàn** cơ chế lây lan của worm. - Bật **fail2ban** hoặc giới hạn tốc độ kết nối SSH; không mở SSH ra Internet nếu không cần. - Server **không cần** gọi ra Discord hay các dịch vụ tra IP, nên **chặn mặc định** ở proxy. - Giám sát **CPU tăng vọt kéo dài** trên server: dấu hiệu đào coin trộm. ## ⚠️ Hiểu lầm hay gặp > *"Diệt tiến trình và xóa file là xong."* **Sai.** Với malware này, kẻ tấn công **đã có mật khẩu thật** và **khóa SSH của host**. Xóa file mà không đổi mật khẩu và tạo lại khóa thì kẻ tấn công **quay lại ngay**, hoặc worm từ máy khác lây lại. > *"Mã MITRE chỉ để trang trí báo cáo."* **Sai.** Mỗi mã nối thẳng tới **danh sách cách phát hiện và cách giảm thiểu** có sẵn trên trang MITRE. Mã đúng giúp SOC tra ngay được biện pháp. Mã **sai**, như T1556.003 nếu ta không sửa, sẽ dẫn đội phòng thủ đi kiểm tra sai chỗ. --- # STEP 9/10: Đối chiếu kết quả — phân tích tĩnh đoán đúng bao nhiêu? ## 🎯 Mục tiêu step này Kết thúc bằng **sự đánh giá**, không bằng công cụ. Chúng ta đã từng **chạy thật** mẫu này trong **máy ảo cô lập** (giai đoạn G4, ngày 26/05): - Trước khi chạy, kiểm tra cách ly đạt 7/7: không ra được LAN, không ra được Internet, DNS bị chuyển hướng vào sinkhole. - Cho chạy 60 giây, ghi lại mọi lời gọi hệ thống bằng `strace`. - Xong thì khôi phục snapshot sạch. Giờ đặt **dự đoán tĩnh** cạnh **quan sát động** để thấy phương pháp nào trả lời được câu hỏi nào. ## 💻 Bảng đối chiếu | Dự đoán từ phân tích tĩnh (step 1–8) | Kết quả khi chạy trong máy ảo (G4) | Đánh giá | |---|---|---| | Liên kết `libpam`, `libpthread`, `libc` (step 2) | Nạp đúng 3 thư viện đó khi khởi động | ✅ **Đúng** | | Tự cài vào `/bin/systemd-worker` (step 4) | Tự copy 30,3 MB sang `/bin/systemd-worker`, **chỉ khi có quyền root** | ✅ **Đúng** + biết thêm điều kiện | | Tạo dịch vụ `systemd-worker.service` (step 4) | **Chưa thấy** trong 60 giây | 🟡 Chưa xác nhận | | Cửa hậu SSH riêng `main.protector` (step 6) | Mở cổng lắng nghe **1919** | ✅ **Đúng**, nhưng **số cổng** chỉ G4 mới thấy | | Worm SSH `main.sshtry` + `randomIP` (step 6) | **65.955 IP khác nhau / 60 giây** (≈1.100 IP/giây), **không cần root** | ✅ **Đúng**, nhưng **tốc độ** chỉ G4 mới đo được | | Gửi về Discord webhook (step 4) | Chỉ thấy **truy vấn DNS** (bị sinkhole), không kết nối được | 🟡 Có ý đồ, chưa thấy gửi | | Đào coin XMRig / NBMiner (step 4) | **Không thấy** chạy miner | 🟡 Đúng như dự đoán ở step 4: cấu hình phải nhận từ C2, mà C2 đã bị chặn | | Đọc khóa host `/etc/ssh/ssh_host_*_key` (step 4, 6) | Thấy thử đọc **`/root/.ssh/id_rsa`** và **`~/.ssh/id_rsa`** của người dùng | ⚠️ **Tĩnh bỏ sót một phần**, xem bên dưới | | PAM để kiểm tra login vào cửa hậu (step 6) | `libpam` được nạp, nhưng **không có ai đăng nhập** vào cổng 1919 nên không quan sát được | 🟡 Code xác nhận, hành vi chưa quan sát | | *(không đoán được)* | Dùng **148 MB RAM, ~42% CPU** chỉ cho việc quét | ➕ Chỉ G4 mới đo được | **Về `id_rsa`, một bài học trung thực:** chuỗi `id_rsa` **có trong file** (`strings | grep -c id_rsa` → 1). Nhưng ở step 4 ta grep theo mẫu `\.ssh/id_rsa`, mà Go lưu tên file và đường dẫn **tách rời**, nên ta **bỏ sót**. Phân tích tĩnh chỉ tốt bằng **câu hỏi** mà người phân tích đặt ra. **Về cổng 1919:** số `1919` **không nằm dưới dạng chuỗi ký tự** trong file. Nó là một **hằng số** trong code máy, nên `strings` không thể thấy. Muốn tìm bằng phân tích tĩnh, phải lần trong Ghidra tới tham số của `net.Listen` bên trong `main.protector`, tốn công hơn nhiều so với chạy thử 60 giây. ## 🔍 Tổng kết | | Phân tích tĩnh | Phân tích động (G4) | |---|---|---| | **Trả lời được** | Nó **có thể** làm gì: toàn bộ khả năng, kể cả nhánh code chưa chạy tới | Nó **thực sự** làm gì: số liệu, tốc độ, điều kiện kích hoạt | | **Ưu điểm** | An toàn tuyệt đối, không cần môi trường đặc biệt, thấy cả code "ngủ" | Có bằng chứng hành vi; thấy hằng số và cấu hình lúc chạy | | **Hạn chế** | Không thấy giá trị nhận lúc chạy (pool, cổng); phụ thuộc vào câu hỏi của người phân tích | Chỉ thấy những gì xảy ra **trong 60 giây đó**; malware có thể phát hiện sandbox; cần hạ tầng cách ly | | **Chi phí** | Thấp | Cao: máy ảo, mạng cách ly, kiểm tra 7 điểm, khôi phục snapshot | **Hai phương pháp bổ sung cho nhau:** phân tích tĩnh cho **bản đồ khả năng** và cho biết **cần quan sát gì** khi chạy; phân tích động **xác nhận và đo lường** trên bản đồ đó. > ### 📌 Điều cần nhớ > **Phân tích tĩnh cho biết nó *CÓ THỂ* làm gì.** > **Phân tích động cho biết nó *THỰC SỰ* làm gì.** > **Entropy cho biết nên *BẮT ĐẦU* từ đâu.** ## ⚠️ Hiểu lầm hay gặp > *"Chạy thử trong sandbox là đủ, không cần phân tích tĩnh."* **Sai.** Trong 60 giây G4, malware **không đào coin**, **không gửi dữ liệu**, **không tạo service**. Nếu chỉ dựa vào sandbox, ta sẽ đánh giá thấp nó. Chính phân tích tĩnh mới cho biết nó có cả cửa hậu SSH, worm, miner và cơ chế diệt đối thủ. > *"Báo cáo đã viết là đúng."* **Không phải lúc nào cũng vậy.** Hôm nay ta đã sửa hai điểm trong báo cáo cũ: **địa chỉ `cbPAMConv`** và **kết luận "PAM hook"**. Báo cáo cũng là giả thuyết, và cần được **kiểm chứng lại khi có bằng chứng mới**. ## 🛡️ Ý nghĩa với SOC: khi nào cần phân tích động? - **Cần G4** khi: phân tích tĩnh thấy có kết nối mạng hoặc cấu hình nhận từ C2; mẫu bị pack hoặc mã hóa (entropy > 7,5); hoặc cần **số liệu** để đánh giá mức độ ảnh hưởng (tốc độ lây, tài nguyên). - **Phân tích tĩnh là đủ** khi: mẫu thuộc họ đã biết, khớp rule YARA tin cậy, và chỉ cần IOC để chặn. --- # STEP 10/10: Đối chứng — khi phân tích tĩnh "bị bịt mắt" ## 🎯 Mục tiêu step này Chạy **đúng các lệnh hôm nay** trên một mẫu khác, **f14937a1**, cũng thu được từ honeypot của chúng ta, để thấy **vì sao quy trình phải chọn hướng phân tích theo entropy**. Hiểu một quy tắc qua **chỗ nó thất bại** thường nhớ lâu hơn qua chỗ nó thành công. ## 💻 Lệnh và kết quả, so sánh với mẫu chính ``` $ file f14937a19cf6… ELF 32-bit LSB executable, ARM, EABI5 version 1 (GNU/Linux) $ python3 -c '<shannon entropy>' f14937a19cf6… entropy = 7.9998 bits/byte $ readelf -h → Number of section headers: 0 $ readelf -d → There is no dynamic section in this file. $ strings -n 8 f14937a19cf6… | wc -l → 245 $ strings -n 8 f14937a19cf6… | head Zl#B9iI4C <4y?rGg^ `kgIlj)S u2D2A/]i 4nl+oWx% ← toàn rác $ strings ... | grep -cE 'Xmrig|systemd|http|/etc/' → 0 chuỗi hữu ích $ upx -t f14937a19cf6… upx: NotPackedException: not packed by UPX ← công cụ nói "không pack" $ grep -c 'UPX!' f14937a19cf6… 1 ← nhưng bên trong có dấu UPX! ``` | Câu hỏi | 94f2e4d8 (mẫu chính) | f14937a1 (mẫu đối chứng) | |---|---|---| | Kiến trúc | x86-64 (server) | **ARM 32-bit** (router, camera, thiết bị IoT) | | Entropy | 6,36 → đọc được | **7,9998** → gần như ngẫu nhiên hoàn toàn | | Section headers | 35 | **0** | | Thư viện khai báo (step 2) | `libpam`, `libc`… → ra manh mối | **Không có**: không còn manh mối | | Strings hữu ích (step 4) | hàng chục IOC | **0** | | Tên hàm (step 5–6) | 5.978 hàm, 76 hàm `main.*` | Ghidra **không đọc được**: code đang bị nén | | Hướng xử lý theo SOP | **G3B**: Ghidra đầy đủ | **G3B-ALT**: chỉ dùng script, rồi tìm cách giải nén | ## 🔍 Diễn giải - **Entropy 7,9998** nghĩa là toàn bộ code thật đã bị **nén (pack)**. File chỉ còn một đoạn nhỏ để **tự giải nén trong bộ nhớ** khi chạy. Trên đĩa, ta chỉ thấy dữ liệu nén, giống hệt file `ls.gz` (7,99) ở step 3. - **Kẻ tấn công còn chủ động gây khó:** file được nén bằng **UPX**, một công cụ nén phổ biến có sẵn lệnh giải nén `upx -d`. Nhưng header đã bị **sửa** nên chính công cụ UPX báo *"not packed"* và **từ chối giải nén**, dù dấu `UPX!` vẫn nằm trong file. - **Kiến trúc ARM** nhắm vào thiết bị IoT. Sandbox x86-64 của chúng ta **không chạy được** nó, nên G4 cũng bị chặn. - **Vậy làm sao ta vẫn biết đây là botnet RedTail đào coin?** Thông tin **không đến từ file binary**. Nó đến từ **ngữ cảnh**: log honeypot ghi lại kẻ tấn công upload file với tên **`redtail.arm7`**, kèm một script `clean.sh` đọc được, có lệnh diệt miner đối thủ, dọn crontab, `chattr -ia`, xóa thư mục tạm. 👉 **Khi binary "câm", ngữ cảnh lên tiếng.** **Các lựa chọn khi gặp mẫu như thế này:** 1. **Sửa header UPX bằng tay** rồi giải nén, sau đó quay lại G3B. 2. Chạy trong **trình giả lập ARM** (QEMU) đã cách ly, để code tự giải nén rồi trích bộ nhớ. 3. Dựa vào **ngữ cảnh**: log phiên tấn công, script đi kèm, tên file, tình báo về họ malware đã biết. **Đây cũng là lý do của đề xuất sửa SOP ở step 3:** **kiểm tra dấu `UPX!` trước, rồi mới xét entropy**. Với mẫu này, entropy 8,0 đẩy thẳng sang G3B-ALT, và bước "thử giải nén UPX" **không bao giờ được thử**. ## ⚠️ Hiểu lầm hay gặp > *"Công cụ nói 'not packed' thì nghĩa là không bị pack."* **Sai.** Công cụ chỉ kiểm tra **định dạng chuẩn**. Kẻ tấn công biết điều đó và **cố tình làm hỏng định dạng**. Luôn **đối chiếu nhiều dấu hiệu**: entropy 8,0 + dấu `UPX!` + 0 section header cùng nói một điều. > *"Không đọc được binary thì không phân tích được."* **Sai.** Binary chỉ là **một nguồn** bằng chứng. Log honeypot, script đi kèm, hạ tầng mạng và tình báo về họ malware thường **đủ để phân loại và phòng thủ**. ## 🛡️ Ý nghĩa với SOC - Mẫu ARM nghĩa là **thiết bị IoT** (router, camera, NAS) cũng là mục tiêu. Hãy đưa chúng vào phạm vi giám sát, đổi mật khẩu mặc định, và không mở SSH/Telnet ra Internet. - Khi gặp file **entropy ≈ 8 + có dấu `UPX!`**: ghi vào ticket *"packed, header UPX bị sửa"*, chuyển cho bộ phận phân tích chuyên sâu, và trong lúc chờ thì **dựa vào IOC ngữ cảnh** (IP upload, tên file, script đi kèm) để chặn ngay. --- # 🎓 TỔNG KẾT BUỔI PHÂN TÍCH **Hành trình hôm nay, chỉ đọc file mà không chạy lần nào:** ``` Step 0 Hash và an toàn → định danh vật chứng Step 1 Magic bytes → "sshd" là đồ giả, viết bằng Go Step 2 Thư viện → libpam: giả thuyết về đăng nhập Step 3 Entropy 6,36 → đọc được → đi nhánh Ghidra Step 4 Strings → miner, persistence, C2 Discord, do thám IP Step 5 Symbols → cbPAMConv + API PAM: 2 giả thuyết Step 6 Ghidra → bác bỏ "PAM hook"; xác nhận cửa hậu SSH + worm Step 7 YARA → rule v2 dựng từ code của kẻ tấn công, 0 báo nhầm Step 8 MITRE ATT&CK → 12 kỹ thuật + kế hoạch ứng phó Step 9 Đối chiếu với G4 → tĩnh = khả năng; động = hành vi thực Step 10 Mẫu bị pack → khi binary "câm", ngữ cảnh lên tiếng ``` > **Phân tích tĩnh cho biết nó *CÓ THỂ* làm gì · Phân tích động cho biết nó *THỰC SỰ* làm gì · Entropy cho biết *BẮT ĐẦU* từ đâu · Và mọi kết luận, kể cả trong báo cáo của chính mình, đều phải được kiểm chứng bằng bằng chứng.**
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mikrotik/labs#6