リバースエンジニアリングへの道
出田 守です。
最近、情報セキュリティに興味を持ち、『リバースエンジニアリング-Pythonによるバイナリ解析技法』という本(以降、「教科書」と呼びます)を読みました。
「こんな世界があるのか!かっこいい!」と感動し、私も触れてみたいということでド素人からリバースエンジニアリングができるまでを書いていきたいと思います。
ちなみに、教科書ではPython言語が使用されているので私もPython言語を使用しています。
ここを見ていただいた諸先輩方からの意見をお待ちしております。
軌跡
環境
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
前回はさらっと基本の所だけPEフォーマットを学びました。
今回は長らく教科書から遠ざかっていましたがここで一旦教科書に戻って、フックについて学びます。
以前よりフックが一つの目標でした。フックを知ったときは「マジか!」と電車でボソッと言ってしまったほど、私は惹きつけられました。
時間がかかりましたが今回からフックを学んでいきます。
フックとは
既存の処理の流れに割り込んで、独自の処理を追加することのようです。
フックはソフトウェアだけでなく、ハードウェア、ミドルウェアなどにも当然適用できます。今回はソフトウェアフックについて学びます。
例えば改変自由なソフトウェアに対して、「この機能を入れたい!」ってということがあれば、ある処理の途中で自分が作成した独自の処理を入れ込ませることができるということですね!
フック体験1
教科書ではPyDbgを使用してフックを行っているようです。PyDbgを調べてみるとちょっと古くて更新もされていなさそうです。
なので復習もかねて再度Windows API関数をごりごり使いながらフックを実現していきます。
まずは教科書にならって単純なループプログラムに対してのフックをしてみます。
フックされてしまうプログラム
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)
フックしてしまうプログラム
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)。
...
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)
# 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);
}
}
フックされてしまうプログラム
# 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プログラミングやアセンブリとのにらめっこなど貴重な体験ができました。目的の変数を見つけられた時やフックが成功したときの嬉しさは尋常ではないです。ここまできてやっとスタートライン手前近くまで来られたという感じがします。
今回は被フックプログラムを自作したのでどういう流れでどういう変数が使用されているのかある程度把握できました。本当はソースコードがないアプリケーションを流れも変数もアセンブリから割り出すのでしょう。そこら辺の体験もしていきたいですね。次回ももう少しフックについて学習すると思います。
まとめ
- Windows環境ではVisualStudioをインストールするとcl.exeというコンパイラが付いてくる。
- コンパイルするとprintf関数が置き換わる。
- フックが楽しい。
参考URL
参考になりましたページのURLです。本当にありがとうございました。
http://d.hatena.ne.jp/s-kita/20110409/1302337508
https://qiita.com/tanakah/items/226336e0e7aa2ecdc9c0