【React】 Scratch のフロントエンドをハックしよう
https://scratch.mit.edu/ は React で書かれています。これをハックしてみましょう。
外部から Scratch のフロントエンドの State にアクセスすることができるため、いろいろ遊べます。
_reactRootContainer
Scratch は React 16 を使用していますが、React にマウントされたルートの Element には _reactRootContainer というプロパティがついています。これを使えば、React の内部状態にアクセスできます。
Scratch のプロジェクトページ では、ルートの Element の ID は app です。これを使ってみましょう:
const app = document.getElementById('app')
const rootContainer = app._reactRootContainer
console.log(rootContainer)
ブラウザのコンソールで実行してみると、以下のような結果が得られます。

Fiber ノードを取得する
React は内部的に Fiber というグラフ構造のデータを使用して UI を管理しています。先ほど取得した rootContainer から Fiber ノードを取得することができます。
const rootFiberNode = rootContainer._internalRoot.current
console.log(rootFiberNode)

しかし、これはルートの Fiber ノードであり、実際のコンポーネントの Fiber ノードはその子供にあります。これを取得するためには、rootFiberNode.child を使います。
const appFiberNode = rootFiberNode.child
memoizedProps というプロパティを使うことで、コンポーネントの Props にアクセスできます。
ルートのコンポーネントには、store という props を使って Redux のストアを渡しています。これを取得して、Scratch のストアにアクセスしてみましょう。
const scratchState = appFiberNode.memoizedProps.store.getState()
console.log(scratchState)

いくつかのストアが組み合わさっていますが、重要なのは scratchGui というストアです。これを使うことで、Scratch エディタにアクセスできます。
vm を使って遊ぼう
Scratch は、Scratch VM という仮想マシンを利用してプロジェクトを実行しています。この VM は、scratchGui ストアの中に格納してあります。取り出してみましょう。
const vm = scratchState.scratchGui.vm
console.log(vm)

vm を取得すればいろいろなことができます。例えば、コンソールからターボモードを有効にすることができます。
vm.runtime.turboMode = true

また、このように変数を外部から変更できます。

ストアからアクセスできない値を変更する
ストアからアクセスできない値もあります。例えば、Scratch では自分以外が作成したプロジェクトで「中を見る」ボタンを押すと、「クラウド変数」と呼ばれる WebSocket を使ったユーザ共通の変数の通信が接続されます。この切断処理はストアからアクセスすることができません。
しかし、Fiber ノードを使い、任意のコンポーネントを取得することが可能です。
export function getSpecifiedFiber(
root: ReactFiber,
cond: (fiber: ReactFiber) => boolean,
) {
const stack = [root]
while (true) {
const fiber = stack.pop()
if (!fiber) {
return null
}
if (cond(fiber)) {
return fiber
}
if (fiber.child) {
stack.push(fiber.child)
}
if (fiber.sibling) {
stack.push(fiber.sibling)
}
}
}
この関数は、Fiber ノードを深さ優先探索で探索し、条件を満たすノードを返す関数です。これを使います。
クラウド変数の接続処理は、cloudManagerHOC という HOC が CloudManager というコンポーネントを UI に追加することで行われています:
この中に「中を見る」を押したときにの切断処理があります。
これを空の関数に置き換えることで、クラウド変数の接続を切断を無効化することができます!
まず、getSpecifiedFiber を使って CloudManager がマウントされている Fiber ノードを取得しましょう。
このように propTypes に canModifyCloudData というプロパティがあるので、これを使って判定します。
const cloudManagerFiber = getSpecifiedFiber(root, (fiber) => {
if (typeof fiber.type === 'function') {
const propTypes = fiber.elementType.propTypes
if (propTypes && 'canModifyCloudData' in propTypes) {
return true
}
}
return false
});
こうして取得した Fiber ノードから、コンポーネントを取得しましょう。
const cloudManagerHOC = cloudManagerHOCFiber.elementType.prototype
このコンポーネントの disconnectFromCloud メソッドを空の関数に置き換えます。
cloudManagerHOC.disconnectFromCloud = () => {}
これで「中を見る」を押したとしても、クラウド変数の接続が切断されなくなります。

このように、React の Fiber ノードを使うことで、ストアからアクセスできない値も変更することができます。
まとめ
- Scratch は React 16 を使用している
- Fiber ノードを使えば Scratch を外部からハックできる
- Redux ストアからアクセスできない値も Fiber ノードを使えば変更できる
みなさんも、Scratch のソースコードを読み、ハックしてみてください。Happy Hacking!
Discussion