👋

[humanify] 圧縮されたjsをLLMでデコンパイルする

2024/08/11に公開

JSデコンパイルの技術

  • 世の中には、JavaScriptで書かれたたくさんのマルウェアが存在します。
  • 以前はChrome拡張機能として問題視されることが多かったこの問題ですが、最近はVSCodeやpolyfill.ioなど、より広範囲に渡って問題が顕在化しています。
  • 今回は静的解析でのマルウェア解析について紹介します。
  • これらのプログラムは圧縮/難読化されていることが多く、そのままでは読むことができません。
  • 読みやすくする工程のことをDeobfuscateと言います。

Deobfuscateをする方法についてHackTrickでは3つのツールが紹介されています。

https://github.com/pionxzh/wakaru

Javascriptデコンパイラ、アンパッカーおよびアンミニファイツールキット

Wakaruは現代のフロントエンド用のJavascriptデコンパイラです。バンドルされ、トランスパイルされたソースから元のコードを復元します。

https://github.com/j4k0xb/webcrack

obfuscator.ioをデオブフスケートし、ミニファイを解除し、バンドルされたjavascriptをアンパックします

https://github.com/jehna/humanify

ChatGPTを使用してJavascriptコードをアンミニファイします

今回はこのhumanifyを利用していきます。

humanifyの使い方

ちょっと使いにくかったので、修正してpublishしたバージョンを利用します。

https://github.com/HikaruEgashira/humanify

(8/24追記)最近出たhumanify v2はいい感じになってたのでそちらお勧めします

npx humanifyjs openai -k sk-xxx background.bundle.js
# → output/...

まずはobfuscated(難読化された)なコードを用意します。
Chrome Extensionは以下のフォルダに配置されます。
Defaultはプロファイルを複数作ってる場合、Profile 1のようになってる場合があります。

# MacOSの場合
~/Library/Application Support/Google/Chrome/Default/Extensions

# Windowsの場合
C:\Users\{ユーザー名}\AppData\Local\Google\Chrome\User Data\Default\Extensions

npxを使ってhumanifyを実行します。

OPENAI_API_KEY=sk-xxx npx @hikae/humanify ~/Library/Application\ Support/Google/Chrome/Default/Extensions/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/1.0.0_0/js/background.js -o output

これでoutputディレクトリにデコンパイルされたコードが出力されます。

$ tree output
output
├── 1.js
├── 2.js
...

何をしてるのか

humanifyは「webcrack」、「beautifier」、「rename」、「prettier」の4工程に分けられます。

webcrack

  • 多くのobfuscatorやbundlerに対応したデコンパイルライブラリです。
  • これで可読性はかなり上がりますが、変数名が1文字だったりとまだ読みにくいです。

beautifier

  • humanify.tsに実装されたbabel pluginです。
  • convertVoidToUndefined, flipComparisonsTheRightWayAround, makeNumbersLongerが実装されており、ここでも可読性が向上します

rename

  • humanifyの一番の特徴がLLMを用いた変数名の変換です
  • プログラムから1文字変数/関数を一般的な名称に変換する処理を行います
  • 本家ではLocal LLMで実行するモードが存在してますが、zeromqがM1 Macだと動かすの面倒なので削除しています
  • 以下のプロンプトで変数名のマッピングを生成してそれをbabel plugin経由で変換します
JavaScriptのすべての変数と関数を、コード内での使用に基づいて説明的な名前にリネームします。

prettier

  • 最後にprettierをかけてコードを整形します

実際の結果を見てみましょう。
以下はChrome Extension 「SimilarSite」 の例です。

$ OPENAI_API_KEY=sk-xxx bunx @hikae/humanify -o out ~/Library/Application\ Support/Google/Chrome/Default/Extensions/necpbmbhhdiplmfhmjicabdeighkndkn/7.3.8_0/background/background.js
deobfuscating: out/deobfuscated.js
This process may take some time.
Splitting code into blocks
Splitted code 9 blocks
...  
  { name: 'l', newName: 'localStorageHandler' },
  { name: 'd', newName: 'directory' },
  { name: 'f', newName: 'fetchService' },
  { name: 'c', newName: 'chromeRequest' },
  { name: 'm', newName: 'messageProcessor' },
  { name: 'e', newName: 'extensionFunction' },
  { name: 't', newName: 'parameter' },
  ...
  { name: 'Ba', newName: 'Lodash' },
  ... 342 more items
]
deobfuscated: out/deobfuscated.js
$ tree out
out
└── deobfuscated.js

fetchService, chromeRequestみたいな文字列が出てきておりこの辺りを読めば拡張機能が通信をしているということがわかります。
変数BaがLodashに変換されました。検索してみると確かにLodashみたいな関数が定義されてます。

...
          Lodash.constant = nu;
          Lodash.countBy = bs;
          Lodash.create = function (inputData, transformFunction) {
            var optionalArgument = objectConstructor(inputData);
            if (transformFunction == null) {
              return optionalArgument;
            } else {
              return checkFunctionValueExist(
                optionalArgument,
                transformFunction,
              );
            }
          };
          Lodash.curry = function inputData(
            transformFunction,
            optionalArgument,
            lengthOrDefault,
          ) {
            var index = Zr(
              transformFunction,
              8,
              emptyArray,
              emptyArray,
              emptyArray,
              emptyArray,
              emptyArray,
              (optionalArgument = lengthOrDefault
                ? emptyArray
                : optionalArgument),
            );
            index.placeholder = inputData.placeholder;
            return index;
          };
...

参考

Discussion