私は「The Shellcoders Handbook」という本を読み、その中にシェルコードを実行するCコードがあります(exit syscallのみを呼び出します)。
char shellcode [] =“\xbb\x00\x00\x00\x00\xb8\x01\x00\x00\x00\xcd\x80”; int main( ){ int * ret; ret =(int *)&ret + 2; (* ret)=(int)shellcode; }
メイン関数のこれら3つの行に興味があります。彼らは正確に何をしていて、どのようにシェルコードを実行しているのですか?
私はそれを理解したかもしれません:mainがスタックで呼び出される前に、以前のスタックフレームからebpとリターンアドレスがプッシュされたので、ここでそのアドレスを上書きしてシェルコードをそこに配置しています。そうですか?
TL; DRこれは、動作しないシェルコードを実行する方法です。
シェルコードは、char
型の変数など、通常は見つからない場所の単なるマシンコードです。 Cでは、関数と変数の区別はありません。関数は、実行可能コードを指す単なる変数です。つまり、実行可能コードを指す変数を作成し、それを関数であるかのように呼び出すと、実行されます。それが単なる変数であることを説明するには、次の簡単なプログラムを参照してください。
_#include <stdio.h>
#include <stdint.h>
void print_hello(void)
{
printf("Hello, world!\n");
}
void main(void)
{
uintptr_t new_print_hello;
printf("print_hello = %p\n", print_hello);
new_print_hello = (uintptr_t)print_hello;
(*(void(*)())new_print_hello)();
print_hello();
}
_
コンパイルして実行すると、このプログラムは次のような出力を提供します。
_$ ./a.out
print_hello = 0x28bc4bf6da
Hello, world!
Hello, world!
_
これにより、関数がメモリ内のアドレスにすぎず、タイプ_uintptr_t
_と互換性があることが簡単にわかります。関数を変数として単純に参照できる方法を確認できます。この場合は、値を出力するか、互換性のあるタイプの別の変数にコピーして、関数のように変数を呼び出します。 Cコンパイラを幸せにするために。関数が実行可能メモリを指す変数に過ぎないことを確認したら、手動で定義したバイトコードを指す変数を実行する方法を確認するのは簡単ではありません。
関数がメモリ内の単なるアドレスであることがわかったので、関数が実際にどのように実行されるかを知る必要があります。通常、call
命令を使用して関数を呼び出すと、(現在実行中の命令を指す)命令ポインタが、関数の最初の命令を指すように変化します。関数が呼び出される直前の場所は、call
によってスタックに保存されます。関数が終了すると、ret
命令で終了し、スタックからポップして、IPに保存します。したがって、(多少簡略化された)ビューは、call
がIPをスタックにプッシュし、ret
がそれをポップバックするというものです。
使用しているアーキテクチャとOSに応じて、関数への引数はレジスタまたはスタックで渡され、戻り値は別のレジスタまたはスタックで渡される場合があります。これは関数呼び出しABIと呼ばれ、システムの各タイプに固有です。あるタイプのシステム用に設計されたシェルコードは、アーキテクチャが同じでオペレーティングシステムが異なる場合や、その逆であっても、別のタイプでは機能しない場合があります。
あなたが提供したシェルコードの逆アセンブルを見てみましょう:
_0000000000201010 <shellcode>:
201010: bb 00 00 00 00 mov ebx,0x0
201015: b8 01 00 00 00 mov eax,0x1
20101a: cd 80 int 0x80
_
これは3つのことを行います。最初に、ebx
を0に設定します。次に、eax
レジスタを1に設定します。最後に、32ビットシステムでは、syscall割り込みである割り込み0x80をトリガーします。 SysV呼び出しABIでは、syscall番号はeax
に配置され、最大6つの引数がebx
、ecx
、edx
、esi
、edi
、ebp
に渡されます。この場合、ebx
のみが設定されます。つまり、syscallは引数を1つしか取りません。 0x80割り込みが呼び出されると、カーネルが引き継ぎ、これらの値を確認して、正しいシステムコールを実行します。システムコール番号は_/usr/include/asm/unistd_32.h
_で定義されています。これを見ると、syscall 1はexit()
であることがわかります。このことから、このシェルコードが行う3つのことがわかります。
全体像を見ると、シェルコードは基本的にexit(0)
と同等であることがわかります。 ret
は返されないため不要で、代わりにプログラムを終了させます。関数に戻りたい場合は、最後にret
を追加する必要があります。少なくとも[ret
]を使用しない場合、exit()
syscallを使用した例のように、プログラムが関数の最後に到達する前に終了しない限り、プログラムはクラッシュします。
表示しているシェルコードを呼び出す方法 機能しない がなくなりました。以前はLinuxで使用されていましたが、現在では任意のデータの実行が許可されていないため、難解なキャストが必要です。この古い手法は、有名な 楽しみと利益のためのスタックのスマッシング の記事でよく説明されています。
_ Lets try to modify our first example so that it overwrites the return
address, and demonstrate how we can make it execute arbitrary code. Just
before buffer1[] on the stack is SFP, and before it, the return address.
That is 4 bytes pass the end of buffer1[]. But remember that buffer1[] is
really 2 Word so its 8 bytes long. So the return address is 12 bytes from
the start of buffer1[]. We'll modify the return value in such a way that the
assignment statement 'x = 1;' after the function call will be jumped. To do
so we add 8 bytes to the return address. Our code is now:
example3.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
int *ret;
ret = buffer1 + 12;
(*ret) += 8;
}
void main() {
int x;
x = 0;
function(1,2,3);
x = 1;
printf("%d\n",x);
}
------------------------------------------------------------------------------
What we have done is add 12 to buffer1[]'s address. This new address is
where the return address is stored. We want to skip pass the assignment to
the printf call. How did we know to add 8 to the return address? We used a
test value first (for example 1), compiled the program, and then started gdb
_
新しいシステムのシェルコードの 正しいバージョン は次のようになります:
_const char shellcode[] = “\xbb\x00\x00\x00\x00\xb8\x01\x00\x00\x00\xcd\x80”;
int main(){
int (*ret)() = (int(*)())shellcode;
ret();
}
_