リバースエンジニアリングへの道 - その22

# リバースエンジニアリングへの道
出田 守です。
最近、情報セキュリティに興味を持ち、『リバースエンジニアリング-Pythonによるバイナリ解析技法』という本(以降、「教科書」と呼びます)を読みました。
「こんな世界があるのか！かっこいい！」と感動し、私も触れてみたいということでド素人からリバースエンジニアリングができるまでを書いていきたいと思います。
ちなみに、教科書ではPython言語が使用されているので私もPython言語を使用しています。
ここを見ていただいた諸先輩方からの意見をお待ちしております。

## 軌跡
[リバースエンジニアリングへの道 - その21](https://qiita.com/deta-mamoru/items/74214cfb1677405d1dd6)

## 環境
OS: Windows10 64bit Home (日本語)
CPU: Intel® Core™ i3-6006U CPU @ 2.00GHz × 1
メモリ: 2048MB
Python: 3.6.5

私の環境は、普段Ubuntu16.04を使っていますが、ここではWindows10 64bitを仮想マシン上で立ち上げております。
ちなみに教科書では、Windowsの32bitで紹介されています。

## ソフトフックがやりたくて - その1
[前回](https://qiita.com/deta-mamoru/items/74214cfb1677405d1dd6)はさらっと基本の所だけPEフォーマットを学びました。
今回は長らく教科書から遠ざかっていましたがここで一旦教科書に戻って、フックについて学びます。
以前よりフックが一つの目標でした。フックを知ったときは「マジか！」と電車でボソッと言ってしまったほど、私は惹きつけられました。
時間がかかりましたが今回からフックを学んでいきます。

### フックとは
既存の処理の流れに割り込んで、独自の処理を追加することのようです。
フックはソフトウェアだけでなく、ハードウェア、ミドルウェアなどにも当然適用できます。今回はソフトウェアフックについて学びます。
例えば改変自由なソフトウェアに対して、「この機能を入れたい！」ってということがあれば、ある処理の途中で自分が作成した独自の処理を入れ込ませることができるということですね！

### フック体験1
教科書ではPyDbgを使用してフックを行っているようです。PyDbgを調べてみるとちょっと古くて更新もされていなさそうです。
なので復習もかねて再度Windows API関数をごりごり使いながらフックを実現していきます。
まずは教科書にならって単純なループプログラムに対してのフックをしてみます。

#### フックされてしまうプログラム
```py:print_loop.py
import time
import os

from ctypes  import *

msvcrt = cdll.msvcrt

i   = 0
pid = os.getpid()
while True:
    msvcrt.wprintf("[%d]Loop iteration %d!\n", pid, i)
    i += 1
    time.sleep(1)
```

#### フックしてしまうプログラム
```py:hook1.py
import random

from ctypes       import *

from defines      import *
from func_resolve import func_resolve
from memory       import write_process_memory, read_process_memory, set_sw_bp, sw_bp_postproc
from thread       import open_thread, get_thread_context, set_thread_context

kernel32 = windll.kernel32

def show_error(h_process=None, pid=None):
    print(WinError(GetLastError()))
    if pid:
        kernel32.DebugActiveProcessStop(pid)
    if h_process:
        kernel32.CloseHandle(h_process)
    exit(1)

def hook_proc(h_process, tid):
    h_thread = open_thread(tid)
    if not h_thread:
        return False
    context = get_thread_context(h_thread)
    if not context:
        return False
    # print("[Rip]0x{:016X}".format(context.Rip))
    # print("[Rax]0x{:016X}".format(context.Rax))
    # print("[Rcx]0x{:016X}".format(context.Rcx))
    # print("[Rdx]0x{:016X}".format(context.Rdx))
    # print("[Rbx]0x{:016X}".format(context.Rbx))
    # print("[Rsp]0x{:016X}".format(context.Rsp))
    # print("[Rbp]0x{:016X}".format(context.Rbp))
    # print("[Rsi]0x{:016X}".format(context.Rsi))
    # print("[Rdi]0x{:016X}".format(context.Rdi))
    print("[R8]0x{:016X}".format(context.R8))
    context.R8 = random.randint(0, 1000)
    set_thread_context(h_thread, context)
    return True

pid = int(input("pid: "))
# open process
h_process = kernel32.OpenProcess(PROCESS_ALL_ACCESS, False, pid)
if not h_process:
    show_error()
# attach process
if not kernel32.DebugActiveProcess(pid):
    show_error(h_process=h_process)

sw_bps      = {}
address     = func_resolve("msvcrt.dll", b"wprintf")
debug_event = DEBUG_EVENT()
first_break = False # windows break
counter     = 0
while True:
    # set breakpoint
    if not set_sw_bp(h_process, address, sw_bps):
        show_error(h_process=h_process, pid=pid)
    if kernel32.WaitForDebugEvent(byref(debug_event), INFINITE):
        # print(debug_event.dwDebugEventCode)
        exception_record = debug_event.u.Exception.ExceptionRecord
        if debug_event.dwDebugEventCode==1 and exception_record.ExceptionCode==EXCEPTION_BREAKPOINT: # exception breakpoint
            if first_break:
                hook_proc(h_process, debug_event.dwThreadId)
                sw_bp_address = sw_bp_postproc(h_process, debug_event.dwThreadId, sw_bps, exception_record.ExceptionAddress)
                if not sw_bp_address:
                    show_error(h_process=h_process, pid=pid)
                print("counter = {}".format(counter))
                counter += 1
            else:
                first_break = True
        kernel32.ContinueDebugEvent(debug_event.dwProcessId,
                                    debug_event.dwThreadId,
                                    DBG_CONTINUE)
        if counter>=5:
            break
kernel32.DebugActiveProcessStop(pid)
kernel32.CloseHandle(h_process)
```
最初のブレークポイントはWindowsが標準で例外を発生させるのでスキップしました(first_break)。

```py:memory.py
...
def sw_bp_postproc(h_process, tid, sw_bps, exception_address):
    break_info = [info for info in sw_bps.values() if info[0]==exception_address] # address and original_byte at break point
    break_idx = [k for k, info in sw_bps.items() if info[0]==exception_address]
    if not break_info:
        return False
    break_info = break_info[0]
    h_thread = open_thread(tid)
    if not h_thread:
        return False
    context = get_thread_context(h_thread)
    if not context:
        return False
    if not write_process_memory(h_process, cast(break_info[0], POINTER(BYTE)), bytes.fromhex(break_info[1])):
        return False
    context.Rip    -= 0x1
    # context.EFlags |= 1<<8
    if not set_thread_context(h_thread, context):
        return False
    context = get_thread_context(h_thread)
    if not context:
        return False
    for k in break_idx:
        sw_bps.pop(k)
    return break_info[0]
```
sw_bp_postprocでは、ブレークポイントを一度解除して通常の処理を続行させています。

(私自身忘れていたので...)前回までの記事を参考に他の処理などを作成しました。

流れとしては、print_loop.pyが実行されて、iをインクリメントしながら表示させている箇所にフックをし、0-1000までのランダムな数値を書き込み表示させているという感じですね。これがフックといえるのかどうか分かりませんが、表示結果を見ていると嬉しいし楽しいですね！
ただ正直だいぶ苦労しました。教科書ではprintf関数を呼び出す際、ESP(つまりスタック)から引数にアクセスしているのですが、私の環境ではR8、rdx(edx)、rcxの順にprintf関数の引数を格納していました。このパターンのことをすっかり忘れていました私は、printf関数のアドレスが実は違うのか、スタックがちゃんととれていないのかなど結構悩んだ末に以上のこと思い出しました。これで今後おそらくこのことは忘れないでしょう。笑

Cでも書けるようになりたいので、勉強のためCバージョンも作成します。

#### フックされてしまうプログラム(C)

```c:printf_loop.c
#include <stdio.h>
#include <windows.h>
#include <process.h>

int main(void) {
	int i   = 0;
	int pid = 0;
	pid = _getpid();
	while (1) {
		printf("[%d]Loop iteration %d!\n", pid, i);
		i++;
		Sleep(1*1000);
	}
}
```

#### フックされてしまうプログラム

```c:hook1.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <windows.h>
#include <tlhelp32.h>
#include "memory.h"
#include "module.h"
#include "privilege.h"
#include "thread.h"
#include "util.h"

int usage(char *command) {
	puts("usage");
	printf("%s <dll name> <function name>\n", command);

	return (0);
}

int hook(HANDLE h_process, HANDLE h_thread) {
	CONTEXT ct;
	SIZE_T  count = 0;

	ct.ContextFlags = CONTEXT_FULL;
	get_thread_context(h_thread, &ct);
	printf("Rsp=0x%016llX\n", ct.Rsp);
	// char   buf[BUF_SIZE] = { 0x09 };
	int   buf = rand();
	count = write_process_memory(h_process, (LPVOID)(ct.Rsp + 0x1BF8), &buf, sizeof(buf));
	//count = write_process_memory(h_process, (LPVOID)(ct.Rsp + 0x268), &buf, sizeof(buf));
	if (!count) {
		return (1);
	}
	printf("count=%zd\n", count);
	return (0);
}

int main(int argc, char *argv[]) {
	int         pid = -1;
	int         first_break = 0;
	int         quit = 0;
	int         dwStatus = 0;
	int         ret = 0;
	HANDLE      h_process;
	HANDLE      h_thread;
	FARPROC     address;
	DEBUG_EVENT de;
	char        orig_byte[BUF_SIZE] = {0};
	char        read_buf[BUF_SIZE] = {0};
	SIZE_T      count = 0;
	DWORD       tid = 0;


	//printf("BEFORE\n");
	//show_privileges();
	ret = set_debug_privilege("seDebugPrivilege");
	if (ret) {
		//printf("ret=%d\n", ret);
		show_error();
		return (1);
	}
	//printf("AFTER\n");
	//show_privileges();

	// check arguments
	if (argc <= 3) {
		usage(argv[0]);
		return (1);
	}

	srand((unsigned)time(NULL));

	pid = input_pid();
	h_process = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); // get process handle
	if (!h_process) {
		show_error();
		return (1);
	}
	if (!DebugActiveProcess(pid)) { // attach
		CloseHandle(h_process);
		show_error();
		return (1);
	}
	/* printf("attached\n"); */
	// address = module_address("ucrtbased.dll", "__stdio_common_vfprintf");
	address = module_address("KERNEL32.DLL", "WriteFile");
	if (!address) {
		return (1);
	}
	count = read_process_memory(h_process, address, &orig_byte, sizeof(orig_byte));
	if (!count) {
		return (1);
	}
	if (set_sw_bp(h_process, address, &read_buf)) {
		return (1);
	}
	for (;;) {
		if (!WaitForDebugEvent(&de, INFINITE)) {
			break;
		}
		dwStatus = DBG_EXCEPTION_NOT_HANDLED;
		printf("%d\n", de.dwDebugEventCode);
		switch (de.dwDebugEventCode) {
		case EXCEPTION_DEBUG_EVENT:
			/* printf("  %d\n", de.u.Exception.ExceptionRecord.ExceptionCode); */
			switch (de.u.Exception.ExceptionRecord.ExceptionCode) {
			case EXCEPTION_BREAKPOINT:
				if (!first_break) {
					first_break = 1;
				}
				else {
					//printf("%d==%d\n", de.dwProcessId, pid);
					if (de.dwProcessId == pid) {
						//puts("here1");
						h_thread = OpenThread(THREAD_ALL_ACCESS, FALSE, de.dwThreadId);
						if (!h_thread) {
							show_error();
							return (1);
						}
						tid = de.dwThreadId;
						hook(h_process, h_thread);
						if (sw_bp_post_proc(h_process, h_thread, address, &orig_byte)) {
							return (1);
						}
						if (switch_TF(h_thread)) {
							return (1);
						}
						dwStatus = DBG_CONTINUE;
						CloseHandle(h_thread);
						//quit = 1;
					}
				}
				break;
			case EXCEPTION_SINGLE_STEP:
				//printf("%d==%d\n", de.dwProcessId, pid);
				if (de.dwProcessId == pid) {
					//puts("here2");
					h_thread = OpenThread(THREAD_ALL_ACCESS, FALSE, de.dwThreadId);
					if (!h_thread) {
						show_error();
						return (1);
					}
					tid = de.dwThreadId;
					set_sw_bp(h_process, address, &read_buf);
					/* switch_TF(h_thread); */
					dwStatus = DBG_CONTINUE;
					CloseHandle(h_thread);
					//quit = 1;
				}
				break;
			case EXCEPTION_ACCESS_VIOLATION:
				printf("EXCEPTION_ACCESS_VIOLATION\n");
				break;
			case EXCEPTION_DATATYPE_MISALIGNMENT:
				printf("EXCEPTION_DATATYPE_MISALIGNMENT\n");
				break;
			case EXCEPTION_ARRAY_BOUNDS_EXCEEDED:
				printf("EXCEPTION_ARRAY_BOUNDS_EXCEEDED\n");
				break;
			case EXCEPTION_FLT_DENORMAL_OPERAND:
				printf("EXCEPTION_FLT_DENORMAL_OPERAND\n");
				break;
			case EXCEPTION_FLT_DIVIDE_BY_ZERO:
				printf("EXCEPTION_FLT_DIVIDE_BY_ZERO\n");
				break;
			case EXCEPTION_FLT_INEXACT_RESULT:
				printf("EXCEPTION_FLT_INEXACT_RESULT\n");
				break;
			case EXCEPTION_FLT_INVALID_OPERATION:
				printf("EXCEPTION_FLT_INVALID_OPERATION\n");
				break;
			case EXCEPTION_FLT_OVERFLOW:
				printf("EXCEPTION_FLT_OVERFLOW\n");
				break;
			case EXCEPTION_FLT_STACK_CHECK:
				printf("EXCEPTION_FLT_STACK_CHECK\n");
				break;
			case EXCEPTION_FLT_UNDERFLOW:
				printf("EXCEPTION_FLT_UNDERFLOW\n");
				break;
			case EXCEPTION_INT_DIVIDE_BY_ZERO:
				printf("EXCEPTION_INT_DIVIDE_BY_ZERO\n");
				break;
			case EXCEPTION_INT_OVERFLOW:
				printf("EXCEPTION_INT_OVERFLOW\n");
				break;
			case EXCEPTION_PRIV_INSTRUCTION:
				printf("EXCEPTION_PRIV_INSTRUCTION\n");
				break;
			case EXCEPTION_IN_PAGE_ERROR:
				printf("EXCEPTION_IN_PAGE_ERROR\n");
				break;
			case EXCEPTION_ILLEGAL_INSTRUCTION:
				printf("EXCEPTION_ILLEGAL_INSTRUCTION\n");
				break;
			case EXCEPTION_NONCONTINUABLE_EXCEPTION:
				printf("EXCEPTION_NONCONTINUABLE_EXCEPTION\n");
				break;
			case EXCEPTION_STACK_OVERFLOW:
				printf("EXCEPTION_STACK_OVERFLOW\n");
				break;
			case EXCEPTION_INVALID_DISPOSITION:
				printf("EXCEPTION_INVALID_DISPOSITION\n");
				break;
			case EXCEPTION_GUARD_PAGE:
				printf("EXCEPTION_GUARD_PAGE\n");
				break;
			case EXCEPTION_INVALID_HANDLE:
				printf("EXCEPTION_INVALID_HANDLE\n");
				break;
			}
			break;
		}
		if (quit) {
			break;
		}
		if (!ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwStatus)) {
			break;
		}
	}
	DebugActiveProcessStop(pid);
	CloseHandle(h_process);
	for (;;) {
	}
	return (0);
}
```

やっていることはそこまでPython版と違いはありませんので、メインのモジュールだけ載せます。
Cが初心者レベルなので、不審な点が多いでしょうが一応動作します。
今回混乱したのはVisualStudioをインストールした際に付いてくるコンパイラのcl.exeを使用してコマンドから被フックプログラム(printf_loop.c)をコンパイルすると、printf関数が内部でWriteFile関数に置き換わっていることが分かりました。一方でVisualStudioからコンパイルした際は\_\_stdio_common_vfprintf関数に置き換わっていることが分かりました。この違いは恐らく通常は\_\_stdio_common_vfprintf関数に置き換わるのですが、cl.exeを使用してコンパイルする際にucrtbased.dllをリンク指定しなかったことが原因ではないかと思っております。
Python版ではprintf関数にソフトウェアブレークポイントを設定し、フックしていました。今回は上記のことがあり、WriteFile関数または\_\_stdio_common_vfprintf関数にフックをかましてやりました。笑
hook関数のct.Rsp+0xXXX(XXXには数値が入る)はVisualStudioのデバッグ機能を使ってアセンブリをちまちま調べた結果です。WriteFile関数の場合と\_\_stdio_common_vfprintf関数の場合でオフセットが違います。また、WriteFile関数の場合は変数iを直接書き換えるので、以降iの変数は書き換わったままになります。本当は表示するときの数値だけを変えたかったのですがアセンブリから今の私の実力では見つけられませんでした。

フックの学習を通してWindows環境でのCプログラミングやアセンブリとのにらめっこなど貴重な体験ができました。目的の変数を見つけられた時やフックが成功したときの嬉しさは尋常ではないです。ここまできてやっとスタートライン手前近くまで来られたという感じがします。
今回は被フックプログラムを自作したのでどういう流れでどういう変数が使用されているのかある程度把握できました。本当はソースコードがないアプリケーションを流れも変数もアセンブリから割り出すのでしょう。そこら辺の体験もしていきたいですね。次回ももう少しフックについて学習すると思います。

## まとめ
1. Windows環境ではVisualStudioをインストールするとcl.exeというコンパイラが付いてくる。
2. コンパイルするとprintf関数が置き換わる。
3. フックが楽しい。

## 参考URL
参考になりましたページのURLです。本当にありがとうございました。
http://d.hatena.ne.jp/s-kita/20110409/1302337508
https://qiita.com/tanakah/items/226336e0e7aa2ecdc9c0