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







