QQNT 9.9.26-44343 (صالح اعتبارًا من 20260217) دليل كامل لاستخراج Key قاعدة البيانات (مع سكربت قابل للتشغيل)

سيناريو الاستخدام: QQNT على Windows بالإصدار 9.9.26-44343، تحتاج إلى استخراج مفتاح قاعدة بيانات SQLCipher (لاستخدامه لاحقًا في فك تشفير قواعد بيانات مثل nt_msg.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)

  • قد يظهر طولان مختلفان؛ خذ المجموعة المطابقة لقاعدة البيانات المستهدفة لديك.
  • قيمة key هي سلسلة ست عشرية (Hex)؛ وأحيانًا تكون في الأصل نص ASCII ممثلًا بصيغة Hex.

مثال PowerShell لتحويله إلى ASCII:

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

5. مسارات قواعد البيانات الشائعة

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

إذا كنت ترى فقط dbName=main، فهذا اسم مستعار لاتصال SQLite، ولا يعني أن اسم ملف قاعدة البيانات خاطئ.

6. توضيح التوافق مع الإصدارات

القيمة TARGET_RVA = 0x021A5BF0 في هذا الدليل تخص فقط QQNT 9.9.26-44343. إذا توقفت عن العمل في الإصدارات اللاحقة، فستحتاج إلى إعادة تحديد الإزاحة.

أضيف فخًّا وقعتُ فيه فعليًا لتجنّب أن يكرّر الجميع التجربة والخطأ:

  1. pcqq_get_key.py منطق PCQQ القديم، واستخدامه مباشرة على QQNT غالبًا سيفشل (الوحدات وسلسلة الاستدعاءات مختلفة).
  2. QQNT متعدد العمليات، والـ attach عشوائيًا على QQ.exe قد يسبّب بسهولة خطأ ProcessNotRespondingError. أعطِ الأولوية للعملية الرئيسية (غالبًا التي يكون فيها cmdline يحتوي على وسيط واحد فقط).
  3. إذا قمتَ بالـ hook قبل تحميل wrapper.node فسيظهر خطأ unable to find module 'wrapper.node'. الحل هو الاستقصاء (polling) إلى أن يتم تحميل الوحدة ثم التعليق، أو استخدام طريقة spawn للحقن مبكرًا.
  4. dbName=main ليست استثناءً؛ إنها اسم مستعار لاتصال SQLite وليست اسم ملف قاعدة البيانات.

خلاصة بجملة واحدة: يجب أن تتطابق الشروط الثلاثة: الإصدار، العملية، والتوقيت في آنٍ واحد، حتى تحصل على المفتاح بشكل مستقر.


هوا تنغ (化腾) أنت الآن أسرِع وتوسّل إليّ ألا أرحل، ههه