リバースエンジニアリングへの道
出田 守です。
最近、情報セキュリティに興味を持ち、『リバースエンジニアリング-Pythonによるバイナリ解析技法』という本(以降、「教科書」と呼びます)を読みました。
「こんな世界があるのか!かっこいい!」と感動し、私も触れてみたいということでド素人からリバースエンジニアリングができるまでを書いていきたいと思います。
ちなみに、教科書ではPython言語が使用されているので私もPython言語と練習のためC言語を使用しています。
ここを見ていただいた諸先輩方からの意見をお待ちしております。
軌跡
環境
OS: Windows10 64bit Home (日本語)
CPU: Intel® Core™ i3-6006U CPU @ 2.00GHz × 1
メモリ: 2048MB
Python: 3.6.5
私の環境は、普段Ubuntu16.04を使っていますが、ここではWindows10 64bitを仮想マシン上で立ち上げております。
ちなみに教科書では、Windowsの32bitで紹介されています。
ソフトフックがやりたくて - その3
前回はprintf_loop.exeのアセンブリを解読してみました。
今回は関数呼び出しを行うアプリケーションにソフトフックをかけてみます。初っ端から飛ばしすぎた感じがあるのでまずは簡単そうなものからソフトフックをかけてみます。
覚書
アセンブリを眺めている中で学んだことを書いていきます。
WORD
今更なのですが、今までWORDのサイズがすべてのCPUで16bitだと思っていました。
『大熱血!アセンブラ入門』を読んでいたところ、他の32bitCPUではWORDが32bitになっておりました。
ただ、インテルのCPUでは16bitでした(ですよね?)。これは昔の名残りだそうです。
WORDのサイズはCPUが自然に処理できるデータサイズだそうです。
gs
mov rax,QWORD PTR gs:0x30 # rax=QWORD PTR gs:0x30
いつものmov命令ですが、gsというセグメントを見かけました。調べてみると、スレッド固有のメモリにアクセスする際のデータセグメントとのことです。
lock cmpxchg
lock cmpxchg QWORD PTR [rip+0x205a],rbx
CMPXCHGはこちらのページの内容を引用します。
http://softwaretechnique.jp/OS_Development/Tips/IA32_Instructions/CMPXCHG.html
オペランドのサイズに従ってAL、AX、EAXの値を第1オペランド(格納先)と比較します。2つの値が等しい場合は第2オペランド(読み込み元)が第1オペランド(格納先)で指定したメモリにロードされます。等しくない場合は第1オペランド(格納先)がAL、AX、EAXにロードされます。
つまるところ、
if QWORD PTR [rip+0x205a]==rbx:
QWORD PTR [rip+0x205a] = rbx
else:
RAX = QWORD PTR [rip+0x205a]
こういうことでしょうか。
次にlockプレフィクスです。これは命令をアトミック操作したい場合に付与するそうです。アトミック操作とはすべての操作が完了するまで、他のシステムに割り込ませないことだそうです(参照:https://ja.wikipedia.org/wiki/不可分操作)。つまりlockはその名のとおり操作が完了するまでメモリをロックするということですね。
フックされるもの
まず今回のフック対象を載せておきます。
# include <stdio.h>
void say_hello(void) {
printf("Hello!\n");
}
void dummy_say_hello(void) {
puts("Hello... Who called me?");
}
int main(int argc, char *argv[]) {
for (;;) {
say_hello();
Sleep(1*1000);
}
return (0);
}
main関数のループの中でsay_hello関数を呼び出します。アセンブリ解読の練習のために違う関数を使ってみました(あまり意味が無いかもしれませんが笑)。
今回はmain関数でdummy_say_hello関数を呼び出せるようにソフトフックをかけてみます。
アセンブリ解読
フックをかけるためにアセンブリを解読していきます。
call_function.exe: ファイル形式 pei-x86-64
call_function.exe
アーキテクチャ: i386:x86-64, フラグ 0x0000012f:
HAS_RELOC, EXEC_P, HAS_LINENO, HAS_DEBUG, HAS_LOCALS, D_PAGED
開始アドレス 0x0000000140011023
固有 0x22
executable
large address aware
Time/Date Thu Nov 1 11:03:41 2018
Magic 020b (PE32+)
MajorLinkerVersion 14
MinorLinkerVersion 15
SizeOfCode 00007c00
SizeOfInitializedData 00007400
SizeOfUninitializedData 00000000
AddressOfEntryPoint 0000000000011023
BaseOfCode 0000000000001000
ImageBase 0000000140000000
SectionAlignment 0000000000001000
FileAlignment 0000000000000200
MajorOSystemVersion 6
MinorOSystemVersion 0
MajorImageVersion 0
MinorImageVersion 0
MajorSubsystemVersion 6
MinorSubsystemVersion 0
Win32Version 00000000
SizeOfImage 00025000
SizeOfHeaders 00000400
CheckSum 00000000
Subsystem 00000003 (Windows CUI)
DllCharacteristics 00008160
SizeOfStackReserve 0000000000100000
SizeOfStackCommit 0000000000001000
SizeOfHeapReserve 0000000000100000
SizeOfHeapCommit 0000000000001000
LoaderFlags 00000000
NumberOfRvaAndSizes 00000010
ImageBaseが0x140000000で、EntryPointが0x11023となっています。つまり開始アドレスが0x140011023です。
では開始アドレスのアセンブリを見ていきます。
140011023: e9 58 13 00 00 jmp 0x140012380
...
140012050: 48 83 ec 28 sub rsp,0x28
140012054: e8 e5 f2 ff ff call 0x14001133e
140012059: e8 12 00 00 00 call 0x140012070
14001205e: 48 83 c4 28 add rsp,0x28
...
140012380: 48 83 ec 28 sub rsp,0x28
140012384: e8 c7 fc ff ff call 0x140012050
140012389: 48 83 c4 28 add rsp,0x28
前回と同じようなところは一気に載せました。まず、0x140011023ではジャンプするだけです。その先の0x140012380も0x140012050をcallするだけです。その先の0x14001133eのcallは前回紹介したsecurity_cookieの初期化です。
140012070: 48 83 ec 68 sub rsp,0x68
140012074: b9 01 00 00 00 mov ecx,0x1
140012079: e8 9d f2 ff ff call 0x14001131b
14001207e: 0f b6 c0 movzx eax,al
140012081: 85 c0 test eax,eax
140012083: 75 0a jne 0x14001208f
140012085: b9 07 00 00 00 mov ecx,0x7
14001208a: e8 fb f1 ff ff call 0x14001128a
...
140012199: e8 22 01 00 00 call 0x1400122c0 # <= here
14001219e: 89 44 24 28 mov DWORD PTR [rsp+0x28],eax
...
140012209: 48 83 c4 68 add rsp,0x68
14001220d: c3 ret
そして0x140012070から先もしばらくsecurityに関するライブラリの初期化が続きます。だいたい半ば辺りでmain関数を呼び出す準備をしているようです。
1400122c0: 48 83 ec 38 sub rsp,0x38
1400122c4: e8 9b ed ff ff call 0x140011064
1400122c9: 48 89 44 24 20 mov QWORD PTR [rsp+0x20],rax
1400122ce: e8 d6 ee ff ff call 0x1400111a9
1400122d3: 48 89 44 24 28 mov QWORD PTR [rsp+0x28],rax
1400122d8: e8 71 ef ff ff call 0x14001124e
1400122dd: 48 8b 4c 24 20 mov rcx,QWORD PTR [rsp+0x20]
1400122e2: 4c 8b c1 mov r8,rcx
1400122e5: 48 8b 4c 24 28 mov rcx,QWORD PTR [rsp+0x28]
1400122ea: 48 8b 11 mov rdx,QWORD PTR [rcx]
1400122ed: 8b 08 mov ecx,DWORD PTR [rax]
1400122ef: e8 a1 ee ff ff call 0x140011195
1400122f4: 48 83 c4 38 add rsp,0x38
1400122f8: c3 ret
1-3行目
1行目はrspから56byte減算し、領域を確保しています。
2行目で0x140011064をcallしています。これはdumpbinを見ると_get_initial_narrow_environment関数だそうです。
3行目は戻り値raxをQWORD PTR [rsp+0x20]に格納しています。
4-5行目
4行目で0x1400111a9をcallしています。これはdumpbinを見ると__p___argv関数だそうです。
5行目は戻り値raxをQWORD PTR [rsp+0x28]に格納しています。
6-14行目
6行目で0x14001124eをcallしています。これはdumpbinを見ると__p___argc関数だそうです。
7-8行目はQWORD PTR [rsp+0x20]をrcxに格納し、rcxをr8に格納しています。
9行目はrcxにQWORD PTR [rsp+0x28]を格納しています。
10-11行目はrdxにQWORD PTR [rcx]、ecxにDWORD PTR [rax]を格納しています。ここまで引数の準備でしょうか。
12行目は0x140011195をcallしています。恐らくこれがmain関数でしょう。
13-14行目は確保した領域を解放し、リターンしています。
では12行目のcall先0x140011195を見てみます。
140011195: e9 e6 06 00 00 jmp 0x140011880
...
140011880: 48 89 54 24 10 mov QWORD PTR [rsp+0x10],rdx
140011885: 89 4c 24 08 mov DWORD PTR [rsp+0x8],ecx
140011889: 55 push rbp
14001188a: 57 push rdi
14001188b: 48 81 ec e8 00 00 00 sub rsp,0xe8
140011892: 48 8d 6c 24 20 lea rbp,[rsp+0x20]
140011897: 48 8b fc mov rdi,rsp
14001189a: b9 3a 00 00 00 mov ecx,0x3a
14001189f: b8 cc cc cc cc mov eax,0xcccccccc
1400118a4: f3 ab rep stos DWORD PTR es:[rdi],eax
1400118a6: 8b 8c 24 08 01 00 00 mov ecx,DWORD PTR [rsp+0x108]
1400118ad: 48 8d 0d 4f f7 00 00 lea rcx,[rip+0xf74f] # 0x140021003
1400118b4: e8 c9 f7 ff ff call 0x140011082
1400118b9: e8 d2 f8 ff ff call 0x140011190
1400118be: b9 e8 03 00 00 mov ecx,0x3e8
1400118c3: e8 da fa ff ff call 0x1400113a2
1400118c8: eb ef jmp 0x1400118b9
1400118ca: 33 c0 xor eax,eax
1400118cc: 48 8d a5 c8 00 00 00 lea rsp,[rbp+0xc8]
1400118d3: 5f pop rdi
1400118d4: 5d pop rbp
1400118d5: c3 ret
まず0x140011195はさらに0x140011880へジャンプしていました。
0x140011880から見ていきます。
1-5行目
1-2行目はrspのそれぞれ+0x10と+0x8のアドレス先にrdx, ecxをコピーしています。rdxはQWORD PTR [rcx]を、ecxはDWORD PTR [rax]をmain関数呼び出し前に格納されました。恐らくmain関数の引数としてのargcとargvでしょうか。ただ、rspの領域を確保する前にrspの指す先に格納しているみたいですが、呼び出し元の関数の領域に上書いたりしないのでしょうか。
イメージですが関数を呼び出した時点で確か戻りアドレスがスタックに格納されたと思います。そしてrdx、ecxはその戻りアドレスと呼び出し元のスタックフレーム内の値を上書がかないように呼び出し元のスタックフレーム領域にmain関数の引数として格納しているのではないでしょうか。つまり呼び出し元のスタック領域を広めにとっていたのかもしれません。すみません、あくまで推測です。
3-5行目はスタックにrbp、rdiをpushして退避させています。その後、rspを0xe8(232Byte)減算して領域を確保しています。
6-13行目
6行目はrbpに[rsp+0x20]のアドレスをコピー。
7行目はrdiにrspをコピー。
8行目はecxに0x3aをコピー。
9行目はeaxに0xccccccccをコピー。
10行目はes:[rdi]に0x3a(58)回eaxをコピー。
11行目はecxにDWORD PTR [rsp+0x108]をコピー。
12行目はrcxに[rip+0xf74f]のアドレスをコピー。
13行目で0x140011082をcall。これは前回も出てきた__CheckForDebuggerJustMyCode関数です。
14行目
14行目で0x140011190をcall。これがsay_hello関数ですね。
15-17行目
15行目はecxに0x3e8(1000)をコピー。Sleep関数の引数ですね。
16行目で0x1400113a2をcall。Sleep関数でしょう。
17行目で0x1400118b9(14行目)へjmp。
18-22行目
18行目はeax同士をxorしています。
19行目はrspに[rbp+0xc8]をコピー。rspの復元でしょうか。
20行目はrdiをpop。
21行目はrbpをpop。
22行目でreturn。
dummy_say_hello関数のアドレス
say_hello関数のアドレスが分かったので、次はdummy_say_hello関数のアドレスを調べます。
ちょっとせこいですが、dumpbinの出力のfunction tableを見ちゃいます。
Function Table (617)
Begin End Info Function Name
...
00001818 00011830 00011871 0001B190 dummy_say_hello
...
どうやらBegin-(End-1)までがdummy_say_hello関数のようです。
140011830: 40 55 rex push rbp
140011832: 57 push rdi
140011833: 48 81 ec e8 00 00 00 sub rsp,0xe8
14001183a: 48 8d 6c 24 20 lea rbp,[rsp+0x20]
14001183f: 48 8b fc mov rdi,rsp
140011842: b9 3a 00 00 00 mov ecx,0x3a
140011847: b8 cc cc cc cc mov eax,0xcccccccc
14001184c: f3 ab rep stos DWORD PTR es:[rdi],eax
14001184e: 48 8d 0d ae f7 00 00 lea rcx,[rip+0xf7ae] # 0x140021003
140011855: e8 28 f8 ff ff call 0x140011082
14001185a: 48 8d 0d d7 83 00 00 lea rcx,[rip+0x83d7] # 0x140019c38
140011861: ff 15 99 ea 00 00 call QWORD PTR [rip+0xea99] # 0x140020300
140011867: 48 8d a5 c8 00 00 00 lea rsp,[rbp+0xc8]
14001186e: 5f pop rdi
14001186f: 5d pop rbp
140011870: c3 ret
ソフトフック
だいたい流れが分かったので、ソフトフックができそうです。
今回はmain関数から呼ばれるsay_hello関数をdummy_say_hello関数に変えようという試みです。
実際にデバッガでこれらのアドレスを調べたところ以下になりました。
say_hello関数=0x7FF68B5D1190(jmp 0x7FF68B5D19D0)
dummy_say_hello関数=0x7FF68B5D1262(jmp 7FF68B5D1830)
アドレス(jmp アドレス)の意味は前のアドレスにジャンプ後すぐに後ろのアドレスにジャンプするという意味です。
下4桁は調べた下4桁と同じようです。
フックするソースコードのmainは以下です。
# 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 hook(HANDLE h_process, HANDLE h_thread) {
CONTEXT ct;
SIZE_T count = 0;
ct.ContextFlags = CONTEXT_FULL;
get_thread_context(h_thread, &ct);
printf("Rip=0x%016llX\n", ct.Rip);
ct.Rip = 0x7FF68B5D1830;
set_thread_context(h_thread, &ct);
//input_pid(); // for debug
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;
LONG64 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();
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 = 0x7ff68b5d1190;
count = read_process_memory(h_process, address, &orig_byte, sizeof(orig_byte));
if (!count) {
goto quit;
}
if (set_sw_bp(h_process, address, &read_buf)) {
goto quit;
}
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();
goto quit;
}
tid = de.dwThreadId;
hook(h_process, h_thread);
//if (sw_bp_post_proc(h_process, h_thread, address, &orig_byte)) {
// goto quit;
//}
//if (switch_TF(h_thread)) {
// goto quit;
//}
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();
goto quit;
}
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");
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;
}
}
quit:
//sw_bp_post_proc(h_process, h_thread, address, &orig_byte);
DebugActiveProcessStop(pid);
CloseHandle(h_process);
for (;;) {
}
return (0);
}
前回と仕組みはほとんど変わりません。
say_hello関数を呼び出す0x140011190のところでブレークポイントを仕込み、ripをdummy_say_hello関数のアドレスに変更してみました。こうすることで次の命令がdummy_say_hello関数へ飛ばされるということです。今回は一度フックをかければsay_hello関数を実行することはないので、ブレークポイントは元に戻さずシングルステップモードも不要でした。
実行されないはずの関数を実行してみたいという欲望から今回のフックを試してみました。今回はdummy_say_hello関数の場所をdumpbinで見ました。本当はdumpbinに頼らず、PEフォーマットから解読したいですね。
では今回は以上です。
まとめ
- WORDサイズはCPUによって違う!(いまさら笑)
- lockプレフィクスでアトミック操作
- 静的解析で調べたアドレスの下4桁は実行時の下4桁と同じ?