Login
Immutable PageDiscussionInfoAttachments
CTF/Writeup/SIGINT 2013/VM

MMA

vm

昔解いた問題の蔵出し。いつの間にか問題がexploit込みで公開されていた。http://archive.aachen.ccc.de/sigintctf-2013/challenges/22/

出題時の環境を再現するには多分,makeした後,ディレクトリを分けて

をコピーし,mainを vm.bin という名前で配置し,flagファイルにフラグを置いて

% socat tcp-listen:1024,fork exec:./vm.bin

すれば良い。

公開された想定exploitと別の方法で解いていたようだ。

初動分析

よくわからない仮想マシンvm.binが動いている。仮想マシンはx86-64で書かれている。しかしvm.binの全体を取り出す方法がわからない。

Cmd> ls
vm.bin
exploit
.
hexdump
hexpaste
..
ls
bios.bin
uname
exit
cmd
flag

hexdumpコマンドでサーバ上のファイルを読み出せるが先頭4KBまでのようである。

Cmd> hexdump

File? ls
10 1f 1c 00 1d 01 12 1f 3b 0c 00 20 10 3e 00 e3
3e 20 00 e5 3b 09 10 e4 10 15 00 20 10 1d 3b 06
...

ただしflagファイルについてはアクセスすることができない。

Cmd> hexdump

File? flag
Access Forbidden!

Not Found

hexpasteコマンドでVM上にファイルを配置できる。

Cmd> hexpaste

File? ABC
01020304
. <-16進数とスペース改行以外なら何でもよい

やるべきことは次の流れであると考えられる。

  1. 仮想マシンの構造を把握する
  2. UselessOSにかかってる制限を把握する
  3. flagを読み出すプログラムを書いてアップロードして実行

VMの構造の解析

クラッシュ時に16個のレジスタの値が表示されるようになっている。 これを利用して,次のようなコマンド列を流しこんでレジスタにどのような変化が生じるか挙動を観察する。

hexpaste
a
0090 0000 0010
.
a

いろいろ試してみると次のようなことに気づく。

ディスアセンブラを作成しつつ命令の詳細を把握する。bios.binやunameの中身を見ながら, 不明な命令の挙動を推測することもかなり役に立った。

instructions

0000

(crash)

0001

(r1 -= 2)

0010

ret

0020

syscall

r3にシステムコール番号、r4に引数, biosに制御を移す, bios中から呼び出すとvmの機能を呼び出し

0040

exitsyscall

biosからプログラムに制御を戻す

00X3

r2:3 = rX==0; r2:4 = rX<0

00X4

inc rX

00X5

dec rX

00X6

push.byte rX

*((byte*)r1)-- = rX

00X7

push.word rX

*((word*)r1)-- = rX

00X8

pop.byte rX

rX = *((byte*)r1)++

00X9

pop.word rX

rX = *((word*)r1)++

00Xa

swap rX, xrX

10XY

rY = rX

20XY

rX = rY

30YY

call +YY

push r0 and jump

40YY

jump +YY

50XY

rY = *(byte*)rX

60XY

*(byte*)rY = rX

70XY

rY = *(word*)rX

80XY

*(word*)rY = rX

90XX

push 0xXX

c0XY

r2 = rX==rY ? 1 : rX<rY ? 2 : 4

XeYY

jump +YY if r2:X

XfYY

jump +YY if !r2:X

f1XY

r15 = X>Y ? rY + rX : rY * rX

f2XY

r15 = rY - rX

f3XY

r15 = rY / rX

f4XY

r15 = rY % rX

f5XY

r15 = rY xor rX

f6XY

r15 = X<Y ? rX|rY : rX&rY

後でレジスタに値をセットしてから確認

f7XY

r15 = rY << (rX & 0xff)

f8XY

r15 = rY >> (rX & 0xff)

f9XY

r15 = rY rot<< (rX & 0xff)

faXY

r15 = rY rot>> (rX & 0xff)

fbXX

r15 = XX

fcXX

r15l = XX

fdXX

r15h = XX

disasm.pl

   1 use strict;
   2 use warnings;
   3 use feature ':5.10';
   4 
   5 my $file = shift @ARGV;
   6 
   7 sub r { "r" . hex(shift) }
   8 sub reladdr {
   9     my $addr = shift;
  10     my $reladdr = hex(shift);
  11     $addr += 2 + $reladdr;
  12     if ($reladdr >= 0x80) { $addr -= 0x100 }
  13     sprintf "0x%04x", $addr;
  14 }
  15 
  16 sub disas {
  17     my $addr = shift;
  18     my $hex = shift;
  19     given ($hex) {
  20     when (/0010/) { return "ret" }
  21     when (/0020/) { return "syscall" }
  22     when (/0040/) { return "exitsyscall" }
  23     when (/00(.)3/) { return "r2:3 = @{[r($1)]}==0; r2:4 = @{[r($1)]}<0" }
  24     when (/00(.)4/) { return "inc @{[r($1)]}" }
  25     when (/00(.)5/) { return "dec @{[r($1)]}" }
  26     when (/00(.)6/) { return "push.byte @{[r($1)]}" }
  27     when (/00(.)7/) { return "push.word @{[r($1)]}" }
  28     when (/00(.)8/) { return "pop.byte @{[r($1)]}" }
  29     when (/00(.)9/) { return "pop.word @{[r($1)]}" }
  30     when (/00(.)a/) { return "swap @{[r($1)]}, x@{[r($1)]}" }
  31     when (/10(.)(.)/) { return "@{[r($2)]} = @{[r($1)]}" }
  32     when (/20(.)(.)/) { return "@{[r($1)]} = @{[r($2)]}" }
  33     when (/30(..)/) { return "call @{[reladdr($addr, $1)]}" }
  34     when (/40(..)/) { return "jump @{[reladdr($addr, $1)]}" }
  35     when (/50(.)(.)/) { return "@{[r($2)]} = *(byte*)@{[r($1)]}" }
  36     when (/60(.)(.)/) { return "*(byte*)@{[r($2)]} = @{[r($1)]}" }
  37     when (/70(.)(.)/) { return "@{[r($2)]} = *(word*)@{[r($1)]}" }
  38     when (/80(.)(.)/) { return "*(word*)@{[r($2)]} = @{[r($1)]}" }
  39     when (/90(..)/) { return "push 0x$1" }
  40     when (/c0(.)(.)/) { return "r2 = @{[r($1)]}==@{[r($2)]} ? 1 : @{[r($1)]}<@{[r($2)]} ? 2 : 4" }
  41     when (/(.)e(..)/) { return "jump @{[reladdr($addr, $2)]} if r2:$1" }
  42     when (/(.)f(..)/) { return "jump @{[reladdr($addr, $2)]} if !r2:$1" }
  43     
  44     when (/(.)1(.)(.)/) {
  45         if (hex($2) > hex($3)) {
  46             return "@{[r($1)]} = @{[r($3)]} + @{[r($2)]}"
  47         } else {
  48             return "@{[r($1)]} = @{[r($3)]} * @{[r($2)]}"
  49         }
  50     }
  51     when (/(.)2(.)(.)/) { return "@{[r($1)]} = @{[r($3)]} - @{[r($2)]}" }
  52     when (/(.)3(.)(.)/) { return "@{[r($1)]} = @{[r($3)]} / @{[r($2)]}" }
  53     when (/(.)4(.)(.)/) { return "@{[r($1)]} = @{[r($3)]} % @{[r($2)]}" }
  54     when (/(.)5(.)(.)/) { return "@{[r($1)]} = @{[r($3)]} xor @{[r($2)]}" }
  55     when (/(.)6(.)(.)/) {
  56         if (hex($2) < hex($3)) {
  57             return "@{[r($1)]} = @{[r($3)]} | @{[r($2)]}"
  58         } else {
  59             return "@{[r($1)]} = @{[r($3)]} & @{[r($2)]}"
  60         }
  61     }
  62     when (/(.)7(.)(.)/) { return "@{[r($1)]} = @{[r($3)]} << (@{[r($2)]} & 0xff)" }
  63     when (/(.)8(.)(.)/) { return "@{[r($1)]} = @{[r($3)]} >> (@{[r($2)]} & 0xff)" }
  64     when (/(.)9(.)(.)/) { return "@{[r($1)]} = @{[r($3)]} rot<< (@{[r($2)]} & 0xff)" }
  65     when (/(.)a(.)(.)/) { return "@{[r($1)]} = @{[r($3)]} rot>> (@{[r($2)]} & 0xff)" }
  66     when (/(.)b(..)/) { return "@{[r($1)]} = 0x$2" }
  67     when (/(.)c(..)/) { return "@{[r($1)]}l = 0x$2" }
  68     when (/(.)d(..)/) { return "@{[r($1)]}h = 0x$2" }
  69     default { "" }
  70     }
  71 }
  72 
  73 open my $fh, '<', $file or die;
  74 my $addr = 0;
  75 while (read($fh, my $word, 2)) {
  76     my $h = unpack("H*", $word);
  77     my $s = $word;
  78     $s =~ s/(.)/if (ord($1) < 0x20 || ord($1) >= 0x7f) { "." } else { $1 }/sge;
  79     printf "%04x: %s %s %s\n", $addr, $h, $s, disas($addr, $h);
  80     $addr += 2;
  81 }

dumpmem

何のために作ったかいまいち覚えていないメモリダンプツール

注: abcdefはそれぞれ:;<=>?で出力される

# dump 0xf000-0xffff
hexpaste
a
101e 1df0 1c00 9d10 9c00 8b01 bb10 ab30 
0058 0093 3e1c 9289 3b06
3b06 43b5 454a 0020
3b06 44b5 454a 0020
3b06 4b20 0020
40de 10e1
0010
.
a

      101e  r14 = r1
      1df0  r1 = 0xf000
      1c00  
      9d01  r9 = 0x100
      9c00  
      8b01  r8 = 1
      bb10  r11 = 0x10
      ab30  r10 = 0x30 // '0'
        LOOP:
      0058  r5 = *((byte*)r1)++
      0093  r2:3 = r9 == 0
      3e1c  jump EXITLOOP if r2:3
      9289  r9 = r9 - r8

      3b06  r3 = 0x06
      43b5  r4 = r5 / r11
      454a  r4 = r4 + r10
      0020  syscall
      
      3b06  r3 = 0x06
      44b5  r4 = r5 % r11
      454a  r4 = r4 + r10
      0020  syscall
      
      3b06  r3 = 0x06
      4b20  r4 = 0x20 // ' '
      0020  syscall

      40de  jump LOOP
      10e1  r1 = r14
        EXITLOOP:
      0010  ret

制限1: フラグへのアクセス制限

bios.binを全て抜き出して見てみると0x12aの位置に"Access Forbidden!"という文字列が,0x155の位置に"flag"という文字列が含まれている。

エラーメッセージはどこで参照されているかというと0x0058の位置で,

0058: fd01 .. r15h = 0x01
005a: fc2a .* r15l = 0x2a
005c: 3066 0f call 0x00c4

ここに到達する条件は0x0042における比較で *0x003a == r4 となった時にアクセス禁止エラーとなる。

003c: fd00 .. r15h = 0x00
003e: fc3a .: r15l = 0x3a
0040: 70ff p. r15 = *(word*)r15
0042: c04f .O r2 = r4==r15 ? 1 : r4<r15 ? 2 : 4
0044: 0e12 .. jump 0x0058 if r2:0

よってこの条件分岐を削除することでアクセス制限を回避することが出来ると考えられる。

制限2: BIOSメモリ領域へのアクセス制限

フラグへのアクセス制限を行っている場所がわかったので命令を書き換えてしまいたいが, BIOSのメモリ領域に対してはアプリケーション側からはアクセス制限がかかっており, アクセスしようとするとクラッシュしてしまう。

アクセス制限されている範囲を調べてみると0x0-0x359の範囲であることが分かるので, bios中にそれらしい即値がないかを調べてみると各所に0x35aという値を発見することが出来る。

000e: 1d03 .. r1h = 0x03
0010: 1c5a .Z r1l = 0x5a
0012: 0047 .G push.word r4
...

0072: 4d03 M. r4h = 0x03
0074: 4c5a LZ r4l = 0x5a
0076: 3b0d ;. r3 = 0x0d
0078: 0020 .  syscall
...

00e2: ed03 .. r14h = 0x03
00e4: ec5a .Z r14l = 0x5a
00e6: c04e .N r2 = r4==r14 ? 1 : r4<r14 ? 2 : 4
00e8: 1e22 ." jump 0x010c if r2:1
...

特に二番目の

0072: 4d03 M. r4h = 0x03
0074: 4c5a LZ r4l = 0x5a
0076: 3b0d ;. r3 = 0x0d
0078: 0020 .  syscall

はアクセス制限を設定するVM命令ではないかと疑われる。

ところで,ユーザモードでsyscallされた時にbiosの0番地から処理が実行されると仮定すると 0x03, 0x0a, 0x0f以外のsyscallはそのままVMを呼び出すようになっていると考えられる。

0016: fb03 .. r15 = 0x03
0018: c03f .? r2 = r3==r15 ? 1 : r3<r15 ? 2 : 4
001a: 0e20 .  jump 0x003c if r2:0
001c: fb0a .. r15 = 0x0a
001e: c03f .? r2 = r3==r15 ? 1 : r3<r15 ? 2 : 4
0020: 0e1a .. jump 0x003c if r2:0
0022: fb0f .. r15 = 0x0f
0024: c03f .? r2 = r3==r15 ? 1 : r3<r15 ? 2 : 4
0026: 0e3c .< jump 0x0064 if r2:0

先ほどのフラグへのアクセス制限の処理が0x003cから呼び出されていたことや, unameで用いられているsyscall(0x0f)がbiosに含まれる文字列を出力することからこれは正しい仮定であると思える。

実際,ユーザモードから r3 = 0x0d, r4 = 0 でsyscallを呼び出してみれば, BIOSのメモリ領域へのアクセス制限が予想通り解除できた。これで攻撃に必要なエクスプロイトが出揃った。

解法

  1. メモリ制限を解除

    4b00 r4 = 0
    3b0d r3 = 0x0d
    0020 syscall
  2. bios.binのflagへのアクセスチェック部分をNOPで潰す

    3d90 r3 = 0x9000
    3c00 
    4d00 r4 = 0x0044
    4c44 
    8034 *(word*)r4 = r3
  3. hexdump flag

次の順で送信することでフラグが得られる。

hexpaste
a
4d00 4c00 3b0d 0020
3d90 3c00 4d00 4c44 8034
0010
.
a
hexdump
flag

CTF/Writeup/SIGINT 2013/VM (last edited 2015-01-15 01:25:48 by ytoku)