CSCV2026 Quals Forensics Writeup
Vừa qua mình có tham gia tranh tài tại vòng loại cuộc thi Cybersecurity Student Contest 2026. Ở mảng Forensics năm nay có khá nhiều thử thách thú vị và thực tế. Dưới đây là phần writeup chi tiết cho các bài mà mình đã giải quyết được, mời mọi người cùng tham khảo nhé!
Silent Room
Đề cho file SilentRoom_public.7z kèm mã SHA256. Mình giải nén bằng 7-Zip và được file evidence.E01 cùng vài file ghi chú. E01 là định dạng ảnh đĩa của EnCase. FTK Imager là công cụ miễn phí của Exterro dùng để đọc các định dạng ổ đĩa như E01.
Treen FTKImager, mở rộng dần các nhánh, mình thấy phân vùng NTFS tên A-LAPTOP, trong đó có thư mục người dùng Users\A. Thư mục mình mở là Downloads.
Có hai file booking_HSR_260820_preview.png và ticket_DN1842.png. Dấu X đỏ nghĩa là file đã bị xóa, nhưng bản ghi của nó trong bảng MFT của NTFS vẫn còn, nên FTK Imager vẫn đọc được tên, kích thước và ngày giờ. Cạnh đó là booking_BAB_403_receipt.html, một biên nhận đặt phòng vẫn còn.
Để làm việc thoải mái với các công cụ khác, mình chuột phải vào thư mục Users trong Evidence Tree, chọn Export Files… và lưu ra Desktop của máy ảo.
File đầu tiên mình mở là biên nhận còn nguyên trong Downloads. Bấm đúp vào nó, máy ảo mở bằng Internet Explorer.
Biên nhận ghi Babarian Hotel, phòng 403, Da Nang, trạng thái CONFIRMED. Vậy là cô gái đó đã đặt phòng ở Babarian Hotel, Da Nang để làm 1 chuyến du lịch sao? Có hai điều làm mình nghi ngờ. Thứ nhất, file này còn nguyên, trong khi hai file bị xóa lại là thứ có vẻ là muốn giấu đi. Thứ hai, dòng Status shown on this receipt chỉ cho mình biết rằng phòng được Confirmed vào lúc đó, 06:56:02, chứ cũng chẳng biết sau đó có gì thay đổi không. Mình ghi lại Babarian 403 như một giả thuyết rồi đi tìm thêm các dấu vết.
Trong AppData\Roaming có một thư mục lạ tên ChatApp, bên trong là file cấu hình Local State và cơ sở dữ liệu msg_cache.db. Mình chuột phải vào Local State, chọn Edit with Notepad++.
File cho biết người đang trò chuyện sử dụng ChatApp này là fi-operator-73, gắn với một mã hồ sơ FI-217. Dòng cuối trỏ tới mã nguồn của ứng dụng ở AppData\Local\Programs\ChatApp\resources\app.bundle.js, nên mình mở luôn file đó trong Notepad++.
Từ đoạn code trên, mình có thể hiểu được cơ chế mã hóa tin nhắn của ứng dụng. Cụ thể, khóa được tạo ra bằng cách nối ba chuỗi chatapp-web-v2, tên người dùng, và mã hồ sơ (cách nhau bởi dấu gạch đứng |), sau đó đem băm bằng SHA-256. Tin nhắn được mã hóa và giải mã bằng thuật toán đối xứng AES-256-CBC. Với chế độ CBC, hệ thống cần thêm một Vector Khởi tạo (IV) và may mắn là mỗi tin nhắn đều đã tự đính kèm IV riêng của nó. Như vậy, toàn bộ các thành phần cần thiết để tạo khóa đều có sẵn trong Local State
Tiếp theo mình mở msg_cache.db bằng DB Browser for SQLite, sang tab Browse Data và chọn bảng messages.
Có 15 tin nhắn. Cột direction cho biết chiều tin nhắn:
- in là A nhận
- out là A gửi
Và chỉ có tin 10 và 12 là A trả lời. Cột body là một đoạn JSON chứa iv và ct, ct là bản mã đã mã hóa và đổi sang Base64.
Mình tạo khóa trước trong CyberChef. Gõ SHA2 vào ô Search ở cột Operations, kéo thao tác SHA2 thả vào khung Recipe, để kích thước 256, rồi gõ vào ô Input chuỗi ghép từ ba thành phần.
1
chatapp-web-v2|fi-operator-73|FI-217
Khóa là 5cc72b55a0ab3bec95a332c69bae29582ab59181e062d04ea94e9faca648d10d.
Trước khi giải cả 15 tin, mình thử tay một tin để chắc khóa đúng. Mình mở tab CyberChef mới, kéo From Base64 rồi AES Decrypt vào Recipe. Ở AES Decrypt, mình dán khóa vào ô Key và để kiểu HEX, đổi kiểu ô IV sang BASE64, đổi Input sang Raw vì From Base64 đã trả ra byte thô. Mode giữ CBC.
Quay lại DB Browser, mình bấm vào ô body của tin số 6. Khung Edit Database Cell bên phải hiện nội dung ô, chuyển Mode sang JSON và bấm nút định dạng thì mỗi khóa nằm một dòng, dễ copy hơn.
Mình bôi đen giá trị iv, dán vào ô IV của CyberChef. Sau đó mình copy giá trị ct dán vào Input.
Output ra tiếng Việt đọc được, CyberChef còn tự đổi bảng mã hiển thị sang UTF-8. Khóa và cách làm đều đúng. Tin số 6 nói: mở vé xe, xác nhận PNR rồi ghi nhớ, vì PNR là một phần khóa đối chiếu. Mình chưa hiểu khóa đối chiếu là gì, nên cần đọc hết các tin còn lại.
Copy từng tin 15 lần thì quá lâu. Mình chọn hết các ô trong cột body ở DB Browser, Ctrl+C, rồi dán vào Input của CyberChef, mỗi tin một dòng. Recipe cho nhiều dòng phức tạp hơn một chút. Fork tách input thành từng dòng và chạy phần còn lại của recipe trên từng dòng. Register dùng biểu thức chính quy để lấy iv vào biến $R0 và ct vào $R1. Find / Replace thay cả dòng bằng $R1. Sau đó là From Base64 và AES Decrypt như lúc nãy, chỉ khác ô IV giờ ghi $R0. Mình kéo thêm ba thao tác này vào recipe, điền regex dưới đây vào ô Extractor của Register.
1
"iv":"([^"]+)","ct":"([^"]+)"
Bản CyberChef mới có thêm ô IV from input, nên sau một lần kéo thả các ô Mode và Output bị trống, mình phải chọn lại CBC và Raw. Mình cũng phải xóa dòng trống cuối input, nếu không CyberChef báo lỗi IV dài 1 byte.
Nội dung là một vụ lừa đảo kiểu bắt cóc online. Kẻ gian giả làm cơ quan điều tra, dọa rằng A dính tới rửa tiền, cấm A báo cho gia đình và yêu cầu A làm theo từng bước. Ba dòng được đóng khung là quan trọng nhất cho bài:
- Tin 6: mở vé xe, ghi nhớ mã PNR, vì PNR là một phần của khóa đối chiếu.
- Tin 14: ảnh chứng minh cuối đã bị khóa bằng XOR với chuỗi đối chiếu, mở bằng CyberChef với key UTF-8.
- Tin 15: thứ tự chuỗi đối chiếu là mã PNR vé xe | trạng thái đặt phòng bằng chữ | số phòng | tên nơi ở viết liền | tỉnh thành viết liền.
Các tin khác cũng có ý nghĩa. Tin 7 yêu cầu xóa file đã tải, lý giải hai dấu X đỏ ở Downloads. Tin 9 và 13 nói ảnh đặt phòng cũ chỉ là bản chụp, trạng thái đang có hiệu lực trong hệ thống mới là thật.
Các file đã tải nằm trong thư mục Chrome, nên mình xem lịch sử Chrome bằng BrowsingHistoryView của NirSoft. Trong Advanced Options, mình chọn Load history items from any time để công cụ không lọc mất dữ liệu cũ, và chọn Load history from the specified history files, rồi trỏ ô Chrome history files tới file History trong thư mục đã export. Bấm vào tiêu đề cột Visit Time để xếp theo thời gian.
Máy ảo đặt múi giờ +07, nên giờ ở đây là giờ Việt Nam. Ngày 19/08/2026, A mở trang Urgent verification notice rồi tìm Google về việc làm việc với công an qua video call và tài khoản liên quan rửa tiền. Sáng 20/08, lúc 06:24:37 A mở vé NorthStar Express NSE1842. Lúc 06:55:40 A mở biên nhận Babarian Hotel, nhưng đến 07:03:15 lại mở một đặt phòng khác là HSR-260820-0401. Ba phút sau A mở chỉ đường Google Maps, rồi tìm cách XOR file trong CyberChef và nội quy nhận phòng trễ của khách sạn. Để biết file nào được tải từ trang nào, mình mở chính file History bằng DB Browser và chọn bảng downloads. Cột đường dẫn bị cắt ngắn, nên mình bấm vào từng ô để đọc đủ trong khung Edit Database Cell.
Dòng 4 lưu file booking_HSR_260820_preview.png, tải từ trang preview của HSR-260820-0401. Dòng 2 là ticket_DN1842.png tải từ trang vé NSE1842. Vậy hai file đã bị xóa chính là vé xe và ảnh xem trước của đặt phòng thứ hai, mở sau biên nhận Babarian bảy phút. Giả thuyết Babarian 403 bắt đầu lung lay.
Mình thử ChromeCacheView trước, nhưng công cụ trả về 0 mục, vì thư mục cache này không có file index mà Chrome thật vẫn tạo. Vậy mình mở thẳng thư mục Cache\Cache_Data bằng File Explorer.
Thư mục có các file f_0000xx, là các phản hồi lớn Chrome lưu thành file riêng. Hai file f_000033 nặng 63 KB và f_000054 nặng 60 KB có kích thước đúng bằng vé và ảnh preview đã bị xóa. Mình chọn bốn file nhỏ f_000020, f_000021, f_000041 và f_000088, chuột phải và chọn Edit with Notepad++.
f_000020 là đoạn JavaScript của trang đặt phòng StayHub. Nó cho biết ý nghĩa từng mã trạng thái: 2 là confirmed, 7 là cancelled.
f_000021 là phản hồi API của đặt phòng BAB-403DN tại Babarian Hotel, phòng 403. Trạng thái hiện tại là 7, tức đã hủy, lúc 06:58:09. Trường supersededBy cho biết nó đã được thay bằng HSR-260820-0401. Biên nhận HTML trong Downloads vẫn ghi CONFIRMED, vì nó được tải lúc 06:56:02, trước khi đặt phòng bị hủy. Đây chính là bản chụp cũ mà tin nhắn 9 và 13 cảnh báo.
f_000041 là đặt phòng thay thế HSR-260820-0401 với trạng thái 2, tức confirmed, và roomNumber là 401. Mã thành phố DAD là mã IATA của Đà Nẵng, nhận phòng từ 19:00 ngày 20/08. Để chắc hơn, mình xem thêm Local Storage. Đây là nơi trang web lưu dữ liệu riêng trên máy người dùng, Chrome cất nó trong thư mục Local Storage\leveldb. Mình bấm qua từng thư mục trong File Explorer, chọn cả ba file và mở bằng Notepad++.
File MANIFEST-000001 cho biết log đang dùng là 000009.log. File cũ 000003.log ghi rằng BAB-403DN có state 7 và được thay bằng HSR-260820-0401. File mới 000009.log ghi activeReservation là HSR-260820-0401 với roomNumber=401 và state=2. Ba nguồn độc lập đều nói cùng một điều, nên mình bỏ giả thuyết Babarian.
f_000088 nói thẳng f_000089 là một ảnh PNG đã bị biến đổi bằng XOR, key dạng UTF-8, và key phải dựng từ tin nhắn ChatApp. GCHQ public chef là cách gọi khác của CyberChef. Vậy còn thiếu mã PNR và tên khách sạn.
Để xem f_000033 và f_000054, mình đổi tên bản sao trong thư mục export bằng phím F2, thêm đuôi .png, rồi mở bằng Paint.
Vé xe của A đi từ Hà Nội, bến Mỹ Đình, tới Bến xe Trung tâm Đà Nẵng, khởi hành 07:25 và dự kiến đến 18:45 ngày 20/08, ghế B12. Mã PNR là NSE1842.
Ảnh preview của HSR-260820-0401 cho biết khách sạn là Hana River Side ở Da Nang, nhận phòng 19:00 đến 22:00. Dòng chữ đỏ ghi số phòng nằm ở trạng thái đặt phòng đang hiệu lực chứ không có trong ảnh, và mình đã có số đó từ f_000041.
Ghép năm phần theo thứ tự của tin nhắn 15, tên viết liền không dấu cách:
1
NSE1842|confirmed|401|HanaRiverSide|DaNang
Mình mở một tab CyberChef mới, gõ XOR vào ô Search và kéo XOR vào Recipe, rồi kéo thêm Render Image để xem ảnh ngay trong khung Output. Ở XOR, mình đổi kiểu key từ HEX sang UTF8 đúng như tin nhắn 14 dặn, gõ key vào, rồi bấm nút Open file as input ở khung Input và bấm đúp vào f_000089 trong hộp thoại.
Ảnh hiện ra là LOCATION CONFIRMATION STILL: phòng 401, Hana River Side, Da Nang, và ghi chú tìm thấy trên bàn trong phòng chứa flag. Để chắc chắn mình không chỉ gặp may, mình thử thay key bằng giả thuyết cũ, dùng thông tin Babarian 403.
1
NSE1842|confirmed|403|BabarianHotel|DaNang
Output chỉ còn một biểu tượng ảnh hỏng. XOR không báo lỗi khi key sai, nó chỉ cho ra rác và rác thì không được rồi. Chỉ key có Hana River Side và phòng 401 mới cho ra ảnh đọc được.
Flag: CSCV2026{F04nd_h3r_4t_401_HanaRiverSide_DaNang_fm0923812}
From Update To Encrypt
Đề cho file VuVT.7z nặng hơn 1 GB, mở thư mục vừa giải nén bằng File Explorer.
Bên trong có ba nguồn bằng chứng chính là mem.raw bản chụp RAM, network.pcapng lưu lượng mạng bắt được, còn sysmon.evtx là log của Sysmon. Thư mục Users chứa dữ liệu người dùng được thu thập từ ổ đĩa.
Câu chuyện bắt đầu từ việc máy bị mã hóa, nên việc đầu tiên mình làm là đi tìm các file đã bị mã hóa. Xem qua thư mục người dùng, mình bắt gặp thư mục Users\Public\MetaData\Data.
Thư mục này có 7 file cùng đuôi .enc, tất cả đều được tạo lúc 11:35 ngày 25/08/2026, trong đó có flag.jpeg.enc và hi.txt.enc. Vì nạn nhân nói mình chỉ chạy update, mình mở tiếp thư mục Downloads của người dùng để xem họ đã tải gì về.
Có hai thứ đáng chú ý ở đây. File UpdateX7A91C.7z được tải về lúc 11:34, tức chỉ một phút trước khi các file bị mã hóa, nên nhiều khả năng đây chính là gói update trong mô tả. Bên cạnh đó là 7z2409-x64.exe, bộ cài 7-Zip mà người dùng tải hôm trước. Mình mở thư mục UpdateX7A91C đã được giải nén sẵn bên cạnh.
Gói này có sáu file, trong đó update.exe là file duy nhất được tạo đúng ngày xảy ra sự cố, lúc 10:21 25/08, trong khi các file còn lại mang ngày cũ hơn nhiều. Vì vậy mình chọn update.exe là đối tượng để phân tích.
Mở IDA, chọn update.exe, IDA tự nhận ra đây là file Portable executable for 80386 (PE) 32-bit. Khi IDA phân tích xong, mình mở cửa sổ Strings lên kiểm tra.
Phần lớn là thông báo lỗi chuẩn của thư viện C++ mà trình biên dịch tự gói vào. Có một dòng khá đáng nghi là http://192.168.56.1:8080/api/check, và một khối chuỗi được viết bắt đầu bằng [-] Nhiều người viết công cụ quen đánh dấu thông báo thành công bằng [+] và thông báo lỗi bằng [-].
Danh sách chuỗi bão lỗi gần như kể lại toàn bộ cách mã độc làm việc. Nó đọc file, gọi các hàm BCrypt của Windows để mã hóa, rồi ghi ra file mới theo thứ tự WriteFile(magic), WriteFile(placeholder), WriteFile(ciphertext), sau đó gọi SetFilePointerEx để quay lại và ghi WriteFile(tag).
Từ đây mình đoán file .enc gồm một chuỗi nhận diện ở đầu, một vùng để trống, dữ liệu đã mã hóa, rồi vùng trống được ghi đè bằng một giá trị tag tính sau cùng. Để biết khóa được tạo ra như thế nào, mình chuyển sang tab IDA View-A, nơi IDA đã đặt sẵn con trỏ ở hàm main, rồi chọn Jump > Jump to pseudocode để xem mã giả.
Ngay đầu hàm, mã độc gọi GetComputerNameA để lấy tên máy, tiếp đó gọi GetCurrentProcessId để lấy PID của chính nó, rồi gửi request tới http://192.168.56.1:8080/api/check. Mình cuộn xuống để xem các giá trị này được dùng vào việc gì.
Dòng 165 nằm trong một vòng lặp gọi tolower cho từng ký tự, nên tên máy bị đổi hết sang chữ thường. Đoạn dòng 178 đến 180 chia PID cho 10 liên tục và cộng 48, là mã ASCII của số 0, để đổi PID từ số sang chuỗi chữ số.
Dòng 250 gán giá trị 124 vào cuối chuỗi tên máy, mà 124 chính là mã ASCII của dấu gạch đứng, còn dòng 258 nối thêm một dấu ”|“ nữa sau PID. Như vậy chuỗi đang có dạng tên máy, dấu gạch đứng, PID và thêm một dấu gạch đứng ở cuối.
Ở dòng 277, hàm sub_407B80 nối thêm vào cuối một chuỗi lấy từ phản hồi của server. Mình đi theo hàm xử lý phản hồi và thấy nó tìm khóa “campaign” trong JSON rồi lấy giá trị nằm giữa hai dấu ngoặc kép, nên phần cuối của chuỗi chính là tên chiến dịch do server trả về.
Khi chuỗi đã được ghép xong, dòng 348 mở thuật toán SHA256 bằng BCryptOpenAlgorithmProvider.
BCryptHashData băm chuỗi vừa ghép, còn BCryptFinishHash lấy ra 0x20u, tức 32 byte kết quả. Sau đó 32 byte này được đưa vào sub_405F20 ở dòng 393, hàm duyệt thư mục và mã hóa từng file.
Tóm lại, khóa AES là SHA-256 của chuỗi tên máy viết thường|PID|campaign. Mình bấm đúp vào sub_405F20 rồi đi tiếp vào hàm xử lý từng file là sub_405C90.
Dòng 39 gọi BCryptGenRandom với cbBuffer: 0xCu, nghĩa là mỗi file được cấp một dãy 12 byte ngẫu nhiên riêng. Dãy này gọi là nonce, một giá trị chỉ dùng một lần, có tác dụng làm cho hai file giống nhau vẫn cho ra bản mã khác nhau. Điều đáng lo là trong danh sách chuỗi lúc nãy không hề có bước nào ghi nonce ra file, nên mình mở hàm mã hóa sub_404FA0 để kiểm tra.
Hàm này đặt thuộc tính ChainingMode thành ChainingModeGCM, tức là mã hóa bằng AES ở chế độ GCM. GCM vừa mã hóa vừa tạo ra một tag xác thực 16 byte, và khi giải mã thì tag giúp phát hiện dữ liệu bị sửa hay khóa bị sai.
Trước khi gọi BCryptEncrypt, mã độc điền vào cấu trúc thông tin xác thực các giá trị 64 là kích thước cấu trúc, 12 là độ dài nonce, và tag dài 16 byte ở dòng 393. Phần ghi file ở phía dưới cho biết dữ liệu nào được lưu lại.
Dòng 507 chép chuỗi 1337DaKL vào bộ đệm rồi ghi đúng 8 byte này ra đầu file, đây là chuỗi nhận diện mà danh sách chuỗi gọi là magic.
Tiếp theo, mã độc ghi 0x10u tức 16 byte trống, ghi toàn bộ ciphertext, gọi SetFilePointerEx để quay về vị trí 8, rồi ghi tag đè lên vùng trống đó. Như vậy file .enc có cấu trúc 8 byte magic, 16 byte tag và phần ciphertext, còn nonce thì bị xóa khỏi bộ nhớ sau khi dùng và không được lưu ở đâu cả. Trước mắt cần tìm đủ ba thành phần của khóa.
Cả tên máy lẫn PID đều có trong log Sysmon, vì Sysmon ghi lại mọi tiến trình được tạo kèm theo PID và tên máy. Mình mở Event Viewer và chọn sysmon.evtx. Log có 3629 sự kiện, filter Event ID 1 và chọn sự kiện lúc 11:35:05 AM, ngay trước khi các file .enc xuất hiện.
Cửa sổ Event Properties cho thấy ProcessId: 2844 với Image là C:\Users\bkav\Downloads\UpdateX7A91C\update.exe, chạy lúc 04:35:05 UTC, và trường Computer là DESKTOP-7NNKNIK. Sau khi đổi sang chữ thường như hàm main đã làm, tên máy sẽ là desktop-7nnknik.
Thành phần cuối cùng là campaign do server trả về, nên mình bấm đúp vào network.pcapng để mở bằng Wireshark rồi gõ http vào ô lọc phía trên danh sách gói tin.
Chỉ còn 5 gói HTTP, trong đó gói 196 là lúc máy nạn nhân tải /UpdateX7A91C.7z từ 192.168.56.1, còn gói 2741 là request GET /api/check mà mình đã thấy trong IDA. Mình chuột phải vào gói 2741, chọn Follow > HTTP Stream để đọc trọn cuộc trao đổi.
Request mang User-Agent: update.exe/1.0, nên chắc chắn nó do update.exe gửi đi, và server trả về {“campaign”:”X7A91C”,”status”:”ok”}. Server không gửi khóa nào cả mà chỉ gửi tên chiến dịch X7A91C, vì vậy khóa hoàn toàn có thể dựng lại được từ những gì đã có.
Ghép ba thành phần theo đúng thứ tự trong IDA, mình có chuỗi đầu vào sau.
1
desktop-7nnknik|2844|X7A91C
Mình mở CyberChef, gõ SHA2 vào ô Search, kéo thao tác SHA2 vào khung Recipe, đổi Size thành 256 và Rounds thành 64, rồi dán chuỗi trên vào Input.
Khóa AES-256 là 3c0249e672188f75d3f6a845d534ecfc1bdd2c7c681562393f636d24d4409830. Để chắc rằng cấu trúc file đúng như những gì đọc được trong IDA, mình mở hi.txt.enc bằng HxD, một trình xem file ở dạng hex.
Tám byte đầu là 31 33 33 37 44 61 4B 4C, đọc ra 1337DaKL, đúng chuỗi magic trong IDA. File dài 58 byte, trừ đi 8 byte magic và 16 byte tag thì còn 34 byte ciphertext, và cũng không có chỗ nào chứa nonce.
Nonce không được lưu, nhưng GCM có một đặc điểm giúp lấy lại nó khi đã biết khóa. Trong GCM, tag được tính bằng cách mã hóa khối gồm nonce và bộ đếm 00000001 bằng khóa K, rồi XOR với một giá trị GHASH chỉ phụ thuộc vào khóa và ciphertext. Khi có khóa, mình tính được GHASH từ ciphertext, XOR nó với tag đọc từ file, rồi giải mã AES một khối là ra lại nonce kèm bốn byte bộ đếm. Bốn byte bộ đếm này phải bằng 00000001, nên đây cũng là cách kiểm tra khóa có đúng hay không. CyberChef không có thao tác tính GHASH, vì vậy mình viết một script Python ngắn dùng thư viện pycryptodome để xử lý cả 7 file.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import hashlib, os, struct, sys
from Crypto.Cipher import AES
from Crypto.Cipher._mode_gcm import _GHASH, _ghash_portable
SRC = r"C:\Users\DFIRVM\Desktop\VuVT\Users\Public\MetaData\Data"
OUT = r"C:\Users\DFIRVM\Desktop\recovered"
seed = sys.argv[1].encode()
KEY = hashlib.sha256(seed).digest()
print("seed :", seed.decode())
print("key :", KEY.hex())
def recover_nonce(key, ct, tag):
ecb = AES.new(key, AES.MODE_ECB)
ghash = _GHASH(ecb.encrypt(bytes(16)), _ghash_portable)
ghash.update(ct + bytes(-len(ct) % 16) + struct.pack(">QQ", 0, len(ct) * 8))
j0 = ecb.decrypt(bytes(a ^ b for a, b in zip(tag, ghash.digest())))
return j0[:12], j0[12:]
os.makedirs(OUT, exist_ok=True)
for name in sorted(os.listdir(SRC)):
blob = open(os.path.join(SRC, name), "rb").read()
tag, ct = blob[8:24], blob[24:]
nonce, ctr = recover_nonce(KEY, ct, tag)
if ctr != b"\x00\x00\x00\x01":
print(f"{name:20} counter={ctr.hex()} -> sai khoa")
continue
pt = AES.new(KEY, AES.MODE_GCM, nonce=nonce).decrypt_and_verify(ct, tag)
open(os.path.join(OUT, name[:-4]), "wb").write(pt)
print(f"{name:20} nonce={nonce.hex()} OK {len(pt)} byte")
Mình chạy script trong PowerShell và truyền chuỗi đầu vào làm tham số.
1
python Z:\recover.py "desktop-7nnknik|2844|X7A91C"
Script in ra đúng khóa đã tính trong CyberChef, và cả 7 file đều báo OK, nghĩa là bộ đếm đều bằng 00000001 và tag GCM đều khớp khi giải mã. File 7z2409-x64.exe.enc giải ra 1637343 byte, tương ứng với kích thước 1599 KB của bộ cài 7-Zip gốc còn nằm trong Downloads, nên đây là thêm một dấu hiệu cho thấy khóa đúng. Nonce của hi.txt.enc là 24c696f9341fd8e7882b44f0.
Mở thư mục recovered, chuột phải vào hi.txt và chọn Edit with Notepad++.
Flag: CSCV2026{c0rr3l4t3_b3f0r3_d3crypt}
Insidotage
Đề cho file dist.zip, giải nén ra hai phần, collect.zip là bản triage toàn bộ hệ thống file của một server Linux, và mem.raw là file capture RAM 4.3 GB của chính server đó. Vì kẻ tấn công đã xóa thư mục và purge log trước khi máy bị cô lập, phần lớn bằng chứng chỉ còn trong RAM, nên hướng chính của bài là memory forensics.
Để Volatility đọc được ảnh bộ nhớ Linux, mình cần symbol table mô tả cấu trúc kernel đúng phiên bản. Mình lấy banner kernel trực tiếp từ ảnh bộ nhớ, tải gói debuginfo tương ứng, rút file vmlinux ra rồi dùng dwarf2json sinh bảng ký hiệu đó.
1
vol -f mem.raw banners.Banners
Kết quả là Linux version 5.14.0-22.el9.x86_64, một kernel CentOS Stream 9.
Sau khi dựng xong bảng ký hiệu cho đúng bản này, mình kiểm tra nhanh danh sách tiến trình để chắc chắn ảnh bộ nhớ hợp lệ.
1
vol -s ~/vol-symbols -f mem.raw linux.pslist
Hai tiến trình đáng chú ý lộ ra ngay, firefox-bin chạy dưới quyền người dùng thường, và một tiến trình tên kworker1 cũng chạy dưới UID 1000. Tiến trình kworker thật luôn là kernel thread, chạy dưới UID 0 và có tiến trình cha là PID 2, nên một kworker chạy dưới quyền người dùng là dấu hiệu mã độc giả danh. Firefox lo phần ví và dấu chân cá nhân, kworker1 lo phần đào tiền, hai nhánh này sẽ chạy song song suốt bài.
Trước khi vào RAM, mình xem qua bản triage đĩa để nắm bối cảnh. Trong collect/opt có thư mục firefox-155, nghĩa là nạn nhân tự tải và cài Firefox 155 ngoài kho phần mềm. Mọi dấu vết duyệt web của Firefox nằm trong profile người dùng, mặc định ở ~/.mozilla/firefox, nên mình mở thư mục home của người dùng centos để tìm.
Thư mục .mozilla không còn, có lẽ là do insider đã xóa các thư mục quan trọng. Thứ duy nhất sót lại là một thư mục Old Firefox Data trên Desktop, là bản Firefox tự sao lưu khi người dùng bấm Refresh, tức là profile cũ trước khi cài ví. Mình mở file extensions.json của profile cũ này bằng Notepad++ để xem danh sách tiện ích.
Toàn bộ các tiện ích trong profile cũ đều là add-on hệ thống của Mozilla, nhìn ở trường location chỉ có app-system-defaults và app-builtin, không có cái nào app-profile do người dùng tự cài. Vậy thì khả năng cao ví được cài trên profile đang dùng, mà chính profile đó đã bị xóa khỏi đĩa. Đây là lúc rời đĩa và chuyển hẳn sang mem.raw, vì khi máy bị chụp RAM thì Firefox vẫn đang chạy, nghĩa là bộ quản lý add-on của nó vẫn giữ nguyên extensions.json trong bộ nhớ.
Q1: What is the name of the cryptocurrency wallet installed in the browser by the insider?
Firefox lưu danh sách tiện ích đang chạy trong một pref tên extensions.webextensions.uuids, ánh xạ mỗi tiện ích với một mã UUID. Mình trích pref này từ ảnh bộ nhớ để liệt kê các tiện ích đang hoạt động trên profile thật.
1
grep -a -o 'extensions.webextensions.uuids[^;]*' mem.raw | tr ',' '\n'
Giữa một loạt tiện ích của Mozilla, nổi lên một mã lạ không thuộc Mozilla, đó là keplr-extension@keplr.app. Để chắc chắn đây là ví chứ không phải tên ngấu nhiên, mình tìm bản ghi add-on đầy đủ ngay trong RAM, nơi Firefox lưu cả tên và mô tả của tiện ích.
1
grep -a -o '{"id":"keplr-extension@keplr.app".\{0,380\}' mem.raw | head -1
Bản ghi ghi rõ tên Keplr, phiên bản 0.13.37, mô tả là ví tiện ích trình duyệt cho hệ sinh thái blockchain Inter, nguồn cài trỏ về addons.mozilla.org.
Answer: Keplr
Q2: When did the insider first visit the cryptocurrency wallet’s homepage? (Unix epoch in seconds)
Lịch sử duyệt web của Firefox nằm trong file places.sqlite của profile. Profile thật đã bị xóa, nhưng các bản ghi SQLite của nó vẫn còn trong RAM, nên mình carve lại các dòng của bảng moz_places từ mem.raw rồi dựng lại thành một file places.sqlite để mở bằng DB Browser for SQLite cho dễ đọc.
Mình tách riêng bản ghi của profile Firefox 155 đang dùng vào bảng moz_places_ff155, mỗi dòng gồm URL, thời điểm truy cập dạng micro giây và cột first_visit_epoch_s mình tính sẵn ra epoch giây, vì Firefox ghi thời điểm theo micro giây nên chỉ cần chia cho một triệu.
Trình tự truy cập đọc được rất đời thường, người dùng gõ google.com, tìm từ khóa keplr, bấm vào kết quả rồi tới trang chủ https://www.keplr.app/ vào lúc 1788604050, tức 2026-09-05 10:27:30 UTC, sau đó mở trang tải và vào trang tiện ích để cài. Đây là lần đầu trang chủ ví được mở trên profile đang dùng.
Answer: 1788604050
Q3: What is the installation timestamp of the cryptocurrency wallet extension in epoch time (in milliseconds)?
Thời điểm cài một tiện ích được Firefox ghi trong chính bản ghi add-on ở trường installDate, đơn vị là mili giây epoch. Mình quay lại bản ghi Keplr đã trích ở Q1 và đọc hai trường installDate với updateDate.
Cả hai trường đều là 1788604175124, tức 2026-09-05 10:29:35 UTC, khớp với mạch thời gian ở Q2 khi người dùng vừa vào trang tiện ích vài phút trước đó. Vì installDate bằng updateDate nên tiện ích chưa từng được cập nhật sau khi cài.
Answer: 1788604175124
Q4: What is the user-defined name of the cryptocurrency wallet used by the insider?
Keplr lưu dữ liệu ví trong IndexedDB của profile, phần chưa mã hóa chứa tên ví mà người dùng tự đặt ở trường keyRingName. Khóa trong IndexedDB được mã hóa từng byte và giá trị được nén, các bản ghi lớn trải trên nhiều trang 4 KiB nằm rải rác trong bộ nhớ vật lý, nên mình ghép lại vùng chứa ví rồi mở phần dữ liệu đã giải nén bằng HxD để đọc.
Ngay sau nhãn keyRingName là chuỗi Vietdollar, kế đó là keyRingType mnemonic, tức ví được tạo từ một cụm từ khôi phục. Để chắc chắn đây là ví đang dùng, mình đối chiếu với dữ liệu live trong RAM, nơi khóa công khai của ví này cũng gắn với tên Vietdollar.
Answer: Vietdollar
Q5: What is the recovery phrase (mnemonic) of the insider’s cryptocurrency wallet?
Cụm từ khôi phục nằm trong phần sensitive của ví, và phần này được Keplr mã hóa AES. Vì Firefox đã khởi động lại trước khi máy bị chụp RAM nên ví đã khóa lại, không còn cụm từ nào ở dạng chữ trong bộ nhớ. Điểm yếu nằm ở cách Keplr kiểm tra mật khẩu, nó dẫn xuất khóa bằng PBKDF2 với số vòng lặp thấp rồi so khớp một mã xác thực, nên mình brute mật khẩu offline chỉ cần có các giá trị salt, mã xác thực và bản mã. Những giá trị này nằm ngay trong các bản ghi IndexedDB mà mình đã biết cách carve, mình đọc chúng bằng DB Browser.
Với PBKDF2 chỉ bốn nghìn vòng, một CPU thường thử hết rockyou trong ít phút. Mình viết một đoạn ngắn chạy đa luồng để kiểm tra mật khẩu.
Mật khẩu mở khóa là chocolate. Từ mật khẩu mình dẫn ra khóa AES ngẫu nhiên của ví, giải phần sensitive ra một chuỗi JSON chứa cụm từ khôi phục mười hai chữ, impose uniform fish special tip divert express increase push glide invite area.
Answer: impose uniform fish special tip divert express increase push glide invite area
Q6: What is the full path of the cryptomining binary executed on the system?
Quay lại tiến trình kworker1 giả danh ở đầu bài. Mình xem lịch sử lệnh của shell bằng Volatility để biết nó được chạy thế nào.
1
vol -s ~/vol-symbols -f mem.raw linux.bash
Lịch sử lệnh cho thấy kẻ tấn công vào thư mục /tmp, cấp quyền thực thi rồi chạy nền bằng nohup ./kworker1 > out 2>&1 &, sau đó xóa luôn file để phi tang, dọn thêm cache của VMware và cuối cùng chụp RAM bằng avml. Kết hợp với tiến trình kworker1 chạy dưới UID 1000 ở Volatility, đường dẫn đầy đủ của mã đào là /tmp/kworker1. File đã bị xóa khỏi đĩa nhưng do tiến trình vẫn chạy nên nội dung của nó còn nguyên trong page cache.
Answer: /tmp/kworker1
Q7: What is the cryptocurrency wallet address (payout address) used by the miner malware?
Vì tiến trình kworker1 còn chạy lúc chụp RAM, nội dung file đã bị xóa vẫn nằm trong page cache, mình dump binary ra rồi nạp vào IDA Free. Phần chuỗi cho thấy đây là một bản XMRig 6.26.0 đổi thương hiệu thành namqb, kèm một cấu hình JSON nhúng sẵn. Mình chuyển IDA sang Hex View và nhảy tới vùng chứa cấu hình để đọc nguyên khối JSON ở cột ký tự.
Cấu hình cho thấy coin là XMR, pool trỏ tới một địa chỉ nội bộ 172.24.106.17:3333, đăng nhập bằng tài khoản worker worker-7f3a91c2 và mật khẩu là một token tok_4d9c67e2a15b. Đây là mô hình đào qua proxy, mã độc không nối thẳng tới pool công khai mà qua một máy trung gian trong mạng, và chính máy proxy đó mới giữ địa chỉ ví nhận thưởng.
Answer: 425LXZNnbSkh1SyXdbBtvJdst6uab98knQi5BscyzkAKTPSnDDB4i2Ue5t9BxETLuuKNHnP4n8VjsaqtzprvGsf115Keabc
Q8: What is the text written on the image viewed by the insider in the browser? (without spaces)
Dấu vết cuối cùng là một tấm ảnh mà người dùng đã mở trong trình duyệt. Ngay ở bảng lịch sử câu trước, dòng cuối cùng là một link media.discordapp.net trỏ tới file 3dcd833c-2b43-4af4-9ebd-…webp, truy cập lúc 13:10:11, người dùng tải một ảnh từ Discord về xem. File webp nén đó đã bị xóa khỏi page cache nên không dump lại được, nhưng Firefox đã giải mã ảnh ra pixel để hiển thị, và vùng pixel đã giải mã vẫn còn trong bộ nhớ cửa sổ. Việc dựng ảnh từ pixel thô không có công cụ GUI nào làm thay, nên mình viết một đoạn Python ngắn đọc vùng nhớ đó rồi ghép thành ảnh. Các trang nhớ nằm rải rác khiến đa số lần dựng ra chỉ là nhiễu, cho tới khi mình đọc đúng vùng surface và dựng theo chiều rộng 640 pixel thì ảnh hiện ra rõ nét.
Đó là ảnh có dòng chữ AIplsforgiveme.
Answer: AIplsforgiveme
Flag: CSCV2026{ef14be7cddcc5adfbf0ac2dc68a690fa8f345731248ae69e2b8a43f8476f8331}
Res Impossibilis
Comming soon…
Flag:CSCV2026{tommyxiaomihackerbox@gmail.com_9ec3d41b5db1baee571dbe1bebff4774b85d61e046edce96a86dd489644ce2e7_VLT9-35579852-2beeedad_730f0c0eadc0edb118e4fdc6fbee892e}
































































