はじめに

QEMUは動的バイナリ変換を用いた完全仮想化式のハイパーバイザである.
QEMUは実行対象の命令列を逆アセンブルし,いちど中間表現に変換したうえで,ホストのアーキテクチャの命令列に変換して実行する.これによりQEMUは異なるアーキテクチャのバイナリを実行することができる.
しかしQEMUは遅かったため,アクセラレータとしてkqemuが開発された.
kqemuはユーザーモードのコードをホストのCPUに実行させる準仮想化ドライバであり,やがてKVMへと変貌を遂げた.さらにKVMはハードウェアによる仮想化支援機能を用いることで,さらなる高速化を実現した.もはやKVMにおいてQEMUはハードウェアのエミュレーションにしか用いられていない.
かわいそうなQEMU!
しかしQEMUの活躍する場面はいまだ多く残されている.たとえばマルウェア解析で.
それでも遅いのは困りものだ.QEMUはどこが遅いのだろうか.
ここではperfを用いてQEMUのボトルネックを雑に特定する.

実験

今回はARMエミュレーションについて調査した.ホスト・ゲストともOSにはUbuntu 12.04(3.2.0-23-generic)を用いた.perfでQEMUのプロファイリングを行っている間,ゲストではgccを用いてコンパイルを実行した.

  • ARM版Ubuntu 12.04のインストール

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # sudo apt-get install qemu
    # wget http://odroid.us/odroid/users/osterluk/qemu-example/qemu-example.tgz
    # tar xzf qemu-example.tgz ./zImage
    # wget http://releases.linaro.org/12.04/ubuntu/precise-images/developer/linaro-precise-developer-20120426-86.tar.gz
    # tar xzf linaro-precise-developer-20120426-86.tar.gz
    # qemu-img create -f raw rootfs.img 3G
    # mkfs.ext3 rootfs.img
    # mkdir mnt
    # sudo mount -o loop rootfs.img mnt
    # rsync -a binary/boot/filesystem.dir/ mnt/
    # sudo umount mnt
  • QEMUのビルド

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # sudo apt-get build-dep qemu
    # git clone git://git.qemu-project.org/qemu.git
    # cd qemu
    # git log | grep '^commit' | head -1 | awk '{print $2}'
    6169b60285fe1ff730d840a49527e721bfb30899
    # git submodule update --init dtc
    # git submodule update --init pixman
    # ./configure --extra-ldflags=-pg --target-list=arm-softmmu
    # make
  • perfのインストール

    1
    2
    # sudo apt-get install linux-tools
    # sudo echo 0 > /proc/sys/kernel/perf_event_paranoid
  • perfによるプロファイリング

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    # perf record -a -g -F100000 qemu/arm-softmmu/qemu-system-arm -M vexpress-a9 -m 512 -kernel zImage -sd rootfs.img -append "root=/dev/mmcblk0 rw physmap.enabled=0 console=ttyAMA0" -monitor stdio
    WARNING: Image format was not specified for '../../rootfs.img' and probing guessed raw.
    Automatically detecting the format is dangerous for raw images, write operations on block 0 will be restricted.
    Specify the 'raw' format explicitly to remove the restrictions.
    QEMU 2.3.90 monitor - type 'help' for more information
    (qemu) audio: Could not init `oss' audio driver
    (qemu) q
    [ perf record: Woken up 37 times to write data ]
    [ perf record: Captured and wrote 9.465 MB perf.data (~413514 samples) ]
    Failed to open /tmp/perf-6993.map, continuing without symbols
    Failed to open [vmxnet], continuing without symbols
    Failed to open [vmci], continuing without symbols
    Failed to open [vsock], continuing without symbols
    no symbols found in /bin/dash, maybe install a debug package?
    no symbols found in /usr/bin/xargs, maybe install a debug package?
    no symbols found in /usr/bin/updatedb.mlocate, maybe install a debug package?
    no symbols found in /usr/bin/dpkg-query, maybe install a debug package?
  • Flame Graphsによる可視化

    1
    2
    3
    4
    # wget https://raw.githubusercontent.com/brendangregg/FlameGraph/master/stackcollapse-perf.pl
    # wget https://raw.githubusercontent.com/brendangregg/FlameGraph/master/flamegraph.pl
    # perf script> perf_data.txt
    # perl stackcollapse-perf.pl perf_data.txt|perl flamegraph.pl --title "Flame Graphs - qemu-system-arm" > flamegraphs-qemu-system-arm.svg

perfの結果をFlame Graphsを用いて可視化すると下図のようになった.Flame GraphsはUSENIX LISA’2013にて発表された可視化ツールである.

x軸は時系列,y軸はコールスタックを意味する.横幅が広く上位に位置する関数がボトルネックであるといえる.
よってボトルネックはclear_page()すなわちTCGの各関数で発生しているページフォルトである.

おわりに

ページフォルトだけは勘弁してほしい.

参考文献

はじめに

田中久美子『記号と再帰: 記号論の形式・プログラムの必然』(“Semiotics of Programming,” Cambridge University Press, 2010.)は,プログラミング言語の記号論についての記念碑的な書籍である.かねてよりPeter B. Andersenらによってコンピュータ記号論(computional semiotics)は語られていたものの,プログラミング言語については議論が追い付いていなかった.
本書はまず,対応関係が不明瞭であったソシュールの記号論とパースのそれとを,関数型パラダイムとオブジェクト指向パラダイムとの対比をもって整理する.その上で,記号の本質は再帰にある(p.1)として,絵画,プログラミング言語,哲学を横断した考察が展開される.とまあ随分と衒学的な書籍なのだが,それゆえ危うさを孕んでいるようにも思う.ここではあえて,本書の批判とはいかないまでも,いくつかの不満点を提示したい.

汎記号主義

本書は「記号を媒介することなく対象を人間が認識できない」(p.27)とされ,「記号の解釈を記号系の中だけで捉える」(p.26)という汎記号主義を前提としている.この妥当性について考えても埒が明かないので,ひとまずはそういうことにしておこう.

HaskellとJavaによるプログラム例

本書で用いられるプログラミング言語は,HaskellとJavaである.
以下はHaskellによる平面上の長方形,楕円,円の面積を計算するプログラムである(p.15).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
data Shape = Rectangle Double Double
| Ellipse Double Double
| Circle Double
area (Rectangle width height) = width * height
area (Ellipse width height) = pi * width * height / 4.0
area (Circle radius) = area (Ellipse (radius * 2.0)(radius *2.0))
main = let
r = Rectangle 5.0 8.0
u = Ellipse 3.0 4.0
v = Circle 3.0
ss = [r, u, v]
in
for (\s -> putStr("area: "++show (area s)++"\n")) ss
for f [] = do return ()
for f (s:ss) = do { (f s); for f ss}

いきなり不安になってくる.Haskellについてはずぶの素人の私だが,それでもこのような場面ではforMmapMを用いるべきだと知っている.「このfor関数は,次の図2.2のJavaプログラムとの整合性をふまえて定義されている」(p.17)とあるが,首を傾げざるを得ない.なおmapはp.141に至るまで登場しない.
続いて,同様にJavaによるプログラムが示される(p.18).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Shape{
double width, height;
Shape (double w, double h){ width = w; height = h; }
public double area () { return width * height; }
}
class Rectangle extends Shape{
Rectangle (double w, double h){ super(w, h); }
}
class Ellipse extends Shape{
Ellipse (double w, double h){ super(w, h); }
public double area(){ return Math.PI*width*height/4.0; }
}
class Circle extends Ellipse{
Circle(double r){ super(r*2.0, r*2.0); }
}
void run(){
Rectangle r = new Rectangle(5.0, 8.0);
Ellipse u = new Ellipse(3.0, 4.0);
Circle v = new Circle(3.0);
Shape [] ss = new Shape[]{r, u, v};
for (Shape s : ss){ putStr("area: " + s.area() + "\n"); }
}

これらのプログラムは異なるパラダイム,異なる記法によって書かれているが,しかし同じ出力を得ることができる.ここに本書は,ソシュール二元論とパース三元論を託つける.

ソシュール二元論とパース三元論

ソシュールとパースはそれぞれ記号について以下のように整理した.

  • ソシュール二元論
シニフィアン シニフィエ
“tree”
ラベル 内容
  • パース三元論
表意体 解釈項 対象
“tree” 木の解釈
ラベル イデア 対象

この対応について,本書は,パース「記号は人に働きかけ,人の心の中に等価な記号を作り出し,あるいはより発展した記号を作り出す.もとの記号が作り出すこの記号のことを私は解釈項と呼ぶ」を根拠に(p.40),「解釈項は記号の解釈を呼び,記号過程を生成するもので」あり「これに対応するものがソシュールの記号モデルの中にあるならば,それはシニフィエ以外にありえない」(p.41)と導く.
続いて,「記号の差異は他の記号と対比されてはじめて立ち現れるものである.とすると,記号の解釈にまつわる記号の使用はソシュールの記号モデルの構成要素には含まれてはいなが,全体論的価値として位置付けられていると考えることができる」(p.41)ことから,「ソシュールのシニフィエはパースの直接対象に対応し,そしてパースの解釈項はソシュールの記号モデルに外在する全体論的価値に内包される」(pp.41-42)という仮説を提示する.

全体論的価値

関数型パラダイムとオブジェクト指向パラダイムの比較を通じて仮説を検証する前に,この「全体論的価値」とは何を意味するのだろうか.おそらくは意図的なのだろうが,本書にはパロールやラング,レフェランといった語が一切登場しない.これによって混乱してしまったのは私だけだろうか.記号論についてもずぶの素人である私の苦し紛れの解釈では「全体論的価値」はラングに相当するが,詳細は述べられていない.ソシュールの「言語記号は恣意的です」という原理も後に登場する(p.57)が,社会的慣習と全体論的価値が絡められることはなかった.

ソシュール二元論とパース三元論の接続

さて,ソシュールとパースの関係は,先に挙げたプログラム例のarea関数のあり方によって整理される.

プログラミング言語 Haskell Java
記号論 二元論 三元論
area関数 外在的 内在的

Haskellのプログラム例では,areaは他のデータ構造に外在するが,Javaのプログラム例では内在する(p.42).言い換えれば,Haskellにおいては記号がその使用によって意味付けられるが,Javaにおいては記号の定義がその使用を内在する.つまり,次のような対応関係になる(p.49).

  • ソシュールのシニフィアンはパースの表意体に対応する.
  • ソシュールのシニフィエはパースの直接対象に相当する(心的な対象).
  • パースの解釈項は識別子の使用に相当する.パースの記号モデルでは,記号の使用は解釈項として記号モデルの中に埋め込まれ,記号過程は解釈項を次々と呼ぶことによって生成される.ソシュールにおいては,記号過程は記号が別の記号に呼ばれ,それがまた別の記号に呼ばれることにより生成される.ある記号の使用によって記号に付される価値は記号モデルに外在し,それは記号を併置した際の差異として現れる.

こうして,ソシュールとパースの論が接続された.なるほど綺麗にまとまったかのように思える.

プログラミングパラダイム

しかし,これはプログラミングパラダイムから見て妥当だろうか.HaskellやJavaに固有の特徴と,パラダイムの特徴を同一視していないだろうか.本書において,HaskellやJavaを選択した基準は最後まで明確に示されない(私はJavaScriptが良いのではないかと思った).
CTMCPこと『コンピュータプログラミングの概念・技法・モデル』(“Concepts, Techniques, and Models of Computer Programming, “ The MIT Press, 2004.)のPeter Van-Royによると,オブジェクト指向パラダイムとは関数型パラダイムに状態(state)を追加したものである.私はこの考えに沿った展開に期待していたのだが,しかし状態の概念はp.190に至るまでほとんどといって触れられず,HaskellとJavaの比較に絡められることはなかった.
オブジェクト指向すなわちカプセル化という前提に立っているところ,プロトタイプベースのオブジェクト指向について触れていないところも勿体無い.

モナドと状態

状態の概念は,「インタラクションを参照透明に記述しようと思うと,」「莫大な量の記号を使い捨てなければならない」(pp.198-199)からこそモナドが考案されたという流れで援用される.すなわち,参照透明性を保ちつつ状態を記述するためにモナドがある(p.196).失敗系モナドが無視されているにせよ,このあたりの説明が明快なだけに,HaskellとJavaの比較において状態の概念が持ち出されなかったことが残念に思われる.

参照の値渡し

本書はJavaを「関数に値とアドレスのどちらかを渡すのかは,再び文脈により実用論的に決まり,」「プログラマが定義して導入する複合型」は「アドレスを渡す」ものとして紹介している(p.117).これは端的に誤りである.なぜなら,Javaはあらゆる場面において値渡しであり,参照は参照の値(reference values)として扱われるためである.

おわりに

主にプログラミングの観点から,『記号と再帰』に見られる不満点を述べた.要約すると次のようになる.

  • Haskellのコードが微妙
  • 全体論的価値の定義が不明瞭
  • HaskellとJavaを題材に選んだ根拠が不明瞭
  • Javaの参照の値渡しについての説明が不適切

だからといって私は本書が駄本であると切り捨てたいわけではない.
シニフィアンを持たない状態で分節されたλ項にシニフィアンが付与されていく簡約の過程は大変面白かったし,再帰から是態(交換不可能性?)が立ち上るという考えも論証不足ながら興味深かった.
汎記号主義という極端な見地から,記号が投機的に記号系に導入されるからこそ再帰が成立すると言い切ることで,ここまで手際よく語ることができるのかという興奮があった.
だからこそ,(今後本書が重版されることがあればの話だが)今回述べたような不満点が解消されることを望む.
終盤に本書は,「何ら形式的な制約なく自然に作られた」自然言語の構造的な系と,「停止性が保証される最小の記号群からはじめ,停止性の制約下でボトムアップに構築される」プログラミング言語の構成的な系とを対比している(p.183).再帰による是態の獲得.構造的な系.次に語られるのはニューラルネットの記号論だろうか.

参考文献

Dynamic Binary Instrumentation

Dynamic Binary Instrumentation(DBI)とは,実行中のプログラムにコードを挿入する技術である.
その目的は,プログラムの性能評価であったり,アーキテクチャのシミュレーションであったり,プログラムのデバッグであったり,プログラムのsheparding(ポリシを用いたセキュリティ制限)であったり,プログラムの最適化であったり,テイント解析などのデータフロー解析であったり,リバースエンジニアリングであったり,マルウェア解析であったり,……と多岐にわたっている.
デバッガとDBIを区別することは難しく,ニュアンスの問題になってしまうが,DBIはより「挿入するコードがプログラマブルであること」を重視している.また,instrumentation(名詞)/instrument(動詞)という語は,コードの挿入を通してプログラムの情報を取得したり,プログラムを制御したりといったニュアンスを含んでいる.
DBIのメリットとして,言語に依存しないこと,古いソフトウェアに対しても適用できること,再コンパイルの必要がないこと,動的に生成されるコードを扱うことができること,実行中のプロセスにアタッチできることが挙げられる.

Pin

Intelによって開発されているPinは,DBIフレームワークの中でも最も成功している一つであり,そのAPIの数は450種類を越える.
Pinにおけるinstrumentationの手法は,コードの実行時にinstrumentするjust-in-time Instrumentation(JITI)と,イメージ(実行ファイルや共有ライブラリ,構造体)のロード時にinstrumentするahead-of-time-insturumentation(AOTI)の二種類に大別される.
これらのinstrumentにあたって,Pinは解析対象のプログラムにpinvm.dllを挿入する.

JITI

JIITは以下の三種類を単位として行うことができる.

Instruction Level

第一に,最も低い粒度として,命令単位のinstrumentationがサポートされている.
これは,INS_AddInstrumentFunctionのコールバックとして実現される.関連するAPIは,Pin 2.1.3の時点で142種類存在する.

Basic Block Level(BBL)

第二に,Basic Block(BB)単位のinstrumentationがサポートされている.
ここでのBBとは,一つの入口と一つの出口を持ち,内部に分岐を含まない命令列のことであり,コンパイラ最適化におけるそれと同義である.Pinでは,BBL_InsHeadBBL_InsTailBBL_NextBBL_Prevといった関数を用いてBBの有向グラフを操作することができる.一方でBBL_AddInstrumentationFunctionのコールバックは存在しない.関連するAPIは,Pin 2.1.3の時点で14種類存在する.

Trace Level

第三に,Trace単位のinstrumentationがサポートされている.
Traceとは,Pin独自の単位であり,分岐命令を入り口とし,無条件分岐(jmp, call, retなど)を出口とする命令列のことである.つまり,Traceは複数のBBを含みうる.これは,TRACE_AddInstrumentFunctionのコールバックとして実現される.関連するAPIは,Pin 2.1.3の時点で14種類存在する.
Pinでは,objdumpなどと同様に線型分析法によってバイナリを逆アセンブルし,Traceを取得する.ここで,JITIに用いるVMをトラップすることなく別のTraceに分岐を行うためのtrace-linking optimization[PDF]が行われる.そのため,Trace単位のinstrumentationが命令単位のinstrumentationよりも高速になる場合がある.

AOTI

AOTIは以下の二種類を単位として行うことができる.

IMG instrumentation

第一に,IMG単位のinstrumentationがサポートされている.
IMGは,特定の実行ファイルないし共有ライブラリ,構造体に相当する単位である.これは,IMG_AddInstrumentFunction APIによって実現される.また,IMGのセクション(SEC)についてもinstrumentを行うことができる.関連するAPIは,IMGについて27種類,SECについて16種類が存在する.

RTN instrumentation

第二に,RTN単位のinstrumentationがサポートされている.
RTNとは関数(Routine)に相当する単位である.これは,RTN_AddInstrumentFunction APIによって実現される.関連するAPIは,Pin 2.1.3の時点で39種類存在する.

Transparency

このように様々な機能を備えるPinだが,マルウェア解析においてはどれほど有用なのだろうか.
Pinのinstrumentationは,素朴なDLLインジェクションによって成り立っており,Anti-Debuggingについては考えられていない.そのためPinは,親プロセス,引数,ロードされるpinvm.dll, エクスポート関数であるCharmVersionC, ZwAllocateVirtualMemoryの実行権限,KiUserApcDispatcher, KiUserCallbackDispatcher, KiUserExceptionDispatcher, LdrInitializeThunkのインラインフック,命令のパターン,セクション名,FSTENV命令,FSAVE命令,FXSAVE命令,……といった様々な要素から検出される危険性を孕んでいる.
これらは,eXaitを用いてテストできる.なお,筆者の環境ではint2e/SYSENTERについてfalse positiveが発生した.
Pinを検出するマルウェアの存在は寡聞にして知らないが,Pinをマルウェア解析に用いるのであれば,現状の構造は不適切である.
Anti-Debuggingのイタチごっこから脱するためには,エージェントをゲストOSに挿入するin-VMではなく,エージェントをゲストOSに挿入しないout-of-VMな解析環境でなければならない.

QEMU

さらにPinにはカーネルへのinstrumentationが不可能であるという欠点が存在する.
翻ってQEMUは,フルシステムエミュレーションを行うため,アーキテクチャに依存することなくカーネルへのinstrumentationを行うことができる.しかしながら,QEMUのソースコードは難解であり,PinのようにユーザーフレンドリーなAPIが提供されていない.QEMUはC言語によるメタプログラミングとオブジェクト指向の良質なサンプルであるとも言えるが,できることなら避けて通りたい道である.

TEMU

QEMUにテイント解析機能を搭載し,これまで多くの研究で用いられてきたTEMUは,QEMUのソースコードを理解するための労力を削減することに成功した(と,後述するPEMUの論文にすら書かれている)が,APIの充実度はPinに劣っている.また,カーネルモジュールをゲストOSに挿入する必要があり,マルウェアから検出される可能性があること,いまや時代遅れのQEMU 0.9.1をベースとしていることから,現代的なマルウェア解析環境の候補からは外れつつある.
後継のDECAFはQEMU 1.0.0をベースとしており,カーネルモジュールを必要としないものの,APIの充実度はTEMUと変わらない.

PEMU

こうした流れを汲んで開発されたのが,VEE’15にて新たに発表されたPEMUである.嬉しい事にオープンソースとして提供されている.
Pinはin-VMであり,マルウェアの解析やカーネルへのinstrumentationに適さない.QEMUやTEMUはout-of-VMだが,Pinほど充実したAPIを用いることができない.そこでPEMUは,両者の利点と欠点を踏まえた上で,以下の四点を目的として開発された.

  1. Rich APIs
    • Pinの既存APIをそのまま流用できること
  2. Cross-OS
    • WindowsとLinuxをサポート
  3. Strong Isolation
    • ゲストOSのRing 3やRing 0ではなく,out-of-VM(Ring -1)から監視(VM Introspection)を行うこと
  4. VM Introspection
    • ゲストOSのプロセスやカーネルのセマンティクス情報を取得するAPIを提供すること

これらを達成するため,PEMUはQEMUの動的バイナリ変換器Tiny Code Generator(TCG)に手を加えてPin APIのサポートを追加した.

Instrumentation Engine

さて,PEMUにおけるinstrumentationはどのようになっているのだろうか.
問題となるのは,QEMUとPinとの差異である.QEMUは命令とBB単位でしかinstrumentできず,PinにおけるTrace単位をサポートしていない.また,PEMUのベースとなっているQEMU 1.5.3では,BBの数が640までと制限されている.
そこで,PEMUではBBとTraceを対応付けるTRACE Constructorと,PinのAPIを通じてinstrumentするCode Injectorが導入された.

TRACE Constructor

TRACE Constructorは,複数のBBを組み合わせてTraceの単位に抽象化するものである.
まずXED2ライブラリを用いて,ゲストの実行前にQEMUでBBの先頭から条件分岐命令まで逆アセンブルを行う(/target-i386/PEMU/DISAS.c).スワップされたか,ロードされていない命令については,ページフォルトを通じて取得する.さらに,コードを挿入する命令の位置をglobal hooking point hash-table(HPHT)にキャッシュする(/target-i386/PEMU/hashTable.c).
ここにおけるアルゴリズムは以下のようになっている.

まず,Trace単位の開始アドレスであるPCの度にXED2を呼び出し,PCをTPCという保存領域に保存する.次に,全命令を逆アセンブルし,命令単位のinstrumentationが必要な場合はそのアドレスをHPHTに保存する.さらに,無条件分岐の度にBBとTraceを対応付け,BB単位のinstrumentationが必要な場合はそのアドレスをHPHTに保存する.そして,新しいBBを割り当て,次のTrace単位が始まるアドレスを探す.Trace単位のinstrumentationが必要な場合はそのアドレスをHPHTに保存する.そして,さらなるTraceを逆アセンブルしていくといった仕組みになる.

Code Injector

TraceとBBの対応が取れた後は,QEMUのTCGが提供するtcg_gen_helperを用いてHPHTを参照し,instrumentを行えばよい(/target-i386/PEMU/pemu_hook_helper.c).
IMGやSEC単位のinstrumentはmallocなどの引数を確認する(セマンティクス情報を復元する)ことで実現している.
全体像は以下のようになる.

Introspection Engine

PEMUでは,Page Global Directory(PGD)とCR3レジスタの対応を用いてプロセスの識別を行う(target-i386/PEMU/pemu_helper.h).ここでは,プロセス作成時に発生するCR3レジスタへのMOVを検出して利用する.終了したプロセスのCR3は再利用されるため,プロセスの終了を検出する必要があるが,これはexit syscallを監視することで実現できる.スレッドは,kernel stack pointerのマスクから識別する.
ゲストとのセマンティックギャップの解決にあたっては,一時的にゲストを停止してシステムコールを挿入する手法を用いる.PEMU_というprefixでsyscallに相当するAPIが提供され,getpid, gettimeofdayなどゲストOSの情報を取得するためのAPI 28種類と,open, fstat, lseekなどゲストOSを操作するためのAPI 15種を用いることができる.これらは都度レジスタコンテキストの退避と復元を伴って実行される.

評価

論文での性能評価では,32-bit Ubuntu 12.04(Linux kernel 3.0.0-31-generic-pae)をホストとし,Intel Core i7, メモリ8GBのマシンが用いられた.速度の評価には32-bit Ubuntu 11.04(Linux kernel 2.6.38-8-generic)がゲストとして用いられた.
その結果として,PEMUではQEMUよりも4.33倍,Pinよりも83.61倍のオーバーヘッドが生じることが明らかになっている.

また,eXaitを用いた検出テストのため,Windows XP 32bitがゲストとして用いられた.論文ではeXaitの17手法全てがPinを検出した一方で,いずれの手法もPEMUを検出することができなかった.

関連研究

PEMUがQEMUにPin APIのサポートを追加したのに対して,PinOSはXenにPin APIのサポートを追加した.だがPinOSのinstrumentationは貧弱であり,セマンティクス情報を取得するAPIが提供されていない.また,解析ルーチンがカーネルや解析対象からアクセスできるため,isolationとして不十分である.
また,PinOS以外にカーネルへのinstrumentationをサポートしているDRKは,LKMとして実装されているため,in-VMな手法に留まっている.

おわりに

こうして,Pin APIを用いてout-of-VMにマルウェアを解析するための道標が示された.
今後のPEMUを用いた研究に期待したい,と他人事のように言っている場合ではなく,そろそろ論文を書かなければ.

補足

PEMUは新しいといえどQEMU 1.5.3をベースとしており,やがてはQEMU 0.9.1をベースとしているTEMUのように技術的負債となるだろう.
そこで,PEMUをforkしQEMU 1.5.3からQEMU 2.2.1にアップデートを試みた.が,QEMU 2.2.1に至るまでcpu_single_env変数が廃止されたことと,謎のコンパイルエラーが発生することから頓挫中である.そのうち何とかする.

参考文献

耐解析機能の分類

Gabriel N. BarbosaRodrigo R. Brancoは,マルウェアの耐解析機能をAnti-Debugging, Anti-Disassembly, Obfuscation, Anti-VMの四種類に分類した.彼らはIntelのセキュリティ研究者であり,マルウェアに備わった耐解析機能の統計を過去二回に渡って発信してきた.
気になるのは,サンドボックスの実装にあたって問題となるAnti-VMだ.8,103,167もの検体を用いた調査によると,その内訳は以下のようになっている.

これらはいずれもVMware社製品を検出するための手法である.残念ながら他の仮想マシンモニタについての情報は掲載されていない

仮想マシンモニタの分類

以前にも少しばかり述べた,サンドボックスの透明性(transparency)に関する話を蒸し返そう.
サンドボックスにOut-of-the-boxなVMIを採用すれば,少なくともAnti-Debuggingについては無視できる.
では,サンドボックスにXenを用いれば,QEMUより検出されにくくなるだろうか.あるいは,その逆はどうだろうか.
多くの研究者が好き勝手なことを言っているが,仮想マシンモニタの実現手法をもってサンドボックスの透明性を語るのは些か性急だろう.なぜなら,サンドボックスの透明性はその設計ではなくマルウェアの実装によって左右されるからだ.Out-of-the-box VMIは設計の話だが,Anti-VMは実装の話だ(と思う).
ところで私は,仮想マシンモニタの実現手法を「プロセッサ拡張によるもの」と「バイナリ変換によるもの」に分類していたが,別の分類方法を見つけた.

命令セットアーキテクチャを基準に分類することで,より分かりやすくなっている.今後はこの図に準拠したい.
話を戻そう.
何が言いたいのかというと,in-the-wildなマルウェアに備わったAnti-VMの内訳が分からないことには,サンドボックスの透明性について云々することはできないということだ.

検体

誰もやっていないようなので,調べてみることにした.
やや恣意的(誤用?)な選択だが,今回はVirusShare.comからCitadelのバリアント479検体や,MandiantのAPT1レポートにて報告された中国軍総参謀部第三部第二局こと61398部隊と思しきグループ製の874検体を含む総計33,260検体を用いた.

Anti-VMの実態

YARAを用いてAnti-VMの実態を調べた.
ルールには公式のantidebug.yarを用いた.なお,このルールにはAnti-VMのみならずAnti-Debuggerについても記載されている.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# awk '{ print $1}' yara.log | sort | uniq -c | sort -r
20340 DebuggerPattern__RDTSC
18966 DebuggerPattern__CPUID
9292 DebuggerTiming__Ticks
7371 DebuggerPattern__SEH_Inits
6955 DebuggerException__UnhandledFilter
5715 DebuggerTiming__PerformanceCounter
4377 DebuggerPattern__SEH_Saves
2949 DebuggerCheck__API
2631 DebuggerCheck__QueryInfo
2182 SEH__vba
1315 DebuggerOutput__String
887 ThreadControl__Context
604 vmdetect
303 DebuggerException__SetConsoleCtrl
186 SEH__vectored
83 DebuggerHiding__Thread
80 DebuggerHiding__Active
52 DebuggerException__ConsoleCtrl
10 Check_Dlls
5 DebuggerCheck__RemoteAPI
2 DebuggerCheck__GlobalFlags
1 ▒▒DebuggerPattern__RDTSC
1 Debuggeattern__SEH_Inits
1 Check_VBox_VideoDrivers
1 Check_VBox_Description

マルウェアの開発者がよりgenericな手法を好むことは,RDTSC命令を用いる検体が61.15%, CPUID命令を用いる検体が57.02%含まれていることからも明らかだ.たった2バイトを基準に検出しているため,false positiveだらけなのだろうとはいえ.
一方でvmdetectとして計上された検体は全体の1.18%に過ぎない.これはVMware, Virtual PC, Xen, Virtual Boxに関する文字列が含まれる検体を抽出するルールであり,コードは以下のようになっている.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
rule vmdetect
{
meta:
author = "nex"
description = "Possibly employs anti-virtualization techniques"
strings:
// Binary tricks
$vmware = {56 4D 58 68}
$virtualpc = {0F 3F 07 0B}
$ssexy = {66 0F 70 ?? ?? 66 0F DB ?? ?? ?? ?? ?? 66 0F DB ?? ?? ?? ?? ?? 66 0F EF}
$vmcheckdll = {45 C7 00 01}
$redpill = {0F 01 0D 00 00 00 00 C3}
// Random strings
$vmware1 = "VMXh"
$vmware2 = "Ven_VMware_" nocase
$vmware3 = "Prod_VMware_Virtual_" nocase
$vmware4 = "hgfs.sys" nocase
$vmware5 = "mhgfs.sys" nocase
$vmware6 = "prleth.sys" nocase
$vmware7 = "prlfs.sys" nocase
$vmware8 = "prlmouse.sys" nocase
$vmware9 = "prlvideo.sys" nocase
$vmware10 = "prl_pv32.sys" nocase
$vmware11 = "vpc-s3.sys" nocase
$vmware12 = "vmsrvc.sys" nocase
$vmware13 = "vmx86.sys" nocase
$vmware14 = "vmnet.sys" nocase
$vmware15 = "vmicheartbeat" nocase
$vmware16 = "vmicvss" nocase
$vmware17 = "vmicshutdown" nocase
$vmware18 = "vmicexchange" nocase
$vmware19 = "vmdebug" nocase
$vmware20 = "vmmouse" nocase
$vmware21 = "vmtools" nocase
$vmware22 = "VMMEMCTL" nocase
$vmware23 = "vmx86" nocase
$vmware24 = "vmware" nocase
$virtualpc1 = "vpcbus" nocase
$virtualpc2 = "vpc-s3" nocase
$virtualpc3 = "vpcuhub" nocase
$virtualpc4 = "msvmmouf" nocase
$xen1 = "xenevtchn" nocase
$xen2 = "xennet" nocase
$xen3 = "xennet6" nocase
$xen4 = "xensvc" nocase
$xen5 = "xenvdb" nocase
$xen6 = "XenVMM" nocase
$virtualbox1 = "VBoxHook.dll" nocase
$virtualbox2 = "VBoxService" nocase
$virtualbox3 = "VBoxTray" nocase
$virtualbox4 = "VBoxMouse" nocase
$virtualbox5 = "VBoxGuest" nocase
$virtualbox6 = "VBoxSF" nocase
$virtualbox7 = "VBoxGuestAdditions" nocase
$virtualbox8 = "VBOX HARDDISK" nocase
// MAC addresses
$vmware_mac_1a = "00-05-69"
$vmware_mac_1b = "00:05:69"
$vmware_mac_1c = "000569"
$vmware_mac_2a = "00-50-56"
$vmware_mac_2b = "00:50:56"
$vmware_mac_2c = "005056"
$vmware_mac_3a = "00-0C-29" nocase
$vmware_mac_3b = "00:0C:29" nocase
$vmware_mac_3c = "000C29" nocase
$vmware_mac_4a = "00-1C-14" nocase
$vmware_mac_4b = "00:1C:14" nocase
$vmware_mac_4c = "001C14" nocase
$virtualbox_mac_1a = "08-00-27"
$virtualbox_mac_1b = "08:00:27"
$virtualbox_mac_1c = "080027"
condition:
any of them
}

見ての通りQEMUに関する記述が存在しない.
そこで,vmdetectに以下の様なコードを追加し,再度スキャンしてみた.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
rule vmdetect
{
~ 略 ~
strings:
$QEMU1 = "Bochs" nocase
$QEMU3 = "QEMU_AUDIO_DRV" nocase
$QEMU4 = "QEMU VVFAT" nocase
$QEMU5 = "QEMU ADB Mouse" nocase
$QEMU6 = "QEMU ADS7846-driven Touchscreen" nocase
$QEMU7 = "QEMU HARDDISK" nocase
$QEMU8 = "QEMU CD-ROM" nocase
$QEMU9 = "QEMU MICRODRIVE" nocase
$QEMU10 = "QEMU-MEMORY" nocase
$QEMU11 = "QEMU_BIOS" nocase
$QEMU12 = "QEMU PS/2 Mouse" nocase
$QEMU13 = "QEMU Sun Mouse" nocase
$QEMU14 = "QEMU TSC2102-driven Touchscreen" nocase
$QEMU15 = "QEMU USB Mouse" nocase
$QEMU16 = "QEMU USB Tablet" nocase
$QEMU17 = "QEMU_VERSION" nocase
$QEMU18 = "QEMU USB Keyboard" nocase
$QEMU19 = "QEMU USB Hub" nocase
$QEMU20 = "QEMU USB MSD" nocase
$QEMU21 = "QEMU PenPartner tablet" nocase
$QEMU22 = "QEMUware SVGA" nocase
condition:
any of them
}

結果は608検体.QEMUを検出するのは4検体だけという結果になった.
自分がマルウェアを開発する立場だったらこうした文字列をそのまま埋め込むことはしないし,このルールは表層的なものでしかなく,例えばQEMUの動的バイナリ変換のスケジューリングに着目したAnti-VMを検出することはできない.
とはいえ,おかげで現状を大雑把に把握することはできた.

パッカーの実態

ついでに,PEiDを用いてパッカーの利用状況を調べた.
UserDBにはBob / Team PEiD signaturesを用いた.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
# awk '{{for(i=3;i<NF;i++)printf("%s ",$i) }print($NF)}' peid.log | sort | uniq -c | sort -r
21159 Nothing found *
3065 Nothing found [Overlay] *
1412 Microsoft Visual Basic 5.0 / 6.0
916 Microsoft Visual C++ 8.0 DLL Method2
747 Microsoft Visual C++ 6.0
685 Microsoft Visual Basic 5.0 - 6.0
405 Borland Delphi 6.0 - 7.0 [Overlay]
351 UPX 0.89.6 - 1.02 / 1.05 - 2.90 -> Markus & Laszlo
333 Microsoft Visual C++ 7.0 [Overlay]
320 UPX 2.93 - 3.00 [LZMA] -> Markus Oberhumer, Laszlo Molnar & John Reiser [Overlay] *
239 Borland Delphi 6.0 - 7.0
201 Microsoft Visual C++ 6.0 [Overlay]
185 UPX 0.89.6 - 1.02 / 1.05 - 2.90 -> Markus & Laszlo [Overlay]
167 Nullsoft PiMP Stub [Nullsoft PiMP SFX] *
99 Microsoft Visual C++ 6.0 DLL
83 Microsoft Visual C++ 7.0 [Debug] [Overlay]
82 Nothing found [Debug] *
79 UPX 0.89.6 - 1.02 / 1.05 - 2.90 (Delphi) stub -> Markus & Laszlo [Overlay]
63 Microsoft Visual C++ 7.0
62 Microsoft Visual C# / Basic .NET
61 Microsoft Visual Basic 5.0 / 6.0 [Overlay]
42 Themida 1.8.x.x - 1.9.x.x -> Oreans Technologies
42 ASPack 2.12 -> Alexey Solodovnikov [Overlay]
38 ASProtect 1.2x - 1.3x [Registered] -> Alexey Solodovnikov
32 ASPack 2.12 -> Alexey Solodovnikov
31 PE Win32 DLL (0 EntryPoint)
31 Nothing found [RAR SFX] *
31 Microsoft Visual C++ 6.0 DLL [Overlay]
28 Borland C++
26 PECompact 2.x -> Jeremy Collake [Overlay]
24 Morphine 1.2 - 1.3 -> rootkit
23 MinGW GCC 3.x [Overlay] *
22 UPX 0.89.6 - 1.02 / 1.05 - 2.90 (Delphi) stub -> Markus & Laszlo
22 Microsoft Visual C# / Basic .NET [Overlay]
19 Nothing found [ZIP SFX] *
18 PECompact 2.x -> Jeremy Collake
18 Microsoft Visual C++ 7.0 [Debug]
15 Borland C++ [Overlay]
14 Borland Delphi 4.0 - 5.0
14 ASProtect 1.33 - 2.1 Registered -> Alexey Solodovnikov *
13 UPX 2.93 - 3.00 [LZMA] -> Markus Oberhumer, Laszlo Molnar & John Reiser *
13 Microsoft Visual C++ 6.0 [Debug]
13 Microsoft Visual C++ 5.0
12 UPX 0.80 - 1.24 DLL -> Markus & Laszlo
12 BobSoft Mini Delphi -> BoB / BobSoft [Overlay] *
11 Themida 1.8.x.x -> Oreans Technologies *
11 Microsoft Visual C++ Private Version 1
11 Borland Delphi 6.0
11 Borland Delphi 2.0 [Overlay]
10 BobSoft Mini Delphi -> BoB / BobSoft *
9 NsPack 2.9 -> North Star *
9 Microsoft Visual C++ 5.0 [Debug]
8 Microsoft Visual C++ 7.1 DLL
8 Microsoft Visual C++ 7.0 Method2
7 Xtreme-Protector v1.05 *
7 Microsoft Visual C++ 8.0 DLL Method2 [Overlay]
6 Xtreme-Protector v1.05 [Overlay] *
6 Microsoft Visual C++ 8.0 [Debug] [Debug]
6 ASProtect 1.2x - 1.3x [Registered] -> Alexey Solodovnikov [Overlay]
5 nSPack 3.7 -> North Star/Liu Xing Ping
5 Wise Installer Stub [Overlay] *
5 WinZip 32-bit SFX 8.x module
5 Thinstall 2.4x - 2.5x -> Jitit Software [Overlay] *
5 PureBasic 4.x -> Neil Hodgson *
5 PE Win32 DLL (0 EntryPoint) [Overlay]
5 Microsoft Visual C++ 7.1 DLL [Overlay]
4 tElock 0.98b1 -> tE!
4 Themida 1.8.x.x - 1.9.x.x -> Oreans Technologies [Overlay]
4 PureBasic 4.x -> Neil Hodgson [Overlay] *
4 PC-Guard 5.0 -> Blagoje Ceklic [Overlay]
4 MoleBox v2.0 *
4 Microsoft Visual C++ 8.0 [Debug]
4 Microsoft Visual C++ 5.0 [Overlay]
4 Borland Delphi 4.0 - 5.0 [Overlay]
4 Borland C++ 1999 [Overlay]
4 Borland C++ 1999
4 Armadillo 3.78 - 4.xx -> Silicon Realms Toolworks
4 Armadillo 1.xx - 2.xx -> Silicon Realms Toolworks
4 ASProtect 2.1x SKE -> Alexey Solodovnikov
3 nSPack 3.7 -> North Star/Liu Xing Ping [Overlay]
3 Upack 0.39 beta -> Dwing
3 UPX 0.80 - 1.24 DLL -> Markus & Laszlo [Overlay]
3 Themida 1.2.0.1 (compressed) -> Oreans Technologies [Overlay]
3 Themida 1.2.0.1 (compressed) -> Oreans Technologies
3 NsPack 3.4 -> North Star *
3 NakedPacker 1.0 - by BigBoote *
3 MinGW GCC 3.x [ZIP SFX] *
3 Microsoft Visual C++ [Overlay]
3 Microsoft Visual C++ 6.0 SPx Method 1
3 Microsoft Visual Basic 5.0 - 6.0 [Overlay]
3 Inno Setup Module Heuristic Mode [Overlay]
3 ASProtect 2.1x SKE -> Alexey Solodovnikov [Overlay]
2 tElock 0.90 -> tE! [Overlay]
2 Upack 0.28 - 0.399 (relocated image base) - Delphi, .NET, DLL -> Dwing
2 Upack 0.24 - 0.29 beta -> Dwing
2 UltraProtect 1.x -> RISCO Software Inc. [Overlay]
2 UltraProtect 1.x -> RISCO Software Inc.
2 UPX 0.89.6 - 1.02 / 1.05 - 2.90 -> Markus & Laszlo [RAR SFX]
2 SVKP 1.3x -> Pavol Cerven
2 PureBasic DLL -> Neil Hodgson *
2 PEtite 2.x [Level 0] -> Ian Luck
2 PE-Armor V0.7X -> hying *
2 PC-Guard 5.0 -> Blagoje Ceklic
2 Nullsoft Install System 2.1x [Nullsoft PiMP SFX]
2 NsPack 1.4 -> Liuxingping *
2 NTKrnl Security Suite -> NTKrnl Team
2 Morphine 1.4 - 2.7 -> Holy_Father & Ratter/29A
2 MinGW GCC 3.x *
2 Microsoft Visual C++ Private Version 1 [Overlay]
2 Microsoft Visual C++ DLL Method 1
2 Microsoft Visual C++ 7.0 Method2 [Debug] [Overlay]
2 Microsoft Visual C++ 7.0 DLL Method 3
2 Microsoft Visual C++
2 Microsoft Visual Basic 5.0 / 6.0 [Debug]
2 MASM32 / TASM32
2 EXECryptor 2.2.4 -> Strongbit/SoftComplete Development (h1) *
2 EXECryptor 1.x.x -> SoftComplete Developement
2 Borland Delphi DLL
2 Borland Delphi 6.0 [Overlay]
2 Borland Delphi 3.0
2 Borland C++ DLL Method 2 [Overlay]
1 yoda's Protector 1.3 -> Ashkbiz Danehkar
1 yoda's Protector 1.03.3 -> Ashkbiz Danehkar
1 yoda's Protector 1.03.2 -> Ashkbiz Danehkar
1 tElock v0.90 [Overlay] *
1 tElock v0.90 *
1 tElock 1.0 (private) -> tE!
1 tElock 0.61 -> tE!
1 nSpack V2.3 -> LiuXingPing *
1 eXPressor 1.3.0 -> CGSoftLabs
1 Winkript 1.0 -> Mr. Crimson/WKT
1 WinRAR 32-bit SFX Module [RAR SFX] *
1 WWPack32 1.x -> Piotr Warezak
1 Upack 0.28 - 0.399 (relocated image base) - Delphi, .NET, DLL -> Dwing [Overlay]
1 Upack 0.24 - 0.29 beta -> Dwing [Overlay]
1 UPX 1.03 - 1.04 -> Markus & Laszlo [Overlay]
1 UPX 1.03 - 1.04 -> Markus & Laszlo
1 UPX 0.89.6 - 1.02 / 1.05 - 2.90 -> Markus & Laszlo [Nullsoft PiMP SFX]
1 Themida 1.8.x.x -> Oreans Technologies [Overlay] *
1 Themida -> Oreans Technologies 2004 *
1 SafeDisc 2.05.030 -> Macrovision [Overlay]
1 RLPack V1.11 -> ap0x [Overlay] *
1 RLPack 1.20 Basic Edition [aPLib] -> Ap0x *
1 RCryptor v1.6d --> Vaska *
1 Pelles C 3.00, 4.00, 4.50 EXE (X86 CRT-LIB) [Overlay]
1 PKLITE32 v1.1 *
1 PEtite 2.x [Level 1/9] -> Ian Luck [Overlay]
1 PEtite 2.x [Level 1/9] -> Ian Luck
1 PEncrypt 4.0 Gamma / 4.0 Phi -> junkcode
1 PESpin 0.3x - 1.xx -> cyberbob
1 PECompact 2.xx --> BitSum Technologies [Overlay] *
1 PECompact 2.xx --> BitSum Technologies *
1 PECompact 1.40 - 1.45 -> Jeremy Collake [Overlay]
1 PEBundle 2.0x - 2.4x-> Jeremy Collake
1 PEBundle 0.2 - 3.x -> Jeremy Collake
1 PE Pack 1.0 -> ANAKiN [Overlay]
1 PE Pack 1.0 -> ANAKiN
1 PE Crypt 1.02 -> random, killa & acpizer
1 Obsidium 1.3.0.4 -> Obsidium Software [Overlay]
1 Nullsoft PiMP stub [Nullsoft PiMP SFX]
1 Nullsoft Install System 2.x [Nullsoft PiMP SFX]
1 NsPack 2.9 -> North Star [Overlay] *
1 Nothing found [CAB SFX] *
1 NSPack 3.x -> Liu Xing Ping *
1 Morphine 1.4 - 2.7 -> Holy_Father & Ratter/29A [Overlay]
1 MoleBox v2.0 [Overlay] *
1 Microsoft Visual C++ 8.0 [Debug] [Overlay]
1 Microsoft Visual C++ 7.0 Method2 [Overlay]
1 Microsoft Visual C++ 7.0 DLL Method 3 [Overlay]
1 Microsoft Visual C++ 6.0 [ZIP SFX]
1 Microsoft Visual C++ 6.0 SPx Method 1 [Overlay]
1 Microsoft Visual C++ 6.0 DLL [Debug]
1 Microsoft Visual C++ 4.x [Overlay]
1 Microsoft Visual C++ 4.x [Debug]
1 Microsoft Visual Basic 6.0 DLL [Overlay]
1 Microsoft CAB SFX module
1 LCC Win32 1.x -> Jacob Navia
1 KByS V0.28 -> shoooo [Overlay] *
1 KByS V0.22 -> shoooo *
1 InstallShield AFW [Overlay]
1 Install Stub 32-bit -> InstallShield [Overlay]
1 Inno Installer 4.0.5 [Inno SFX]
1 FSG 2.0 -> bart/xt
1 EZIP 1.0 -> Jonathan Clark [Overlay]
1 EXECryptor 1.x.x -> SoftComplete Developement [Overlay]
1 CD-Cops II -> Link Data Security [Overlay]
1 Borland Delphi DLL [Overlay]
1 Borland Delphi 5.0 KOL/MCK [Overlay]
1 Borland Delphi 5.0 KOL [Overlay]
1 Borland Delphi 3.0 [Overlay]
1 Borland Delphi 2.0 [Inno SFX]
1 Borland Delphi 2.0
1 Borland Component
1 Armadillo v1.71 [Overlay] *
1 Armadillo 3.78 - 4.xx -> Silicon Realms Toolworks [Overlay]
1 Armadillo 2.01 -> Silicon Realms Toolworks [Overlay]
1 ASProtect SKE 2.1x (exe) -> Alexey Solodovnikov [Overlay] *
1 ASProtect 2.0x Registered -> Alexey Solodovnikov [Overlay]
1 ASProtect 2.0x Registered -> Alexey Solodovnikov
1 ASProtect 1.33 - 2.1 Registered -> Alexey Solodovnikov [Overlay] *
1 ASProtect 1.2 / 1.2c-> Alexey Solodovnikov
1 ASPack 2.12b -> Alexey Solodovnikov
1 ASPack 1.08.03 -> Alexey Solodovnikov [Overlay]
1 ASPack 1.06b / 1.061b -> Alexey Solodovnikov [Overlay]
1 AHTeam EP Protector 0.3 (fake Microsoft Visual C++ 7.0) -> FEUERRADER *
1 ACProtect V2.0 -> risco *
1 ACProtect 2.00 - RISCO Software Inc. [Overlay]
1 ACProtect 1.40 - 1.41 - RISCO Software Inc.
1 .BJFNT 1.1b -> :MARQUiS:
1 * PseudoSigner 0.2 [Microsoft Visual Basic 5.0 - 6.0] --> Anorganix [Overlay] *
1 * PseudoSigner 0.2 [Microsoft Visual Basic 5.0 - 6.0] --> Anorganix *

ほとんど検出できていないのが残念.ひときわ面倒なThemidaは11検体だった.

サンドボックスの実態

サンドボックスの透明性が研究されている一方で,実際のマルウェアはあまり凝ったAnti-VMを備えていなかった.
一方で,現実のサンドボックスはどうだろうか.いくら論文で優れた手法が編み出されようが,役立てられていなければ意味がない.
そこで,一般に公開されているサンドボックスに対してシステム情報を取得するプログラムを挿入し,仮想マシンモニタの種類を判別できるかどうか試してみたのだが,実態はあまりにもお粗末だった.
詳細は言うまい.
だが,systeminfoコマンドとNtQuerySystemInformationだけで露見する代物ばかりだった.
マルウェアの解析ではなく検出を目的としたサービス,言ってしまえばVirusTotalをサンドボックスのフロントエンドとしたことが,この一因として挙げられる.
凝ったAnti-VMを備えたマルウェアがあまり多くないことも考えると,現状のサンドボックスで十分だということなのだろうか.

感想

研究をしているうちに袋小路に突入してしまい,サンドボックスの透明性やAnti Anti-VMあくまでマルウェア解析の一側面に過ぎないことを私は忘れかけていた.
本質は別のところにある.

TEQUILABOOMBOOM

公開されているサンドボックスの中にTEQUILABOOMBOOMというホスト名のものがあった.
Trojan.Win32.Inject.uljvBackdoor.Win32.Agent.dkbpなど一部のマルウェアは,このホスト名に基づいてサンドボックスを検出することが知られている.調べてみると,Question about keylogged PC - Printable Version[FUD][FREE] Agent Tesla [Keylogger] [ClipboardLogger] [On-Screen Keyboard Logger] - Page 16など,2013年の時点で情報が攻撃者の間で共有されている.

So I managed to crypt a keylogger and have my victim download it through dropbox (I think). I haven’t received any logs yet but it shows that I’m connected to a computer from france which is janettedoe@TEQUILABOOMBOOM. I’m not sure how this is possible, or if it’s some sort of error. Does anybody know what this is? My keylogger hasn’t been sent anywhere other than to my targets email.

どうやらこれはGoogleが動かしているCuckoo Sandboxが用いるホスト名らしく,攻撃者によるサンドボックスの実態調査が進んでいることを伺わせる.
サンドボックスの透明性について拘りすぎる必要はないにせよ,その情報が攻撃者の手に渡ることを前提としたサービスについては検討の余地がある.

参考文献

__attribute__((constructor))

main()が呼ばれる前に関数を呼ぶ方法として,一般にgcc拡張である__attribute__((constructor))が利用されている.
例えば次のコードでは,main()の前にconstructor()が呼び出される.

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
__attribute__((constructor)) void constructor(){
printf("constructor\n");
}
int main(){
printf("main\n");
return 0;
}

__libc_start_main()

gccが内部で呼び出すldは,glibcの初期化を行うためにcrt*.oを静的リンクする.これによって,_start()などのシンボルがバイナリに埋め込まれることになる.
ここで埋め込まれるシンボルには未定義なものが存在する.その一つとして,crt1.oに含まれる__libc_start_main()が挙げられる.
これはmain()より先んじて呼び出される関数であり,暗黙的にcrt1.oがリンクの対象となる.しかし未定義であるがゆえに,先にソースコードに__libc_start_main()が宣言されていた場合,ldはこちらをシンボルとして採用してしまう.

どちらが先か

上記を踏まえて,次のコードではどちらの関数が先に呼び出されるだろうか.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
__attribute__((constructor)) void constructor(){
printf("constructor\n");
}
int __libc_start_main(){
printf("__libc_start_main\n");
return 0;
}
int main(){
printf("main\n");
return 0;
}

正解は__libc_start_main()だ.私は実行するまで分からなかった.

__do_global_ctors_aux()

なぜ__libc_start_main()__attribute__((constructor))よりも先に呼び出されるのか.それは__attribute__((constructor))が,__do_global_ctors_aux()によって実現されているからだ.
__do_global_ctors_aux()_start(), __libc_start_main(), __libc_csu_init(), _init()を通ってようやく呼び出される.そのため,__libc_start_main()の方がより先んじて呼び出されるのだ.

__libc_csu_init()

Sigreturn-oriented Programmingでにわかに脚光を浴びている__libc_csu_init()__libc_start_main()の引数にセットされるコンストラクタだが,シンボルを上書きして__libc_start_main()の代わりにこちらをmain()ないし__attribute__((constructor))の先に呼び出すこともできる.
当初私は__attribute__((constructor))はこの辺りから呼び出されるものだと誤解していた.

Windowsでは

今回は触れないが,.CRT$X??のセクションやThread Local Storageのコールバックによって同様の機能を実現できる.

参考文献

はじめに

LibVMIはXenならびにKVMを対象としたVMIライブラリである.ここでは,その基盤と応用についてまとめる.

libxc

そもそもXenにおけるVMIの研究ではかねてより,各ドメインの制御機能ないしハイパーコールを提供するlibxcというライブラリが用いられてきた.libxcという名称はthe Xen Control libraryの略称であり,Xenによって公式に提供されている(tools/libxc/).なお,libxcとして提供されている関数はxc_というprefixを備えている.
このうち,VMIと密接に関連があるのはxenctrl.hxenguest.hである.なかでもxenctrl.hはlibxenctrlとも呼ばれ,PFNの参照やハイパーコールの追加などの機能が提供されている.
LibVMIのソースコードにgrepを掛けると,libxcに由来する構造体と関数が多く用いられていることが分かる.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
% grep -rhoe "xc_[a-zA-z]*_[a-zA-z]*" . | grep -v "]" | sort | uniq
xc_domain_debug_control
xc_domain_decrease_reservation_exact
xc_domain_getinfo
xc_domain_getinfolist
xc_domain_hvm_getcontext
xc_domain_hvm_getcontext_partial
xc_domain_hvm_setcontext
xc_domain_maximum_gpfn
xc_domain_pause
xc_domain_populate_physmap_exact
xc_domain_set_access_required
xc_domain_unpause
xc_domaininfo_t
xc_dominfo_t
xc_evtchn_bind_interdomain
xc_evtchn_close
xc_evtchn_fd
xc_evtchn_notify
xc_evtchn_open
xc_evtchn_pending
xc_evtchn_t
xc_evtchn_unbind
xc_evtchn_unmask
xc_get_hvm_param
xc_hvm_get_mem_access
xc_hvm_inject_trap
xc_hvm_set_mem_access
xc_interface_close
xc_interface_open
xc_map_foreign_batch
xc_map_foreign_range
xc_mem_access_disable
xc_mem_access_enable
xc_mem_access_resume
xc_mem_event_disable
xc_mem_event_enable
xc_memory_op
xc_set_hvm_param
xc_set_mem_access
xc_vcpu_getcontext
xc_vcpu_setcontext

とりわけVMIにおいて重要なのはxc_map_foreign_range()という関数である.

1
2
3
void *xc_map_foreign_ranges(xc_interface *xch, uint32_t dom,
size_t size, int prot, size_t chunksize,
privcmd_mmap_entry_t entries[], int nentries);

Xenの各ドメインにはドメインや仮想CPUの構造体(xen/include/xen/sched.h)のほか,マシンメモリ(物理メモリ)と擬似物理メモリが割り当てられる.ドメインにおけるページングは,このマシンフレーム番号(MFN)と疑似物理フレーム番号(PFN)とを対応付けるP2Mテーブルによって実現されている.つまり,ドメインの仮想アドレスは擬似物理メモリフレーム番号に対応し,その疑似物理フレーム番号はさらにマシンフレーム番号に対応する.なお,Intel EPTが有効な環境においてはP2MはEPTによって代替される(ept-p2m.c).
xc_map_foreign_range()はこうした仕組みに基づき,ページテーブルを辿ってDom0のメモリ空間にDomUのマシンフレームを割り当てる.
VMIに求められるデータ構造の解釈はXenにおいて,この関数があることから成り立つものである.また,これによるフォールトインジェクションに関してもかつて研究が行われていたようだ.

libxenstore

Xenではxenstore-lsやxenstore-readなどのコマンドからxenstoredというデーモンを経由して各ドメインの情報を読み出すことができる.この仕組みをXenStoreといい,関連してlibxenstoreというライブラリが提供されている.(tools/xenstore/).libxsと呼ばれることもある.
XenStoreは名の通りKVSとしてドメインのUUIDやドメイン名,メモリのコミット状況などを格納している.
LibVMIにおいても,xenstordへのアクセスのため用いられている(xen_private.h).

libxenlight

libxenlightはlibxcとlibxenstoreのラッパとして公式に開発されているライブラリである(tools/libxl/).
LibVMIでは用いられていないものの,Xen Projectの本流ではこちらが推進されているように見受けられる.

XenAccess

XenAccessはlibxcのラッパであり,LibVMIの前身にあたる.
目的としてセマンティックギャップの解決が掲げられており,任意のPIDに対応するEPROCESS構造体を取得することができる.
EPROCESS構造体はWindowsの各プロセスに割り当てられる構造体であり,Windowsのプロセスリストはこの構造体のActiveProcessLinksメンバからなる双方向リンクリストによって成り立っている.タスクマネージャが参照するのはこの構造体である.
XenAccessはこの構造体を手掛かりにカーネルのベースアドレスを取得する.
まず,メモリ空間をPEヘッダの先頭すなわちMZという文字列で検索する.そして,発見したアドレスについてシステムプロセス固有のPsInitialSystemProcessのオフセットを加算し,EPROCESS構造体の先頭メンバである_EPROCESS.Pcb.headerにあたるシグネチャと比較する.一致した場合,発見したアドレスがベースアドレスということになる(windows_memory.c, windows_core.c, windows_process.c).
これによって,プロセスリストと任意のプロセスの情報を取得することができるようになった.

LibVMI

LibVMIはXenAccessの後継であり,かつてvmitoolsとも呼ばれていた.
先述したようなカーネル空間の解析機能をLibVMIはフォレンジックツールに委譲した.フォレンジックツールを導入するメリットとして,より詳細な構造体の解析を低い実装コストで実現できるという点が挙げられる.
さて,LibVMIはVolatilityRekallという二種類のフォレンジックツールに対応している.RekallはGoogleによるVolatilityの拡張であり,かつてはLibVMIのサポート外だったものの,commit a4f065a31b986562dc39c9a10d9dff080792f3f4にて導入された.
では,両者の差異はどこにあるのか.

Kernel Debugger Block

XenAccessが行っていたようなEPROCESS構造体のスキャンについて考えてみよう.
VolatilityとRekallはいずれも,まずリンクリストの先頭であるPsActiveProcessHeadを特定しようとする.
x86において,このPsActiveProcessHeadは,KPCR(Kernel Processor Control Region)構造体のKdVersionBlockメンバが示すKDDEBUGGER_DATA32構造体から取得することができる.
このKPCR構造体だが,x86ではFSレジスタ,x86_64ではGSレジスタが先頭アドレスを保持しており,Windows XPでは0xFFDFF000というマジックナンバーを用いて検索することができる.
では,KdVersionBlockはどうか.なんと,Windows 7の64-bit版からこのメンバは空になってしまっている.ゆえに,x86_64ではKDDEBUGGER_DATA64構造体をKdVersionBlockに頼らず検索する必要が生ずる.
ここで,VolatilityはKDDEBUGGER_DATA64構造体のヘッダに存在するKDBGシグネチャをスキャンするのに対し(kdbgscan.py),Rekallはカーネルのバージョンに対応したプロファイル情報を用いて構造体にアクセスする.

DRAKVUF

DRAKVUFはTamas K Lengyelによって開発されたサンドボックスであり,論文のオーサーにはXenAccessとLibVMIの開発者であるBryan D. Payneも名を連ねている.
LibVMIにRekallのサポートを追加したのがほかでもないTamas K Lengyelその人である.
Volatilityは前述の通りメモリからKDBG構造体を検索する.だが,ゲストOSのメモリはマルウェアによって改竄されてしまうおそれがあることに加え,Windows 8の64-bit版ではKDBG構造体が難読化されているという問題点がある.
一方で,Rekallのプロファイルはゲストから不可視であり,改竄の影響を受けないため,この問題を解決することができる.
DRAKVUFはマルウェア解析を目的としてLibVMIを拡張しており,当然プログラムの実行をトラップする機能を備えている.
特筆すべきは,Intel EPTを用いたメモリアクセスの監視である(vmi.c).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// Now we can set the EPT permissions
if (!container->guard) {
container->guard = g_malloc0(sizeof(vmi_event_t));
SETUP_MEM_EVENT(container->guard, container->pa, VMI_MEMEVENT_PAGE,
VMI_MEMACCESS_RW, trap_guard);
if (VMI_FAILURE == vmi_register_event(vmi, container->guard)) {
printf("*** FAILED TO REGISTER MEMORY GUARD @ PAGE %lu ***\n",
pa >> 12);
free(container->guard);
free(container);
continue;
}
container->guard->data = g_hash_table_new(g_int64_hash,
g_int64_equal);
printf("\t\tNew memory event trap set on page %lu\n", pa >> 12);
} else {
printf("\t\tMemory event trap already set on page %lu\n", pa >> 12);
}
struct memevent *test = g_hash_table_lookup(container->guard->data,
&container->pa);
if (!test) {
g_hash_table_insert(container->guard->data, &container->pa,
container);
} else if (test->sID == SYMBOLWRAP) {
printf("Address is already guarded\n");
} else {
printf("Address is trapped by another feature! ERROR/TODO!\n");
}

このアイデア自体はKVMをベースとして開発されたCXPInspectorを下敷きにしている.
かつては任意のアドレスについてNX bitを設定し,ページフォルトハンドラをブレークポイントとする手法が一般的であった.これがカーネル空間から可視であるのに対し,EPTは不可視であるためよりカーネルルートキットの解析に適している.

おわりに

LibVMIの登場と洗練によってもはや,セマンティックギャップは過去の問題になりつつある.XenにおけるVMIにおいて,これを用いない手はないだろう.

参考文献

VMI

仮想マシンモニタからゲストのリソースを監視・制御する技術をVMI(Virtual Machine Introspection)という.
その発祥は2003年に遡る.当時はハニーポットの研究が盛んな時期であり,VMIは仮想マシンにおける侵入検知を実現する手法として提案された.これはマルウェア解析の自動化に応用されており,仮想マシンモニタによるサンドボックスを構築し,VMIによってマルウェアの挙動を監視するというアプローチが一般的となっている.
さて,VMIにおける問題点にセマンティックギャップというものがある.原義は高級言語とハードウェアとの乖離であるが,OSと同じ粒度で情報を取得することのできるゲスト内部(in-the-box)とハードウェアの粒度で情報を取得することしかできないゲスト外部(out-of-the-box)の乖離という意味でも援用される.

Transparent Sandbox

セマンティックギャップを解決する方法として,ゲスト内部にエージェントを挿入し,仮想マシンモニタに情報を送信するというアプローチが考えられる.
だがマルウェア解析において,ゲスト内部にエージェントを挿入することは,自らの姿を曝け出していることにほかならない.ファイルやサービス名,フックによって変化するAPIの挙動といったものは,マルウェアがエージェントと同じ権限で動作する限り避けられない.そもそも仮想マシンモニタは特権を有していたところでRDTSCICEBPなどの命令,あるいはハードウェア名やIDTといった要素から検出される可能性を孕んでいる.そのため,ゲスト内部にエージェントを挿入することは,仮想マシンモニタがマルウェアから検出される可能性を高めることに繋がる.フォレンジックにおける完全性(integrity)のためにも,ゲスト内部にエージェントを挿入すべきではないと私は考える.
この議論に関して,マルウェアから検出されない仮想マシンモニタをtransparent sandbox(透明なサンドボックス?)といい,仮想マシンモニタの検出を試みるマルウェアをevasive malwareという.
ではどのような仮想マシンモニタがより「透明」に近いのか.

仮想マシンモニタの歴史

ここでPopekとGoldbergの仮想化要件に立ち返ると,ベアメタルな仮想マシンモニタは等価性(equivalence),資源管理(resource control),効率性(efficiency)の三原則を実現しており,なおかつユーザセンシティブ命令が特権命令でなければならない.センシティブ命令かつ特権命令でなければ,例外によって仮想マシンモニタに通知することができないためである.
だが,以前のx86アーキテクチャはこの要件を満たしていなかった.そこで,ニ種類の解決方法が編み出された.
一つ目はバイナリ変換(binary translation)による完全仮想化である.これはソフトウェアで命令をエミュレートする手法で,QEMUやVMwareが該当する.QEMUは全ての命令を中間コードに変換するのに対し,VMwareはPopekとGoldbergの仮想化要件を満たさない命令をライブラリ呼び出しに変換する.
二つ目は準仮想化である.これはPopekとGoldbergの仮想化要件を満たさない命令に差し掛かると仮想マシンモニタに通知されるようゲストOSのカーネルを修正する手法で,Xenが該当する.
ではバイナリ変換による完全仮想化とカーネル書き換えによる準仮想化のどちらがより「透明」に近いのか.
その前にIntel VT-xについて考えなければならない.Intel VT-xはCPUのモード切り替えによってセンシティブ命令かつ特権命令でない命令のトラップを実現する拡張機能である.すなわちPopekとGoldbergの仮想化要件を満たすハードウェア側からのアプローチである.これにより準仮想化カーネルの必要はなくなった.エージェントの挿入が「透明」性に関わるのと同様,カーネルの書き換えは仮想マシンモニタを検出する手掛かりとなる.
そのためマルウェア解析においては,バイナリ変換による完全仮想化か,Intel VT-xによる完全仮想化が前提となる.なおKVMはIntel Vt-xを前提としているが,部分的にQEMUを用いている.
ではどちらの手法がより「透明」に近いのか.

バイナリ変換とIntel VT-x

Christopher KruegelはWineのようなシステムコール単位のエミュレーションあるいはフックは情報量として不十分であり,またIntel VT-xによる完全仮想化はRedPillのようなアプローチで検出可能であることから,バイナリ変換による完全仮想化すなわちQEMUを支持している.
システムコール単位の監視はIsDebuggerPresentのようなカーネルモードに遷移せず構造体を参照するだけの処理を見逃してしまう.システムコールのフックを用いた研究や製品は多数存在するが,それらは不完全である.この点で私は氏と意見を一にするが,バイナリ変換がより「透明」に近いかどうかは検討の余地があるだろう.寧ろ氏は実行パスの拡張がより容易に行えるがゆえにバイナリ変換を支持しているのだろうということは読み取れるが,氏がチーフサイエンティストを務めるLastlineの製品がQEMUをベースとしていることもあり,些かポジショントークのようにも感じられる.
このような論調でバイナリ変換とIntel VT-xを二項対立の図式に落とし込んでいる論文が数多く見られる.しかし,どちらがより「透明」に近いのかという議論はナンセンスであろう.
ゲスト内部にエージェントを挿入することで,果たして本当にマルウェアから検出されやすくなるのかと言うと,それは飽くまで可能性でしかない.だがゲスト内部にエージェントを挿入しないという選択で,その問題は検討項目から外すことができる.一方で,バイナリ変換であるという理由でIntel VT-xを用いた仮想マシンモニタよりも検出されにくいといったことはない.逆もまた然りである.
そしてそもそも,ネットワーク経由で仮想マシンモニタを検出するという手法がある以上,完全に「透明」なサンドボックスは実現不可能であるとされる.
ゆえにそれぞれのアプローチについて「透明」性の他にもメリット・デメリットを検討し,なおかつ両者を接合するようなシステムこそが望ましい.

参考文献

はじめに

2014.12.07~08にわたって開催されたSECCON CTF 2014の予選(英語版)にて,ROP: Impossibleというpwn問題が出題された.タイトルの通り,この問題ではROPが制限されている.
ここでは,その実現手法として用いられていたIntel Pin(pintool)について,またこの問題にあった欠陥について述べる.
したがって,このエントリはROP: Impossibleのネタバレを兼ねている.注意されたし.

問題の概要

問題文は以下の通り.Pinによって保護された脆弱なバイナリからフラグを読み出せというものだ.

ropi.pwn.seccon.jp:10000
read /flag and write the content to stdout, such as the following pseudo code.
open("/flag", 0);
read(3, buf, 32);
write(1, buf, 32);
Notice that the vuln executable is protected by an Intel Pin tool, the source code of which is norop.cpp.

パッと見,バイナリは良心的な構成であるように思われる.

1
vuln: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, for GNU/Linux 2.6.26, BuildID[sha1]=0xcb671b1dc0409082c3f3962818d366fcb8771ead, not stripped

有効になっているのはNXだけだ.

1
2
RELRO STACK CANARY NX PIE RPATH RUNPATH FILE
No RELRO No canary found NX enabled No PIE No RPATH No RUNPATH vuln

だからといって,解法が自明であるわけではない.この問題の肝は,いかにしてPinによる保護をかい潜るかにある.

Pin

Pinは,Intelによって開発されたDBI(dynamic binary instrumentation)フレームワークである.
DBIとは,プログラムにコードを挿入することで実行時の情報を取得・操作する技術であり,プログラムのパフォーマンス測定やエラー検出,CPUキャッシュの分析や未定義命令のエミュレーションなど,多方面で応用されている.
さて,ROP: Impossibleは以下のコードによって保護されている.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
#include <stdlib.h>
#include "pin.H"
ADDRINT shadow_stack[4096];
int shadow_sp = -1;
VOID push_retaddr(ADDRINT esp, ADDRINT eip)
{
if(shadow_sp >= (int)sizeof(shadow_stack) - 1){
// cannot push retaddr to shadow stack
exit(-1);
}
PIN_SafeCopy(&shadow_stack[++shadow_sp], (VOID*)esp, sizeof(ADDRINT));
}
VOID pop_retaddr(ADDRINT esp, ADDRINT eip)
{
ADDRINT retaddr;
PIN_SafeCopy(&retaddr, (VOID*)esp, sizeof(ADDRINT));
while(shadow_sp >= 0 && shadow_stack[shadow_sp--] != retaddr);
if(shadow_sp < 0){
exit(-1);
}
}
VOID check_syscall(ADDRINT eax)
{
switch(eax){
// syscalls for exploit
case 3: // sys_read
case 4: // sys_write
case 5: // sys_open
case 6: // sys_close
// syscalls executed until entry point
case 45: // sys_brk
case 122: // sys_newuname
case 192: // sys_mmap2
case 197: // sys_fstatfs64
case 243: // sys_set_thread_area
break;
// invalid syscalls
default:
exit(-1);
}
}
VOID insert_hooks(INS ins, VOID *val)
{
if(INS_IsCall(ins)){
// push retaddr to shadow stack
if(XED_ICLASS_CALL_FAR == INS_Opcode(ins)){
exit(-1);
}
INS_InsertCall(ins, IPOINT_TAKEN_BRANCH,(AFUNPTR)push_retaddr,
IARG_REG_VALUE, REG_ESP, IARG_INST_PTR, IARG_END);
}else if(INS_IsRet(ins)){
// pop retaddr from shadow stack, and then check it
if(XED_ICLASS_RET_FAR == INS_Opcode(ins)){
exit(-1);
}else{
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)pop_retaddr,
IARG_REG_VALUE, REG_ESP, IARG_INST_PTR, IARG_END);
}
}else if(INS_IsSyscall(ins)){
// check syscall
INS_InsertCall(ins, IPOINT_BEFORE, (AFUNPTR)check_syscall,
IARG_REG_VALUE, REG_EAX, IARG_END);
}
}
int main(int argc, char *argv[])
{
PIN_Init(argc, argv);
INS_AddInstrumentFunction(insert_hooks, NULL);
PIN_StartProgram();
return 0;
}

このプログラムが,vulnの共有ライブラリとして噛まされている.INS_*は命令単位のinstumentationのために提供されているPinのAPIである.
要するに,リターンアドレスの検証によって,ROPが制限されているのだ.

JOP

ROPができない環境ならば,どうすればよいのか.
そもそも,ROPに用いられるretは,スタックの最上位アドレスに対するpopjmpと等価であると見做せる.ゆえにjmpによってchainを構築すれば,retを用いること無くROPと同様なコードを作成することができる.これをJOP(Jump-oriented programming)という.
さらに,スタックに対するアドレスのpushjmpは,callと等価であると見做せる.ゆえにcallpopによってROPを代替することができる.
つまり,ROP: Impossibleはret制限下の環境を前提にjmpcallでchainを構築しろというストイックな問題だった.
Pinに起因する欠陥がなければ.

writeup

この問題を最初に解いたのは,TOEFL BEGINNERという謎のチームだった.続いてbinja, PPPと続いている.
優勝チームであるPPPメンバーのRicky Zhouが公開しているwriteupを見てみよう.

明らかにおかしい.bssセグメントにシェルコードを置いて実行しているだけではないか.
だが,このバイナリはNXが有効だったはずだ.ローカルでこのコードを実行しても,SIGSEGVが発生する.
これは一体どうしたことだろう.

JITコンパイルの弊害

結論から言うと,Pinの制御下にあるバイナリのNXは無効化されるようになっている.
/proc/pid/mapsを確認すると,一見nonexecであるように見える.

1
bf984000-bf9a5000 rw-p 00000000 00:00 0 [stack]

だが,実際はそうではない.
PinはJITコンパイルによってinstrumentationを実現している.そのため,バイナリはPinによってmmapされ,execされる.このとき,Pinは元来バイナリに付与されていた実行権限を無視してしまう.
つまり,ROPを制限するためのPinが,NXを無効化してしまっていたのだ.
そもそも,LinuxにおいてNXの状態をmaps以外から取得するのは難しい.強いて挙げるならば,checksecのようにreadelf -W -l file | grep 'GNU_STACK'を叩くといったところだろうか.だが,これだけではmmapやmprotectに追随することができない.WindowsのVirtualQueryに相当する機能はないのだろうか.

おわりに

XSS Bonsaiも同様だが,CTFについてもテストは重要であるということを気付かされた.
運営の穴を突くのもCTFの醍醐味のひとつなのだろうが,しかし厄介な問題である.
最近はPinやDynamoRioによるマルウェア解析が流行っているように見受けられるが,やがてはDBIツールのデメリットについても検討を加えなければならなくなるだろう.

はじめに

V2E: Combining Hardware Virtualization and Software Emulation for Transparent and Extensible Malware Analysis[PDF]“という論文を通して,Record and Replayを用いた解析環境の設計を学ぶ.

著者

この論文のラストオーサーであるYinはSyracuse Universityの助教である.彼はテイント解析の第一人者として知られ,現在はDECAFの開発を主導している.またBitBlaze ProjectのDawn Songや,マルウェア解析の大御所であるChristopher Kruegelと過去に共著を出している.
ファーストオーサーのYanは提案手法をAndroidに適用し,2013年に博士論文を著している.Android向けの拡張にあたっては,DECAFのサブプロジェクトであるDroidScopeを用いているようだ.

Record and Replay

あるいはLogging and Replay, Lockstep, 順序再演法,最小情報トレースなどと呼ばれるこれらは,仮想マシンモニタ上のイベントを記録し,再生するための技術である.乱暴に言うとhistoryからdockerfileやvagrantfileを作成し,deployするようなものだ.
一般にステートマシンにおける命令の出力は,内部状態から一意に与えられる.そこで,ある環境の初期状態と入力のみを記録(Record)し,同じ環境を別の環境の上に再生(Raplay)するといった試みがなされてきた.実マシンにおいては,時刻や割り込みなどの非決定性とその記録の困難性からRecord and Replayは不可能であるとされる.だが,仮想マシンモニタの世界ではこれらの問題をある程度無視できる.実際,VMware Workstationなどにこの機能は実装されている.
本論文では,マルウェア解析に求められるサンドボックスの特性を加味したRecord and Replayについて論じられている.かつて大居さん(@a4lg)が研究されていた内容に近い.

背景

マルウェアを解析するためのサンドボックスは検出不可能(transparent)であり,なおかつ拡張可能であるべきだ.
いかに仮想マシンモニタの存在を隠蔽しようと,Timing Attackなどが成り立つ以上,transparentなサンドボックスを実現することは不可能である.しかし,Intel VTなどハードウェア仮想化技術を用いることで,ほぼtransparentな環境を作り出すことは可能である.例えば,Etherは命令・システムコール単位の解析機能を備えているが,Xenをベースに高いtransparent性を実現している.
だが,ハードウェア仮想化技術に依存した解析環境には拡張性の点で問題がある.特に,instrumentationによる詳細な解析が不可能であるという点は,大きな痛手である.
他方,QEMUなどのソフトウェアエミュレーションは動的バイナリ変換を用いることでDBI(dynamic binary instrumentation)と解析効率の向上を実現している.しかし,transparentなサンドボックスには至れない.
QEMUの動的バイナリ変換はバイナリをコードブロック単位で変換し,softmmuからTLBを参照してアドレス変換を行うというものだ.
そしてその性質から,以下のような自己書き換えコードによって検出されうる.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
__asm{
xor eax, eax
mov edi, target
mov esi, copy_insn
mov ecx, 3
target:
rep movsw // f3 66 a5
jmp qemu_found // eb XX
nop // 90
non_qemu:
...
qemu_found:
...
copy_insn:
_emit 0xeb // eb
_emit 0x01 // 01
_emit 0x90 // 90
_emit 0xeb
_emit 0x01
_emit 0x90
}

実マシンでは,ecx*word=6バイトが1回で書き換わるが,QEMUの動的バイナリ変換では2バイトずつ3回で書き換わる.そのため,1回目の書き換えで0xeb 0x01 0xa5すなわちjmp +01になってしまい,qemu_foundにジャンプしてしまう.
その他には,ページ境界を越えてブロックの変換が行われた場合にページフォルトが発生してしまうことも考えられるし,ブロック境界でのみ割り込みが行われる,CPUサイクルの消費が著しいといった実マシンとの相違点もある.
さらに問題なのが,フラグの遅延評価だ.例えば,cmpjzの組み合わせなどの条件分岐は,EFLAGSを更新する.だが,QEMUにおいてはcmpが実行される段階でEFLAGSの計算は行われない.実際に計算されるのはjzの実行時,それも分岐を決定するためのZFのみが計算される.この設計はエミュレーションの高速化に寄与しているが,もちろん検出に用いることが可能だ.
当然ながら命令のエミュレーション自体にも限界がある.SIMDさえ厳しいのだ,システム管理モードやIntel TXTなんてものは考えたくないだろう.
このように,QEMUによるエミュレーションが検出される余地は枚挙に暇がない.
なお,解析環境検出をテーマとした最近の研究では,第2回システム系論文輪読会で紹介したBareCloudや忠鉢さん(@yuzuhara)のTENTACLEなどがある.

研究目的

ハードウェア仮想化技術を用いる解析環境とソフトウェアエミュレーションを用いる解析環境にはそれぞれ問題がある.そこで,ハードウェア仮想化技術を用いる解析環境でRecordを行い,ソフトウェアエミュレーションを用いる解析環境でReplayを行うことで,検出不可能性と拡張可能性という二つの目的について達成したのが,今回紹介するV2Eである.

形式的定義

本論文におけるRecord and Replayの設計はどのようなものか.
Recorderにおける遷移関数fについて,毎時iにおけるプログラムの状態をSiとし,入力をIiとする.すなわちfSi= f(Si-1, Ii-1)と表される.
次にReplayerにおける遷移関数f'についてプログラムの初期状態をS0とし,全ての入力をIとしたとき,f = f'と言えないだろうか.
これは二つのチューリング機械の同値性が解決可能であるかという問題に相当する.だがEQTM= {(M1, M2)|M1とM2はTMであり,L(M1) = L(M2)}は判定不可能とされ(『計算理論の基礎』における定理5.4),実装上でもハードウェアの割り込みなどの要因からf != f'となってしまう.
そこでV2Eは,プログラムの初期状態と全ての入力を保存することに加え,Sj= S'jとなるようなjについて状態の変化を保存することにした.これはj= Sj- Sj-1と表される.
ここで,新たな遷移関数f'rS'i= f'r(S'i-1, Ii-1, ⊿i)として定義する.これは,i!= nullのときS'i-1+ ⊿iと同値であり,それ以外についてf'(Si-1, Ii-1)と同値をとる.
なお,S0, I, およびf'r, S'i= Sii ∈ [0, n]について恒真である.
これを実装に起こすと,特定の命令やイベントが正しくエミュレート可能な場合は単にそれらをエミュレートし,そうでなければ状態の変化を記録し,Replayにあたって変更を適用するというアプローチになる.

理論と実際

プログラムの大部分を占めるmov, push, popなどのデータ転送命令,call, ret, jz, jmpなどの制御転送命令,add, shlなどの整数演算命令におけるエミュレーションは失敗しないものと見做せる.これらはそのままRecorderでエミュレートされる.
一方で,割り込み,MMIO, Port IO, DMA, TSCについては,V2Eは既存研究を踏襲し,監視領域においてのみこれらをRecordするようになっている.
では,例外,モデル固有レジスタ,cpuidはどうすべきだろうか.既存手法は,これらのエミュレーションは困難であるという理由から,そもそも入力として扱わない戦略を採っていたようだ.V2Eではこれらを, すなわちエミュレートが困難な状態の変化としてRecordすることで,Replayの正確性を高めている.
次に問題となるのが浮動小数点演算とSIMDの扱いだ.MMXやSSEを正確にエミュレートするのは難しい.だが,として命令の結果を記録する設計は大幅なパフォーマンスの低下を招く.そこでV2EはReplayにあたってこれらの命令をパススルーする.もちろん,ReplayerはSIMDをサポートしているマシンで実行されることが前提にある.

Transparent Recorder

RecorderはKVMを用いて実装されている.
V2EはRecord対象の領域とそれ以外のシステムを分割するため,TDP(two dimensional paging)を用いている.何のことかと思ったら,Intel EPTやAMD NPTの総称らしい.

要するに,TDPとは仮想マシンと物理マシン間のページテーブルのことだ.通常のページングでは,メモリアクセスに応じてMMUによってページテーブルが参照され,仮想アドレスが物理アドレスへと変換される.TDPでは,ゲストマシンからの仮想メモリ空間へのアクセスに応じてCR3にセットされたページテーブルが参照され,ゲスト物理アドレスがホスト物理アドレスへと変換される.
この仕組を用いたV2Eは,監視対象用のTDPテーブルと,それ以外用のTDPテーブルを別々に作成する.マルウェアに属するページはCR3の監視に基づき監視対象用のTDPテーブルに書き込まれる.マルウェアとそれ以外の部分のインタラクションはTDPページフォルトやVMExitによって媒介される.共有されるデータは読み取り専用として双方に与えられる.なお,TDPページフォルトに応じてCPUの状態が入力Iiとして保存される.

Precise Replayer

ReplayerはTEMUを用いて実装されている.バイナリ変換器に中間表現がなく,単純に古いQEMU 0.9.1をベースとしているのが玉に瑕だが,TEMUは動的テイント解析に求められる機能をほぼ網羅している.
TEMUはプラグインを共有ライブラリとしてロードし,コールバック関数からテイント解析の機能を呼び出すプラットフォームとなっている.V2EのReplayerは既存のプラグインであるtracecap, unpackerを用いる.だがRecordされたログには監視対象の情報のみが記述されているため,
return ((*TEMU_cpu_hflags & HF_CPL_MASK) != 3)といった,現在実行しているコードが解析対象のものかどうか判定するコードは除去されているようだ.TEMUのプラグインについては,こうした僅かな変更しか施されていない.
一方でその下で動くQEMUには結構手が加えられている.フラグの遅延評価は廃止され,ページフォルト以外の例外は除去されており,SIMDについては独自のヘルパー関数が追加されたようだ.QEMUのdyngenに手を加えるのはなかなか骨の折れる作業だと思う.
さて,Replayを行うためには,ReplayerはRecorderと同様のページングの仕組みをエミュレーションによって再現しなければならない.そこで,V2Eは物理ページコンテナという仕組みを用いている.これは,物理ページがログからロードされていることを示すものである.通常,物理ページコンテナは監視対象用のTDPテーブルを複製する.Replayされたプログラムが物理ページコンテナに存在しないページにアクセスした場合,Replayerは適切なタイミングでログからCPUの状態を復元し,ロードするようになっている.
V2Eにおけるテイント解析はおそらく,マルウェアのメモリ領域を正確に把握するためのものではない.それは,Recorderの段階でTDPテーブルの分割というアプローチによって実現されるべきものだからだ.TEMUのプラグインを使いたかったのだろうが,論文中からはあまりテイント解析を導入することのメリットが読み取れなかった.ここでのテイント解析は解析環境検出の対策に先立つものとして設定されているのだろうか.

評価

解析環境検出については問題なし.cpuid, rdtsc, cmpxch8b, icbp, rep stosb, fnstcwといった命令や,一般保護例外などについてテストされている.なお,rdtscについてはVMRESUME前にホストのTSCを参照することで対応している.
in-the-wildのマルウェアについても実験が行われており,アンパッカーとして期待できるパフォーマンスを見せている.
Recordにおける速度だが,コンテキストスイッチが頻繁に発生するカーネルモードルートキットで17倍,Internet Explorerで5倍と高速だ.KVMのシングルステップモードには3000倍のオーバーヘッドがあると言うのに.
全体的にpositiveな結果で,かなり良い.

おわりに

マルウェアによる解析環境検出に対して,復数の環境を組み合わせたRecord and Replayを用いる研究について紹介した.何より,監視対象とそれ以外で別個にTDPテーブルを作るというアプローチが素晴らしい.自分も研究でテイント解析を扱っているが,この流れを包摂していきたい.
ただ,これはサンドボックス全般に言えることなのだが,感染ホストにおけるユーザーのブラウザ操作が条件分岐に影響するMITBマルウェアについて,どう対処すべきなのだろうか.正常系のユーザーのブラウザ操作をデータセットとしてRecorderに与えたいところだが,果たしてどうなるのか.
このエントリはシステム系論文紹介 Advent Calendar 2014の9日目として書かれた.

参考文献

はじめに

選択的シンボリック実行について紹介する.

シンボリック実行

シンボリック実行とは,プログラムに含まれる変数に具体値を入力せず,その代わりとして値を代表するシンボルの操作を通じてプログラムを模擬的に実行し,結果を評価する技術である.シンボリック実行の目的は,コードカバレッジの拡大にある.シンボリック実行は全てのケースに対してforkする,あるいは,条件分岐の制約をもとにテストケースを生成するといった形態でソフトウェアテストに用いられている.
ひとまず,以前書いたシンボリック実行に入門しようとしたをご覧頂きたい.

選択的シンボリック実行

選択的シンボリック実行(selective symbolic execution)は,シンボリック実行の弱点を改善すべくS2Eにて提案,実装された.
シンボリック実行には実行パスにおける計算爆発(path explosion)の問題があった.プログラム中の全ての実行パスを通るための制約はあまりにも多い.そして,全ての実行パスというのは解析対象の実行パスだけではない.考えてもみよう,実システムでプログラムを実行した際,プログラムは自身以外の様々なものを呼び出す.呼び出されるlibcなどのライブラリ,そしてカーネルやデバイスドライバ,さらにそのファームウェアは,一体どこまで解析対象における実行パスの分岐に影響を与えるのか.
この頭が痛い問題に対応するべく編み出されたのが選択的シンボリック実行,すなわちシンボリック実行を行う範囲の限定である.S2Eはシンボリック実行を行いたい部分以外に具体値(concrete value)を用いることで,解析対象のプログラムだけにシンボリック実行を適用する(concolic testing).具体的には,S2Eは指定した変数が使用されている部分のみシンボリック実行を適用している.

S2E

動的バイナリ変換

S2Eは,QEMUをベースに開発された.QEMUはエミュレーションを実現するべく,以下のような流れで動的バイナリ変換を行う.

1.ゲストコードの逆アセンブル
2.マイクロオペレーションに変換
3.コード辞書を参照してホストコードに変換

S2Eはこのコード辞書をLLVM bitcodeに差し替えることで,x86のバイナリをLLVM bitcodeに変換する.そして,変換後のLLVM bitcodeをKLEEに渡すことで,シンボリック実行を行う.
このとき,解析対象の全部分がLLVM bitcodeに変換されるわけではない

指定した変数が使用されている部分のみシンボリック実行を適用すると書いたように,S2Eはシンボル化したデータにアクセスしているか否かによって,解析対象の実行方式を切り替えている.実行方式は以下の二通りだ.

  1. 具体値にアクセスしている場合,通常通り実行
  2. シンボルにアクセスしている場合,LLVM bitcodeに変換してKLEE上で実行

これは,解析対象を多数のコードブロックに分割するというQEMUのバイナリ変換方式に極めて依存している.

S2E opcodes

S2Eは独自の拡張命令S2E opcodesを用いてシンボリック実行のための機能をinstrumentする.S2E opcodesは以下のような機能を提供する.

  • S2SYM: データのシンボル化
  • S2ENA: 複数パスの実行を有効化
  • S2DIS: 複数パスの実行を無効化
  • S2OUT: デバッグ情報の出力

この中身はs2e-x86.hs2e.hに記述されている.例としてS2SYMの実装を見てみよう.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define S2E_INSTRUCTION_COMPLEX(val1, val2) \
".byte 0x0F, 0x3F\n" \
".byte 0x00, 0x" #val1 ", 0x" #val2 ", 0x00\n" \
".byte 0x00, 0x00, 0x00, 0x00\n"
#define S2E_INSTRUCTION_SIMPLE(val) \
S2E_INSTRUCTION_COMPLEX(val, 00)
~ 略 ~
static inline void s2e_make_symbolic(void *buf, int size, const char *name)
{
__s2e_touch_string(name);
__s2e_touch_buffer(buf, size);
__asm__ __volatile__(
S2E_INSTRUCTION_SIMPLE(03)
: : "a" (buf), "d" (size), "c" (name) : "memory"
);
}

例えば,解析対象のソースコード上でs2e_make_symbolic()の引数にシンボル化したい変数を渡すことで,この関数を利用することができる.

Windowsデバイスドライバに対する選択的シンボリック実行

Analyzing Windows Drivers: Step-by-Step Tutorialという公式チュートリアルでは,プラグインを用いてWindowsデバイスドライバにアノテーションを付加する方法が紹介されている.

1
2
3
4
5
6
7
8
9
10
11
12
13
function annotation_example(state, plg)
-- Write custom Lua code here (e.g., to inject symbolic values)
end
pluginsConfig.Annotation =
{
init1 = {
active=true,
module="pcntpci5_sys_1",
address=0x169c9,
instructionAnnotation="annotation_example"
}
}

チュートリアルの例ではpcntpci5.sysというドライバが0x169c9というアドレスを呼び出す際にannotation_example()が実行される.BSoDもフックできるので,NotMyFaultドライバで遊ぼう.

オーバーヘッド

いつだって問題となるのは実行速度だ.論文によると,S2Eは具体値による実行時(concrete mode)にQEMUの6倍,シンボリック実行時にQEMUの78倍のオーバーヘッドが生ずるとされている.注意したいのは,実機の78倍ではなくQEMUの78倍である点だ.

おわりに

S2Eについて紹介した.
このエントリはソフトウェアテストあどべんとかれんだー2014の3日目として書かれた.

参考文献

より詳しくは以下の論文を参照されたい.これも全てVitaly Chipounovって奴の仕業なんだ.