Tutorial completo para extraer la Key de la base de datos en QQNT 9.9.26-44343 (válido para 20260217) (incluye script ejecutable)

Escenario aplicable: QQNT 9.9.26-44343 en Windows; es necesario extraer la key de la base de datos SQLCipher (para el descifrado posterior de bases de datos como nt_msg.db).

0. Aviso de riesgos

  • Existe cierto riesgo de control de riesgos de la cuenta; se recomienda hacer primero una copia de seguridad del historial de chat.
  • Solo para exportación de datos personales y aprendizaje; cumpla con las leyes y normativas y los acuerdos de la plataforma.

1. Preparación del entorno

  • PowerShell como administrador
  • QQNT instalado: 9.9.26-44343
  • Python 3.10+ (probado, 3.13 funciona)

Instalar dependencias:

py -m pip install frida psutil

2. Guardar el script (solo para esta versión)

Crear un archivo: qqnt_get_key_9926.py, con el siguiente contenido:

import time
import frida
import psutil

# Solo compatible con QQNT 9.9.26-44343 (desplazamiento dentro de 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 Win: 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. Ejecución

Asegúrese de que QQ haya iniciado sesión (o deje que el script inicie QQ automáticamente):

py .\\qqnt_get_key_9926.py

Si aparece una salida similar, es correcto:

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

O:

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

4. Cómo usar la key

  • Pueden aparecer ambas longitudes; elija la que corresponda a la base de datos objetivo.
  • key es una cadena hexadecimal; a veces, en esencia, es la representación hexadecimal de texto ASCII.

Ejemplo de PowerShell para convertir a ASCII:

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

5. Ruta común de la base de datos

C:\\Users\\tu_nombre_de_usuario\\Documents\\Tencent Files\\tu_numero_de_QQ\\nt_qq\\nt_db\\nt_msg.db

Si solo ve dbName=main, esto es un alias de conexión de SQLite y no significa que el nombre del archivo de la base de datos sea incorrecto.

6. Nota de compatibilidad de versiones

En este tutorial, TARGET_RVA = 0x021A5BF0 solo corresponde a QQNT 9.9.26-44343. Si deja de funcionar en versiones posteriores, será necesario volver a localizar el desplazamiento.

Añado un escollo con el que me topé en la práctica para evitar que todos tengan que probar una y otra vez:

  1. pcqq_get_key.py es la lógica antigua de PCQQ; si lo usas tal cual para atacar QQNT, casi seguro que va a fallar (los módulos y la cadena de llamadas no son los mismos).
  2. QQNT es multiproceso; adjuntarte a cualquier QQ.exe al azar suele provocar ProcessNotRespondingError. Prioriza el proceso principal (lo común es el que en cmdline solo tiene 1 parámetro).
  3. Si haces hook antes de que se cargue wrapper.node, dará unable to find module 'wrapper.node'. La solución es hacer sondeo (polling) y enganchar después de que el módulo se cargue, o usar el modo spawn para inyectar lo antes posible.
  4. dbName=main no es una excepción: es el alias de la conexión SQLite, no el nombre del archivo de base de datos.

Resumen en una frase: versión, proceso y momento deben coincidir a la vez para poder obtener el key de forma estable.


Huateng, ahora date prisa y suplícame que no me vaya. Jejeje.