ジムがソーシャルメディアを押し付けてきたので、アプリをリバースエンジニアリングした
My gym forced a social media down my throat. So I reverse-engineered its app (shayon.work)
要約
筆者は、ジムのアプリに強制的に表示されるソーシャルフィードに不満を感じ、QRコードでの入退場ロジックのみを抽出するため、アプリのリバースエンジニアリングを試みた。Flutter製アプリのDartネイティブコードを解析し、AES-128-CTR暗号化された有効期限とチェックサムの生成に必要な秘密鍵を、ルート化されたAndroidデバイスから抽出することに成功した。これにより、ソーシャルメディアの要素なしでQRコードを生成できるWebアプリを開発し、ジムでの利用に成功した。
全文翻訳
ジムがソーシャルメディアを押し付けてきた。そのアプリをリバースエンジニアリングした。
最近、新しいジムに入会した。設備も管理もブランディングも、かなり良いジムだ。
そして、アプリがある。
これは、どこかの適当なひどいコードで書かれたアプリではない。製品としては、実際かなり良い。非常に洗練されたUI/UXと、よく考えられた一貫性のあるデザイン言語を持っている。バグは一つしか見つけなかった。
しかし、私にとって唯一のユースケースは、改札でQRコードを見せて出入りすることだ。
ところが、問題がある。アプリのホーム画面はソーシャルフィードだ。チェックアウトすると、セッションのストーリーを投稿するように促される。ホームフィードは、人々のジムでの自撮りでいっぱいだ。アプリを開くたびに、実質的にTinderのようなものを見せられる。
私の友人は、これが天才的なビジネスアイデアだと考えている。彼女によれば、運動する人々は自分の体を見せびらかしたいと思っているが、普通のソーシャルメディアでそれを投稿するのは眉をひそめられる。だから、このジム会社は本当に良いプロダクト・マーケット・フィットを見つけたのだという。
彼女は正しいと思う。
しかし、私はソーシャルメディアや、自分でコントロールできないフィードに対して非常に攻撃的だ。オフにしたり、並べ替えたり、フィルタリングしたりできないフィードを表示するアプリは、私に対する心理戦だと受け止めている。
12年以上ソーシャルメディアを使っていない。広告をブロックしたり、YouTubeのコメント、Shorts、おすすめ動画をブロックしたり、YouTubeのタイトルやサムネイルを sanitizing したりすることには非常にこだわっている。LinkedInのような、どうしても使わなければならないものについては、ニュースフィードを完全にブロックするブラウザ拡張機能を使っている。
だから、これは個人的な問題として受け止めた。
アプリの余計な機能なしで、QRコードのロジックを抽出できるかどうか知りたかった。
そこで掘り下げ始めた。コードは15秒ごとに変わることをすぐに知った。スクリーンショットもその画面ではブロックされており、AndroidのFLAG_SECUREと一致していた。何かのTOTP(ワンタイムパスワード)ではないかと疑問に思った。生成はサーバーサイドで行われているのだろうか?
最も簡単なことをした:Wi-Fiをオフにした。アプリはすぐに、コードが再接続なしでN日間有効であることを伝えてきた。
なるほど、ローカルで生成できるということか。
GLM 5.3とDeepSeek v4.1 Flashを使用した。私のセッションでは、GLMはそれほど進歩せずに、トークンコストの10〜20倍を消費していた。DeepSeekの方がずっと速く進んでいるように見えた。
明確にしておくが、ジムと私自身を匿名に保つために、いくつかの詳細は省略している。ジムのサブスクリプションを失いたくない。実際、そのジムは気に入っている。
アプリはFlutterで書かれており、ほとんどのロジックはDartネイティブコードにコンパイルされていた。それを調べるためにblutterを、Android側のコードを調べるためにjadxを、パッケージを調べるためにAndroidのSDKツールを使用した。後で、OpenSSLとWebCryptoを使って暗号化をチェックし、zbarを使ってQRコードを独自にデコードした。
私は、これまでこのようなことをしたことがなかったので、ほとんど直感から、あちこちでDeepSeekにプロンプトを与えていた。時には、実装はそれよりも単純であるはずだと感じた方向に迷い込んでいるように見えた。私の推測を伝えたり、もっと簡単な方法があるはずだと言ったりすることが、正しい方向へ導くのに役立った。
QRコードはURLであり、エンコードされたアカウントハンドル、暗号化された有効期限のタイムスタンプ、ランダムなノンス、チェックサムの4つの要素を含んでいることをすぐに理解した。
有効期限はAES-128-CTRで暗号化されていた。チェックサムの計算には別の秘密鍵が使用されていた。そのため、アカウントに必要なのはその2つの秘密鍵だった。
最初のアプローチは、DeepSeekにAndroidエミュレータを操作させてサインインし、キーを抽出させることだった。しかし、友人に借りた10年前のラップトップでは、Androidソフトウェアのエミュレーションを合理的な速度で行うことはできないだろうとすぐに気づいた。
DeepSeekはルート化された電話を提案した。そこで、機能するルート化されたAndroidフォンが手元にあることを思い出した。
それを起動し、ワイヤレスADBで接続し、DeepSeekに調査させた。その結果、アプリは私たちが関心のある呼び出しのリクエストとレスポンスのペイロードをログに記録していることが判明した。キー素材は、電話上のプレーンテキストのログファイルにあった。
このプロジェクトの半分はripgrepだった。
キーを抽出した後、SvelteKitのシンプルなWebアプリを構築してQRコードを生成した。
途中でいくつかの誤った仮定があった。有効期限のタイムスタンプは秒ではなくミリ秒である必要があった。エンコーディングは標準のbase64ではなくbase64urlであり、パディングを保持していた。
また、独自のラウンドトリップテストをパスする間違ったAES実装もあった。何かを暗号化し、復号化すると元の入力に戻ってきた。しかし、両方向で同じ間違いを共有していたため、それがAESであるとは証明できなかった。
WebCryptoと比較することで、その不一致が明らかになった。OpenSSLと既知のテストベクトルで確認した。ブラウザアプリはWebCryptoを使用している。
その後、元のアプリの実際の出力と比較した。同じ入力に対して、私たちの実装はバイト単位で同一のURLを生成した。QR画像のデコードも独自に行い、正しいURLが含まれていることを確認した。
その時点で、動作するプロトタイプができた。Webアプリは抽出したキーからローカルでコードを生成できた。これによりスタンドアロンログインが解決されたわけではなく、キーがローテーションしたときに新しいキーを取得する必要性はなくなったが、ジムで試すには十分だった。
翌日、ジムに行き、友人に作ったものを見せるのが非常に誇らしかったが、ゲートでうまくいくかどうかはまだ少し不安だった。
そして、うまくいった!
チェックインとチェックアウト。
金属を持ち上げるために施設に入るためだけに、ダークパターンで操作的なソーシャルメディアに注意を奪われることはもうない。
フッター
- 長い間、私のLinkedInのプロフィールには「I HATE LINKEDIN(LinkedInは嫌いだ)」と書かれていた。LinkedInは企業世界の最悪な部分とソーシャルメディアの最悪な部分を組み合わせていると誰かに言われたが、それがどれほど悪いかの半分しか説明していないと思う。しかし、 alas、今は求職中であり、応募のためにLinkedInプロフィールが必要だ。↩
- アプリを匿名に保つために、正確な日数は明らかにしない。↩