どうやら、このコードはスタックを変更して、main
関数が戻ったときにプログラムの実行が定期的にランタイム ライブラリに戻らず (通常はプログラムを終了させます)、代わりにshellcode
配列に保存されたコードにジャンプするようにしています。
1) int *ret;
main
関数の引数のすぐ下で、スタック上の変数を定義します。
2) ret = (int *)&ret + 2;
スタックの 2 つ上に配置されret
た を変数が指すようにします。おそらく、それは、プログラムが戻ったときに続行するリターンアドレスが配置されている場所です。int *
int
ret
main
2) (*ret) = (int)shellcode;
戻りアドレスはshellcode
配列の内容のアドレスに設定されるため、戻りshellcode
時に の内容が実行されmain
ます。
shellcode
launch へのシステムコールを実行する可能性のあるマシン命令が含まれているよう/bin/sh
です。私は実際に分解していないので、これについて間違っている可能性がありますshellcode
。
PS:このコードはマシンとコンパイラに依存しており、すべてのプラットフォームで機能するとは限りません。
2 番目の質問に答えます。
ret=(int)&ret +2 を使用するとどうなりますか? なぜ 2 を追加したのでしょうか? なぜ3か4ではないのですか?そして、intは4バイトだと思うので、2は8バイトになりますか?
ret
は として宣言されているため、 ( などの) をそれにint*
割り当てるとエラーになります。なぜ 2 が追加され、他の数字は追加されないのかについて: どうやら、このコードは戻りアドレスがスタック上のその場所にあると想定しているためです。次の点を考慮してください。int
(int)&ret
このコードは、何かがプッシュされたときにコール スタックが下向きに成長することを前提としています (Intel プロセッサなどで実際に行われているように)。これが、数値が加算され、減算されない理由です。戻りアドレスは、自動 (ローカル) 変数 (など) よりも上位のメモリ アドレスにありますret
。
Intel のアセンブリ時代の記憶から、C 関数は次のように呼び出されることがよくあります。まず、すべての引数が逆の順序 (右から左) でスタックにプッシュされます。次に、関数が呼び出されます。したがって、戻りアドレスはスタックにプッシュされます。次に、新しいスタック フレームが設定されます。これには、ebp
レジスタがスタックにプッシュされることが含まれます。次に、ローカル変数は、この時点までにプッシュされたすべての下のスタックに設定されます。
ここで、プログラムのスタック レイアウトを次のように仮定します。
+-------------------------+
| function arguments | |
| (e.g. argv, argc) | | (note: the stack
+-------------------------+ <-- ss:esp + 12 | grows downward!)
| return address | |
+-------------------------+ <-- ss:esp + 8 V
| saved ebp register |
+-------------------------+ <-- ss:esp + 4 / ss:ebp - 0 (see code below)
| local variable (ret) |
+-------------------------+ <-- ss:esp + 0 / ss:ebp - 4
一番下にありますret
(これは 32 ビット整数です)。その上に保存されたebp
レジスタがあります (これも 32 ビット幅です)。その上に 32 ビットの戻りアドレスがあります。(その上にはmain
の引数 --argc
とargv
-- がありますが、ここでは重要ではありません。) 関数が実行されると、スタック ポインターは を指しますret
。戻りアドレスは 64 ビット「上」にあり、これはin にret
対応します。+ 2
ret = (int*)&ret + 2;
はaであり、 anは 32 ビットである+ 2
ため、2 を追加することは、それを 2 × 32 ビット (=64 ビット) 上のメモリ位置に設定することを意味します。上記の段落は正しいです。ret
int*
int
(int*)&ret
エクスカーション: C 関数がどのように呼び出されるかを Intel アセンブリ言語で示してみましょう(私の記憶が正しければ、私はこのトピックの第一人者ではないので、間違っている可能性があります)。
// first, push all function arguments on the stack in reverse order:
push argv
push argc
// then, call the function; this will push the current execution address
// on the stack so that a return instruction can get back here:
call main
// (afterwards: clean up stack by removing the function arguments, e.g.:)
add esp, 8
main 内では、次のことが発生する可能性があります。
// create a new stack frame and make room for local variables:
push ebp
mov ebp, esp
sub esp, 4
// access return address:
mov edi, ss:[ebp+4]
// access argument 'argc'
mov eax, ss:[ebp+8]
// access argument 'argv'
mov ebx, ss:[ebp+12]
// access local variable 'ret'
mov edx, ss:[ebp-4]
...
// restore stack frame and return to caller (by popping the return address)
mov esp, ebp
pop ebp
retf
このトピックの別の説明については、Cでのプロシージャー呼び出しシーケンスの説明も参照してください。