OpenAI、コード復元(Unminifying)で驚くべき性能を示す
(glama.ai)- ある開発者がWebで見かけたASCIIアートコンポーネントの実装を理解しようと、縮小されたJavaScriptをChatGPTに入力したところ、難読化された流れを読みやすい構造に展開した
- 元の実装は、Reactコンポーネントがウィンドウサイズに合わせて列・行を計算し、
setIntervalで60fpsの文字列グリッドを更新する方式だった - ChatGPTは文字セットの選択、座標・時間ベースの文字計算、
div.textContentの更新フローを説明したうえで、同等のTypeScript/Reactコードとして書き直した - 当初は
░▒▓█のようなブロック文字が抜けており実装が異なると見られたが、元のスクリプトを再確認するとコピー&ペースト時のエンコーディング問題だった - 正しい文字セットに更新した結果、出力は元のコンポーネントと同じになり、縮小コードの理解と再実装にChatGPTを実用的に使えることを示した
縮小されたJavaScriptからASCIIアート実装を見つける
- reactive.network hackathonページのASCIIアートブロックがどのように実装されているか確認するため、ソースコードを調べた
- 関連コードは縮小されており、人間がすぐに読むのは難しく、通常は自力で解読するか
.mapファイルを探して元のコードを復元する必要がある状況だった - 今回は縮小されたコード全体をChatGPTに貼り付け、コードの動作を説明してほしいと依頼した
縮小コードから見えた構造
- コードはJavaScriptとReactを使って動的なテキストコンテンツを生成する構造だった
- 主な要素は次の通り
Math.floor、Math.abs、Math.minのような数学関数に短いエイリアスを付けて使用している"reactive.network REACTIVE.NETWORK"と特殊文字・ブロック文字で構成された文字セットを持つDate.now() % 3の結果に応じて使用する文字セットを選択する- 選択された文字セットの長さを基準に、各座標に表示する文字インデックスを計算する
- 中核関数である
eTは、位置座標と時間値を受け取り、表示する文字を1つ返す- 時間値は
t.time * 8e-5に変換される - 列・行のうち小さい値と画面比率を使って座標を補正する
Math.cos(position.x * position.x - position.y * position.y)の計算結果と時間値を組み合わせて文字インデックスを決める
- 時間値は
Reactコンポーネントのレンダリング方式
- コンポーネントは
useRefでテキストを入れるdivを参照し、useStateでウィンドウのheightとwidthを保存する - 1つ目の
useEffectはwindow.innerHeight、window.innerWidthで初期サイズを設定し、resizeイベントが発生したときにサイズを更新する - 2つ目の
useEffectは画面サイズをもとにASCIIグリッドサイズを計算するcolsはMath.floor(width / 12) * 1.6rowsはMath.floor(height / 12)aspectはcols / rows
setIntervalは1_000 / 60間隔で実行され、60fpsに合わせて文字列を再生成する- 各フレームではすべての行と列を走査して計算された文字を連結し、最終的な文字列を
element.textContentに入れる
TypeScriptで読みやすいコードに書き直す
- 開発者は続いて、同等のコードをTypeScriptで実装しつつ、人間が読みやすい形にしてほしいと依頼した
- ChatGPTが作成したコードは、縮小コードの役割を次の名前に展開した
selectedCharacterSetcharacterSetLengthcalculateCharacterSize型AsciiArtGeneratorReactコンポーネント
- 難解な変数名は、実装意図が分かる名前に変わった
calculateCharacterは座標、列・行、画面比率、時間値を受け取り、表示する文字を計算するtextRefは実際のテキストが入るdivを指すsizeとsetSizeはウィンドウサイズの状態を表す
- 書き直されたコードは元の縮小コードよりはるかに読みやすく、コンポーネントの実装方式を学ぶには十分なレベルだった
文字欠落の誤解とエンコーディング問題
- 初期の比較では、LLMがいくつかの実装詳細を見落としたように見えた
- 特に元のコンポーネントに見えていた
░▒▓█のような文字がChatGPTの結果に抜けていると判断した - その後、HNフォーラムのユーザーがコピー&ペーストの問題である可能性を指摘し、追加確認の結果、元のコードにはChatGPTに貼り付けた文字とは別の文字が入っていた
- スクリプトを直接ダウンロードすると正しい文字を取得でき、問題の原因はエンコーディングの違いだと確認された
- 文字セットを修正した後、出力は元のコンポーネントと同一になった
1件のコメント
Hacker Newsのコメント
HumanifyJSを作った者ですが、この用途向けにLLMベースのツールを別途作りました
LLMをASTレベルで使い、難読化解除後もコードが動き続けることを保証します: https://github.com/jehna/humanify
通常のコードからユーザーによる意味のある名前をすべて消したうえで、アルゴリズムや残った名前、たとえば組み込み名をもとに新しい名前を選ぶ機能のことです
リファクタリング時にLLMで手作業することがたまにありますが、
parallel_line_opposite_directionの代わりにantiparallel_lineのようなより標準的な用語を見つけたり、find_animal_instance_in_animalsの代わりにfind_instance_in_listのような、より一般化しやすい名前を見つけたりするのに役立ちます正直、ASTレベルのバージョン管理をいまだに待っています
関数全体を渡して「何をリネームすべきか?」と、すべて変わるまで繰り返し聞く方式なのか、それとも全体を一度にやらせてから出力をパースし、ASTがまだ一致しているか確認する方式なのか気になります
5万行くらいのものを想定しています
追記: 今、わずか1.2千行のJSファイルをopenaiモードで回しているところですが、20分経っても70%しか終わっていません。理論上5万行のファイルで動くとしても、試さないほうがよさそうです
JSの圧縮はかなり機械的で単純なので、元に戻す作業も比較的簡単なはずです
もちろん一般的には人間が手でやるには十分退屈ですが、変換自体はかなり限定的なので、難読化された識別子を追跡するメモさえあれば読むことは可能です
より一般的な圧縮解除や難読化解除は、まだ未解決の問題のように見えます。以前、意図的に難読化したプログラムをいくつか書いたことがありますが、私の経験ではChatGPTは表面的なレベルでも理解できませんでした
たとえば、160バイトのCで書いたBrainfuckインタプリタのgistに、GPT-4でコードを説明しようとしたコメントがありましたが [1]、その「明確化された版」は元のコードとはまったく似ていませんでした
[1] https://gist.github.com/lifthrasiir/596667#gistcomment-47512...
たとえば乗算/素因数分解、微分/積分、過去の記憶/未来の予測があります
コードの難読化解除は明らかにこうした難しい逆問題の一つであり、バグ、使われていない、あるいは無関係なルーチン、偶然正しい結果を出す誤った実装によって、簡単にさらに難しくなり得ます
そういう意味では、ChatGPTが難読化解除でそれなりの結果を出すなら喜ばしいことで、先験的にそうできるはずだという期待はありません。より難しい問題でChatGPTの限界を確認したのは良いことですが、見つかったのは上限にすぎないと思います。元記事の例を些細だとは見ていません
let userSignedInTimestamp = new Date()をlet x = new Date()に変えるのは些細ですが、逆に進むにはxがどんな文脈で使われているかを見るために周辺コードを読み、理解する必要がある可能性が高いですそのうえ残りのコードも圧縮されているのでさらに難しく、すべて正しくできたとしても結局は不可逆変換なので、変数名がコードに明示されていない特性を含んでいた可能性もあります
これは過程でデータが失われたためです
圧縮も同じように機能します。コードを理解するために必要な情報が多く失われ、その情報を取り戻すのは非常に厄介になり得ます
timeFactorをはるかに短いiに変える行為は機械的で単純かもしれませんが、その過程は破壊的で、元に戻すのは比較的簡単ではありません実際にコードが何をしているのかをかなり精巧に理解しない限り不可能です。ここでLLMがやったのはまさにその部分で、驚くことではないにせよ見事です。あまりにも即座に貶める態度はよくありません
PCではないのでテストはできていません
タイトルには会社名であるOpenAIではなく、ChatGPTやモデル名のGPT-4を使うべきでは?
LLMはテキスト変換に優れている
これが中核的な強みなのに、十分に活用されていないように思う
推論したり知識を保持したりするという他の能力は、意図された意味を「理解」せずにテキストを再生成する、この超人的な能力の副作用にすぎない
たとえばリストのソートはできないが、言語翻訳はできる。ほぼ正しくソートされたリストは誤りだが、ほぼ正しい翻訳は私たちがしばしば受け入れるからだ
チームがPythonコードの大きな塊をElixirに置き換える価値があるか悩んでいるなら、もう推測だけに頼る必要はない
少し前にPythonスクリプトをJavaScriptに翻訳してみたが、非の打ちどころがなく、少しのガードレールがあれば規模を拡大できそうだった
私たちは主に、別の形式から構造化データを抽出するために使っている
そのうえ、awk、Python、Perlなどでより速く処理するための知識や技術も不足している必要がある
圧縮を難読化手段として使っていた開発者の一部は、間違いなく腹を立てそうだ
昔のColdFusionのバージョンにコードの「暗号化」ツールがあったことを思い出す。アルゴリズムが非常に弱かったので、誰かが復号ツールを作るのに長くはかからなかった
それでも、そのツールは安全だと信じていて、ソースを公開せずにコードを売るときに使っていた人たちは嫌がっていた。オープンソースが圧倒的なデフォルトになる前、90年代後半から2000年代前半の話だ
難読化されたクライアントサイドコードに一部依存するコンテストをWebサイトが提供し、賢い参加者はそれをChatGPTに入れて勝率を高められるようになる
まだ始まったばかりだ
これは人間より優れた知的遂行能力の例だ
否定できない。この作業は知的であり、単純な暗記とは関係がない。Web上には、LLMが学習できる圧縮コード/展開コードのペアが膨大にあるわけでもない
LLMは自分が展開している対象を理解しており、この特定のテーマに限っては概して人間より優れている
Web上に圧縮コードと非圧縮コードのペアが多くないと確信できるのか?既存のJSから簡単に生成して学習セットに使えるし、すでに学習に使われた非圧縮JSも膨大にある
人間が圧縮されたコードを理解しにくい理由は、不透明な変数名、不自然なループ、最小化された空白などにある。コンピューターはこうしたものを難しいとは感じないし、そもそも私たちが圧縮する理由もそこにある。アテンション構造はこうした作業を得意とするはずだ
圧縮/非圧縮コードも非常に多いと思う。ソースマップファイルの目的がまさにそれだ。OpenAIがWebブラウジングとソフトウェア開発に投資していることを考えると、学習に圧縮/非圧縮データを含めていても驚かない
コンピューターが1000桁の数を即座に掛け合わせるのは、人間がまずそうした道具を作り、掛け算に成功した例だ。そこで知的に成功したものが他に何かあるのか?コンピューターが自分自身を作ったわけでもない
人間の知能の境界を頭蓋骨で切り、その知能が作って使う道具を問題解決過程の一段階と見なさないなら、人間は宇宙へ飛んだり、手術をしたり、食事の大半を調理したりできるほど知的ではない、ということも受け入れなければならない
変数名が失われたコードの抽象的な形を認識し、巨大なインターネットで見た似たパターンに、人間が読める変数名をマッピングすることは、途方もないことには見えない
興味深いし、かなり良くなったように思う
1年少し前、ハッキングされたWordPressサイトのカスタムプラグインについて助けを求めるメールを受け取り、悪性コードの難読化解除にGPTを使ってみようとした。そのときは、自分でコードを段階的に追ったほうがずっと進みがよかった
この記事を読んで、もう一度試してみた [0]。理解できる内容は出してくれたが、Windowオブジェクトを通じて読めない文字列を事実上evalする程度に難読化されていたので、それだけでは十分ではなかった
当時送った報告書の一部はこうだった。激しく難読化されたJavaScriptコードを掘り下げ、ページ読み込みを検知し、理論上は定数である計算の見せかけを呼び出したうえで、ページを悪性サイトへリダイレクトするロジックを解読できた、という内容だった
[0] https://chatgpt.com/share/f51fbd50-8df0-49e9-86ef-fc972bca6b...
デコンパイラLLMに取り組んでいる人はいるのだろうか?
すべてのコードをオープンソースのようにできそうに思える。
この場合、学習データは作りやすい。無料のGitHubコードを複数のコンパイラで大量にビルドし、コンパイルを元に戻すよう学習させればよい。合成学習データが適切で、かなり簡単に生成できるケースだ。
デコンパイラはコンパイルを元に戻すことだけを学習させ、コメント追加のような作業には既存のより大きなコードLLMを使えばよい。
オープンソース化だけでなく、プロプライエタリアプリを簡単にデコンパイルして修正し、バグを直したり機能を追加したりする状況も想像できる。特に、ずっと前に死んだプログラムにとっては大きな解放区になり得る。
法的な理由から、主要モデルではこうした動作はブロックされた機能になる可能性が高い。
そのような権利を与えられるのはコードの作者だけだ。
AIがコードを本当にうまく理解するなら、単にバイナリを作ったり直したりするはずだ。
本物のAIプログラマーが現れたら、人間が読めるコードなど気にせず、すべて機械語でコーディングすると信じている。既存システムを保守しろという課題を受けたら、ソフトウェアとハードウェアのスタック全体を丸ごと理解するだろう。
画像生成の拡散モデルが実際の描画技法を学ぶことにこだわらないのと似ている。
変数名や関数名を変え、改行を入れることに近い。
私がLLMをよく使うやり方と非常によく似ている [0]
自力でコードを読まなければならないとき、解読の最初のステップとして使っている。
スパゲッティのようなめちゃくちゃなコードを入れて、もっと読みやすく展開してくれと頼み、そこから始められるのはものすごく解放感がある。
著者も見つけたように、LLMはときどき細部を見落とすが、そこは自分で拾えばよい。
もう一つの使い道は、コードが何をしているかは理解しているが、なぜそのような形になっているのかが頭に入りにくいときだ。特に、そのコードを書いた人がもう会社にいない場合にそうだ。そのときはメソッドをLLMチャットに入れ、何をしているかを説明したうえで、なぜいくつかのものが特定のやり方になっている可能性があるのかを尋ねる。
常に完璧ではないが、かなりの頻度で、実際に筋が通っていてレビューにも耐え、新しい洞察を与える説明を返してくる。一度か二度は、後で面倒になっていたであろうリファクタリングを防いでくれたこともある。
[0] ChatGPTと、最近は主にClaude系の複数の別モデルをフロントエンドにするopenwebUIを使って違いを見ている。異なるモデルに互いの回答をレビューさせる面白い構成も可能だ。
展開されたコードが圧縮されたコードと一致しないなら、最後のほうにある「LLMの応答はいくつかの実装詳細を見落としているようだ」という内容のように、有用性は大きく下がる。
特に多くの場合、記事のようにコードを簡単に実行して差分を確認できないからだ。
追記: 下のスレッドを見ると、エンコーディングの問題だったようだ。それでも正確性についてはまだ少し心配している。
場合によっては、ChatGPTが展開に加えて独自のリファクタリングまで行っていた。
元のコードは
handleResizeを即座には呼び出さず、その内容をインラインで持っている。おそらく圧縮ツールが実際にインライン化したのだろう。ここで唯一の実際の違いは、if (typeof window < "u")条件が抜けていることだ。具体的にはブロック文字のことだが、HNはUnicode文字の貼り付けを許していないように見える。元の版では見えるが、ChatGPT生成版では見えない。
ただし、私が必要な文脈をすべて含めていなかったためである可能性も十分ある。
文字が数個抜けているという理由で出力全体を貶めるのは、細かすぎる揚げ足取りに近い。それ以外は、私には出力は同一に見える。
変数名とメソッド名だけを変えるよう頼んでいれば、元のコードにもっと近かったと思う。