私はいくつかのセキュリティ関連のことを研究していて、今は自分のスタックで遊んでいます。私がやっていることは非常に些細なはずです。スタックを実行しようとさえしていません。単に、64 ビット システムで命令ポインターを制御できることを示すためです。知っているすべての保護メカニズムをオフにして、それで遊ぶことができるようにしました(NXビット、ASLR、-fno-stack-protector -z execstackでコンパイルも)。私は 64 ビット アセンブリの経験があまりなく、自分自身を検索して実験するのに時間を費やした後、私が経験している問題に誰かが光を当てることができるかどうか疑問に思っています。
境界チェックなしで文字列をスタック常駐バッファーに単純にコピーするプログラム (以下のソース コード) があります。ただし、一連の 0x41 で上書きすると、RIP が 0x4141414141414141 に設定されることが予想されますが、代わりに RBP がこの値に設定されることがわかります。セグメンテーション違反が発生しますが、RSP が有効な値に設定されていても、RET 命令の実行時に RIP がこの (不正な) 値に更新されません。RET命令の直前にRSPに一連の0x41を含む読み取り可能なメモリがあることをGDBで確認しました。
私は LEAVE 命令がしたという印象を受けました:
MOV (E)SP、(E)BP
POP (E)BP
ただし、64ビットでは、「LEAVEQ」命令が実行されるようです(同様):
MOV RBP、QWORD PTR [RSP]
この命令の実行前後にすべてのレジスタの内容を観察するだけで、これを行うと考えています。ただし、LEAVEQ は RET 命令のコンテキスト依存の名前のようですが (GDB の逆アセンブラーがそれを指定します)、それはまだ 0xC9 のままです。
そして、RET 命令は RBP レジスタで何かを行っているようで、おそらくそれを逆参照していますか? 私は RET がしたという印象を受けました (同様に):
MOV RIP、QWORD PTR [RSP]
ただし、前述のように、RBP を逆参照しているようです。これは、他のレジスタに不正な値が含まれていないように見えるときにセグメンテーション違反が発生するためだと考えています。
プログラムのソース コード:
#include <stdio.h>
#include <string.h>
int vuln_function(int argc,char *argv[])
{
char buffer[512];
for(int i = 0; i < 512; i++) {
buffer[i] = 0x42;
}
printf("The buffer is at %p\n",buffer);
if(argc > 1) {
strcpy(buffer,argv[1]);
}
return 0;
}
int main(int argc,char *argv[])
{
vuln_function(argc,argv);
return 0;
}
for ループは、バッファーの正当な部分を 0x42 で埋めるためだけにあるため、オーバーフローの前に、デバッガーでどこにあるかを簡単に確認できます。
デバッグ セッションの抜粋は次のとおりです。
(gdb) disas vulnerable
Dump of assembler code for function vulnerable:
0x000000000040056c <+0>: push rbp
0x000000000040056d <+1>: mov rbp,rsp
0x0000000000400570 <+4>: sub rsp,0x220
0x0000000000400577 <+11>: mov DWORD PTR [rbp-0x214],edi
0x000000000040057d <+17>: mov QWORD PTR [rbp-0x220],rsi
0x0000000000400584 <+24>: mov DWORD PTR [rbp-0x4],0x0
0x000000000040058b <+31>: jmp 0x40059e <vulnerable+50>
0x000000000040058d <+33>: mov eax,DWORD PTR [rbp-0x4]
0x0000000000400590 <+36>: cdqe
0x0000000000400592 <+38>: mov BYTE PTR [rbp+rax*1-0x210],0x42
0x000000000040059a <+46>: add DWORD PTR [rbp-0x4],0x1
0x000000000040059e <+50>: cmp DWORD PTR [rbp-0x4],0x1ff
0x00000000004005a5 <+57>: jle 0x40058d <vulnerable+33>
0x00000000004005a7 <+59>: lea rax,[rbp-0x210]
0x00000000004005ae <+66>: mov rsi,rax
0x00000000004005b1 <+69>: mov edi,0x40070c
0x00000000004005b6 <+74>: mov eax,0x0
0x00000000004005bb <+79>: call 0x4003d8 <printf@plt>
0x00000000004005c0 <+84>: cmp DWORD PTR [rbp-0x214],0x1
0x00000000004005c7 <+91>: jle 0x4005e9 <vulnerable+125>
0x00000000004005c9 <+93>: mov rax,QWORD PTR [rbp-0x220]
0x00000000004005d0 <+100>: add rax,0x8
0x00000000004005d4 <+104>: mov rdx,QWORD PTR [rax]
0x00000000004005d7 <+107>: lea rax,[rbp-0x210]
0x00000000004005de <+114>: mov rsi,rdx
0x00000000004005e1 <+117>: mov rdi,rax
0x00000000004005e4 <+120>: call 0x4003f8 <strcpy@plt>
0x00000000004005e9 <+125>: mov eax,0x0
0x00000000004005ee <+130>: leave
0x00000000004005ef <+131>: ret
strcpy() の呼び出しの直前に中断しますが、バッファーが 0x42 でいっぱいになった後です。
(gdb) break *0x00000000004005e1
プログラムは引数として 650 個の 0x41 を使用して実行されます。これは、スタック上の戻りアドレスを上書きするのに十分なはずです。
(gdb) run `perl -e 'print "A"x650'`
戻りアドレス 0x00400610 のメモリを検索します (これは main の逆アセンブリを見て見つけたものです)。
(gdb) find $rsp, +1024, 0x00400610
0x7fffffffda98
1 pattern found.
x/200x でメモリを調べると、そのサイズのためにここでは省略した概要が得られますが、バッファの正当なサイズを示す 0x42 と戻りアドレスがはっきりとわかります。
0x7fffffffda90: 0xffffdab0 0x00007fff 0x00400610 0x00000000
strcpy() の直後の新しいブレークポイント:
(gdb) break *0x00000000004005e9
(gdb) set disassemble-next-line on
(gdb) si
19 }
=> 0x00000000004005ee <vulnerable+130>: c9 leave
0x00000000004005ef <vulnerable+131>: c3 ret
(gdb) i r
rax 0x0 0
rbx 0x0 0
rcx 0x4141414141414141 4702111234474983745
rdx 0x414141 4276545
rsi 0x7fffffffe17a 140737488347514
rdi 0x7fffffffdb00 140737488345856
rbp 0x7fffffffda90 0x7fffffffda90
rsp 0x7fffffffd870 0x7fffffffd870
r8 0x1 1
r9 0x270 624
r10 0x6 6
r11 0x7ffff7b9fff0 140737349550064
r12 0x400410 4195344
r13 0x7fffffffdb90 140737488346000
r14 0x0 0
r15 0x0 0
rip 0x4005ee 0x4005ee <vulnerable+130>
0x00000000004005ee <vulnerable+130>: c9 leave
=> 0x00000000004005ef <vulnerable+131>: c3 ret
(gdb) i r
rax 0x0 0
rbx 0x0 0
rcx 0x4141414141414141 4702111234474983745
rdx 0x414141 4276545
rsi 0x7fffffffe17a 140737488347514
rdi 0x7fffffffdb00 140737488345856
rbp 0x4141414141414141 0x4141414141414141
rsp 0x7fffffffda98 0x7fffffffda98
r8 0x1 1
r9 0x270 624
r10 0x6 6
r11 0x7ffff7b9fff0 140737349550064
r12 0x400410 4195344
r13 0x7fffffffdb90 140737488346000
r14 0x0 0
r15 0x0 0
rip 0x4005ef 0x4005ef <vulnerable+131>
(gdb) si
Program received signal SIGSEGV, Segmentation fault.
0x00000000004005ee <vulnerable+130>: c9 leave
=> 0x00000000004005ef <vulnerable+131>: c3 ret
(gdb) i r
rax 0x0 0
rbx 0x0 0
rcx 0x4141414141414141 4702111234474983745
rdx 0x414141 4276545
rsi 0x7fffffffe17a 140737488347514
rdi 0x7fffffffdb00 140737488345856
rbp 0x4141414141414141 0x4141414141414141
rsp 0x7fffffffda98 0x7fffffffda98
r8 0x1 1
r9 0x270 624
r10 0x6 6
r11 0x7ffff7b9fff0 140737349550064
r12 0x400410 4195344
r13 0x7fffffffdb90 140737488346000
r14 0x0 0
r15 0x0 0
rip 0x4005ef 0x4005ef <vulnerable+131>
リターン アドレスが上書きされていることを確認しました。RIP が次のアドレスに設定されることを期待する必要がありました。
(gdb) x/4x 0x7fffffffda90
0x7fffffffda90: 0x41414141 0x41414141 0x41414141 0x41414141
(gdb) x/4x $rsp
0x7fffffffda98: 0x41414141 0x41414141 0x41414141 0x41414141
それでもRIPは明らかに:
rip 0x4005ef 0x4005ef <vulnerable+131>
Why has not RIP gotten updated as I'm expecting? What does LEAVEQ and RETQ really do on 64-bit? In short, what am I missing here? I have tried to omit the compiler arguments when compiling just to see if it makes any difference, it doesn't seem to make any difference.