# macOS WeChat 4.1.11+ Transient Key Capture

Status: prototype implemented and verified on WeChat 4.1.13 (Apple Silicon)

Date: 2026-08-27

## Summary

The original extractor searched readable/writable WeChat memory for SQL text literals shaped like `x'<64-192 hex characters>'`. That representation exists in WeChat 4.1.8, but it is no longer retained in memory by WeChat 4.1.11 and 4.1.13.

WeChat 4.1.13 support now uses transient capture instead:

1. Launch an ad-hoc-signed copy of WeChat with a small Rust interpose dylib.
2. Capture 32-byte inputs and outputs at `CCKeyDerivationPBKDF` and 32-byte keys at `CCCryptorCreate`.
3. Deduplicate candidates and write private fixed-size records.
4. Import the candidate file with `init --candidate-file`.
5. Treat a candidate as a database key only after it passes the existing SQLCipher page-1 HMAC-SHA512 check.

The first 4.1.13 verification extracted 27 of 29 database keys. `sessions`, `contacts`, `favorites`, and `unread` all succeeded against the real database directory with the newly captured keys.

## What Was Done

### Safety and initialization

- Added separate `--allow-untested-version` and `--allow-resign` gates.
- Added WeChat version and signature preflight output.
- Added `--wechat-app` so experiments can target a copied app bundle instead of the installed official app.
- Kept the official `/Applications/WeChat.app` Developer ID signature unchanged during testing.
- Changed the state directory to mode `0700` and secret files to `0600`.
- Restored state ownership to `SUDO_UID`/`SUDO_GID` after privileged initialization.
- Made config and key writes atomic.
- Added extraction coverage and missing-database diagnostics.
- Removed salts from successful-candidate log lines.

### Persistent-memory candidate scanner

The scanner now recognizes and HMAC-validates:

- legacy SQL-wrapped hex (`x'<key>'` and `x'<key><salt>'`);
- bare 64/96-character hex;
- standard and URL-safe Base64;
- high-entropy raw 32-byte buffers near any known database salt.

Raw salts are located with Aho-Corasick so all database salts can be searched in one pass. Raw candidates reject mostly printable strings, zero-heavy buffers, low-diversity data, and likely pointer arrays.

This fallback improves diagnostics and remains useful across platforms, but it did not recover a 4.1.13 key.

### Transient hook

`macos-key-hook` builds a small `cdylib` containing a macOS `__DATA,__interpose` section.

The retained hooks are:

- `CCKeyDerivationPBKDF`: capture 32-byte password inputs and 32-byte derived outputs;
- `CCCryptorCreate`: capture 32-byte AES keys.

An attempted `CCHmacInit` hook produced high volume but no valid database encryption keys, so it was removed.

The hook writes only unique `(source, candidate)` pairs. This prevents the initial high-frequency crypto calls from creating a large candidate file.

### Candidate record format

Each record is exactly 40 bytes:

| Offset | Size | Meaning |
| --- | ---: | --- |
| 0 | 4 | ASCII magic `WCK1` |
| 4 | 1 | Source: `1` PBKDF input, `2` PBKDF output, `4` AES key |
| 5 | 3 | Reserved |
| 8 | 32 | Candidate bytes |

The file is created with mode `0600`. The CLI ignores malformed records, deduplicates candidates with a hash set, and never logs candidate bytes.

## Evidence

### Version matrix

| WeChat build | Persistent scan | Transient capture | Query with saved keys |
| --- | --- | --- | --- |
| 4.1.8 website | 25/29 keys | Not required | Pass |
| 4.1.11 App Store | Re-signed copy rejected by launchd | Not completed | Pass |
| 4.1.11 website | 0 keys after full scan | Not yet re-run with final hook | Pass |
| 4.1.13 website | 0 keys after full scan | 27/29 keys | Pass |

### 4.1.13 persistent scan

- Memory scanned: 1,987 MB
- Regions: 880
- SQL-wrapped hex candidates: 0
- Bare hex shapes: 2,646
- Base64 shapes: 214,630
- Raw salt hits: 48
- Raw high-entropy candidates near salts: 205
- HMAC checks: 37,829
- Valid keys: 0

This rules out a simple migration from SQL hex text to Base64 or a long-lived raw key stored beside the database salt.

### 4.1.13 transient capture

The first prototype wrote 553,322 records before in-hook deduplication. They reduced to 77 unique candidate byte strings.

| Source | Records | Unique candidates | Unique candidates matching captured DB keys |
| --- | ---: | ---: | ---: |
| PBKDF input | 93 | 35 | 23 |
| PBKDF output | 89 | 57 | 12 |
| HMAC key (removed) | 276,178 | 38 | 0 |
| AES key | 276,962 | 36 | 25 |

Offline HMAC validation performed 1,175 checks and found 27 of 29 database keys after cross-verifying shared keys.

Missing databases:

- `favorite/favorite_fts.db`
- `migrate/unspportmsg.db`

Newly recovered compared with the 4.1.8 extraction included:

- `chatbot/chatbot_message.db`
- `message/weclaw.db`
- `third_app_icon/third_app_icon.db`

## Technical Conclusion

The 4.1.11+ key is not simply Base64-encoded or persistently stored as another obvious string. The database encryption scheme and HMAC verification remain compatible: keys captured at crypto call boundaries decrypt and authenticate the same SQLCipher database pages.

The relevant change is key lifetime and representation. Newer WeChat builds no longer leave the SQLCipher raw-key SQL literal in long-lived writable memory. The key still appears transiently as a 32-byte argument to PBKDF/AES setup, which is why function-boundary capture succeeds while a later full-memory scan fails.

The supplied Windows reference used Frida to hook hundreds of statically compiled WCDB/OpenSSL functions and inspect `rcx/rdx/r8/r9`. On Apple Silicon the equivalent argument registers are `x0-x7`, but broad Stalker tracing was unstable and unnecessary for this build. Narrow interposition at imported CommonCrypto boundaries recovered the required candidates with less overhead and a smaller attack surface.

## Current Developer Workflow

The macOS 0.1.4 release publishes the hook next to the CLI binaries as:

- `wechat-key-hook-macos-arm64.dylib`
- `wechat-key-hook-macos-x86_64.dylib`

The one-line installer stores the matching hook at `~/.local/lib/wechat-cli-rs/libwechat_key_hook.dylib`. Normal query commands never load it.

The verified prototype workflow is intentionally explicit:

1. Build `wechat-cli-rs` and `wechat-key-hook` in release mode.
2. Copy the official WeChat app and strip only the copy's extended attributes.
3. Ad-hoc sign the copy with `get-task-allow` by using `init --wechat-app ... --allow-resign`.
4. Launch the copied executable with:
   - `DYLD_INSERT_LIBRARIES=<libwechat_key_hook.dylib>`
   - `WECHAT_CLI_CAPTURE_PATH=<private candidate file>`
5. Allow WeChat to log in and open its databases.
6. Run `init --candidate-file <candidate file>` against a database-page snapshot or the real database directory.
7. Verify core database coverage before replacing a working key file.
8. Restore the real config, validate query commands, clear the candidate file, remove the copied app, and reopen the official app.

## Next Work

### P0: productize the verified path

- Add a single `init --capture-transient` workflow that creates the temporary app copy, signs it, launches it with the bundled hook, polls candidates, validates them, restores the official app, and cleans up.
- Package `libwechat_key_hook.dylib` with macOS release artifacts and verify its checksum before loading.
- Keep the existing key file untouched until the new candidate set passes core coverage (`session`, `contact`, and at least one `message` database).
- Back up the previous key file automatically before any successful replacement.
- Always truncate and remove the transient candidate file after validation.

### P1: completion and diagnostics

- Determine why `favorite_fts` and `migrate/unspportmsg` did not produce candidates and document command impact.
- Record capture source counts in CLI diagnostics without exposing candidate bytes.
- Add stable error codes for hook library missing, injection rejected, capture timeout, no candidates, and candidates rejected by HMAC.
- Add a read-only `doctor` command that reports installed versions, signatures, state permissions, and extraction/query compatibility separately.

### P2: release hardening

- Add checksum/signature verification for the installer and hook dylib.
- Test on clean macOS users with different TCC and Full Disk Access states.
- Add fixture tests for hook-record parsing, deduplication, malformed/truncated records, and partial coverage.
- Publish a machine-readable compatibility matrix shared by CLI help, README, and website.

## Acceptance Criteria

- The installed official WeChat app is never modified by default.
- Re-signing and unverified-version testing require separate explicit flags.
- A 4.1.13 clean initialization captures enough keys for `sessions`, `contacts`, and message queries.
- Existing valid keys remain recoverable if capture fails or produces partial coverage.
- State directory and secret files remain owned by the login user with `0700/0600` modes.
- Candidate bytes, database keys, salts, usernames, chat names, and database paths are not emitted in normal logs.
- Candidate and temporary app artifacts are removed after validation.
