拾い物のコンパス

まともに書いたメモ

セキュリティキャンプ講義「仮想化技術を用いたマルウェア解析」にチャレンジしてみた(準備編)

今年の八月中旬に行われたセキュリティキャンプ全国大会2015の解析トラック15・16-Dでの講義「仮想化技術を用いたマルウェア解析」にチャレンジして、一通りできた。
発表者の黒米さんの許可が戴けたのでその流れを書き残す。
発表で使われたスライドは黒米さんの技術ブログ" 一生あとで読んでろ "に置いてある。

構成

プラグインを書くことよりも環境の準備に相当手間取ったので、準備編とプラグイン開発編の二段階で書いていく。
内容は以下の通り。
* 準備編
DECAFの導入と操作方法、注意点。
* プラグイン開発編
DECAF上で動かしているWindows仮想マシンで、仮想化検知を施されたプログラムblue.exeの検知を回避するプラグインを開発していく。

DECAFの準備

DECAFとは

DECAFは"Dynamic Executable Code Analysis Framework)の略で、QEMUにテイント解析機能を追加したTEMUの後継として作成されたバイナリ解析フレームワークのこと。詳しくはBitBlazeプロジェクトを検索。

インストール

インストールした環境は以下の通り。
* Ubuntu 14.04 LTS 64bit
他にはUbuntu 14.04 LTS 32bitArch Linux 64bitでインストールを試みたが、コンパイルが上手くいかず断念した。

まず、適当なディレクトリに移動してから以下のコマンドを実行していく。

$ sudo apt-get update
$ git clone https://github.com/sycurelab/DECAF
$ cd DECAF/decaf
$ pwd
/home/<user>/DECAF/decaf
$ sudo apt-get install qemu
$ sudo apt-get install build-dep qemu
$ sudo apt-get install binutils-dev
$ sudo apt-get install libboost-all-dev
$ ./configure # スライドの方にはテイント解析などをオンにするオプションが載っているが、今回はなくても問題なかった。  
$ make

もしもpython関係のエラーが出たら、コマンドのpythonが3系にリンクが張られていないかを確認すること。DECAFのスクリプトは2系で書かれていて、例外処理関係でエラーが出る。
具体的にはconfig-host.makPYTHON=pythonPYTHON=python2.7とかに書き換える。
これ以外のエラーが出たら・・・頑張れ!
DECAF自体のインストールはmakeが上手くいけばおしまい。
次はWindows仮想マシンを用意する。

仮想マシンの準備

Developer Resources : Microsoft Edge Devで30日だけ動作するWindows仮想マシンを配布しているので、ダウンロードする。
トップページ下にある"Virtual Machine"をクリック。
Windowsタブで
* Virtual Machine => IE8 on XP
* Select Platform => VMWare
と設定し、"Download .zip"からダウンロード。

$ pwd
/home/<user>/DECAF/decaf
$ mkdir ../VMs
$ mv $HOME/Downloads/IE8.XP.For.Windows.VMware.zip $HOME/DECAF/VMs
$ cd ../VMs
$ unzip IE8.XP.For.Windows.VMware.zip

VMWare用の仮想マシンからqemu用の仮想マシンを作成する.
作成には少々時間がかかる.容量も7GBほど食われた覚えがあるから注意.

$ qemu-img convert <name of VM>.vmdk -O qcow2 pre_ie8.qcow2
$ qemu-img convert -p -f qcow2 -O qcow2 -o compat=0.10 pre_ie8.qcow2 ie8.qcow2

qcowに変換しても動作するが,スナップショットが使えない.
作成が終わったら,下のコマンドで起動してみる.
$ $HOME/DECAF/decaf/i386-softmmu/qemu-system-i386 $HOME/DECAF/decaf/VMs/ie8.qcow2 -m 2048 -monitor stdio

これでWindowsXPが立ち上がることを確認する。-mオプションは割り当てるメモリの量を指定する.1GBでは動作が遅い。
また,今回は1つしかプロセッサを与えていないため,しばらくは処理が重いが時間を置くと落ち着く.
画面が引き延ばされるのを修正したいならCtrl+Alt+uを押す。
マウスが暴走するようなら
$ export SDL_VIDEO_X11_DGAMOUSE=0
Windowsが立ち上がることを確認したらシャットダウンする。
次に問題のプログラムをWindnowsにコピーする。

$ git clone https://github.com/ntddk/blue.git
$ sudo modprobe nbd
$ sudo qemu-nbd -c /dev/nbd0 ie8.qcow2
$ sudo /sbin/fdisk -l /dev/nbd0
$ sudo mount -o loop,offset=$((63*512)) /dev/nbd0 /mnt
$ sudo cp -r blue /mnt/Documents and Settings/All Users/Desktop
$ sudo umount /mnt
$ sudo qemu-nbd -d /dev/nbd0
$ sudo rmmod nbd

これで起動したときのデスクトップにblueのフォルダが出るようになる。
また,プラグインコンパイルしておく.

$ pwd
/home/<user>/DECAF/VMs
$ cd ../decaf/plugins
$ git clone https://github.com/ntddk/geteip
$ cd geteip
$ ./configure --decaf-path=/home/<user>/DECAF/decaf
$ make

このプラグインはIsDebuggerPresentをフックする.関数フックのテンプレートとして勉強になる.
今後の起動はDECAFの公式HPやスライドに載っている自動化スクリプトを使ってもできる.
下のスクリプトも同じ動作をするのでHaskell好きな人はどうぞ.

-- decaf_auto.hs
import System.IO
import System.Process         -- createProcess
import System.Posix.Directory -- changeWorkingDirectory
import Control.Concurrent (forkIO, threadDelay)

inputCmd p_in p_out cmd = do
    hPutStrLn p_in cmd
    threadDelay (1000 * 1000)
    -- Nonblocking
    forkIO $ putStrLn =<< hGetContents p_out

rawInput msg = do
    putStr msg                                             
    getLine

main :: IO ()
main = do
    hSetBuffering stdout NoBuffering
    decaf_path <- rawInput "*** Enter the root directory of DECAF (i386-softmmu/qemu-system-i386 should be there): "
    image_path <- rawInput "*** Enter the image path: "
    changeWorkingDirectory decaf_path
    (Just p_stdin, Just p_stdout, _, _) <- 
        createProcess (shell $ "i386-softmmu/qemu-system-i386 " ++ image_path ++ " -m 2048 -monitor stdio") { std_in = CreatePipe, std_out = CreatePipe }
    hSetBuffering p_stdin NoBuffering
    hSetBuffering p_stdout NoBuffering
    threadDelay (3 * 1000 * 1000)
    inputCmd p_stdin p_stdout "ps"
    inputCmd p_stdin p_stdout "help"
    return ()
-- E.O.F.

やっていることはqemu-system-i386のプロセスを作成し,そのプロセスの標準入力・標準出力に対しての操作を行っている.
しかしこのスクリプト(Python版も含めて)は使わないほうがいいと感じた.
これは実行すると書いてある動作以外をせず,qemuのプロンプトが出ないで入力・出力が閉じてしまう.
使うなら下のコマンドで普通に実行したほうがいい.

$ pwd
/home/<user>/DECAF/VMs
$ ../decaf/i386-softmmu/qemu-system-i386 ie8.qcow2 -m 2048 -monitor stdio

出力が一段落ついたあたりで一回Enterを押すと,(qemu)のプロンプトが出る.
プロンプトを表示させてプラグインを読みこませる.

(qemu) load_plugin ../decaf/plugins/geteip/geteip.so
(qemu) geteip blue.exe

これでblue.exeを実行した時にgeteip.cの内容が実行される.blue.exeblue/Release/blue.exeにある.
何もいじっていなければIsDebuggerPresentの実行時と実行後にそれぞれフックしたメッセージが出るだけのはず.
この辺はどうにも不安定で,いじっていなくてもIsDebuggerPresentを回避できたりした.
詳しいことはプラグイン開発編で書く.

最後に

ここまでがうまく行けば,準備は終わり.
誤字やうまく行かないところがあれば気軽に言ってくれると嬉しい.
ここまで長い記事を書いたことがないので,どこか間違いがありそうな気がする.
次は,blue.exeのソースを参考にプラグインを書いていく.

アセンブリ言語をコンパイル・実行してみる

GASとNASMで64bitプログラミングの基本をやってみた。
実行するのは基本中の基本である"hello, world"を出力するだけのプログラム。

環境

  • gcc: version 5.2.0 (GCC)
  • nasm: NASM version 2.11.08 compiled on Mar 24 2015
  • Arch Linux x86_64: 4.2.5-1-ARCH

64bitアセンブラ基本

64bitのアセンブラにおいてはレジスタは以下のように使われる。
rax: システムコール番号
rdi: 第一引数
rsi: 第二引数
rdx: 第三引数
システムコール番号は/usr/include/asm/unistd_64.hに書いてある。

これを今回のwriteの場合に当てはめると
rax: 0x10 (write)
rdi: 1 (stdout)
rsi: pointer of strings ("hello, world!!!")
rdx: 16 (文字列長)

GAS

GASはGNU ASのことであり、GCCコンパイルすることができる。デフォルトではAT&T記法なので今回はIntel記法に変更してからプログラミングする。 ソースは以下の通り。

/* hello.s 64bit */

.intel_syntax noprefix
.globl _start

_start:
// write(1, "hello, world!!!\n", 16)
   xor rax, rax
   xor rdi, rdi
   xor rdx, rdx

   inc rax
   inc rdi

   mov rsi, 0x0a212121646c726f
   push rsi
   mov rsi, 0x77202c6f6c6c6568
   push rsi
   mov rsi, rsp

   push 0x10
   pop rdx
   syscall

// exit(1)
   push 0x3c
   pop rax
   syscall
$ gcc -nostdlib hello.s -o hello_gas  
$ ./hello_gas
hello, world!!!

NASM

NASMはNetwide Assemblerの略であり、nasmというコンパイラを使ってコンパイルする。 NASM自体は公式サイトからダウンロードするか各ディストリのレポジトリからインストールできる。Arch Linuxでは
$ pacman -S nasm
でインストールできる。

; hello.asm 64bit
section .text
global _start

_start:
   xor rax, rax
   xor rdi, rdi
   xor rdx, rdx

   inc rax     ; write
   inc rdi     ; stdout

   mov rsi, 0x0a212121646c726f
   push rsi
   mov rsi, 0x77202c6f6c6c6568
   push rsi
   mov rsi, rsp

   push 0x10
   pop rdx
   syscall

; exit(1)
   push 0x3c
   pop rax
   syscall
   
$ nasm -f elf64 hello.asm && ld hello.o -o hello_nasm
$ ./hello_nasm`  
hello, world!!!

両方ともこんな風にコンパイルされている。

   xor rax, rax
   xor rdi, rdi
   xor rdx, rdx
   inc rax
   inc rdi
   movabs rsi, 0x0a212121646c726f
   push rsi
   movabs rsi, 0x77202c6f6c6c6568
   push rsi
   push rsi
   mov rsi, rsp
   push 0x10
   pop rdx
   syscall
   push 0x3c
   pop rax
   syscall

プログラムは一応NULLバイトをなくしてある。
syscallをsysenterと書いてデバッグしたのはいい思い出である。64bitではint 0x80とsysenterはサポートされていないようだ。
それぞれコメントアウトの仕方が違うのでそこは注意が必要である。

比較

GASの方はコメントの仕方がCライクであったり、pushの時に自動でBYTE変換してくれる、GCCで使えるなど、芸が細かい。
NASMの方はオブジェクトファイルが作りやすいから、CTFとかで使いやすい。
どちらを使うかは好みの問題だと思う。

LightDMの壁紙が変わらないときの対処

Arch Linuxをインストールする際に最初はDM(デスクトップマネージャ)にGDMを使っていたが、認証成功時に一瞬CUIっぽいのが出ていた。これが微妙だったからUbuntuOpenSUSEで利用されている軽量DMのLightDMに変えた。
このとき認証画面の背景を変更するのに割と手間取ったためメモを残しておく。

環境

問題

ArchWikiには背景を変えたいときは/etc/lightdm/lightdm-gtk-greeter.confbackgroundを編集すればよいと書いてある。
具体的には、
・単色にしたいとき
background=#000000
・画像を使いたいとき

background=/usr/share/pixmaps/<wallpaper>.jpg
user-background = true  

これではうまくいかなかった。調べてみると、OpenSUSEのフォーラムに答えがあった。

対処

画像があるディレクトリをroot所有にして、画像自体はパーミッション644にすると上手くいった。
$ chown root <pict_dir>
$ chown root <wallpaper>.jpg
$ chmod 644 <wallpaper>.jpg
やっていること自体は画像を置くことが推奨されている/usr/share/pixmapsに近づけているだけだったりする。要はこれに合わせるか、このディレクトリに入れろってことのようだ。
もしこれでうまくいかないときはlightdm-gtk-greeter-settingsを使うと良い。
GUI画面で異常が起こっているかどうかを確認しながら作業することができる。
これは
$ pacman -S lightdm-gtk-greeter-settings
でインストールできる。
起動したら、Backgroundの右にあるmultihead setupを選び、そこでディスプレイと画像を指定する。
異常がある場合黄色い三角が出てくる。

参考

Problem changing 'lightdm' theme

Ubuntuで認証画面を無限ループさせる方法と対処

最近DM(デスクトップマネージャー)やWM(ウィンドウマネージャー)をいじっていると、ある時突然認証画面でパスワードを入力してもすぐにまた認証画面が出る無限ループに陥った。割と楽しい現象だったので引き起こし方と対処を書き残す。

確認した環境

無限ループを正常に引き起こせたOSは以下の通り(これしか試していない)
・Ubuntu14.04 LTS 32/64bit
・LightDM・GNOMEを入れたArch Linux 64bit

無限ループの呪文

$ sudo startx && sudo reboot
以上。
パスワードの長い認証を抜けると、認証であった。
30回ほどパスしたが、終わらなかった。

対処

まずはシェルを起動する。
簡単な方法としては以下のものがある。
sudoが使える別ユーザでログイン
・Ctrl+Alt+F1とかを押してCUIでログイン
sudoを使えるユーザでログインすることが重要。そして以下のファイルを削除する。

.ICEauthority  : 複数のXクライアント同士で直接通信するときの認証に使われる。内部にランダムなCookieを保有していて、これが同じXクライアント同士が通信できる。
.Xauthority    : Xサーバの認証に使うファイル。詳しくはxauthで調べる。

.ICEauthority.Xauthorityは同じように見えるが、前者はClient to Client, 後者はClient to Serverであることが大きな違い。

これで再起動すれば正常に動くようになる。消したファイルは再度作成される。

原因

認証ファイルを消したらうまくいったってことは、Xサーバの認証の失敗ってことでいいんだろうか。
GDMとLightDMの.Xauthorityファイルの使い方についてもっと詳しく調べたほうがよさそうだ。
正直全くわかっていない。わかったら追記する。

考察

実はもう一つの対処として、ArchのDMをGDMに変更しするというものもある。
これをした場合、sudo startxが自体が成功しなくなった。そして、認証もループが起こらなくなった(無限ループが起こっている状態で隠しファイルを消さずにDMを変更した。このままLightDMに戻すとまたループ)。
UbuntuはDMにLightDMを用いていることから考えると、今回のこの無限ループはLightDMに起因するものなのだろうか。 もしくは、LightDM内で使われているであろうxauthが原因である可能性が高いのではないかと思う。
どちらにせよ、UbuntuDebianベースのXubuntuやLubunt、Kali Linuxのようなファミリーはすべて今回の現象が起こる可能性がある。実際に試してはいないので何とも言えないが。
もし試し方がいたら環境と結果を教えて頂けると非常にありがたい。
これの根本的な原因を追究できるようになりたい・・・。

参考

Ubuntu Xサーバー その3 - ログインしてもログイン画面に戻される時は - Ubuntu kledgeb

open - what is .ICEAuthority file in opensuse 11.2 - Server Fault

見たことあるCコーディングミス

知り合いが実際に書いて悩んでいたCコードを2種類書き残す。

その1

...
int i=0,
    count[i];
...(snip)...

一見してやばいのはわかるはず。要素数0の配列を宣言していた。その場ではただひどいコードだとだけ思ったが、あとから気になってちょっと実験してみた。

実験コード

/* zero_array.c */
#include <stdio.h>

int main(void)
{
   int  a = 0,
        b = 0,
        c[a];
        
   printf("a: %p\nb: %p\nc: %p\n", &a, &b, c);
}
/* E.O.F. */

実行結果

実行したところ、

a: 0xbf877850
b: 0xbf877854
c: 0xbf877840

となった。変数a, bは正しくスタックに積まれているようだ。cは確保されているのかどうかとても怪しいところ。領域は確保されていないがアクセスできるポインタみたいな印象であるが、詳細は不明。わかったら追記する。
当たり前ではあるが、上のソースでc[4]に任意の値を書くとaを書き換えることができる。ただのセキュリティホールにしか見えない。

2015/10/31追記

turn_upさんから指摘があった。要素数0の配列は構造体で

struct hoge {
   int i;
   int a[0];
}

とするとaiの次のアドレスを指すようになる。上手く使うと便利なようだが、マイナーな上にGCC拡張だから、依存性が強い。C++では配列は1個以上の要素を持たないといけないで要素数0の宣言はできない。

その2 (考察間違い。追記要参照)

これも知り合いが書いてしまったコード。詳しく覚えていないが、こんな雰囲気だった。

while (1) {

if (hoge = 32) break;
else ...(snip)...

if文の条件で代入を行うとTrue扱いになってすぐループを抜けてしまっていた。
気になったのでいじっていたら、代入が失敗する場合を1種類だけ見つけた。
それは以下の場合である。

int/char a;
if (a = 0.11) printf("True\n");
else          printf("False\n");

intcharの変数に小数を代入しようとすると失敗するようだ。

2015/10/31追記

turn_upさんから指摘があって、改めて考えてみた。
冷静に考えてみると、分岐命令の中身は
if (i = 0) -> if (i) -> if (0)
の流れでFalseになっているんじゃないかと思って、アセンブラを見てみたら案の定

mov DWORD PTR [esp+0x1c], 0x0
cmp DWORD PTR [esp+0x1c], 0x0
je 0x80......

となっていた。だから、変数に0を代入した場合はすべてFalseになる。これはint/char/float/doubleの時で確認できた。
細かいことではあるが、先ほどのコードのようなシンプルな分岐では、i0以外を代入したときは実行ファイルにcmpはなかった。自明すぎてコンパイラに消されてしまったようだ。-O0をつけても同じだった。

どこかで聞いたのは、分岐条件の記述は変数を後に書いたら良いとのことだった。
例えばif (NULL == ptr)if (32 == hoge)のようにである。これならば間違えてしまったときはコンパイルエラーが出てすぐに気付くことができる。

参考ページ

Using and Porting GNU CC - C 言語ファミリに対する拡張

"B"-con - Laboratory - Data & Algorithm - アクロバティックなコード (1)

GHCでコンパイルしたバイナリを小さくする方法

概要

HaskellGHCコンパイルしてバイナリを生成することができる。しかし、Cの時と比べて意味不明なレベルでバイナリが大きくなる。原因と対策を書いておく。

環境

  • OS
$ uname -a
Linux ubuntu 3.13.0-63-generic #103-Ubuntu i686
$ ghc --version
The Glorious Glasgow Haskell Compilation System, version 7.6.3
$ gcc --version
gcc (Ubuntu 4.8.4-2ubuntu1~14.04) 4.8.4
...

ソース

-- hello.hs

main = putStrLn "Hello, World!"

-- E.O.F.
/* hello.c */
#include <stdio.h>

int main(void)
{
   printf("Hello, World!\n");
   return 0;
}
/* E.O.F. */

どちらも "Hello, World!"と出力するだけの簡単なプログラム。

コンパイル

  • GHC
    $ ghc -o h_hello hello.hs -O2

  • GCC
    $ gcc -o c_hello hello.c -O2

サイズ

$ ll ?_hello
7332     c_hello
839047   h_hello

HaskellはCの100倍以上の大きさを持っている。
最初見たときは叫んだ。

原因

GHCはデフォルトでHaskell runtime(libHSrts.a)を静的リンクするようだ。 こいつが丸々バイナリに組み込まれてしまうのが問題みたいだった。
GCCは最初から動的リンクするイケメン。

対策

動的リンクに切り替えてやればよい。
UbuntuHaskell platformには動的リンクが含まれていない。
だから最初に ghc-dynamic をインストールする。
$ apt-get install ghc-dynamic
それから以下を実行。

$ ghc -dynamic -o h_hello hello.hs`  
$ ll ?_hello
7332     c_hello
16397    h_hello

2倍程度まで小さくなった。 デバッグ情報を消去するともう少し小さくなる。参考までにデバッグ情報をなくしたCバイナリ(sc_hello)も載せておく。

$ strip -p --strip-unneeded --remove-section=.comment -o sh_hello h_hello
$ ll *_hello
7332     c_hello
16397    h_hello
5448     sc_hello
9688     sh_hello

c_helloのほぼ同程度のサイズまで落とせた。

結論

Haskellのバイナリはリンクさえどうにかすればサイズは2倍程度にできることがわかった。

おまけ

このままCバイナリに負けたままでは悔しかったからもう少し頑張ってみた。
実行形式ファイルを実行可能な圧縮ファイルに変換するgzexeというコマンドがある。これは実行ファイルをgzipで圧縮し、圧縮したファイルの先頭に解凍するスクリプトを埋め込む。容量の小さいディスクにおいて有効なコマンドである。

$ gzexe h_hello
h_hello:   74.2%

$ ll *_hello
7332     c_hello
5061     h_hello         <- 圧縮後
16397    h_hello~        <- 圧縮前

・・・ついにHaskellバイナリはCバイナリを越えた!

$ gzexe c_hello
c_hello    68.7%

$ ll *_hello
3132    c_hello         <- 圧縮後
7332    c_hello~

5061    h_hello         <- 圧縮後
16397   h_hello~

という夢を見た。おしまい。

メモ

最初にPreludeの関数をputStrLn以外をhidingしてみたが、サイズ面では全く変化がなかった。
関数のロード関連はサイズに関係しないフラグなどがあるのだろうか。

参考リンク

haskell - Could not find module Prelude... dyn libraries for package base? - Stack Overflow

linker - Making small haskell executables? - Stack Overflow

xinetdが動作しないときの対処

概要

xinetdでサービスを動かしたら接続は上手くいくが、プログラムからの出力が返ってこなかった。この時に試したことを書き残す。
最終的にはxinetdの公式レポジトリからソースを引っ張ってきてインストールしたら正常に動作した。
他にはonly_fromの記述に注意すれば上手く行くことがわかった.
尚、この現象は32ビット版でのみ起こった. 64ビット版では今回の問題は起こらなかった.

環境

Ubuntu 14.04 LTS 32bit Arch Linux 64bit

設定ファイル

# xinetd.conf
service unlisted
{
    type        = UNLISTED
    socket_type = stream
    protocol    = tcp
    wait        = no
    only_from   = localhost 192.168.0.0/16
    port        = 20020
    server      = /bin/echo
    server_args = hogehogehogehoge                                
}

サーバを立てた状態で $ nc localhost 20020 を実行すれば hogehogehogehoge と表示される。

実行コマンド

$ sudo xinetd -f xinetd.conf -d

試したこと

  • 再インストール
    apt-get remove xinetd && apt-get install xinetd
  • デーモンのリスタート
    $ service xinetd restart
  • 再起動
    $ reboot
  • アップデート・アップグレード
    $ apt-get update; apt-get upgrade どれも効果なし。

対処

その1

ソースを取ってきてインストールする。

$ git clone https://github.com/xinetd-org/xinetd.git
$ cd xinetd
$ ./configure
$ make
$ make install

/usr/local/sbin/にインストールされる。
これでちゃんと動いた。

その2

$ ip address show
で自分のIPを調べる.xintd.confonly_fromで接続を192.168.0.0/24に制限しているから,自分のIPが外れていたら適宜修正すること.
$ nc <your_IP> 20020

その3

以下を編集する.

[-] only_from   = localhost 192.168.0.0/16
[+] only_from   = 127.0.0.1 192.168.0.0/16

only_fromの記述にlocalhostは使わないほうがいいようだ.
これならば nc localhost/127.0.0.1/192.168.?.? 20020 どのIPでも上手く行った.
ちなみに,xnetd実行時にsudoを忘れるとどのIPでも出力が返ってこなかった.

まとめ

  • xinetd実行時にはsudoをつけよう
  • xinetd.confファイルのonly_fromではlocalhostは使わない

以上.