├── .gitignore ├── docs ├── public │ ├── head │ │ ├── Disa.png │ │ ├── siten.jpg │ │ ├── yllhwa.jpg │ │ ├── Ljzd-PRO.jpg │ │ ├── fumiama.jpg │ │ ├── leng-yue.jpg │ │ ├── Mythologyli.jpg │ │ ├── Young-Lord.jpg │ │ ├── alphagocc.jpg │ │ └── shenjackyuanjie.jpg │ ├── icons │ │ ├── 404.png │ │ ├── logo.jpg │ │ ├── sign.webp │ │ ├── favicon.ico │ │ ├── cplusplus.svg │ │ ├── javascript.svg │ │ └── python.svg │ ├── img │ │ ├── Myth1.png │ │ ├── Myth2.png │ │ ├── Myth3.png │ │ ├── Myth4.png │ │ ├── Myth6.png │ │ ├── Myth7.png │ │ ├── locals.png │ │ ├── image-ios-1.webp │ │ ├── image-ios-2.webp │ │ ├── image-ios-3.webp │ │ ├── image-ios-4.webp │ │ ├── image-ios-5.webp │ │ ├── image-mac-1.webp │ │ ├── image-mac-2.webp │ │ ├── image-mac-3.webp │ │ ├── image-mac-4.webp │ │ ├── image-mac-5.webp │ │ ├── image-mac-6.webp │ │ ├── image-win-1.webp │ │ ├── image-win-2.webp │ │ ├── image-win-3.webp │ │ ├── mobyw_ida_0.png │ │ ├── mobyw_ida_4.png │ │ ├── mobyw_ida_b.png │ │ ├── strings-view.png │ │ ├── debug_windows.png │ │ ├── gif-linux-gdb.gif │ │ ├── Attach_to_process.png │ │ ├── wrapper_node_offset.png │ │ ├── image-mac-sqlcipher-conf.webp │ │ └── image-win-sqlcipher-conf.webp │ ├── thanks │ │ ├── 失迹.png │ │ ├── vercel.png │ │ ├── GroupChatAnnualReport.jpg │ │ ├── github.svg │ │ └── vitepress.svg │ └── files │ │ ├── android_get_backup_key.py │ │ ├── ios_get_key.js │ │ ├── QQ_Offset.json │ │ ├── pcqq_get_key.py │ │ ├── android_hook_md5.py │ │ ├── android_dump.js │ │ ├── linux_qq_get_key.py │ │ ├── android_get_key.py │ │ ├── windows_ntqq_key.py │ │ ├── android_get_backup_key.js │ │ └── pcqq_DANGER_rekey.py ├── .vitepress │ ├── theme │ │ ├── style │ │ │ ├── index.css │ │ │ ├── var.css │ │ │ └── custom-block.css │ │ ├── index.ts │ │ ├── NotFound.vue │ │ └── components │ │ │ ├── HashCalculator.vue │ │ │ └── QQCachePath.vue │ └── config.mts ├── decrypt │ ├── IOSQQ.md │ ├── index.md │ ├── description.md │ ├── NTQQ (Linux).md │ ├── AndroidQQ.md │ ├── PCQQ (Windows).md │ ├── NTQQ (macOS ARM).md │ ├── decode_db.md │ ├── NTQQ (Windows).md │ ├── NTQQ (Android).md │ ├── NTQQ (macOS x86).md │ └── NTQQ (iOS).md ├── about │ ├── index.md │ ├── 碎碎念.md │ ├── projects.md │ ├── thanks.md │ ├── LICENSE.md │ └── contributors.md ├── view │ ├── index.md │ ├── db_file_analysis │ │ ├── index.md │ │ ├── rich_media.db.md │ │ ├── files_in_chat.db.md │ │ ├── collection.db.md │ │ ├── profile_info.db.md │ │ ├── emoji.db.md │ │ └── group_info.db.md │ ├── read_db.md │ └── message_export.md ├── index.md ├── what-is-this.md └── files.md ├── package.json ├── README.md ├── .github └── workflows │ └── mirror.yml └── LICENSE /.gitignore: -------------------------------------------------------------------------------- 1 | docs/.vitepress/dist/**/* 2 | docs/.vitepress/cache/**/* 3 | node_modules/**/* -------------------------------------------------------------------------------- /docs/public/head/Disa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/head/Disa.png -------------------------------------------------------------------------------- /docs/public/head/siten.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/head/siten.jpg -------------------------------------------------------------------------------- /docs/public/head/yllhwa.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/head/yllhwa.jpg -------------------------------------------------------------------------------- /docs/public/icons/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/icons/404.png -------------------------------------------------------------------------------- /docs/public/icons/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/icons/logo.jpg -------------------------------------------------------------------------------- /docs/public/icons/sign.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/icons/sign.webp -------------------------------------------------------------------------------- /docs/public/img/Myth1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/Myth1.png -------------------------------------------------------------------------------- /docs/public/img/Myth2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/Myth2.png -------------------------------------------------------------------------------- /docs/public/img/Myth3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/Myth3.png -------------------------------------------------------------------------------- /docs/public/img/Myth4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/Myth4.png -------------------------------------------------------------------------------- /docs/public/img/Myth6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/Myth6.png -------------------------------------------------------------------------------- /docs/public/img/Myth7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/Myth7.png -------------------------------------------------------------------------------- /docs/public/img/locals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/locals.png -------------------------------------------------------------------------------- /docs/public/thanks/失迹.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/thanks/失迹.png -------------------------------------------------------------------------------- /docs/.vitepress/theme/style/index.css: -------------------------------------------------------------------------------- 1 | /* index.css */ 2 | @import './var.css'; 3 | @import './custom-block.css'; -------------------------------------------------------------------------------- /docs/public/head/Ljzd-PRO.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/head/Ljzd-PRO.jpg -------------------------------------------------------------------------------- /docs/public/head/fumiama.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/head/fumiama.jpg -------------------------------------------------------------------------------- /docs/public/head/leng-yue.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/head/leng-yue.jpg -------------------------------------------------------------------------------- /docs/public/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/icons/favicon.ico -------------------------------------------------------------------------------- /docs/public/thanks/vercel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/thanks/vercel.png -------------------------------------------------------------------------------- /docs/public/head/Mythologyli.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/head/Mythologyli.jpg -------------------------------------------------------------------------------- /docs/public/head/Young-Lord.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/head/Young-Lord.jpg -------------------------------------------------------------------------------- /docs/public/head/alphagocc.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/head/alphagocc.jpg -------------------------------------------------------------------------------- /docs/public/img/image-ios-1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/image-ios-1.webp -------------------------------------------------------------------------------- /docs/public/img/image-ios-2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/image-ios-2.webp -------------------------------------------------------------------------------- /docs/public/img/image-ios-3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/image-ios-3.webp -------------------------------------------------------------------------------- /docs/public/img/image-ios-4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/image-ios-4.webp -------------------------------------------------------------------------------- /docs/public/img/image-ios-5.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/image-ios-5.webp -------------------------------------------------------------------------------- /docs/public/img/image-mac-1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/image-mac-1.webp -------------------------------------------------------------------------------- /docs/public/img/image-mac-2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/image-mac-2.webp -------------------------------------------------------------------------------- /docs/public/img/image-mac-3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/image-mac-3.webp -------------------------------------------------------------------------------- /docs/public/img/image-mac-4.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/image-mac-4.webp -------------------------------------------------------------------------------- /docs/public/img/image-mac-5.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/image-mac-5.webp -------------------------------------------------------------------------------- /docs/public/img/image-mac-6.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/image-mac-6.webp -------------------------------------------------------------------------------- /docs/public/img/image-win-1.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/image-win-1.webp -------------------------------------------------------------------------------- /docs/public/img/image-win-2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/image-win-2.webp -------------------------------------------------------------------------------- /docs/public/img/image-win-3.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/image-win-3.webp -------------------------------------------------------------------------------- /docs/public/img/mobyw_ida_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/mobyw_ida_0.png -------------------------------------------------------------------------------- /docs/public/img/mobyw_ida_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/mobyw_ida_4.png -------------------------------------------------------------------------------- /docs/public/img/mobyw_ida_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/mobyw_ida_b.png -------------------------------------------------------------------------------- /docs/public/img/strings-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/strings-view.png -------------------------------------------------------------------------------- /docs/public/img/debug_windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/debug_windows.png -------------------------------------------------------------------------------- /docs/public/img/gif-linux-gdb.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/gif-linux-gdb.gif -------------------------------------------------------------------------------- /docs/public/head/shenjackyuanjie.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/head/shenjackyuanjie.jpg -------------------------------------------------------------------------------- /docs/public/img/Attach_to_process.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/Attach_to_process.png -------------------------------------------------------------------------------- /docs/public/img/wrapper_node_offset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/wrapper_node_offset.png -------------------------------------------------------------------------------- /docs/public/thanks/GroupChatAnnualReport.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/thanks/GroupChatAnnualReport.jpg -------------------------------------------------------------------------------- /docs/public/img/image-mac-sqlcipher-conf.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/image-mac-sqlcipher-conf.webp -------------------------------------------------------------------------------- /docs/public/img/image-win-sqlcipher-conf.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QQBackup/QQDecrypt/HEAD/docs/public/img/image-win-sqlcipher-conf.webp -------------------------------------------------------------------------------- /docs/decrypt/IOSQQ.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: IOS版QQ 3 | order: 10 4 | --- 5 | 6 | # IOS版QQ 7 | - [QQ-G 手机QQ本地聊天记录查看器 - 吾爱破解](https://www.52pojie.cn/thread-1227585-1-1.html) 2020/7/29 更新 8 | -------------------------------------------------------------------------------- /docs/about/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 关于 3 | prev: false 4 | next: false 5 | editLink: false 6 | lastUpdated: false 7 | --- 8 | 9 | -------------------------------------------------------------------------------- /docs/view/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 数据库解析 3 | prev: false 4 | next: false 5 | editLink: false 6 | lastUpdated: false 7 | --- 8 | 9 | -------------------------------------------------------------------------------- /docs/decrypt/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 解密教程 3 | prev: false 4 | next: false 5 | editLink: false 6 | lastUpdated: false 7 | --- 8 | 9 | -------------------------------------------------------------------------------- /docs/view/db_file_analysis/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | order: 4 3 | title: db文件分析 4 | --- 5 | 6 | 1. 本文档用于记录QQ数据库解析进度 7 | 8 | 1. 以数据库命名的文档是对其表名的解析 9 | 10 | 1. 消息导出计划是对QQ聊天消息本地存贮内容的分析 11 | 12 | 13 | 本项目仅供学习交流使用,严禁用于任何违反中国大陆法律法规、您所在地区法律法规、[QQ软件许可及服务协议](https://rule.tencent.com/rule/preview/46a15f24-e42c-4cb6-a308-2347139b1201)的行为,开发者不承担任何相关行为导致的直接或间接责任。 14 | 15 | 本项目不对分析内容的完整性、准确性作任何担保,需要更多的帮助来验证真实性 16 | 17 | 本项目基本遵循[LICENSE](/about/LICENSE)里的开源协议 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qqdecrypt", 3 | "version": "1.0.0", 4 | "description": "Your project description", 5 | "scripts": { 6 | "docs:dev": "vitepress dev docs", 7 | "docs:build": "vitepress build docs", 8 | "docs:preview": "vitepress preview docs" 9 | }, 10 | "dependencies": { 11 | "axios": "^1.9.0", 12 | "crypto-js": "^4.2.0", 13 | "node-fetch": "^3.3.2", 14 | "pnpm": "^10.11.1", 15 | "viewerjs": "^1.11.7", 16 | "vitepress-plugin-image-viewer": "^1.1.6" 17 | }, 18 | "devDependencies": { 19 | "vitepress": "^1.6.3", 20 | "vitepress-sidebar": "^1.31.1", 21 | "vue": "^3.5.13" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "QQDecrypt" 7 | text: "QQ 聊天数据库解密" 8 | tagline: 用于解密查看QQ数据库 9 | image: 10 | src: /icons/sign.webp 11 | alt: logo 12 | actions: 13 | - theme: brand 14 | text: 这是什么 15 | link: /what-is-this 16 | - theme: alt 17 | text: GitHub 18 | link: https://github.com/QQBackup/QQDecrypt 19 | 20 | features: 21 | - icon: 🗝 22 | title: 解密 23 | details: 获取数据库密钥 24 | link: /decrypt/description 25 | - icon: 👁‍ 26 | title: 查看 27 | details: 打开数据库/读取信息 28 | link: /view/read_db 29 | - icon: 🚀 30 | title: 社区项目 31 | details: 有关 NTQQ 数据库的项目 32 | link: /about/projects 33 | --- 34 | 35 | -------------------------------------------------------------------------------- /docs/public/icons/cplusplus.svg: -------------------------------------------------------------------------------- 1 | C++ -------------------------------------------------------------------------------- /docs/view/db_file_analysis/rich_media.db.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: rich_media.db 3 | order: 7 4 | --- 5 | 6 | # rich_media.db 7 | 8 | ## `file_table` 9 | 10 | 下载文件信息 11 | 12 | | 列名 | 类型 | 含义 | 注 | 13 | | ----- | ---- | --------------- | ------------ | 14 | | 45401 | int | -- | 不明 | 15 | | 40001 | int | msgid | 消息id | 16 | | 45001 | int | elementid | | 17 | | 45402 | str | filename | 文件名 | 18 | | 45403 | str | filepath | 文件存贮路径 | 19 | | 45405 | int | filesize | 文件大小 | 20 | | 45985 | int | attrType | 未知 | 21 | | 45503 | str | fileUuid | 文件唯一id | 22 | | 40021 | str | peeruid | 群号 | 23 | | 64914 | int | msgBigClubLevel | | 24 | 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 致谢 2 | 该文档站内容源于[qq-win-db-key](https://github.com/QQBackup/qq-win-db-key)项目,是为了将互联网有关NTQQ数据库解密教程整合而生的,感谢[原作者Young-Lord](https://github.com/Young-Lord)等贡献者的付出 3 | 4 | ## 贡献须知 5 | 6 | 请确认项目在编辑后能够正常运行且显示正常。请善用以下命令: 7 | 8 | ```shell 9 | pnpm run docs:dev 10 | ``` 11 | 12 | 如有不明白的操作,请参阅[主题官方教程](https://vitepress.dev/zh/guide/getting-started)。 13 | 本项目使用了部分插件,具体可在config.mts中查看 14 | 15 | 本文档的主要文件储存在docs目录下。新增文件需指定`order`用于侧边栏排序 16 | 17 | ```plaintext 18 | ├─ docs 19 | ├─ .vitepress 20 | │ ├─ theme 21 | │ ├─ config.mts 22 | ├─ about(关于) 23 | │ ├─ contributors.md (贡献者) 24 | │ ├─ thanks.md (致谢) 25 | │ ├─ projects.md (社区项目) 26 | ├─ decrypt (解密数据库相关文档) 27 | ├─ public (媒体文件存放) 28 | ├─ view (读取数据库相关文档) 29 | ├─ files.md (文件) 30 | ├─ index.md 31 | └─ what-is-this.md (了解项目) 32 | ``` 33 | 34 | -------------------------------------------------------------------------------- /docs/what-is-this.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: doc 3 | title: 网站说明 4 | next: 5 | text: '社区项目' 6 | link: '/about/projects' 7 | hidesidebar: true 8 | --- 9 | 10 | ## 网站介绍 11 | 12 | 本站内容基于开源项目 [qq-win-db-key](https://github.com/QQBackup/qq-win-db-key) 构建,旨在提供跨平台QQ聊天数据库解密指南。特别感谢[原作者Young-Lord](https://github.com/Young-Lord)等开发者的开拓性工作,未来计划逐步扩展数据库解析方案。 13 | 14 | 有任何问题和建议欢迎提[issues](https://github.com/QQBackup/QQDecrypt/issues/new/choose) 15 | 16 | ## 寻求合作者 17 | 18 | 欢迎一切 能够实现任何相关数据解析算法/乐意适配其他平台者 参与本项目以及 [QQ-History-Backup](https://github.com/QQBackup/QQ-History-Backup/tree/dev) 的开发!直接开 PR/issue。文档的写作风格随意(但建议图片的 替代文本 和 文件名 好好写),也可以只加入一个指向你的 仓库/博客 等的链接。 19 | 20 | ## 声明 21 | 22 | 本项目基本遵循`LICENSE`里的开源协议,基本接近**标识项目地址**且**禁止商用**;部分文件同时以不同的协议发布,具体参见文件内对应的声明。 23 | 24 | ## 隐私政策 25 | - 不收集/存储用户身份信息 26 | - 禁用Cookie不影响网站功能 27 | - 网站使用vercel静态部署托管 28 | -------------------------------------------------------------------------------- /docs/public/thanks/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/public/icons/javascript.svg: -------------------------------------------------------------------------------- 1 | JavaScript -------------------------------------------------------------------------------- /.github/workflows/mirror.yml: -------------------------------------------------------------------------------- 1 | name: Mirror to shenapex/QQDecrypt 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | workflow_dispatch: 8 | 9 | jobs: 10 | mirror: 11 | if: github.actor != 'github-actions[bot]' 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout source repo 16 | uses: actions/checkout@v2 17 | with: 18 | repository: QQBackup/QQDecrypt 19 | token: ${{ secrets.GH_TOKEN }} 20 | path: source 21 | 22 | - name: Configure Git 23 | run: | 24 | git config --global user.name "QQbackup" 25 | git config --global user.email "172821606+shenapex@users.noreply.github.com" 26 | 27 | - name: Push to target repo 28 | env: 29 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 30 | run: | 31 | cd source 32 | git remote add target https://shenapex:${GH_TOKEN}@github.com/shenapex/QQDecrypt.git 33 | git push target main --force 34 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { h } from 'vue' 2 | import DefaultTheme from 'vitepress/theme' 3 | import HashCalculator from './components/HashCalculator.vue' 4 | import QQCachePath from './components/QQCachePath.vue' 5 | import { onMounted, watch, nextTick } from 'vue' 6 | import { useRoute, useData } from 'vitepress' 7 | import type { Theme } from 'vitepress' 8 | import NotFound from './NotFound.vue' 9 | import 'viewerjs/dist/viewer.min.css' 10 | import imageViewer from 'vitepress-plugin-image-viewer' 11 | import vImageViewer from 'vitepress-plugin-image-viewer/lib/vImageViewer.vue' 12 | import './style/index.css' 13 | 14 | export default { 15 | ...DefaultTheme, 16 | 17 | Layout() { 18 | const { frontmatter } = useData() 19 | const props: Record = {} 20 | 21 | if (frontmatter.value?.layoutClass) { 22 | props.class = frontmatter.value.layoutClass 23 | } 24 | 25 | return h(DefaultTheme.Layout, props, { 26 | 'not-found': () => h(NotFound) 27 | }) 28 | }, 29 | 30 | setup() { 31 | const route = useRoute() 32 | imageViewer(route) 33 | }, 34 | 35 | enhanceApp({ app }) { 36 | app.component('HashCalculator', HashCalculator) 37 | app.component('QQCachePath', QQCachePath) 38 | } 39 | } satisfies Theme 40 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/NotFound.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /docs/decrypt/description.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 说明(必看) 3 | prev: false 4 | order: 1 5 | --- 6 | 7 | 8 | > [!WARNING] 警告 9 | > 教程中可能对数据库进行不可逆操作,请注意备份数据库文件 10 | > 11 | >尽管已经经过实验验证可用,本仓库中给出的指引**可能**有**破坏聊天记录**或**导致封号**的风险,强烈建议在自行审查代码、评估>风险后使用。 12 | >如果您确实要使用,**建议**进行以下操作,以减小风险: 13 | >- 先将聊天记录使用其他更保险的方式**导出**,比如 PCQQ (Windows) 自带的“导出消息记录(`mht`格式) 14 | >- 做好**备份**,比如 安卓端使用系统的备份功能、电脑端全盘备份 15 | >- 将聊天记录使用官方的“**迁移聊天记录**”功能,转移到不常用设备或虚拟机后操作 16 | >- 尽可能选择**不注入 QQ 进程**、**不对 QQ 安装包进行修改**的方式(如:不使用 QAuxiliary;不使用 Frida;不使用 gdb;使用安卓系统自带的备份功能,导出数据库后提取) 17 | > 18 | ## 说明 19 | 本仓库**并非面向纯小白的教程**,而是在**假设您已经有一定逆向、动态调试等知识**的前提下提供的参考资料。尽管开发者可能会为了便捷提供详细教程或完整脚本,您也应当**有一定自行修改、调试的能力**。另外,请在提问前先**完整阅读** [qq-win-db-key](https://github.com/QQBackup/qq-win-db-key/issues) 与 [QQ-History-Backup](https://github.com/QQBackup/QQ-History-Backup/issues) 的**所有 issue**。如果您无论怎么改都跑不起来,请自由开 [issue](https://github.com/QQBackup/qq-win-db-key/issues)。(当然如果您感觉教程太烂了或者愿意补充,也可以直接开 PR,记得`@QQBackup`以通知维护者 merge) 20 | 21 | **如果你不太懂怎么用**:强烈建议自行在B站、各个博客内寻找详细教程。 22 | 23 | 本项目仅供学习交流使用,严禁用于任何违反中国大陆法律法规、您所在地区法律法规、[QQ软件许可及服务协议](https://rule.tencent.com/rule/preview/46a15f24-e42c-4cb6-a308-2347139b1201)的行为,开发者不承担任何相关行为导致的直接或间接责任。 24 | 25 | 本项目不对生成内容的完整性、准确性作任何担保,生成的一切内容**不可用于法律取证**,您不应当将其用于学习与交流外的任何用途。 26 | 27 | 本项目基本遵循`LICENSE`里的开源协议,基本接近**标识项目地址**且**禁止商用**;部分文件同时以不同的协议发布,具体参见文件内对应的声明。 28 | 29 | 本教程中所需要的文件可在网站右上角“文件”栏目中找到,可能会滞后于原仓库[qq-win-db-key](https://github.com/QQBackup/qq-win-db-key)更新,若有需要可前往GitHub下载最新版本 -------------------------------------------------------------------------------- /docs/public/icons/python.svg: -------------------------------------------------------------------------------- 1 | Python -------------------------------------------------------------------------------- /docs/.vitepress/theme/style/var.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --vp-c-brand-1: #005f9e; /* 浅蓝 */ 3 | --vp-c-brand-2: #0077cc; /* 中蓝 */ 4 | --vp-c-brand-3: #0088ff; /* 深蓝 */ 5 | } 6 | 7 | .dark { 8 | --vp-c-brand-1: #3399ff; /* 浅蓝(深色模式) */ 9 | --vp-c-brand-2: #0077cc; /* 中蓝(深色模式) */ 10 | --vp-c-brand-3: #005f9e; /* 深蓝(深色模式) */ 11 | } 12 | 13 | :root { 14 | /* hero标题渐变色 */ 15 | --vp-home-hero-name-color: transparent; 16 | --vp-home-hero-name-background: -webkit-linear-gradient(120deg, #0044cc, #0099ff); 17 | 18 | /* hero logo背景渐变色 */ 19 | --vp-home-hero-image-background-image: linear-gradient(-45deg, #0044cc 50%, #0099ff 50%); 20 | --vp-home-hero-image-filter: blur(40px); 21 | } 22 | 23 | html .vp-doc a { 24 | text-decoration: none !important; 25 | color: var(--vp-c-brand-2) !important; 26 | } 27 | 28 | html .vp-doc a:hover { 29 | color: var(--vp-c-brand-3) !important; 30 | } 31 | 32 | html.dark .vp-doc a { 33 | color: var(--vp-c-brand-1) !important; 34 | } 35 | 36 | html.dark .vp-doc a:hover { 37 | color: var(--vp-c-brand-2) !important; 38 | } 39 | 40 | blockquote { 41 | border-left: 4px solid #42b983; 42 | padding: 0.5rem 1rem; 43 | margin: 1rem 0; 44 | color: #666; 45 | font-style: italic; 46 | background-color: #f8f8f8; 47 | } 48 | 49 | blockquote.single-line { 50 | text-align: center; 51 | padding: 1rem; 52 | border-left: none; 53 | font-size: 1.2em; 54 | background: transparent; 55 | border-top: 1px solid #eee; 56 | border-bottom: 1px solid #eee; 57 | } -------------------------------------------------------------------------------- /docs/view/db_file_analysis/files_in_chat.db.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: files_in_chat.db 3 | order: 6 4 | --- 5 | 6 | # files_in_chat.db 7 | 接受的媒体文件信息 8 | ## files_in_chat_table 9 | 10 | | 列名 | 类型 | 含义 | 注 | 11 | | ----- | ---- | ----------------- | ------------------------------------------------ | 12 | | 45001 | int | clientSeq | 接收的图片顺序,在本地数据库中递增 | 13 | | 82300 | int | msgRandom | 消息随机值,用于对消息去重 | 14 | | 40001 | int | msgid | 消息ID | 15 | | 45403 | str | filepath | 文件存贮路径 | 16 | | 45404 | str | thumbpath | 预览图缓存路径(多见于视频) | 17 | | 40020 | str | nt_uid | 发送者nt_uid | 18 | | 40021 | str | peeruid/senderuin | 来源群号/QQ号 | 19 | | 40010 | int | chatType | 聊天类型,具体[见此](/view/db_file_analysis/nt_msg.db) | 20 | | 82301 | int | 未查明 | | 21 | | 45002 | int | ElementType | | 22 | | 45003 | int | subElementType | | 23 | | 45402 | str | fileName | 文件名 | 24 | | 45405 | int | fileSize | 文件大小(KB) | 25 | | 40050 | int | msgTime | 发送时间 | 26 | | 82302 | int | original | 是否为原图,1为是,0为否 | 27 | 28 | * 这里面主要存贮的是有关下载文件、语音文件、视频文件的信息 29 | 30 | 图片大部分`45403`值为空 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /docs/decrypt/NTQQ (Linux).md: -------------------------------------------------------------------------------- 1 | --- 2 | title: NTQQ (Linux) 3 | order: 6 4 | --- 5 | 6 | # NTQQ (Linux) 7 | 三种方法,其中搜索内存的方法可能更简单,但效率低并且不一定稳定。Frida hook 需要每次去找相关函数地址、参数列表,并适当修改。 8 | 9 | ## Frida hook 10 | 可以直接使用 [msojocs/nt-hook](https://github.com/msojocs/nt-hook) 得到数据库密钥,可能需要微调代码。 11 | 12 | ## 搜索内存 13 | 穷举可能的密码字符串:参考[此 gist](https://gist.github.com/bczhc/c0f29920d4e9d0cc6d2c49f7f2fb3a78) 14 | 15 | ## GDB 法 16 | 借助 Python 脚本自动化调试过程,进而实现自动化输出密钥。感谢[Wenz-jam](https://github.com/Wenz-jam)的[贡献](https://github.com/QQBackup/qq-win-db-key/pull/46) 17 | 18 | 需要的软件软件以及可能的下载方式 19 | `GDB` 是 GNU 调试器,可以通过以下方式下载: 20 | 21 | - **Ubuntu/Debian**: 22 | 23 | ```bash 24 | sudo apt install gdb 25 | ``` 26 | 27 | - **Fedora/RHEL**: 28 | 29 | ```bash 30 | sudo dnf install gdb 31 | ``` 32 | 33 | - **验证GDB是否支持Python** 34 | 在gdb下运行如下命令 35 | 36 | ```plain 37 | (gdb) python print("Hello from Python in GDB") 38 | ``` 39 | 40 | 如果没有错误,并且输出了`Hello from Python in GDB`,那么 Python 支持已启用。 41 | 42 | # 2. **readelf 和 objdump** 43 | 44 | `readelf` 和 `objdump` 是 `binutils` 包的一部分,可以通过`binutils`安装 45 | 46 | - **Ubuntu/Debian**: 47 | 48 | ```bash 49 | sudo apt install binutils 50 | ``` 51 | 52 | - **Fedora/RHEL**: 53 | 54 | ```bash 55 | sudo dnf install binutils 56 | ``` 57 | 58 | # 使用方式 59 | 60 | **由于Python脚本帮我们简化了太多操作,使用非常简单** 61 | 62 | 下载并找到linux_qq_get_key.py 63 | 64 | 打开终端并输入 65 | 66 | ```bash 67 | gdb -x qq 68 | ``` 69 | 70 | 这里的``需要替换成具体的存放`linux_qq_get_key.py`这个脚本文件的位置。 71 | 72 | 初始化过程因为需要反编译`wrapper.node`,所以第一次运行会有点慢。 73 | 74 | 等QQ的窗口弹出后正常的登录即可。 75 | 若成功QQ会自动关闭,这时在终端中可查看密钥。如果QQ正常的登录并弹出了消息界面,这多半是脚本出问题了,如果你乐意可以试着解决他。 76 | 77 | 脚本会自动的将反编译后得到的一些必要信息保存在本地(当前路径下),所以如果想要多次使用,或者不希望他随地乱丢垃圾的话,可以专门把它放在一个目录下。 78 | 79 | 大致的效果就像这样 80 | ![Linux QQ gdb法 效果预览](/img/gif-linux-gdb.gif) 81 | 82 | 83 | # 打开数据库 84 | 85 | 请参考 [NTQQ 解密数据库](decode_db.md)。 86 | -------------------------------------------------------------------------------- /docs/decrypt/AndroidQQ.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 安卓版QQ及TIM 3 | order: 9 4 | --- 5 | 6 | # 安卓版QQ及TIM 7 | ## 获取聊天记录文件 8 | 9 | >[!TIP] 新版TIM说明 10 | >腾讯于2024年11月4日发布了TIM4.0.95版本,从该版本起腾讯TIM也将使用NT架构,经实际测试发现其数据库解密方式与[NTQQ(Android)](NTQQ%20(Android).md#方法1-推荐)方法一相同 11 | 12 | >[!INFO] 注意 13 | > 注:以下提到的“电脑”泛指一切可以运行此程序的环境,如安卓手机上的 Termux 也属于此列 14 | > 注:以下内容假设您使用的是 QQ 而非 TIM,如果您在使用 TIM,请将`com.tencent.mobileqq`改为`com.tencent.tim`,将`MobileQQ`改为`Tim` 15 | 16 | 17 | 如果手机已获得 root 权限,聊天记录可在以下路径找到。 18 | 19 | ```plain 20 | /data/data/com.tencent.mobileqq/ 21 | ``` 22 | 23 | 我们需要的文件只有`databases/.db`,`databases/slowtable_.db`,`files/kc`,因此您可以将整个文件夹压缩后传输到电脑上,亦或将这三个文件单独放在同一个目录中传输。本程序会自动识别这两种不同的目录结构。 24 | 25 | 如果没有 root 权限,可以通过手机自带的备份工具备份整个 QQ,拷贝备份文件到电脑,解压找到 `com.tencent.mobileqq`。该方法可行性及具体操作各个系统有差异,请自行在互联网查询。 26 | 27 | 具体方法可以参见 28 | 29 | > 怎样导出手机中的QQ聊天记录? - 益新软件的回答 - 知乎 30 | > 31 | 32 | 如果同时需要在聊天记录中显示图片,拷贝手机中 `/sdcard/Android/data/com.tencent.mobileqq/Tencent/MobileQQ/chatpic/chatimg` 至 `GUI.exe` 同一文件夹中或者拷贝过来的`com.tencent.mobileqq`目录下。 33 | 34 | (QQ)如果同时需要在聊天记录中显示语音,拷贝手机中 `/sdcard/Android/data/com.tencent.mobileqq/Tencent/MobileQQ//ptt` 至 `GUI.exe` 同一文件夹中或者拷贝过来的`com.tencent.mobileqq`目录下。 35 | 36 | (TIM)如果同时需要在聊天记录中显示语音,拷贝手机中 `/sdcard/Android/data/com.tencent.tim/Tencent/Tim/ptt/` 至 `GUI.exe` 同一文件夹中或者拷贝过来的`com.tencent.mobileqq`目录下,并重命名为`ptt`。 37 | 38 | 其他可能需要提取的数据文件可以参照[此处](https://github.com/lqzhgood/Shmily-Get-MobileQQ-Andriod?tab=readme-ov-file)。 39 | 40 | ## 解密、转换 41 | 42 | 建议使用以下项目(本列表可能随时间更新): 43 | 44 | - [lqzhgood/Shmily-Get-MobileQQ-Andriod](https://github.com/lqzhgood/Shmily-Get-MobileQQ-Andriod) ![GitHub last commit](https://img.shields.io/github/last-commit/lqzhgood/Shmily-Get-MobileQQ-Andriod/main) 45 | - [Hakuuyosei/QQHistoryExport](https://github.com/Hakuuyosei/QQHistoryExport) ![GitHub last commit](https://img.shields.io/github/last-commit/Hakuuyosei/QQHistoryExport/master) 46 | - [ZhangJun2017/QQChatHistoryExporter](https://github.com/ZhangJun2017/QQChatHistoryExporter) ![GitHub last commit](https://img.shields.io/github/last-commit/ZhangJun2017/QQChatHistoryExporter/master) 47 | - [QQBackup/QQ-History-Backup](https://github.com/QQBackup/QQ-History-Backup) ![GitHub last commit](https://img.shields.io/github/last-commit/QQBackup/QQ-History-Backup/master) 48 | - [QQ-G 手机QQ本地聊天记录查看器 - 吾爱破解](https://www.52pojie.cn/thread-1227585-1-1.html) 2020/7/29 更新 49 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitepress'; 2 | import { generateSidebar } from 'vitepress-sidebar'; 3 | 4 | export default defineConfig({ 5 | title: 'QQDecrypt', 6 | description: '解密QQ聊天数据库', 7 | lang: 'zh-CN', 8 | head: [ 9 | ['link', { rel: 'icon', href: '/icons/favicon.ico' }], 10 | ], 11 | vite: { 12 | plugins: [], 13 | }, 14 | sitemap: { 15 | hostname: 'https://docs.aaqwq.top', 16 | }, 17 | markdown: { 18 | lineNumbers: true, 19 | config: (md) => { 20 | }, 21 | image: { 22 | lazyLoading: true, 23 | }, 24 | }, 25 | themeConfig: { 26 | appearance: true, 27 | lastUpdated: { text: '最后更新于' }, 28 | sidebar: generateSidebar({ 29 | documentRootPath: 'docs', 30 | useTitleFromFrontmatter:true, 31 | useFolderTitleFromIndexFile:true, 32 | frontmatterTitleFieldName:'title', 33 | excludeFilesByFrontmatterFieldName: 'hidesidebar', 34 | hyphenToSpace: true, 35 | sortMenusByFrontmatterOrder: true, 36 | useFolderLinkFromIndexFile: true 37 | }), 38 | nav: [ 39 | { text: '主页', link: '/' }, 40 | { 41 | text: '关于', 42 | items: [ 43 | { text: '贡献者名单', link: '/about/contributors' }, 44 | { text: '项目致谢', link: '/about/thanks' }, 45 | { text: '使用协议', link: '/about/LICENSE' }, 46 | { text: '社区项目', link: '/about/projects' } 47 | ] 48 | }, 49 | { text: '文件', link: 'https://github.com/QQBackup/QQDecrypt/tree/main/docs/public/files' }, 50 | ], 51 | footer: { 52 | message: ' CC BY-NC-SA 4.0 License', 53 | copyright: 'Copyright © 2025 ', 54 | }, 55 | editLink: { 56 | pattern: 'https://github.com/QQBackup/QQDecrypt/edit/main/docs/:path', 57 | text: '在 GitHub 上编辑此页', 58 | }, 59 | docFooter: { 60 | prev: '上一篇', 61 | next: '下一篇' 62 | }, 63 | darkModeSwitchLabel: '亮/暗模式', 64 | lightModeSwitchTitle: '调整为亮色模式', 65 | darkModeSwitchTitle: '调整为暗色模式', 66 | sidebarMenuLabel: '菜单', 67 | returnToTopLabel: '回到顶部', 68 | socialLinks: [ 69 | { icon: 'github', link: 'https://github.com/QQBackup/QQDecrypt' }, 70 | { icon: 'telegram', link: 'https://t.me/+0mPSrIRky-hjNDAx' }, 71 | ], 72 | outline: { 73 | level: [2, 3], 74 | label: '页面目录', 75 | }, 76 | } 77 | }); -------------------------------------------------------------------------------- /docs/public/thanks/vitepress.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /docs/decrypt/PCQQ (Windows).md: -------------------------------------------------------------------------------- 1 | --- 2 | title: PCQQ (非 NT 架构) 3 | order: 8 4 | --- 5 | 6 | # PCQQ (非 NT 架构) 7 | 8 | 本教程针对 QQ Windows 版(非 NT 架构),即 QQ9 以前的版本,可以在[官网](https://im.qq.com/pcqq/index.shtml)选择“往期怀旧版下载”下载。 9 | 10 | > [!WARNING] 警告 11 | 本文件中列出的方式**风险较大**,请参考项目首页的[警告](/decrypt/description)使用。 12 | 13 | ## 预先准备 14 | 15 | 备份数据库!备份数据库!备份数据库!默认数据库路径为:`C:\Users\<用户名>\Documents\Tencent Files\\Msg3.0.db` 16 | 17 | 测试可用的 QQ 版本:`QQ9.7.3.28.94`、`QQ9.7.6 (28997)`、`QQ9.7.9 (29059)`、`QQ9.7.23 (29368)`,其它版本也可能可用。 18 | 19 | 如果出现异常,可以尝试消灭`QQProtect`后重试: 20 | 21 | ## 跑(手动) 22 | 23 | ### hook 24 | 25 | 需要 Python 以及 Frida:`pip install frida` 26 | 27 | 备份`Msg3.0.db` -> 打开 QQ -> `python pcqq_get_key.py` -> 登录 -> 得到 key 28 | 29 | ### pcqq_rekey_to_none.cpp 30 | 31 | 将`BYTE pwdKey[16]`的下一行(也就是第 313 行)替换为你得到的 key 32 | 33 | 使用 32 位 MinGW-W64([我用的版本](https://sourceforge.net/projects/mingw-w64/files/Toolchains%20targetting%20Win32/Personal%20Builds/mingw-builds/8.1.0/threads-win32/dwarf/i686-8.1.0-release-win32-dwarf-rt_v6-rev0.7z))编译:`g++ pcqq_rekey_to_none.cpp` (记得把`mingw32\bin`加到`PATH`环境变量) 34 | 35 | 把`a.exe`与`Msg3.0.db`一起放在 QQ 安装目录的`Bin`文件夹(比如`C:\Program Files (x86)\Tencent\QQ\Bin\`下,运行`a.exe`,运行完成后`Msg3.0.db`即为解密状态。 36 | 37 | ### 修复 38 | 39 | 得到的`Msg3.0.db`开头有 1024 字节的扩展头,删掉。 40 | 41 | ## 跑(自动,有风险) 42 | 43 | > [!IMPORTANT] 疑难解答 44 | > Q: 报错:frida.NotSupportedError: unexpectedly failed with error code: 0x00000057 ([#41](https://github.com/QQBackup/qq-win-db-key/issues/41)) 45 | > 46 | > A: 可能是由于 Windows 10 与 Frida 16.5 不兼容,可以尝试降级到 Frida 16.4.10 版本。 47 | 48 | 本方法可能导致 QQ 自身的聊天记录数据库被破坏,请谨慎使用! 49 | 50 | 需要 Python 以及 Frida:`pip install frida` 51 | 52 | 备份`Msg3.0.db` -> 打开 QQ -> `python pcqq_dump.py` -> 登录 -> 得到 key,同时解密并修复后的数据库文件将自动生成在运行目录下 53 | 54 | ## 毁灭(必定损坏原始数据) 55 | 56 | 备份`Msg3.0.db` -> 打开 QQ -> `python pcqq_DANGER_rekey.py` -> 登录 -> 原始数据库被破坏 -> 解密并修复后的数据库文件将自动生成在运行目录下 57 | 58 | ## 读取信息 59 | 60 | ### 需要手动编写代码 61 | [qmsg_unpakcer](https://github.com/Akegarasu/qmsg-unpacker) 62 | golang 写的, 没 example, 需要手动编写一部分代码 63 | 64 | ### 可以直接使用 65 | [qq_msg_decode](https://github.com/saucer-man/qq_msg_decode) 66 | python 重写的 qmsg_unpakcer, 带有可以直接运行的文件 不过依然遗失了一些字段 67 | 68 | ### 未完成 69 | [qqdb-decode](https://github.com/shenjackyuanjie/qqdb-decode) 70 | 用 rust 重写的 qq_msg_decode (是这样的的, 重写套娃) 71 | 72 | ## 致谢(询问一切有关编解码、数据格式的问题前必看!!) 73 | 74 | 75 | 76 | 77 | 78 | ( ) 79 | 80 | 81 | 82 | 83 | 84 | ## 另一种方式 85 | 86 | x64dbg hook sqlite3_key 87 | -------------------------------------------------------------------------------- /docs/view/db_file_analysis/collection.db.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: collection.db 3 | order: 5 4 | --- 5 | 6 | # collection.db 7 | 8 | ## `collection_list_info_table` 9 | 10 | 收藏信息 11 | 12 | | 列名 | 类型 | 含义 | 注 | 13 | | ------ | ----------------------------------------- | ----------------- | ----------------------------------------- | 14 | | 180001 | str | sid | 用于确定唯一性 | 15 | | 180008 | int | type | 详见[下表](#收藏类型),用于区分收藏类型 | 16 | | 180011 | int | last updated time | 最后编辑时间 | 17 | | 180004 | protobuf | Source Info | protobuf,详见[下表](#来源信息) | 18 | | 180009 | int | frist time | 创建/收藏时间 | 19 | | 180015 | protobuf | summary info | protobuf,摘要信息,详见[下表](#摘要信息) | 20 | 21 | 22 | 23 | #### 收藏类型 24 | 25 | | 值 | 注 | 26 | | ---- | ---------------------------- | 27 | | 1 | 来源于聊天记录(文本类消息) | 28 | | 2 | QQ笔记 | 29 | | 9 | 带有链接的聊天消息 | 30 | 31 | 32 | 33 | #### 来源信息 34 | 35 | | Field Number | 注 | 36 | | ------------ | ------------------------ | 37 | | 18504 | 收藏来源群号 | 38 | | 18505 | 群名称 | 39 | | 18506 | 发送者nt_uid | 40 | | 18501 | 发送者QQ号 | 41 | | 180503 | 备注名/群员名称/用户昵称 | 42 | 43 | #### 摘要信息 44 | 45 | | Field Number | 注 | 46 | | ----------------------------------------- | ------------------------------------------------- | 47 | | 181450 | 标题(可为空) | 48 | | 181452 | 外显描述内容 | 49 | | 181453
18504
180550
180553 | 图片信息
来源会话
图片URL
图片名称 | 50 | | 180561 | 图片本地缓存路径 | 51 | | 180610 | 文件保存位置 | 52 | 53 | 54 | 55 | ## `content_list_info_table` 56 | 57 | * 聊天记录和笔记类收藏才存在此信息 58 | 59 | | 列名 | 类型 | 含义 | 注 | 60 | | ------ | ----------------------------------------- | ----------------- | -------------------------------- | 61 | | 180700 | str | sid | 与180001对应,可用于确定收藏消息 | 62 | | 180720 | int | last updated time | 与180011对应,最后编辑时间 | 63 | | 180708 | protobuf | favorite content | protobuf,收藏实际内容 | 64 | 65 | -------------------------------------------------------------------------------- /docs/view/read_db.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: NTQQ 读取数据库 3 | prev: false 4 | order: 1 5 | --- 6 | 7 | # NTQQ 读取数据库 8 | 9 | 阅读本文前,您应当已经通过其他方法,获取到了数据库的已解密文件。如果没有,请参考:[NTQQ 解密数据库](/decrypt/decode_db.md)。 10 | 11 | 以下以 `nt_msg.db` 代指已解密的数据库文件。 12 | 13 | 目前已知消息格式为`protobuf`,较为复杂,可使用 [CyberChef](https://gchq.github.io/CyberChef/#recipe=Protobuf_Decode('',false,false)Decode_text('UTF-8%20(65001)')) 工具进行自动解析,相关解析代码可以参考[提取QQ NT数据库 group_msg_table 中的纯文本](https://github.com/QQBackup/ntdb-plaintext-extracter)、[这份 Python 代码](https://github.com/QQBackup/QQ-History-Backup/issues/9#issuecomment-1929105881)与[这份 protobuf 定义](https://github.com/QQBackup/qq-win-db-key/issues/38#issuecomment-2294619828),完整实现暂无,欢迎贡献。 14 | 15 | ## db文件内容说明 16 | 17 | /data/user/0/com.tencent.mobileqq/databases/nt_db/nt_qq_{QQ_path_hash}/路径下db文件分析 18 | | 是否完成分析 | 数据库名字 | 分析 | 19 | | ------------ | ------------------------------------------------------------ | -------------------------------------------- | 20 | | 🤔 | nt_msg.db | 聊天数据文件 | 21 | | ✅ | profile_info.db | 联系人信息 | 22 | | 🤔 | rich_media.db | 群聊或私聊发送/接收的文件信息存贮路径 | 23 | | ✅ | files_in_chat.db | 媒体文件信息(包括下载的图片视频路径) | 24 | | 🤔 | recent_contact.db | (推测为黑名单,待测试) | 25 | | ❓ | gpro_v1-6_{nt_uid}.db | (由于暂未实现数据库解密无法分析) | 26 | | ✅ | group_info.db | 群聊信息 | 27 | | 🤔 | guild_msg.db | 频道聊天数据 | 28 | | ✅ | collection.db | QQ收藏数据 | 29 | | 🤔 | file_assistant.db | 已下载文件存放数据 | 30 | | 🤔 | misc.db | 见下表 | 31 | | ✅ | emoji.db | (存贮QQ表情包的数据库) | 32 | | ✅ | group_msg_fts.db | 本地搜索使用的数据库 | 33 | | ✅ | data_line_msg_fts.db| 本地搜索使用的数据库 | 34 | | ✅ | buddy_msg_fts.db | 本地搜索使用的数据库 | 35 | | ✅ | discuss_msg_fts.db | 本地搜索使用的数据库 | 36 | | ✅ | msg_fts.db | 本地搜索使用的数据库 | 37 | | ✅ | ~~rdelivery.db~~ | (文件中未发现有效信息) | 38 | | ✅ | ~~settings.db~~ | (无法理解的设置信息,有效信息很少,不再分析) | 39 | | ✅ | ~~yffm.db~~ | (文件中未发现有效信息) | 40 | 41 | 42 | # 注: 43 | ~~已被删除~~是在本人数据库中未发现有意义的数据,因此后续不再探查 44 | 蓝色字体是存在有效信息的数据库,有待继续分析 45 | 的是已完成对表名的分析(列名会单独重开分析) 46 | 当然如果你发现你号对应的db中含有有价值的数据也欢迎提出,再分析……( 47 | 48 | 49 | 50 | ## 社区项目 51 | 52 | 网站收集了互联网上其他有关解析 NTQQ 数据库的开源项目,具体可在[这里](/about/projects)查看 -------------------------------------------------------------------------------- /docs/public/files/android_get_backup_key.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import frida 3 | import sys 4 | import platform 5 | import os 6 | import functools 7 | import subprocess 8 | 9 | # OPTIONS 10 | 11 | PACKAGE = "com.tencent.mobileqq" 12 | 13 | # OPTIONS END 14 | 15 | @functools.cache 16 | def isOnTermux() -> bool: 17 | if ( 18 | platform.system() == "Linux" 19 | and "ANDROID_ROOT" in os.environ.keys() 20 | and ( 21 | os.path.exists("/data/data/com.termux") 22 | or ("TERMUX_VERSION" in os.environ.keys()) 23 | ) 24 | ): 25 | return True 26 | return False 27 | 28 | 29 | generalident = "FF 03 02 D1 F8 5F 04 A9 F6 57 05 A9 F4 4F 06 A9 FD 7B 07 A9 FD C3 01 91 58 D0 3B D5 08 17 40 F9 F3 03 00 AA F4 03 01 AA E8 1F 00 F9 64 2A 40 B9 E4 04 00 34 68 1A 40 F9" 30 | funcident = { 31 | "8.9.76": generalident, 32 | } 33 | 34 | 35 | if __name__ == "__main__": 36 | if len(sys.argv) != 2 or sys.argv[1] not in funcident: 37 | print("usage: qq.version.number") 38 | print("supported version:", *funcident.keys()) 39 | sys.exit(1) 40 | 41 | print("仍在测试。") 42 | print("请先关闭 Magisk Hide 与 Shamiko") 43 | print("请先禁用 SELinux") 44 | print( 45 | "请先打开 QQ 并登录,进入主界面,然后运行该脚本,等待数秒后退出登录并重新登录。" 46 | ) 47 | print("若失败,可尝试彻底关闭 QQ 后直接运行") 48 | print("理论支持 Termux 与 桌面操作系统 运行") 49 | print("请勿使用 x86 或 x64 系统上的安卓模拟器。") 50 | print( 51 | """Termux 环境具体命令: 52 | sudo friendly # 重命名后的 frida-server 53 | python android_hook.py 54 | """ 55 | ) 56 | 57 | if isOnTermux(): 58 | device = frida.get_remote_device() 59 | pid_command = f"su -c 'pidof {PACKAGE}'" 60 | else: 61 | device = frida.get_usb_device() 62 | pid_command = f"adb shell su -c 'pidof {PACKAGE}'" 63 | running = True 64 | try: 65 | pid = int( 66 | subprocess.check_output(pid_command, shell=True) 67 | .decode() 68 | .strip() 69 | .split(" ")[0] 70 | ) 71 | except: 72 | running = False 73 | with open("android_get_backup_key.js", "rb") as f: 74 | jscode1 = f.read().decode() 75 | jscode1 = jscode1.replace("__single_function__parameter__", funcident[sys.argv[1]]) 76 | if running: 77 | print(PACKAGE + " is already running", pid) 78 | session = device.attach(pid) 79 | script = session.create_script(jscode1) 80 | else: 81 | pid = device.spawn([PACKAGE]) 82 | session = device.attach(pid) 83 | script = session.create_script(jscode1) 84 | device.resume(pid) 85 | print("QQ running!! pid = %d" % pid) 86 | 87 | def on_message(message, data): 88 | if message["type"] == "send": 89 | toprint = message["payload"] 90 | else: 91 | toprint = message 92 | toprint = str(toprint) 93 | # toprint=str(list(toprint)) 94 | print(toprint) 95 | 96 | script.on("message", on_message) 97 | script.load() 98 | print("Frida script injected.") 99 | sys.stdin.read() 100 | -------------------------------------------------------------------------------- /docs/public/files/ios_get_key.js: -------------------------------------------------------------------------------- 1 | // frida [-U/-R/-H/-D] QQ -l ios_get_key.js 2 | 3 | const ModuleName = "QQ"; 4 | 5 | // QQ(iOS) v9.0.1.620 6 | // SQLCipher v4.5.1 7 | const SQLLiteKeyV2Offset = 0xDA1BFB4; 8 | 9 | const sqlLiteKeyV2Addr = Module.findBaseAddress(ModuleName).add(SQLLiteKeyV2Offset); 10 | 11 | /** 12 | * @param {Array} buffer 13 | * @returns {string} 14 | */ 15 | function buf2hex(buffer) { 16 | const byteArray = new Uint8Array(buffer); 17 | const hexParts = []; 18 | byteArray.forEach(value => { 19 | const hex = value.toString(16); 20 | const paddedHex = ('00' + hex).slice(-2); 21 | hexParts.push(paddedHex); 22 | }) 23 | return '0x' + hexParts.join(', 0x'); 24 | } 25 | 26 | /** 27 | * @param {Array} buffer 28 | * @returns {string} 29 | */ 30 | function buf2str(buffer) { 31 | let result = ""; 32 | const byteArray = new Uint8Array(buffer); 33 | byteArray.forEach(value => { 34 | result += String.fromCharCode(value); 35 | }) 36 | return result; 37 | } 38 | 39 | /** 40 | * @param {Object} sqlite3 - Database connection (struct sqlite3) 41 | * {@link https://github.com/sqlcipher/sqlcipher/blob/2c672e7dd1f3dee4aa1af0b5bf29092db4b10f78/src/sqliteInt.h#L1513-L1655} 42 | * @returns {string} Name of the database file 43 | */ 44 | function getFilenameFromDB(sqlite3) { 45 | let result = ""; 46 | try { 47 | let db = sqlite3.add(0x8 * 5).readPointer(); // All backends (Db *) 48 | let pBt = db.add(0x8).readPointer(); // The B*Tree structure for this database file (Btree *) 49 | let pBt2 = pBt.add(0x8).readPointer(); // Sharable content of this btree (BtShared *) 50 | let pPager = pBt2.add(0x0).readPointer(); // The page cache (Pager *) 51 | let zFilename = pPager.add(208).readPointer(); // Name of the database file (char *) 52 | result = zFilename.readCString(); 53 | } catch (e) {} 54 | return result; 55 | } 56 | 57 | /* 58 | int sqlite3_key_v2(sqlite3 *db, const char *zDb, const void *pKey, int nKey); 59 | */ 60 | Interceptor.attach(sqlLiteKeyV2Addr, { 61 | /** 62 | * @param {array} args 63 | */ 64 | onEnter: function (args) { 65 | const dbPtr = args[0]; 66 | const zDbPtr = args[1]; 67 | const pKeyPtr = args[2]; 68 | const nKeyPtr = args[3]; 69 | 70 | const nKey = nKeyPtr.toInt32(); 71 | const pKeyByteArray = pKeyPtr.readByteArray(nKey) 72 | const pKey = buf2str(pKeyByteArray) 73 | const pKeyHex = buf2hex(pKeyByteArray) 74 | const zDb = zDbPtr.readUtf8String(); 75 | 76 | const zFilename = getFilenameFromDB(dbPtr) 77 | 78 | const zFilenameParts = zFilename.split("/") 79 | const dirName = zFilenameParts[zFilenameParts.length - 3] 80 | const dbName = zFilenameParts[zFilenameParts.length - 1] 81 | if (dirName === "nt_db" || dbName === "nt_msg.db") { 82 | console.log(`¦- db: ${dbPtr}`); 83 | console.log(`¦- *zDb: ${zDb}`); 84 | console.log(`¦- *pkey: ${pKey}`); 85 | console.log(`¦- *pkey-hex: ${pKeyHex}`); 86 | console.log(`¦- nKey: ${nKey}`); 87 | console.log(`¦+`); 88 | console.log(`¦- zFilename: ${zFilename}`); 89 | console.log("+------------"); 90 | } 91 | } 92 | }); 93 | -------------------------------------------------------------------------------- /docs/decrypt/NTQQ (macOS ARM).md: -------------------------------------------------------------------------------- 1 | --- 2 | title: NTQQ (macOS ARM) 3 | order: 4 4 | --- 5 | 6 | # NTQQ (macOS ARM) 7 | 本文发表于 [冷月的博客](https://lengyue.me/2023/09/19/ntqq-db/), 基于 CC BY-NC-SA 4.0 共享。 8 | 9 | ## 0. 引言 10 | 11 | 为了让每个人都可以把自己练入 LLM, 制作自己的数字分身。 解析 QQ 数据库无疑是快速获得语料库的最佳途径。 然而, 众所周知, QQ 的数据库是加密的 SQLite 数据库, 且不幸的是, 在最新的 NTQQ 中数据库的加密方式已经发生了变化。 本文将介绍如何解析 Mac 的 NTQQ 数据库。 12 | 13 | 参考资料 (win): [QQBackup/qq-win-db-key](https://github.com/QQBackup/qq-win-db-key/blob/master/%E6%95%99%E7%A8%8B%20-%20NTQQ%20(Windows).md) 14 | 15 | 该方案于 2023 年 9 月 19 日在 NTQQ 6.9.17 上测试通过。 严禁用于非法用途。 16 | 17 | ## 1. 准备工作 18 | 19 | 在开始解析之前, 我们需要准备一些工具: 20 | 21 | - [NTQQ](https://im.qq.com/macqq/index.shtml) 22 | - [DB Browser for SQLite](https://sqlitebrowser.org/dl/) 23 | - LLDB(macOS 自带,注意需要关闭 SIP) 24 | - [Hopper Disassembler](https://www.hopperapp.com/download.html) 25 | 26 | 因为我们的流程非常简单, 免费版的 Hopper 即可满足需求。 27 | 28 | ## 2. 分析 29 | 30 | 你需要先准备一个自己喜欢的工作目录, 并且将 NTQQ 的 Library 复制到当前目录下: 31 | 32 | ```bash 33 | cp /Applications/QQ.app/Contents/Resources/app/wrapper.node . 34 | ``` 35 | 36 | 随后, 你需要使用 Hopper 打开 `wrapper.node` 在 Mac M1/M2 上需要选择 `aarch64`, 并且搜索 `nt_sqlite3_key_v2`。 37 | 38 | ![1](/img/image-mac-1.webp) 39 | ![2](/img/image-mac-2.webp) 40 | ![3](/img/image-mac-3.webp) 41 | 42 | 如上图所示, 我们可以跳转到引用该函数的地方, 随后记下该函数地址: 43 | 44 | ![4](/img/image-mac-4.webp) 45 | 46 | ## 3. 断点 & 调试 47 | 48 | 随后我们运行 NTQQ, 找到它的进程 ID, 并且使用 LLDB 进行调试: 49 | 50 | ```bash 51 | ❯ ps aux | grep QQ 52 | user 78488 1.5 0.5 1584651520 162000 ?? S 1:59PM 0:00.61 /Applications/QQ.app/Contents/MacOS/QQ 53 | ``` 54 | 55 | ```bash 56 | lldb -p 78488 57 | ``` 58 | 59 | 我们需要寻找 `wrapper.node` 的加载地址: 60 | 61 | ```bash 62 | (lldb) image list -o -f | grep /Applications/QQ.app/Contents/Resources/app/wrapper.node 63 | [ 0] 0x0000000110088000 /Applications/QQ.app/Contents/Resources/app/wrapper.node 64 | ``` 65 | 66 | 接下来进行一个简单的数学运算, 计算出 `nt_sqlite3_key_v2` 的地址: 67 | 68 | ```bash 69 | (lldb) expr 0x0000000110088000 + 0x000000000192bef8 70 | (unsigned long) $0 = 4590354168 71 | ``` 72 | 73 | 设置断点并且继续运行: 74 | 75 | ```bash 76 | (lldb) br s -a 4590354168 77 | Breakpoint 1: where = wrapper.node`___lldb_unnamed_symbol287604, address = 0x00000001119b3ef8 78 | 79 | (lldb) c 80 | Process 78488 resuming 81 | ``` 82 | 83 | 点击登录后, 如无意外, 你会看到断点被命中, 并且进入了 `nt_sqlite3_key_v2` 函数, 如下图所示: 84 | 85 | ![5](/img/image-mac-5.webp) 86 | 87 | 参考函数签名: 88 | 89 | ```c 90 | int sqlite3_key_v2( 91 | sqlite3 *db, /* Database to be keyed, x0 */ 92 | const char *zDbName, /* Name of the database, x1 */ 93 | const void *pKey, int nKey /* The key, x2, x3 */ 94 | ); 95 | ``` 96 | 97 | 接下来解析 16 个字符即可: 98 | 99 | ```plaintext 100 | (lldb) register read x2 101 | x2 = 0x0000012801b34010 102 | 103 | (lldb) memory read --format c --count 16 --size 1 0x0000012801b34010 104 | 0x12801b34010: L7LA=idk17,fn~uk 105 | ``` 106 | 107 | 至此, 我们已经成功解析出了 NTQQ 的数据库密钥。 108 | 109 | ## 4. 解密 110 | 111 | 数据库位于 (注意 MD5 可能会随着 QQ 的版本更新而改变): 112 | 113 | ```plaintext 114 | /Users/user/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/nt_qq_{MD5}/nt_db 115 | ``` 116 | 117 | 复制你需要的文件, 如 `profile_info.db`: 118 | 119 | ```bash 120 | cp "/Users/user/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/nt_qq_cc067b8bcbf8980fabd93574e09d9efa/nt_db/profile_info.db" test.db 121 | ``` 122 | 123 | 对于解密数据库, 请参考 [NTQQ 解密数据库](decode_db.md)。 124 | 125 | 出于隐私考虑, 不展示解密后的数据库内容。 126 | 127 | ## 5. 总结 128 | 129 | 本文介绍了如何解析 NTQQ 的数据库, 以及如何使用 DB Browser for SQLite 浏览数据库。 130 | 需要注意的是, 数据库结构仍需分析, 本文仅仅是提供了解密的方法。 -------------------------------------------------------------------------------- /docs/view/db_file_analysis/profile_info.db.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: profile_info.db 3 | order: 2 4 | --- 5 | 6 | # profile_info.db 7 | | 完成解析 | 表名 | 分析 | 8 | | -------- | ------------------- | ------------------------------------------------------ | 9 | | ✔ | buddy_list | 好友相关信息 | 10 | | ✔ | buddy_req_list_5 | 好友通知 | 11 | | ✔ | category_list_v2 | 好友分组信息 | 12 | | ✔ | profile_info_adelie | 机器人信息 | 13 | | ✔ | profile_info_v6 | QQ用户信息存贮(包括QID,uin,用户名,个性签名等信息) | 14 | 15 | ## `buddy_list` 16 | 好友相关信息 17 | 18 | | 列名 | 类型 | 含义 | 说明 | 19 | | ----- | ---- | -------- | ------------------------------------ | 20 | | 1000 | str | nt_uid | | 21 | | 1001 | str | QID | | 22 | | 1002 | int | uin | | 23 | | 25007 | int | 分组标识 | 当在默认分组(哪怕已改名)中值为null | 24 | 25 | ## `buddy_req_list_5` 26 | 好友通知 27 | 28 | | 列名 | 类型 | 含义 | 说明 | 29 | | ----- | ---- | ------------ | -------------------------------------------- | 30 | | 21204 | int | 时间戳 | UTC+8:00 | 31 | | 21001 | str | 对方nt_uid | | 32 | | 20002 | str | 用户昵称 | | 33 | | 21502 | int | 申请是否通过 | 0为通过,1为验证中 | 34 | | 21508 | str | 验证消息 | | 35 | | 21509 | str | 申请来源 | | 36 | | 21505 | int | 验证状态 | 1为等待通过,2为同意验证,13为验证过期(?) | 37 | | 60001 | int | 来源群号 | | 38 | | 21501 | int | 申请方标识 | 1为他人发送的申请,0为自己发送的申请 | 39 | 40 | ## `category_list_v2` 41 | 好友分组信息 42 | 43 | 其中25011为`protobuf`格式,代表着分组 44 | 45 | | **Field Number** | 含义 | 说明 | 46 | | ---------------- | -------- | ----------------------------------------------- | 47 | | 25007 | 分组序号 | 0为默认分组 | 48 | | 25008 | 分组名称 | | 49 | | 25010 | 分组人数 | As uint(该分组总人数)/As sint(当前在线人数) | 50 | 51 | `profile_info_adelie`:机器人信息 52 | 53 | ## `profile_info_v6` 54 | QQ用户信息 55 | 56 | | 列名 | 类型 | 含义 | 说明 | 57 | | ----- | ----------------------------------------- | ---------------- | ---------------------------- | 58 | | 1001 | str | QID | 设置了才有否则为null | 59 | | 1002 | int | uin | | 60 | | 20002 | str | 用户昵称 | | 61 | | 20009 | str | 用户备注 | | 62 | | 20011 | str | 个性签名 | | 63 | | 1000 | str | nt_uid | | 64 | | 20004 | str | 头像链接 | s后面需要带参数,如s=640/100 | 65 | | 21000 | protobuf | "尊贵身份标识符" | protobuf | 66 | | 20072 | -- | 是否为好友 | hex值为`c2e60900`表示为好友 | 67 | | 24105 | protobuf | 个性签名校验值 | | 68 | 69 | -------------------------------------------------------------------------------- /docs/view/message_export.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 消息导出计划 3 | order: 3 4 | --- 5 | 6 | # 消息导出计划 7 | 8 | ## 关于聊天图片本地缓存路径 9 | 10 | 11 | 12 | QQ的图片缓存路径位于`/storage/emulated/0/Android/data/com.tencent.mobileqq/Tencent/MobileQQ/chatpic`,以下用`./`代指 13 | 14 | 这个目录下包含三个文件夹: 15 | 16 | - `chatraw`:原图 17 | - `chatimg`:压缩后的普通图片 18 | - `chatthumb`:缩略图,聊天界面的小图预览 19 | 20 | ::: details 关于接收图片存放逻辑 21 | pic_categorized 22 | 23 | ①类图片会先将压缩过的图片存放至`chatimg`文件夹,在用户**手动点击下载原图**后将原图保存至`chatraw`文件夹中 24 | 25 | ②③类会将图片放至`charaw`文件夹 26 | 27 | ④类会将GIF本体保存在`chatraw`文件夹,静态预览图放在`chatimg`文件夹 28 | 29 | 所有类型都将缩略图存放在`chattumb`文件夹 30 | 31 | 32 | | | chatraw | chaimg | chattumb | 33 | | ---- | ------- | ------ | -------- | 34 | | ① | ⬜ | ✔ | ✔ | 35 | | ② | ✔ | ❌ | ✔ | 36 | | ③ | ✔ | ❌ | ✔ | 37 | | ④ | ✔ | ✔ | ✔ | 38 | 39 | ⬜需手动触发下载 ✔存在 ❌不存在 40 | 41 | ::: 42 | 43 | ::: details 发送表情的分类 44 | 45 | QQ中的表情元素可分为四类 46 | 47 | | 类型 | 所属ElementType | 说明 | 48 | | ----------- | ----------------- | ---------------------- | 49 | | ①QQ系统表情 | FaceElement | 超级表情与小黄脸表情 | 50 | | ②emoji表情 | TextElement | 包括手机系统的原生表情 | 51 | | ③收藏表情 | PicElement | 用户自主收藏的表情 | 52 | | ④原创表情 | marketFaceElement | 表情商城中的表情 | 53 | 54 | ①类表情属于特殊Element,其公共表情可通过`faceid`在[QQBOT文档](https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html)中查询,在一些活动中QQ官方会增加**隐藏表情**,其`faceid`未被公开,但可通过已发送消息推断 55 | 56 | ②类表情将以文本形式发送(所以有时出现显示异常) 57 | 58 | ③类表情以图片形式发送 59 | 60 | ④类表情属于特殊Element,文件缓存在`.emotionsm`文件夹中 61 | 62 | ::: 63 | 64 | >[!TIP]说明 65 | >关于发送是否为原图,可通过查询`original`(对应40080的Field Number=45418)判断 66 | > 67 | >0为非原图,1为原图 68 | 69 | ### 路径生成规律 70 | 71 | 在图片消息的40080 值中,Field Number`45406`称作 `md5HexStr` 值(32位小写),以下称为 ``{MD5}``,**所需格式为32位大写** 72 | 73 | 将目标文件夹名与 `{MD5}` 拼接,格式: 74 | ``` 75 | chatimg:{MD5} 76 | ``` 77 | 对拼接好的字符串执行 CRC64 哈希,使用固定多项式: 78 | ``` 79 | 0x9A6C9329AC4BC9B5 80 | ``` 81 | 得到一个 64 位整数。 82 | 83 | 将该整数转 16 进制,去掉 `0x` 前缀,并在前面加上 `Cache_`,得到文件名,并将文件名最后三位作为子文件夹名称 84 | 85 | 例如:Cache_d20372e27ef63b0 普通图片位于./chatimg/3b0/Cache_d20372e27ef63b0 86 | 87 | 若需原图路径,只需将代码中的 `chatimg` 替换为 `chatraw`,再进行CRC64运算即可,`chatthumb`同理 88 | 89 | 示例脚本代码 90 | 91 | ::: code-group 92 | 93 | ````python [Python] 94 | import os 95 | 96 | def crc64(s): 97 | _crc64_table = [0] * 256 98 | for i in range(256): 99 | bf = i 100 | for _ in range(8): 101 | bf = bf >> 1 ^ -7661587058870466123 if bf & 1 else bf >> 1 102 | _crc64_table[i] = bf 103 | v = -1 104 | for c in s: 105 | v = _crc64_table[(ord(c) ^ v) & 255] ^ v >> 8 106 | return v 107 | 108 | def get_img_path(md5, folder): 109 | url = f"{folder}:{md5}" 110 | filename = 'Cache_' + hex(crc64(url)).replace('0x', '') 111 | return os.path.join(f"./{folder}/", filename[-3:], filename).replace("\\", "/") 112 | ::: 113 | 114 | ## 已挖到的api 115 | - 群头像 116 | 117 | api地址 118 | ``` 119 | https://p.qlogo.cn/gh/{random}/{groupid}/{s} 120 | ``` 121 | 其中`groupid`为群号,`{random}`可为任意值不影响最终查询,在`groupid`后加_blank{order},可以获取历史头像 122 | 123 | 124 | `s`为头像大小,目前发现tx提供有40、100、140、640几种,当值为`0`时为原始图片大小,也可不附加此参数,默认返回等同参数`0` 125 | 126 | 例子 127 | ``` 128 | https://p.qlogo.cn/gh/0/9********1_1/140 129 | ``` 130 | 获取群号为`9********1`大小为140x140的群头像 131 | - ⬜ 群名称api 132 | - 用户头像 133 | 134 | api地址 135 | ``` 136 | https://q1.qlogo.cn/g?b=qq&nk={uin}&s={s} 137 | ``` 138 | 其中`{uin}`为QQ号,`{s}`值用法同上,**必须携带有效s值才可查询** 139 | - ⬜ 用户名称api 140 | 141 | 142 | 以上信息或许会从本地进行获取 143 | -------------------------------------------------------------------------------- /docs/decrypt/decode_db.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: NTQQ 解密数据库 3 | next: false 4 | order: 11 5 | --- 6 | 7 | # NTQQ 解密数据库 8 | 阅读本文前,您应当已经通过其他方法,获取到了数据库的密钥。如果没有,请先阅读对应平台的教程。 9 | 10 | (目前)数据库密钥在代码中表示为`pKey`,其值为32位可见字符,例如`abcd1234.,.,ABCD1234567812345678`,以下以`pass`替代。密码在每次打开数据库时都可能改变,您应当在获取密码前、获取密码后均备份一份数据库,并对两份数据库均尝试使用获得的密码解密。 11 | 12 | (目前)加密使用SQLCipher,并更改了一些参数。 13 | 14 | 本教程适用于`nt_msg.db`或`group_msg_fts.db`,也可能可以用于其它部分数据库,以下统一以`nt_msg.db`代替。 15 | 16 | ## 环境 17 | 18 | 建议使用 非 Windows 环境。 19 | 20 | 以下标有`仅 Linux`字样的即代表不可直接使用 Windows 系统的`CMD`或`Powershell`等执行。 21 | 22 | 若为 Windows,可使用 WSL / Git Bash 等模拟 Linux 环境;若为 Android,可使用 Termux 模拟 Linux 环境。 23 | 24 | ## 移除无关文件头 25 | 26 | 由于`nt_msg.db`文件前面有 1024 字节的文件头,导致通常的 SQLite 无法识别。为了使 SQLite 支持读写此类数据库,需要使用以下两种方案中任意一种。 27 | 28 | `复制至新文件`方法:优点为可以直接得到一个正常的 SQLite 3 数据库,缺点为需要完整读写整个数据库,导致存储空间占用、存储设备损耗,并且不能直接读写QQ数据目录下的数据库文件。 29 | 30 | `使用VFS扩展`方法:解决了`复制至新文件`方法的缺点,但是需要在打开数据库时进行额外操作。 31 | 32 | ### 复制至新文件 33 | 34 | 首先,将`nt_msg.db`文件删除前1024字节,这可以通过以下方式完成: 35 | 36 | 使用二进制编辑器:Android 下的 [MT 管理器](https://d.binmt.cc/)(需要付费)、Windows 下的 [HxD](https://mh-nexus.de/en/hxd/) 等软件均可使用,细节从略。 37 | 38 | 使用`tail`命令(仅 Linux):`tail -c +1025 nt_msg.db > nt_msg.clean.db` 39 | 40 | 使用 Python:`python -c "open('nt_msg.clean.db','wb').write(open('nt_msg.db','rb').read()[1024:])"` 41 | 42 | 完成后,得到`nt_msg.clean.db`文件。 43 | 44 | ### 使用VFS扩展 45 | 46 | 对于详细教程,请参考[此文档](https://github.com/artiga033/ntdb_unwrap/tree/main/sqlite_extension#%E7%94%A8%E6%B3%95),以下只介绍大概流程。 47 | 48 | 首先,下载对应平台的动态链接库文件,以下假设文件名为`libsqlite_ext_ntqq_db.so`。不同平台下的文件名可能不同,请在以下流程中使用对应文件名。请勿重命名该文件。 49 | 50 | 若使用`sqlcipher`命令行,在读取数据库阶段,在运行`.open nt_msg.db`之前,运行`.load libsqlite_ext_ntqq_db.so`。 51 | 52 | 若使用`DB Browser for SQLite`等图形化界面,首先执行`新建内存数据库`,再执行`工具->加载扩展`,选择`libsqlite_ext_ntqq_db.so`。成功加载后,再打开`nt_msg.db`即可。 53 | 54 | 与上述扩展原理相似的还有[此扩展](https://github.com/zqhong/sqlite_header_vfs),基于 C 语言(而非上述扩展使用的 Rust)。二者效果相同,使用方式略有差别,可自行阅读对应文档并尝试使用。 55 | 56 | 57 | ## 打开数据库 58 | 59 | 打开数据库可以通过 [SQLiteStudio](https://sqlitestudio.pl/)、[DB Browser for SQLite](https://sqlitebrowser.org/) 或`sqlcipher`命令行 等工具完成。 60 | 61 | ::: tip 如打开就报错file is not a database 62 | 请注意DB Browser应使用DB Browser (SQLCipher)而非DB Browser (SQLite) 63 | ::: 64 | ## 通用配置选项 65 | 66 | 见下。注意`cipher_hmac_algorithm`在一些版本中可能为`HMAC_SHA256`,以下部分截图也使用了此值,请自行尝试更改。 67 | 68 | ```shell 69 | PRAGMA key = 'pass'; -- pass 替换为之前得到的密码(32字节字符串) 70 | PRAGMA cipher_page_size = 4096; 71 | PRAGMA kdf_iter = 4000; -- 非默认值 256000 72 | PRAGMA cipher_hmac_algorithm = HMAC_SHA1; -- 非默认值(见上文) 73 | PRAGMA cipher_default_kdf_algorithm = PBKDF2_HMAC_SHA512; 74 | PRAGMA cipher = 'aes-256-cbc'; 75 | ``` 76 | 77 | ### DB Browser for SQLite 78 | 79 | 选择 `nt_msg.clean.db`,按需修改`KDF iterations`与`HMAC algorithm`(见上文[通用配置选项](#通用配置选项)修改): 80 | 81 | ![3](/img/image-mac-sqlcipher-conf.webp)“SQLCipher 加密”窗口中的具体配置选项(英文,macOS 视图)" width="70%" /> 82 | 83 | ![“SQLCipher 加密”窗口中的具体配置选项(中文,Windows 视图)](/img/image-win-sqlcipher-conf.webp) 84 | 85 | ![4](/img/image-mac-6.webp)DB Browser for SQLite 中,正常打开数据库后可看到 buddy_list 等表" width="70%" /> 86 | 87 | ### SQLiteStudio 88 | 89 | 数据库类型选 SQLCipher,密码(密钥)为空,加密算法配置(可选)输入上文[通用配置选项](#通用配置选项)中内容。 90 | 91 | ### sqlcipher CLI 92 | 93 | > 参考资料:[找到了Linux QQ NT聊天记录数据库密钥](https://gist.github.com/bczhc/c0f29920d4e9d0cc6d2c49f7f2fb3a78) 94 | 95 | 对于Linux,从包管理器安装`sqlcipher`包;对于Windows,从[QQBackup/sqlcipher-github-actions](https://github.com/QQBackup/sqlcipher-github-actions/releases/tag/latest)下载SQLCipher可执行文件。 96 | 97 | 以下命令适用于Linux环境下的Bash等shell,Windows下可使用Git Bash等代替。 98 | 99 | `sqlcipher nt_msg.clean.db "pragma key = 'pass'; pragma kdf_iter = 4000; pragma cipher_hmac_algorithm = HMAC_SHA1;" .d | tail +2 | sqlite3 nt_msg.decrypt.db` 100 | 101 | ## 读取数据库内容 102 | 103 | 请参考 [NTQQ 读取数据库](/view/read_db)。 104 | -------------------------------------------------------------------------------- /docs/public/files/QQ_Offset.json: -------------------------------------------------------------------------------- 1 | { 2 | "9.9.7.21804": { 3 | "offsets": ["0x326E950", "0x4a27df8"], 4 | "contributor": "r4inb00w", 5 | "timestamp": "2025/08/11" 6 | }, 7 | "9.9.10.23873": { 8 | "offsets": ["0x359CB10", "0x4E82ED8"], 9 | "contributor": "r4inb00w", 10 | "timestamp": "2025/08/11" 11 | }, 12 | "9.9.12.25493": { 13 | "offsets": ["0x26A3880", "0x52E8091"], 14 | "contributor": "r4inb00w", 15 | "timestamp": "2025/08/11" 16 | }, 17 | "9.9.12.26466": { 18 | "offsets": ["0x26A7950", "0x52EDA61"], 19 | "contributor": "r4inb00w", 20 | "timestamp": "2025/08/11" 21 | }, 22 | "9.9.15.26909": { 23 | "offsets": ["0x2AA6BB0", "0x5A90A31"], 24 | "contributor": "r4inb00w", 25 | "timestamp": "2025/08/11" 26 | }, 27 | "9.9.15.27391": { 28 | "offsets": ["0x263F7E0", "0x54A0C11"], 29 | "contributor": "r4inb00w", 30 | "timestamp": "2025/08/11" 31 | }, 32 | "9.9.15.27597": { 33 | "offsets": ["0x25AE940", "0x5462331"], 34 | "contributor": "r4inb00w", 35 | "timestamp": "2025/08/11" 36 | }, 37 | "9.9.15.28131": { 38 | "offsets": ["0x25d0630", "0x548E971"], 39 | "contributor": "r4inb00w", 40 | "timestamp": "2025/08/11" 41 | }, 42 | "9.9.16.29271": { 43 | "offsets": ["0x261DF90", "0x55032A1"], 44 | "contributor": "r4inb00w", 45 | "timestamp": "2025/08/11" 46 | }, 47 | "9.9.17.30594": { 48 | "offsets": ["0x2703A10", "0x5748321"], 49 | "contributor": "r4inb00w", 50 | "timestamp": "2025/08/11" 51 | }, 52 | "9.9.17.30899": { 53 | "offsets": ["0x270AB90", "0x5781FA1"], 54 | "contributor": "r4inb00w", 55 | "timestamp": "2025/08/11" 56 | }, 57 | "9.9.17.31363": { 58 | "offsets": ["0x2745C70", "0x57DAC41"], 59 | "contributor": "r4inb00w", 60 | "timestamp": "2025/08/11" 61 | }, 62 | "9.9.18.32690": { 63 | "offsets": ["0x2762620", "0x58245D1"], 64 | "contributor": "r4inb00w", 65 | "timestamp": "2025/08/11" 66 | }, 67 | "9.9.18.32869": { 68 | "offsets": ["0x27637E0", "0x58175D1"], 69 | "contributor": "r4inb00w", 70 | "timestamp": "2025/08/11" 71 | }, 72 | "9.9.18.33139": { 73 | "offsets": ["0x26A7370", "0x5791AA1"], 74 | "contributor": "r4inb00w", 75 | "timestamp": "2025/08/11" 76 | }, 77 | "9.9.19-34362": { 78 | "offsets": ["0x27AAD90", ""], 79 | "contributor": "YisRime", 80 | "timestamp": "2025/08/17" 81 | }, 82 | "9.9.19.34740": { 83 | "offsets": ["0x27AD3F0", "0x5B52891"], 84 | "contributor": "r4inb00w", 85 | "timestamp": "2025/08/11" 86 | }, 87 | "9.9.19.35469": { 88 | "offsets": ["0x27BBBE0", "0x5B6FB51"], 89 | "contributor": "r4inb00w", 90 | "timestamp": "2025/08/11" 91 | }, 92 | "9.9.20.36330": { 93 | "offsets": ["0x2057640", ""], 94 | "contributor": "r4inb00w", 95 | "timestamp": "2025/08/11" 96 | }, 97 | "9.9.20.36580": { 98 | "offsets": ["0x2070A20", ""], 99 | "contributor": "r4inb00w", 100 | "timestamp": "2025/08/11" 101 | }, 102 | "9.9.20.37051": { 103 | "offsets": ["0x20A34E0", ""], 104 | "contributor": "r4inb00w", 105 | "timestamp": "2025/08/11" 106 | }, 107 | "9.9.20.37625": { 108 | "offsets": ["0x20A6E10", ""], 109 | "contributor": "shenapex", 110 | "timestamp": "2025/08/11" 111 | }, 112 | "9.9.21.38503": { 113 | "offsets": ["0x20C5BF0", ""], 114 | "contributor": "shenapex", 115 | "timestamp": "2025/08/17" 116 | } 117 | } -------------------------------------------------------------------------------- /docs/view/db_file_analysis/emoji.db.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: emoji.db 3 | order: 4 4 | --- 5 | 6 | # emoji.db 7 | ## `base_sys_emoji_table` 8 | QQ默认表情数据 9 | | 列名 | 类型 | 含义 | 说明 | 10 | | ----- | ----------------------------------------- | -------------------- | -------------------------------------------------------- | 11 | | 81211 | str | 表情ID | | 12 | | 81212 | str | 表情描述(外显文字) | | 13 | | 81218 | protobuf | protobuf格式 | 为81229与81230的下载链接内容 | 14 | | 81221 | int | 特殊类表情标识 | 0为正常,1为特殊 | 15 | | 81224 | int | 特殊限定类表情标识 | | 16 | | 81225 | int | 特殊限定类表情标识 | | 17 | | 81226 | int | EmojiType | 系统表情为1,emoji表情为2,动态可变表情为3(比如掷骰子) | 18 | | 81266 | int | 表情类型说明 | | 19 | | 81229 | str | 静态图片下载地址 | | 20 | | 81230 | str | APNG图片下载地址 | emoji 表情没有此链接 | 21 | 22 | `bottom_emoji_table`:protobuf格式,包含该账号下已收藏的原创表情数据 23 | 24 | `emoji_config_storage_table`:QQ系统表情数据 25 | 26 | `emoji_group_table`:包含表情ID说明(如 超级表情0-16) 27 | 28 | `emoji_misc_data_table`:QQ默认表情下载地址 29 | 30 | ## `fav_emoji_info_storage_table` 31 | QQ收藏表情数据 32 | 33 | | 列名 | 类型 | 含义 | 说明 | 34 | | ----- | ---- | -------------- | ------------------------------------------------------- | 35 | | 80002 | str | 文件名 | 格式为`{uin}_0_0_0_{MD5}_0_0` | 36 | | 80001 | int | 表情排序 | 倒序,即最新的收藏表情序号为0 | 37 | | 1002 | str | 收藏用户uin | | 38 | | 80012 | str | 本地存贮路径 | | 39 | | 80010 | str | 下载地址 | 格式为`hxxps://p.qpic.cn/qq_expression/{1002}/{8002}/0` | 40 | | 80011 | str | 表情MD5 | 32位大写 | 41 | | 80213 | int | 是否为原创表情 | 0为否,1为是 | 42 | | 80201 | str | 原创表情标识符 | 当80213为1时,显示为对应原创表情的标识符 | 43 | | 80202 | int | 原创表情包ID | 当80213为1时,显示对应原创表情包ID | 44 | | 80223 | str | 表情备注 | | 45 | | 80225 | str | 表情备注 | | 46 | 47 | ## `market_emoticon_package_table` 48 | 原创表情市场 49 | | 列名 | 含义 | 说明 | 50 | | ------ | -------- | ----------------------------------------------------------------------------------- | 51 | | 80943 | 原创表情 ID | 知道 ID 可在 ../{uin}/nt_qq/nt_data/Emoji/marketface/{ID} 找到已缓存的原创表情包 | 52 | | 80947 | 原创表情包名字 | | 53 | | 80948 | 表情包说明 | | 54 | 55 | ## `market_emoticon_table` 56 | 已下载的原创表情信息 57 | | 列名 | 含义 | 说明 | 58 | | ------ | ---------- | ---------------------------------------------- | 59 | | 80920 | 表情标识符 | | 60 | | 80943 | 原创表情包 ID | | 61 | | 80921 | 表情描述 | 外显文字 | 62 | 63 | -------------------------------------------------------------------------------- /docs/public/files/pcqq_get_key.py: -------------------------------------------------------------------------------- 1 | import frida 2 | import sys 3 | import psutil 4 | 5 | QQ_PID = None 6 | # GUI -> ['C:\\Program Files (x86)\\Tencent\\QQ\\Bin\\QQ.exe'] 7 | # 要hook的 -> ['C:\\Program Files (x86)\\Tencent\\QQ\\Bin\\QQ.exe', '/hosthwnd=2164594', '/hostname=QQ_IPC_{12345678-ABCD-12EF-9976-18373DEAB821}', '/memoryid=0', 'C:\\Program Files (x86)\\Tencent\\QQ\\Bin\\QQ.exe'] 8 | for pid in psutil.pids(): 9 | p = psutil.Process(pid) 10 | if p.name() == "QQ.exe" and len(p.cmdline()) > 1: 11 | QQ_PID = pid 12 | del p 13 | break 14 | 15 | if QQ_PID is None: 16 | print("QQ not launched. exit.") 17 | sys.exit(1) 18 | print("QQ pid is:", QQ_PID) 19 | demo_script = """ 20 | var pMessageBoxW = Module.findExportByName("user32.dll", 'MessageBoxA') 21 | var lpText = Memory.allocAnsiString("I'm New MessageBox"); 22 | var funMsgBox = new NativeFunction(pMessageBoxW, 'uint32',['uint32','pointer','pointer','uint32']); 23 | 24 | funMsgBox(0, ptr(lpText), ptr(lpText), 0); 25 | """ 26 | hook_script = """ 27 | function buf2hex(buffer) { 28 | const byteArray = new Uint8Array(buffer); 29 | const hexParts = []; 30 | for(let i = 0; i < byteArray.length; i++) { 31 | const hex = byteArray[i].toString(16); 32 | const paddedHex = ('00' + hex).slice(-2); 33 | hexParts.push(paddedHex); 34 | } 35 | return '0x' + hexParts.join(', 0x'); 36 | } 37 | 38 | const kernel_util = Module.load('KernelUtil.dll'); 39 | function single_function(pattern) { 40 | pattern = pattern.replaceAll("##", "").replaceAll(" ", "").toLowerCase().replace(/\\s/g,'').replace(/(.{2})/g,"$1 "); 41 | var akey_function_list = Memory.scanSync(kernel_util.base, kernel_util.size, pattern); 42 | if (akey_function_list.length > 1) { 43 | send("pattern FOUND MULTI!!") 44 | send(pattern) 45 | send(akey_function_list) 46 | send("!!exit") 47 | } 48 | if (akey_function_list.length == 0) { 49 | send("pattern NOT FOUND!!") 50 | send("!!exit") 51 | } 52 | return akey_function_list[0]['address']; 53 | } 54 | 55 | // const key_function = Module.findExportByName("KernelUtil.dll", 'sqlite3_key') 56 | const key_function = single_function("55 8b ec 56 6b 75 10 11 83 7d 10 10 74 0d 68 17 02 00 00 e8") 57 | const name_function = single_function("55 8B EC FF 75 0C FF 75 08 E8 B8 D1 02 00 59 59 85") 58 | // send(key_function) 59 | var funcName = new NativeFunction(name_function, 'pointer', ['pointer', 'pointer']); 60 | 61 | 62 | Interceptor.attach(key_function, { 63 | onEnter: function (args, state) { 64 | 65 | console.log("[+] key found:"); 66 | var dbName = funcName(args[0], NULL).readUtf8String(); 67 | if (dbName.replaceAll('/', '\\\\').split('\\\\').pop().toLowerCase() == 'Msg3.0.db'.toLowerCase() || false) { 68 | //console.log("¦- db: " + args[0]); 69 | console.log("¦- nKey: " + args[2].toInt32()); 70 | //console.log("¦- pkey: " + args[1]); 71 | console.log("¦- *pkey: " + buf2hex(args[1].readByteArray(args[2].toInt32()))); 72 | console.log("¦- dbName: " + funcName(args[0], NULL).readUtf8String()); 73 | //console.log("¦- *pkey: " + buf2hex(Memory.readByteArray(new UInt64(args[1]), args[2]))); 74 | } 75 | }, 76 | 77 | onLeave: function (retval, state) { 78 | } 79 | 80 | }); 81 | """ 82 | 83 | session = frida.get_local_device().attach(QQ_PID) 84 | script = session.create_script(hook_script) 85 | 86 | 87 | def on_message(message, data): 88 | if message["type"] == "send": 89 | if message["payload"] == "!!exit": 90 | exit(3) 91 | print(message["payload"]) 92 | elif message["type"] == "error": 93 | print(message["stack"]) 94 | 95 | 96 | def on_destroyed(): 97 | print("process exited.") 98 | sys.exit(0) 99 | 100 | 101 | script.on("message", on_message) 102 | script.on("destroyed", on_destroyed) 103 | script.load() 104 | print("hooked.") 105 | sys.stdin.read() 106 | -------------------------------------------------------------------------------- /docs/public/files/android_hook_md5.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import frida 3 | import sys 4 | import platform 5 | import os 6 | import functools 7 | import subprocess 8 | 9 | # OPTIONS 10 | 11 | PACKAGE = "com.tencent.mobileqq" 12 | 13 | # OPTIONS END 14 | 15 | @functools.cache 16 | def isOnTermux() -> bool: 17 | if ( 18 | platform.system() == "Linux" 19 | and "ANDROID_ROOT" in os.environ.keys() 20 | and ( 21 | os.path.exists("/data/data/com.termux") 22 | or ("TERMUX_VERSION" in os.environ.keys()) 23 | ) 24 | ): 25 | return True 26 | return False 27 | 28 | 29 | general_script = """ 30 | const module_name = "libbasic_share.so" 31 | 32 | function hook(){ 33 | /* 34 | * https://codeshare.frida.re/@oleavr/read-std-string/ 35 | * Note: Only compatible with libc++, though libstdc++'s std::string is a lot simpler. 36 | */ 37 | 38 | function readStdString (str) { 39 | const isTiny = (str.readU8() & 1) === 0; 40 | if (isTiny) { 41 | return str.add(1).readUtf8String(); 42 | } 43 | return str.add(2 * Process.pointerSize).readPointer().readUtf8String(); 44 | } 45 | 46 | var result_addr; 47 | 48 | const upd = { 49 | onEnter: function(args) { 50 | // console.log('Backtrace 1:\\n' + Thread.backtrace(this.context).map(DebugSymbol.fromAddress).join('\\n') + '\\n'); 51 | console.log('update param ->', args[1].readCString(args[2].toInt32())); 52 | // result_addr = args[1] 53 | // console.log("¦- *zDb: " + args[0].readCString()); 54 | } 55 | } 56 | const ggg = { 57 | onLeave: function(ret) {console.log('MD5:', readStdString(ret))} 58 | } 59 | // Interceptor.attach(Module.findBaseAddress('libkernel.so').add(0x1b394e0), ggg); 60 | 61 | //Interceptor.attach(Module.findExportByName(module_name, '_ZN4xpng8SHA1HashEPKhm'), ggg); 62 | //Interceptor.attach(Module.findExportByName(module_name, 'SHA256_Update'), ggg); 63 | Interceptor.attach(Module.findExportByName(module_name, '_ZN4xpng9MD5UpdateEPA88_cPKhm'), upd); 64 | ///////// Interceptor.attach(Module.findExportByName(module_name, '_ZN4xpng8MD5FinalEPNS_9MD5DigestEPA88_c'), ggg); 65 | Interceptor.attach(Module.findExportByName(module_name, '_ZN4xpng17MD5DigestToBase16ERKNS_9MD5DigestE'), ggg); 66 | //Interceptor.attach(Module.findExportByName('libxplatform.so', '_ZN2xp3md55CRC32EjPKhi'), ggg); 67 | } 68 | 69 | 70 | var hasHooked = false; 71 | console.log("Script loaded. Waiting for " + module_name + " to load..."); 72 | const dlopen_process = { 73 | onEnter: function (args) { 74 | this.path = Memory.readUtf8String(args[0]); 75 | if (0) console.log("Loading " + this.path); 76 | }, 77 | onLeave: function (retval) { 78 | if (this.path.indexOf(module_name) !== -1 && !hasHooked) { 79 | hasHooked = true; 80 | if (1) console.log("Hooked!!"); 81 | hook(); 82 | } 83 | }, 84 | }; 85 | 86 | try { 87 | Interceptor.attach(Module.findExportByName(null, "dlopen"), dlopen_process); 88 | } catch (err) {} 89 | try { 90 | Interceptor.attach( 91 | Module.findExportByName(null, "android_dlopen_ext"), 92 | dlopen_process 93 | ); 94 | } catch (err) {} 95 | 96 | """ 97 | 98 | if __name__ == "__main__": 99 | jscode = general_script 100 | 101 | if isOnTermux(): 102 | device = frida.get_remote_device() 103 | pid_command = f"su -c 'pidof {PACKAGE}'" 104 | else: 105 | device = frida.get_usb_device() 106 | pid_command = f"adb shell su -c 'pidof {PACKAGE}'" 107 | pid = device.spawn([PACKAGE]) 108 | session = device.attach(pid) 109 | script = session.create_script(jscode) 110 | device.resume(pid) 111 | print("QQ running!! pid = %d" % pid) 112 | 113 | def on_message(message, data): 114 | print(message) 115 | 116 | script.on("message", on_message) 117 | script.load() 118 | print("Frida script injected.") 119 | sys.stdin.read() 120 | -------------------------------------------------------------------------------- /docs/about/碎碎念.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 碎碎念 3 | description: 我的想法 4 | sidebar: false 5 | next: false 6 | prev: false 7 | editLink: false 8 | hidesidebar: true 9 | --- 10 | 11 | # TODO 12 | 13 | - 完成对教程的动图或完整视频制作 14 | - 美化主题 15 | - 尽可能将教程小白化 16 | 17 | # 碎碎念…… 18 | 19 | 可以说算是网站的更新日志?另外再夹杂着本人的胡言乱语(bushi 20 | 21 | ## 2025-11-21 22 | - 勉强更新一下下~ 23 | 24 | ## 2025-10-01 25 | - 国庆快乐~ 26 | - 项目又处于半荒废状态了…… 27 | 28 | ## 2025-08-14 29 | - 诺瓦生日快乐~ 30 | - 100star庆祝! 31 | 32 | ## 2025-08-02 33 | - 鸽了好久/_ \但是好累,啥也不想干…… 34 | - 想不到还会犯一些低级错误 35 | 36 | ## 2025-06-28 37 | - 想不到真的有人投稿欸ww 38 | 39 | ## 2025-06-07 40 | - 等有时间会去分析频道数据库滴 41 | 42 | ## 2025-06-06 43 | - 要高考了啊……祝25届考生顺利~ 44 | 45 | ## 2025-06-01 46 | - 儿童节快乐~ 47 | - 从现在开始探查element的字段含义啦,祝顺利( •̀ ω •́ )✧ 48 | - 看起来好多定义还是有一些问题,需要重新探查…… 49 | - 我宣布Gemini 2.5 Pro 比 ChatGPT4o 好用(*  ̄︿ ̄)GPT生成的代码算出来完全是错的……怎么改都纠正不了,Gemini一次就给出了可以运行正确的代码 50 | 51 | ## 2025-05-31 52 | - 端午节快乐~ 53 | - WW终于摸清本地图片缓存路径了,我真是个笨蛋,多项式早就确定没有变化了,结果败在了用小写MD5去计算,实际用的是的大写MD5 54 | - 将文件路径去中文化了^_~同时我也发现也不是大写字母就一定会好看hha 55 | 56 | ## 2025-05-18 57 | - 标记了所有发现数据的类型,并…对protobuf类型进行高亮 58 | - 在整理过程中我发现了一个~~绝望的事实~~,安卓端与win端上的数据部分表名不一致,即你有我没有,我甚至发现部分表名已经消失了?(。_。) 59 | - TODO一个也没完成(但是我确实也该考虑下学业了……~~那就把项目丢给Young-Lord当甩手掌柜吧~~ 60 | - 小小庆祝下 50 star ?^_^以及,这是第90次commit 61 | ## 2025-05-04 62 | - 发现手机上多了两个db文件`filebridge_download.db`和`filebridge_pc.db`,但是暂时没有发现用处是啥…… 63 | - 增加了`files_in_chat.db`与`rich_media.db`文件内容的解析 64 | 65 | ## 2025-05-01 66 | - 劳动节快乐~ 67 | - 完成了对收藏信息的解析……其实早就知道他是存贮收藏信息的数据库了,但是当时忙着头大不想分析…… 68 | 69 | ## 2025-04-25 70 | - 将近半个月没有更新的原因是……~~当然不是因为懒啦~~ 71 | - 拖了大半年的 nt_msg.db 文件解析终于算是搞定了 72 | - ~~200多kb的logo文件严重拖慢网站速度~~ 73 | 74 | ## 2025-04-04 75 | - `wrapper.node`文件的路径都变动好久了都没被发现……还是测试时发现路径对不上才知道的 76 | 77 | - 之前的图片缩放插件还是不太好用,换了一个……这个感觉不错 78 | 79 | - 虽然一直在尝试遵循[Conventional Commits](https://www.conventionalcommits.org/)规范,但是我发现对于一个纯文档站性质的项目想要遵循这类规范是很难的,主要是如何界定属于哪类修改……脑子比较乱,后续会尝试改改 80 | 81 | - 在逆向QQ安装包后发现其图片路径CRC64规律并没有改变……多项式与旧版QQ一致,不过我一直没法算出正确的图片路径……怀疑是`chatimg`在加载原图后被删除,只存原图在`chatraw`,具体的有待测试 82 | 83 | - 一转眼 QQDecrypt 项目也半年了…… 84 | 85 | ## 2025-03-29 86 | 超级重大更新-O- 87 | 1. 网站的主页图标之前是随手拼接的,~~难看死了~~,现在找AI重新生成了一张,猫猫可爱捏\( ̄︶ ̄*\)))就是整体的色调看起来好奇怪 88 | 89 | 1. 404界面怎么能突兀的显示英文呢,~~全部给我变成猫猫~~ 90 | 91 | 1. 终于把之前nt_msg.db的分析内容合并了,顺便增加了一些官方 `MsgRecord` 定义,说起来最近对于数据库的分析都开始懈怠了,以后会开始更专注于内容而不是网站本身 92 | 93 | 1. 社区项目我一直在想要不要引入一些卡片丰富一下内容,不过最后也没想好…… 94 | 95 | 1. 把表名都写进了三级目录里,话说后面得考虑要不要加个搜索啥的(+_+)? 96 | 97 | 1. 之前虽然虽然更换了sidebar插件,不过一直没解决关于文件夹在sidebar中显示为英文的现象,也是我笨,搞了半天都要准备手写侧边栏了,结果发现插件配置里支持……不过为了隐藏空白的`index.md`倒是烦了我好一会儿,最后选择直接跳转到对应板块的说明文件去 98 | 99 | 1. 对上下页链接的中文描述因为我配置写错位置导致一直显示的是英文……笨(ノへ ̄、)至此,完成了主题描述的所有汉化,什么,为什么只有中文?现在的QQ基本没有海外用户了吧😆 100 | 101 | 1. 对社区项目进行分类,希望大家能更快速地寻找到自己想要的项目把 102 | 103 | ## 2025-03-21 104 | 超级重大更新(づ ̄ 3 ̄)づ 105 | 106 | - 这周在学校突然就想到了好多好多想改的( ´・・)ノ(._.`) 107 | 108 | 1. 那个已经两年没更新的 `vite-plugin-vitepress-auto-sideba` 插件……虽然现在还能用,但更新vitepress1.6.2后就警告我不兼容了……现在换成 `vitepress-sidebar` 真的舒服多了,更精细化的控制侧边栏显示(。・∀・)ノ゙ 109 | 110 | 1. 之前我就觉得把解密和查看完全分开总感觉怪怪的,不过一直没时间来改,现在终于算是翻新了 111 | 112 | 1. GitHub的issues上有好多佬分享自己的项目,不过我除了能点个赞也做不了什么(太菜了),现在把他们全部聚合在一起方便大家寻找||ヽ(* ̄▽ ̄*)ノミ|Ю 113 | 114 | 1. 导航栏和button按钮一直不知道放啥,现在终于让她发挥出了该有的用处:) 115 | 116 | 1. 创建了TG频道`@QQBackup`, 不管用不用,先占个位吧…… 117 | 118 | - 大家少熬夜呀⊙﹏⊙∥ 119 | 120 | 121 | 122 | ## 2025-03-15 123 | - 可是一个人分析真的有用吗……如果没有其他人来核对最后发现大错特错还要重头来 124 | 125 | ## 2025-02-15 126 | - 要开学了啊,只能止步于此了 127 | - 把网站从泉州放出来了,公众号申诉真的有用欸 128 | 129 | 130 | ## 2025-02-10 131 | - 这几天累死我力(数据库好杂 132 | - 不过确实有收获的……只是一个人真的好累啊 133 | - 感觉对其他都提不起兴趣了…… 134 | 135 | 136 | ## 2025-02-05 137 | - 迟到的新年快乐捏~ 138 | - 对IDA教程再次进行了补充……希望小白也能看懂啊吧 139 | 140 | 141 | ## 2025-01-15 142 | - 移除了复选框,感觉没必要…… 143 | - 改了下介绍,之前的看起来其奇怪怪怪 144 | - 怎么改也回不到默认深色了(⊙ˍ⊙)?) 145 | 146 | 147 | ## 2025-01-10 148 | 才发现链接文字的css被覆盖了导致都是黑色,根本看不出来是超链接…… 149 | 150 | 最近虽然提交了很多更改,但都是有关美化和优化SEO的……还是太菜了>︿< 151 | 152 | ## 2025-01-04 153 | 其实我觉得默认暗色还挺好看的……但是突然不知道怎么改了( 154 | 155 | 重构了一下结构,避免徒增只有一个文件的文件夹( 156 | 157 | 158 | ## 2025-01-01 159 | 别人的跨年 vs QQ被限制的我跨年😅 160 | 161 | 趁着有时间多改改吧……我的时间不多了😣 162 | 163 | 164 | ## 2024-12-31 165 | 好久没更新……咕咕 166 | 167 | 赶在新年要到来的前一天做了许多修改,元旦快乐❤ 168 | 169 | 今年又是啥也没做好的一年( 170 | 171 | 172 | ## 2024-10-13 173 | 分析/data/user/0/com.tencent.mobileqq/databases/nt_db/nt_qq_{QQ_path_hash}/路径中的一堆db文件都是干嘛的 174 | 175 | 是应该采取做成数据库再写一个程序去读取展示出来(就像原版QQ那样的展示数据?),然而工作量还是很大,或者是导出为HTML之类的更为方便? 176 | 177 | 尝试导出了c2c_msg_table表中的发送QQ号、时间、40800(目前只有文字正常解析了),并将它们分类显示出来 178 | 179 | 180 | ​ 181 | ​ 182 | ​ 183 | ​ -------------------------------------------------------------------------------- /docs/about/projects.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: doc 3 | title: 社区项目 4 | next: false 5 | prev: false 6 | --- 7 | 8 | ## 投稿说明 9 | 10 | 此页用于展示互联网上有关 `NTQQ` 数据库解析、导出的开源仓库 11 | 12 | 欢迎各位投稿自己的项目,可在仓库发起[issues](https://github.com/QQBackup/QQDecrypt/issues/new/choose)申请收录,但是在提交前请注意: 13 | 14 | - 只收录有关解析 `NTQQ` **数据库**的项目,请不要投稿仅支持旧版QQ的项目 15 | 16 | - 若提交的项目非**GitHub仓库**,请尽量用以下示例格式提交申请,以方便后续整理 17 | ```plaintext 18 | 项目名称:QQDecrypt 19 | 项目描述:QQ 聊天数据库解密 20 | License: CC BY-NC-SA 4.0 (开源协议) 21 | 项目链接:https://github.com/QQBackup/QQDecrypt 22 | ``` 23 | 24 | - 申请一般会在周末处理 25 | 26 | ## 用于解密数据库的项目 27 | 28 | ### qq-win-db-key 29 | 30 | 项目地址:[https://github.com/QQBackup/qq-win-db-key](https://github.com/QQBackup/qq-win-db-key) 31 | 32 | 项目描述:全平台 QQ 聊天数据库解密教程 33 | 34 | License: CC BY-NC-SA 4.0 35 | 36 | 收录时间: 2025-03-22 37 | 38 | ![GitHub Last Commit](https://img.shields.io/github/last-commit/QQBackup/qq-win-db-key) 39 | 40 | ### qq-nt-db 41 | 42 | 项目地址:[https://github.com/Mythologyli/qq-nt-db](https://github.com/Mythologyli/qq-nt-db) 43 | 44 | 项目描述:QQ NT Windows 数据库解密+图片/文件清理 45 | 46 | License: CC BY-NC-SA 4.0 47 | 48 | 收录时间: 2025-03-22 49 | 50 | ![GitHub Last Commit](https://img.shields.io/github/last-commit/Mythologyli/qq-nt-db) 51 | 52 | ### qqnt_backup 53 | 54 | 项目地址:[https://github.com/xCipHanD/qqnt_backup](https://github.com/xCipHanD/qqnt_backup) 55 | 56 | 项目描述:适用于Android_NT_QQ数据库解密 57 | 58 | License: unknown 59 | 60 | 收录时间: 2025-03-22 61 | 62 | ![GitHub last commit](https://img.shields.io/github/last-commit/xCipHanD/qqnt_backup) 63 | 64 | ### ntdb_unwrap 65 | 66 | 项目地址:[https://github.com/artiga033/ntdb_unwrap](https://github.com/artiga033/ntdb_unwrap) 67 | 68 | 项目描述:一键解密/解析 NTQQ 数据库!不用删除文件头,直接读取数据库的SQLite VFS扩展 69 | 70 | License: MIT 71 | 72 | 收录时间: 2025-03-22 73 | 74 | ![GitHub last commit](https://img.shields.io/github/last-commit/artiga033/ntdb_unwrap) 75 | 76 | ## 有关解析数据库的项目 77 | 78 | ### GroupChatAnnualReport 79 | 80 | 项目地址:[https://github.com/mobyw/GroupChatAnnualReport](https://github.com/mobyw/GroupChatAnnualReport) 81 | 82 | 项目描述:使用 Windows QQNT 聊天记录制作群聊年度报告! 83 | 84 | License: MIT 85 | 86 | 收录时间: 2025-03-22 87 | 88 | ![GitHub last commit](https://img.shields.io/github/last-commit/mobyw/GroupChatAnnualReport) 89 | 90 | ### QQNT_Export 91 | 92 | 项目地址:[https://github.com/Tealina28/QQNT_Export](https://github.com/Tealina28/QQNT_Export) 93 | 94 | 项目描述:读取并导出解密后的QQNT数据库中的聊天记录 95 | 96 | License: GPL-3.0 97 | 98 | 收录时间: 2025-03-22 99 | 100 | ![GitHub last commit](https://img.shields.io/github/last-commit/Tealina28/QQNT_Export) 101 | 102 | ### ntdb-plaintext-extracter 103 | 104 | 项目地址:[https://github.com/QQBackup/ntdb-plaintext-extracter](https://github.com/QQBackup/ntdb-plaintext-extracter) 105 | 106 | 项目描述:提取QQ NT数据库 group_msg_table 中的纯文本 107 | 108 | License: AGPL-3.0 109 | 110 | 收录时间: 2025-03-22 111 | 112 | ![GitHub last commit](https://img.shields.io/github/last-commit/QQBackup/ntdb-plaintext-extracter) 113 | 114 | ### nt_msg.py 115 | 116 | 项目地址:[https://github.com/BrokenC1oud/nt_msg.py](https://github.com/BrokenC1oud/nt_msg.py) 117 | 118 | 项目描述:A python repo to parse nt_msg.db 119 | 120 | License: unknown 121 | 122 | 收录时间: 2025-03-22 123 | 124 | ![GitHub last commit](https://img.shields.io/github/last-commit/BrokenC1oud/nt_msg.py) 125 | 126 | ### QQ-Chat-Intimacy-Analyzer 127 | 128 | 项目地址:[https://github.com/EndsOculus/QQ-Chat-Intimacy-Analyzer](https://github.com/EndsOculus/QQ-Chat-Intimacy-Analyzer) 129 | 130 | 项目描述:QQ 群聊互动亲密度分析工具 131 | 132 | License: unknown 133 | 134 | 收录时间: 2025-03-22 135 | 136 | ![GitHub last commit](https://img.shields.io/github/last-commit/EndsOculus/QQ-Chat-Intimacy-Analyzer) 137 | 138 | ### QQRootFastDecrypt(停止更新) 139 | 140 | 项目地址: [https://github.com/miniyu157/QQRootFastDecrypt](https://github.com/miniyu157/QQRootFastDecrypt) 141 | 142 | 项目描述:针对于已 root 安卓设备的快捷导出 QQ 聊天记录的脚本 143 | 144 | License: MIT License 145 | 146 | 收录时间:2025-06-28 147 | 148 | ![GitHub last commit](https://img.shields.io/badge/repository_was_archived-red) 149 | 150 | ### QQNT-Database-Export-Tool 151 | 152 | 项目地址:[https://github.com/star-picker/QQNT-Database-Export-Tool](https://github.com/star-picker/QQNT-Database-Export-Tool) 153 | 154 | 项目描述:一个基于 Python 的工具,用于将已解密的 QQNT 数据库中的聊天记录导出 155 | 156 | License: AGPL-3.0 License 157 | 158 | 收录时间:2025-08-10 159 | 160 | ![GitHub last commit](https://img.shields.io/github/last-commit/star-picker/QQNT-Database-Export-Tool) 161 | 162 | ### QQ-DUMP 163 | 164 | 项目地址: https://github.com/miniyu157/qq-dump 165 | 166 | 项目描述: 极速的 Android 端 NTQQ 数据库密钥提取、解密及聊天记录导出工具。遵循 KISS 原则与 Unix 哲学, 针对移动端闪存优化 I/O 策略, 最小化读写磨损。支持后台自动更新以保证代码为最新。 167 | 168 | License: GPL-3.0 169 | 170 | 收录时间:2025-12-13 171 | 172 | ![GitHub last commit](https://img.shields.io/github/last-commit/miniyu157/qq-dump) -------------------------------------------------------------------------------- /docs/about/thanks.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 致谢 3 | sidebar: false 4 | next: false 5 | prev: false 6 | hidesidebar: true 7 | --- 8 | 9 | **网站的编写离不开以下网站或应用的支持,在此表达最诚挚的谢意(排名不分先后):** 10 | - 在原 qq-win-db-key 项目中引用的相关资料,此处不再全部列出 11 | - 若有希望补充/想从致谢名单中移除的,可在 GitHub 开 issues 联系我修改 12 | 13 | 85 | 86 | 183 | 184 | -------------------------------------------------------------------------------- /docs/decrypt/NTQQ (Windows).md: -------------------------------------------------------------------------------- 1 | --- 2 | title: NTQQ (Windows) 3 | order: 3 4 | --- 5 | 6 | # NTQQ (Windows) 7 | 8 | ## IDA分析 9 | 10 | [QQ NT Windows 数据库解密+图片/文件清理](https://github.com/Mythologyli/qq-nt-db):本仓库使用 IDA debugger 完成了逆向分析到解密的全过程,并实现了图片与文件清理。 11 | 12 | [IDA](https://down.52pojie.cn/Tools/Disassemblers/IDA_Pro_v8.3_Portable.zip)下载(吾爱破解提供) 13 | 14 | > [!TIP] 转载说明 15 | 原文发表于[Myth's Blog](https://myth.cx/p/qq-nt-db),作者为Myth,根据 CC BY-SA 4.0 授权协议发布,内容经[issues #50](https://github.com/QQBackup/qq-win-db-key/issues/50)补充 16 | > 17 | >部分图片来自[GroupChatAnnualReport](https://github.com/mobyw/GroupChatAnnualReport) 18 | > 19 | 20 | QQ NT Windows 数据库解密+图片/文件清理 21 | 22 | 笔者测试时使用的 QQ 版本:9.9.3-17412 23 | 24 | 经验证的 QQ 版本:9.9.3-17749 9.9.12-26339 25 | 26 | ### 找到数据库 `passphrase` 27 | 28 | 1. 解压IDA压缩包,双击打开 `ida64.exe` ,选择 `new` , 在弹出的资源管理器界面中定位到 QQNT 安装目录下的 `./versions/{version}/resources/app`文件夹,其中 `{version}` 为 QQNT 的版本号。在右下角过滤器中选择全部文件,单击 `wrapper.node`文件,并点击右下角的 "Open" 按钮。 29 | ![](/img/mobyw_ida_0.png) 30 | >[!note] wrapper.node文件路径说明 31 | 早期 Windows QQ_NT 版本 `wrapper.node` 文件位于 `./resources/app/versions/{version}` 文件夹中 32 | 33 | 保持默认导入选项,点击 "OK" 按钮。如果有弹出让选择PDB文件相关提示框点击 "No"。等待文件加载完成。 34 | 35 | **分析时间较长,请耐心等待** 36 | 37 | 分析完成后打开 Strings 视图(如果没有同时按 shift + F12 或按图片示例打开),CTRL+F搜索 `nt_sqlite3_key_v2: db=%p zDb=%s` 字符串, 双击 `nt_sqlite3_key_v2: db=%p zDb=%s` 一行,跳转到 IDA View-A 标签的对应行。 38 | 39 | 40 | ![](/img/strings-view.png) 41 | ![](/img/Myth1.png) 42 | 43 | 44 | 2. 在 IDA View-A 标签中,单击 `nt_sqlite3_key_v2` 的名称 `aNtSqlite3KeyV2`,按快捷键 X 打开交叉引用窗口。双击第一条结果,转到引用位置。 45 | ![](/img/mobyw_ida_4.png) 46 | 或者右键此处点击`Jump to xref to operand..` 47 | ![](/img/Myth2.png) 48 | JumpOpXref 后发现在 49 | ![](/img/Myth3.png) 50 | 51 | 3. 按下 F5 反编译此函数, 如果有弹出窗口点击 "Yes"。等待反编译完成。单击引用语句的左侧蓝色圆点添加断点。 52 | ![](/img/Myth4.png) 53 | 54 | 4. 退出 QQ 并重新打开,但不要登录。在上端工具栏右侧选择**Local Windows debugger** 55 | ![](/img/debug_windows.png) 56 | 在 IDA 中点击顶部导航栏 "Debugger" 中的 "Attach to process..." 菜单项, 从列表最后开始找到第一个 `QQ.exe` 进程,双击打开。 57 | ![](/img/Attach_to_process.png) 58 | >如果等待附加进程时QQ已经无响应了(卡在登录界面无法点击),等待加载完成后按快捷键 F9 继续运行。 59 | 60 | 点击登录 QQ,此时成功在断点处停下,打开一个 Locals 视图(Debugger->Debugger windows->Locals)查看参数的值 61 | ![](/img/locals.png) 62 | 63 | 5. 在 a3 对应的位置右键jump to,直到看到如下图所示的 16 位字符串。`#8xxxxxxxxxxx@uJ` 即为我们需要的 `passphrase` (不一定是这个格式,但总字符数是一样的) 64 | ![](/img/mobyw_ida_b.png) 65 | ![](/img/Myth6.png) 66 | 67 | ### 打开数据库 68 | 69 | 请参考 [NTQQ 解密数据库](decode_db.md)。 70 | 71 | ## 使用 frida hook 72 | ### 1. 定位 `nt_sqlite3_key_v2:` 73 | 74 | 此处采用 [IDA](https://down.52pojie.cn/Tools/Disassemblers/IDA_Pro_v8.3_Portable.zip) 演示,您可以替换成您喜欢的任何反编译器 75 | 76 | ![在 Strings 窗口中搜索 nt_sqlite3_key_v2,得到多个结果](/img/image-win-1.webp) 77 | 78 | 定位到字符串 `nt_sqlite3_key_v2: db=%p zDb=%s` 79 | 80 | ![在 IDA View 中定位到此字符串](/img/image-win-2.webp) 81 | 82 | 在此字符串上按`x`,或右键选此字符串并选择`Jump to xref`查看引用,进入引用的函数 83 | 84 | ![通过字符串找到目标函数](/img/image-win-3.webp) 85 | 86 | 记录函数地址,切换到 Hex View,复制从函数地址开始的一段字节序列,作为特征 Hex 87 | 88 | QQ 9.9.1.15043 为 89 | 90 | ```plain 91 | 48 89 5C 24 08 48 89 6C 24 10 48 89 74 24 18 57 92 | 48 83 EC 20 41 8B F9 49 8B F0 4C 8B CA 4C 8B C1 93 | 48 8B EA 48 8B D9 48 8D 15 33 05 A0 00 B9 08 00 94 | ``` 95 | 96 | ### 2. Hook 并找到 Key 97 | 98 | 根据 指出 99 | 100 | `sqlite3_key_v2` 的签名为 101 | 102 | ```c 103 | int sqlite3_key_v2( 104 | sqlite3 *db, /* Database to be keyed */ 105 | const char *zDbName, /* Name of the database */ 106 | const void *pKey, int nKey /* The key */ 107 | ); 108 | ``` 109 | 110 | 其中对我们有用的是 `pKey` 和 `nKey` 111 | 112 | 作者本人采用 frida hook 113 | 114 | 根据 repo 提供的脚本略加修改,很容易得到我们需要的 `pKey` 和 `nKey` 115 | 116 | (如果你对如何修改有疑问,可以使用 [msojocs/nt-hook](https://github.com/msojocs/nt-hook/tree/4414f372ee4847be9d91d7436abb7653f8908f91) 中给出的完整脚本。注意,编译此脚本需要你的系统安装有 Node.js 环境,但编译得到的`.js`文件可以直接运行。注意,本仓库最新版本可能不能在 Windows 平台下直接使用,请自行根据 commit 信息找到可用版本(比如超链接给出的版本),或自行更改相关代码。) 117 | 118 | PS:有概率你会得到的一个长度为 20 的 key,但那不是我们想要的,可以挂上一个动态调试器来观察 key 对应的具体数据库 119 | 120 | ## 使用Python脚本(暂时废弃) 121 | ::: details (需要帮助) 122 | 123 | 下载脚本windows_ntqq_key.py 124 | 125 | 先退出QQ,运行脚本,然后运行登录QQ获取密钥 126 | 127 | > [!WARNING] 注意 128 | > 此方法只适用于已被适配的版本,具体可在[QQ_Offset.json](/files/QQ_Offset.json)中查看 129 | > 130 | > **9.9.21.38503**之后的版本**已失效**, 笔者尝试在内存中搜索已知密钥也一无所获,怀疑QQ在登陆成功后从内存销毁了密钥 131 | 132 | > [!TIP] 社区共建 133 | > 134 | > 我们期待社区成员共同分享各版本的基址信息,便于后续获取密钥。如果你有兴趣参与,请按照以下方式投稿: 135 | > 136 | > 按照[IDA分析](NTQQ%20(Windows).md#找到数据库-passphrase)走到步骤2,向上翻找到偏移地址 137 | > 138 | > 示例,QQ(9.9.20.37625)偏移地址为0x20A6E 139 | > 140 | > ![>](/img/wrapper_node_offset.png) 141 | > 142 | >找到后,请发送版本号和偏移地址(并配图)到 issues 。 143 | ::: 144 | 145 | ### 3. 打开数据库 146 | 147 | 请参考 [NTQQ 解密数据库](decode_db.md)。 148 | -------------------------------------------------------------------------------- /docs/decrypt/NTQQ (Android).md: -------------------------------------------------------------------------------- 1 | --- 2 | title: NTQQ(Android) 3 | order: 2 4 | --- 5 | 6 | # NTQQ(Android) 7 | 8 | > [!TIP] 注意 9 | 如果运行脚本时出现问题,请尝试将所有文件中的所有`libkernel`替换为`libbasic_share`。 10 | 11 | ## 方法1(推荐) 12 | 13 | > [!INFO] 说明 14 | >本方法不需要 root,只需要使用系统自带的备份功能导出 QQ 的数据即可。由于 QQ 限制,部分系统可能无法导出 QQ 数据,此时可以使用“聊天记录迁移”功能迁移到其他设备上。 15 | 16 | 经测试适用于`9.0.65`至`9.1.60`版本,更低版本可能无法使用此方法。 17 | 18 | 以下的`md5`函数返回结果均为 32 位,字符均为小写,在Python中等价于以下函数:`def md5(s): i=__import__('hashlib').md5();i.update(s.encode('utf8'));return i.hexdigest()` 19 | 为了方便起见,假设 QQ 号(表示为`uin`)为`390251789`,`nt_uid`为`u_mIicAReWrdCB-kST6TXH7A`。 20 | 一切以`/data/user/0/com.tencent.mobileqq/`开头的路径均表示root后可以访问到的绝对路径,若为处理备份文件,则此路径可能有所不同。 21 | 22 | # 便捷获取 23 | 24 | 25 | ### 获取nt_uid 26 | 27 | 获取`nt_uid`有多种方式,任意一种均可。 28 | 29 | - 将`/data/user/0/com.tencent.mobileqq/databases/beacon_db_com.tencent.mobileqq`文件作为纯文本文件打开,查找你的 QQ 号对应的`uid`,形式如`"home_uin": "390251789","uid":"u_mIicAReWrdCB-kST6TXH7A"`,其中`u_mIicAReWrdCB-kST6TXH7A`即为`nt_uid`。 30 | ![1](https://ooo.0x0.ooo/2024/10/05/O48sUY.jpg) 31 | - 在`/data/user/0/com.tencent.mobileqq/files/uid/`目录下,可见到文件名形如`390251789###u_mIicAReWrdCB-kST6TXH7A`的若干个文件,其中`u_mIicAReWrdCB-kST6TXH7A`即为`nt_uid`。 32 | ![2](https://ooo.0x0.ooo/2024/10/05/O48Vyv.jpg) 33 | - 若使用了[QAuxiliary](https://github.com/cinit/QAuxiliary)模块,可以通过打开`[辅助功能]聊天和消息-[消息]转发消息点头像查看详细信息`功能,合并转发由自己发送的消息,查看消息的`senderUid`属性获取,详见[#32](https://github.com/QQBackup/qq-win-db-key/issues/32#issue-2418610093)。 34 | ![3](https://ooo.0x0.ooo/2024/10/05/O48Ysq.jpg) 35 | 36 | 对`nt_uid`取`md5`即可得到`QQ_UID_hash`(即`QQ_UID_hash = md5(nt_uid) = md5("u_mIicAReWrdCB-kST6TXH7A") = "255c42fc0f4d295678e6ff0135fcf5dd"`) 37 | 38 | ### 获取聊天记录文件 39 | 40 | 聊天记录可在以下路径找到: 41 | 42 | ```plain 43 | /data/user/0/com.tencent.mobileqq/databases/nt_db/nt_qq_{QQ_path_hash}/nt_msg.db 44 | ``` 45 | 46 | 此处的`QQ_path_hash`有两种方法获得: 47 | 48 | 1. 猜测 49 | 50 | 如果你登录的 QQ 账号数量不多,可以通过文件更新时间、文件大小等猜测。 51 | 52 | 2. 计算 53 | 54 | 对`QQ_UID_hash`进行如下运算即可得到`QQ_path_hash`:`QQ_path_hash = md5(md5(nt_uid) + "nt_kernel") = md5("255c42fc0f4d295678e6ff0135fcf5ddnt_kernel") = "b69bfb8e74137f4e4253d1af3e99493a" 55 | 56 | 则聊天记录路径为`/data/user/0/com.tencent.mobileqq/databases/nt_db/nt_qq_b69bfb8e74137f4e4253d1af3e99493a/nt_msg.db` 57 | 58 | ### 获取密钥 59 | 60 | 使用`HxD`或者其他二进制查看工具打开`nt_msg.db`文件,将文件头部跟随在`QQ_NT DB`后的可读字符串复制,形如`6tPaJ9GP`,记为`rand`。 61 | 62 | 此时可以计算出数据库密钥`key`:`key = md5(QQ_UID_hash + rand) = md5("255c42fc0f4d295678e6ff0135fcf5dd6tPaJ9GP") = "71c0dfcef3b5ceae7c4a1c68ca662f4a"`。 63 | 64 | 则数据库密钥为`71c0dfcef3b5ceae7c4a1c68ca662f4a`。 65 | 66 | 另外,文件头部还可能含有`cipher_hmac_algorithm`的值(如`HMAC_SHA1`)等与解密相关的信息,可被解析为Protobuf数据,详见[#29 (comment)](https://github.com/QQBackup/qq-win-db-key/issues/29#issuecomment-2227660390),此处不展开说明。 67 | 68 | #### 打开数据库 69 | 70 | 请参考 [NTQQ 解密数据库](decode_db.md)。 71 | 72 | 如果文件头中出现`HMAC_SHA1`字样([示例](https://github.com/QQBackup/qq-win-db-key/issues/29#issuecomment-2227660390)),则将其作为`cipher_hmac_algorithm`的值。否则,可尝试`HMAC_SHA512`(默认值)或`HMAC_SHA256`。 73 | 74 | ## 方法2 75 | 76 | 此方法要求您拥有手机的 root 权限。 77 | > [!TIP] 若你想直接使用示例脚本 78 | > 建议QQ版本为9.0.65或更低 79 | 80 | ### 基础环境 81 | 82 | 以下环境配置任选一种即可。 83 | 84 | #### Termux 85 | 86 | 首先,安装 Termux,换源并执行`pkg up`、`termux-setup-storage` 87 | 88 | 然后依次执行以下命令: 89 | 90 | 安装基础依赖: 91 | 92 | ```shell 93 | pkg in python wget tsu root-repo 94 | pkg in frida frida-python 95 | ``` 96 | 97 | 关闭 SELinux: 98 | 99 | ```shell 100 | su -c setenforce 0 101 | ``` 102 | 103 | 接着,手动下载主版本相同的`frida-server`,解压到`/data/local/tmp`下,并重命名为`friendly`(不一定要完全一致,仅是建议文件名不包含`frida`以略微避免检测); 104 | 105 | 赋予`friendly`可执行权限后,新开一个终端以`root`权限运行`friendly` 106 | 107 | 下载 hook 脚本: 108 | 109 | ```shell 110 | wget https://github.com/QQBackup/qq-win-db-key/raw/master/android_get_key.py 111 | ``` 112 | 113 | #### PC 114 | 115 | 你也可以在电脑端使用`adb`等来避免在手机端配置`Termux`。具体过程略。 116 | 117 | ### 获取密钥 118 | 119 | 打开 QQ 并完成登录,进入主界面。将 QQ 切换到后台后继续下一步。 120 | 121 | 在 Termux 终端 / 电脑的终端 中运行: 122 | 123 | ```shell 124 | python android_get_key.py 125 | ``` 126 | 127 | 也可手动指定版本号,但目前所有支持的版本号使用的脚本均相同。 128 | 129 | ```shell 130 | python android_get_key.py 8.9.58 131 | ``` 132 | 133 | 此时应当输出`Frida script injected.`,若没有,请检查: 134 | 135 | - 是否以`root`权限运行`frida-server` 136 | - 是否以关闭 SELinux (即设置为宽容模式) 137 | - 是否已经关闭`Magisk Hide`与`Shamiko`,并且重启手机 138 | - `frida-server`与 Termux 中的`frida`版本号第一个点号前的数字是否相同 139 | - QQ 版本是否一致 140 | 141 | 接下来,可以确认命令行是否给出数据库密钥(以`pKey`显示)。 142 | 143 | ### 打开数据库 144 | 145 | 请参考 [NTQQ 解密数据库](decode_db.md)。 146 | 147 | ## 方法3 148 | 149 | 此方法同样要求您拥有手机的 root 权限,环境配置与[方法2](#方法2)相同。不同之处在于,本方法可以直接导出解密后的数据库文件,而无需额外修复与解密。 150 | 151 | > 该部分内容来源于[Android QQ NT 版数据库解密](https://blog.yllhwa.com/2023/09/29/Android%20QQ%20NT%20%E7%89%88%E6%95%B0%E6%8D%AE%E5%BA%93%E8%A7%A3%E5%AF%86/),由[@yllhwa](https://github.com/yllhwa)贡献。 152 | 153 | 关闭`Magisk Hide`与`Shamiko`,并且重启手机 154 | 155 | 可能需要关闭`SELinux`(也就是设为`Permissive`) 156 | 157 | 需要授予手机QQ读写存储权限 158 | 159 | 下载 android_dump.js 160 | 161 | 如果先前已经运行过,则先删除上一次运行生成的`/sdcard/Download/plaintext.db` 162 | 163 | 终端中运行`frida -U -f com.tencent.mobileqq -l android_dump.js`(如果用的有线连接adb就直接写`-U`,如果是Termux或无线连接就把`-U`改成`-R`) 164 | 165 | 如果一切顺利,已解密的`plaintext.db`将会在至少5秒后导出至`/sdcard/Download/plaintext.db`。 166 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # 非商用开源协议 2 | 3 | 第1版 2023-04-30 4 | 5 | ## 版权声明 6 | 7 | 版权所有 © 2023 LY 8 | 9 | 本协议证基于 通用许可协议 版本 2,原始发布地址: [https://github.com/qwq233/license](https://github.com/qwq233/license) / [https://qwq2333.top/license-v2](https://qwq2333.top/license-v2)。通用许可协议 版本 2 版权属于 © 2022 James Clef 。 10 | 11 | 允许在遵守 CC BY-NC-SA 4.0 协议的同时,复制和分发此协议文档的逐字记录副本,且允许对其进行更改,但必须保留其版权信息与原作者。如果您提出申请特殊权限,协议作者可口头或书面授予任何人任何的使用本协议的权利。 12 | 13 | ## 正文 14 | 15 | 请务必仔细阅读和理解通用许可协议书中规定的所有权利和限制。在使用前,您需要仔细阅读并决定接受或不接受本协议的条款。除非或直至您接受本协议的条款,否则本作品及其相关副本、相关程序代码或相关资源不得在您的任何终端上下载、安装或使用。 16 | 17 | 您一旦下载、使用本作品及其相关副本、相关程序代码或相关资源,即表示您同意接受本协议各项条款的约束。如您不同意本协议中的条款,您则应当立即删除本作品、附属资源及其相关源代码。 18 | 19 | 本作品权利只许可使用,而不许可出售。 20 | 21 | 1. 定义 22 | 23 | 1. “本协议”指非商用开源协议第 1 版。 24 | 25 | 2. “您”指依据本通用许可协议行使其所获得授予之权利的个人或机构。 “您的” 有相应的含义。 26 | 27 | 3. “作者”或“本作品作者”指通过本协议进行授权的个人或组织和/或根据《与贸易有关的知识产权协定》所规定的对本作品拥有著作权的个人或组织。 28 | 29 | 4. “协议作者”指上文提及的协议版权方,即 LY 。 30 | 31 | 5. “本作品”或“作品”指根据本协议的任何受版权保护的作品,如源代码。 32 | 33 | 6. “本作品发布网址”指本作品作者初次或后续发布的所指定的唯一或多个的网址。 34 | 35 | 7. “修改”作品是指以需要版权许可的方式复制或改编该作品的全部或部分内容,而不是制作一个完全的副本。由此产生的作品也被称为本作品的"修改版"或“基于”本作品的作品。 36 | 37 | 8. “修改作品作者”指任何依据本通用许可协议对在修改作品中所贡献的部分所享有的著作权的个人或机构。 38 | 39 | 9. “传播”作品指对该作品进行任何未经许可的行为,这将使您直接或次要根据适用的版权法承担侵权责任,但在计算机上执行该作品或修改其私人副本除外。传播包括复制、分发(进行或不进行修改)、向公众公开、以及在某些国家/地区进行其他活动。 40 | 41 | 10. “非商业使用”指该使用的主要意图或者指向并非获取商业优势或金钱报酬。为本协议之目的,以数字文件共享或类似方式,用本作品交换其他受到著作权与类似权利保护的作品是非商业性使用,只要该交换不直接或潜在涉及金钱报酬的支付。 42 | 43 | 11. “商业使用”指该使用的主要意图或者指向为获取商业优势或金钱报酬。以数字文件共享或类似方式,用本作品交换其他受到著作权与类似权利保护的作品是商业性使用,只要该交换直接或潜在涉及金钱报酬的支付。 44 | 45 | 12. “源代码”指生成、安装和(对于可执行作品)运行目标代码以及修改作品所需的所有源代码,包括控制这些活动的脚本。但是,它不包括作品的系统库,也不包括在执行这些活动时未经修改但不属于作品的通用工具或普遍可用的免费程序。 46 | 47 | 13. “目标代码”指通过本作品源代码或修改作品源代码生成的计算机可识别的机器语言或近似与机器语言的代码。“编译作品”有相同含义。 48 | 49 | 50 | 2. 本协议无意削减、限制、或约束您基于以下法律规定对本作品的合法使用:合理使用,权利穷竭原则,及著作权法或其他相关法律对著作权人专有权利的限制。 51 | 52 | 3. 授权范围 53 | 54 | 根据本协议的条款和条件,作者在此授予您全球性、免版税、非独占并且在本作品的著作权存续期间内均有效的许可,就本作品行使以下权利: 55 | 56 | 1. 在一台个人所有终端上安装、使用、显示、运行本作品的一份副本。 57 | 58 | 2. 为了防止副本损坏而制作备份复制品。这些备份复制品不得通过任何方式提供给他人使用,并在您丧失该合法副本的所有权时,负责将备份复制品销毁。 59 | 60 | 3. 为了把本作品用于实际的终端应用环境或者改进其功能、性能而进行必要的修改。 61 | 62 | 4. 对本作品进行反向工程、反向编译或反汇编;或进行其他获得本作品源代码的访问或行为。 63 | 64 | 5. 发行、公开传播本作品及其修改作品。 65 | 66 | 6. 根据本协议的条款,作者授予您在全球范围内,免费的、不可再许可、非独占、不可撤销的许可,以对本作品行使以下“协议所授予的权利”: 67 | 68 | 1. 复制和分享本作品的全部或部分,仅限于非商业性使用。 69 | 70 | 2. 以非商业使用为目的制作、复制和分享修改作品。 71 | 72 | 以上权利可在任何现有的或者以后出现的并为可适用的法律认可的媒体和形式上行使。上述权利包括为在其他媒体和形式上行使权利而必须进行技术性修改的权利。作者在此保留所有未明示授予的权利。 73 | 74 | 4. 限制 75 | 76 | 1. 您在发行或公开传播本作品时,必须遵守本协议。在您发行或公开传播的本作品的每一份复制件中,您必须附上一份本协议协议的复制件。您不得就本作品提出或增加任何条款,从而限制本协议协议或者限制获得本作品的第三方行使本协议协议所赋予的权利。您不得对本作品进行再许可。您必须在您发行或公开传播的每份作品复制件中完整保留所有与本协议协议及免责条款相关的声明。 在发行或公开传播本作品时,您不得对本作品施加任何技术措施,从而限制从您处获得本作品的第三方行使本协议协议授予的权利。 77 | 78 | 2. 您必须以下述许可条款发行或公开传播修改作品: 79 | 80 | 1. 本协议或后续版本 81 | 82 | 2. 您不得就修改作品提出或增加任何条款,从而限制“可适用的协议”的规定,或者限制获得修改作品的第三方行使“可适用的协议”所赋予的权利。在发行或公开传播包含本作品的修改作品时,您必须在本作品的每一份复制件中完整地保留所有与“可适用的协议”及免责条款相关的声明。在发行或公开传播修改作品时,您不得对修改作品施加任何技术措施,从而限制从您处获得修改作品的第三方行使“可适用的协议”所赋予的权利。本项(第 4 款第 2 项)规定同样适用于收录在汇编作品中的修改作品,但并不要求汇编作品中除基于本作品而创作的修改作品之外的其他作品受“可适用的协议”的约束。 83 | 84 | 3. 以源代码的形式传播本作品或编译作品时,您必须满足以下所有条件: 85 | 86 | 1. 修改作品必须有醒目的声明,说明您修改了它,并给出相关的日期。 87 | 88 | 2. 若修改作品使用了本作品所包含全部或部分源代码和/或其他部分的本作品,需提供完整的修改作品,如其全部源代码、可用于生成修改作品的编译作品的脚本、修改作品使用到的资源。 89 | 90 | 4. 以编译作品的形式传播本作品或修改作品时,以下列方式之一传递源代码: 91 | 92 | 1. 在实体产品(包括实体销售媒介)中传递编译作品,或体现在实体产品(包括实体销售媒介)中,同时将相应的源代码固定在通常用于软件交换的实体媒介上。 93 | 94 | 2. 点对点传输。 95 | 96 | 3. 可免费访问的网络服务器。 97 | 98 | 3. 您不得进行商业使用,这里的商业使用包括第 1 款第 10 项所提到的内容、以盈利为目的的提供本作品和/或修改作品的帮助和/或指南、以盈利为目的的使用或授予他人本作品的所提供给您的权利/许可。 99 | 100 | 4. 在发行或公开传播本作品、任何修改作品时,您必须完整保留所有关于本作品的著作权声明,并以适于所使用的媒介或方法的形式提供下述信息: 101 | 102 | 1. 作者的姓名或者其他能够体现作者身份的标志物。 103 | 104 | 2. 标明本作品发布网址 105 | 106 | 3. 依第 4 条第 2 项之要求,注明修改作品中使用的本作品的作者的姓名或者其他能够体现作者身份的标志物和作品名称。为避免疑义,本条有关标示作者姓名和作品名称之规定,仅适用于前述署名的用途;除非您事先另行取得作者的书面同意,否则您不得以明示或者默示的方式主张或暗示,您本人或您对作品的使用与作者有关联或者已获得上述人士的赞助或者支持。 107 | 108 | 5. 您在复制、发行或者公开表演本作品,或者复制、发行或者公开表演作为任何修改作品一部分的本作品时,不得歪曲、损害或者以其他方式损害本作品,导致原始作者的名誉或者荣誉受损。 109 | 110 | 6. 您不得利用本作品的全部或部分申请商业用途的商标和/或专利。 111 | 112 | 7. 作者拥有对本协议的修改权,当您使用本作品、源代码及其附属资源的修改协议后的作品,需遵守最新协议。 113 | 114 | 5. 免责声明: 115 | 116 | 1. 您在下载并使用本作品时均被视为已经仔细阅读本协议并完全同意。凡以任何方式使用本作品,或直接、间接使用本作品,均被视为自愿接受相关声明和用户服务协议的约束。 117 | 118 | 2. 除非本协议的当事人相互以书面的方式做出相反约定,且在相关法律所允许的最大范围内,否则作者按其现状提供本作品,对本作品不作任何明示或者默示、依照法律或者其他规定的陈述或担保,包括但是不限于任何有关可否商业性使用、是否符合特定的目的、不具有潜在的或者其他缺陷、准确性或者不存在不论能否被发现的错误的担保。有些司法管辖区不允许排除前述默示保证,因此这些排除性规定并不一定适用于您。 119 | 120 | 3. 用户明确并同意其使用本作品所存在的风险及法律风险将完全由其本人承担;因其使用作品而产生的一切后果也由其本人承担,本作品作者对此不承担任何责任。 121 | 122 | 4. 除非书面同意,否则在任何情况下,任何作者与协议作者,或经其修改和/或传送上述程序的任何其他方均不对您承担赔偿责任,包括任何一般的,特殊的,因本作品而使您对其他法律实体造成的一切损害。本作品及作者已提前告知您此类损害的可能性。 123 | 124 | 5. 您在传播、使用本作品及其修改作品时,应自行保证您的一切行为与本作品的全部功能符合一切对您有管辖权的法律法规的要求,由您传播、使用本作品产生的法律风险及其造成的相应后果,将由您自行承担,本作品及其作者不承担任何责任。 125 | 126 | 6. 本协议最终解释权归本作品作者与协议作者所有。 127 | 128 | 129 | 6. 许可终止: 130 | 131 | 1. 在您违反本协议协议任何条款时,本协议及其所授予的权利将自动终止。然而,根据本协议从您处获取修改作品的自然人、法人或者其他组织,如果他们仍完全遵守相关条款,则对他们的许可不会随之终止。即使本协议被终止,第 1 款、第 2 款、第 5 款、第 6 款仍然有效。 132 | 133 | 2. 在上述条款及条件的前提下,此处授予的许可在法定著作权保护期限内有效。即便如此,作者保留依其他许可条款发行本作品及在任何时候停止发行本作品的权利;但是,作者的上述权利不能被用于撤销本协议或任何其他在本协议条款下授予的或必须授予的许可,除本款第 1 项指明的终止外,本协议将保持其完全效力。 134 | -------------------------------------------------------------------------------- /docs/decrypt/NTQQ (macOS x86).md: -------------------------------------------------------------------------------- 1 | --- 2 | title: NTQQ (macOS x86) 3 | order: 5 4 | --- 5 | 6 | # NTQQ (macOS x86) 7 | ## 0. 背景信息 8 | 9 | * **测试系统**:`macOS Sequoia 15.5` 10 | * **CPU 架构**:`x86_64` 11 | * **QQ 版本**:[6.9.58-28971](https://qq.en.uptodown.com/mac/download/1033570073) 12 | * **发布日期**:2024 年 10 月 28 日 13 | * **SHA256**:`47a09e5ea5211012ca0f7e7ef634aef0c4a3fe87c27bac61ac0b74afdbd577d1` 14 | * **备份链接**:[Google Drive](https://drive.google.com/file/d/1ktW7jg1ZdkOrnmEyWAT0LUyyO3eBgXr_/view?usp=sharing) 15 | * **备注**:为确保文件完整性,请在使用前核对 SHA256 值。您也可以从官网下载最新版本,但请注意,新版本的函数地址和结构可能与本教程有所不同。 16 | 17 | > **特别注意**:本教程仅适用于 **x86 (Intel) 架构**的 macOS 系统。如果您的设备是 **ARM (Apple Silicon) 架构**,请参考[这篇文章](https://gilbert.vicp.io/2024/03/20/Unlocking-Memories-Extracting-QQ-NT-Chat-History-from-Encrypted-Databases/)。 18 | 19 | ## 1. 准备阶段 20 | 21 | 我们要用到 `lldb` 调试器,需要暂时禁用系统的完整性保护 (System Integrity Protection, SIP)。 22 | 23 | 具体操作步骤请参考[这篇资料](https://gist.github.com/imhet/c96889529826915343d7b41a4c6ec770)。 24 | 25 | ## 2. 获取密钥 (Key) 26 | 27 | ### 2.1 定位 `nt_sqlite3_key_v2` 函数的偏移地址 28 | 29 | 首先,我们需要在 QQ 的核心库文件 `wrapper.node` 中找到加密函数 `nt_sqlite3_key_v2` 的位置。 30 | 31 | ```bash 32 | # 将核心库文件复制到当前目录以便分析 33 | cp /Applications/QQ.app/Contents/Resources/app/wrapper.node . 34 | 35 | # 使用 objdump 反汇编并查找关键函数 36 | objdump -d wrapper.node | grep -B 20 "nt_sqlite3_key_v2" 37 | 38 | # --- 输出结果示例 --- 39 | 332cdd7: 48 8d 35 73 cb 57 00 leaq 0x57cb73(%rip), %rsi ## literal pool for: "main" 40 | 332cdde: 48 89 df movq %rbx, %rdi 41 | 332cde1: 4c 89 fa movq %r15, %rdx 42 | 332cde4: 44 89 f1 movl %r14d, %ecx 43 | 332cde7: 48 83 c4 08 addq $0x8, %rsp 44 | 332cdeb: 5b popq %rbx 45 | 332cdec: 41 5e popq %r14 46 | 332cdee: 41 5f popq %r15 47 | 332cdf0: 5d popq %rbp 48 | 332cdf1: e9 00 00 00 00 jmp 0x332cdf6 49 | 332cdf6: 55 pushq %rbp # <--- 函数实际入口 50 | 332cdf7: 48 89 e5 movq %rsp, %rbp 51 | 332cdfa: 41 57 pushq %r15 52 | 332cdfc: 41 56 pushq %r14 53 | 332cdfe: 41 54 pushq %r12 54 | 332ce00: 53 pushq %rbx 55 | 332ce01: 41 89 ce movl %ecx, %r14d 56 | 332ce04: 49 89 d7 movq %rdx, %r15 57 | 332ce07: 49 89 f4 movq %rsi, %r12 58 | 332ce0a: 48 89 fb movq %rdi, %rbx 59 | 332ce0d: 48 8d 35 cb a3 65 00 leaq 0x65a3cb(%rip), %rsi ## literal pool for: "nt_sqlite3_key_v2: db=%p zDb=%s" 60 | ``` 61 | 62 | **分析**: 63 | 1. `jmp` 指令将程序执行流无条件转移至 `0x332cdf6`,这是一种常见的尾调用优化。 64 | 2. 在地址 `0x332cdf6` 处,我们看到了 x86-64 架构标准的函数序言(function prologue)指令: 65 | * `pushq %rbp`:保存调用者的栈底指针。 66 | * `movq %rsp, %rbp`:建立本函数的新栈帧。 67 | 68 | 综上所述,我们可以确定 `nt_sqlite3_key_v2` 函数的实际入口**偏移地址**为 `0x332cdf6`。 69 | 70 | ### 2.2 使用 lldb 附加进程并读取密钥 71 | 72 | `sqlite3_key_v2` 函数原型如下,它决定了我们将从哪些寄存器中读取参数: 73 | 74 | ```c 75 | int sqlite3_key_v2( 76 | sqlite3 *db, // 目标数据库对象 (第一个参数, 存放在 rdi 寄存器) 77 | const char *zDbName, // 数据库名 (第二个参数, 存放在 rsi 寄存器) 78 | const void *pKey, // 密钥地址 (第三个参数, 存放在 rdx 寄存器) 79 | int nKey // 密钥长度 (第四个参数, 存放在 rcx 寄存器) 80 | ); 81 | ``` 82 | 83 | 现在,我们通过 `lldb` 调试器在运行时捕获这些参数。 84 | 85 | ```bash 86 | # 查找正在运行的 QQ 进程的 PID 87 | ps aux | grep 'QQ$' | awk '{print $2}' 88 | 2349 # <-- 这是示例 PID,请替换为您自己的 89 | 90 | # 启动 lldb 并附加到 QQ 进程 91 | lldb --attach-pid 2349 92 | 93 | # 获取 wrapper.node 库在内存中的基地址 94 | (lldb) image list -o -f | grep /Applications/QQ.app/Contents/Resources/app/wrapper.node 95 | [ 0] 0x0000000110068000 /Applications/QQ.app/Contents/Resources/app/wrapper.node 96 | 97 | # 计算函数的实际内存地址 (基地址 + 偏移地址) 98 | (lldb) expr 0x0000000110068000 + 0x332cdf6 99 | (long) $0 = 4617489910 100 | 101 | # 在该地址设置断点 102 | (lldb) br s -a 4617489910 103 | 104 | # continue,让 QQ 继续运行 105 | (lldb) c 106 | 107 | # 点击 QQ 的任意聊天窗口 108 | # 一般情况下,lldb 将在我们设置的断点处暂停执行 109 | 110 | # 读取密钥长度 (从 rcx 寄存器) 111 | # 如果值为 0x10 (十进制的 16),通常表示已成功捕获。 112 | (lldb) register read rcx 113 | rcx = 0x0000000000000010 114 | 115 | # 读取密钥的内存地址 (从 rdx 寄存器) 116 | (lldb) register read rdx 117 | rdx = 0x0000010805f442c0 118 | 119 | # 从该地址读取 16 字节的密钥内容 120 | # 下方的 "Z[12?_]7OMsX?X22" 即为密钥。注意:密钥在不同环境下是不同的,此值仅作示例。 121 | (lldb) memory read --format c --count 16 --size 1 0x0000010805f442c0 122 | 0x10805f442c0: Z[12?_]7OMsX?X22 123 | 124 | # 密钥获取成功,退出调试 125 | (lldb) detach 126 | (lldb) exit 127 | ``` 128 | 129 | ## 3. 解密数据库 130 | 131 | 拿到密钥后,我们就可以使用 `sqlcipher` 工具来解密数据库文件了。 132 | 133 | ```bash 134 | # 数据库文件通常位于以下路径,其中 {MD5} 是与你 QQ 号相关的哈希值 135 | # ~/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/nt_qq_{MD5}/nt_db 136 | # 示例路径:~/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/nt_qq_cc067b8bcbf8980fabd93574e09d9efa/nt_db 137 | 138 | # 我们可以先复制一份 profile_info.db 用于解密测试 139 | cp '~/Library/Containers/com.tencent.qq/Data/Library/Application Support/QQ/nt_qq_{MD5}/nt_db/profile_info.db' . 140 | 141 | # SQLCipher v4 的数据库文件包含一个 1024 字节的头部,需要先剥离才能被正确识别 142 | cat ./profile_info.db | tail -c +1025 > profile_info_clean.db 143 | 144 | # 使用 sqlcipher 打开处理后的数据库文件 145 | sqlcipher ./profile_info_clean.db 146 | ``` 147 | 148 | 进入 `sqlcipher` 命令行后,依次执行以下 PRAGMA 指令来配置解密参数: 149 | 150 | ```sql 151 | -- 将 "your_key" 替换为上一步中获取到的实际密钥 152 | PRAGMA key = "your_key"; 153 | 154 | -- 设置 NTQQ 使用的加密参数 155 | PRAGMA kdf_iter = 4000; 156 | PRAGMA cipher_page_size = 4096; 157 | PRAGMA cipher_hmac_algorithm = HMAC_SHA1; 158 | PRAGMA cipher_default_kdf_algorithm = PBKDF2_HMAC_SHA512; 159 | 160 | -- 验证是否解密成功,如果能列出表名,则代表成功 161 | .tables 162 | ``` 163 | 164 | 输出示例: 165 | ``` 166 | buddy_list profile_info_v6 167 | ``` 168 | 169 | 至此,数据库解密成功。 -------------------------------------------------------------------------------- /docs/about/LICENSE.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: LICENSE 3 | sidebar: false 4 | next: false 5 | prev: false 6 | editLink: false 7 | hidesidebar: true 8 | --- 9 | 10 | # 非商用开源协议 11 | 12 | 第1版 2023-04-30 13 | 14 | ## 版权声明 15 | 16 | 版权所有 © 2023 LY 17 | 18 | 本协议证基于,[通用许可协议 版本 2](https://github.com/qwq233/license) 。 版权属于 © 2022 James Clef 。 19 | 20 | 允许在遵守 CC BY-NC-SA 4.0 协议的同时,复制和分发此协议文档的逐字记录副本,且允许对其进行更改,但必须保留其版权信息与原作者。如果您提出申请特殊权限,协议作者可口头或书面授予任何人任何的使用本协议的权利。 21 | 22 | ## 正文 23 | 24 | 请务必仔细阅读和理解通用许可协议书中规定的所有权利和限制。在使用前,您需要仔细阅读并决定接受或不接受本协议的条款。除非或直至您接受本协议的条款,否则本作品及其相关副本、相关程序代码或相关资源不得在您的任何终端上下载、安装或使用。 25 | 26 | 您一旦下载、使用本作品及其相关副本、相关程序代码或相关资源,即表示您同意接受本协议各项条款的约束。如您不同意本协议中的条款,您则应当立即删除本作品、附属资源及其相关源代码。 27 | 28 | 本作品权利只许可使用,而不许可出售。 29 | 30 | 1. 定义 31 | 32 | 1. “本协议”指非商用开源协议第 1 版。 33 | 34 | 2. “您”指依据本通用许可协议行使其所获得授予之权利的个人或机构。 “您的” 有相应的含义。 35 | 36 | 3. “作者”或“本作品作者”指通过本协议进行授权的个人或组织和/或根据《与贸易有关的知识产权协定》所规定的对本作品拥有著作权的个人或组织。 37 | 38 | 4. “协议作者”指上文提及的协议版权方,即 LY 。 39 | 40 | 5. “本作品”或“作品”指根据本协议的任何受版权保护的作品,如源代码。 41 | 42 | 6. “本作品发布网址”指本作品作者初次或后续发布的所指定的唯一或多个的网址。 43 | 44 | 7. “修改”作品是指以需要版权许可的方式复制或改编该作品的全部或部分内容,而不是制作一个完全的副本。由此产生的作品也被称为本作品的"修改版"或“基于”本作品的作品。 45 | 46 | 8. “修改作品作者”指任何依据本通用许可协议对在修改作品中所贡献的部分所享有的著作权的个人或机构。 47 | 48 | 9. “传播”作品指对该作品进行任何未经许可的行为,这将使您直接或次要根据适用的版权法承担侵权责任,但在计算机上执行该作品或修改其私人副本除外。传播包括复制、分发(进行或不进行修改)、向公众公开、以及在某些国家/地区进行其他活动。 49 | 50 | 10. “非商业使用”指该使用的主要意图或者指向并非获取商业优势或金钱报酬。为本协议之目的,以数字文件共享或类似方式,用本作品交换其他受到著作权与类似权利保护的作品是非商业性使用,只要该交换不直接或潜在涉及金钱报酬的支付。 51 | 52 | 11. “商业使用”指该使用的主要意图或者指向为获取商业优势或金钱报酬。以数字文件共享或类似方式,用本作品交换其他受到著作权与类似权利保护的作品是商业性使用,只要该交换直接或潜在涉及金钱报酬的支付。 53 | 54 | 12. “源代码”指生成、安装和(对于可执行作品)运行目标代码以及修改作品所需的所有源代码,包括控制这些活动的脚本。但是,它不包括作品的系统库,也不包括在执行这些活动时未经修改但不属于作品的通用工具或普遍可用的免费程序。 55 | 56 | 13. “目标代码”指通过本作品源代码或修改作品源代码生成的计算机可识别的机器语言或近似与机器语言的代码。“编译作品”有相同含义。 57 | 58 | 59 | 2. 本协议无意削减、限制、或约束您基于以下法律规定对本作品的合法使用:合理使用,权利穷竭原则,及著作权法或其他相关法律对著作权人专有权利的限制。 60 | 61 | 3. 授权范围 62 | 63 | 根据本协议的条款和条件,作者在此授予您全球性、免版税、非独占并且在本作品的著作权存续期间内均有效的许可,就本作品行使以下权利: 64 | 65 | 1. 在一台个人所有终端上安装、使用、显示、运行本作品的一份副本。 66 | 67 | 2. 为了防止副本损坏而制作备份复制品。这些备份复制品不得通过任何方式提供给他人使用,并在您丧失该合法副本的所有权时,负责将备份复制品销毁。 68 | 69 | 3. 为了把本作品用于实际的终端应用环境或者改进其功能、性能而进行必要的修改。 70 | 71 | 4. 对本作品进行反向工程、反向编译或反汇编;或进行其他获得本作品源代码的访问或行为。 72 | 73 | 5. 发行、公开传播本作品及其修改作品。 74 | 75 | 6. 根据本协议的条款,作者授予您在全球范围内,免费的、不可再许可、非独占、不可撤销的许可,以对本作品行使以下“协议所授予的权利”: 76 | 77 | 1. 复制和分享本作品的全部或部分,仅限于非商业性使用。 78 | 79 | 2. 以非商业使用为目的制作、复制和分享修改作品。 80 | 81 | 以上权利可在任何现有的或者以后出现的并为可适用的法律认可的媒体和形式上行使。上述权利包括为在其他媒体和形式上行使权利而必须进行技术性修改的权利。作者在此保留所有未明示授予的权利。 82 | 83 | 4. 限制 84 | 85 | 1. 您在发行或公开传播本作品时,必须遵守本协议。在您发行或公开传播的本作品的每一份复制件中,您必须附上一份本协议协议的复制件。您不得就本作品提出或增加任何条款,从而限制本协议协议或者限制获得本作品的第三方行使本协议协议所赋予的权利。您不得对本作品进行再许可。您必须在您发行或公开传播的每份作品复制件中完整保留所有与本协议协议及免责条款相关的声明。 在发行或公开传播本作品时,您不得对本作品施加任何技术措施,从而限制从您处获得本作品的第三方行使本协议协议授予的权利。 86 | 87 | 2. 您必须以下述许可条款发行或公开传播修改作品: 88 | 89 | 1. 本协议或后续版本 90 | 91 | 2. 您不得就修改作品提出或增加任何条款,从而限制“可适用的协议”的规定,或者限制获得修改作品的第三方行使“可适用的协议”所赋予的权利。在发行或公开传播包含本作品的修改作品时,您必须在本作品的每一份复制件中完整地保留所有与“可适用的协议”及免责条款相关的声明。在发行或公开传播修改作品时,您不得对修改作品施加任何技术措施,从而限制从您处获得修改作品的第三方行使“可适用的协议”所赋予的权利。本项(第 4 款第 2 项)规定同样适用于收录在汇编作品中的修改作品,但并不要求汇编作品中除基于本作品而创作的修改作品之外的其他作品受“可适用的协议”的约束。 92 | 93 | 3. 以源代码的形式传播本作品或编译作品时,您必须满足以下所有条件: 94 | 95 | 1. 修改作品必须有醒目的声明,说明您修改了它,并给出相关的日期。 96 | 97 | 2. 若修改作品使用了本作品所包含全部或部分源代码和/或其他部分的本作品,需提供完整的修改作品,如其全部源代码、可用于生成修改作品的编译作品的脚本、修改作品使用到的资源。 98 | 99 | 4. 以编译作品的形式传播本作品或修改作品时,以下列方式之一传递源代码: 100 | 101 | 1. 在实体产品(包括实体销售媒介)中传递编译作品,或体现在实体产品(包括实体销售媒介)中,同时将相应的源代码固定在通常用于软件交换的实体媒介上。 102 | 103 | 2. 点对点传输。 104 | 105 | 3. 可免费访问的网络服务器。 106 | 107 | 3. 您不得进行商业使用,这里的商业使用包括第 1 款第 10 项所提到的内容、以盈利为目的的提供本作品和/或修改作品的帮助和/或指南、以盈利为目的的使用或授予他人本作品的所提供给您的权利/许可。 108 | 109 | 4. 在发行或公开传播本作品、任何修改作品时,您必须完整保留所有关于本作品的著作权声明,并以适于所使用的媒介或方法的形式提供下述信息: 110 | 111 | 1. 作者的姓名或者其他能够体现作者身份的标志物。 112 | 113 | 2. 标明本作品发布网址 114 | 115 | 3. 依第 4 条第 2 项之要求,注明修改作品中使用的本作品的作者的姓名或者其他能够体现作者身份的标志物和作品名称。为避免疑义,本条有关标示作者姓名和作品名称之规定,仅适用于前述署名的用途;除非您事先另行取得作者的书面同意,否则您不得以明示或者默示的方式主张或暗示,您本人或您对作品的使用与作者有关联或者已获得上述人士的赞助或者支持。 116 | 117 | 5. 您在复制、发行或者公开表演本作品,或者复制、发行或者公开表演作为任何修改作品一部分的本作品时,不得歪曲、损害或者以其他方式损害本作品,导致原始作者的名誉或者荣誉受损。 118 | 119 | 6. 您不得利用本作品的全部或部分申请商业用途的商标和/或专利。 120 | 121 | 7. 作者拥有对本协议的修改权,当您使用本作品、源代码及其附属资源的修改协议后的作品,需遵守最新协议。 122 | 123 | 5. 免责声明: 124 | 125 | 1. 您在下载并使用本作品时均被视为已经仔细阅读本协议并完全同意。凡以任何方式使用本作品,或直接、间接使用本作品,均被视为自愿接受相关声明和用户服务协议的约束。 126 | 127 | 2. 除非本协议的当事人相互以书面的方式做出相反约定,且在相关法律所允许的最大范围内,否则作者按其现状提供本作品,对本作品不作任何明示或者默示、依照法律或者其他规定的陈述或担保,包括但是不限于任何有关可否商业性使用、是否符合特定的目的、不具有潜在的或者其他缺陷、准确性或者不存在不论能否被发现的错误的担保。有些司法管辖区不允许排除前述默示保证,因此这些排除性规定并不一定适用于您。 128 | 129 | 3. 用户明确并同意其使用本作品所存在的风险及法律风险将完全由其本人承担;因其使用作品而产生的一切后果也由其本人承担,本作品作者对此不承担任何责任。 130 | 131 | 4. 除非书面同意,否则在任何情况下,任何作者与协议作者,或经其修改和/或传送上述程序的任何其他方均不对您承担赔偿责任,包括任何一般的,特殊的,因本作品而使您对其他法律实体造成的一切损害。本作品及作者已提前告知您此类损害的可能性。 132 | 133 | 5. 您在传播、使用本作品及其修改作品时,应自行保证您的一切行为与本作品的全部功能符合一切对您有管辖权的法律法规的要求,由您传播、使用本作品产生的法律风险及其造成的相应后果,将由您自行承担,本作品及其作者不承担任何责任。 134 | 135 | 6. 本协议最终解释权归本作品作者与协议作者所有。 136 | 137 | 138 | 6. 许可终止: 139 | 140 | 1. 在您违反本协议协议任何条款时,本协议及其所授予的权利将自动终止。然而,根据本协议从您处获取修改作品的自然人、法人或者其他组织,如果他们仍完全遵守相关条款,则对他们的许可不会随之终止。即使本协议被终止,第 1 款、第 2 款、第 5 款、第 6 款仍然有效。 141 | 142 | 2. 在上述条款及条件的前提下,此处授予的许可在法定著作权保护期限内有效。即便如此,作者保留依其他许可条款发行本作品及在任何时候停止发行本作品的权利;但是,作者的上述权利不能被用于撤销本协议或任何其他在本协议条款下授予的或必须授予的许可,除本款第 1 项指明的终止外,本协议将保持其完全效力。 -------------------------------------------------------------------------------- /docs/files.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 文件下载 3 | sidebar: false 4 | next: false 5 | prev: false 6 | hidesidebar: true 7 | --- 8 | 9 | # 文件下载 10 | 11 | 102 | 103 | 173 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/HashCalculator.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 106 | 107 | -------------------------------------------------------------------------------- /docs/public/files/android_dump.js: -------------------------------------------------------------------------------- 1 | // frida -U -f com.tencent.mobileqq -l final.js 2 | // https://blog.yllhwa.com/2023/09/29/Android%20QQ%20NT%E7%89%88%E6%95%B0%E6%8D%AE%E5%BA%93%E8%A7%A3%E5%AF%86/ 3 | const DATABASE = "nt_msg.db"; 4 | const module_name = "libkernel.so"; 5 | 6 | // FOR LOG 7 | let SQLITE3_EXEC_CALLBACK_LOG = true; 8 | let index1 = 0; 9 | let xCallback = new NativeCallback( 10 | (para, nColumn, colValue, colName) => { 11 | if (!SQLITE3_EXEC_CALLBACK_LOG) { 12 | return 0; 13 | } 14 | console.log(); 15 | console.log( 16 | "------------------------" + index1++ + "------------------------" 17 | ); 18 | for (let index = 0; index < nColumn; index++) { 19 | let c_name = colName 20 | .add(index * 8) 21 | .readPointer() 22 | .readUtf8String(); 23 | let c_value = ""; 24 | try { 25 | c_value = 26 | colValue 27 | .add(index * 8) 28 | .readPointer() 29 | .readUtf8String() ?? ""; 30 | } catch {} 31 | console.log(c_name, "\t", c_value); 32 | } 33 | return 0; 34 | }, 35 | "int", 36 | ["pointer", "int", "pointer", "pointer"] 37 | ); 38 | 39 | // CODE BELOW 40 | var kernel_so = null; 41 | function single_function(pattern) { 42 | pattern = pattern 43 | .replaceAll("##", "") 44 | .replaceAll(" ", "") 45 | .toLowerCase() 46 | .replace(/\\s/g, "") 47 | .replace(/(.{2})/g, "$1 "); 48 | var akey_function_list = Memory.scanSync( 49 | kernel_so.base, 50 | kernel_so.size, 51 | pattern 52 | ); 53 | if (akey_function_list.length > 1) { 54 | console.log("pattern FOUND MULTI!!"); 55 | console.log(pattern); 56 | console.log(akey_function_list); 57 | throw Error("pattern FOUND MULTI!!"); 58 | } 59 | if (akey_function_list.length == 0) { 60 | console.log("pattern NOT FOUND!!"); 61 | console.log(pattern); 62 | throw Error("pattern NOT FOUND!!"); 63 | } 64 | return akey_function_list[0]["address"]; 65 | } 66 | 67 | let get_filename_from_sqlite3_handle = function (sqlite3_db) { 68 | // full of magic number 69 | let zFilename = ""; 70 | try { 71 | let db_pointer = sqlite3_db.add(0x8 * 5).readPointer(); 72 | let pBt = db_pointer.add(0x8).readPointer(); 73 | let pBt2 = pBt.add(0x8).readPointer(); 74 | let pPager = pBt2.add(0x0).readPointer(); 75 | zFilename = pPager.add(208).readPointer().readCString(); 76 | } catch (e) {} 77 | return zFilename; 78 | }; 79 | 80 | let hook = function () { 81 | var process_Obj_Module_Arr = Process.enumerateModules(); 82 | for (var i = 0; i < process_Obj_Module_Arr.length; i++) { 83 | if (process_Obj_Module_Arr[i].path.indexOf(module_name) !== -1) { 84 | kernel_so = process_Obj_Module_Arr[i]; 85 | } 86 | } 87 | if (kernel_so === null) { 88 | console.log(module_name + " not loaded. exit."); 89 | throw Error(".so not loaded"); 90 | } 91 | 92 | // sqlite3_exec -> sub_1CFB9C0 93 | // let sqlite3_exec_addr = base_addr.add(0x1cfb9c0); 94 | let sqlite3_exec_addr = single_function( 95 | "FF 43 02 D1 FD 7B 03 A9 FC 6F 04 A9 FA 67 05 A9 F8 5F 06 A9 F6 57 07 A9 F4 4F 08 A9 FD C3 00 91 54 D0 3B D5 88 16 40 F9 F8 03 04 AA F5 03 03 AA F6 03 02 AA" 96 | ); // 貌似是稳定的,先这样写 97 | console.log("sqlite3_exec_addr: " + sqlite3_exec_addr); 98 | 99 | let sqlite3_exec = new NativeFunction(sqlite3_exec_addr, "int", [ 100 | "pointer", 101 | "pointer", 102 | "pointer", 103 | "int", 104 | "int", 105 | ]); 106 | 107 | let target_db_handle = null; 108 | let js_sqlite3_exec = function (sql) { 109 | if (target_db_handle === null) { 110 | return -1; 111 | } 112 | let sql_pointer = Memory.allocUtf8String(sql); 113 | return sqlite3_exec(target_db_handle, sql_pointer, xCallback, 0, 0); 114 | }; 115 | 116 | // ATTACH BELOW 117 | Interceptor.attach(sqlite3_exec_addr, { 118 | onEnter: function (args) { 119 | // sqlite3*,const char*,sqlite3_callback,void*,char** 120 | let sqlite3_db = ptr(args[0]); 121 | let sql = Memory.readCString(args[1]); 122 | let callback_addr = ptr(args[2]); 123 | let callback_arg = ptr(args[3]); 124 | let errmsg = ptr(args[4]); 125 | let database_name = get_filename_from_sqlite3_handle(sqlite3_db); 126 | if ( 127 | database_name.slice(database_name.lastIndexOf("/") + 1) === DATABASE 128 | ) { 129 | console.log("sqlite3_db: " + sqlite3_db); 130 | console.log("sql: " + sql); 131 | target_db_handle = sqlite3_db; 132 | } 133 | }, 134 | }); 135 | setTimeout(function () { 136 | let EXPORT_FILE_PATH = "/storage/emulated/0/Download/plaintext.db"; 137 | // 不建议更改导出路径 138 | console.log("Start exporting database to " + EXPORT_FILE_PATH); 139 | let ret = js_sqlite3_exec( 140 | `ATTACH DATABASE '` + 141 | EXPORT_FILE_PATH + 142 | `' AS plaintext KEY '';SELECT sqlcipher_export('plaintext');DETACH DATABASE plaintext;` 143 | ); 144 | console.log("Export end."); 145 | console.log("js_sqlite3_exec ret: " + ret); 146 | }, 4000); // hook 后 导出前 等待4秒 147 | }; 148 | 149 | var hasHooked = false; 150 | console.log("Script loaded. Waiting for " + module_name + " to load..."); 151 | const dlopen_process = { 152 | onEnter: function (args) { 153 | this.path = Memory.readUtf8String(args[0]); 154 | if (0) send("Loading " + this.path); 155 | }, 156 | onLeave: function (retval) { 157 | if (this.path.indexOf(module_name) !== -1 && !hasHooked) { 158 | hasHooked = true; 159 | if (1) send("Hooked!!"); 160 | hook(); 161 | } 162 | }, 163 | }; 164 | 165 | try { 166 | Interceptor.attach(Module.findExportByName(null, "dlopen"), dlopen_process); 167 | } catch (err) {} 168 | try { 169 | Interceptor.attach( 170 | Module.findExportByName(null, "android_dlopen_ext"), 171 | dlopen_process 172 | ); 173 | } catch (err) {} 174 | -------------------------------------------------------------------------------- /docs/public/files/linux_qq_get_key.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import subprocess 4 | import re 5 | import hashlib 6 | from pathlib import Path 7 | 8 | QQ_CONFIG = Path.home() / '.config' / 'QQ' 9 | WRAPPER_NODE_PATH = '/opt/QQ/resources/app/wrapper.node' 10 | 11 | def error(msg): 12 | print(f"\033[1;31m Error: {msg}\033[0m") 13 | exit(1) 14 | 15 | def Assert(condition, msg): 16 | if not condition: 17 | error(msg) 18 | 19 | # get Program Header and Section Mapping 20 | result = subprocess.run(["readelf", "-lW" , WRAPPER_NODE_PATH], stdout = subprocess.PIPE, text= True) 21 | output = result.stdout 22 | program_header = [] 23 | rodata_info = None 24 | read_program_header = False 25 | read_section_mapping = False 26 | re_ph = re.compile(r"\s*(\w+)\s+0x(\d+)\s+0x(\d+).*") 27 | re_mp = re.compile(r"\s*(\d+)\s+(.*)") 28 | for line in output.splitlines(): 29 | line = line.strip() 30 | if "Program Headers:" in line: 31 | read_program_header = True 32 | continue 33 | if "Section to Segment mapping:" in line: 34 | read_program_header = False 35 | read_section_mapping = True 36 | continue 37 | if read_program_header: 38 | m = re_ph.match(line) 39 | if m: 40 | program_header.append({ 41 | "type": m.group(1), 42 | "section_offset": int(m.group(2), 16), 43 | "virt_addr": int(m.group(3), 16) 44 | }) 45 | if read_section_mapping: 46 | m = re_mp.match(line) 47 | if m: 48 | (idx , sections) = m.groups() 49 | if ".rodata" in sections: 50 | rodata_info = program_header[int(idx)] 51 | break 52 | 53 | Assert(rodata_info is not None, "rodata section not found") 54 | print(f"rodata_info: {rodata_info}") 55 | 56 | # get string offset 57 | result = subprocess.Popen(["strings", "-t" , "d" , WRAPPER_NODE_PATH], stdout = subprocess.PIPE, text= True) 58 | pattern = re.compile('nt_sqlite3_key_v2: db=%p zDb=%s$') 59 | for line in result.stdout: 60 | line = line.strip() 61 | if(pattern.search(line)): 62 | offset = line.split(" ")[0] 63 | Assert(offset.isdecimal(), "offset should be decimal") 64 | result.terminate() 65 | break 66 | str_off = int(offset) 67 | print(f"str_off offset: {hex(str_off)}") 68 | 69 | # get reference offset 70 | 71 | def get_file_hash(file_path): 72 | hasher = hashlib.sha256() 73 | with open(file_path, 'rb') as file: 74 | while chunk := file.read(8192): 75 | hasher.update(chunk) 76 | return hasher.hexdigest() 77 | 78 | ref_off_cache_file = Path.cwd() / 'ref_off_cache' 79 | shoud_recalculate = True 80 | if(Path.exists(ref_off_cache_file)): 81 | with open(ref_off_cache_file, 'r') as f: 82 | try: 83 | file_hash = f.readline().strip() 84 | ref_off = list(map(str, f.readline().strip().split())) 85 | shoud_recalculate = file_hash != get_file_hash(WRAPPER_NODE_PATH) or len(ref_off) == 0 86 | except Exception as e: 87 | print(f"Error: {e}\n recalculate ref_off") 88 | 89 | 90 | if(shoud_recalculate): 91 | result = subprocess.Popen(["objdump", "-D" , "-j" , ".text" , WRAPPER_NODE_PATH], stdout = subprocess.PIPE, text= True) 92 | str_addr = rodata_info["virt_addr"] + str_off - rodata_info["section_offset"] 93 | pattern = re.compile(f".*# {str_addr:x}") 94 | ref_off = [] 95 | for line in result.stdout: 96 | line = line.strip() 97 | if(pattern.search(line)): 98 | offset = line.split(":")[0] 99 | Assert(re.match(r'[0-9a-f]+', offset), "offset should be hex") 100 | ref_off.append(offset) 101 | result.terminate() 102 | with open(ref_off_cache_file, 'w') as f: 103 | f.write(f"{get_file_hash(WRAPPER_NODE_PATH)}\n") 104 | f.write(" ".join(map(str, ref_off)) + "\n") 105 | 106 | print(f"ref_off offset: {ref_off}") 107 | 108 | import gdb 109 | 110 | def exit_gdb(): 111 | gdb.execute("set confirm off") 112 | gdb.execute("quit") 113 | 114 | hook_stop_script = """ 115 | define hook-stop 116 | x /10i $pc 117 | p (char[16])*(char *)$rsi 118 | p $rdx 119 | end 120 | """ 121 | gdb.execute(hook_stop_script, to_string=True) 122 | gdb.execute("set pagination off") 123 | 124 | # load wrapper.node 125 | gdb.execute("break dlopen") 126 | gdb.execute("run") 127 | finish = False 128 | i = 20 129 | while not finish: 130 | output = gdb.execute("x /s file", to_string=True) 131 | if i == 0: 132 | finish = True 133 | if "wrapper.node" in output: 134 | break 135 | gdb.execute("continue") 136 | gdb.execute("finish") 137 | 138 | output = gdb.execute("info proc mappings", to_string=True) 139 | base_addr = None 140 | for line in output.splitlines(): 141 | if "wrapper" in line: 142 | base_addr= int(line.split()[0],16) 143 | break 144 | print(f"Base address found: {base_addr}") 145 | 146 | # find function address 147 | breakpoints = [] 148 | gdb.execute("delete breakpoints") 149 | for ref in ref_off: 150 | breakpoint_addr = base_addr + int(ref, 16) 151 | gdb.execute(f"break *{breakpoint_addr}") 152 | gdb.execute(f"x /10i {breakpoint_addr}") 153 | 154 | 155 | gdb.execute("continue") 156 | gdb.execute("finish") 157 | 158 | func_addr = None 159 | for i in range(16): 160 | inst = gdb.execute(f"x /i $pc - {i}", to_string=True) 161 | if "call" in inst: 162 | func_addr = int(inst.split(" ")[-1], 16) 163 | break 164 | 165 | if func_addr is None: 166 | print("Function address not found.") 167 | exit_gdb() 168 | 169 | print(f"func_addr: {hex(func_addr)}") 170 | #get zDb 171 | gdb.execute("delete breakpoints") 172 | gdb.execute(f"break *{func_addr}") 173 | gdb.execute(f"continue") 174 | while(gdb.parse_and_eval("$rdx") != 16): 175 | gdb.execute("continue") 176 | zDb = gdb.parse_and_eval("(char[16])*(char *)$rsi") 177 | print(f"zDb: {zDb}") 178 | 179 | exit_gdb() -------------------------------------------------------------------------------- /docs/about/contributors.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: 贡献者 4 | sidebar: false 5 | hidesidebar: true 6 | --- 7 | 8 | 9 | 184 | 185 | 186 | 187 | 188 | 191 | 194 | 195 | 196 | 197 | 198 | 199 | 202 | 205 | 206 | 207 | 208 | -------------------------------------------------------------------------------- /docs/public/files/android_get_key.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | import frida 3 | import sys 4 | import platform 5 | import os 6 | import functools 7 | import subprocess 8 | 9 | # OPTIONS 10 | 11 | PACKAGE = "com.tencent.mobileqq" 12 | 13 | # OPTIONS END 14 | 15 | @functools.cache 16 | def isOnTermux() -> bool: 17 | if ( 18 | platform.system() == "Linux" 19 | and "ANDROID_ROOT" in os.environ.keys() 20 | and ( 21 | os.path.exists("/data/data/com.termux") 22 | or ("TERMUX_VERSION" in os.environ.keys()) 23 | ) 24 | ): 25 | return True 26 | return False 27 | 28 | 29 | general_script = """ 30 | const module_name = "libkernel.so" 31 | 32 | function hook(){ 33 | function buf2hex(buffer) { 34 | const byteArray = new Uint8Array(buffer); 35 | const hexParts = []; 36 | for(let i = 0; i < byteArray.length; i++) { 37 | const hex = byteArray[i].toString(16); 38 | const paddedHex = ('00' + hex).slice(-2); 39 | hexParts.push(paddedHex); 40 | } 41 | return '0x' + hexParts.join(', 0x'); 42 | } 43 | function buf2str(buffer) { 44 | let result = ""; 45 | const byteArray = new Uint8Array(buffer); 46 | for (let i = 0; i < byteArray.length; i++) { 47 | result += String.fromCharCode(byteArray[i]); 48 | } 49 | return result; 50 | } 51 | var kernel_util = null; 52 | var process_Obj_Module_Arr = Process.enumerateModules(); 53 | for(var i = 0; i < process_Obj_Module_Arr.length; i++) { 54 | if(process_Obj_Module_Arr[i].path.indexOf("libkernel.so")!=-1) { 55 | console.log("模块名称:",process_Obj_Module_Arr[i].name); 56 | console.log("模块地址:",process_Obj_Module_Arr[i].base); 57 | console.log("大小:",process_Obj_Module_Arr[i].size); 58 | console.log("文件系统路径",process_Obj_Module_Arr[i].path); 59 | kernel_util = process_Obj_Module_Arr[i]; 60 | }} 61 | if(kernel_util == null) { 62 | send("libkernel.so not loaded. exit.") 63 | } else { 64 | function single_function(pattern) { 65 | pattern = pattern.replaceAll("##", "").replaceAll(" ", "").toLowerCase().replace(/\\s/g,'').replace(/(.{2})/g,"$1 "); 66 | send("定位函数使用的序列: " + pattern) 67 | var akey_function_list = Memory.scanSync(kernel_util.base, kernel_util.size, pattern); 68 | if (akey_function_list.length == 0) { 69 | send("Pattern NOT FOUND!! EXIT!!") 70 | return null; 71 | } 72 | if (akey_function_list.length > 1) { 73 | send("Multi-pattern FOUND!! Take first item.") 74 | } 75 | send("Attach key_v2_function addr: " + akey_function_list[0]['address']) 76 | return akey_function_list[0]['address']; 77 | } 78 | 79 | const key_v2_function = single_function("FD 7B BD A9 F6 57 01 A9 F4 4F 02 A9 FD 03 00 91 F6 03 01 AA F5 03 00 AA ?? ?? ?? ?? F3 03 03 2A F4 03 02 AA") 80 | 81 | if(key_v2_function != null) Interceptor.attach(key_v2_function, { 82 | onEnter: function(args) { 83 | let nk = args[3].toInt32(); 84 | let pk = args[2].readByteArray(nk); 85 | console.log("¦- targetDB: " + args[0]); 86 | console.log("¦- *zDb: " + args[1].readUtf8String()); 87 | console.log("¦- *pkey: " + buf2str(pk)); 88 | console.log("¦- *pkey-hex: " + buf2hex(pk)); 89 | console.log("¦- nKey: " + nk); 90 | }, 91 | }); 92 | } 93 | } 94 | 95 | hook() 96 | """ 97 | 98 | version_scripts = { 99 | "8.9.58": general_script, 100 | "8.9.63": general_script, 101 | "8.9.68": general_script, 102 | "8.9.76": general_script, 103 | } 104 | 105 | if __name__ == "__main__": 106 | if len(sys.argv) > 2: 107 | print("用法: [qq.version.number]") 108 | print("支持的版本:", *version_scripts.keys()) 109 | print("例:", __file__, "8.9.58") 110 | sys.exit(1) 111 | jscode = general_script 112 | if len(sys.argv) == 2 and sys.argv[1] in version_scripts.keys(): 113 | jscode = version_scripts[sys.argv[1]] 114 | else: 115 | print("使用默认版本注入脚本") 116 | 117 | print("仍在测试...") 118 | print("请先关闭 Magisk Hide 与 Shamiko") 119 | print("请先禁用 SELinux") 120 | print( 121 | "请先打开 QQ 并登录,进入主界面,然后运行该脚本,等待数秒后退出登录并重新登录。" 122 | ) 123 | print("若失败,可尝试彻底关闭 QQ 后直接运行") 124 | print("理论支持 Termux 与 桌面操作系统 运行") 125 | print("请勿使用 x86 或 x64 系统上的安卓模拟器。") 126 | print("适用版本:") 127 | print("https://downv6.qq.com/qqweb/QQ_1/android_apk/qq_8.9.58.11050_64.apk") 128 | print("https://github.com/QQBackup/QQ-History-Backup/issues/9") 129 | print( 130 | """Termux 环境具体命令: 131 | sudo friendly # 重命名后的 frida-server 132 | python android_get_key.py 133 | """ 134 | ) 135 | 136 | if isOnTermux(): 137 | device = frida.get_remote_device() 138 | pid_command = f"su -c 'pidof {PACKAGE}'" 139 | else: 140 | device = frida.get_usb_device() 141 | pid_command = f"adb shell su -c 'pidof {PACKAGE}'" 142 | running = True 143 | try: 144 | pid = int( 145 | subprocess.check_output(pid_command, shell=True) 146 | .decode() 147 | .strip() 148 | .split(" ")[0] 149 | ) 150 | except: 151 | running = False 152 | if running: 153 | print(PACKAGE + " is already running", pid) 154 | session = device.attach(pid) 155 | script = session.create_script(jscode) 156 | else: 157 | pid = device.spawn([PACKAGE]) 158 | session = device.attach(pid) 159 | script = session.create_script(jscode) 160 | device.resume(pid) 161 | print("QQ running!! pid = %d" % pid) 162 | 163 | def on_message(message, data): 164 | if message["type"] == "send": 165 | toprint = message["payload"] 166 | else: 167 | toprint = message 168 | toprint = str(toprint) 169 | print(toprint) 170 | 171 | script.on("message", on_message) 172 | script.load() 173 | print("Frida script injected.") 174 | sys.stdin.read() 175 | -------------------------------------------------------------------------------- /docs/public/files/windows_ntqq_key.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import argparse 5 | import ctypes 6 | import json 7 | from ctypes import wintypes 8 | import frida 9 | import psutil 10 | import requests 11 | import win32process 12 | import winreg 13 | 14 | PROCESS_ALL_ACCESS = 0x1F0FFF 15 | PROCESS_NAME = "QQ.exe" 16 | OFFSET_URL = "https://docs.aaqwq.top/files/QQ_Offset.json" 17 | 18 | session = None 19 | offset_data = {} 20 | version = None 21 | 22 | ReadProcessMemory = ctypes.windll.kernel32.ReadProcessMemory 23 | ReadProcessMemory.argtypes = [ 24 | wintypes.HANDLE, wintypes.LPCVOID, wintypes.LPVOID, ctypes.c_size_t, 25 | ctypes.POINTER(ctypes.c_size_t) 26 | ] 27 | ReadProcessMemory.restype = wintypes.BOOL 28 | 29 | 30 | def get_offset_data_from_url(url): 31 | try: 32 | response = requests.get(url, timeout=10) 33 | response.raise_for_status() 34 | return response.json() 35 | except requests.exceptions.RequestException as e: 36 | print(f"错误: 获取在线偏移量数据失败: {e}") 37 | sys.exit(1) 38 | except json.JSONDecodeError: 39 | print("错误: 无法解析从URL返回的JSON数据。") 40 | sys.exit(1) 41 | 42 | 43 | def open_process(pid): 44 | return ctypes.windll.kernel32.OpenProcess(PROCESS_ALL_ACCESS, False, pid) 45 | 46 | 47 | def get_module_base_address(process_handle, module_name): 48 | try: 49 | modules = win32process.EnumProcessModules(process_handle) 50 | for module in modules: 51 | module_path = win32process.GetModuleFileNameEx(process_handle, module) 52 | if module_name.lower() in os.path.basename(module_path).lower(): 53 | return module 54 | except win32process.error as e: 55 | if e.winerror != 299: 56 | raise 57 | return None 58 | 59 | 60 | def read_memory(process_handle, address, size): 61 | buffer = ctypes.create_string_buffer(size) 62 | bytesRead = ctypes.c_size_t(0) 63 | if ReadProcessMemory(process_handle, address, buffer, size, ctypes.byref(bytesRead)): 64 | return buffer.raw 65 | raise ctypes.WinError() 66 | 67 | 68 | def is_qq_running(): 69 | return any(PROCESS_NAME.lower() in p.name().lower() for p in psutil.process_iter(['name'])) 70 | 71 | 72 | def wait_for_qq_to_start(): 73 | print("正在等待QQ启动...") 74 | while True: 75 | pids = {p.pid for p in psutil.process_iter(['pid', 'name']) if PROCESS_NAME.lower() in p.info['name'].lower()} 76 | ppids = {p.ppid() for p in psutil.process_iter(['name', 'ppid']) if PROCESS_NAME.lower() in p.info['name'].lower()} 77 | main_pids = pids.intersection(ppids) 78 | if main_pids: 79 | new_pid = main_pids.pop() 80 | print(f"检测到QQ主进程 (PID: {new_pid})。") 81 | time.sleep(3) 82 | return new_pid 83 | time.sleep(1) 84 | 85 | 86 | def on_key_found(pid, key): 87 | print("\n" + "="*50) 88 | print(f"成功获取到密钥!") 89 | print(f" - 密钥: {key}") 90 | print("="*50 + "\n") 91 | 92 | 93 | def on_message(pid, message, data): 94 | if message.get('type') == 'send': 95 | payload = message.get('payload', {}) 96 | if isinstance(payload, dict) and 'key' in payload: 97 | on_key_found(pid, payload['key']) 98 | else: 99 | print(f"[*] Script message: {payload}") 100 | elif message.get('type') == 'error': 101 | print(f"[!] Script error: {message.get('description')}") 102 | os._exit(1) 103 | 104 | 105 | def get_qq_version(): 106 | try: 107 | with winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Tencent\QQNT", 0, winreg.KEY_READ) as key: 108 | return winreg.QueryValueEx(key, "Version")[0] 109 | except Exception: 110 | return None 111 | 112 | 113 | def attach_to_qq(pid, script_content): 114 | global session 115 | print(f"正在尝试附加到 QQ.exe (PID: {pid})...") 116 | try: 117 | session = frida.attach(pid) 118 | script = session.create_script(script_content) 119 | script.on('message', lambda msg, data: on_message(pid, msg, data)) 120 | script.load() 121 | except frida.ProcessNotFoundError: 122 | print(f"错误: 进程 {pid} 未找到,可能已关闭。") 123 | sys.exit(1) 124 | except Exception as e: 125 | print(f"附加到QQ时发生未知错误: {e}") 126 | sys.exit(1) 127 | 128 | 129 | if __name__ == "__main__": 130 | parser = argparse.ArgumentParser(description="从运行中的QQNT进程获取密钥。") 131 | parser.add_argument('--pid', type=int, help='直接指定一个已在运行的QQ进程PID以获取其密钥。') 132 | args = parser.parse_args() 133 | 134 | print(f"正在从 {OFFSET_URL} 获取偏移量数据...") 135 | offset_data = get_offset_data_from_url(OFFSET_URL) 136 | 137 | version = get_qq_version() 138 | print(f"当前QQ版本: {version}") 139 | 140 | if version not in offset_data: 141 | print("无法找到用户配置文件,请手动输入:") 142 | while True: 143 | user_input = input("请输入基址偏移(0x开头十六进制):").strip() 144 | if user_input.lower().startswith("0x"): 145 | try: 146 | key_offset = int(user_input, 16) 147 | contributor = "未知" 148 | timestamp = "未知" 149 | break 150 | except ValueError: 151 | print("输入格式错误,请输入有效的十六进制数字") 152 | else: 153 | print("请输入以0x开头的十六进制数字") 154 | else: 155 | version_data = offset_data[version] 156 | offsets = version_data["offsets"] 157 | contributor = version_data.get("contributor", "未知") 158 | timestamp = version_data.get("timestamp", "未知") 159 | print(f"版本 {version} 的基址由 {contributor} 贡献(提交时间:{timestamp})") 160 | key_offset = int(offsets[0], 16) 161 | 162 | script_content = f""" 163 | (function () {{ 164 | const baseAddr = Module.findBaseAddress("wrapper.node"); 165 | if (!baseAddr) return; 166 | const hookAddr = baseAddr.add({key_offset}); 167 | Interceptor.attach(hookAddr, {{ 168 | onEnter: function (args) {{ 169 | try {{ 170 | const key = Memory.readAnsiString(args[2], 16); 171 | if (key && key.length > 0) {{ 172 | send({{ key: key.split('\\x00')[0] }}); 173 | Interceptor.detachAll(); 174 | }} 175 | }} catch (e) {{}} 176 | }} 177 | }}); 178 | }})(); 179 | """ 180 | 181 | target_pid = None 182 | if args.pid: 183 | if not psutil.pid_exists(args.pid) or PROCESS_NAME.lower() not in psutil.Process(args.pid).name().lower(): 184 | print(f"错误: 指定的PID {args.pid} 无效或不是QQ进程。") 185 | sys.exit(1) 186 | target_pid = args.pid 187 | else: 188 | if is_qq_running(): 189 | print("检测到QQ正在运行,请退出QQ后重新运行...") 190 | while is_qq_running(): 191 | time.sleep(1) 192 | print("正在等待附加进程。") 193 | target_pid = wait_for_qq_to_start() 194 | 195 | attach_to_qq(target_pid, script_content) 196 | print("\n已成功注入进程,查找密钥……") 197 | 198 | try: 199 | sys.stdin.read() 200 | except KeyboardInterrupt: 201 | print("\n用户终止操作。") 202 | finally: 203 | if session: 204 | session.detach() 205 | -------------------------------------------------------------------------------- /docs/view/db_file_analysis/group_info.db.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: group_info.db 3 | order: 3 4 | --- 5 | 6 | # group_info.db 7 | | 完成解析 | 表名 | 分析 | 8 | | -------- | ----------------------- | ------------------------------ | 9 | | ✅ | doubt_group_notify_list | 过滤群通知 | 10 | | ✅ | group_bulletin | 群公告(只有最新一条会被存贮) | 11 | | ✅ | group_detail_info_ver1 | 群聊更多信息 | 12 | | ✅ | group_essence | 群精华消息 | 13 | | ✅ | group_list | 群聊名称列表 | 14 | | ✅ | group_member3 | 群成员信息 | 15 | | ✅ | group_member_level_info | 群等级头衔信息 | 16 | | ✅ | group_notify_list | 群通知 | 17 | 18 | ## `group_notify_list`与`doubt_group_notify_list` 19 | 群通知 20 | 21 | | 列名 | 类型 | 含义 | 说明 | 22 | | ----- | ----------------------------------------- | ------------ | --------------------------------------------- | 23 | | 61001 | int | 消息时间戳 | 删除末三位并使用毫秒级时间戳 | 24 | | 61002 | int | 状态 | 详见下方说明 | 25 | | 61003 | int | 验证状态 | 1为过滤,0为正常,2为同意,3为拒绝,4为忽略 | 26 | | 61004 | protobuf | 群组信息 | 群号与群昵称 | 27 | | 61005 | protobuf | 被操作者 | nt_uid与QQ昵称 | 28 | | 61006 | protobuf | 操作者 | 显示于group_notify_list,nt_uid 与QQ昵称 | 29 | | 61007 | protobuf | 操作人信息 | 显示于doubt_group_notify_list,nt_uid与QQ昵称 | 30 | | 61008 | int | 操作时间戳 | 秒级时间戳 | 31 | | 61009 | | | | 32 | | 61010 | str | 对方附加说明 | | 33 | | 61011 | str | 系统附加说明 | 如风险提示 | 34 | 35 | 61001说明 36 | 37 | 申请加群:1 38 | 39 | 被设置为管理员:3 40 | 41 | 被移出群聊:6 42 | 43 | 被管理员拒绝加入:11 44 | 45 | 自主退出群聊:13 46 | 47 | 被取消管理员权限:15 48 | 49 | ## `group_bulletin` 50 | 群公告 51 | 52 | | 列名 | 类型 | 含义 | 说明 | 53 | | ----- | ----------------------------------------- | --------------- | -------------------- | 54 | | 60001 | int | peerUid(群号) | | 55 | | 64205 | protobuf | 群公告内容 | protobuf格式,详见下 | 56 | 57 | 64205为`protobuf`格式 58 | 59 | | Byte Range | Field Number | Type | 含义 | 60 | | --------------- | ------------ | -------- | ----------------------- | 61 | | 0-1089 | 64205 | protobuf | | 62 | | ├─ 0-8 | 60001 | varint | 群号 | 63 | | ├─ 8-1084 | 64202 | protobuf | | 64 | | │ ├─ 0-8 | 60001 | varint | 群号 | 65 | | │ ├─ 8-36 | 64221 | string | 发布公告者nt_uid | 66 | | │ ├─ 40-76 | 64223 | string | fid | 67 | | │ ├─ 80-88 | 64225 | varint | msgTime(消息发布时间) | 68 | | │ ├─ 88-96 | 64226 | varint | ctime(公告发布时间) | 69 | | │ └─ 4-17 | 64452 | string | 群公告内容 | 70 | 71 | ## `group_detail_info_ver1` 72 | 群聊更多信息 73 | 74 | | 列名 | 类型 | 含义 | 说明 | 75 | | ----- | ----------------------------------------- | ---------------- | ------------------------- | 76 | | 60001 | int | 群号 | | 77 | | 60007 | str | 群名称 | | 78 | | 60216 | protobuf | 最新一条置顶公告 | 仅存贮外显部分内容* | 79 | | 60217 | protobuf | 群描述* | | 80 | | 60026 | str | 群备注* | | 81 | | 60002 | str | 群主nt_uid | | 82 | | 60004 | int | 建群时间 | | 83 | | 60005 | int | 群聊规模 | 最大可容纳人数 | 84 | | 60006 | int | 成员总人数 | | 85 | | 60218 | str | 群标签* | | 86 | | 60224 | str | 入群问题* | | 87 | | 60240 | protobuf | 群描述 | 旧版QQ迁移数据 | 88 | | 60340 | int | 退群标志 | 0为群成员,为已不是群成员 | 89 | 90 | *表示不一定存在数据 91 | 92 | *注意到`group_list`表中信息与`group_detail_info_ver1`对应,不再单独分析*·~~如果有大佬能帮忙分析最好了~~ 93 | 94 | ## `group_member3` 95 | 群成员信息 96 | 97 | | 列名 | 类型 | 含义 | 说明 | 98 | | ----- | ---- | ---------------------- | ---------------------- | 99 | | 64003 | str | 群昵称* | 未设置为空 | 100 | | 20002 | str | QQ昵称 | | 101 | | 60001 | int | 所在群聊群号 | | 102 | | 1000 | str | nt_uid | | 103 | | 1002 | int | uin | QQ号 | 104 | | 64007 | int | 入群时间 | | 105 | | 64008 | int | 最后发言时间 | | 106 | | 64009 | int | 最近一次禁言解封时间戳 | | 107 | | 64010 | int | 管理员标志 | 0为普通成员,1为管理员 | 108 | | 64015 | int | | | 109 | | 64016 | int | 是否为群成员 | 0为是,1为已退群 | 110 | | 64023 | str | 群自定义头衔* | | 111 | | 64035 | int | 群成员等级 | | 112 | 113 | ## `group_member_level_info` 114 | 群等级头衔信息 115 | 116 | | 列名 | 类型 | 含义 | 说明 | 117 | | ----- | ----------------------------------------- | -------------- | ---- | 118 | | 60001 | int | 群号 | | 119 | | 67100 | int | 群等级 | | 120 | | 67103 | protobuf | 默认群头衔信息 | | 121 | 122 | ## `group_essence` 123 | 精华消息设置提醒 124 | 125 | | 列名 | 类型 | 含义 | 说明 | 126 | | ----- | ---- | ----------------- | -------------------------------------- | 127 | | 60001 | int | 群号 | | 128 | | 67501 | int | msgSeq 群聊消息ID | 在每个群聊中依次递增 | 129 | | 67502 | int | msgRandom | 消息随机值,用于对消息去重(用于定位) | 130 | | 67503 | int | uin | 消息发送者QQ号 | 131 | | 67504 | str | QQ昵称 | 发送者QQ昵称 | 132 | | 67505 | int | 设置状态 | 1为被设置为精华,2为已被取消精华 | 133 | | 67506 | int | uin | 设置精华者QQ号 | 134 | | 67507 | str | QQ昵称 | 设置精华者QQ昵称 | 135 | | 67508 | int | 时间戳 | 设置精华的时间 | 136 | 137 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/style/custom-block.css: -------------------------------------------------------------------------------- 1 | /* 默认浅色模式 */ 2 | :root { 3 | --custom-block-info-left: #cccccc; 4 | --custom-block-info-bg: #fafafa; 5 | 6 | --custom-block-tip-left: #009400; 7 | --custom-block-tip-bg: #e6f6e6; 8 | 9 | --custom-block-warning-left: #e6a700; 10 | --custom-block-warning-bg: #fff8e6; 11 | 12 | --custom-block-danger-left: #e13238; 13 | --custom-block-danger-bg: #ffebec; 14 | 15 | --custom-block-note-left: #4cb3d4; 16 | --custom-block-note-bg: #eef9fd; 17 | 18 | --custom-block-important-left: #a371f7; 19 | --custom-block-important-bg: #f4eefe; 20 | 21 | --custom-block-caution-left: #e0575b; 22 | --custom-block-caution-bg: #fde4e8; 23 | } 24 | 25 | /* 深色模式 */ 26 | .dark { 27 | --custom-block-info-left: #cccccc; 28 | --custom-block-info-bg: #474748; 29 | 30 | --custom-block-tip-left: #009400; 31 | --custom-block-tip-bg: #003100; 32 | 33 | --custom-block-warning-left: #e6a700; 34 | --custom-block-warning-bg: #4d3800; 35 | 36 | --custom-block-danger-left: #e13238; 37 | --custom-block-danger-bg: #4b1113; 38 | 39 | --custom-block-note-left: #4cb3d4; 40 | --custom-block-note-bg: #193c47; 41 | 42 | --custom-block-important-left: #a371f7; 43 | --custom-block-important-bg: #230555; 44 | 45 | --custom-block-caution-left: #e0575b; 46 | --custom-block-caution-bg: #391c22; 47 | } 48 | 49 | 50 | /* 标题字体大小 */ 51 | .custom-block-title { 52 | font-size: 16px; 53 | } 54 | 55 | /* info容器:背景色、左侧 */ 56 | .custom-block.info { 57 | border-left: 5px solid var(--custom-block-info-left); 58 | background-color: var(--custom-block-info-bg); 59 | } 60 | 61 | /* info容器:svg图 */ 62 | .custom-block.info [class*="custom-block-title"]::before { 63 | content: ''; 64 | background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-11v6h2v-6h-2zm0-4v2h2V7h-2z' fill='%23ccc'/%3E%3C/svg%3E"); 65 | width: 20px; 66 | height: 20px; 67 | display: inline-block; 68 | vertical-align: middle; 69 | position: relative; 70 | margin-right: 4px; 71 | left: -5px; 72 | top: -1px; 73 | } 74 | 75 | /* 提示容器:边框色、背景色、左侧 */ 76 | .custom-block.tip { 77 | /* border-color: var(--custom-block-tip); */ 78 | border-left: 5px solid var(--custom-block-tip-left); 79 | background-color: var(--custom-block-tip-bg); 80 | } 81 | 82 | /* 提示容器:svg图 */ 83 | .custom-block.tip [class*="custom-block-title"]::before { 84 | content: ''; 85 | background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23009400' d='M7.941 18c-.297-1.273-1.637-2.314-2.187-3a8 8 0 1 1 12.49.002c-.55.685-1.888 1.726-2.185 2.998H7.94zM16 20v1a2 2 0 0 1-2 2h-4a2 2 0 0 1-2-2v-1h8zm-3-9.995V6l-4.5 6.005H11v4l4.5-6H13z'/%3E%3C/svg%3E"); 86 | width: 20px; 87 | height: 20px; 88 | display: inline-block; 89 | vertical-align: middle; 90 | position: relative; 91 | margin-right: 4px; 92 | left: -5px; 93 | top: -2px; 94 | } 95 | 96 | /* 警告容器:背景色、左侧 */ 97 | .custom-block.warning { 98 | border-left: 5px solid var(--custom-block-warning-left); 99 | background-color: var(--custom-block-warning-bg); 100 | } 101 | 102 | /* 警告容器:svg图 */ 103 | .custom-block.warning [class*="custom-block-title"]::before { 104 | content: ''; 105 | background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M576.286 752.57v-95.425q0-7.031-4.771-11.802t-11.3-4.772h-96.43q-6.528 0-11.3 4.772t-4.77 11.802v95.424q0 7.031 4.77 11.803t11.3 4.77h96.43q6.528 0 11.3-4.77t4.77-11.803zm-1.005-187.836 9.04-230.524q0-6.027-5.022-9.543-6.529-5.524-12.053-5.524H456.754q-5.524 0-12.053 5.524-5.022 3.516-5.022 10.547l8.538 229.52q0 5.023 5.022 8.287t12.053 3.265h92.913q7.032 0 11.803-3.265t5.273-8.287zM568.25 95.65l385.714 707.142q17.578 31.641-1.004 63.282-8.538 14.564-23.354 23.102t-31.892 8.538H126.286q-17.076 0-31.892-8.538T71.04 866.074q-18.582-31.641-1.004-63.282L455.75 95.65q8.538-15.57 23.605-24.61T512 62t32.645 9.04 23.605 24.61z' fill='%23e6a700'/%3E%3C/svg%3E"); 106 | width: 20px; 107 | height: 20px; 108 | display: inline-block; 109 | vertical-align: middle; 110 | position: relative; 111 | margin-right: 4px; 112 | left: -5px; 113 | } 114 | 115 | /* 危险容器:背景色、左侧 */ 116 | .custom-block.danger { 117 | border-left: 5px solid var(--custom-block-danger-left); 118 | background-color: var(--custom-block-danger-bg); 119 | } 120 | 121 | /* 危险容器:svg图 */ 122 | .custom-block.danger [class*="custom-block-title"]::before { 123 | content: ''; 124 | background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2c5.523 0 10 4.477 10 10v3.764a2 2 0 0 1-1.106 1.789L18 19v1a3 3 0 0 1-2.824 2.995L14.95 23a2.5 2.5 0 0 0 .044-.33L15 22.5V22a2 2 0 0 0-1.85-1.995L13 20h-2a2 2 0 0 0-1.995 1.85L9 22v.5c0 .171.017.339.05.5H9a3 3 0 0 1-3-3v-1l-2.894-1.447A2 2 0 0 1 2 15.763V12C2 6.477 6.477 2 12 2zm-4 9a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm8 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4z' fill='%23e13238'/%3E%3C/svg%3E"); 125 | width: 20px; 126 | height: 20px; 127 | display: inline-block; 128 | vertical-align: middle; 129 | position: relative; 130 | margin-right: 4px; 131 | left: -5px; 132 | top: -1px; 133 | } 134 | 135 | /* NOTE容器:背景色、左侧 */ 136 | .custom-block.note { 137 | border-left: 5px solid var(--custom-block-note-left); 138 | background-color: var(--custom-block-note-bg); 139 | } 140 | 141 | /* NOTE容器:svg图 */ 142 | .custom-block.note [class*="custom-block-title"]::before { 143 | content: ''; 144 | background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm-1-11v6h2v-6h-2zm0-4v2h2V7h-2z' fill='%234cb3d4'/%3E%3C/svg%3E"); 145 | width: 20px; 146 | height: 20px; 147 | display: inline-block; 148 | vertical-align: middle; 149 | position: relative; 150 | margin-right: 4px; 151 | left: -5px; 152 | top: -1px; 153 | } 154 | 155 | /* IMPORTANT容器:背景色、左侧 */ 156 | .custom-block.important { 157 | border-left: 5px solid var(--custom-block-important-left); 158 | background-color: var(--custom-block-important-bg); 159 | } 160 | 161 | /* IMPORTANT容器:svg图 */ 162 | .custom-block.important [class*="custom-block-title"]::before { 163 | content: ''; 164 | background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1024 1024'%3E%3Cpath d='M512 981.333a84.992 84.992 0 0 1-84.907-84.906h169.814A84.992 84.992 0 0 1 512 981.333zm384-128H128v-42.666l85.333-85.334v-256A298.325 298.325 0 0 1 448 177.92V128a64 64 0 0 1 128 0v49.92a298.325 298.325 0 0 1 234.667 291.413v256L896 810.667v42.666zm-426.667-256v85.334h85.334v-85.334h-85.334zm0-256V512h85.334V341.333h-85.334z' fill='%23a371f7'/%3E%3C/svg%3E"); 165 | width: 20px; 166 | height: 20px; 167 | display: inline-block; 168 | vertical-align: middle; 169 | position: relative; 170 | margin-right: 4px; 171 | left: -5px; 172 | top: -1px; 173 | } 174 | 175 | /* CAUTION容器:背景色、左侧 */ 176 | .custom-block.caution { 177 | border-left: 5px solid var(--custom-block-caution-left); 178 | background-color: var(--custom-block-caution-bg); 179 | } 180 | 181 | /* CAUTION容器:svg图 */ 182 | .custom-block.caution [class*="custom-block-title"]::before { 183 | content: ''; 184 | background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12 2c5.523 0 10 4.477 10 10v3.764a2 2 0 0 1-1.106 1.789L18 19v1a3 3 0 0 1-2.824 2.995L14.95 23a2.5 2.5 0 0 0 .044-.33L15 22.5V22a2 2 0 0 0-1.85-1.995L13 20h-2a2 2 0 0 0-1.995 1.85L9 22v.5c0 .171.017.339.05.5H9a3 3 0 0 1-3-3v-1l-2.894-1.447A2 2 0 0 1 2 15.763V12C2 6.477 6.477 2 12 2zm-4 9a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm8 0a2 2 0 1 0 0 4 2 2 0 0 0 0-4z' fill='%23e13238'/%3E%3C/svg%3E"); 185 | width: 20px; 186 | height: 20px; 187 | display: inline-block; 188 | vertical-align: middle; 189 | position: relative; 190 | margin-right: 4px; 191 | left: -5px; 192 | top: -1px; 193 | } -------------------------------------------------------------------------------- /docs/decrypt/NTQQ (iOS).md: -------------------------------------------------------------------------------- 1 | --- 2 | title: NTQQ(iOS) 3 | order: 7 4 | --- 5 | 6 | # NTQQ(iOS) 7 | 8 | 参考了 以及 中的各平台教程。 9 | 10 | ## I 环境配置 11 | 12 | ### 越狱环境 13 | 14 | 可参考 [Frida 文档 - With Jailbreak](https://frida.re/docs/ios/#with-jailbreak) 15 | 16 | 在 iOS 设备上操作 17 | 18 | ~1. Cydia / Sileo / Zebra 添加 Frida 官方软件源 `https://build.frida.re`~ 19 | 20 | > [!Important] 重要提醒 21 | > 自 Frida 17.0.0 开始,`Module.findBaseAddress()` 被移除, \ 22 | > 因此安装的是老版本,否则执行脚本将报错 `TypeError: not a function` \ 23 | > 更新日志:https://frida.re/news/2025/05/17/frida-17-0-0-released/ 24 | 25 | 26 | 1. 前往 [Frida Release 16.7.19](https://github.com/frida/frida/releases/tag/16.7.19) 下载 Frida 27 | - 下载 `frida_17.2.17_iphoneos-arm64.deb`(有根越狱则 `frida_17.2.17_iphoneos-arm.deb`) 28 | 29 | 2. 用 Cydia / Sileo / Zebra 安装上一步下载好的安装包 30 | 31 | 3. 启动 Frida 服务器 32 | 33 | 在终端(NewTerm 之类的终端App,或SSH)运行: 34 | 35 | ```sh 36 | frida-server -v 37 | ``` 38 | 39 | 如果端口被占用,更换其他端口,如: 40 | 41 | ```sh 42 | frida-server --listen='0.0.0.0:27043' -v 43 | ``` 44 | 45 | 启动 Frida 服务端。 46 | 47 | > 命令前部加上 `sudo` 赋予 root 用户权限也许会更好 48 | 49 | ### 免越狱环境 50 | 51 | - 需要有 **TrollStore 巨魔** 环境 52 | - 安装教程:[cfw iOS Guide - Installing TrollStore](https://ios.cfw.guide/installing-trollstore/) 53 | 54 | - 参考链接:[Frida 文档 - Without Jailbreak](https://frida.re/docs/ios/#without-jailbreak) 55 | 56 | #### 开始 57 | 58 | 1. App 脱壳,可参考[下一章节](#app-脱壳)(需要得到完整的 IPA 安装包) 59 | 60 | > [!Important] 重要提醒 61 | > 由于自 Frida 17.0.0 开始,`Module.findBaseAddress()` 被移除, \ 62 | > 因此安装的是老版本,否则执行脚本将报错 `TypeError: not a function` \ 63 | > 更新日志:https://frida.re/news/2025/05/17/frida-17-0-0-released/ 64 | 65 | 2. 下载 Frida Gadget 动态链接库 **`frida-gadget-x.x.x-ios-universal.dylib.gz`** \ 66 | 并解压得到 `frida-gadget-ios-universal.dylib` 67 | - [Frida Release 16.7.19](https://github.com/frida/frida/releases/tag/16.7.19) 68 | 69 | 3. 注入 Frida Gadget 动态链接库,有两种方法,Sideloadly 更方便且支持 Windows,命令行方式 optool 需要在 macOS 环境下进行 70 | 71 | #### 方式一、使用 Sideloadly 注入 Frida Gadget 72 | 73 | 直接照图进行配置: 74 | 75 | - 不允许自动更改 Bundle ID 76 | - 开启文件共享(即开放 App 沙盒 Documents 目录到系统自带的文件 App) 77 | - 注入动态链接库 78 | - 仅导出 IPA 79 | 80 | 然后点击 Start 开始导出 81 | 82 | ![Sideloadly 中的具体配置选项](/img/image-ios-5.webp) 83 | 84 | #### 方式二、使用 optool 注入 Frida Gadget 85 | 86 | 1. 解压脱壳得到的 IPA 安装包 87 | 88 | 2. 安装 optool,这里需要使用 `xcodebuild` 命令 89 | 90 | ```sh 91 | git clone https://github.com/alexzielenski/optool.git 92 | cd optool 93 | git submodule update --init --recursive 94 | xcodebuild 95 | ln -s $PWD/build/Release/optool /usr/local/bin/optool 96 | ``` 97 | 98 | 3. 将前面下载的 **`frida-gadget-ios-universal.dylib`** 放入 IPA 解压目录下的 `Payload/QQ.app/Frameworks` 目录 99 | 100 | ```sh 101 | cp frida-gadget-ios-universal.dylib Payload/QQ.app/Frameworks 102 | ``` 103 | 104 | 4. 用 optool 把动态链接库加载命令插入到 QQ 主程序中(此处路径无法自动补全,注意不要打错了) 105 | 106 | ```sh 107 | optool install -c load -p "@executable_path/Frameworks/frida-gadget-ios-universal.dylib" -t Payload/QQ.app/QQ 108 | ``` 109 | 110 | 运行成功的输出: 111 | 112 | ```plaintext 113 | Found thin header... 114 | Load command already exists 115 | Successfully inserted a LC_LOAD_DYLIB command for arm64 116 | Writing executable to Payload/QQ.app/QQ... 117 | ``` 118 | 119 | 5. 为了方便后续导出聊天记录数据库等文件,需要修改 App 配置,允许用户通过系统自带的文件 App 访问 App 沙盒 Documents 目录,具体操作可以网上查询( 120 | 121 | 6. 重新打包 IPA,可以直接压缩 `Payload` 目录为 zip 归档,然后重命名文件后缀为 `ipa`,把重新打包好的 IPA 安装包发送到 iOS 设备 122 | 123 | #### 安装重新打包好的 IPA 124 | 125 | 发送重新打包好的 QQ IPA 安装包到 iOS 设备,**使用 TrollStore 巨魔** 进行安装,如果提示安装失败,应用已存在,选择强制安装 126 | 127 | > 需要使用巨魔的原因:附带了 Frida Gadget 的 QQ 安装包需要以覆盖,或者更新的方式安装到设备,这样它才能读取到聊天记录,不然系统会分配新的沙盒空间。为了实现这一点,App Bundle ID 需要保持不变,即 `com.tencent.mqq`,但如果使用腾讯的 `com.tencent.*` 作为 Bundle ID,在签名过程中就会失败,因此需要绕过签名,即使用 TrollStore 安装。 128 | 129 | 将 iOS 设备有线连接至 PC,可以自行用 `frida-ps` 等工具测试一下 Frida 服务器是否正常工作(无线连接等可以参考 [Frida 文档 - Gadget](https://frida.re/docs/gadget/)) 130 | 131 | ## II 反编译 132 | 133 | 如果你的 QQ 版本和脚本 [ios_get_key.js](/files/ios_get_key.js) 内注释的版本一致,可直接跳过进入下一步 134 | 135 | 如果 QQ 版本不一致,但下一步的脚本 [ios_get_key.js](ios_get_key.js) 可以正常使用,也可以忽略本节 136 | 137 | ### App 脱壳 138 | 139 | 可选工具 140 | 141 | - dumpdecrypted () 142 | - frida-ios-dump () 143 | - AppsDump2(有图形界面的App,用起来很方便,不过似乎找不到它的仓库链接) 144 | ![AppsDump2](/img/image-ios-4.webp) 145 | - ... 146 | 147 | 只需要得到脱壳后的 Mach-O 二进制主程序即可,不需要完整的 IPA 安装包。 148 | 149 | ### 反编译 150 | 151 | 本篇示例 152 | 153 | - iOS QQ v9.0.1.620 154 | - SQLCipher v4.5.1 155 | 156 | **主要目的是获取 `sqlite3_key_v2` 函数位置**,页大小、纯文本文件大小、PBKDF2 迭代次数等通常是固定的。 157 | 158 | 1. 用例如 IDA 的反编译工具反编译脱壳得到的 Mach-O 二进制主程序 159 | 160 | 2. 搜索二进制片段 `sqlite3_key_v2`,可以找到日志文本,从而定位到SQLCipher C API 的 `sqlite3_key_v2` 函数在程序中的位置。第三个参数即为数据库密钥,根据传入的其他实参,还能得到更多信息 161 | - 右键 - 点击“List cross references to...” 即可查找引用 162 | 163 | > [!Note] 注意 164 | > 如果没有找到引用 `sqlite3_key_v2` 的代码,一般是因为 IDA 尚未解析完整个二进制程序,可以静默等待其处理一段时间后再尝试 165 | 166 | ```c 167 | int sqlite3_key_v2(sqlite3 *db, const char *zDb, const void *pKey, int nKey); 168 | ``` 169 | 170 | > 171 | 172 | 得到目标函数的位置为 `000000010DA1BFB4`。由于IDA基址设为了 `0000000100000000`,两者相减,\ 173 | **得到偏移量为 `0xDA1BFB4`,将其设置为脚本文件 [ios_get_key.js](/files/ios_get_key.js) 中的 `SQLLiteKeyV2Offset` 的值**。 174 | 175 | ```javascript 176 | // ios_get_key.js 177 | const SQLLiteKeyV2Offset = 0xDA1BFB4; 178 | ``` 179 | 180 | ![sqlite3_key_v2 函数在 IDA 中的汇编代码与伪代码](/img/image-ios-1.webp) 181 | 182 | 3. 根据调用关系 `sqlite3CodecAttach`->`sqlcipher_codec_ctx_init`->`sqlcipher_codec_ctx_set_pagesize` 可以确定页大小为 `4096` 183 | 184 | ![在 IDA 中分析 sqlcipher_codec_ctx_set_pagesize 的调用关系](/img/image-ios-3.webp) 185 | 186 | 4. 根据调用关系 \ 187 | `sqlite3CodecAttach`->`sqlcipher_codec_ctx_init`->`sqlcipher_codec_ctx_set_plaintext_header_size`,\ 188 | 用 Frida hook `sqlcipher_codec_ctx_set_plaintext_header_size` 函数,可以确定纯文本文件大小为 `0` 189 | 190 | ![在 IDA 中分析 sqlcipher_codec_ctx_set_pagesize 的调用关系](/img/image-ios-2.webp) 191 | 192 | ## III Frida 附加到 QQ 进程,获取密钥 193 | 194 | 1. **在 PC 上安装** frida(确保 Python 已安装) 195 | 196 | ```bash 197 | # pipx 可以把包安装到隔离的环境,避免依赖冲突 198 | pip install pipx 199 | pipx install frida 200 | frida --version 201 | ``` 202 | 203 | > [!Important] 重要提醒 204 | > 同上,由于自 Frida 17.0.0 开始,`Module.findBaseAddress()` 被移除, \ 205 | > 因此安装的是老版本 Frida,包括 PC 上作为客户端的 frida-tools \ 206 | > 更新日志:https://frida.re/news/2025/05/17/frida-17-0-0-released/ 207 | 208 | 2. 下载 [ios_get_key.js](/files/ios_get_key.js)(如果上一步已经下载过了可以跳过) 209 | 210 | 3. iOS 设备打开 QQ App 211 | 212 | 4. **PC Frida** 连接至 iOS 设备,终端运行命令以附加脚本到 QQ 进程 213 | 214 | 连接方式有两种: 215 | 216 | - USB 连接: 217 | 218 | ```shell 219 | frida -U QQ -l ios_get_key.js 220 | ``` 221 | 222 | - 网络连接 223 | 224 | ```shell 225 | frida -H <设备IP地址>: QQ -l ios_get_key.js 226 | ``` 227 | 228 | IP地址为iOS设备的IP地址,端口号在[第一步](#越狱环境)配置过。例如: 229 | 230 | ```shell 231 | frida -H 192.168.1.163:27043 QQ -l ios_get_key.js 232 | ``` 233 | 234 | > iOS frida-server 只能附加到已有进程,不能以 spawn 方式生成进程,原因暂不知 235 | 236 | 5. iOS 设备 QQ 进行登录操作 237 | 238 | 6. 查看 **PC 终端** 输出内容,可以看到一条条信息被输出,其中 239 | 240 | - **`pKey` 为数据库密钥** 241 | - **`zFilename` 为对应的数据库文件所在路径** 242 | 243 | 输出的数据库信息已经过筛选。 244 | 245 | 例如: 246 | 247 | ```plaintext 248 | +------------ 249 | ¦- db: 0x152ec8710 250 | ¦- *zDb: main 251 | ¦- *pkey: d3c1d0f05b2cxxxxxxxxxxc1ac161c29 252 | ¦- *pkey-hex: ... 253 | ¦- nKey: 32 254 | ¦+ 255 | ¦- zFilename: /var/mobile/Containers/Data/Application/22675923-xxxx-xxxx-xxxx-C1CB389E8E22/Documents/QQNT/DB/nt_db/nt_qq_15207xxxxxxxxxxxxxxxxx0d0be/nt_msg.db 256 | +------------ 257 | ¦- db: 0x152ec8710 258 | ¦- *zDb: main 259 | ¦- *pkey: d3c1d0f05b2cxxxxxxxxxxc1ac161c29 260 | ¦- *pkey-hex: ... 261 | ¦- nKey: 32 262 | ¦+ 263 | ¦- zFilename: /var/mobile/Containers/Data/Application/22675923-xxxx-xxxx-xxxx-C1CB389E8E22/Documents/QQNT/DB/nt_db/nt_qq_15207xxxxxxxxxxxxxxxxx0d0be/guild_msg.db 264 | +------------ 265 | ``` 266 | 267 | **复制其中的 `pKey` 和 `zFilename`** 268 | 269 | 7. 从 iOS 设备下载数据库文件 270 | - 越狱用户可使用 SFTP 或 Filza App 271 | - 免越狱方式直接用 iOS 自带的文件 App 查看 272 | 273 | 路径为上一步的 `zFilename` 所在目录的路径 274 | 275 | ```plaintext 276 | /var/mobile/Containers/Data/Application/22675923-xxxx-xxxx-xxxx-C1CB389E8E22/Documents/QQNT/DB/nt_db/nt_qq_15207xxxxxxxxxxxxxxxxx0d0be 277 | ``` 278 | 279 | 该目录下有数个数据库,除了一般的聊天记录以外还有(QQ频道记录?)更多数据。 280 | 281 | 8. 打开数据库 282 | 283 | 请参考 [NTQQ 解密数据库](decode_db.md)。 284 | -------------------------------------------------------------------------------- /docs/public/files/android_get_backup_key.js: -------------------------------------------------------------------------------- 1 | const module_name = "libmsgbackup.so" 2 | 3 | function hook(){ 4 | function buf2hex(buffer) { 5 | const byteArray = new Uint8Array(buffer); 6 | const hexParts = []; 7 | for(let i = 0; i < byteArray.length; i++) { 8 | const hex = byteArray[i].toString(16); 9 | const paddedHex = ('00' + hex).slice(-2); 10 | hexParts.push(paddedHex); 11 | } 12 | return '0x' + hexParts.join(', 0x'); 13 | } 14 | function buf2str(buffer) { 15 | let result = ""; 16 | const byteArray = new Uint8Array(buffer); 17 | for (let i = 0; i < byteArray.length; i++) { 18 | result += String.fromCharCode(byteArray[i] + ")"); 19 | } 20 | return result; 21 | } 22 | function hex2str(hex) { 23 | let str = ''; 24 | for (let i = 0; i < hex.length; i += 2) { 25 | let charCode = parseInt(hex.substr(i, 2), 16); 26 | str += String.fromCharCode(charCode); 27 | } 28 | return str; 29 | } 30 | var kernel_util = null; 31 | var process_Obj_Module_Arr = Process.enumerateModules(); 32 | for(var i = 0; i < process_Obj_Module_Arr.length; i++) { 33 | if(process_Obj_Module_Arr[i].path.indexOf(module_name)!=-1) { 34 | console.log("模块名称:",process_Obj_Module_Arr[i].name); 35 | console.log("模块地址:",process_Obj_Module_Arr[i].base); 36 | console.log("大小:",process_Obj_Module_Arr[i].size); 37 | console.log("文件系统路径",process_Obj_Module_Arr[i].path); 38 | kernel_util = process_Obj_Module_Arr[i]; 39 | kernel_util.size = 159744; // prevent access violation accessing >= 0x77f222a000 40 | }} 41 | if(kernel_util == null) { 42 | send(module_name + " not loaded. exit.") 43 | } else { 44 | function single_function(pattern) { 45 | pattern = pattern.replaceAll("##", "").replaceAll(" ", "").toLowerCase().replace(/\\s/g,'').replace(/(.{2})/g,"$1 "); 46 | send("定位函数使用的序列: " + pattern) 47 | var akey_function_list = Memory.scanSync(kernel_util.base, kernel_util.size, pattern); 48 | if (akey_function_list.length == 0) { 49 | send("Pattern NOT FOUND!! EXIT!!") 50 | return null; 51 | } 52 | if (akey_function_list.length > 1) { 53 | send("Multi-pattern FOUND!! Take first item.") 54 | } 55 | send("Attach key_function addr: " + akey_function_list[0]['address']) 56 | return akey_function_list[0]['address']; 57 | } 58 | 59 | const key_function = single_function("__single_function__parameter__") 60 | 61 | if(key_function != null) Interceptor.attach(key_function, { 62 | onEnter: function(args) { 63 | console.log("¦-------------------"); 64 | //console.log("¦- arg0: " + buf2hex(args[0].readByteArray(32)) + "(" + args[0] + ")"); 65 | //console.log("¦- arg1: " + buf2hex(args[1].readByteArray(32)) + "(" + args[1] + ")"); 66 | //console.log("¦- arg2: " + args[2].toInt32() + "(" + args[2] + ")"); 67 | //console.log("¦- arg3: " + args[3].toInt32() + "(" + args[3] + ")"); 68 | //console.log("¦- arg4: " + args[4].toInt32() + "(" + args[4] + ")"); 69 | //console.log("¦- arg5: " + buf2hex(args[5].readByteArray(32)) + "(" + args[5] + ")"); 70 | //console.log("¦- arg6: " + args[6].toInt32() + "(" + args[6] + ")"); 71 | //console.log("¦- arg7: " + args[7].toInt32() + "(" + args[7] + ")"); 72 | //console.log("¦- arg8: " + buf2hex(args[8].readByteArray(32)) + "(" + args[8] + ")"); 73 | //console.log("¦- arg9: " + buf2hex(args[9].readByteArray(32)) + "(" + args[9] + ")"); 74 | //console.log("¦- arg10:" + buf2hex(args[10].readByteArray(32)) + "(" + args[10] + ")"); 75 | if(args[7].toInt32() < 1000000000) 76 | console.log("¦- arg11:" + args[11].readUtf8String() + "(" + args[11] + ")"); 77 | //console.log("¦- arg12:" + buf2hex(args[12].readByteArray(32)) + "(" + args[12] + ")"); 78 | //console.log("¦- arg13:" + buf2hex(args[13].readByteArray(32)) + "(" + args[13] + ")"); 79 | //console.log("¦- arg14:" + buf2hex(args[14].readByteArray(32)) + "(" + args[14] + ")"); 80 | //console.log("¦- arg15:" + buf2hex(args[15].readByteArray(32)) + "(" + args[15] + ")"); 81 | //console.log("¦- arg16:" + buf2hex(args[16].readByteArray(32)) + "(" + args[16] + ")"); 82 | //console.log("¦- arg17:" + buf2hex(args[17].readByteArray(32)) + "(" + args[17] + ")"); 83 | //console.log("¦- arg18:" + buf2hex(args[18].readByteArray(32)) + "(" + args[18] + ")"); 84 | //console.log("¦- arg19:" + buf2hex(args[19].readByteArray(32)) + "(" + args[19] + ")"); 85 | //console.log("¦- arg20:" + buf2hex(args[20].readByteArray(32)) + "(" + args[20] + ")"); 86 | //console.log("¦- arg21:" + buf2hex(args[21].readByteArray(32)) + "(" + args[21] + ")"); 87 | //console.log("¦- arg22:" + buf2hex(args[22].readByteArray(32)) + "(" + args[22] + ")"); 88 | //console.log("¦- arg23:" + buf2hex(args[23].readByteArray(32)) + "(" + args[23] + ")"); 89 | //console.log("¦- arg24:" + buf2hex(args[24].readByteArray(32)) + "(" + args[24] + ")"); 90 | //console.log("¦- arg25:" + buf2hex(args[25].readByteArray(32)) + "(" + args[25] + ")"); 91 | //console.log("¦- arg26:" + buf2hex(args[26].readByteArray(32)) + "(" + args[26] + ")"); 92 | //console.log("¦- arg27:" + buf2hex(args[27].readByteArray(32)) + "(" + args[27] + ")"); 93 | //console.log("¦- arg28:" + buf2hex(args[28].readByteArray(32)) + "(" + args[28] + ")"); 94 | //console.log("¦- arg29:" + buf2hex(args[29].readByteArray(32)) + "(" + args[29] + ")"); 95 | //console.log("¦- arg30:" + buf2hex(args[30].readByteArray(32)) + "(" + args[30] + ")"); 96 | /* 97 | console.log("¦- arg31:" + args[31] + "(" + args[31] + ")"); 98 | console.log("¦- arg32:" + args[32] + "(" + args[32] + ")"); 99 | console.log("¦- arg33:" + args[33] + "(" + args[33] + ")"); 100 | console.log("¦- arg34:" + args[34] + "(" + args[34] + ")"); 101 | console.log("¦- arg35:" + args[35] + "(" + args[35] + ")"); 102 | console.log("¦- arg36:" + args[36] + "(" + args[36] + ")"); 103 | console.log("¦- arg37:" + args[37] + "(" + args[37] + ")"); 104 | console.log("¦- arg38:" + args[38] + "(" + args[38] + ")"); 105 | console.log("¦- arg39:" + args[39] + "(" + args[39] + ")"); 106 | console.log("¦- arg40:" + args[40] + "(" + args[40] + ")"); 107 | console.log("¦- arg41:" + args[41] + "(" + args[41] + ")"); 108 | console.log("¦- arg42:" + args[42] + "(" + args[42] + ")"); 109 | console.log("¦- arg43:" + args[43] + "(" + args[43] + ")"); 110 | console.log("¦- arg44:" + args[44] + "(" + args[44] + ")"); 111 | console.log("¦- arg45:" + args[45] + "(" + args[45] + ")"); 112 | console.log("¦- arg46:" + args[46] + "(" + args[46] + ")"); 113 | console.log("¦- arg47:" + args[47] + "(" + args[47] + ")"); 114 | console.log("¦- arg48:" + args[48] + "(" + args[48] + ")"); 115 | console.log("¦- arg49:" + args[49] + "(" + args[49] + ")"); 116 | console.log("¦- arg50:" + args[50] + "(" + args[50] + ")"); 117 | console.log("¦- arg51:" + args[51] + "(" + args[51] + ")"); 118 | console.log("¦- arg52:" + args[52] + "(" + args[52] + ")"); 119 | console.log("¦- arg53:" + args[53] + "(" + args[53] + ")"); 120 | console.log("¦- arg54:" + args[54] + "(" + args[54] + ")"); 121 | console.log("¦- arg55:" + args[55] + "(" + args[55] + ")"); 122 | console.log("¦- arg56:" + args[56] + "(" + args[56] + ")"); 123 | console.log("¦- arg57:" + args[57] + "(" + args[57] + ")"); 124 | console.log("¦- arg58:" + args[58] + "(" + args[58] + ")"); 125 | console.log("¦- arg59:" + args[59] + "(" + args[59] + ")"); 126 | console.log("¦- arg60:" + args[60] + "(" + args[60] + ")"); 127 | */ 128 | console.log("¦-------------------"); 129 | }, 130 | }); 131 | } 132 | } 133 | 134 | hook() 135 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/components/QQCachePath.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 145 | 146 | -------------------------------------------------------------------------------- /docs/public/files/pcqq_DANGER_rekey.py: -------------------------------------------------------------------------------- 1 | print("别用!!!!!!!") 2 | print("必定损坏原始数据库!") 3 | print("rekey 之后是不能用原先的 key 解锁的!") 4 | # print("理论上可能会损坏原数据库,慎用!如果要用请自行去除下一行的 exit()") 5 | exit() 6 | 7 | import frida 8 | import sys 9 | import psutil 10 | import shutil 11 | import time 12 | import os 13 | 14 | QQ_PID = None 15 | # GUI -> ['C:\\Program Files (x86)\\Tencent\\QQ\\Bin\\QQ.exe'] 16 | # 要hook的 -> ['C:\\Program Files (x86)\\Tencent\\QQ\\Bin\\QQ.exe', '/hosthwnd=2164594', '/hostname=QQ_IPC_{12345678-ABCD-12EF-9976-18373DEAB821}', '/memoryid=0', 'C:\\Program Files (x86)\\Tencent\\QQ\\Bin\\QQ.exe'] 17 | exit() 18 | for pid in psutil.pids(): 19 | p = psutil.Process(pid) 20 | if p.name() == "QQ.exe" and len(p.cmdline()) > 1: 21 | QQ_PID = pid 22 | del p 23 | break 24 | 25 | if QQ_PID is None: 26 | print("QQ not launched. exit.") 27 | sys.exit(1) 28 | print("QQ pid is:", QQ_PID) 29 | exit() 30 | demo_script = """ 31 | var pMessageBoxW = Module.findExportByName("user32.dll", 'MessageBoxA') 32 | var lpText = Memory.allocAnsiString("I'm New MessageBox"); 33 | var funMsgBox = new NativeFunction(pMessageBoxW, 'uint32',['uint32','pointer','pointer','uint32']); 34 | 35 | funMsgBox(0, ptr(lpText), ptr(lpText), 0); 36 | """ 37 | hook_script = """ 38 | function buf2hex(buffer) { 39 | const byteArray = new Uint8Array(buffer); 40 | const hexParts = []; 41 | for(let i = 0; i < byteArray.length; i++) { 42 | const hex = byteArray[i].toString(16); 43 | const paddedHex = ('00' + hex).slice(-2); 44 | hexParts.push(paddedHex); 45 | } 46 | return '0x' + hexParts.join(', 0x'); 47 | } 48 | 49 | const kernel_util = Module.load('KernelUtil.dll'); 50 | function single_function(pattern) { 51 | pattern = pattern.replaceAll("##", "").replaceAll(" ", "").toLowerCase().replace(/\\s/g,'').replace(/(.{2})/g,"$1 "); 52 | var akey_function_list = Memory.scanSync(kernel_util.base, kernel_util.size, pattern); 53 | if (akey_function_list.length > 1) { 54 | send("pattern FOUND MULTI!!") 55 | send(pattern) 56 | send(akey_function_list) 57 | send("!!exit") 58 | } 59 | if (akey_function_list.length == 0) { 60 | send("pattern NOT FOUND!!") 61 | send("!!exit") 62 | } 63 | return akey_function_list[0]['address']; 64 | } 65 | 66 | // const key_function = Module.findExportByName("KernelUtil.dll", 'sqlite3_key') 67 | const key_function = single_function("55 8b ec 56 6b 75 10 11 83 7d 10 10 74 0d 68 17 02 00 00 e8") 68 | const name_function = single_function("55 8B EC FF 75 0C FF 75 08 E8 B8 D1 02 00 59 59 85") 69 | const rekey_function = single_function("##558BEC837D1010740D682F020000E8") 70 | const close_function = single_function("##55 8B EC 56 8B 75 08 85 F6 74 6D 56 E87C 3E 01 00") 71 | const exec_function = single_function("##558BEC8B45088B40505DC3") 72 | // send(key_function) 73 | var name_function_caller = new NativeFunction(name_function, 'pointer', ['pointer', 'pointer']); 74 | var rekey_function_caller = new NativeFunction(rekey_function, 'int', ['pointer', 'pointer', 'int']); 75 | var key_function_caller = new NativeFunction(key_function, 'int', ['pointer', 'pointer', 'int']); 76 | var close_function_caller = new NativeFunction(close_function, 'int', ['pointer', 'int']); 77 | var exec_function_caller = new NativeFunction(exec_function, 'int', ['pointer', 'pointer', 'pointer', 'pointer', 'pointer']); 78 | var TARGET_KEY_LENGTH = 16; 79 | var key_length = 0; 80 | var dbName; 81 | var empty_password = Memory.alloc(TARGET_KEY_LENGTH) 82 | var original_password = Memory.alloc(TARGET_KEY_LENGTH) 83 | empty_password.writeByteArray(Array(TARGET_KEY_LENGTH).fill(0)) 84 | const no_sync = "PRAGMA synchronous=ON" 85 | const no_sync_address = Memory.allocUtf8String(no_sync) 86 | const TEST_PWD_SQL = Memory.allocUtf8String("SELECT count(*) FROM sqlite_master;") 87 | 88 | var store_args_1, store_args_2, target_db; 89 | var should_copy = false; 90 | var calling_key = false; 91 | var should_show = false; 92 | 93 | function is_db_ok(){ 94 | return !exec_function_caller(target_db, TEST_PWD_SQL, NULL, NULL, NULL); 95 | } 96 | 97 | Interceptor.attach(key_function, { 98 | onEnter: function (args, state) { 99 | if(calling_key){ return; } 100 | should_copy = false; 101 | should_show = false; 102 | console.log("[+] key found:"); 103 | dbName = name_function_caller(args[0], NULL).readUtf8String(); 104 | if (dbName.replaceAll('/', '\\\\').split('\\\\').pop().toLowerCase() == 'Msg3.0.db'.toLowerCase() || false) { 105 | should_show = true; 106 | target_db = args[0]; 107 | // disable memory cache 108 | //console.log("¦- db: " + args[0]); 109 | key_length = args[2].toInt32() 110 | console.log("¦- nKey: " + key_length); 111 | //console.log("¦- pkey: " + args[1]); 112 | console.log("¦- *pkey: " + buf2hex(args[1].readByteArray(key_length))); 113 | console.log("¦- dbName: " + name_function_caller(args[0], NULL).readUtf8String()); 114 | //console.log("¦- *pkey: " + buf2hex(Memory.readByteArray(new UInt64(args[1]), key_length))); 115 | if(key_length == TARGET_KEY_LENGTH){ 116 | Memory.copy(original_password, args[1], key_length) 117 | should_copy = true; 118 | } 119 | } 120 | }, 121 | 122 | onLeave: function (retval, state) { 123 | if(calling_key){ return; } 124 | if(!should_show){ return; } 125 | console.log("¦- sqlite3_key return: " + retval); 126 | console.log("¦- is_db_ok: " + is_db_ok()); 127 | if (should_copy) { 128 | // exec_function_caller(target_db, no_sync_address, NULL, NULL, NULL); 129 | console.log("rekey to NULL: " + rekey_function_caller(target_db, empty_password, key_length)) 130 | console.log("¦- is_db_ok: " + is_db_ok()); 131 | // console.log(close_function_caller(target_db, 0)) 132 | send("!!MSG3.0: " + dbName) 133 | recv(function(){}).wait(); 134 | console.log("rekey to orig: " + rekey_function_caller(target_db, original_password, key_length)) 135 | console.log("¦- is_db_ok: " + is_db_ok()); 136 | calling_key = true; 137 | // console.log(key_function_caller(target_db, original_password, key_length)) 138 | calling_key = false; 139 | } 140 | } 141 | 142 | }); 143 | 144 | var rekey_show_result = false; 145 | Interceptor.attach(rekey_function, { 146 | onEnter: function (args, state) { 147 | console.log("[*] rekey:"); 148 | dbName = name_function_caller(args[0], NULL).readUtf8String(); 149 | rekey_show_result = false 150 | if (dbName.replaceAll('/', '\\\\').split('\\\\').pop().toLowerCase() == 'Msg3.0.db'.toLowerCase() || false) { 151 | rekey_show_result = true; 152 | //console.log("¦- db: " + args[0]); 153 | key_length = args[2].toInt32() 154 | console.log("¦- nKey: " + key_length); 155 | //console.log("¦- pkey: " + args[1]); 156 | console.log("¦- *pkey: " + buf2hex(args[1].readByteArray(key_length))); 157 | console.log("¦- dbName: " + name_function_caller(args[0], NULL).readUtf8String()); 158 | //console.log("¦- *pkey: " + buf2hex(Memory.readByteArray(new UInt64(args[1]), key_length))); 159 | } 160 | }, 161 | 162 | onLeave: function (retval, state) { 163 | if(!rekey_show_result){ return; } 164 | console.log("¦- sqlite3_rekey return: " + retval); 165 | } 166 | 167 | }); 168 | """ 169 | 170 | session = frida.get_local_device().attach(QQ_PID) 171 | script = session.create_script(hook_script) 172 | message_seq = 0 173 | 174 | 175 | def on_message(message, data): 176 | global message_seq 177 | if message["type"] == "send": 178 | if message["payload"] == "!!exit": 179 | exit(3) 180 | if message["payload"].startswith("!!MSG3.0: "): 181 | filename = message["payload"][10:] 182 | script.post({}) 183 | new_filename = ( 184 | filename.split("\\")[-1] 185 | + "_" 186 | + str(message_seq) 187 | + "_" 188 | + str(time.time()).split(".")[0] 189 | + ".db" 190 | ) 191 | message_seq += 1 192 | print("Copying decrypted file:", filename, "to", new_filename) 193 | shutil.copyfile(filename, new_filename) 194 | file1 = open(new_filename, "rb") 195 | extra_flag = False 196 | # detect extra sqlite header "SQLite header 3" 197 | if file1.read(15) == b"SQLite header 3": 198 | file1.seek(1024) 199 | if file1.read(15) == b"SQLite format 3": 200 | file1.seek(1024) 201 | extra_flag = True 202 | print("NEW PLAIN TEXT DB detected!") 203 | if not extra_flag: 204 | file1.seek(0) 205 | print("hmm db seems still encrypted... anything wrong?") 206 | file1.close() 207 | # remove extra 1024 bytes header of a huge file 208 | if extra_flag: 209 | file1 = open(new_filename, "rb") 210 | file2 = open(new_filename + ".tmp", "wb") 211 | file1.seek(1024) 212 | shutil.copyfileobj(file1, file2) 213 | file1.close() 214 | file2.close() 215 | os.remove(new_filename) 216 | os.rename(new_filename + ".tmp", new_filename) 217 | print("Done. File Path:") 218 | print(os.path.abspath(new_filename)) 219 | else: 220 | print(message["payload"]) 221 | elif message["type"] == "error": 222 | print(message["stack"]) 223 | 224 | 225 | def on_destroyed(): 226 | print("process exited.") 227 | os._exit(0) 228 | 229 | 230 | script.on("message", on_message) 231 | script.on("destroyed", on_destroyed) 232 | script.load() 233 | print("hooked.") 234 | sys.stdin.read() 235 | --------------------------------------------------------------------------------