web-dev-qa-db-ja.com

シェルコードは実際にどのように実行されますか?

私は「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とリターンアドレスがプッシュされたので、ここでそのアドレスを上書きしてシェルコードをそこに配置しています。そうですか?

18

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つの引数がebxecxedxesiediebpに渡されます。この場合、ebxのみが設定されます。つまり、syscallは引数を1つしか取りません。 0x80割り込みが呼び出されると、カーネルが引き継ぎ、これらの値を確認して、正しいシステムコールを実行します。システムコール番号は_/usr/include/asm/unistd_32.h_で定義されています。これを見ると、syscall 1はexit()であることがわかります。このことから、このシェルコードが行う3つのことがわかります。

  1. Syscallの最初の引数を0に設定します(終了の成功を意味します)。
  2. Syscall番号を1に設定します。これは出口呼び出しです。
  3. Syscallを呼び出し、プログラムをステータス0で終了させます。

全体像を見ると、シェルコードは基本的に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();
}
_
39
forest