No Result
View All Result
HungTNT.Dev Blog
  • Trang chủ
  • Tech
    • Tin công nghệ
    • Linux
    • Windows
    • Network
  • Life
  • Photography
  • Trang chủ
  • Tech
    • Tin công nghệ
    • Linux
    • Windows
    • Network
  • Life
  • Photography
No Result
View All Result
HungTNT.Dev Blog
No Result
View All Result

Hành trình giải mã Zalo – Phần 2: Database

Hưng Trần by Hưng Trần
23/08/2026
6
VIEWS
ShareShareShare

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

Tiếp theo phần 1, mình đã giải mã thành công file backup của Zalo và có thể đọc được toàn bộ nội dung tin nhắn. Tuy nhiên, mình vẫn gặp một vài vấn đề:

– File backup của Zalo, phần tin nhắn không trực tiếp lưu tên của người gởi mà chỉ lưu UID, có lẽ Zalo sẽ tự mapping khi thực hiện restore. Tuy nhiên, khi chỉ có file backup trong tay thì rất khó để xác định được UID trong message tương ứng với tài khoản nào.

– Mỗi lần cần cập nhật dữ liệu chat để AI đọc thì khá mất thời gian vì phải cho xuất file, xong mới đem giải mã để thu được text, và vẫn không giải được bài toán mapping.

Từ những vấn đề trên, mình nghĩ có thể làm theo cách là trực tiếp giải mã database của Zalo lưu trên máy, lúc đó có thể viết kịch bản để chạy tự động, và chỉ cần cập nhật những file database có thay đổi kể từ lần chạy trước.

Thú vị là, trong quá trình này, mình cũng đã tìm được cách để tự động mapping UID với tên người chat luôn, một công đôi ba việc. Bắt đầu thôi.

 

Chương 1: Những file Sqlite mã hóa

Zalo lưu lịch sử chat và media ngay trên thiết bị của người dùng chứ không lưu trên máy chủ. Việc đó đã thay đổi kể từ khi họ ra mắt zCloud , media sẽ được lưu trên máy chủ Zalo (nếu bạn có mua gói zCloud), nhưng toàn bộ lịch sử chat (phần text) vẫn nằm tại chính thiết bị của người dùng.

Mò mẫm thì cũng biết được Zalo lưu data ở %AppData%\ZaloData\Database

Nhìn cấu trúc thư mục 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.

Kèm theo đó, khi bung asar của Zalo, mình để ý thấy họ đã kèm theo sẵn một file libcrypto-1_1.dll, như vậy Zalo đã sử dụng SQLCipher cho việc mã hóa và giải mã database mà không tự triển khai giải thuật riêng (may quá).

Nếu đã là SQLCipher, vậy Zalo đã sử dụng theo cách nào? Zalo đã dùng cách kèm theo khóa giải mã database (PRAGMA) mỗi khi cần đọc 1 file .db, cụ thể là

const c = async (e, t = null, n = READWRITE) => {
  const s = new Database(e, OPEN_READWRITE | OPEN_CREATE | OPEN_WAL, cb);
  const d = [journal_mode=WAL, synchronous=NORMAL, ...];
  if (t) d.unshift(`PRAGMA key="${t}"`);
  await exec(d.join(";"));
  return s;
}

Vậy các file database khác nhau có được dùng key khác nhau không hay cùng 1 key? Mã nguồn cũng đã cho mình câu trả lời: chỉ có 1 key duy nhất cho toàn bộ các file db trong ZaloData

const { userKey, sharedKey } = this.remoteConfig.cipher;
const key = sessional ? userKey : sharedKey;

/////////////////////////////////////////////////////

getPartition(table, session) {
  let userId, cipherKey;
  if (this.session) {
    userId = session.userId;
    cipherKey = await getUserScopeKey();
  } else {
    userId = "";
    cipherKey = await getSharedKey();
  }
  ...
}

getUserScopeKey() { return (await this.authWaiter.promise).UIN; }

Một lần nữa, uin lại xuất hiện. Như trong phần 1, uin chính là chìa khóa để giải mã file backup của Zalo. Còn bây giờ, nó cũng chính là chìa khóa để mở tất cả các file .db trên thiết bị.

Lúc này, mình đã có thể dùng uin đã có từ phần 1 để mở hết các file.db. Nhưng bản tính tò mò trỗi dậy, mình muốn tìm đáp án cho câu hỏi: uin rốt cuộc là cái gì, và nó sinh ra từ đâu?

 

Chương 2: Chìa khóa đăng nhập

Nhìn cái tên UIN, mình cũng lờ mờ đoán ra được đây có lẽ là User Identification Number, nhưng với Zalo, họ đã có UID, và nó là một dãy số riêng đã có sẳn tính Unique, trong khi đó UIN lại là một dãy khác hoàn toàn, vậy ai sinh ra UIN, UIN để làm gì?

Truy ngược lên code, UIN được gán từ một trường tên là dkey, và trường dkey này được server Zalo trả về khi người dùng  thực hiện login

getLoginInfo(...).then(e => {
  ...
  resolve({
    config: e.data.data,
    UIN: e.data.data.dkey,
    decryptKey: e.data.data.zpw_enk,
    ...
  });
});

Kết luận: máy chủ Zalo đang lưu trữ khóa giải mã của từng người dùng và khi họ đăng nhập thành công, Zalo sẽ chủ động gởi khóa này cho ứng dụng, ứng dụng sẽ lấy nó để làm UIN phục vụ cho tất cả các thao tác cần khóa mã hóa. Có thể việc dùng UIN thay cho UID là để User khó đoán ra khóa giải mã hơn chăng?

Lúc này, mình nghĩ có thể đào sâu thêm để có thể chặn lấy dkey ngay từ khi đăng nhập. Tuy nhiên vì đã có UIN tìm được từ trước nên mình tạm dừng việc tìm hiểu ở đây, và tập trung vào mục đích chính: Đọc và xử lý các file local db

Nếu sau có thời gian, có thể mình sẽ làm tiếp Phần 3: giải mã luồng dữ liệu client-server của Zalo 😀

 

Chương 3: XOR

Tìm được chìa khóa rồi, mình thử mở ngay 1 file database để xem nội dung. May mắn là Zalo đã sử dụng kiến trúc SQLCipher 4 mặc định ( 256000 KDF interactions, page size 4096 byte, HMAC-KDF SHA512) nên không mất nhiều công sức dò dẫm.

Mỗi file này chỉ chứa duy nhất 1 bảng, với đầy đủ thông tin giống như cấu trúc của file zmessge.db đã giải mã được từ phần 1. Nhưng từ đây, một vấn đề nữa lộ diện.

File database này có 76 cột, và các cột có thể đọc được ngay, trừ một cột quan trọng nhất chính là message chứa nội dung tin nhắn thì lại đang bị mã hóa. Cũng có nghĩa là, Zalo đã thực hiện mã hóa nội dung tin nhắn trước khi ghi nó vào database, sau đó Zalo lại mã hóa chính database một lần nữa. Nội dung tin nhắn được mã hóa (ít nhất) 2 lần.

Lần này, nhìn bằng mắt thường, mình cũng thấy ngay một điều khá thú vị: Tất cả các dòng đều có chuỗi đầu tiên giống nhau (trừ những chuỗi dạng blob nhị phân). Kết hợp với kết quả thu được từ việc xuất file backup, mình đoán đây có thể là dấu nháy kép (“) vì message này có định dạng là json. Vậy suy ra được là cột này được áp dụng một quy tắc mã hóa biến đổi ký tự, mà mỗi ký tự đi vào sẽ có đầu ra cố định, vậy thì chỉ cần xác định xem ký tự nào sẽ biến đổi ra cái gì là sẽ giải mã được message?

Lý thuyết thì là vậy, nhưng mình có mã nguồn trong tay mà, sao phải đoán? Mình tiếp tục dò trong mã nguồn để xác định cơ chế mã hóa message trước khi vào DB. Và không lâu sau đó thì mọi thứ hiện nguyên hình 😀

revealSync(key, data) {
  let n = 0, out = [];
  for (let i = 0; i < data.length; i++) {
    out.push(String.fromCharCode(data.charCodeAt(i) ^ key.charCodeAt(n)));
    n++;
    if (n > key.length - 1) n = 0;
  }
  return out.join("");
}

Vậy là, Zalo đã thực hiện mã hóa tin nhắn trước khi ghi vào DB bằng cách XOR nó với chính UIN (lại là UIN), sau đó mới ghi nó vào cột message trong database và lại khóa database đó lại bằng chính UIN một lần nữa. Chưa rõ vì sao Zalo lại chọn cách khóa hai lần với cùng một chìa khóa như thế này, có lẽ họ muốn làm khó những người muốn vọc vạch, chỉ khổ đội dev Zalo :))

Bên cạnh đó, từ mã nguồn, mình thấy Zalo có một hàm chọn thuật toán mã hóa với 1 bên là XOR, và một bên là AES-256. Hiện tại, hàm ghi vào database đang gọi method Basic (XOR). Rất có thể trong tương lai gần, Zalo sẽ chuyển sang mã hóa message bằng AES-256. Đến lúc đấy chắc mình lại lục trong mã nguồn và giải mã tiếp vậy :))

function j(e) {
  switch (e) {
    case I.a.BasicEncryption:  return x.create();  // ← XOR
    case I.a.AESCBCEncryption: return R.create();  // ← AES-CBC
    default: throw new Error("Encryption handler not found!");
  }
}
u.setAdapterSpecificConfigs(i.a.SQLite, { transforms: new d.a({ version: {
  0: { sentiveFields: ["message"], methodology: p.a.BasicEncryption, migrate: false }
}})});

Gởi tới bác dev Zalo nào nếu đang đọc cái này: Trong code viết sai chính tả nhé bác: secretKey chứ secrectKey là cái mòe gì :))

class M {
  constructor() { this.secrectKey = ""; }
  init(e)   { this.secrectKey = e; }
  reveal(e, t) { return j(e).reveal(this.secrectKey, t); }
}

 

Từ những kết quả trên, mình đã xây dựng được một script python để thực hiện việc đọc tất cả các file database của Zalo, giải mã cột message và xuất ra thành json hoàn toàn tự động mà không cần thao tác export data như trước.

Lúc này, bài toán cuối cùng xuất hiện.

 

Chương 4: Ai là người gởi?

Như các bạn cũng có thể thấy, file json xuất ra lúc này tuy đã có message, nhưng lại không biết người gởi là ai. Vì trường fromUid và toUid hoàn toàn chỉ là các dãy số, không thể mapping được với một cái tên nào.

Mình nghĩ Zalo sẽ thực hiện mapping UID này với tên người dùng khi load nội dung từ máy chủ về, nhưng theo mình thì nếu làm vậy, máy chủ Zalo sẽ phải gánh một lượng tải khổng lồ từ hàng triệu User chỉ để yêu cầu lấy tên người dùng từ UID, như vậy thì không tối ưu, mà Zalo thì không thể không biết điều đó. Chắc chắn họ phải có một bản mapping ở đâu đó trong thiết bị của người dùng. Từ cách nghĩ này, mình đã tìm kiếm và tìm được một file có tên là Index.db cũng nằm trong ZaloData.

File này cũng được mã hóa, tuy nhiên vẫn dùng chung khóa là UIN, vì vậy mình không gặp khó khăn gì khi mở ra. Nhìn vào danh sách các bảng trong file, có 2 bảng rất hứa hẹn đó là bảng friend và group, nơi chứa các trường userID và kèm theo cả DisplayName

Kiểm tra qua data, đúng thứ mình cần đây rồi!

Bây giờ công việc còn lại chỉ là map userID từ trong các file DB xuất ra vào DisplayName tương ứng với userID trong hai bảng này, chúng ta sẽ có ngay được một cuộc trò chuyện hoàn chỉnh và sẵn sàng để tiến hành bước tiếp theo: cho LLM học lịch sử chat và tạo database phục vụ RAG cho việc tổng hợp data sau này.

Hành trình giải mã Zalo đến đây là kết thúc. Mình cũng đã mất khá nhiều thời gian để tìm hiểu và xử lý hết những cục xương do đội dev Zalo quăng cho :)) Trong tương lai, hi vọng Zalo sẽ triển khai E2EE để đảm bảo bảo mật hơn nữa, hiện tại Zalo vẫn là người nắm chìa khóa để giải mã toàn bộ dữ liệu, người sử dụng chỉ có một lựa chọn là tin tưởng ứng dụng này mà thôi.

Previous Post

Hành trình giải mã Zalo – Phần 1: Backup

Bài liên quan

Hành trình giải mã Zalo – Phần 1: Backup
Tech

Hành trình giải mã Zalo – Phần 1: Backup

23/08/2026
Protected: Hướng dẫn giải mã Zalo Backup
Tech

Protected: Hướng dẫn giải mã Zalo Backup

23/08/2026
Hướng dẫn cài đặt Docker và Docker Compose trên Linux
Linux

Hướng dẫn cài đặt Docker và Docker Compose trên Linux

04/06/2022
Linux

Mở nhiều port SSH cùng lúc trên Linux

09/11/2021
Ví dụ về một Job Description tồi ngành IT
Life

Ví dụ về một Job Description tồi ngành IT

18/08/2026
Lỗi SSL trên Cyberpanel từ tháng 6/2021
Linux

Lỗi SSL trên Cyberpanel từ tháng 6/2021

25/06/2021
HungTNT

© HungTNT - Tập làm văn

Chuyên mục

  • Tech
  • Photography
  • Life

Follow

No Result
View All Result
  • Frontpage
  • Life
  • Linux
  • Network
  • News
  • Photography
  • Tech
  • Windows

© HungTNT - Tập làm văn