3

簡単にするために、2 つの同様の関数があります。

void f1()
{
    printf("%d", 123);
}
void f2()
{
    printf("%d", 124);
}

main で f1 を呼び出すと、123 が出力されます。コンパイルすると、 の逆アセンブリは次のf1ようになります。

08048424 <f1>:
 8048424:       55                      push   %ebp
 8048425:       89 e5                   mov    %esp,%ebp
 8048427:       83 ec 18                sub    $0x18,%esp
 804842a:       b8 40 86 04 08          mov    $0x8048640,%eax
 804842f:       c7 44 24 04 7b 00 00    movl   $0x7b,0x4(%esp)
 8048436:       00
 8048437:       89 04 24                mov    %eax,(%esp)
 804843a:       e8 05 ff ff ff          call   8048344 <printf@plt>
 804843f:       c9                      leave
 8048440:       c3                      ret

f2 のマシンコードは f1 のものと似ています。

ここで、実行時に f1 を f2 のマシン コードに置き換えたいと考えています。私は memcpy(f1, f2, SIZE_OF_F2_MACHINE_CODE) を使用しています。確かに問題が発生します — セグメント障害です。

この問題を解決するソリューションが存在するかどうかを知りたいです。これは一般的な C プログラムです。私が知っているように、以下のようなコードを使用して、Linux カーネルでページを書き込み可能に設定できます。

int set_page_rw(long unsigned int addr)
{
    unsigned int level;
    pte_t *pte = lookup_address(addr, &level);

    if(pte->pte & ~_PAGE_RW)
        pte->pte |= _PAGE_RW
}

ただし、通常の Linux C プログラムでは機能しません。次に、何が機能しますか?

4

3 に答える 3

3

なぜ聞くのですか?コードが同じプロセスによって生成されたいくつかの関数を最終的に呼び出すことができるようにしたい場合は、別の方法で進めることができます。

  1. このような動的に生成された関数を呼び出すには、常に関数ポインタを使用してください。私の提案は、読みやすさの理由から、typedefポインターを宣言する前に署名することです。この回答を参照してください。
  2. 関数を生成し、それへのポインターを取得します。

    • たとえば、C ソース ファイルgenerated.cを生成し、おそらく でプロセスをフォークしsystem("gcc -fPIC -O -shared generated.c -o generated.so");てコンパイルしdlopen("./generated.so", RTLD_GLOBAL)、生成された関数のポインタを で取得できますdlsym。詳細については、 dlopen(3)の man ページを参照してください。参考までに、メルトはそうしています。

    • 関数のマシンコードをメモリ内に生成することもできます (おそらくフラグを使用してmmap(2)PROT_EXECで取得されます)。いくつかの JIT (ジャストインタイム変換) ライブラリが利用可能です: GNU lightning (実行速度の遅いマシン コードの高速生成)、myjitlibjitLLVM (最適化されたマシン コードの低速生成)、LuaJIT ...

既存の関数コードを本当に上書きしたい場合は、それを行うこともできますが、それには多大な注意が必要であり、苦痛を伴います (たとえば、新しい関数コードは古い関数コードよりも多くのスペースを必要とするため、また再配置の問題のため)。mmap(2)および/またはmprotect(2)システムコールを使用して、そのようなトリックの許可を取得してください。ただし、デバッグの悪夢に備えてください。Python スクリプトgdbを使用してデバッガーのスクリプトを作成したい場合があります。

カーネル モジュールの場合は話が異なります。iptables一部のネットワーク関連のカーネル コード (おそらく?) は、JIT 手法を使用してマシン コードを生成し、実行する可能性があると聞きました。

于 2012-10-18T09:39:17.483 に答える
3

手順を上書きしないでください。代わりに、シンボル テーブル内のシンボル参照を上書きしてください。それには動的リンケージが必要です。または、関数の呼び出しを他の関数の呼び出しで上書きすることもできますが、NXビットのようなものが邪魔になる場合があります。自己変更コードは一般的に嫌われます。

于 2012-10-18T05:02:23.853 に答える
2

私はあなたへの答えを見つけようとしましたが、失敗しました。私が実際に成功したのは、疑わしいコードを単純化することだけです。

void f1( )
{
}
int main( )
{
  *(char*) f1 = *(char*) f1;
  return( 0 );
}

はい、セグメンテーション違反(gcc)またはメモリアクセス違反(MS VC)で失敗します。

編集:

実は私はあなたがやりたいことをすることに成功しました

(Basile Starynkevitchの回答に基づく)。ただし、x86のみ、gccのみ、および特定の例のみ。以下はいくつかのコード例です。

最初に-簡略化された例。

#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>

void f1( )
{
}

int main( )
{
  int rc;
  int pagesize;
  char *p;

  f1( );

  pagesize = sysconf( _SC_PAGE_SIZE );
  printf( "pagesize=%d (0x%08X).\n", pagesize, pagesize );
  if( pagesize == -1 )
    return( 2 );

  p = (char*) f1;
  printf( "p=0x%08X.\n", p );
  p = (char*) ((size_t) p & ~(pagesize - 1));
  printf( "p=0x%08X.\n", p );

  rc = mprotect( p, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC );
  printf( "rc=%d.\n", rc );
  if( rc != 0 )
    return( 2 );

  printf( "'mprotect()' succeeded.\n" );

  *(char*) f1 = *(char*) f1;

  printf( "Write succeeded.\n" );

  f1( );

  printf( "Call succeeded.\n" );

  return( 0 );
}

これをコンパイルして一度起動します。失敗しますが、ページサイズはわかります。たとえば、4096です。次に、この例を次のようにコンパイルします。

gcc a1.c -falign-functions=4096

そしてそれはうまくいくはずです。

出力:

pagesize=4096 (0x00001000).
p=0x00402000.
p=0x00402000.
rc=0.
'mprotect()' succeeded.
Write succeeded.
Call succeeded.

高度な例:

#include <stdio.h>
#include <sys/mman.h>
#include <unistd.h>
#include <string.h>

//extern void f1( void ) __attribute__(( aligned( 4096 ) ));
__asm__( ".text" );
__asm__( ".align 4096" );
void f1( void )
{
  printf( "%d\n", 123 );
}

void f2( void )
{
  printf( "%d\n", 124 );
}

int main( void )
{
  int rc;
  int pagesize;
  char *p;
  int i;

  printf( "f1=0x%08X.\n", f1 );
  printf( "f2=0x%08X.\n", f2 );

  f1( );
  f2( );

  pagesize = sysconf( _SC_PAGE_SIZE );
  printf( "pagesize=%d (0x%08X).\n", pagesize, pagesize );
  if( pagesize == -1 )
    return( 2 );

  p = (char*) f1;
  printf( "p=0x%08X.\n", p );
  p = (char*) ((size_t) p & ~(pagesize - 1));
  printf( "p=0x%08X.\n", p );

  rc = mprotect( p, pagesize, PROT_READ | PROT_WRITE | PROT_EXEC );
  printf( "rc=%d.\n", rc );
  if( rc != 0 )
    return( 2 );

  printf( "'mprotect()' succeeded.\n" );

  for( i = 0; i < (size_t) f2 - (size_t) f1; i++ ) {
    if( ((char*) f2)[ i ] == 124 ) {
      printf( "i=%d.\n", i );
      ((char*) f1)[ i ] = ((char*) f2)[ i ];
    }
  }

  //memcpy( f1, f2, (size_t) f2 - (size_t) f1 );

  printf( "Write succeeded.\n" );

  f1( );
  f2( );

  printf( "Call succeeded.\n" );

  return( 0 );
}

「 f1()」および「f2()」内の「printf() 」の呼び出しは絶対的ではなく相対的であるため、ここでは「 memcpy() 」を使用できません(コメント付き)。そして、それらを絶対にする方法を見つけることができませんでした(「-fPIC」も「-fno-PIC」も私の場合は機能しませんでした)。「 f1()」と「f2() 」に相対関数呼び出しがない場合は、 「 memcpy() 」を使用できると思います(ただし、試しませんでした)。

また、「f1() 」をページサイズに合わせる必要があります(「 f1() 」が開始する前に十分なコードがあることが確実な場合を除く)。gcc 4.3以降を使用している場合は、属性を使用できます(gcc v4.1.2を使用しているため、コメントが付けられています)。そうでない場合は、その醜くて信頼できない " _asm_ "を使用できます。

出力:

f1=0x00402000.
f2=0x0040201E.
123
124
pagesize=4096 (0x00001000).
p=0x00402000.
p=0x00402000.
rc=0.
'mprotect()' succeeded.
i=12.
Write succeeded.
124
124
Call succeeded.

そしてもちろん、その恐ろしい「if(((char *)f2)[i] == 124)」。これは、何を置き換える必要があるか(印刷された番号)と何を置き換えるべきでないか(相対参照)を区別するのに役立ちます。明らかに、これは非常に単純化されたアルゴリズムです。あなたはあなたの仕事に適したあなた自身を実装しなければならないでしょう。

于 2012-10-18T10:17:21.287 に答える