Headless Chromeを使ってプロパティアクセスや関数の実行を動的検出する
この記事はRecruit Engineers Advent Calendar 2020の14日目の記事です。
ふとChrome DevTools Protocolで遊んでみようと思い立ち、試しにプロパティアクセスや関数の呼び出しの動的検出に挑戦してみたのでこれについてまとめておきます。
Chrome DevTools Protocol
Chrome DevTools Protocol(以下、CDP)はChromiumと相互的に通信するためのプロトコルです。 Chromiumが処理しているNetworkやProfileなどの情報を取得したり、逆にChromiumに指定のウェブサイトを開かせたり任意のJavaScriptを実行させたりできます。 CDPを使用している有名なプロジェクトとして、Chrome DevToolsやpuppeteer/puppeteerなどがあります。
今回の目標
globalThis['eval']('document.write(navigator.userAgent)')
をJavaScript Obfuscator Toolを使って難読化して読み込ませたウェブサイトを作成しました。
ソースコードを見ると、evalやnavigator、userAgentといった文字列が見つからないことがわかります。
今回は、local環境でホストしたこのウェブサイトから以下の2つを動的に検出することを目的とします。
-
navigator.userAgent
へのアクセスを検出する -
eval
の呼び出しを検出する (引数の値も取得する)
検出までの流れ
CDPのPage.addScriptToEvaluateOnNewDocument(source: string)
は引数に指定したJavaScriptのコードをドキュメントを読み込む前のタイミングで実行することができます。これを利用して、プロパティアクセスや関数の実行の際に任意の関数を実行させることが可能です。
また、Runtime.consoleAPICalled
はConsole APIが呼び出された際に発生するイベントです。今回はconsole.log
を用いてプロパティアクセスや関数の実行を通知させるようにしました。
検出までの手順は以下の通りです。
-
Page.addScriptToEvaluateOnNewDocument
を用いて、Chromium側でのプロパティアクセスや関数の実行時にconsole.log
を実行するように細工する -
Page.navigate
を用いて、Chromiumで対象のウェブサイトを開く - Chromium側でプロパティアクセスや関数の実行が行われ、対応する
console.log
が実行される - CDPを通して
Runtime.consoleAPICalled
イベントが発生し、検出に至る
プロパティアクセスを検出する
Page.addScriptToEvaluateOnNewDocument
で実行するScript
(function (target, prop) {
let value = target[prop]
const {
get = () => value,
set = v => { value = v },
} = Object.getOwnPropertyDescriptor(target, prop) ?? {}
Object.defineProperty(target, prop, {
get: () => {
console.trace({ mode: 'get', target, prop, value })
return get()
},
set: v => {
console.trace({ mode: 'set', target, prop, value })
return set(v)
},
})
})(navigator, 'userAgent')
検出結果
{
message: 'detect accessing property: navigator["userAgent"]',
arguments: [
{ name: 'mode', type: 'string', value: 'get' },
{ name: 'target', type: 'object', value: 'Navigator' },
{ name: 'prop', type: 'string', value: 'userAgent' },
{ name: 'value', type: 'string', value: '{{USER_AGENT_STRING}}' }
],
stackTrace: [
{
functionName: 'get',
scriptId: '11',
url: '',
lineNumber: 11,
columnNumber: 20
},
{
functionName: '',
scriptId: '4',
url: 'http://{{TARGET_WEBSITE}}/src/index.js',
lineNumber: 3,
columnNumber: 22
}
]
}
関数の実行を検出する
Page.addScriptToEvaluateOnNewDocument
で実行するScript
(function(target, prop) {
const original = target[prop]
target[prop] = function() {
console.log(arguments)
return original.apply(this, arguments)
}
})(globalThis, 'eval')
検出結果
{
message: 'detect calling function: globalThis["eval"]',
arguments: [
{
name: '0',
type: 'string',
value: 'document.write(navigator.userAgent)'
},
{ name: 'callee', type: 'function', value: '' },
{ name: 'Symbol(Symbol.iterator)', type: 'function', value: '' }
],
stackTrace: [
{
functionName: 'target.<computed>',
scriptId: '3',
url: '',
lineNumber: 4,
columnNumber: 20
},
{
functionName: '',
scriptId: '4',
url: 'http://{{TARGET_WEBSITE}}/src/index.js',
lineNumber: 117,
columnNumber: 28
}
]
}
終わりに
プロパティアクセスや関数の実行を動的検出する方法について解説しました。実際にCDPを使ってみた感想として、簡単に取り扱えて幅広く活用できそうだと感じました。もっと簡単な検出方法やCDPで検出したいテーマがあれば、コメント等で教えていただけると幸いです。
今後もCDPを使って色々遊んでみたいと思います!
Discussion