QQNT 9.9.26-44343(20260217有効)データベース Key 抽出の完全チュートリアル(実行可能スクリプト付き)

適用シーン:Windows 上の QQNT 9.9.26-44343 で、SQLCipher データベースの key を抽出する(後続で nt_msg.db などの DB を復号するため)。

0. リスク説明

  • 一定のアカウントリスク管理(風控)リスクがあります。先にチャット履歴のバックアップを推奨します。
  • 個人データのエクスポートと学習用途に限ります。法令・規約およびプラットフォームの利用規約を遵守してください。

1. 環境準備

  • 管理者 PowerShell
  • QQNT インストール済み:9.9.26-44343
  • Python 3.10+(検証では 3.13 で使用可)

依存関係のインストール:

py -m pip install frida psutil

2. スクリプトを保存(バージョン専用)

新規ファイル:qqnt_get_key_9926.py を作成し、内容は以下:

import time
import frida
import psutil

# QQNT 9.9.26-44343 のみに対応(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. 実行

QQ にログイン済みであることを確認(またはスクリプトに QQ を自動起動させる):

py .\qqnt_get_key_9926.py

次のような出力が出れば成功:

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

または:

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

4. key の使い方

  • 2 種類の長さが出る可能性があります。対象 DB に対応する方を使用してください。
  • key は 16 進文字列です。場合によっては、本質的に ASCII テキストを 16 進で表現したものです。

PowerShell で ASCII に変換する例:

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

5. DB の一般的なパス

C:\Users\あなたのユーザー名\Documents\Tencent Files\あなたのQQ号\nt_qq\nt_db\nt_msg.db

dbName=main しか見えない場合、これは SQLite の接続エイリアスであり、DB ファイル名が誤っていることを意味しません。

6. バージョン互換性について

このチュートリアルの TARGET_RVA = 0x021A5BF0 は QQNT 9.9.26-44343 にのみ対応します。以降のバージョンで動かなくなった場合は、オフセットを再特定する必要があります。

実際に自分が踏んだ落とし穴を補足しておきます。みんなが何度も試行錯誤しなくて済むように:

  1. pcqq_get_key.py は旧PCQQのロジックで、これをそのままQQNTに当てると基本的に事故ります(モジュールと呼び出しチェーンが違う)。
  2. QQNTはマルチプロセスなので、適当に QQ.exe にattachすると ProcessNotRespondingError が出やすい。優先すべきはメインプロセス(よくあるのは cmdline の引数が1個だけのやつ)。
  3. wrapper.node がまだロードされていない段階でhookすると、unable to find module 'wrapper.node' が出る。対処は、モジュールがロードされたのをポーリングしてからフックするか、spawn方式で早めに注入する。
  4. dbName=main は異常ではなく、SQLiteの接続エイリアスであって、データベースファイル名ではない。

一言でまとめると:バージョン・プロセス・タイミングの3条件が全部噛み合って初めて、安定してkeyを取れる。


化騰 お前、今すぐ俺に「行かないで」って頼めよ へへ