初めに
僕は自分の知識を赤の他人に教えるということがそもそもあまり好きではない。
何かとても強い理由がある、というわけでもなく、性格が悪いというだけなので、あまり気にしないで欲しい。
一応、ある程度Exploitation手法という分野に関しては知識と経験はある程度持っていると思っているし、今の意味不明なCTFブームを鑑みても、ももいろテクノロジーのような記事を書いたら喜ばれたのかもしれない。
でも単純にそれがめんどくさいので嫌だった。
さて、@ntddkのReturn-oriented programming以後という記事を読んだ。
僕からすると引用先の論文のURL以外は既知の内容であった。
最後に紹介されているCOPもそうである、というか@ntddkに初めてCOPについて言及したのは僕である。
先ほど「論文のURL以外は」と言ったように、僕はあまり論文のサーベイはしないし、サーベイが上手くないので、「既になされている研究なのか」や「既に広く知れ渡っていることなのか」ということはあまり把握してない。
そもそも、僕は論文を読んでCOPを見つけたのではなく、自分でExploitを書いてる内に普通に身についていた物なので、勝手にCOPと呼んでいただけで、既に論文があるということは知らなかったし、無いなら無いで秘密にしておこうと思った。
しかし、既にCOPは論文内で少し言及されていて、このまま知れ渡るのも癪なので、COPと呼ばれるExploitation手法について少し紹介しようと思った。
この記事ではCOP以外の手法について詳しく述べるつもりはないので、それらについては他の文献を参照のこと。
紹介
Call Oriented Programming(COP)とは、Return Oriented Programming同様call命令をベースとして、命令gadgetの連鎖を起こし、期待する副作用を得るという攻撃手法である。
知っての通り、広義の意味でROPに含まれるReturn to libcがSolar Designerによって公開されて17年、Return Oriented Programmingがいくつかの場所で紹介されるようになって7年近くが経とうとしている。
近年になって、やっとROPに対するmitigation手法がいくつか考案され、実装されるようになってきた。
有名なもので言えば、ROPGuard, ひいてはEMETだろう。
Stack Pivot等の副産物的な攻撃手法等に対する対策もあり、まあ、ある一定の効果はあると思う(実はcall-retの対応の確認は、__libc_csu_init のような関数テーブルから関数を呼び出してくれるものさえあれば簡単に騙すことが出来るので、個人的にはStack Pivot以外のmitigationはほとんど意味をなしてない。それどころかパフォーマンスの都合上、Exploit検知は CreateProcess等の重要な関数にしか働いていないので、それまでに取り繕えばStack Pivot検知さえも回避してしまうことは実は可能であるのだが)。
少 なくとも、現在の一般的なアーキテクチャ上でプログラムもExploitもretという命令に依存してしまうことは当然と言えば当然であり、プログラムが retと等価な操作を行えつつ、Exploitがretを利用することは防いでしまうような対策が取られれば、Return Oriented Programmingは破綻する。
しかし、 retが「スタックの最も上に存在しているアドレスに対するpopとjmp」と同値であることは分かりきっており、ret命令が豊富に存在しており使い やすかったというだけで、ret命令がなくなってもなお、任意のアドレスにジャンプすることはjmp命令によって当然出来る。
この考え方に基づいて提案されているのがJOPである。
jmp命令による任意のアドレスジャンプで最もイメージしやすいのはjmp eaxのようなレジスタが指し示すアドレスへのジャンプであろうか。
これは、結局レジスタを任意の値にセットした状態でjmp eaxを呼び出せばretとほぼ同値な操作が出来るようになる(利用するアドレスの存在がスタック上にあるか、レジスタにあるかというだけである)。
ここまで把握していれば、COPも存在しうるということがすぐに理解できるはずである。
何故なら、callは「あるアドレスのスタックに対するpushとそのアドレスへのjmp」と同値であるからだ。
よって「callした後で一度popを行うこと」でjmp命令とほぼ等価な操作を行うことが出来る。
利用目的
紹介ではROPやJOPと同様にCOPをジェネリックな手法として用いることが出来るということを説明した。
ここで、COPがJOPと異なる点は何かということが気になってくる。
COPはJOPと比べるとやや直感的でない上に、無駄な処理を増やしてしまうため、COPとJOPが同値な操作を行うことが出来る、というだけでは、あまり便利ではないのだ。
例えば、以下のようなコードブロックについて考えてみよう。
見ての通り、昔のx86のWindowsバイナリで見られるような、関数呼び出し部である。
本来の使用法としては、当然esiにプログラム内で呼び出したい関数のアドレス、edx, eaxにはそれぞれ引数を与えるわけであるが、exploit内でgadgetとして用いる場合は違った見方が可能になる。
その1つとして、esiに1pop-ret gadget、edxに本当に自分が呼び出したいgadget、eaxには1つ前に呼び出したAPIの戻り値が入ってる状態でこのコードブロックに飛んだと仮定しよう。
まずcall esiされ、スタックにはこのコードブロックへのreturn addressがpushされ、1pop-ret gadgetへと飛ぶ。
1pop-ret gadgetでは、1度popした状態でret命令が実行されるから、popでコードブロックへのreturn addressがpopされ、edxに入っていた目的のgadgetへとreturnされる。
お分かりいただけただろうか。
つまり、一度関数の呼び出し部を経由して本来の目的のgadgetへとジャンプすることで、eaxをスタックにpushした状態にすることに成功するのだ。
このように、COPはROPではあり得なかった「スタックへのpush」という操作を可能にする場合がある。
もちろん、これは呼び出し規約に依存するものであり、例えばレジスタ渡しであれば今言ったような「スタックへのpush」を実行することは出来ないだろう。
しかし、ret周辺に存在する命令しか利用できなかったROPに比べると、関数呼び出し部付近の命令を用いることの出来るCOPは確実に表現力が増す。
x64において、rdiやrsiを任意の値にするgadgetはほとんどと言っていいほど皆無であるが、COPを利用することでrdiやrsiを任意の値にした上で本来呼び出したい関数を呼び出すなどの使用法が考えられる。
また、jmp命令よりも、利用できる確率が高いという点で確実にJOPより利用できる機会は多いだろう。
最近はよく知られているが、__libc_csu_initのような関数テーブルから“関数を呼び出す”gadgetは、著名なコンパイラが吐くバイナリのほとんどに存在している。
従って、アドレスが既知なメモリ内の値を書き換えることが出来るならば、ほとんどの場合COPを用いることが出来るのである。
後はそもそも、ret命令が使えない場合というのが存在する。
x86, x64で言えば、ret命令前にleave命令が存在しており、スタック領域のアドレスが既知でない上にstack pivotによるstagerが利用できない場合、leave命令によってstack pointerがbase pointerに変わってしまうと、gadgetの連鎖を続けることが出来なくなるため、leaveが存在している箇所でのret命令ベースのgadgetの連鎖は不可能となる。
その場合においても、COPは基本的な制約さえ満たしてしまえば、まだある程度自由度の高いexploitが構築できる。
問題点としては、COPはROPが検知されうるような箇所のbypassにおいては有効であるが、pop命令を必要とする場合が多いため、ret命令自体が利用できない場合、COPも失敗する可能性はあるということである。
Proof of Concept
PoCとして、以下に1つWindowsのバイナリとexploitを載せる。
それ自身に欠点のないASLRはbypass出来ていないため、不完全なexploit codeではあるが、base addressとDLL内部のアドレスさえ合わせれば動くはずである。
このPoCはついでにEMET4.1をbypassするデモにもなっているので、Stack Pivot検知をどう回避しているか等も読むことが出来るなら分かると思う。
vuln.exeはVC++2010でStack Guard以外の特別なオプション無しでコンパイルしたものであり、脆弱性を含む自作プログラム内の命令は全く用いていないおらず、コンパイラが標準で吐き出した命令とidata上のAPIだけを利用している。
必要であれば、PoCコードの解説は書くつもりではいるが、大したことはしていないので、今のところ予定はない。