QQNT 9.9.26-44343 (gültig am 20260217) Komplettanleitung zum Extrahieren des Datenbank-Key (inkl. ausführbarem Skript)

Anwendungsszenario: QQNT 9.9.26-44343 unter Windows; es muss der SQLCipher-Datenbank-Key extrahiert werden (für die anschließende Entschlüsselung von Datenbanken wie nt_msg.db).

0. Risikohinweis

  • Es besteht ein gewisses Risiko für Account-Risikokontrollen; es wird empfohlen, zuerst den Chatverlauf zu sichern.
  • Nur für den Export persönlicher Daten und zu Lernzwecken; bitte Gesetze/Verordnungen sowie Plattformvereinbarungen einhalten.

1. Umgebung vorbereiten

  • PowerShell als Administrator
  • QQNT installiert: 9.9.26-44343
  • Python 3.10+ (getestet: 3.13 funktioniert)

Abhängigkeiten installieren:

py -m pip install frida psutil

2. Skript speichern (versionsspezifisch)

Neue Datei anlegen: qqnt_get_key_9926.py, Inhalt wie folgt:

import time
import frida
import psutil

# Nur passend für QQNT 9.9.26-44343 (Offset innerhalb wrapper.node)
TARGET_RVA = 0x021A5BF0
QQ_EXE = r"C:\\Program Files\\Tencent\\QQNT\\QQ.exe"


def try_attachable_main_pid():
    for p in psutil.process_iter(['pid', 'name', 'cmdline']):
        if (p.info['name'] or '').lower() != 'qq.exe':
            continue
        cmd = p.info['cmdline'] or []
        if len(cmd) != 1:
            continue
        pid = p.info['pid']
        try:
            s = frida.attach(pid)
            s.detach()
            return pid
        except Exception:
            pass
    return None


def build_script():
    return f"""
function bytesToHex(ab) {{
  if (!ab) return '';
  const u8 = new Uint8Array(ab);
  let s = '';
  for (let i = 0; i < u8.length; i++) s += u8[i].toString(16).padStart(2, '0');
  return s;
}}

(function waitAndHook() {{
  const moduleName = 'wrapper.node';
  const targetRva = ptr('0x{TARGET_RVA:x}');
  let hooked = false;

  function doHook() {{
    if (hooked) return;
    let m = null;
    try {{
      m = Process.getModuleByName(moduleName);
    }} catch (e) {{
      return;
    }}
    if (!m) return;

    hooked = true;
    const target = m.base.add(targetRva);
    console.log('[hook] base=' + m.base + ' target=' + target);

    Interceptor.attach(target, {{
      onEnter(args) {{
        try {{
          // x64 Windows: rcx, rdx, r8, r9
          const dbNamePtr = args[1];
          const keyPtr = args[2];
          const nKey = args[3].toInt32();
          const dbName = dbNamePtr.isNull() ? '' : (dbNamePtr.readUtf8String() || '');
          const keyHex = (keyPtr.isNull() || nKey <= 0) ? '' : bytesToHex(keyPtr.readByteArray(nKey));

          console.log('[dbName] ' + dbName);
          console.log('[nKey] ' + nKey);
          console.log('[key] ' + keyHex);
        }} catch (e) {{
          console.log('[hook-error] ' + e);
        }}
      }}
    }});
  }}

  doHook();
  const timer = setInterval(() => {{
    if (hooked) {{
      clearInterval(timer);
      return;
    }}
    doHook();
  }}, 200);
}})();
"""


def run_attach_mode(pid):
    sess = frida.attach(pid)
    script = sess.create_script(build_script())

    def on_message(msg, data):
        if msg.get('type') == 'send':
            print(msg.get('payload'))
        elif msg.get('type') == 'error':
            print(msg)

    script.on('message', on_message)
    script.load()
    print(f'[info] attached pid={pid}, waiting...')
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        pass
    sess.detach()


def run_spawn_mode():
    dev = frida.get_local_device()
    pid = dev.spawn([QQ_EXE])
    sess = dev.attach(pid)
    script = sess.create_script(build_script())

    def on_message(msg, data):
        if msg.get('type') == 'send':
            print(msg.get('payload'))
        elif msg.get('type') == 'error':
            print(msg)

    script.on('message', on_message)
    script.load()
    print(f'[info] spawned pid={pid}, resuming...')
    dev.resume(pid)

    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        pass

    sess.detach()


if __name__ == '__main__':
    pid = try_attachable_main_pid()
    if pid:
        run_attach_mode(pid)
    else:
        run_spawn_mode()

3. Ausführen

Sicherstellen, dass QQ bereits eingeloggt ist (oder das Skript QQ automatisch starten lassen):

py .\\qqnt_get_key_9926.py

Wenn eine ähnliche Ausgabe erscheint, war es erfolgreich:

[dbName] main
[nKey] 16
[key] 5a6b457d79777b2e6150376e582d6572

oder:

[dbName] main
[nKey] 20
[key] 4244313536443637313044353444383738324634

4. Wie der Key verwendet wird

  • Beide Längen können auftreten; nimm diejenige Gruppe, die zu deiner Zieldatenbank passt.
  • key ist ein Hex-String; manchmal ist er im Kern die Hex-Darstellung von ASCII-Text.

PowerShell-Beispiel für Hex → ASCII:

[Text.Encoding]::ASCII.GetString([Convert]::FromHexString('4244313536443637313044353444383738324634'))

5. Häufige Datenbankpfade

C:\\Users\\你的用户名\\Documents\\Tencent Files\\你的QQ号\\nt_qq\\nt_db\\nt_msg.db

Wenn du nur dbName=main siehst: Das ist ein Alias für die SQLite-Verbindung und bedeutet nicht, dass der Datenbankdateiname falsch ist.

6. Versionskompatibilität

TARGET_RVA = 0x021A5BF0 in dieser Anleitung gilt nur für QQNT 9.9.26-44343. Falls es in späteren Versionen nicht mehr funktioniert, muss der Offset neu ermittelt werden.

Ergänzend ein Fallstrick, in den ich selbst getappt bin, damit ihr nicht immer wieder durch Trial-and-Error müsst:

  1. pcqq_get_key.py ist alte PCQQ-Logik – das direkt auf QQNT anzuwenden geht meist schief (Module und Call-Chain sind anders).
  2. QQNT ist multiprozessig; wenn man einfach irgendein QQ.exe attach’t, bekommt man leicht ProcessNotRespondingError. Bevorzugt den Hauptprozess (häufig der, dessen cmdline nur 1 Parameter hat).
  3. Wenn man hook’t, bevor wrapper.node geladen ist, kommt unable to find module 'wrapper.node'. Lösung: Polling, bis das Modul geladen ist, und dann erst hooken – oder per spawn möglichst früh injizieren.
  4. dbName=main ist kein Fehler; das ist ein SQLite-Connection-Alias, nicht der Name der Datenbankdatei.

In einem Satz: Version, Prozess und Zeitpunkt müssen alle drei gleichzeitig passen, sonst bekommt man den Key nicht stabil.


Huateng, du solltest mich jetzt schnell anflehen, nicht zu gehen, hehe