tag:crieit.net,2005:https://crieit.net/tags/seedrandom/feed
「seedrandom」の記事 - Crieit
Crieitでタグ「seedrandom」に投稿された最近の記事
2020-09-24T10:25:23+09:00
https://crieit.net/tags/seedrandom/feed
tag:crieit.net,2005:PublicArticle/16070
2020-09-24T10:25:23+09:00
2020-09-24T10:25:23+09:00
https://crieit.net/posts/unlock-bank
ハッカー専用パズルゲームを作ったので全てネタバレする
<blockquote>
<p>当記事は <a target="_blank" rel="nofollow noopener" href="https://zenn.dev/kinmi/articles/b6646b4902dbda585c0b">ハッカー専用パズルゲームを作ったので全てネタバレする</a> のクロス投稿です</p>
</blockquote>
<h1 id="💰作ったもの💰"><a href="#%F0%9F%92%B0%E4%BD%9C%E3%81%A3%E3%81%9F%E3%82%82%E3%81%AE%F0%9F%92%B0">💰作ったもの💰</a></h1>
<p><a href="https://crieit.now.sh/upload_images/aa565ccbf6a415ffff3d41f0f20fe93f5f6bf02787aff.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/aa565ccbf6a415ffff3d41f0f20fe93f5f6bf02787aff.png?mw=700" alt="UNLOCK BANK" /></a><br />
<a target="_blank" rel="nofollow noopener" href="https://unlock-bank.vercel.app/">UNLOCK BANK</a></p>
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">@銀行口座をHackしたくてウズウズしている皆さん「違法行為はしたくないけどハッキングはしたい…」そんな貴方の悩みを解決するゲームをご用意しました。このゲームでは何をやっても合法です。是非、不正ログインしてみてください💰<a target="_blank" rel="nofollow noopener" href="https://t.co/LyIpYLCRrP">https://t.co/LyIpYLCRrP</a> <a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/UNLOCK_BANK?src=hash&ref_src=twsrc%5Etfw">#UNLOCK_BANK</a></p>— きんみ | ツイッター大喜利サイト🎍ついぎり🎍作りました🙄 (@_kinmi) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/_kinmi/status/1307988548591611904?ref_src=twsrc%5Etfw">September 21, 2020</a></blockquote>
<h2 id="仕様"><a href="#%E4%BB%95%E6%A7%98">仕様</a></h2>
<ul>
<li>Account Number(数字8桁) と Password(数字4桁) によるログイン認証</li>
<li>Account Number は全て存在する(8桁なので全口座数は1億口)</li>
<li>Password を3回間違えるとロック(再試行不可)される</li>
<li><em>[裏設定]</em> 銀行口座を模している為、もちろん<strong>パスワードが流出したら他者もログイン可能</strong></li>
</ul>
<p>ユーザーはどんな攻撃手法を用いても良い。<br />
最近話題の1段階認証を突破してみようというパズルゲーム。</p>
<h2 id="システム構成"><a href="#%E3%82%B7%E3%82%B9%E3%83%86%E3%83%A0%E6%A7%8B%E6%88%90">システム構成</a></h2>
<ul>
<li>JSフレームワーク: Nuxt.js 2.14.5(Vue.js 2.x)</li>
<li>トランスパイル: TypeScript 4.0</li>
<li>CSSフレームワーク: TailwindCSS</li>
<li>ホスティング: Vercel</li>
</ul>
<p>お馴染みの構成を選択したが、Vue.jsの使用については少し後悔している(後述)</p>
<h1 id="以下ネタバレ"><a href="#%E4%BB%A5%E4%B8%8B%E3%83%8D%E3%82%BF%E3%83%90%E3%83%AC">以下ネタバレ</a></h1>
<h2 id="想定する解法"><a href="#%E6%83%B3%E5%AE%9A%E3%81%99%E3%82%8B%E8%A7%A3%E6%B3%95">想定する解法</a></h2>
<p>基本的には以下の2つと考えている。<br />
- <strong>☠️リバースブルートフォース攻撃☠️</strong><br />
件の金融事件で脚光を浴びた攻撃手法。<br />
暗証番号の総当たり(ブルートフォース攻撃)は一般的なシステムだと試行回数に制限を設けているが、ユーザーIDの様な公開情報については秘匿性が低いため無制限に試行できるシステムが多い。そこを突いてパスワードは固定のままIDの方を総当たりするという攻撃。<br />
特に当ゲームのような、認証情報に数値しか許容していないシステムだと少ない試行回数で突破できるため致命的な脆弱性となる。</p>
<ul>
<li><strong>😈リバースエンジニアリング😈</strong><br />
JavaScriptのみで構成している為、コードを解析してパスワードの取得が可能。<br />
当初はNode.jsサーバー等を用意して認証処理を隠蔽するつもりだったがガチ攻撃された場合、サーバーか私の財布が死ぬと思い断念。<br />
しかし「ハッカー専用」と銘打っている以上、コードを覗くだけで突破されては面白くないので悪足掻きとして難読化を施している。</li>
</ul>
<h2 id="仕組みについて"><a href="#%E4%BB%95%E7%B5%84%E3%81%BF%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">仕組みについて</a></h2>
<h3 id="🔐パスワード生成"><a href="#%F0%9F%94%90%E3%83%91%E3%82%B9%E3%83%AF%E3%83%BC%E3%83%89%E7%94%9F%E6%88%90">🔐パスワード生成</a></h3>
<p>仕様として、口座番号とパスワードの紐付きを作らないといけない(パスワードが流出したら誰でもログインできるように)<br />
最初は愚直に1億口座分のパスワードマッピングを定数で用意しようと考えた。<br />
しかし上述のコードを隠蔽できない事情によりリテラル値で持つのは避けたかった為、下記のライブラリを採用した。<br />
<a target="_blank" rel="nofollow noopener" href="https://github.com/davidbau/seedrandom">seedrandom.js</a><br />
これを用いてシード値から再現性のある乱数を生成(※1)する。</p>
<p>下記のように口座番号をシード値とした乱数を生成した後、整形してパスワードとしている。</p>
<pre><code class="ts">const seedrandom = require('seedrandom')
function authenticated(account: string, password: string): boolean {
const rng = seedrandom(account)
const _pass = String(Math.round(rng() * 10000)).padStart(4, '0')
return _pass === password
}
</code></pre>
<p>これで口座番号とパスワードの紐付きを再現している。</p>
<h4 id="seedrandom.jsの使用感"><a href="#seedrandom.js%E3%81%AE%E4%BD%BF%E7%94%A8%E6%84%9F">seedrandom.jsの使用感</a></h4>
<ul>
<li>🙆♂️ <strong>良かった所</strong>
<ul>
<li>直感的に使える</li>
<li>複数の乱数アルゴリズムに対応している</li>
<li><code>Math.random()</code>をラップできる為、テストにも使えそう</li>
</ul></li>
<li>🙅♂️ <strong>頑張って〜</strong>
<ul>
<li>型定義がない(2020/09現在、<a target="_blank" rel="nofollow noopener" href="https://github.com/davidbau/seedrandom/pull/70">issue</a>にて対応中)</li>
</ul></li>
</ul>
<p>個人的には「再現性のある乱数を生成する処理」というのは<a target="_blank" rel="nofollow noopener" href="https://qiita.com/kinmi/items/ddd213bf7a09f67f68ee">バーコードバトラーライクのゲーム</a>を作る際に重宝するので、有難いライブラリ。</p>
<h3 id="状態管理, 認証処理"><a href="#%E7%8A%B6%E6%85%8B%E7%AE%A1%E7%90%86%2C+%E8%AA%8D%E8%A8%BC%E5%87%A6%E7%90%86">状態管理, 認証処理</a></h3>
<p>本筋と外れるが、クライアントで認証処理を行う為に口座番号と暗証番号をグローバル管理する必要があった。<br />
Vuexはオーバースペックなので状態を<a target="_blank" rel="nofollow noopener" href="https://ja.nuxtjs.org/guide/plugins/#%E7%B5%B1%E5%90%88%E3%81%95%E3%82%8C%E3%81%9F%E6%B3%A8%E5%85%A5">Inject</a>するプラグインを自作してstoreとして使用した。</p>
<blockquote>
<p>~/plugins/auth.ts</p>
</blockquote>
<pre><code class="ts">import Vue from 'vue'
import { Plugin } from '@nuxt/types'
import { Seedrandom } from '../types/seedrandom'
const seedrandom = require('seedrandom') as Seedrandom
type InjectTypeAuth = {
/**
* 口座番号
*/
accountNumber: string
/**
* 暗証番号
*/
password: string
/**
* 認証処理
* seedから4桁の乱数(先頭0埋め)を生成し、passwordとの比較結果を返却する
* @param {string} seed シード値となる値
* @param {string} password パスワード
*/
authenticated(seed: string, password: string): boolean
}
declare module '@nuxt/types' {
interface Context {
$auth: InjectTypeAuth
}
interface NuxtAppOptions {
$auth: InjectTypeAuth
}
}
declare module 'vue/types/vue' {
interface Vue {
$auth: InjectTypeAuth
}
}
type State = {
accountNumber: string
password: string
}
/**********************************************
* 認証情報プラグイン
* @param {Context} ctx
* @param {(key: string, value: any) => void} inject
*/
const AuthPlugin: Plugin = (_ctx, inject) => {
/**
* Observable properties
*/
const state = Vue.observable({
accountNumber: '',
password: '',
} as State)
function authenticated(seed: string, password: string): boolean {
const rng = seedrandom(seed)
const _pass = String(Math.round(rng() * 10000)).padStart(4, '0')
if (_pass === password) state.password = _pass
return _pass === password
}
/**
* Injection
*/
inject('auth', {
get accountNumber() {
return state.accountNumber
},
set accountNumber(accountNumber: string) {
state.accountNumber = accountNumber
},
get password() {
return state.password
},
authenticated,
})
}
export default AuthPlugin
</code></pre>
<p>※ 実際の乱数生成処理はもう少しノイズを入れてるので、このまま動かしても同じパスワードは生成されません</p>
<p>この<code>authenticated</code>をEnterボタン押下時と、直アクセスを防ぐ目的でログイン後ページ内の<code>validate()</code>hookでも呼び出して認証処理としている。</p>
<p>⚠️注意<br />
<em>当然ですが、フロントエンドでパスワードの一致チェック等の認証処理を行ってはいけません。</em></p>
<p>Vue3のRCが外れて正式リリースとなったが、<a target="_blank" rel="nofollow noopener" href="https://v3.vuejs.org/api/basic-reactivity.html">Reactivity API</a>等を活用したVuex(ver 5)のリリースはまだ先の様なので暫く状態管理はこの手法に落ち着きそう。</p>
<h2 id="攻撃方法の紹介"><a href="#%E6%94%BB%E6%92%83%E6%96%B9%E6%B3%95%E3%81%AE%E7%B4%B9%E4%BB%8B">攻撃方法の紹介</a></h2>
<p>以上を踏まえ、解法の一つであるリバースブルートフォース攻撃の実施方法を紹介していく。<br />
尚、当ゲームでは外部からブラウザ操作しやすいように主要な要素に対してid属性を付与している。<br />
<a href="https://crieit.now.sh/upload_images/8e2ef469b048d0e22a0b6e053a8a9f7d5f6bf3fec95cd.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8e2ef469b048d0e22a0b6e053a8a9f7d5f6bf3fec95cd.png?mw=700" alt="振っているID一覧" /></a></p>
<h3 id="前提知識: JavaScript の場合"><a href="#%E5%89%8D%E6%8F%90%E7%9F%A5%E8%AD%98%3A+JavaScript+%E3%81%AE%E5%A0%B4%E5%90%88">前提知識: JavaScript の場合</a></h3>
<p>「<strong>システム構成</strong>」の節でも触れたが、Vue.jsの採用を後悔したのはここ。</p>
<p>システムがHTML+PureJSの構成であれば、ブラウザの開発者コンソールから下記の様に実行して入力値を動的に与える事が可能。</p>
<pre><code class="js">const digit1 = document.getElementById('digit1')
digit1.value = "1"
</code></pre>
<p>これは一般的な方法だし、私も当初このやり方を想定していた。<br />
しかしVue.jsの場合、inputイベントを検知してVueインスタンス内に保有するデータを更新する。<br />
直接input要素のvalue値を書き換えてもイベントは発火せず、データは更新されない。<br />
この挙動を想定しておらず、ユーザーに無駄なハードルを与えることとなってしまった。</p>
<p>ではどうするのか。<br />
当ゲームには数値をカウントアップ/カウントダウンするボタンを実装している。<br />
<a href="https://crieit.now.sh/upload_images/ffc2f4e46d2b2c361e83cb2dd2287bde5f6bf42460c65.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/ffc2f4e46d2b2c361e83cb2dd2287bde5f6bf42460c65.png?mw=700" alt="これ" /></a></p>
<p><strong>金融システムって誰得UI多いよな</strong> という遊び心で実装したボタンだが、Vue内のデータを書き換えるイベントに直結している。<br />
このボタンをプログラムからクリックすることが出来れば口座番号入力の自動化が可能となる。</p>
<p>ここから先の解説は、既にUNLOCK成功された<a target="_blank" rel="nofollow noopener" href="https://twitter.com/yoneapp">@yoneapp</a>さんが記事を書かれている為、そちらに任せることとする。<br />
<a target="_blank" rel="nofollow noopener" href="https://zenn.dev/yoneapp/articles/ec2892c7e2e5c499684d">リバースブルートフォース攻撃を使ってUNLOCK BANKの口座に不正ログインして優勝する</a>(Zenn)<br />
(改めて解説記事の執筆ありがとうございます🙇♂️)</p>
<h3 id="前提知識: Vue.js の場合"><a href="#%E5%89%8D%E6%8F%90%E7%9F%A5%E8%AD%98%3A+Vue.js+%E3%81%AE%E5%A0%B4%E5%90%88">前提知識: Vue.js の場合</a></h3>
<p>Vue.jsには <a target="_blank" rel="nofollow noopener" href="https://github.com/vuejs/vue-devtools">vue-devtools</a> というデバッグ用のブラウザ拡張が存在する。<br />
これを使えばコンポーネント構成を覗きつつ、各種データの書き換えやイベント発火が可能となるが通常はProduction環境で開けない。<br />
しかし、開発者ツールを使いこれをこじ開ける方法がある。</p>
<p><a target="_blank" rel="nofollow noopener" href="https://qiita.com/POPOPON/items/60010faae1eb4e4e67ac">本番公開されているサイトで Vue devtools を使う裏技</a>(Qiita)</p>
<p>厳密にはソースの難読化を施してるのため上記記事と全く同じではないが、ソースを検索すれば<code>devtool</code>というキーワードは見つかるはず。<br />
あとはその後ろにブレークポイントを張り、<code>_0x1bd719.devtools = true</code> を実行すれば開発者ツールを開けることが可能。</p>
<p><img src="https://storage.googleapis.com/zenn-user-upload/y04xwot067stonsyjc0dxwtwrbj6" alt="ProductionでDevtoolsを開いた図" /></p>
<p>図の状態(Devtoolsでコンポーネントを選択した状態)だと、開発者コンソール上で<code>$vm0</code>というオブジェクトが使用出来る。これは選択したコンポーネントのVueインスタンスであり、コンポーネント内に存在するデータやメソッドが全て内包されている。<br />
あとはそこからアタリを付けて、自動化プログラムを組めば良い。<br />
実際には下記をイジれば入力操作の自動化が可能となる。</p>
<pre><code class="js">$vm0.accountNumbers // 口座番号(1桁ずつ格納した配列)
$vm0.password // パスワード
$vm0.enter() // Enterボタン押下時の処理を実行
</code></pre>
<p>⚠️注意<br />
<em>初めてこれ(Productionでdevtoolsが使えること)を知った方はセキュリティ面に不安を覚えるかもしれませんが、それはお門違いです。</em><br />
<em>フロントエンドで保持するデータはユーザーから自由に改ざんされる前提であるべきです。</em></p>
<h3 id="前提知識: Nuxt.js の場合"><a href="#%E5%89%8D%E6%8F%90%E7%9F%A5%E8%AD%98%3A+Nuxt.js+%E3%81%AE%E5%A0%B4%E5%90%88">前提知識: Nuxt.js の場合</a></h3>
<p>Nuxt.jsで作られたアプリケーションは<code>window</code>直下に<code>$nuxt</code>というオブジェクトが作られる。<br />
そこには全ての情報が含まれており、上記のようにDevtoolsを開かずとも開発者コンソールからデータの書き換えやメソッドの実行が可能。<br />
しかし内包する情報量が膨大な為、開発者ではない第三者が操作したい対象を探すのは一苦労かもしれない。<br />
ちなみに当ゲームでは下記が自動化に必要な対象となる。</p>
<pre><code class="js">window.$nuxt.$children[1].$children[0].$children[0].accountNumbers
window.$nuxt.$children[1].$children[0].$children[0].password
window.$nuxt.$children[1].$children[0].$children[0].enter()
</code></pre>
<h3 id="その他の手法"><a href="#%E3%81%9D%E3%81%AE%E4%BB%96%E3%81%AE%E6%89%8B%E6%B3%95">その他の手法</a></h3>
<p><a target="_blank" rel="nofollow noopener" href="https://www.selenium.dev/documentation/ja/">Selenium</a> や <a target="_blank" rel="nofollow noopener" href="https://github.com/puppeteer/puppeteer">Puppeteer</a> 等を用いたブラウザ操作の自動化があげられる。<br />
今のところ、これらを使ってUNLOCKしたという報告は観測していない。ブラウザの開発者ツールが万能すぎる。</p>
<p>⚠️注意<br />
<em>悪意が無くともサーバーに高負荷をかける行為は法律により罰せられる可能性があります。</em><br />
<em>しかし、当ゲームで行う分には問題ありません。存分に攻撃してください。</em></p>
<h3 id="更にネタバレ"><a href="#%E6%9B%B4%E3%81%AB%E3%83%8D%E3%82%BF%E3%83%90%E3%83%AC">更にネタバレ</a></h3>
<p>もしproduction環境でログイン後のページを確認する必要が出た場合を考慮して、<br />
- <strong>Account Number</strong>: 1145-1419<br />
- <strong>Password</strong>: 1919</p>
<p>と入力したら開発コンソールに上記口座のパスワードが出力されるようになっている。<br />
攻撃するのは面倒くさいけどログインしてみたい、という方はどうぞ。</p>
<h2 id="確率について"><a href="#%E7%A2%BA%E7%8E%87%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6">確率について</a></h2>
<p>数値4桁のパスワードというのは 0000 ~ 9999 の1万パターンしかない。<br />
パスワードを固定にして、口座番号を総当たりした場合の確率は下記で求められる。</p>
<p><em>1 - ( 9999 / 10000 )^x</em><br />
x=試行回数</p>
<p>(間違ってたらご指摘ください)<br />
確率を表にするとこうなる。<br />
<div class="table-responsive"><table>
<thead>
<tr>
<th>試行回数</th>
<th>HITする確率</th>
</tr>
</thead>
<tbody>
<tr>
<td>3回</td>
<td>0.03%未満</td>
</tr>
<tr>
<td>10回</td>
<td>約0.1%</td>
</tr>
<tr>
<td>100回</td>
<td>約1%</td>
</tr>
<tr>
<td>1,000回</td>
<td>約10%</td>
</tr>
<tr>
<td>10,000回</td>
<td>約63%</td>
</tr>
<tr>
<td>100,000回</td>
<td>約99%</td>
</tr>
</tbody>
</table></div></p>
<p>つまり、口座番号8桁(1億パターン)全て試行せずとも、10万回程度で1口座はUNLOCKできてしまう。<br />
下5桁総当たりすればいい計算で、実際にTwitterでUNLOCKされた方の反応も「思ったより早かった」という声が多かった。<br />
当ゲームはパスワードを乱数によって生成しているが、これが本当の銀行口座の場合、パスワードの偏りが生じるはずなので更にUNLOCKは容易となるだろう。</p>
<h1 id="おわり"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A">おわり</a></h1>
<p>ネタのつもりで作ったんですが思いのほか反響があって、解説記事を書いて頂いたり、難読化をデコードしてリバースエンジニアリングまでやって下さってる方もいて、個人開発冥利に尽きるなと思いました。<br />
他にも「こんなやり方あるよ」という方がいましたらご連絡ください。</p>
<h1 id="注釈"><a href="#%E6%B3%A8%E9%87%88">注釈</a></h1>
<p>※1: JavaScriptにはシード値から乱数を生成する機能が備わっていない</p>
きんみ