最近のLinuxカーネルバージョン(5.4より前の場合もあります)までは、gcc -z execstack
-を使用してコンパイルするだけで、読み取り専用データ( )や読み取り/書き込みデータ()を含むすべてのページが実行可能になります。.rodata
.data
char code[] = "..."
現在-z execstack
は実際のスタックにのみ適用されるため、現在は非定数ローカル配列に対してのみ機能します。 つまり、に移動char code[] = ...
しmain
ます。
カーネルの変更については、「。data」セクションに対するLinuxのデフォルトの動作、および古い動作のプロジェクトにアセンブリファイルが含まれている場合のmmapからの予期しないexec権限を参照してください:そのプログラムに対してLinuxのREAD_IMPLIES_EXEC
プロセスを有効にします。(Linux 5.4では、そのQ&Aは、本当に古いバイナリのようREAD_IMPLIES_EXEC
に、欠落しているものだけを取得することを示しています。最新のGCCは、実行可能ファイルにメタデータを設定し、Linux 5.4は、スタック自体のみを実行可能にするものとして処理します。その前のある時点で、結果として。)PT_GNU_STACK
-z execstack
PT_GNU_STACK = RWX
PT_GNU_STACK = RWX
READ_IMPLIES_EXEC
もう1つのオプションは、実行時にシステムコールを実行して実行可能ページにコピーするか、ページのアクセス許可を変更することです。これは、ローカル配列を使用してGCCにコードを実行可能スタックメモリにコピーさせるよりもさらに複雑です。
(最新のカーネルで有効にする簡単な方法があるかどうかはわかりませんREAD_IMPLIES_EXEC
。ELFバイナリにGNUスタック属性がまったくない場合は、32ビットコードでは有効になりますが、64ビットでは有効になりません。)
さらに別のオプションは、__attribute__((section(".text"))) const char code[] = ...;
実用的な例です:https ://godbolt.org/z/draGeh 。
配列を書き込み可能にする必要がある場合、たとえば、文字列にゼロを挿入するシェルコードの場合は、とリンクすることができますld -N
。ただし、おそらく-zexecstackとローカル配列を使用するのが最適です。
質問の2つの問題:
- noexecの読み取り+書き込み
.data
セクションに配置される配列を使用したため、ページのexec権限。
- マシンコードは
ret
命令で終わらないので、たとえ実行されたとしても、実行は戻るのではなく、メモリ内の次にあるものに分類されます。
そしてところで、REXプレフィックスは完全に冗長です。 "\x31\xc0"
xor eax,eax
とまったく同じ効果がありxor rax,rax
ます。
実行権限を取得するには、マシンコードを含むページが必要です。x86-64ページテーブルには、従来の386ページテーブルとは異なり、読み取り権限とは別に実行するための個別のビットがあります。
静的配列をread+execメモリに配置する最も簡単な方法は、を使用してコンパイルすることでしたgcc -z execstack
。(スタックおよび他のセクションを実行可能にするために使用され、現在はスタックのみです)。
最近(2018年または2019年)まで、標準ツールチェーン(binutils ld
)はセクション.rodata
をと同じELFセグメント.text
に配置するため、両方に読み取り+実行権限がありました。したがってconst char code[] = "...";
、execstackを使用せずに、手動で指定したバイトをデータとして実行するには、を使用するだけで十分でした。
しかし、私のArch LinuxシステムではGNU ld (GNU Binutils) 2.31.1
、それはもはや当てはまりません。 readelf -a
は、セクションがと.rodata
でELFセグメントに入り、読み取り権限しか持っていないことを示しています。 Read + Execでセグメントに入り、Read + Writeでセグメントに入ります(およびと一緒に)。(ELFファイル形式のセクションとセグメントの違いは何ですか).eh_frame_hdr
.eh_frame
.text
.data
.got
.got.plt
この変更は、ret
またはjmp reg
命令のバイトで終わる「ガジェット」として有用なバイトのシーケンスを使用できる実行可能ページに読み取り専用データを持たないことにより、ROPおよびSpectre攻撃をより困難にすることだと思います。
// TODO: use char code[] = {...} inside main, with -z execstack, for current Linux
// Broken on recent Linux, used to work without execstack.
#include <stdio.h>
// can be non-const if you use gcc -z execstack. static is also optional
static const char code[] = {
0x8D, 0x04, 0x37, // lea eax,[rdi+rsi] // retval = a+b;
0xC3 // ret
};
static const char ret0_code[] = "\x31\xc0\xc3"; // xor eax,eax ; ret
// the compiler will append a 0 byte to terminate the C string,
// but that's fine. It's after the ret.
int main () {
// void* cast is easier to type than a cast to function pointer,
// and in C can be assigned to any other pointer type. (not C++)
int (*sum) (int, int) = (void*)code;
int (*ret0)(void) = (void*)ret0_code;
// run code
int c = sum (2, 3);
return ret0();
}
古いLinuxシステムの場合:(グローバル/静的アレイgcc -O3 shellcode.c && ./a.out
で動作するため)const
5.5より前のLinux(またはそれくらい)gcc -O3 -z execstack shellcode.c && ./a.out
(-zexecstack
マシンコードがどこに保存されているかに関係なく動作します)。おもしろい事実:gccは-zexecstack
スペースなしで許可しますが、clangは。のみを受け入れますclang -z execstack
。
.rdata
これらは、の代わりに読み取り専用データが入力されるWindowsでも機能します.rodata
。
コンパイラによって生成されたmain
ものは次のようになります(from objdump -drwC -Mintel
)。 内部で実行し、ブレークポイントを設定できますgdb
。code
ret0_code
(I actually used gcc -no-pie -O3 -zexecstack shellcode.c hence the addresses near 401000
0000000000401020 <main>:
401020: 48 83 ec 08 sub rsp,0x8 # stack aligned by 16 before a call
401024: be 03 00 00 00 mov esi,0x3
401029: bf 02 00 00 00 mov edi,0x2 # 2 args
40102e: e8 d5 0f 00 00 call 402008 <code> # note the target address in the next page
401033: 48 83 c4 08 add rsp,0x8
401037: e9 c8 0f 00 00 jmp 402004 <ret0_code> # optimized tailcall
または、システムコールを使用してページの権限を変更します
でコンパイルする代わりにgcc -zexecstack
、を使用mmap(PROT_EXEC)
して新しい実行可能ページを割り当てたりmprotect(PROT_EXEC)
、既存のページを実行可能ファイルに変更したりできます。(静的データを保持するページを含みます。)もちろん、通常は少なくともPROT_READ
、場合PROT_WRITE
によっては必要です。
静的配列で使用mprotect
するということは、既知の場所からコードを実行していることを意味し、ブレークポイントを設定しやすくなる可能性があります。
Windowsでは、VirtualAllocまたはVirtualProtectを使用できます。
データがコードとして実行されることをコンパイラーに通知する
通常、GCCのようなコンパイラは、データとコードが分離していると想定します。これはタイプベースの厳密なエイリアシングに似ていますが、使用してもchar*
、バッファに格納してそのバッファを関数ポインタとして呼び出すことは明確に定義されていません。
__builtin___clear_cache(buf, buf + len)
GNU Cでは、マシンコードバイトをバッファに書き込んだ後にも使用する必要があります。これは、オプティマイザが関数ポインタの逆参照をそのアドレスからのバイトの読み取りとして処理しないためです。デッドストアの削除は、ストアがデータとして読み取られていないことをコンパイラが証明した場合に、マシンコードバイトのストアをバッファに削除できます。 https://codegolf.stackexchange.com/questions/160100/the-repetitive-byte-counter/160236#160236およびhttps://godbolt.org/g/pGXn3Bには、gccが実際にこの最適化を行う例があります。 「知っている」malloc
。
(また、I-cacheがD-cacheとコヒーレントでない非x86アーキテクチャでは、実際に必要なキャッシュ同期を実行します。x86では、純粋にコンパイル時の最適化ブロッカーであり、命令自体には拡張されません。)
Re:3つのアンダースコアが付いた奇妙な名前:これは通常の__builtin_name
パターンですが、name
です__clear_cache
。
@AntoineMathysの回答に対する私の編集はこれを追加しました。
mmap(MAP_ANONYMOUS)
実際には、GCC/clangは彼らが知っている方法を「知りません」malloc
。したがって、実際には、オプティマイザは、バッファへのmemcpyが、。がなくても、関数ポインタを介した非インライン関数呼び出しによってデータとして読み取られる可能性があると想定します__builtin___clear_cache()
。(関数型をとして宣言した場合を除きます__attribute__((const))
。)
I-cacheがデータキャッシュとコヒーレントであるx86では、呼び出しが正確である前に、ストアをasmで発生させます。他のISAでは、__builtin___clear_cache()
実際に特別な命令を発行するだけでなく、正しいコンパイル時の順序を保証します。
パフォーマンスのコストがかからず、仮想の将来のコンパイラがコードを壊さないようにするため、コードをバッファにコピーするときに含めることをお勧めします。(たとえば、mmap(MAP_ANONYMOUS)
mallocのように、他に何もポインタを持たない、新しく割り当てられた匿名メモリを提供することを理解している場合。)
__attribute__((const))
sum()
現在のGCCでは、オプティマイザーが純粋関数(グローバルメモリではなく引数のみを読み取る)であることを通知するために使用することで、GCCに不要な最適化を実際に実行させることができました。GCCはsum()
、結果をデータとして読み取ることができないことを認識memcpy
します。
呼び出し後に別のバッファを同じバッファに入れると、GCCは呼び出し後memcpy
の2番目のストアだけにデッドストア除去を実行します。これにより、最初の呼び出しの前にストアがなくなるため、バイトが実行され、segfaultingされます。00 00 add [rax], al
// demo of a problem on x86 when not using __builtin___clear_cache
#include <stdio.h>
#include <string.h>
#include <sys/mman.h>
int main ()
{
char code[] = {
0x8D, 0x04, 0x37, // lea eax,[rdi+rsi]
0xC3 // ret
};
__attribute__((const)) int (*sum) (int, int) = NULL;
// copy code to executable buffer
sum = mmap (0,sizeof(code),PROT_READ|PROT_WRITE|PROT_EXEC,
MAP_PRIVATE|MAP_ANON,-1,0);
memcpy (sum, code, sizeof(code));
//__builtin___clear_cache(sum, sum + sizeof(code));
int c = sum (2, 3);
//printf ("%d + %d = %d\n", a, b, c);
memcpy(sum, (char[]){0x31, 0xc0, 0xc3, 0}, 4); // xor-zero eax, ret, padding for a dword store
//__builtin___clear_cache(sum, sum + 4);
return sum(2,3);
}
GCC9.2 -O3を使用してGodboltコンパイラエクスプローラでコンパイル
main:
push rbx
xor r9d, r9d
mov r8d, -1
mov ecx, 34
mov edx, 7
mov esi, 4
xor edi, edi
sub rsp, 16
call mmap
mov esi, 3
mov edi, 2
mov rbx, rax
call rax # call before store
mov DWORD PTR [rbx], 12828721 # 0xC3C031 = xor-zero eax, ret
add rsp, 16
pop rbx
ret # no 2nd call, CSEd away because const and same args
異なる引数を渡すと別の引数が得call reg
られますが、2回の呼び出しでもCSEを実行でき__builtin___clear_cache
sum(2,3)
ます。 __attribute__((const))
関数のマシンコードへの変更を尊重しません。しないでください。ただし、関数を1回JITしてから何度も呼び出す場合は、安全です。
__clear_cache
最初の結果のコメントを外す
mov DWORD PTR [rax], -1019804531 # lea; ret
call rax
mov DWORD PTR [rbx], 12828721 # xor-zero; ret
... still CSE and use the RAX return value
最初の店は__clear_cache
、sum(2,3)
電話のためにそこにあります。(最初のsum(2,3)
呼び出しを削除すると、デッドストアの削除が全体で発生し__clear_cache
ます。)
によって返されるバッファへの副作用mmap
が重要であると想定されているため、2番目のストアがあり、それが最終的な値にmain
なります。
プログラムを実行するGodboltの./a.out
オプションは、依然として常に失敗しているようです(終了ステータス255)。多分それはJITingをサンドボックス化しますか?それは私のデスクトップで動作し、__clear_cache
なしでクラッシュします。
mprotect
既存のC変数を保持するページ。
単一の既存のページに読み取り+書き込み+実行権限を与えることもできます。これは、でコンパイルする代わりになります-z execstack
__clear_cache
最適化するストアがないため、読み取り専用のC変数を保持するページは必要ありません。(スタック上の)ローカルバッファを初期化するためにも必要です。それ以外の場合、GCCは、非インライン関数呼び出しが確実にポインターを持たないこのプライベートバッファーの初期化子を最適化します。(エスケープ分析)。を介して指定しない限り、バッファが関数のマシンコードを保持する可能性は考慮されません__builtin___clear_cache
。
#include <stdio.h>
#include <sys/mman.h>
#include <stdint.h>
// can be non-const if you want, we're using mprotect
static const char code[] = {
0x8D, 0x04, 0x37, // lea eax,[rdi+rsi] // retval = a+b;
0xC3 // ret
};
static const char ret0_code[] = "\x31\xc0\xc3";
int main () {
// void* cast is easier to type than a cast to function pointer,
// and in C can be assigned to any other pointer type. (not C++)
int (*sum) (int, int) = (void*)code;
int (*ret0)(void) = (void*)ret0_code;
// hard-coding x86's 4k page size for simplicity.
// also assume that `code` doesn't span a page boundary and that ret0_code is in the same page.
uintptr_t page = (uintptr_t)code & -4095ULL; // round down
mprotect((void*)page, 4096, PROT_READ|PROT_EXEC|PROT_WRITE); // +write in case the page holds any writeable C vars that would crash later code.
// run code
int c = sum (2, 3);
return ret0();
}
この例で使用PROT_READ|PROT_EXEC|PROT_WRITE
したので、変数がどこにあるかに関係なく機能します。それがスタック上のローカルであり、省略した場合、スタックがリターンアドレスをプッシュしようとしたときにのみ読み取り専用にした後、失敗しますPROT_WRITE
。call
また、PROT_WRITE
自己修正するシェルコードをテストできます。たとえば、ゼロを独自のマシンコード、または回避していた他のバイトに編集できます。
$ gcc -O3 shellcode.c # without -z execstack
$ ./a.out
$ echo $?
0
$ strace ./a.out
...
mprotect(0x55605aa3f000, 4096, PROT_READ|PROT_WRITE|PROT_EXEC) = 0
exit_group(0) = ?
+++ exited with 0 +++
コメントアウトするとmprotect
、GNU Binutilsの最近のバージョンでセグメンテーション違反がld
発生し、読み取り専用の定数データがセクションと同じELFセグメントに配置されなくなります.text
。
のようなことをした場合は、その後、ストアが最適化されていないことを確認ret0_code[2] = 0xc3;
する必要があり__builtin___clear_cache(ret0_code+2, ret0_code+2)
ますが、静的配列を変更しない場合は、その後は必要ありませんmprotect
。C(with)で書き込まれたバイトを実行したいので、mmap
+または手動ストアの後に必要です。memcpy
memcpy