HSP3のチート対策いろいろ -メモリハック編-

#HSPのチート対策いろいろ -メモリハック編-
本記事では、HSP3を使ったWindows向けゲームの簡単なチート対策について説明します。今回サンプルとして使用したゲームはHSP3付属の洞窟探検ゲーム(game/doukutu.hsp)です。

##<i class="fa fa-cubes"></i>概要
###<i class="fa fa-cube"></i>実行環境
今回はHSPにしては低レイヤなところを触ります。時間が無かったこともありほとんどデバッグできていないのでバグだらけなのは確実なのでご了承ください。ということで、実行環境の重要な部分は明記しておきます。
OS : Windows 10(64bit)
HSP : v3.4 (32bit)
正直Windows 10でしか動かないんじゃないかとすら思ってるので、もし他の環境でサンプルコードが動作しましたら、是非コメントください m(_ _)m
###<i class="fa fa-cube"></i>対象となるチート
ゲームのチート(不正行為)にもいろいろな種類があります。今回は、ゲーム実行中に不正な操作をする**メモリハック**に対するチート対策方法について考えます。実際のチート対策は逆解析対策やセーブデータ改変への対策などが必要になってきますので、この記事の内容だけで満足しないようにお願いします。
###<i class="fa fa-cube"></i>目標
今回は、洞窟ゲーム用にHSP3でチート対策モジュールを作ります。このモジュールでは次ようなチートを検出、防止します。

- スコアの改ざん
- 実行速度の変更

ただし、次のようなチートには対応しません。

- メモリハック以外のチート
- マクロ
- Ring3(ユーザーモード)以外でのチート
- ソースコードがバレている前提でのチート

##<i class="fa fa-cubes"></i>ソースコード
いきなりですが、まずは最終的なソースコードを書きます。

```hsp:mod_protect.hsp
#module
	#uselib "ntdll.dll"
		#func LdrLoadDll "LdrLoadDll"
	#uselib "kernel32.dll"
		#cfunc GetCurrentProcess "GetCurrentProcess"
		#func WriteProcessMemory "WriteProcessMemory" sptr, sptr, sptr, sptr, sptr
		#cfunc VirtualAllocEx "VirtualAllocEx" sptr, sptr, sptr, sptr, sptr
	#uselib "user32.dll"
		#func SendMessageA "SendMessageA"
	#define MEM_COMMIT 0x1000
	#define PAGE_EXECUTE_READWRITE 0x40
	
	/*--------------------------------------------------
		ハッシュにより整合性を確認する
	--------------------------------------------------*/
	#defcfunc ValidateHash int val, int hash
		if CalculateHash(val) != hash {
			return -1
		}
	return 0
	
	/*--------------------------------------------------
		ハッシュを計算する
	--------------------------------------------------*/
	#defcfunc CalculateHash int val, local h, local v
		h = val & 0xF8000000
		v = val << 5
		v = v ^ (h >> 27)
		v = v ^ rnd(0xFFFFFFFF)
	return v
	
	/*--------------------------------------------------
		LdrLoadDllを無効化する
	--------------------------------------------------*/
	#deffunc DisableLdrLoadDll local asm, local pFilter, local msg
		/* フィルタを実装する */
		msg = rnd(0xFFFF)
		dim asm, 14
		asm(0) = 0x74ec8360	; pushad; sub esp, 0x10
		asm(1) = 0x0c245489	; mov DWORD PTR [esp+0xC], edx
		asm(2) = 0x08245489	; mov DWORD PTR [esp+0x8], edx
		asm(3) = 0x042444c7	; mov DWORD PTR [esp+0x4], (msg)
		asm(4) = msg
		asm(5) = 0x2404c790	; mov DWORD PTR [esp], (hWnd)
		asm(6) = hWnd
		asm(7) = 0xb8909090	; mov eax, <SendMessageA>
		asm(8) = varptr(SendMessageA)
		asm(9) = 0xc483d0ff	; call eax
		asm(10)= 0xb8906164	; add esp, 0x64 ; popad
		asm(11)= varptr(LdrLoadDll) + 5	; mov eax, <original_LdrLoadDll + 5>
		asm(12) = 0x90ec8b55	; push ebp; mov ebp, esp
		asm(13)= 0x9090e0ff	; jmp eax
		pFilter = VirtualAllocEx(GetCurrentProcess(), 0, 14*4, MEM_COMMIT, PAGE_EXECUTE_READWRITE)
		WriteProcessMemory GetCurrentProcess(), pFilter, varptr(asm), 14*4, varptr(asm)
		/* トランポリン(?)を書き込む */
		dim asm, 2
		asm(0) = 0xFFFFFFe9	; jmp <pFilter>
		asm(1) = 0x909090FF
		lpoke asm, 1, pFilter - varptr(LdrLoadDll) - 5
		WriteProcessMemory GetCurrentProcess(), varptr(LdrLoadDll), varptr(asm), 5, varptr(asm)
	return msg
#global
```

以下は、これを利用してチート対策を施したgame/doukutu.hspです。

```hsp:doukutu.hsp
;#packopt hide 1	; なぜかhide 1すると実行ファイルでフリーズする
#packopt name "doukutu"
#epack "face.bmp"

	;	洞窟探検ゲーム(改)
	;
	; HSP付属の洞窟探検ゲーム(game/doukutu.hsp)に
	; チート対策を追加しました
	;
#include "mod_protect.hsp"
	
	/* [追加]LdrLoadDllをフィルターする */
	DisableLdrLoadDll
	oncmd gosub *filter, stat
	
screen 0, 640, 480
	face=1
	celload "face.bmp",face
	celdiv face,64,64,32,32			; パーツのサイズと中心位置
*start
	cls 4
	wscr=0
	wxs=640
	wys=480
	gys=320
	gy=(wys-gys)/2
	spd=8
	px=100
	py=240
	dy=0
	bom=0
*main
	;	キャラクターにぶつかったか調べる
	a=0
	pget px+32,py-32
	a=a+ginfo_g
	pget px+32,py+32
	a=a+ginfo_g
	pget px+64,py
	a=a+ginfo_g
	if a>0 : goto *dead

	redraw 0

	;	洞窟を描画
	color 0,192,0
	boxf wxs-spd,0,wxs,wys
	color 0,0,0
	boxf wxs-spd,gy,wxs,gy+gys
	pos 0,0
	gmode 0,wxs-spd,wys
	gcopy 0,spd,0

	scr=scr+1
	/* [追加]ハッシュ値を更新 */
	scr_hash = CalculateHash(scr)
	title "点数="+scr

	gys=320-(scr/30)
	if gys<100 :gys=100

	if scr<60 : goto *mymove

	dy=dy+1
	if dy>16 : dy=16
	stick ky,16
	if ky=16 : dy=dy-3
	py=py+(dy/2)
*mymove
	gmode 2,64,64
	pos px,py
	celput face

	if cn<1 :cn=rnd(16) :cn=cn+3 :ry=rnd(17) :ry=ry-8
	cn=cn-1
	gy+=ry
	if gy+gys>448 : gy=448-gys
	if gy<32 : gy=32

	redraw 1
	await 24
	/* [追加]整合性の検証(awaitの後) */
	if (ValidateHash(scr, scr_hash) == -1) {
		goto *invalid
	}
	goto *main
*dead
	;	ぶつかった
	font msgothic,60
	pos 160,200
	color 255,255,255
	mes "いてーーー!"
*keymati
	stick ky
	if ky&32 : goto *start
	wait 10
	goto *keymati

/* [追加]LdrLoadDllをフィルターする */
*filter
	if (banned == 1) : end	// なんか終了できずにずっとfilterが呼び出されることがあるので
	// ロードされたDLLのパスを取得する
	sdim dllPath, 256
	dupptr dllPath, lParam, 256, 2
	dllPath = str(getpath(cnvwtos(dllPath), 16))
	// 許可するDLLのリスト（環境依存だし本当はファイル名だけじゃなくパスも調べる必要アリ）
	sdim white_list, 256, 5
	white_list(0) = "oleaut32.dll"
	white_list(1) = "c:\\windows\\system32\\ole32.dll"
	white_list(2) = "imjppred.dll"
	white_list(3) = "imesearchdll.dll"
	white_list(4) = "hsp3debug.dll"
	banned = 1
	foreach white_list
		// ホワイトリストに一致すれば許可
		if (instr(dllPath, 0, white_list(cnt)) != -1) : banned = 0 : break
		// よくわからないけどDLLになっていなければ許可
		if (getpath(dllPath, 2) == "") : banned = 0 : break
	loop
	// ホワイトリストになければログを出力して終了
	if (banned == 1) {
		sdim log_message, 256
		log_message = strf("許可されていないDLLがロードされました\nパス：%s", dllPath)
		bsave "injected.log", log_message
		end
	}
return

/* [追加]メモリ改ざん検知 */
*invalid
	dialog "メモリの改ざんを検知しました", 1, "エラー"
end
```

###<i class="fa fa-cube"></i>スコア改竄の検知
スコアの改竄はCalculateHash関数とValidateHash関数で検知されます。まず、プログラムはスコア(scr)を算出した後にスコアのハッシュ(scr_hash)をCalculateHash関数で生成します。この関数はscrを元に、適当なハッシュ値を作成します。次に、ValidateHash関数で現在のスコアと以前生成したスコアのハッシュ値が等しいかを確認します。もしこれが等しくなければ、CalculateHash関数の呼出からValidateHash関数の呼出までの間にスコアが改竄されたことになります。
これを検出すると以下のようにメッセージを表示します。
![injection.gif](https://qiita-image-store.s3.amazonaws.com/0/151385/21cb1668-2bb3-b776-3c20-527e5b20a038.gif)
ただ、どうも次に説明するDLLインジェクションの検知用ルーチンとdialogの相性が悪いらしく、実行ファイルにするとフリーズしてしまいます。まあ、チートしていない環境では問題ないから今回は無視しました。

### <i class="fa fa-cube"></i>DLLインジェクションの検知
DLLインジェクションとは、特定のプログラムに外部からDLLを読み込ませることで、そのプログラムの挙動を変更する技術です。チートツールもこの技術を多く採用しており、例えば実行速度の変更などに使われています。DisableLdrLoadDll関数はこれを防ぐ役割があります。
DLLインジェクションでは、外部からCreateRemoteThread関数を使用して、プログラム側にLoadLibraryを呼び出させることがほとんどです。したがって、プログラム側の防御策としてはLoadLibraryを潰せばいいのですが、今回は、更にLoadLibraryが呼び出すLdrLoadLibraryという関数を潰すことにしました。LdrLoadLibraryは以下のように開始されます。

```nasm:LdrLoadLibrary(変更前)
mov edi, edi
push ebp
mov ebp, esp
...
```

これを以下のように変更すればLdrLoadLibrary関数は使えなくなります。

```nasm:LdrLoadLibrary(変更後)
ret
...
```

しかしこれだけでは問題があります。LoadLibrary関数を使えなくしてしまうと、自分自身も困るのです。HSPは内部から何度もLoadLibrary関数を使うので、これが使えなくなるとHSPの機能がほとんど役に立たなくなってしまいます。そこで、今回はLoadLibrary関数を無効化する代わりにHSP側にLoadLibraryが呼び出されたことと、LoadLibraryで使われようとしているDLLのパスを通知するように処理を変更しました。そのDLLを許可するかはHSP側に委ねるということです。理想としては以下のような処理ができれば完璧です。

```nasm:LdrLoadLibrary(変更後)
push ebp
mov ebp, esp
sub esp, 0x64
1.HSP側に通知する
2.元の処理を実行する
...
leave
ret
```

しかし、ここでまた問題があります。HSP側に通知する処理を入れてしまうと、元の処理部分が壊れてしまいます。また、元の処理部分を下にずれせばいいと思うかもしれませんが、機械語では相対アドレスが度々現れるので、単純に機械語列をずらしただけではやはり壊れてしまいますし、ずらすと次に存在する別の関数を上書きしてしまうかもしれません。本当はこの辺で詰んでて「もうだめだー」みたいになってたのですが、Advent Calenderで次の日を担当しているtheoldmoon0602さんが次のように、HSPへの通知部分を別の場所で実行する方法を考案してくれました。もうこれdetoursですね。

```nasm:LdrLoadLibrary(変更後)
jmp <notifier> ; 「mov edi, edi; push ebp; mov ebp, esp」は上書きされます
sub esp, 0x64
...
leave
ret

notifier:
pushad ; レジスタの内容を保存します
sub esp, 0x74 ; LdrLoadLibraryで0x64使っていたので、そこに引数サイズだけ足しました
mov DWORD PTR [esp+0xC], edx ; edxにはDLLのパスへのポインタが入っています
mov DWORD PTR [esp+0x8], edx ; 上の一行とここはwparamとlparamです
mov DWORD PTR [esp+0x4], (msg) ; HSPに通知するためのメッセージID
mov DWORD PTR [esp], (hWnd) ; HSPのウィンドウハンドル
mov eax, <SendMessageA> ; LdrLoadDllでeaxは破壊されるので使用可能です
call eax
add esp, 0x64 ; subした分戻しますが、0x10はSendMessageにより使われました
popad ; レジスタの内容を元に戻します
mov eax, <LdrLoadDll + 5> ; LdrLoadDllのjmpの次の命令から開始します
; 下の二行でLdrLoadDllのjmpで潰された部分を実行します
push ebp
mov ebp, esp
jmp eax ; 何事も無かったかのようにLdrLoadDllを再開します
```

これをHSP側ではoncmdでキャッチできます。そこで受け取ったDLLのパスを(穴だらけの)ホワイトリストでフィルターします。本当はもっとちゃんとチェックする必要があるのでご注意ください。
検知すると次のようにすぐにプログラムが終了して、ログが保存されます。
![overwrite.gif](https://qiita-image-store.s3.amazonaws.com/0/151385/dfe2fee9-9760-4802-1ee3-b7cb567edf2c.gif)


##<i class="fa fa-cubes"></i>まとめ
- 短時間の割に(自分の環境で)ちゃんと動いてくれたので感動
- call, leave, ret, jmpあたりとスタックの関係が勉強できた
- Ring3でチート対策って凄い難しいことを再認識
- HSPはvarptrで#funcした関数のアドレスが取得できる謎仕様

将来的にはもっと幅広いチートに対応したモジュールを作れたらと考えています。とりあえず今回はこんなところで。