Một buổi sáng ... bị lừa đảo gọi
Phân tích tĩnh và động một APK Flutter đóng gói bằng packer thương mại, dùng NetEase NIM làm kênh truyền và một lớp AES tự viết để né kiểm duyệt.
Một buổi sáng random thất nghiệp như mọi ngày, tự nhiên mình nhận được một cuộc gọi:
- Anh ơi em không bán hàng gì cả, bên em xin tặng anh một món quà hoàn toàn miễn phí và sẽ được gửi về tận nhà
- Quà gì vậy em ?
- Ba món quà là: Máy xay sinh tố, Nồi cơm điện, Ấm Siêu tốc

... nghe đến đây mình biết là lừa đảo rồi. Và mình muốn đớp thính để xem cái app lừa đảo của nó là gì để mình xem nó làm như nào ... Jobless behavior.
Nhưng ... Mình nhận được tiền thật. Rất xin lỗi hi vọng bạn không bị Chick Điện. Mình đã gửi toàn bộ số tiền đó vào quỹ Hi Vọng để xây trường cho trẻ em vùng cao, giảm một ít nghiệp cho chúng ta.
Link App cho bạn nào muốn điều tra: ZentraOne.apk.zip - mật khẩu giải nén là omelet.tech, zip mã hoá AES-256 nên cần 7-Zip hoặc WinRAR để mở.
Một APK Flutter 82 MB, bảy lớp chống phân tích, chứng chỉ ký bịa danh tính, và không một dòng backend nào của chính nó. Toàn bộ hạ tầng nằm nhờ trên NetEase, còn nội dung lừa đảo được bọc bằng một lớp AES tự viết để chính NetEase cũng không đọc được.
- Mẫu: ZentraOne.apk
- SHA-256: e0769a21eb15eb4c153f7eb49a3d838af03ebb26f51cb56673cf52d62abdf7b3
- Kích thước: 82.492.142 byte
- Phạm vi: phân tích tĩnh toàn diện + phân tích động trong sandbox có root

Mẫu chạy trên emulator có root, lưu lượng bắt ở cấp vNIC. Ảnh minh hoạ.
Tóm tắt
Một app chat mượn toàn bộ hạ tầng của người khác
ZentraOne trông như một ứng dụng chat. Bóc ra thì phần chat là mã mẫu của NetEase Yunxin, tải về không sửa gì đáng kể. Giá trị của mẫu nằm ở lớp mà kẻ vận hành viết thêm: hệ thống tài khoản hai hạng, giới hạn IP cho nhân sự nội bộ, số thành viên nhóm bịa được bằng tay, giấy phép kinh doanh của Taobao và Tmall đóng gói sẵn để dựng uy tín, và một lớp AES bọc lên từng tin nhắn trước khi đẩy qua NetEase.
Tôi bắt đầu bằng câu hỏi đơn giản: backend của app này nằm ở đâu. Quét 42.177 chuỗi ASCII trong libapp.so không ra URL nào. Quét thêm bản arm32, các file DEX, resources.arsc và toàn bộ assets/ cũng không. Giả thuyết ban đầu của tôi là chuỗi bị mã hoá trong Dart snapshot. Sau khi dựng emulator có root, cài CA vào system store và bắt gói ở cấp vNIC, câu trả lời hoá ra khác: app không có backend riêng. Từng byte rời thiết bị đều đi tới NetEase, cộng thêm một ảnh đại diện tải từ CDN Facebook.
Kết luận đó đổi hẳn hướng phòng thủ. Không có domain nào để chặn, không có server nào để takedown. Chỉ có một appKey NetEase duy nhất giữ toàn bộ hệ thống đứng vững.
Phần thứ hai của bài viết về những gì tôi thấy khi để app chạy. Bốn ảnh giấy phép trong gói cài hoá ra là giấy phép kinh doanh thật của JD, Douyin, Taobao và Tmall, tải từ cổng công bố thông tin doanh nghiệp Trung Quốc. Kịch bản chat chạy qua một chuỗi có nhịp: dụ quà, lấy địa chỉ nhà, giao nhiệm vụ cày tương tác TikTok, trả ba khoản thưởng nhỏ, rồi đẩy nạn nhân vào một phòng nhóm mở đúng giờ hẹn với vài chục tài khoản cùng lúc reo hò. Ảnh chụp màn hình từng bước nằm ở hai mục "Kịch bản" và "Phòng nhóm".
Quy ước đọc
[Xác nhận] quan sát trực tiếp từ mẫu hoặc từ lưu lượng bắt được. [Suy luận] kết luận rút ra từ nhiều dấu hiệu, có cơ sở nhưng không quan sát trực tiếp. [Bỏ ngỏ] giả thuyết chưa kiểm chứng.
Định danh mẫu
Package khi cài khác application ID
- Tên hiển thị: Zentra One
- Application ID: com.zentraone.chat
- Package khi cài: net.vo72xf.linker.zvan9.base
- Application class: com.zentraone.chat.FlutterIMApplication
- Module Flutter: zentraoneandroid
- Version: 1.0.0 (versionCode 72701)
- Dart snapshot ID: 78da37fed6bf1489361a312568249f3f
- targetSdkVersion: 24 (Android 7.0, phát hành 2016)
- NIM appKey: f4acf0707e9c02c80e68bfb2ae4123c6
Thuộc tính package trong manifest và application ID thật lệch nhau. Tôi từng đoán net.vo72xf.linker.zvan9.base là mồi nhử, sau đó pm list packages trên máy thật cho thấy đó mới là package ID được cài. Chuỗi com.zentraone.chat chỉ còn sống trong tên lớp Java và tên các quyền tuỳ biến. Khuôn tên gói gồm bốn đoạn ngẫu nhiên khớp với khuôn CN trong chứng chỉ ký, dấu hiệu của hạ tầng build sinh danh tính mới cho mỗi lần đóng gói.
Chống phân tích
Bảy lớp chống phân tích và cách vượt qua
Đây là khoản đầu tư kỹ thuật lớn nhất trong mẫu. Bảy kỹ thuật xếp chồng, mỗi kỹ thuật đánh vào một công cụ phân tích khác nhau.

Packer thương mại Trung Quốc đặt bảy lớp bẫy khác nhau, mỗi lớp nhắm một công cụ. Ảnh minh hoạ.
3.1 Cờ encrypted giả trong ZIP header
Packer đặt bit 0x1 trong general purpose bit flag của AndroidManifest.xml. Android bỏ qua bit này khi cài, còn mọi thư viện ZIP tuân thủ chuẩn thì dừng lại:
keytool -printcert -jarfile ZentraOne.apk
-> ZipException: invalid CEN header (encrypted entry)
python zipfile.extractall()
-> RuntimeError: File 'AndroidManifest.xml' is encrypted
Cách vượt: duyệt toàn bộ ZipInfo, xoá bit 0x1 khỏi flag_bits, rồi giải nén. 1302 entry ra thành công.
import zipfile
z = zipfile.ZipFile("ZentraOne.apk")
for info in z.infolist():
info.flag_bits &= ~0x1 # xóa cờ encrypted giả
z.extract(info, "out/")
3.2 Entry mồi lồng sâu
50 entry còn lại thất bại với WinError 3. Chúng mang tên giả dạng file thật rồi lồng thêm nhiều tầng Unicode Ả Rập:
classes2.dex/afضgٜmur/ypzl۞dmٝ/.../ؗاْٚطتكهِؐ۬۫ۥذوّجٛخٕ.dex
resources.arsc/mؔznٜdgz/zر/تz/.../ًٟ۞ٍۗصَُؚٕۭۧۤۨ۟دؘٞۛعٜذ۠بؐؒ۞أدۘ۩.arsc
AndroidManifest.xml/هxcyl/mhvoe/.../بكٍٍ۬ۧۛۛٝٗۗرصۙهّۦِقه.xml
Trên Windows chúng vượt MAX_PATH và làm hỏng quá trình giải nén. Tool nào bốc nhầm file .dex trong đám này sẽ đi phân tích dữ liệu rác.
3.3 Manifest phình to
AndroidManifest.xml nặng 744.820 byte, so với 2 tới 20 KB của một app bình thường. String pool chứa 9.672 chuỗi, trong đó chỉ 255 chuỗi là ASCII thật. Phần còn lại là rác Unicode.
3.4 Namespace URI bị đầu độc
Các URI namespace trong AXML bị thay bằng ký tự Unicode không hợp lệ. lxml ném lỗi và kéo theo cả androguard:
ValueError: Invalid namespace URI 'ِ۫ظ۟تؓ'
-> androguard 4.1.4 AXMLPrinter thất bại hoàn toàn
3.5 Resource ID giả cho attribute
Toàn bộ attribute mang resource ID rác. Androguard trả về mọi thuộc tính dưới dạng UNKNOWN_SYSTEM_ATTRIBUTE_xxxxxxxx, tức là không đọc được gì.
Cách vượt: viết parser AXML thô, đọc tên attribute trực tiếp từ string pool và tra bảng resource ID chuẩn của Android cho các trường hợp còn thiếu. Một chi tiết dễ sai ở đây:
# attributeStart tính từ đầu ResXMLTree_attrExt, tức chunk offset + 16
# KHÔNG phải từ đầu chunk. Tính sai thì parser in ra giá trị thay vì tên thuộc tính.
p = off + 16 + attribute_start
3.6 File mồi ở gốc APK
Khoảng 20 file ở thư mục gốc và 8 file trong assets/ mang tên Ả Rập ngẫu nhiên, nội dung là văn bản rác với entropy 4,76.
3.7 Dấu vết packer thương mại
assets/.signature/dex.sig 526 byte, entropy 7.21
assets/dexopt/baseline.prof
assets/dexopt/baseline.profm
.signature/dex.sig là dấu tay của họ packer hardening thương mại Trung Quốc.
Chứng chỉ ký
Bốn bất thường trong bảy dòng
Vì cờ encrypted giả phá keytool, tôi bóc META-INF/61W5KCDV.RSA ra và parse PKCS#7 bằng asn1crypto.
Subject : CN=dev-RocLHb2w, OU=Research, O=NextGen, L=Nanjing, ST=Guangdong, C=CN
Issuer : giống hệt Subject (tự ký)
Serial : 1114521921369073213
Valid : 2026-07-28 03:02:50 UTC -> 2027-03-18 03:02:50 UTC
SigAlg : sha384WithRSA, RSA 2048-bit
SHA-256 : b2ae6672746ea747698a1939adb72a63b4e7e133e0926165d3fb87d8c2b630c3
| Dấu hiệu | Nội dung |
|---|---|
| Địa lý bịa | L=Nanjing, ST=Guangdong. Nam Kinh thuộc Giang Tô, không thuộc Quảng Đông. Người điền thật không nhầm chỗ này. |
| CN sinh máy | dev-RocLHb2w mang hậu tố 8 ký tự ngẫu nhiên, cùng khuôn với package ID. |
| Ký trong ngày | Hiệu lực từ 2026-07-28, đúng ngày mẫu được tải về. Build tạo riêng, không phải bản phát hành chung. |
| Hiệu lực 7,7 tháng | Google Play yêu cầu chứng chỉ có hiệu lực tối thiểu tới 2033. Mẫu này không thể lên Play Store ngay từ khâu ký. |
O=NextGen và OU=Research là chuỗi placeholder, không ứng với pháp nhân nào. Fingerprint SHA-256 của chứng chỉ là pivot mạnh nhất để săn các mẫu cùng chiến dịch. Vì hạ tầng build sinh danh tính mới mỗi lần, fingerprint sẽ khác nhau giữa các bản, nhưng khuôn Subject CN=dev-<8 ký tự>, O=NextGen, OU=Research thì giữ nguyên.
Hạ cấp bảo mật
targetSdk 24 và 46 quyền
App nhắm Android 7.0 phát hành năm 2016, trong khi bản build được ký năm 2026. Không có lý do kỹ thuật nào để một app Flutter viết năm 2026 làm vậy. Lợi ích thu được nằm ở chỗ khác:
| Cơ chế | Trạng thái ở targetSdk 24 |
|---|---|
| Cleartext HTTP | Cho phép mặc định. usesCleartextTraffic chỉ mặc định false từ SDK 28. |
| Scoped Storage | Được miễn trừ. Truy cập toàn bộ bộ nhớ ngoài theo kiểu cũ. |
| CA do người dùng cài | Không tin. Proxy phân tích phải nạp CA vào system store. |
| PendingIntent mutability | Không ràng buộc. |
| Google Play | Không đủ điều kiện. Play yêu cầu SDK 34 trở lên. |
Trong 46 quyền khai báo, nhóm nhạy cảm gồm:
android.permission.MANAGE_EXTERNAL_STORAGE <- toàn quyền file system
android.permission.CAMERA
android.permission.RECORD_AUDIO
android.permission.ACCESS_FINE_LOCATION
android.permission.READ_PHONE_STATE <- định danh thiết bị
android.permission.WRITE_SETTINGS
android.permission.READ_MEDIA_IMAGES / AUDIO / VIDEO
MANAGE_EXTERNAL_STORAGE cộng với targetSdk 24 cho phép đọc ghi toàn bộ bộ nhớ ngoài mà không vướng Scoped Storage. Với một app chat, tổ hợp camera, micro, GPS và toàn quyền file system không phục vụ tính năng nào nhìn thấy được trong giao diện.
Kiến trúc
App được xây từ demo của chính NetEase
Bản đồ module Dart lấy từ đường dẫn package: trong snapshot chỉ thẳng tới im_demo, ứng dụng mẫu chính thức của NetEase Yunxin IM:
package:im_demo/main.dart
package:im_demo/src/home/home_page.dart
package:im_demo/l10n/demo_localization/demo_kit_client_localizations_vi.dart
nim_chatkit / nim_chatkit_ui / nim_conversationkit_ui
nim_contactkit_ui / nim_teamkit_ui / nim_searchkit_ui
netease_common_ui / netease_corekit
Toàn bộ phần nhắn tin, nhóm, danh bạ và tìm kiếm là mã mẫu của NetEase, chỉ đổi thương hiệu. Kẻ vận hành không viết ứng dụng chat. Họ lấy demo rồi bổ sung một lớp riêng lên trên, và lớp riêng đó mới đáng đọc.
flowchart TB
T3["TẦNG 3 - LỚP TỰ VIẾT CỦA KẺ VẬN HÀNH
______________________________________
auth: login / register / verify / forgot_password
Xác thực doanh nghiệp + license_preview
Cấp bậc thành viên + yxVirtualMemberCount
Mật khẩu bảo mật 8 số + quản lý thiết bị
Lớp AES bọc tin nhắn
Console desktop cho nhân sự nội bộ"]
T2["TẦNG 2 - im_demo, MÃ MẪU NETEASE, KHÔNG SỬA
______________________________________
nim_chatkit_ui / nim_conversationkit_ui
nim_contactkit_ui / nim_teamkit_ui / nim_searchkit_ui
netease_common_ui / netease_corekit"]
T1["TẦNG 1 - NETEASE YUNXIN NIM SDK v2
______________________________________
link protocol / NOS object storage / LBS + HTTPDNS"]
T3 --> T2 --> T1
Hình 1. Ba tầng của ứng dụng. Tầng 1 và tầng 2 tải về từ NetEase. Chỉ tầng 3 là do kẻ vận hành viết, và toàn bộ giá trị phân tích nằm ở đó.
Cơ chế vận hành
Bốn tính năng không phục vụ người dùng
7.1 yxVirtualMemberCount, số thành viên bịa bằng tay
yxVirtualMemberCount <- khóa lưu trên server NIM (tiền tố yx = team extension)
"Virtual member count" <- nhãn giao diện tiếng Anh
"Số lượng thành viên ảo" <- nhãn giao diện tiếng Việt
_buildVirtualMemberCountTile@1606302877 <- dựng ô nhập trong cài đặt nhóm
_applyVirtualMemberCountUpdate@1606302877 <- ghi giá trị
_virtualCountController@1606302877 <- TextEditingController
Quản trị nhóm gõ tay một con số. Con số đó ghi vào trường mở rộng của nhóm trên server NIM rồi hiển thị cho mọi thành viên thay cho số thành viên thật. Một nhóm 5 người trông như nhóm 5.000 người.
Không có nhu cầu chính đáng nào cho tính năng này trong một sản phẩm nhắn tin. Nó tồn tại để dựng bằng chứng xã hội giả.
7.2 Hai hạng tài khoản
"Tài khoản nội bộ" <- internal account
"Tài khoản thường" <- regular account
"Loại tài khoản này không được đăng nhập loại ứng dụng này"
desktop_account_type
package:im_demo/desktop/desktop_conversation_page.dart
package:im_demo/desktop/notification_window.dart
package:im_demo/desktop/windows_toolbar.dart
Backend phân biệt tài khoản nội bộ với tài khoản thường và chặn tài khoản nội bộ đăng nhập từ client của người dùng thường. Kèm theo là một bộ trang desktop riêng. Nhân sự vận hành ngồi trên console Windows, nạn nhân dùng app điện thoại.
7.3 Giới hạn IP cho tài khoản nội bộ
"Non-internal IP" / "Không phải IP nội bộ"
Backend từ chối đăng nhập tài khoản nội bộ từ IP ngoài danh sách cho phép. Đây là biện pháp bảo vệ hoạt động của bên vận hành, không phải tính năng phục vụ người dùng.
7.4 Giấy phép sàn TMĐT dùng làm chứng nhận
assets/flutter_assets/assets/license/license.jpeg 230 KB JD.com
assets/flutter_assets/assets/license/tb.jpeg 123 KB Taobao
assets/flutter_assets/assets/license/tm.jpeg 73 KB Tmall
assets/flutter_assets/assets/license/dy.jpeg 120 KB Douyin
+ liên kết https://jd.com/ nhúng trong libapp.so
Bốn ảnh giấy phép đóng gói sẵn trong APK, hiển thị qua LicensePreviewPage, gắn với luồng "Xác thực doanh nghiệp" và huy hiệu tick ic_enterprise_check.svg. Tôi giải nén cả bốn và mở ra xem. Chúng không phải giấy tờ giả.
assets/flutter_assets/assets/license/, không chỉnh sửa. Đây là giấy phép kinh doanh thật của bốn pháp nhân có thật, tải về từ hệ thống công bố thông tin doanh nghiệp quốc gia Trung Quốc. Chữ chìm trên nền và dòng www.gsxt.gov.cn ở chân trang còn nguyên.Bốn tài liệu này thật, được nhà nước Trung Quốc công bố công khai, và bất kỳ ai cũng tải được. Cái bị làm giả là ngữ cảnh. Một người Việt mở app, thấy tick xanh "doanh nghiệp đã xác thực" rồi thấy giấy phép có quốc huy và con dấu đỏ, sẽ hiểu rằng ZentraOne được JD, Taobao, Tmall và Douyin uỷ quyền. Không hề.
Chọn giấy tờ Trung Quốc để dụ người Việt nghe có vẻ vụng, nhưng nó phục vụ đúng mục đích. Nạn nhân không đọc được chữ Hán, không biết gsxt.gov.cn là gì, và không có cách nào đối chiếu. Một tài liệu càng khó kiểm chứng thì càng dễ được tin. Cùng lúc, việc bốn file này nằm sẵn trong gói cài cho thấy bộ kit gốc do người nói tiếng Trung dựng, khớp với 26 tên section lỗi bằng tiếng Trung ở mục 7.5.
Vì là tài nguyên đóng gói lúc build chứ không tải từ mạng, chúng tồn tại trong mọi bản cài và không biến mất khi hạ tầng bị gỡ. Bốn giá trị băm dưới đây là chỉ dấu săn mẫu bền hơn cả domain lẫn IP.
64284f711aaac7d25544df59c5fa134e27adf46266d25f0e20a50a9f282a3724 license.jpeg JD
c3b2cceb3edaa666e4c42a883689fd284ed53212bb00971bdd896efef51e7392 dy.jpeg Douyin
bde26821dcb8bb138ca30be0fc2ad440d7ee5e5690d16e6008baf1f1100c765a tb.jpeg Taobao
c1f7ac41579b1c5a5352e65bc1a18971ccbc530363cedf56c7676b0274e6376b tm.jpeg Tmall
Phần vỏ thương hiệu cũng nằm trong cùng thư mục và cùng một khuôn: một logo chữ Z cam đen, một màn hình chờ dựng bằng AI với thành phố, ruy băng đỏ và pháo giấy. Không có cái nào liên quan tới thương mại điện tử. Chúng chỉ cần trông sinh động và đắt tiền.
7.5 Ai nói chuyện với ai
Bốn dấu hiệu độc lập cùng chỉ về một mô hình:
| Dấu hiệu | Suy ra |
|---|---|
| Bản địa hoá chỉ có en, vi, zh | Người dùng cuối là người Việt |
| 274 mã lỗi có message_vi, nhưng 26 tên section đều tiếng Trung (登录错误码, 群组错误码, 反垃圾错误码) | Đội phát triển và vận hành nói tiếng Trung |
| Đóng gói lpinyin, thư viện sắp xếp danh bạ theo pinyin | Có người dùng nội bộ dùng tiếng Trung trên cùng app |
| GoogleTranslateService + dịch tự động theo từng hội thoại | Cầu ngôn ngữ giữa hai nhóm |
Người vận hành nói tiếng Trung, nạn nhân nói tiếng Việt, app dịch qua lại theo thời gian thực. Nội dung tin nhắn vì thế đi qua Google Translate ở dạng rõ, thêm một bên thứ ba đọc được hội thoại riêng tư.
Kênh truyền
Giao thức nhị phân NIM chạy trên cổng 443
Chat không đi qua HTTP và không đi qua TLS. Nó dùng giao thức nhị phân riêng của NetEase NIM, chạy trên TCP cổng 443 để trông giống HTTPS.
Đích là link-ga-sg.yunxinfw.com, phân giải thành 34.126.79.182, Google Cloud Singapore. Byte đầu của stream không phải TLS record:
ce09 01 05 000001ba 040000 | 789c 01 ba04 45fb | <body>
^^^^ ^^ ^^ ^^^^^^^^ ^^^^^^ ^^^^ ^^ ^^^^ ^^^^
| | | | | | | |
| | | | | | | +-- độ dài 1210 + bù
| | | | | | +------- block type 01 = STORED, không nén
| | | | | +----------- zlib header
| | | | +------------------- serial / flags
| | | +--------------------------- body_len = 442
| | +-------------------------------- command 0x05 (login)
| +----------------------------------- service 0x01
+---------------------------------------- magic
Block deflate là kiểu stored, tức không nén và không mã hoá. Body đọc thẳng ra được:
@21 100991 <- SDK version
@43 f4acf0707e9c02c80e68bfb2ae4123c6 <- NIM appKey, ASCII tran
Phát hiện
appKey nằm lộ thiên trong gói đầu tiên của mọi phiên đăng nhập. Ai nghe được đường truyền, kể cả ISP hay chủ một Wi-Fi công cộng, đều lấy được appKey mà không cần chạm vào file APK.
Từ frame thứ hai trở đi, cả hai chiều đều là dữ liệu entropy cao, không còn zlib header nào giải nén được. Đó là lớp mã hoá riêng của NIM sau khi trao khoá.
sequenceDiagram
autonumber
participant App as ZentraOne
participant LBS as lbs.netease
participant Edge as link-ga-sg
participant NOS as NOS storage
participant Op as Console
App->>LBS: POST /lbs/conf.jsp?k=appKey (HTTPS)
LBS-->>App: danh sách endpoint, region SG
App->>Edge: TCP 443, frame ce09 01 05 (PLAINTEXT)
Note over App,Edge: appKey + SDK version lộ trên đường truyền
Edge-->>App: handshake, từ đây trở đi mã hoá riêng của NIM
App->>App: AES bọc nội dung tin nhắn
App->>Edge: gửi ciphertext qua NIM
Edge-->>Op: NIM chuyển tiếp ciphertext
Op->>Op: giải mã bằng khoá của kẻ vận hành
App->>NOS: upload ảnh / file (HTTPS thật, TLS đầy đủ)
Note over NOS: KHÔNG bọc AES
Hình 4. Trình tự một phiên. lbs.netease là lbs.netease.im, link-ga-sg là link-ga-sg.yunxinfw.com, Console là máy trạm của nhân sự vận hành. Frame đăng nhập đầu tiên đi ở dạng rõ. Nội dung tin nhắn được bọc AES trước khi vào NIM, còn ảnh và file thì không.
Mã hoá
Lớp AES tự viết để làm mù NetEase
Màn hình hiển thị tiếng Việt đọc được. Cơ sở dữ liệu tin nhắn thì không. Tôi đọc trực tiếp bảng msghistory trong msg.db:
| Người gửi | Kích thước sau base64 | mod 16 | Entropy |
|---|---|---|---|
| zentraone (hệ thống) | - | - | plaintext |
| khanhly2 (vận hành) | 176, 272, 256, 80, 288, 48 B | 0 | 6,01 - 7,24 |
| tài khoản thử nghiệm | 32, 32, 96, 48, 32 B | 0 | 4,88 - 6,35 |
Mọi tin nhắn người với người đều là bội số chính xác của 16 byte, entropy cao. Đó là AES khối mã hoá phía client, nằm ngoài NIM SDK. Tin hệ thống do chính app phát thì để nguyên dạng rõ.
import sqlite3, base64, collections, math
def entropy(b):
c = collections.Counter(b)
return -sum((v/len(b)) * math.log2(v/len(b)) for v in c.values())
db = sqlite3.connect("msg.db")
for content, sender in db.execute("select content, fromid from msghistory"):
try:
raw = base64.b64decode(content, validate=True)
except Exception:
print(f"{sender:14} PLAINTEXT")
continue
print(f"{sender:14} {len(raw):4}B mod16={len(raw) % 16} H={entropy(raw):.2f}")
Khoá không nằm trong Asymmetric.xml, file này chỉ chứa VERSION_CODE. Quét tĩnh libapp.so cho thấy pointycastle được đóng gói kèm, nhận ra qua các hằng số đường cong elliptic của thư viện này. Crypto vì thế chạy thuần Dart bên trong snapshot đã strip, không đi qua javax.crypto, nên hook ở tầng Java không chạm tới được. Tôi chưa lấy được khoá.
flowchart TB
R1["ISP
chủ Wi-Fi công cộng"]
R2["NetEase"]
R3["Kẻ vận hành"]
T1["TẦNG 1 - TCP 443
Metadata: ai nói với ai, khi nào"]
T2["TẦNG 2 - Frame NIM ce09
Frame đầu PLAINTEXT
appKey + SDK version"]
T3["TẦNG 3 - Mã hoá riêng của NIM
Thân phiên"]
T4["TẦNG 4 - AES của kẻ vận hành
Nội dung tin nhắn"]
T1 --> T2 --> T3 --> T4
R1 -.->|đọc được| T1
R1 -.->|đọc được| T2
R2 -.->|đọc được| T3
R3 -.->|đọc được| T4
Hình 5. Bốn tầng bảo vệ và ai đọc được tầng nào. NetEase vận chuyển và lưu trữ toàn bộ tin nhắn nhưng chỉ giữ được ciphertext, vì tầng trên cùng nằm ngoài SDK của họ.
Hệ quả với bên phòng thủ
Kiểm duyệt nội dung tự động của NetEase không đọc được kịch bản lừa đảo. Một yêu cầu cung cấp dữ liệu gửi tới NetEase cũng chỉ thu được ciphertext. Lớp AES này không bảo vệ người dùng, vì khoá do chính kẻ vận hành nắm. Nó bảo vệ kẻ vận hành khỏi nhà cung cấp hạ tầng của mình.
Ảnh và file đính kèm thì đi đường khác, qua NetEase NOS bằng HTTPS thật và không được bọc AES. Ảnh giấy tờ tuỳ thân nạn nhân gửi trong chat nằm trên NOS ở dạng NetEase đọc được.
Backend
Câu trả lời là không có backend riêng
Giả thuyết ban đầu của tôi sai. Tôi đi tìm một API base URL bị mã hoá trong Dart snapshot. Nó không tồn tại.

Emulator Android 13 có root, CA mitmproxy trong system store, bắt gói ở cấp vNIC. Ảnh minh hoạ.
Sau khi dựng môi trường, đây là toàn bộ endpoint mà app chạm tới trong một phiên khởi động cộng tự đăng nhập đầy đủ, 456 gói:
| IP | SNI | Chủ sở hữu |
|---|---|---|
| 34.49.191.111 | lbs.netease.im, abt-online.netease.im, change-api.netease.im | NetEase |
| 34.126.79.182 | không SNI, giao thức NIM thô | NetEase link SG |
| 8.219.207.113 | statistic-overseas.yunxinfw.com | NetEase |
| 220.197.34.75/76 | statistic.live.126.net | NetEase |
| 156.59.129.229 | wannos.127.net | NetEase NOS |
| 113.171.62.81 | scontent.fsgn24-1.fna.fbcdn.net | Facebook CDN, ảnh đại diện |
| 142.251.152.119 | www.google.com | Android tự kiểm tra kết nối |
Đối chiếu DNS công khai qua 8.8.8.8 và 1.1.1.1 khớp chính xác từng IP. Không có DNS hijack, không có domain lạ. Không một byte nào đi tới hạ tầng do kẻ vận hành sở hữu.
Điều đó giải thích vì sao 42.177 chuỗi ASCII trong libapp.so không chứa URL tuỳ biến nào. Không có gì để giấu. Kẻ vận hành điều khiển toàn bộ hệ thống qua NIM Server API từ phía họ, gọi từ console desktop, không phải từ app.
Một chỗ tôi chưa mở được: tab "Discover" nạp một WebView nhưng chặn lại bằng hộp thoại "Bind invitation code". Tôi không nhập mã, nên kết luận trên chỉ chắc chắn với những gì đã quan sát, tức khởi động, đăng nhập, chat riêng và phòng nhóm. Mục "Nghi ngờ và giả thuyết" nói rõ khoảng trống này rộng đến đâu.
Hai cái bẫy tôi mắc phải
Lần bắt gói đầu tiên trả về pcap 2 KB gần như rỗng. Nguyên nhân:adb emu network capturechỉ ngheeth0, còn AVD đang định tuyến quawlan0. Chạysvc wifi disableđể ép traffic sang cellular thì pcap nhảy lên 93 KB và đầy đủ.
Cái thứ hai: tra chứng chỉ TLS của IP đích không giúp định danh khi đích nằm sau Alibaba Cloud Global Accelerator. Cả bốn IP trong dải155.102.4.0/24đều trả cert mặc định*.certfallback.com.
# ép traffic qua eth0 rồi bắt gói ở cấp vNIC
adb -s emulator-5556 shell svc wifi disable
adb -s emulator-5556 emu network capture start capture.pcap
adb -s emulator-5556 shell am force-stop net.vo72xf.linker.zvan9.base
adb -s emulator-5556 shell monkey -p net.vo72xf.linker.zvan9.base -c android.intent.category.LAUNCHER 1
sleep 35
adb -s emulator-5556 emu network capture stop
Kịch bản
Luồng lừa đảo bắt được trực tiếp
Tôi tạo một tài khoản thử nghiệm và để luồng chạy tự nhiên. Ngay sau khi đăng ký, hai tài khoản chủ động nhắn tới:
| Tài khoản NIM | Nick hiển thị | Vai trò |
|---|---|---|
| zentraone | Zentra One | Tài khoản hệ thống tự động |
| khanhly2 | Chuyên Viên - Khánh Ly | Nhân sự vận hành, ảnh đại diện hotlink từ CDN Facebook |

Giai đoạn này chưa đòi tiền. Mục tiêu là thu tên thật, số điện thoại, địa chỉ nhà, rồi chuyển kênh sang thoại. Ảnh minh hoạ.
flowchart TD
S1["Đăng ký bằng mã mời"] --> S2["zentraone gửi lời chào tự động"]
S2 --> S3["khanhly2 tự động kết bạn
'Chúng ta đã là bạn, bắt đầu trò chuyện nhé~'"]
S3 --> S4["Dụ khai báo phần thưởng vật chất"]
S4 --> S5["Nạn nhân nhập tên quà
+ địa chỉ nhà đầy đủ"]
S5 --> S6["'Tổng đài sẽ điện lại vào SĐT trên
để xác nhận... vui lòng bắt máy nhé ạ'"]
S6 --> S7["Giao nhiệm vụ tương tác video TikTok"]
S7 --> S8["'Xác nhận hoàn thành 3 sản phẩm
Phần thưởng 18k đang được thanh toán'"]
S8 --> S9["Khảo sát năm sinh, nghề nghiệp,
nhóm hàng quan tâm - thưởng 10k"]
S9 --> S10["Đẩy sang phòng nhóm
Điểm danh 18h00 - thưởng 30k"]
S10 --> S11["Giai đoạn nạp tiền
ngoài phạm vi quan sát"]
S5 -.->|thu hoạch| H1["Tên thật
Số điện thoại
Địa chỉ nhà"]
S9 -.->|thu hoạch| H2["Năm sinh
Nghề nghiệp
Nhóm hàng quan tâm"]
S3 -.->|ghi log| H3["IP đăng nhập
Device ID"]
Hình 6. Luồng quan sát được qua hai phiên, từ lúc đăng ký tới lúc bị đẩy vào phòng nhóm.
Hai câu quyết định, chép nguyên văn từ khung chat:
"Để đảm bảo quà không bị thất lạc và trao tới tay anh chị, tổng đài sẽ điện lại vào SĐT trên để xác nhận anh chị đang là người thực hiện đăng kí quà, anh chị vui lòng bắt máy nhé ạ"
"Mình nghe máy xong quay lại gặp em phát tiền thưởng nha"
Đây là khuôn task scam kiểu nhận quà rồi nhận tiền thưởng, phổ biến ở Việt Nam. Món quà đóng vai trò lý do hợp lý để nạn nhân tự khai tên thật, số điện thoại và địa chỉ nhà. Cuộc gọi tiếp theo đưa nạn nhân ra khỏi kênh có log mà bên thứ ba đọc được. Khoản tiền thưởng nhỏ ở bước sau xây lòng tin trước khi vào giai đoạn nạp tiền.
Trong lịch sử có hai tin nhắn đã bị thu hồi phía vận hành. Bảng revoke_message giữ lại UUID nhưng nội dung thì mất.
Giai đoạn nhiệm vụ và ba lần trả thưởng nhỏ
Tôi mở lại app sau vài giờ và bắt được phần kịch bản mà phiên đầu chưa chạm tới: công việc mà nạn nhân được giao.
Công việc là bấm tương tác cho video TikTok. Chuyên viên gửi ảnh chụp một video review nồi cơm điện, nạn nhân thao tác rồi gửi ảnh màn hình về, và nhận lại "Xác nhận hoàn thành 3 sản phẩm. Phần thưởng 18k đang được thanh toán". Đây là mô hình cày tương tác thuê, và nó có ích cho kẻ vận hành theo hai hướng cùng lúc: lượt xem là hàng bán được, còn thao tác thì dạy nạn nhân rằng làm xong thì có tiền.
Ngay sau lần trả đầu tiên là một bảng khảo sát, đổi lấy 10k:
"Mình vui lòng dành ít phút cho bên em thêm một vài thông tin sau nhé, phần thưởng là 10K sẽ được gửi tặng sau khi hoàn thành khảo sát ạ. 1. Năm Sinh: 2. Nghề nghiệp: 3. Sản phẩm yêu thích:"
Năm sinh và nghề nghiệp không phục vụ việc gửi quà. Chúng phục vụ việc chấm điểm: ai đủ tuổi có tiền tiết kiệm, ai làm nghề tự do nên rảnh giờ hành chính, ai đang quan tâm nhóm hàng nào. Cộng với tên thật, số điện thoại và địa chỉ nhà đã lấy ở bước trước, hồ sơ nạn nhân đã đủ để phân loại và định giá.
Ba mức thưởng 18k, 10k, 30k đều nhỏ đến mức trả thật vẫn rẻ. Đó là điều kiện tiên quyết của giai đoạn sau: khi hệ thống đã trả đúng ba lần liên tiếp, yêu cầu nạp tiền để "mở nhiệm vụ giá trị cao hơn" nghe hợp lý. Tôi dừng trước bước đó.
App tự khoe hạ tầng theo dõi cho nạn nhân
Tin hệ thống "Thông báo đăng nhập an toàn" in ra cho chính nạn nhân xem:
Thời gian đăng nhập: 11:12:02
Phương thức đăng nhập: Đăng nhập bằng mật khẩu
Loại thiết bị: Android
ID thiết bị: 2da95f6b-****-****-****-70028b932d9f
IP đăng nhập: [đã che]
Bên vận hành ghi và đối chiếu IP cùng device fingerprint cho từng lần đăng nhập, và có thể buộc đăng xuất từ xa. Chuỗi giao diện xác nhận điều đó: "Thiết bị không đáng tin", "Buộc đăng xuất", "Tài khoản của bạn đã bị hệ thống buộc đăng xuất!".
Mật khẩu lưu ở dạng rõ
<!-- shared_prefs/FlutterSharedPreferences.xml -->
<string name="flutter.im_demo_auto_login_account">[đã che]</string>
<string name="flutter.im_demo_auto_login_password">[đã che]</string>
<boolean name="flutter.im_demo_auto_login_enabled" value="true" />
Tên khoá giữ nguyên tiền tố im_demo_ của mẫu NetEase. Bất kỳ ai cầm được máy, hoặc bất kỳ app nào có quyền đọc bộ nhớ ngoài, đều lấy được mật khẩu. Với thói quen dùng lại mật khẩu, một tài khoản chat rò rỉ kéo theo nhiều tài khoản khác.
Phòng nhóm
Nơi kịch bản chuyển từ một người sang một đám đông
Chuyên viên hẹn 17h55 mở phòng, 18h00 điểm danh. Phòng mở đúng 17h55. Đó là chi tiết đáng chú ý đầu tiên: một trò lừa vặt không chạy theo lịch, một dây chuyền thì có.
Nội quy nhóm là biện pháp cô lập nạn nhân
Chép nguyên văn thông báo ghim của tài khoản chủ nhóm:
"@all Các bạn chú ý, đúng 17h55 nhóm mở chào đón những THÀNH VIÊN mới và bắt đầu ĐIỂM DANH lúc 18h00. Điểm danh sẽ nhận 30k."
"QUY TẮC NHÓM: 1-KHÔNG ĐĂNG ẢNH CỬA HÀNG LÊN NHÓM. 2-KHÔNG CHIA SẺ THÔNG TIN CÁ NHÂN TRÊN NHÓM. 3-NGHIÊM CẤM HOẠT ĐỘNG BUÔN BÁN VÀ TUYÊN TRUYỀN CÁC HÀNH VI VI PHẠM PHÁP LUẬT. LƯU Ý: KHÔNG GỬI ẢNH HOẠT ĐỘNG LÊN NHÓM. GỬI CHO CHUYÊN VIÊN !!"
Đọc như nội quy của một cộng đồng tử tế. Đọc lại như thiết kế của bên vận hành thì từng dòng đều có công dụng:
| Điều khoản | Công dụng thật |
|---|---|
| Cấm đăng ảnh cửa hàng | Không ai thấy được nhiệm vụ người khác đang làm, nên không ai đối chiếu được |
| Cấm chia sẻ thông tin cá nhân | Nạn nhân không trao đổi liên lạc, không lập được kênh riêng để cảnh báo nhau |
| Cấm hành vi vi phạm pháp luật | Lớp sơn tuân thủ, đặt ngay cạnh hai điều trên để cả ba trông giống nhau |
| Ảnh hoạt động gửi riêng cho chuyên viên | Mọi bằng chứng chảy vào kênh 1-1 mà chỉ bên vận hành đọc được |
Kết quả là một căn phòng nơi vài chục người cùng làm một việc mà không ai biết người bên cạnh đang trải qua điều gì. Nạn nhân thấy đám đông và tin rằng mình đang ở giữa một cộng đồng; thứ họ không bao giờ thấy là một người khác kể lại chuyện đã mất tiền.
Bộ khung nhân sự và dàn tài khoản mồi
Danh sách thành viên có một chủ nhóm Quản Lý Hệ Thống mang avatar vương miện VIP, và ba quản trị viên: Chuyên Viên - Yến Nhi cùng hai tài khoản khác nhau dùng chung một nick Chuyên Viên Ngọc Hương với hai ảnh đại diện khác nhau. Một cái tên gán cho hai tài khoản là dấu hiệu của việc dùng persona theo ca trực chứ không phải người thật.
Lúc phòng mở, hàng chục tài khoản chào nhau trong vòng một phút bằng những câu gần như giống hệt: "Chào mn nhé", "chào cả nhà", "hello mọi người". Đúng 18h00, cũng chừng ấy tài khoản đồng loạt gõ "Điểm danh", rồi tự giục nhau:
"điểm danh xong chụp gửi chuyên viên nhận thưởng nhanh mn ơi"
"@Văn Tín điểm danh chụp màn hình gửi cho chuyên viên là được nhé mn"
Không tài khoản nào trong số này giữ vai trò quản trị viên, nên với người mới vào chúng trông y hệt bạn đồng cảnh. Chúng làm hai việc: dựng bằng chứng xã hội rằng mọi người đều tham gia, và lặp lại chỉ dẫn quan trọng nhất từ miệng người ngang hàng thay vì từ ban quản trị. Một mệnh lệnh nghe như lời khuyên thì khó cưỡng hơn nhiều.
Nối lại với mục 7.1
ChuỗiyxVirtualMemberCountvà ô nhập "Số lượng thành viên ảo" trong cài đặt nhóm giờ có ngữ cảnh. Bên vận hành chỉnh được con số thành viên hiển thị mà không cần tạo tài khoản. Số người trong phòng, cũng như tên nhóm bắt đầu bằng "762", đều là tham số nhập tay.
Tôi chỉ quan sát và không gửi tin nhắn nào vào phòng. Vì vậy tôi không biết bước sau khi gửi ảnh điểm danh, và không biết khoản 30k có thực sự được trả hay không.
Nghi ngờ
Giả thuyết, tách khỏi những gì đã chứng minh
Phần này tách rõ những gì tôi suy luận khỏi những gì tôi quan sát được, để nhóm nghiên cứu tự cân nhắc.
[Suy luận] Hạ tầng dùng một lần cho một chiến dịch
Chứng chỉ ký trong ngày mẫu được phát tán, hiệu lực 7,7 tháng, CN sinh ngẫu nhiên, package ID sinh ngẫu nhiên. Không dấu hiệu nào trong số này cần thiết cho một sản phẩm sống lâu. Chúng cần thiết cho một bản build phát cho một nhóm nạn nhân rồi bỏ. Săn mẫu khác theo khuôn Subject CN=dev-<8 ký tự>, O=NextGen, OU=Research sẽ lộ họ ứng dụng cùng chiến dịch.
[Suy luận] Không có backend riêng là lựa chọn chủ động
Nhóm này viết được lớp AES riêng, dựng được console desktop, cấu hình được giới hạn IP. Họ đủ sức dựng một API server. Việc đẩy toàn bộ trạng thái lên NIM Server API mang lại ba thứ: không server nào để takedown, không domain nào để chặn, và hoá đơn hạ tầng gần bằng không. Đổi lại họ phụ thuộc hoàn toàn vào một appKey.
[Suy luận] Lớp AES nhắm vào NetEase, không nhắm vào cơ quan điều tra
Nếu mục tiêu là chống điều tra thì họ đã bọc cả ảnh trên NOS. Họ không làm. Chỉ có nội dung tin nhắn văn bản được bọc, đúng phần mà bộ lọc chống spam và kiểm duyệt tự động của NetEase quét. Thiết kế này nhằm giữ appKey không bị đình chỉ vì vi phạm nội dung.
[Bỏ ngỏ] Dạng chiến dịch cuối cùng
Phiên quan sát thứ hai đã thu hẹp câu hỏi này. Nhiệm vụ là cày tương tác cho video TikTok, trả công 18k, 10k và 30k qua ba lần. Đó là nhánh nhiệm vụ đơn hàng chứ không phải nhánh đầu tư. Nhưng cả hai nhánh đều dùng chung phần mở đầu, và chỉ tách nhau ở bước đòi nạp tiền. App không có ví, không có luồng rút tiền, nên việc chuyển tiền diễn ra ngoài app qua hướng dẫn trong chat. Xác định dạng cuối cần theo dõi thêm một phiên tới bước đó, việc tôi không làm.
[Bỏ ngỏ] WebView sau hộp thoại mã mời
Tab "Discover" mở một WebView của flutter_inappwebview, nhưng chặn lại bằng hộp thoại "Bind invitation code". Tôi không nhập mã, nên không biết nó tải trang nào. Đây là khoảng trống rõ nhất trong kết luận ở mục 10: một WebView chưa mở khoá có thể trỏ tới bất kỳ đâu, và nếu bên vận hành có hạ tầng riêng thì đây là chỗ nó lộ ra. Đóng lại khoảng trống này cần một mã mời hợp lệ cùng một phiên bắt gói riêng.
[Bỏ ngỏ] Luồng đăng ký tài khoản mới
Pcap 35 giây phủ khởi động và tự đăng nhập, không phủ đăng ký. Bản im_demo gốc gọi tới demo server của NetEase ở bước này. Nếu kẻ vận hành thay bằng server riêng, nó chỉ lộ ra ở lần đăng ký đầu tiên. Cần một pcap riêng để đóng lại khoảng trống này.
[Bỏ ngỏ] Payload nạp động
Mẫu dùng packer thương mại. Phân tích tĩnh không loại trừ được khả năng có mã nạp thêm sau khi cài. Tôi không quan sát thấy lần tải payload nào trong phiên bắt gói, nhưng 35 giây là cửa sổ hẹp.
Đính chính
Sáu nhận định tôi đã tự bác bỏ
Ghi lại để ai đọc lại quá trình không tiếp tục lan truyền chúng.
| Nhận định ban đầu | Thực tế |
|---|---|
| App có ví và luồng rút tiền (Withdraw, Balance) | Sai. Withdraw là labelShouldWithdraw của Flutter, Balance là WhiteBalance trong EXIF và AudioBalanceRight trong audio. Không có tính năng tài chính nào. |
| lbs.chatnos.com là hạ tầng của kẻ vận hành | Sai. NS trỏ về blogns{1,3,4}.netease.com, CNAME về ntes53.netease.com. Domain thay thế do NetEase vận hành, nhúng sẵn trong SDK. Không phải IOC. |
| 78da37fed6bf1489361a312568249f3f là appKey | Sai. Đó là _kDartSnapshotBuildId. |
| net.vo72xf.linker.zvan9.base là package mồi | Sai. pm list packages xác nhận đó là package ID thật khi cài. com.zentraone.chat chỉ còn trong tên lớp và tên quyền. |
| App ship kèm tài khoản đăng nhập sẵn | Sai. Tài khoản đó do tôi tạo. Tôi đã force-stop rồi mở lại, nên app tự đăng nhập bằng credential đã lưu. |
| Dữ liệu định tuyến qua Trung Quốc | Cần chỉnh. Tenant NIM đặt ở Singapore, LBS trả về region SG và desc "SG online". Push và crash report thì vẫn qua hạ tầng Trung Quốc. |
IOC
Chỉ dấu để săn mẫu cùng chiến dịch
File và chứng chỉ
sha256 e0769a21eb15eb4c153f7eb49a3d838af03ebb26f51cb56673cf52d62abdf7b3
cert_sha256 b2ae6672746ea747698a1939adb72a63b4e7e133e0926165d3fb87d8c2b630c3
cert_sha1 55437e4b6e7e34fe83d61f110737fd2834af26e7
cert_subject CN=dev-RocLHb2w, OU=Research, O=NextGen, L=Nanjing, ST=Guangdong, C=CN
cert_serial 1114521921369073213
Định danh ứng dụng
app_id com.zentraone.chat
package_installed net.vo72xf.linker.zvan9.base
flutter_module zentraoneandroid
nim_appkey f4acf0707e9c02c80e68bfb2ae4123c6
Quyền tuỳ biến, dùng làm chữ ký phát hiện
com.zentraone.chat.permission.MIPUSH_RECEIVE
com.zentraone.chat.push.permission.MESSAGE
com.zentraone.chat.permission.C2D_MESSAGE
com.zentraone.chat.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION
Ảnh giấy phép đóng gói sẵn, chỉ dấu bền nhất
64284f711aaac7d25544df59c5fa134e27adf46266d25f0e20a50a9f282a3724 license/license.jpeg
c3b2cceb3edaa666e4c42a883689fd284ed53212bb00971bdd896efef51e7392 license/dy.jpeg
bde26821dcb8bb138ca30be0fc2ad440d7ee5e5690d16e6008baf1f1100c765a license/tb.jpeg
c1f7ac41579b1c5a5352e65bc1a18971ccbc530363cedf56c7676b0274e6376b license/tm.jpeg
Tài khoản vận hành trên NIM
zentraone tài khoản hệ thống tự động, có tick xanh trong giao diện
khanhly2 nick "Chuyên Viên - Khánh Ly", avatar hotlink từ fbcdn
nick "Quản Lý Hệ Thống", chủ phòng nhóm, avatar vương miện VIP
nick "Chuyên Viên - Yến Nhi", quản trị viên
nick "Chuyên Viên Ngọc Hương", quản trị viên, 2 tài khoản chung 1 nick
Phòng nhóm quan sát được
tên nhóm "762 SẢN PHẨM THƯƠNG MẠI"
lịch chạy 16h20 thông báo / 17h55 mở phòng / 18h00 điểm danh
mọi câu "điểm danh xong chụp gửi chuyên viên nhận thưởng"
Thông tin xác thực lộ trong manifest
vivo_push_api_key 496dab64d277a004ac9f5d8b0f53f951
vivo_push_app_id 105579057
Dấu vết packer và build dev sót lại
assets/.signature/dex.sig
assets/dexopt/baseline.prof
http://10.0.2.2:20005 <- loopback emulator, còn trong classes*.dex
Không phải IOC
Các domain sau thuộc hạ tầng hợp pháp của NetEase Yunxin và nằm sẵn trong SDK. Chặn chúng sẽ ảnh hưởng mọi ứng dụng dùng NIM, không riêng mẫu này.
Phòng thủ
Xử lý thiết bị và đòn bẩy lớn nhất
Nếu app đã cài trên một thiết bị
Thứ tự quan trọng. Thu hồi quyền trước khi gỡ, để giảm thứ app kịp đọc khi nhận tín hiệu bị gỡ.
- Bật chế độ máy bay.
- Thu hồi toàn bộ quyền. Kiểm tra riêng mục "Quyền truy cập tất cả tệp", vì
MANAGE_EXTERNAL_STORAGEnằm ở màn hình khác với danh sách quyền thông thường. - Gỡ cài đặt.
- Đổi mật khẩu mọi tài khoản dùng chung mật khẩu với app. Mật khẩu app lưu ở dạng rõ.
- Xem lại ảnh và tài liệu trong bộ nhớ ngoài. Với targetSdk 24 và
MANAGE_EXTERNAL_STORAGE, app đọc được ảnh chụp màn hình chứa OTP, ảnh giấy tờ tuỳ thân, tài liệu tải về. - Nếu thiết bị từng dùng cho ngân hàng hoặc công việc, cân nhắc factory reset. Packer thương mại khiến phân tích tĩnh không loại trừ được payload nạp động.
Nếu đã cung cấp dữ liệu qua app
Giả định mọi thứ đã nhập đều nằm trong tay bên vận hành: tên thật, số điện thoại, địa chỉ, ảnh giấy tờ gửi qua chat, toàn bộ lịch sử tin nhắn, vị trí GPS và danh bạ nếu đã cấp quyền. Ưu tiên khoá các tài khoản dùng chung số điện thoại đó, bật xác thực hai lớp không dùng SMS, và cảnh báo người thân về khả năng bị mạo danh.
Đòn bẩy lớn nhất
Điểm nghẽn
Toàn bộ hoạt động của app phụ thuộc vào một appKey NIM duy nhất: f4acf0707e9c02c80e68bfb2ae4123c6. Không có backend dự phòng, không có domain riêng, không có đường lui. Báo cáo abuse tới NetEase Yunxin để đình chỉ appKey làm app chết hoàn toàn.
Lưu ý khi báo cáo: vì lớp AES của kẻ vận hành, NetEase không tự đọc được nội dung vi phạm trong log của họ. Hồ sơ báo cáo cần kèm ảnh chụp màn hình từ phía client, nơi nội dung đã được giải mã.
Các đầu mối khác nếu app đang phát tán tới người dùng Việt Nam: VNCERT/CC, và gửi mẫu qua VirusTotal để Google Play Protect nhận diện.
Phương pháp
Môi trường, ghi chú và giới hạn
Môi trường
AVD Android 13, google_apis, x86_64, đã root
CA mitmproxy CA nạp vào /system/etc/security/cacerts/
Instrumentation frida-server 17.16.4
Packet capture adb emu network capture (cấp vNIC)
Mạng thiết bị chạy sau VPN
Quan sát 2 phiên, cách nhau ~7 giờ, trên cùng 1 tài khoản thử nghiệm
Ảnh màn hình adb shell screencap, không tương tác ngoài thao tác điều hướng
Ghi chú cho ai lặp lại
- Hook Frida ở tầng
libssl.sokhông lấy được traffic Dart. Flutter nhúng BoringSSL riêng tronglibflutter.sođã strip, không exportSSL_writehaySSL_read. - Đọc SNI từ TLS ClientHello bằng cách hook
libc!write/send/sendmsg/writevđúng về nguyên tắc, nhưng phiên Frida của tôi không giữ được kết nối ổn định. adb emu network capturecho kết quả tốt hơn: bắt ở cấp vNIC, không phụ thuộc Frida, không phụ thuộc certificate pinning. Nhớ tắt Wi-Fi để ép traffic quaeth0.- Frida 17 bỏ các API tĩnh. Dùng bản mới thay cho bản cũ:
// Frida 16 - không còn dùng được
const addr = Module.findExportByName("libc.so", "connect");
const val = Memory.readU16(ptr);
// Frida 17
const addr = Process.findModuleByName("libc.so").findExportByName("connect");
const val = ptr.readU16();
- Trên Windows, Git Bash biến
/data/local/tmp/xthànhC:/Program Files/Git/data/local/tmp/x. Chạy adb qua PowerShell thay vì Git Bash.
Giới hạn
- Pcap kéo dài 35 giây, phủ khởi động và tự đăng nhập. Không phủ luồng đăng ký tài khoản mới.
- Tab "Discover" nạp một WebView bị chặn sau hộp thoại mã mời. Tôi không nhập mã, nên không quan sát được nó tải trang nào.
- Khoá AES chưa lấy được. Nội dung tin nhắn trong
msg.dbvẫn ở dạng ciphertext. Bản chép trong bài lấy từ ảnh chụp màn hình, tức sau khi app tự giải mã. - Phân tích dừng ở giai đoạn thu thập dữ liệu định danh và cày tương tác. Bước nạp tiền nằm ngoài phạm vi quan sát.
- Trong phòng nhóm tôi chỉ đọc, không gửi tin nhắn nào. Không xác định được tài khoản nào là mồi, tài khoản nào là nạn nhân thật.
- Dữ liệu cá nhân trong bài đã che: địa chỉ nhà, số điện thoại, tên tài khoản thử nghiệm, mật khẩu và IP đăng nhập. Trong ảnh chụp màn hình, avatar của mọi thành viên đều làm mờ; nick của nhân sự vận hành để nguyên vì đó là persona chứ không phải người thật.
- Ảnh trong bài có hai loại. Ảnh tang vật gồm ảnh trích thẳng từ APK và ảnh chụp emulator, giữ nguyên màu. Ảnh minh hoạ đã chuyển sang đen trắng và ghi rõ trong chú thích.

Bình