Tutorial completo para extrair a Key do banco de dados do QQNT 9.9.26-44343 (válido em 20260217) (inclui script executável)

Cenário aplicável: QQNT 9.9.26-44343 no Windows; é necessário extrair a key do banco de dados SQLCipher (para decriptar posteriormente bancos como nt_msg.db etc.).

0. Aviso de riscos

  • Há certo risco de acionamento de controles/risco da conta; recomenda-se fazer backup do histórico de conversas primeiro.
  • Apenas para exportação de dados pessoais e estudo; cumpra as leis/regulamentos e os termos da plataforma.

1. Preparação do ambiente

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

Instalar dependências:

py -m pip install frida psutil

2. Salvar o script (específico desta versão)

Crie um novo arquivo: qqnt_get_key_9926.py, com o seguinte conteúdo:

import time
import frida
import psutil

# Compatível apenas com QQNT 9.9.26-44343 (offset 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 \u003c 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 no 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 \u003c= 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. Executar

Garanta que o QQ já esteja logado (ou deixe o script iniciar o QQ automaticamente):

py .\\qqnt_get_key_9926.py

Se aparecer uma saída parecida, deu certo:

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

Ou:

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

4. Como usar a key

  • Podem aparecer dois comprimentos; use o conjunto correspondente ao banco-alvo.
  • key é uma string hexadecimal; às vezes, na essência, é o hexadecimal que representa texto ASCII.

Exemplo de conversão de PowerShell para ASCII:

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

5. Caminho comum do banco de dados

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

Se você só vir dbName=main, isso é um alias de conexão do SQLite e não significa que o nome do arquivo do banco esteja errado.

6. Observações de compatibilidade de versão

O TARGET_RVA = 0x021A5BF0 deste tutorial corresponde apenas ao QQNT 9.9.26-44343. Se falhar em versões posteriores, será necessário localizar o offset novamente.

Complementando com um “pitfall” que eu realmente encontrei, para evitar que todo mundo fique errando e tentando de novo:

  1. pcqq_get_key.py é a lógica antiga do PCQQ; usar isso diretamente no QQNT quase sempre dá ruim (os módulos e a cadeia de chamadas são diferentes).
  2. O QQNT é multiprocessos; dar attach em qualquer QQ.exe aleatoriamente costuma resultar em ProcessNotRespondingError. Dê prioridade ao processo principal (geralmente é o que tem apenas 1 parâmetro no cmdline).
  3. Se você fizer hook antes de wrapper.node ter carregado, vai dar unable to find module 'wrapper.node'. A solução é fazer polling até o módulo carregar e só então “pendurar” o hook, ou usar o modo spawn para injetar o mais cedo possível.
  4. dbName=main não é uma exceção; é um alias de conexão do SQLite, não o nome do arquivo do banco.

Resumo em uma frase: versão, processo e timing — esses três fatores precisam bater ao mesmo tempo para conseguir obter a key de forma estável.


Ma Huateng, agora se apressa e me implora para eu não ir embora, hehe