システムコールの変更。 パート2

前の部分では、ネイティブカーネルモジュールのコードでエクスポートされていないLinuxカーネル名を使用して、エクスポートされたものと同じ成功を収めることができることに同意しました。 カーネル内のそのような名前の1つは、すべてのLinuxシステムコールのセレクタテーブルです。 実際、これはアプリケーションのカーネルサービスへのメインインターフェイスです。 次に、独自のビジョンに従って、システムコールの元のハンドラーを変更、置換、または実行に多様性を追加する方法を見ていきます。



修正テクニック



オペレーティングシステムへのシステムコールを変更する手法は、以前から知られており、さまざまなオペレーティングシステムで使用されています。 これは、ウイルス作成者がMS-DOSシステムから始めたお気に入りのテクニックです。MS-DOSシステムは、このような実験を単に引き起こしたシステムです。 しかし、私たちは平和的な目的のためにこの手法を使用します...(異なる出版物では、そのようなアクションは異なって呼ばれます:変更、埋め込み、実装、置換、傍受-それらは独自のニュアンスを持っていますが、議論の同義語として使用できます)



Linuxオペレーティングシステムのカーネル内のシステムコールがsys_call_tableシステムコールテーブル(配列)のアドレスを介して間接的に呼び出され、このセレクターテーブルのアドレスを独自のハンドラー関数に置き換えた場合、システムコールハンドラーを置き換えます。 これは、実際には修正の手法です。 実際には、そのような程度まで急進主義は必要ありません。実際には、元のシステムコールを行う必要がありますが、その前(前処理)または終了後(後処理)(または両方の組み合わせ)のいずれかで独自のアクションの一部を行っています。



前のパートでは、場所を見つけて、エクスポートされていないカーネルシンボルを含む任意のものを使用できることを確認しました。 原則として、システムコールを変更するために必要なのは、sys_call_table配列のベースアドレスを見つけ、必要なシステムコール呼び出しのオフセット番号を使用して独自の処理関数のアドレスを書き留めることだけです。



実際、スキームはもう少し複雑になります-最初にシステムハンドラーの古い(元の)値を保存する必要があります。

-変更されたコードの実行の前後に、独自の処理関数から元のハンドラーを呼び出す。

-モジュールをアンロードするときに元のハンドラーを復元します。



実装



常に起こるように、実現は理論よりもやや複雑です。 最初の小さな難点は、特定のシステムコール用に独自の処理関数のプロトタイプを作成する方法です。 プロトタイプのわずかな不正確さは、高い確率でオペレーティングシステムの崩壊につながるだけです。 解決策は、カーネルヘッダーファイル<linux / syscalls.h>からこのシステムコールの処理関数のプロトタイプをのぞき見するだけです(ドラップのように)。



ここでのはるかに大きな次の難点は、これを許可するプロセッサアーキテクチャ(およびその中のI386とX86_64)のsys_call_tableセレクタテーブルが読み取り専用(読み取り専用)のメモリページにあることです。 これはハードウェア(MMU-メモリ管理ユニット)によって制御され、アクセス権が侵害されると例外がスローされます。 したがって、sys_call_table要素の変更中に書き込み禁止フラグをクリアし、後で復元する必要があります。



I386およびX86_64アーキテクチャでは、書き込み可能フラグは、隠しプロセッサステータスレジスタCR0のビットフラグによって決定されます。 必要なアクションを実行するには、32ビットアーキテクチャの適切な関数を使用します。たとえば、次のようになります(ファイルCR0.c、このコードはインラインアセンブラーインサート-GCCコンパイラーの拡張機能で記述されています)。

// page write protect - on #define rw_enable() \ asm( "cli \n" \ "pushl %eax \n" \ "movl %cr0, %eax \n" \ "andl $0xfffeffff, %eax \n" \ "movl %eax, %cr0 \n" \ "popl %eax" ); // page write protect - off #define rw_disable() \ asm( "pushl %eax \n" \ "movl %cr0, %eax \n" \ "orl $0x00010000, %eax \n" \ "movl %eax, %cr0 \n" \ "popl %eax \n" \ "sti " );
      
      





PS書き込み保護されたページに書き込むためのさまざまなオプションについては、たとえば、 WP:Safe or Not? Linuxカーネルの書き込み保護された領域を変更するKosherの方法



これで、Linuxシステムコール(man(2))を独自のハンドラー関数に置き換える準備ができました-これが目的でした。 メソッドの操作性を説明するために、システムコールwrite(1、...)を置き換え(展開)-端末への出力、出力ストリームをシステムログに複製します(teeコマンドの動作と同様):

 #define PREFIX "! " #define DEB2(...) if( debug > 1 ) printk( KERN_INFO PREFIX " ---- " __VA_ARGS__ ) #define LOG(...) printk( KERN_INFO PREFIX __VA_ARGS__ ) #define ERR(...) printk( KERN_ERR PREFIX __VA_ARGS__ ) static int debug = 0; // debug output level: 0, 1, 2 module_param( debug, uint, 0 ); asmlinkage long (*old_sys_write) ( unsigned int fd, const char __user *buf, size_t count ); #define LEN 250 asmlinkage long new_sys_write ( unsigned int fd, const char __user *buf, size_t count ) { if( 1 == fd ) { char msg[ LEN + 1 ]; int n = count < LEN ? count : LEN, r; if( ( r = copy_from_user( msg, (void*)buf, n ) ) != 0 ) return -EINVAL; if( '\n' == msg[ n - 1 ] ) msg[ n - 1 ] = '\0'; else msg[ n ] = '\0'; if( strchr( msg, '!' ) != NULL ) goto rec; // to prevent recursion LOG( "{%04d} %s\n", count, msg ); } rec: return old_sys_write( fd, buf, count ); // original write() }; static void **taddr; // address of sys_call_table static int __init wrchg_init( void ) { void *waddr; if( NULL == ( taddr = find_sym( "sys_call_table" ) ) ) { ERR( "sys_call_table not found\n" ); return -EINVAL; } old_sys_write = (void*)taddr[ __NR_write ]; if( NULL == ( waddr = find_sym( "sys_write" ) ) ) { ERR( "sys_write not found\n" ); return -EINVAL; } if( old_sys_write != waddr ) { ERR( "Oooops! : addresses not equal\n" ); return -EINVAL; } LOG( "set new sys_write syscall [%p]\n", &new_sys_write ); show_cr0(); rw_enable(); taddr[ __NR_write ] = new_sys_write; show_cr0(); rw_disable(); show_cr0(); return 0; } static void __exit wrchg_exit( void ) { rw_enable(); taddr[ __NR_write ] = old_sys_write; rw_disable(); LOG( "restore old sys_write syscall [%p]\n", (void*)taddr[ __NR_write ] ); return; } module_init( wrchg_init ); module_exit( wrchg_exit );
      
      





カーネルの文字検索関数find_sym()は、カーネルAPI呼び出しkallsyms_on_each_symbol()を使用して、 前の部分で説明しました。 さらに、元のsys_write()の名前のアドレスと、sys_call_tableテーブルの__NR_writeの位置にある同じアドレスの一致を制御します(詳しく説明します)。



これで、端末に表示されるすべてのものの並列ロギングを使用してシステムを実行できます(write()実験は特に美しいとは言えませんが、他のLinuxシステムコールと比較して実験の初期段階で非常にわかりやすく、さらに安全です):

 $ sudo insmod wrlog.ko debug=2 $ ls CR0.c find.c Makefile Modi.hist wrlog.0.c wrlog.1.c wrlog.2.c wrlog.3.c wrlog.c wrlog.hist wrlog.ko $ sudo rmmod wrlog $ dmesg | tail -n31 [ 1594.231242] ! set new sys_write syscall [f8854000] [ 1594.231248] ! ---- CR0 = 80050033 [ 1594.231250] ! ---- CR0 = 80040033 [ 1594.231252] ! ---- CR0 = 80050033 [ 1594.232737] ! {0052} /home/olej/2015_WORK/own.BOOK/SysCalls/Modi/examles [ 1594.233368] ! {0078} \x1b[01;32molej@nvidia\x1b[01;34m ~/2015_WORK/own.BOOK/SysCalls/Modi/examles $\x1b[00m [ 1596.866659] ! {0001} l [ 1597.154675] ! {0001} s [ 1597.644985] ! {0110} CR0.c find.c Makefile Modi.hist wrlog.0.c wrlog.1.c wrlog.2.c wrlog.3.c wrlog.c wrlog.hist wrlog.ko [ 1597.645196] ! {0113} [ 1597.645196] CR0.c find.c Makefile Modi.hist wrlog.0.c wrlog.1.c wrlog.2.c wrlog.3.c wrlog.c wrlog.hist wrlog.ko [ 1597.645321] ! {0052} /home/olej/2015_WORK/own.BOOK/SysCalls/Modi/examles [ 1597.645951] ! {0078} \x1b[01;32molej@nvidia\x1b[01;34m ~/2015_WORK/own.BOOK/SysCalls/Modi/examles $\x1b[00m [ 1600.226651] ! {0001} s [ 1600.346587] ! {0001} u [ 1600.522683] ! {0001} d [ 1601.026667] ! {0001} o [ 1602.170701] ! {0001} [ 1602.426522] ! {0001} r [ 1603.218682] ! {0001} m [ 1603.682677] ! {0001} m [ 1603.906615] ! {0001} o [ 1604.338566] ! {0001} d [ 1606.442570] ! {0001} [ 1606.946670] ! {0001} w [ 1607.226667] ! {0001} r [ 1607.834662] ! {0001} l [ 1608.106672] ! {0001} o [ 1608.842694] ! {0001} g [ 1612.003059] ! {0002} [ 1612.014102] ! restore old sys_write syscall [c1179f70]
      
      





議論



同様に、Linuxシステムコールの動作を変更できます。 これは、モジュールをロードすることで動的に行われ、アンロードされると、システムの元の動作が復元されます。 この手法の適用分野は広く、開発期間中の監視機能とデバッグ機能、プロジェクトタスクの個々のシステムコールの動作のターゲットを絞った変更などです。



示されているコードは著しく単純化されています。 実際のモジュールは、整合性を確保するために一連の安全対策を講じる必要があります。 たとえば、新しいハンドラー関数は、try_module_get(THIS_MODULE)を呼び出してモジュールの参照カウンターを増やし、関数の実行中にモジュールがアンロードされないようにします(これは非常に小さいですが、有限の確率で可能です)。 戻る前に、関数は逆の処理を行います:module_put(THIS_MODULE)。 たとえば、モジュールのロードおよびアンロード中に他の予防措置が必要になる場合があります。 しかし、これはカーネルモジュールではかなり一般的な手法であり、原則を複雑にしないために説明されていません。



議論の次の部分で、いくつかの追加のニュアンスとテクニックの特殊なケースを示します。



実験用のコードアーカイブは、 ここまたはここから入手できます (例が重要でないため、GitHubには投稿していません)。



PS表示されているものはすべて32ビットで変更なく動作します。 64ビットアーキテクチャでは、32ビットアプリケーションをエミュレートする必要があるため、状況はやや複雑になります。 状況を複雑にしないために、このオプションは意図的に対処されませんでした(おそらく今のところ、後で戻ってみる価値があります)。



All Articles