私たちは、macOS 上のあなた自身のWeChatを、AI agent / スクリプト用のローカルインターフェースにするツールを作りました。メッセージ送信、会話の検索、履歴の読み取りを、すべてあなたのMac上で行い、クラウドには上げず、iPadプロトコルも使いません。
初めて動いた日は、かなり達成感がありました。するとWeChatが更新されました。

バージョンはいつも前を走っている
ツールの原理は、WeChatのローカルデータベースを直接読み、メモリ内でいくつかの重要な関数とフィールドを特定することです。問題は、それらの「ものがどこにあるか」がすべて特定のバージョンに依存していることです。WeChatの小さなバージョン番号が上がるたびに(4.1.8 → 4.1.9 → 4.1.10)、コンパイルされたバイナリでは関数の位置が移動したり、構造体フィールドのオフセットが変わったりする可能性があります。普通のユーザーにはまったく分かりませんが、私たちにとってはその場で動かなくなるということです。
だからこのプロジェクトの日常は「新機能を作る」ことではなく、追うことです。ランニングマシンの上の小さな人はまさに私で、緑の吹き出しはいつも手の届かない前にあります。
更新するとすぐ壊れる、しかもよく静かに壊れる

さらに刺激的なのがサイレントホットアップデートです。WeChatはバックグラウンドで中核コンポーネント(すべてのロジックを担うあの動的ライブラリ)を新しいバージョンに差し替えますが、ダイアログは一切出ません。朝は正常だったのに、午後にはスクリプトがエラーを出し始める。そしてバイナリがすでに差し替えられていたことに、あなたはまったく気づいていません。毎回の調査では、まず「またホットアップデートされたのか?」を確認しなければなりません。
これによって、私たちが最初にやらざるを得なかったことは、むしろ**自動ホットアップデートを止める**ことでした。更新パスをロックし、現在の適応が夜中にこっそり壊されないようにして、安定して作業できる時間を確保するためです。
keyを取る:WeChatに迫られて進化した三世代の方法

WeChatのローカルデータベースはSQLCipherで暗号化されているため、データを読むにはまず32バイトの生の鍵を手に入れる必要があります。この部分では三世代の方法を使い替えました。どの世代も新しいバージョンに迫られて生まれたものです。
第一世代——ヒープ内の鍵テキストをスキャンする。 SQLCipherがデータベースを復号するとき、PRAGMA key = "x'……'" のような文を実行します。初期バージョンでは、この16進数の鍵テキストがプロセスのヒープに残っていました。プロセスメモリを直接スキャンし、この形にマッチさせればkeyを拾えました。4.1.9ではまだ使えました。
第二世代——バイナリ鍵をスキャンし、ページヘッダーのHMACで検証する。 WeChatが鍵を平文テキストとしてヒープに残さなくなると、第一世代は使えなくなりました。そこで次善策として、ヒープ内の32バイトごとの断片をすべて候補鍵とみなし、データベースの最初のページのHMACで検証しました(SQLCipherはページごとにHMAC検証を持っており、鍵が違うと通りません)。検証に通ったものが本物のkeyです。問題は4.1.10になると、数百MBのヒープと数百万の候補をスキャンしても、HMACのヒット数がゼロだったことです。平文鍵はそもそもヒープにありませんでした。しかも一回スキャンするのに数十分かかり、実用的な方法とは言えないほど遅いものでした。
第三世代——システム暗号関数にブレークポイントを置く。 発想の転換はこうです。鍵はヒープにはないが、どこかを必ず「流れる」。SQLCipherの下層ではmacOSシステムのCommonCryptoが使われており、データベースを復号するたびに CCCryptorCreate を呼んでAESコンテキストを作ります。そしてこのシステム関数のシンボルは削られていません。そこにブレークポイントを置き、鍵長が32のとき、引数内のそのポインタが指しているものがkeyです。これなら一発で捕まえられます。
4.1.10では、ついでに鍵モデルも変わりました。「1つのマスター鍵」から、データベースごとに1つ(per-DB key)になったのです。だからWeChatに各データベースを一度ずつ復号させないと、ブレークポイントで鍵を全部捕まえられません。
この図を一言で言えば、大きなバージョンが一つ上がるたびに、鍵の隠し方が変わり、古い方法は鍵穴の前でいつもほんの少しだけ合わない、ということです。
バックグラウンド送信:「これは誰に送るか」を変える必要がある
メッセージ送信はデータを読むよりずっと難しいです。私たちが欲しいのは、完全|かんぜん}にバックグラウンドで、あなたのマウスフォーカスを奪わずにメッセージを送ることです。そこで避けて通れない問題があります。WeChatは、このメッセージをどの会話に入れるべきかをどう知るのか、ということです。
メモリ内には、「現在このメッセージを誰に送るか」を記録しているフィールドがあります。バックグラウンド送信の考え方は、送信チェーンのどこかのブレークポイントで、このルーティングフィールドを私たちの目標に書き換え、下流がその新しい値に従ってメッセージを投げるようにすることです。いくつものバージョンで、この手は通用しました。
4.1.10で詰まりました。しかもとても勉強になる詰まり方でした。私たちは特徴をたどって「これはルーティングwxidに見える」というフィールドを見つけ、それを書き換えました。ところがメッセージは相変わらず元の会話に入りました。後でようやく分かったのは、そのフィールドは**書き込まれるコピー**(あるsetterがそこに入れるもの)であって、下流がルーティングを決めるために実際に読む場所ではない、ということです。コピーを変えても、源はそのままです。本当のルーティング読み取り地点は、まだ見つかっていません。
本当に再利用できるのは、位置ではなく方法
いくつものバージョンを追ってきて、もっとも価値がある経験はこれです。
あなたが記録したすべての「具体的なアドレス、オフセット、バージョン指紋」は、次のバージョンではほぼすべて使えなくなる。バージョンをまたいで生き残るのは、「どうやってものを見つけるか」という方法だけです。
具体的に何が再利用でき、何が必ず使えなくなるかは、だいたいこうです。
- 必ず使えなくなるもの:ハードコードされた関数アドレス、フィールドオフセット、バイナリ指紋——Tencentが一度並べ替えればすべて期限切れになります。
- 再利用できるもの:特徴的な命令シーケンスで関数を特定すること(たとえば、ある関数本体にとても独特な命令が1つあり、バイナリ全体でそこにしかないなら、それを頼りに新しいバージョンでもその関数を再発見できる)。関数サイズ + 呼び出しチェーンのトポロジーで指紋を作ること(Tencentのホットアップデートは位置だけを移し、関数本体を変えないことが多いため、サイズと形はバージョンをまたいで対応しやすい)。そして**構造的な教訓**——上の「setterのコピーは本当のルーティング地点ではない」のような認識は、ずっと役に立ちます。
だから本当の堀は、ある一回の解析結果ではなく、積み上げてきた**ものを探す技術**です。結果は期限切れになりますが、技術は期限切れになりません。
今どこまで登ったか

最新の4.1.10について正直に言うと、こうです。
- 読み取りは通った——第三世代のkey取得方法(CommonCryptoにブレークポイント)により、会話、履歴、連絡先は正常に取れます。
- 送信はまだ越えられていない——上の「本当のルーティング読み取り地点がまだ見つかっていない」段階で止まっています。
図の中で旗を立て、チェックが付いている山の中腹が現状です。山頂の赤いバツは、まだ登っている部分です。「全部完了」とは装いません。こういう、他人のアプリ内部構造に寄り添う適応では、永遠に次のバージョン、次の山があります。
最後に
位置づけはずっと変わっていません。ローカル優先、クラウドに上げない、データは完全にあなた自身の手元にある。これは *-use シリーズの一員です(ほかにprofile-use、iphone-useがあります)。どれも、agentがあなたのローカルにある本物のツールを直接操作できるようにするもので、データを他人のサーバーへ移すものではありません。
コード、インストール方法、できることの範囲はすべてリポジトリにあります:github.com/leeguooooo/wechat-use。個人 / 研究用途に限ります——商用、一斉送信、他人のアカウントの自動化は、いずれもサポート範囲外です。
バージョンを追うことはまだ続きます。次の山で会いましょう。
コメント
コメントは即時公開されますが、ポリシー違反時は非表示になる場合があります。