ShellCraftとは?
原義の「シェルコード」を指します。
悪いコードというイミでなく、シェルを呼び出す関数です。
ShellCraftが実行するコード
/* execve(path='/bin///sh', argv=['sh'], envp=0) */
/* push '/bin///sh\x00' */
push 0x68
push 0x732f2f2f
push 0x6e69622f
mov ebx, esp
/* push argument array ['sh\x00'] */
/* push 'sh\x00\x00' */
push 0x1010101
xor dword ptr [esp], 0x1016972
xor ecx, ecx
push ecx /* null terminate */
push 4
pop ecx
add ecx, esp
push ecx /* 'sh\x00' */
mov ecx, esp
xor edx, edx
/* call execve() */
push SYS_execve /* 0xb */
pop eax
int 0x80
要約すると、
「/* execve(path='/bin///sh', argv=['sh'], envp=0) */
」
execveは、システムコールです。OSが用意してる関数みたいなもんです。
これを召喚すればシェルやスクリプトを実行してくれるわけです。
【引数】
第一引数 : 実行したいファイルのパス(実行バイナリやスクリプト)
第二引数 : プログラムに渡す配列(NULLポインタが終端)。ls -l
とか
第三引数 : プログラムに渡す環境変数(NULLポインタが終端)
目標として、
eax(ebx, ecx, edx)
の形式になることを確認できれば👌
↑ は、
execve("/bin///sh", "sh", 0)
と同じイミ
それぞれのレジスタ(e[a-d]x
)に文字とかを格納できれば👌
スタックを追う
スタックつうのはprint("HelloWorld!!")
するならこんな感じになります。
引数→リターンアドレスの順に、先端へ積んでいくわけです。ジェンガと同じで、先端から取り出します。底の方から取り出すことはできません。
アセンブリで簡易に表すと ↓
push 00!!
push dlroW
push olleH
call print
push
: スタックに積め、という命令
pop
: スタックから取り出せ、という命令。push
の逆の操作
00 : 終端文字。コンピュータが文字列
と命令文
を区別するために文字列の終端に供えます。16進数「\x00」はASCIIで「NULL」を指します。
では、セクションごとに見ていきましょう。
①/* push '/bin///sh\x00' */
第一引数として、「/bin///sh」という文字列をスタックに追加すると言っています。
/* push '/bin///sh\x00' */
push 0x68
push 0x732f2f2f
push 0x6e69622f
mov ebx, esp
スタックは4バイト度()にアドレスが割り振られており、0x68
(1バイト)は残りの3バイト分を000000
で埋めます。これが終端文字NULLに該当します。
0x00000068 : nullnullnullh
0x732f2f2f : s///
0x6e69622f : nib/
1バイト区切りで逆さから読めば「/bin///sh」という文字列の完成です。ASCII表で16進数
とアルファベット
の対応を確認できます。例えば「2f
」は「/
」に対応しています。
このパスのシェルを呼び出すようですね。
mov ebx, esp
は、ebx
というレジスタにesp
のアドレスを格納する命令です。「esp
」というのは常にスタックの先端アドレスを指すポインタです。
今、このアドレスには「nib/」が格納されており、ここから逆さに文字を辿っていくと、「/bin///sh」と第一引数が完成します🎉
②/* push argument array ['sh\x00'] */
第二引数として、「sh」という文字列をスタックに追加すると言っています。
/* push argument array ['sh\x00'] */
/* push 'sh\x00\x00' */
push 0x1010101
xor dword ptr [esp], 0x1016972
xor ecx, ecx
push ecx /* null terminate */
push 4
pop ecx
add ecx, esp
push ecx /* 'sh\x00' */
mov ecx, esp
xor edx, edx
ただ先ほどと違って命令が多いです。順を追って見ましょう。
push 0x1010101
xor dword ptr [esp], 0x1016972
次の命令は、esp
と0x1016972
でxorをとり、その計算結果をesp
に代入するようです。
esp
には0x1010101
が格納されているので
esp = 0x1010101 xor 0x1016972
という命令になります。
0x6873
という値が得られるはずです。ASCIIでいう「sh」に対応します。
※dword ptrは、esp
を4バイトとして扱います。esp = 0x1010101
は7バイト(7桁)なので0x01010101
という風に8バイトで表現します。計算結果は変わりません。
ちょっとした疑問と予想
そもそもesp
はポインタであり、「データのアドレス」を指しています。しかし、今回のxor演算では「データそのもの(0x1010101)」を表していました。これは、dwordを宣言することでesp
を「アドレスとして」でなく、「ポインタが指すデータそのものとして」扱わせているのかも🤔
😎
😎
😎
xor ecx, ecx
push ecx /* null terminate */
同じ値でxorをとると値は必ず0
になります。ecx
には0
が代入され、
それをpush
すると
となります。👌
この0
が第二引数「sh」の終端文字NULLだそうですね。
😎
😎
😎
push 4
pop ecx
add ecx, esp
push ecx /* 'sh\x00' */
mov ecx, esp
xor edx, edx
add ecx, esp
今、以下の値を足してecx
に格納しようとしています。
ecx
= 4
esp
= 0を格納しているアドレス
すると、、
ecx
には「esp
に4足したアドレス」が格納されます。
※アドレスを「足す」と底に近づきます。先端に行くほどアドレスは小さくなります。4ずつ。正規分布みたく。
ecx
には「sh」を指すアドレスが格納されました。
ecx
には「shを表すアドレスのアドレス」。これが第二引数です。
今、esp + 4
の位置にある0
はおそらく、shのアドレスの終端文字だと考えられます。
最後に、xor edx, edx
でedx
= 0
になります。これが第三引数です。
引数はすべて完了しました。👌
🍵
🍵
🍵
/* call execve() */
ようやくシステムコールの処理です。
このシステムコールがさっき求めた引数を取ります。
/* call execve() */
push SYS_execve /* 0xb */
pop eax
int 0x80
push SYS_execute
・pop eax
スタックはこうなります。👌
int 0x80
は、システムコールの呼び出し(重言)命令です。
整理
eax
= SYS_execute
ebx
= /bin///sh
ecx
= sh
edx
= 0
eax(ebx, ecx, edx)
execve("bin///sh", "sh", 0)
の完成です。
最後に
間違った情報があれば、教えていただきたいです。
今のところ、
"ecx
にshのアドレスが格納されてもなおecx
にesp
を代入して間接的なポインタを作るのはエレガント性に欠けるな~"
くらいです。
これ以外の一連は納得しているつもりです。
「解析」にしては手ごろなサイズだったので、楽しいまま解析し終えました。次はシステムコールがどのように引数を取り、どのような一連を経て実行されるのかが知りたいです。
なにか、疑問点・ここってこうじゃね?的な意見があれば是非コメントお願いします。理解の矯正につながると思います。
51c9670f00071a021114094cc8afa5733d4b6c9f5cee2f71f987c60bd573ffb0