4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-11-02

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

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

軌跡

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

環境

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

asm
mov    rax,QWORD PTR gs:0x30       # rax=QWORD PTR gs:0x30

いつものmov命令ですが、gsというセグメントを見かけました。調べてみると、スレッド固有のメモリにアクセスする際のデータセグメントとのことです。

lock cmpxchg

asm
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にロードされます。

つまるところ、

python
if QWORD PTR [rip+0x205a]==rbx:
	QWORD PTR [rip+0x205a] = rbx
else:
	RAX = QWORD PTR [rip+0x205a]

こういうことでしょうか。

次にlockプレフィクスです。これは命令をアトミック操作したい場合に付与するそうです。アトミック操作とはすべての操作が完了するまで、他のシステムに割り込ませないことだそうです(参照:https://ja.wikipedia.org/wiki/不可分操作)。つまりlockはその名のとおり操作が完了するまでメモリをロックするということですね。

フックされるもの

まず今回のフック対象を載せておきます。

call_function.c
# 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_info.txt
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です。

では開始アドレスのアセンブリを見ていきます。

call_function_asm.txt
   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の初期化です。

call_function_asm.txt
   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関数を呼び出す準備をしているようです。

call_function_asm.txt
   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を見てみます。

call_function_asm.txt
   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を見ちゃいます。

call_function_info_dumpbin.txt
Function Table (617)

           Begin    End      Info      Function Name

  ...
  00001818 00011830 00011871 0001B190  dummy_say_hello
  ...

どうやらBegin-(End-1)までがdummy_say_hello関数のようです。

call_function_asm.txt
   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は以下です。

hook2.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 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フォーマットから解読したいですね。
では今回は以上です。

まとめ

  1. WORDサイズはCPUによって違う!(いまさら笑)
  2. lockプレフィクスでアトミック操作
  3. 静的解析で調べたアドレスの下4桁は実行時の下4桁と同じ?
4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?