チカラの技術

電子工作やプログラミング

【VRChat】リッピングでパスワードギミックが破られるので公開鍵を利用した対策品を作りました【無料配布】

こんにちは! 本記事はVRChatのワールド向けパスワードギミックの紹介・配布記事になります。

まず結論から:要点とギミック配布

従来のパスワードギミックはリッピング(VRChatのデータを不正にダウンロードする行為)によってパスワードが簡単に抜かれてweb上に公開され、誰でも開錠可能になってしまうことが分かりました。

今回その対策としてJWT(公開鍵を利用した検証技術)による新しいパスワードギミックを作成しました。ワールドデータにパスワードを置かないためリッピング耐性があります。従来のパスワードギミックの置き換えとして使用でき、オプションでパスワードの漏洩対策もできます。


こちらで無料配布しています。(MITライセンスで改変・再配布可能です)

power-of-tech.booth.pm
※ VRChat公式のモデレートガイドラインの対応についてはマニュアルの序章に記載しています。

ワンタイムパスワードシステムも新ギミックで稼働します。こちらのサーバーがデモと利用申請の窓口になります。(10名まで)
discord.gg

2023年03月11日11:20 追記:利便性の向上のためワンタイムパスワードシステムはUdonAutoLockに切り替えています。以下ご確認ください。
【VRChat】Discordメンバーのみ自動開錠する鍵ギミックを作りました【無料配布】 - チカラの技術

本ギミックのコアライブラリ(UdonJwt)はkoyashiroさんに作成して頂きました。
https://twitter.com/koyashiro

詳細:開発経緯と技術解説

本記事は経緯と技術説明を交互に行う構成になっています。興味のある見出しを見てもらえればと思います。(もちろん読まなくても配布ギミックは使えます)

経緯①:公開初日にワンタイムパスワードシステムが破られた

今から約1カ月半前、ワンタイムパスワードシステム(以下OTP)の記事とデモを公開しました。(本記事の理解にはタイトルだけ見れば大丈夫です)

power-of-tech.hatenablog.com

そして公開からわずか数時間後に続々と報告が・・・

秘密鍵ってこれですよね?

・・・それです。

(「もしギミックの不備で開錠出来たら報告お願いします」と掲示していました。報告ありがとうございます)

パスワードは暗号化していましたが、秘密鍵で復号するコードは配布ギミックに含まれるのでこの時点で終わったと思いました。

さらにワールドIDと秘密鍵と共に誰でもパスワードを生成できるwebアプリまで作って見せてくれた方までいました。(報告ありがとうございます)
こちらも確かに有効PINを生成しました。


こうして数ヵ月かけて作ったOTPは公開初日に破綻しました・・・

解説:リッピングによるパスワードギミック開錠

本項は秘密鍵を報告してくれた方々に教えて頂いた内容を元に書きました。

VRChatのワールドデータをリッピングするのは比較的容易で、VRChatのプレイ時に残るキャッシュデータもしくはWebAPIからダウンロードできる手法があるそうです。
つまりワールドにジョインするかワールドIDを取得すればすぐ抜き取れるとのことです。そのデータはUnityビルド後のものでワールド製作した時のままでは無いのですが、ここではパスワードギミックに関連する部分のみ説明します。

リッピング後のデータ解析を行えばUdonBehaviourの変数は元のまま取得でき、プログラムはUdonAssemblyの形で取得できます。
以下は報告者の方から提供されたOTP秘密鍵の解析結果です。


つまり全ての設定値は(serializedかプログラム内かに関わらず)取得でき、プログラムもアセンブリから解析できるためパスワードはもちろん、暗号化をしても秘密鍵と共に複合化アルゴリズムを再現されて有効なパスワードを取得可能になってしまいます。私のOTPは後者でコードの解析は私が記事公開する一カ月前からテスト運用していていたワールドにジョインして行っていたそうです。

ここからは一般のパスワードギミックも含めた話になりますが、上記の通りリッピングによって固定パスワードでは3~10分程度で、秘密鍵をワールドデータに置く暗号化パスワードも数日~数週間で突破可能です。(某デジタルデータ販売サイトで無料配布されている10種ほどのパスワードギミックを確認しましたが全てこれに該当しました。)

そうして取得されたパスワードはワールドIDと共にweb上の掲示板などに公開されると誰でも開錠できることになります。
私の知る範囲だけでも他の方のワールドがパスワードをwebに公開されてしまった事例を7件は見ました。当初は入力を大量に繰り返す総当たり攻撃だと思っていましたが、実際はより容易なリッピングで抜かれた結果だと考えるのが自然です。

経緯②:対策は困難。koyashiroさんにご相談

OTPを破られたのはショックでしたが、対策することにしました。

まず検討の結果、残念ながらリッピング自体は防ぐことはできないとの判断に至りました。ただ、パスワードが抜かれることでweb上に公開されて誰でも開錠できるようになる点については対策したいと思いました。
なぜならリッピングやチートによる解析は明確に規約違反ですがwebの公開情報でジョイン・開錠するVRChatユーザーとそれに同行するフレンドはその何倍もいて、しかも正規のVRChat上で開錠を行うのでチート行為を行うより心理的障壁も低いことが予想されるからです。
また、そうして開錠したユーザーとフレンドは他のワールドのパスワードもwebで探すようになりそうです。負の連鎖ですね。

以上から「リッピングで有効なパスワードを抜かれないこと」を対策とすることにしました。


対策を決めてもこれは難しいことです。パスワードも秘密鍵による暗号化キー(共有鍵方式)もワールドデータの解析により生成できるからです。
そこでVRChatでkoyashiroさんに相談に行きました。koyashiroさんはプログラマーの方で今まで何度も助けてもらっているのです。


https://twitter.com/koyashiro

koyashiroさん  「それはワールド側に秘密鍵を置いてると防げませんよ。公開鍵にしないと。」
私       「そうですね・・・」
        公開鍵を使った方法は思いつかなかった訳ではなかったのですが実装が非常に難しいと思って尻込みしていました。
koyashiroさん  「使えそうなのは・・・JSON Web Token(JWT) が良さそう。じゃあ僕が実装しましょうか?
私       「マジですか!お願いします!!!!」
koyashiroさん 「一度作ってみたいし良いですよ」

公開鍵検証を実装したい人なんておる!!??と信じられない思いでしたがkoyashiroさんに畏敬の念を抱きながら即座にお願いしました。
そしてJWTの実装開発が始まりました。

解説:JSON Web Token (JWT) による鍵ギミック

JWTは主にwebで使われるJSONデータに署名や暗号化を施す方法を定めた標準規格の名前です。 鍵ギミックとしては公開鍵による検証を利用したパスワードギミックを実現するために利用します。 本記事ではギミックの説明のため簡略化してお伝えします。詳細を知りたい方は「JWT」「RSA暗号」といったワードで検索すると多くの技術記事がヒットします。

まず公開鍵を利用する方法にすると何が嬉しいのかというと鍵ギミックにパスワードや秘密鍵を置かなくて良いのでリッピングされてもそこから有効なパスワードを得ることはできなくなることです。

※ 「できなくなる」を少し正確に言うと「RS256 - RSA 2048bitの暗号強度なので現在のパソコンでは解読に数億年かかるとされているため、脆弱性が発見されるまでは現実的には解読できなくなると言えます。」となりますが、本記事では説明を簡単にするため暗号強度を前提にした事柄は「できない」と表現を置き換えます。

(もしパソコンで公開鍵からパスワードが得られるようになったらワールドの鍵なんて開けてる場合じゃないです!研究成果として世界に発表してください!)

ここからは作成したJWT鍵ギミックの説明です。
主な役割を果たす3つのデータがあります。秘密鍵・公開鍵・トークンです。


ギミックの仕組みを図示します。これは最も基本的な使い方で従来の固定パスワードギミックとの置き換えを想定しています。

図の通りワールドデータには秘密鍵トークンを含んでいません。また公開鍵は誰に見られても問題ありません。

秘密鍵windowsアプリ内部で生成されますが非表示なので誰も知り得ません。よってリッパー(リッピングする攻撃者)は秘密鍵を得ることができないため有効なトークンも作成出来ない、という仕組みです。
なお検証はローカル動作なのでトークンは外部に通信されません。
(ランダムキー生成アプリはギミックに同梱されていて、オープンソースです)

トークンは長い文字列なので開錠時はコピー&ペーストします。 webアプリ経由でワンクリックでコピー可能です。具体的な開錠方法は以下の通りです。
JWT Locker 開錠マニュアル - Google ドキュメント

この最も簡単な使い方ではユーザーがトークンを漏洩してしまった場合は他者に開錠されてしまいますが、トークンを応用して特定ユーザーのみ開錠可能なトークンを作成することもできます。(後述します)

経緯③:U#でJWTライブラリを作ります

koyashiroさんがJWTライブラリの制作を引き受けてくれたので、私はギミック側の制作とライブラリのデバッグの手伝いをしていました。 本章は僕の視点から見たJWTライブラリの作成過程です。

まずライブラリのためのライブラリを作ります

JWTライブラリと一口に言ってもその実現には様々なライブラリが必要で、U# (udon)には用意されていないのでそこから作っていく必要があります。
以下にkoyashiroさんがJWTのために作成したライブラリを紹介します。
もちろんJWT以外にも利用できるので是非使ってみてください!

UdonList:Listを実現します。Arrayとの相互変換能もできるので同期変数にも使えます。
https://github.com/koyashiro/udon-list

UdonEncodingUTF-8形式などの文字列とバイト配列を相互変換します。
https://github.com/koyashiro/udon-encoding

UdonDictionary:Dictionaryを実現します。
https://github.com/koyashiro/udon-dictionary

UdonJsonJsonシリアライズとデシリアライズを行います。
https://github.com/koyashiro/udon-json

ほかライブラリ群:UnsignedBigInteger演算やSHA256変換、ASN1やPKCS1、PKCS8のデコーダーなどです。これらはJWTライブラリに同梱されています。

以上でようやくJWTライブラリ本体を作り始められます。

JWTライブラリ本体を作ります

実装レベルでのJWTは複雑な構造なので詳しくは文末の参考記事を見て頂くとして、一番難しかった部分の話をします。この部分の開発で全体の4割の期間が掛かりました。(繰り返しになりますがJWTライブラリを実装したのはkoyashiroさんで、本章は私から見た解説です)

それは大きな桁の掛け算です。uint配列で256桁×256桁の乗算を100回程度行うもので、これが公開鍵による検証動作全体の演算時間の9割以上を占めていました。 まず、C#で実装して実行したところ60msec (0.06秒) で完了しました。良いですね。

しかし・・・C#からU#に移植すると計算時間は2分にまで膨れ上がりました!
実行時間2000倍です。感度3000倍じゃないんですよ!

鍵を開けるのに2分も掛かっていては使い物になりません。

最低5秒以内には縮めないといけないという意見は一致しkoyashiroさんに最適化をお願いする一方で、私は自分には解決できないと思ったので解決案を色んな方面に聞きにいきました。

そこでVRChatで競技プログラミングのコミュニティを運営されているテッドさんに相談させて頂き、みざーさんという計算アルゴリズムに詳しい方を紹介して頂きました。ありがとうございます!
https://twitter.com/cleantted_s
https://twitter.com/mizar_tech

そしてkoyashiroさんとみざーさんの打ち合わせの上、モンゴメリ乗算剰余法という計算アルゴリズムが有望そうだとなりました。(乗算以外の計算部分もまとめて最適化・高速化するアルゴリズムです)

モンゴメリ乗算剰余法も複雑でしたがkoyashiroさんは実装してくれました。さらに演算を最適化して乗算桁を64桁まで減らし、加えて細かい部分での最適化を重ね・・・・

最終的には2秒まで計算時間を縮めることに成功しました!最適化前は2分だったので60倍の高速化です。
※ CPU性能で計算時間は変わります。私のPCでは3.5秒でした。

こうしてJWTライブラリは完成しました。
UdonJwt
github.com
koyashiroさん、本当にお疲れ様です・・・

なお並行してシェーダーによる掛け算の実装も邪気眼さんにもお願いしていて、256桁同士の乗算1回で2msecという性能を叩き出していたのですが公開鍵検証演算全体の実装前にU#の最適化が完了したので恐縮ながら終了とさせて頂きました。ご協力ありがとうございました!
https://twitter.com/konchannyan

解説:トークンの応用 - 漏洩対策とワンタイムパスワードシステム

JWTのトークンの仕組みを応用して付加機能をつけます。

まず仕組みですがトークンには3つのデータエリアがあります。

トークン生成時にPayloadへ格納したデータに対する改ざんはSignatureによって防げるので、鍵ギミック(U#)での追加の検証に使えます。
(Payloadの変更はSignatureによる改ざん検知で検証が失敗します。またSignature自体も秘密鍵がないと有効なものが生成できないので改ざんすると検証が失敗します)

VRChat名でトークン漏洩対策

PayloadにVRChat名を格納すると、ギミック側でもjoinユーザーのVRC名をAPIで取得してトークンのものと照合、検証できます。要するに特定のユーザーだけが使えるトークンになるので漏洩されても他人には使えません(仕様上VRC名は重複しません)。ワールド製作者の精神安定上も良いですね。

VRC名をそのままPayloadに入れても良いのですが漏洩時にVRC名まで漏れないようにハッシュ化(元に戻せない文字列にする処理)をします。
またトークン発行時刻(iat claim)も混ぜて発行の度に変化するようにします。

例:
生成時刻「1671418937」+ VRC名「nuruwo」
=> SHA256ハッシュ化文字列「4674ed9ae97c2d576317cc16d4b5e9e33c7a0f26d594e19efd6e35c55a59966d」
このハッシュ化文字列を「vrcName」claimとして入力、「iat」claimと一緒にトークンを作ります。 ギミック側でもJoinユーザー名とiatにハッシュ処理を施せば同じ文字列を取得できるので照合ができます。

以上がトークンの漏洩対策の仕組みで、同梱アプリではVRChat名モードとして利用できます。

(ハッシュ化はアプリで自動的に行うのでトークンを生成する人が意識する必要はありません。)

もちろんユーザーの追加にワールド側の更新は必要ありません。

ワンタイムパスワードシステム

トークンは「exp」claimで有効期限を設定できます。
JWTライブラリは「exp」claimがある場合、サーバー時間と照合してトークンがまだ有効か判別します。
有効期限が設けられるメリットですが、グループから脱退したメンバーに鍵を開けて欲しくない場合、その発行済みトークンを失効できることです。特に多人数のグループで必要性が高くなります。
しかし一定期間ごとにメンバー全員にトークンを手動で再発行するのは非常に手間が掛かるため現実的ではありません。(だからWindowsアプリでは有効期限の設定は有りません。)

その発行を自動化して解決するのがワンタイムパスワードシステムです。 discordのbotを利用してトークンを自動発行するので初期設定後はサーバー管理者は基本的にノータッチで運用できます。
これにより以下のメリットがあります。

・メンバーはボタンを押すだけで有効なトークンを得られる。
→ 簡単にトークンを配布できる。

・グループ管理者はトークンに任意の有効期限を設けられる。
→ discordチャンネルから脱退したメンバーは、有効期限後は鍵を開けることはできない。

・もちろんVRC名による漏洩対策も実装している。
→ VRC名の登録処理もbotで自動化しているので管理者の手間がない。

ワンタイムパスワードシステムのブロック図を示します。


前回作成したPIN方式のワンタイムパスワードシステムとの違いは大きく2点です。

トークンは総当たり攻撃ができないので有効期限を長くできる。
→ PINは安全上最大5分としてましたが、トークンは何時間でも何日でも期限を延ばせます。ただし脱退メンバーの失効期限のみ考慮が必要です。(初期値は12時間)

・パスワードの入力方法が違う
→ 前回は人力で4つの数字をボタンで打ち込んでいましたが、トークンはコピー&ペーストで入力します。(楽になりました)

ワンタイムパスワードシステムは数十人以上の中~大規模コミュニティに適します。


以下のサーバーで体験デモと新規導入受付をしています。
discord.gg
(利用は無料ですが、bot管理の都合上先着10名様までとさせて頂きます)

ポエム:やっていきをしましょう

今回ギミックの性質からやむを得ずリッピングの話をしましたが、私からはVRChatのセキュリティ対策に対する意見はありません。対策は工数やパフォーマンス低下とトレードオフで、またゲームエンジンの使用という特性上技術的な限界があると思うからです(Cannyでいくつかセキュリティのvoteもしていますがお願い程度の気持ちです。)

今後もVRChat上でやっていきます!

まとめ

まとめます。

リッピング対策にJWTギミックを作って無料配布したよ。
・ JWTライブラリはkoyashiroさんが作ってくれたよ。
ワンタイムパスワードも便利だからデモで試してみてね。
・ Udon2、待ってるよ!

それでは良きVRChatライフを!

参考文献

① JWT(JSON Web Token)って何に使うの?仕組みとその利便性 #JavaScript - Qiita

② 暗号の安全性

③ OpenID Connect の JWT の署名を自力で検証してみると見えてきた公開鍵暗号の実装の話 #認証 - Qiita

④ C# で RSA暗号鍵(.pem)作成する - 備忘録

⑤ 自堕落な技術者の日記 : 図説RSA署名の巻 - livedoor Blog(ブログ)

⑥ RSA 暗号の高速化: モンゴメリリダクション #C++ - Qiita

⑦ みざーさんによる剰余演算に関わる詳細な技術的補足:本記事のコメントをご参照ください。   

謝辞

今回も多くの方に助けて頂きました!

koyashiroさん

最重要のJWTライブラリを作っていただきました。乗算問題をクリアしたときの「いい勉強になりました」という格闘家のようなコメントが忘れられません。本当にありがとうございました・・・

邪気眼さん

シェーダーによる多倍長乗算を実現して頂きました。首輪ワールド製作のときから本当にお世話になっています!ありがとうございます!

テッドさん

突然のDMにも関わらず多倍長乗算の相談を聞いて頂き、VRChat競技プログラミングコミュニティと繋げて頂きました。ありがとうございます!

みざーさん

暗号演算でモンゴメリ乗算剰余法をはじめ数々の助言を頂きました。おかげで実用時間での実装ができました。ありがとうございます!剰余演算に関する詳細な技術的補足についても本記事にコメント頂きました!

スペシャルサンクス

デバッグ等や助言等頂きました。ありがとうございます!
CHIHAYAさん、HAYA-CHANさん、ヨドコロちゃんさん、おもちのびるさん、FLAFLAさん
(名前は出せませんがリッピングの報告をしてくれた方も!)

修正履歴

・2022/01/02 18:10 「公開鍵認証」の語句の誤りの指摘を受け文言を修正しました。
 claimについて「aud」の不適切性の指摘から「vrcName」に変更しました。