Disclaimer 01: Bài viết không cung cấp hướng dẫn trực tiếp hay công cụ để thực hiện lại hành động để đảm bảo tuân thủ các điều khoản sử dụng của Zalo. Đây chỉ là một dự án nghiên cứu cá nhân không nhằm mục đích thương mại.
Disclaimer 02: Nếu có anh/chị/em nào ở Zalo cảm thấy bài viết này không nên được công khai, xin ping mình ở [email protected]
Đề bài
Mình có một người bạn làm kinh doanh, hàng ngày chat với rất nhiều khách hàng, trong đó hỏi thăm có, giới thiệu sản phẩm cũng có, mà chốt đơn cũng có. Ti tỉ thứ nằm cùng một cái ứng dụng Zalo. Bạn nào dùng Zalo chắc cũng hiểu, tính năng tìm kiếm yêu cầu phải chính xác từ khóa, mà làm sao có thể tìm nổi với một kho dữ liệu qua năm tháng như vậy. Bạn nhờ tới mình.
Ngay lập tức, mình nghĩ đến việc lấy toàn bộ lịch sử chat của Zalo ra, sau đó sẽ đưa tất cả vào một database, và cho một mô hình LLM học cục database này, sau đó bạn mình chỉ cần hỏi những câu ví dụ như: “Năm 2026 tôi đã chốt bao nhiêu đơn hàng với anh A?” hay “Tổng doanh thu trên tất cả đơn hàng của sản phẩm B là bao nhiêu?” thì AI sẽ ngay lập tức trả lời và cung cấp reference để dễ dàng tổng hợp.
Nghĩ là làm, mình bắt tay vào việc tìm cách lấy lịch sử chat của Zalo. Cuộc vui bắt đầu.
Ải đầu tiên, Zalo Local DB
Chắc hẳn bạn cũng biết, trước đây Zalo lưu lịch sử chat và media ngay trên thiết bị của người dùng chứ không hề lưu trên thiết bị của người dùng. Nhưng việc đó đã thay đổi kể từ khi họ ra mắt zCloud. Lúc này, các file media được lưu trên server của Zalo vì vậy nếu chỉ lấy từ máy người dùng sẽ không đủ nữa.
Thôi kệ nó, lấy được nội dung text chat đã.
Mò mẫm một thời gian thì cũng biết được Zalo lưu data ở %AppData%\Roaming\ZaloData\Database
Sau khi xem qua thì mình thấy là mỗi cuộc trò chuyện sẽ được lưu thành 1 file db riêng đã được mã hóa, file nào có tiền tố g thì là group chat, còn tên file chính là UserID của người bạn đang chat. OK, tìm cách decrypt thôi.
Ải thứ hai, mã nguồn rối
Muốn decrypt thì phải biết cách nó encrypt, mình nghĩ ngay đến việc mò vào mã nguồn của Zalo để xem. Bản chất Zalo là một ứng dụng Electron nên khá đơn giản để tìm hiểu. Mình đã tải file cài đặt của Zalo về, giải nén file exe để lấy file asar và sau đó tiếp tục bung file asar để lấy phần mã xử lý bên trong, và dĩ nhiên, nó đã bị obfuscated gần như không đọc được nếu chỉ dùng sức người bình thường.
Bằng sự giúp đỡ của AI, mình cũng đã dịch ngược lại được cục mã nguồn này và hiểu cơ chế của Zalo khi lưu nội dung chat.
Ải thứ ba, lại là Zalo Local DB
Sau khi hiểu được cơ chế mã hóa, mình thấy việc giải mã cục local DB này khó hơn cả đi hái sao. Vì sao?
Zalo KHÔNG dùng SQLCipher (mã hóa toàn bộ database). Thay vào đó, Zalo dùng mã hóa ở tầng ứng dụng (field-level encryption) – chỉ mã hóa các trường dữ liệu nhạy cảm trước khi ghi vào DB.
Để thực hiện mã hóa, Zalo sử dụng 2 key, DEK (Data Encryption Key) và KEK (Key Encryption Key). Trong đó, DEK được lưu trữ trên máy ở dạng đã mã hóa và cần phải dùng KEK để giải mã. Vậy KEK từ đâu ra?
KEK được Zalo sử dụng DPAPI là một cơ chế mã hóa có sẵn trong Windows, chính xác hơn thì Zalo gọi API safeStorage của Electron và truyền vào DEK, DPAPI sẽ trả về một cipher. Zalo lưu cipher này vào file Local State nằm trong folder ZaloData.
Vậy phải chăng có được cipher này thì ta có thể gọi DPAPI để giải mã ngược lại? Rõ ràng chúng ta dùng cùng user với Zalo mà, chỉ việc gọi DPAPI với phương thức CryptUnprotectData là có ngay DEK? Nhưng thực tế là vì Zalo sử dụng API safeStorage, mà safeStorage ngoài việc gởi DEK tới DPAPI thì nó sẽ tự encrypt cái key đó một lần nữa, vì vậy thực tế là Local State trên máy mà bạn đọc được là DEK đã bị mã hóa 2 lần bởi DPAPI và Electron.
Tới đây thì mình quá mệt mỏi để tìm cách lấy DEK gốc bằng cách decrypt từ Local State, và mình cũng không chắc hướng đi này của mình là đúng, mình thử một cách tiếp cận khác đó là đọc thẳng vào bộ nhớ của Zalo.
Ải thứ tư, rất nhiều lớp key
Tới đây, mình quyết định thử theo hướng bật Zalo ở Debug mode, vì bản chất là một ứng dụng Electron nên sau đó mình sẽ dùng DEv Tools của Chromium để đọc vào storage của nó. Chắc chắn Zalo sẽ phải lưu key DEK ở đâu đó trong này để thực hiện việc mã hóa / giả mã database khi người dùng chat.
Và đúng thật, khi mò vào Storage, mình thấy ngay trường dữ liệu khá hứa hẹn là 0_cl-enk
Key này là một chuỗi base64, tuy nhiên mình đã thử thoát Zalo ra và mở lại thì chuỗi này lại thay đổi, trong khi databse trò chuyện của Zalo rất lớn có thể lên đến hàng GB mỗi file, vì vậy chắc chắn là ứng dụng không thể decrypt / encrypt lại mỗi session được, do đó có thể kết luận đây vẫn chưa phải là DEK, có thể đây chỉ là một Entropy hay Salt trung gian nào đó để giải mã key gốc thôi.
Mình cũng đã thử dùng chuỗi này để làm PRAGMA và load thử một file DB lên để kiểm chứng thì đúng như dự đoán, không thể load được, như vậy Zalo đang dùng một cách nào đó bí ẩn hơn để che đi DEK thật cho các file local database.
Đến đây thì mình nghĩ mình sẽ không tiếp tục theo hướng tiếp cận này nữa, vì một vài lý do:
– Có quá nhiều file cần giải mã, và vì chưa tìm được key, mình không thể chắc chắn được là mỗi DB này có dùng cùng key với nhau hay không, nếu phải tìm tìm hàng ngàn key hoặc Entropy của mỗi key chỉ được tải về khi người dùng load Conversation thì rõ ràng việc này rất bất tiện.
– Như ở trên, nếu người dùng sử dụng zCloud thì sẽ không có các file media đang nằm trên cloud, vì vậy lịch sử dựng lại sẽ không đầy đủ kể cả có giải mã được DB đi nữa.
Ngoài ra thì từ đống local storage này mình cũng biết được một vài thông tin khá thú vị về cách Zalo hoạt động, nhưng nó sẽ không thuộc chủ đề này, và nói toạc ra thì có nguy cơ vi phạm điều khoản quá cao :))
Ải thứ năm, export
Sau khi quyết định là bỏ cuộc với việc giải mã database local, mình nghĩ đến một hướng đi khác đó là giải mã file backup.
Zalo có tính năng Xuất và Nhập dữ liệu, khi dùng tính năng Xuất dữ liệu thì chúng ta sẽ nhận được một file nén có định dạng backup_zalo_dd_mm_yyyy.zl.zip. File này tuy có phần mở rộng là .zip nhưng thực tế ra nó không phải file zip (không hề có ứng dụng nén nào đọc được file này). Rõ ràng đây là một file data đã được Zalo mã hóa và chỉ có thể dùng tính năng Nhập dữ liệu của Zalo sử dụng.
Từ đó, mình quyết định tìm cách để giải mã file backup này, và quả nhiên là nó dễ hơn giải mã trực tiếp database của Zalo :))
Đầu tiên, mình lục trong mã nguồn của Zalo và xác định được function tạo file mã hóa này có tên là achiveBackupFile nằm trong file compact-app-preload.js, dò theo code, và mình xác định được cơ chế xuất file backup của Zalo như thế này:
– Zalo sử dụng Node.js stream để thực hiện đóng gói và mã hóa đồng thời:
– Dữ liệu (các file database, media, v.v.) được đưa vào một Archive Stream đóng gói dạng tar
– Archive Stream này được truyền (pipe) thẳng qua một Cipher Stream.
– Cipher Stream này mã hóa từng byte dữ liệu đi qua nó.
– Output được ghi ra một file tạm có đuôi .ztemp đặt ngạy tại thư mục output
– Khi hoàn tất, file .ztemp được đổi tên thành backup_zalo_dd_mm_yyyy.zl.zip
let e = archiveStream; r.cipher && (e = e.pipe(r.cipher)); // Pipe qua bộ mã hóa AES r.progress && (e = e.pipe(r.progress)); e.pipe(fileWriteStream); // Ghi ra đĩa
Từ đây, mình dò theo codee và tìm được cơ chế mã hóa mà Archive stream sử dụng
– Zalo sử dụng thuật toán AES-256-CBC để mã hóa. Khóa mã hóa (Key) và Vector khởi tạo (Initialization Vector – IV) được tạo ra từ một mật khẩu nào đó mà mình chưa tìm được (cái này xử lý sau)
– Cơ chế tạo Key và IV được làm như sau:
// e = Password form nowhere
static getCipherKey(e) {
return crypto.createHash("sha256").update(e).digest();
}
static getFormattedIv(e) {
const t = "zie" + e.slice(0, 13);
return Buffer.from(t); // Return IV 16 bytes
}
Khi đã xác định được cơ chế mã hóa này rồi thì công việc còn lại đơn giản là đảo ngược nó lại, mình xử lý như sau:
– Tính Key = SHA256(password)
– Tính IV = (“zie” + password).substring(0, 16) (cắt lấy 16 byte đầu)
– Đọc file .zl.zip, dùng thuật toán AES-256-CBC với Key và IV ở trên để giải mã (Decrypt), xử lý mỗi lần 1 block nhỏ thôi cho nhẹ vì file backup nặng hàng chục GB ko load hết lên RAM nổi. Việc decrypt mình sử dụng thư viện cryptography có sẵn của Python.
– Lưu output ra một file mới dạng tar. Lúc này toàn bộ data đã ở trước mắt.
Vậy mấu chốt của vấn đề bây giờ là tìm cho được cái password.
Ải thứ sáu, password from nowhere?
Đi đến đây rồi, giá nào cũng phải tìm cho ra cái password để decrypt file backup.
Với việc đăng nhập Zalo sử dụng QR, rõ ràng password sẽ không thể sử dụng mật khẩu tài khoản người dùng để làm password được (vì người dùng không có nhập). Vả lại, dù có đi nữa thì cũng ko ứng dụng nào lại đi lưu password của người dùng ở dạng clear để dùng lại sau này cả.
Lúc này, nhìn vào thuật toán mã hóa, IV yêu cầu một password chính xác 16 ký tự, trong đó 3 ký tự “zie” là cố định, vậy mật khẩu còn lại sẽ phải là một chuỗi gì đó và phải đảm bảo có 13 ký tự trở lên. Lúc này mình nghĩ đến 2 thứ:
– UserID
– Device ID
Cả 2 thứ này đều là chuỗi số khá dài, tuy nhiên khi thử dùng nó làm password thì đều tạch. Nghĩa là Zalo đang sinh ra password bằng thuật toán riêng thay vì dùng những chuỗi text có sẵn, vậy phải tìm nó.
Vậy mình quay về cách cũ, dùng Debug mode :))
Vì đã xác định được hàm xuất file có tên là achiveBackupFile, mình tạo 1 breakpoint ngay khi Zalo gọi hàm này và kiểm tra bộ nhớ xem nó đã truyền gì vào khi gọi.
Mình đặt 1 break point ngay tại hàm này và chờ. Quả nhiên, tới bước xuất file thì Zalo đã dừng lại
Dựa trên code, mình biết được rằng hàm này nhận biến this.uin để làm password. Vậy công việc bây giờ là show cái biến này ra thôi. Hẹ hẹ. Sau đó cứ cho nó tiếp tục xuất file.
Và…
Đến đây thì file đã được giải mã thành công, phần tiếp theo là xử lý nội dung chat dạng CSV và Media, tuy nhiên nếu có hứng thì mình viết tiếp, không thì thôi :))
p/s: Thật sự là vẫn chưa xác định được mật khẩu trong uin được sinh ra như thế nào, nhưng thôi cũng giải mã được rồi, có thời gian thì nghiên cứu sau vậy.













