본문으로 바로가기

[pwnable.kr] lotto 풀이, write-up

category Wargame/pwnable.kr 2018. 12. 27. 16:10

pwnable.kr --- lotto



ssh lotto@pwnable.kr 2222 (pw:guest)로 접속하면 lotto 문제를 만날 수 있다.


windows 환경의 cmd로 접속한것이 아니라 xshell5로 접속했다.


windows 환경의 cmd로 접속하려면 ssh lotto@pwnable.kr -p2222 (pw:guest)로 접속해야한다.



우선 현재 디렉터리에 어떤 파일이 존재하는지 확인해보자.





우리가 공격해야할 대상인 lotto와 lotto elf 파일을 만들었을 source로 예상되는 lotto.c와 flag가 존재한다.


flag는 현재 id(lotto)의 권한으로는 볼 수 없고, lotto elf 파일은 lotto_pwn의 권한으로 setuid가 설정되어있다.


lotto elf를 실행함으로써, setuid를 얻고 flag를 볼 수 있을것으로 예상된다.



lotto.c 파일을 분석해보자.



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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
 
unsigned char submit[6];
 
void play(){
    
    int i;
    printf("Submit your 6 lotto bytes : ");
    fflush(stdout);
 
    int r;
    r = read(0, submit, 6);
 
    printf("Lotto Start!\n");
    //sleep(1);
 
    // generate lotto numbers
    int fd = open("/dev/urandom", O_RDONLY);
    if(fd==-1){
        printf("error. tell admin\n");
        exit(-1);
    }
    unsigned char lotto[6];
    if(read(fd, lotto, 6!= 6){
        printf("error2. tell admin\n");
        exit(-1);
    }
    for(i=0; i<6; i++){
        lotto[i] = (lotto[i] % 45+ 1;        // 1 ~ 45
    }
    close(fd);
    
    // calculate lotto score
    int match = 0, j = 0;
    for(i=0; i<6; i++){
        for(j=0; j<6; j++){
            if(lotto[i] == submit[j]){
                match++;
            }
        }
    }
 
    // win!
    if(match == 6){
        system("/bin/cat flag");
    }
    else{
        printf("bad luck...\n");
    }
 
}
 
void help(){
    printf("- nLotto Rule -\n");
    printf("nlotto is consisted with 6 random natural numbers less than 46\n");
    printf("your goal is to match lotto numbers as many as you can\n");
    printf("if you win lottery for *1st place*, you will get reward\n");
    printf("for more details, follow the link below\n");
    printf("http://www.nlotto.co.kr/counsel.do?method=playerGuide#buying_guide01\n\n");
    printf("mathematical chance to win this game is known to be 1/8145060.\n");
}
 
int main(int argc, char* argv[]){
 
    // menu
    unsigned int menu;
 
    while(1){
 
        printf("- Select Menu -\n");
        printf("1. Play Lotto\n");
        printf("2. Help\n");
        printf("3. Exit\n");
 
        scanf("%d"&menu);
 
        switch(menu){
            case 1:
                play();
                break;
            case 2:
                help();
                break;
            case 3:
                printf("bye\n");
                return 0;
            default:
                printf("invalid menu\n");
                break;
        }
    }
    return 0;
}
cs



main 함수는 사용자로부터 menu를 입력받아, paly(), help(), 종료, exception handling을 진행한다.

help(), 종료, exception handling는 문제가 될만한 source가 없다.

따라서 자세하게 play() 함수를 분석한다.



play() 함수를 분석하면 아래와 같다.


4 byte 정수형 자료형을 하나 선언하고 "Submit your 6 lotto byte: "를 출력하고 output stream을 비운다.



1
2
3
4
5
void play(){
    
    int i;
    printf("Submit your 6 lotto bytes : ");                                        
    fflush(stdout);
cs



4 byte 정수형 자료형을 하나 선언하고, input stream으로부터 6글자를 받아 char 형 array submit에 저장한다.

"Lotto Start!\n"를 출력한다.



1
2
3
    int r;
    r = read(0, submit, 6);
    printf("Lotto Start!\n");                                                    
cs



random number를 얻기 위해 /dev/urandom을 읽는다.

/dev/urandom은 PRNG로서 crypto를 설명할 때 추가적으로 적을 기회가 있을것이다.

rand() 함수와 같이 무작위 난수를 생성한다고 생각하면 쉽다.

만일 file descriptor를 얻는데 실패하면 "error. tell admin\n"을 출력한다.



1
2
3
4
5
    int fd = open("/dev/urandom", O_RDONLY);                                    
    if(fd==-1){
        printf("error. tell admin\n");
        exit(-1);
    }
cs



char 형 array lotto를 선언한다. 이전 fd(file descriptor)를 이용해 /dev/urandom의 난수를 lotto에 6 byte 전달한다.

read() 함수에 실패하면 "error2, tell admin"을 출력한다.



1
2
3
4
5
    unsigned char lotto[6];
    if(read(fd, lotto, 6!= 6){
        printf("error2. tell admin\n");                                            
        exit(-1);
    }
cs



lotto의 수는 1~45 사이의 숫자이기 때문에 /dev/urandom에 의해 생성된 의사 난수를 1~45 사이의 난수로 조정한다.

fd(file descriptor)를를 닫는다.



1
2
3
4
    for(i=0; i<6; i++){
        lotto[i] = (lotto[i] % 45+ 1;        // 1 ~ 45                            
    }
    close(fd);
cs



사용자가 입력한 값(submit)과 /dev/urandom을 통해 얻은 의사 난수의 lotto를 비교한다.

만일 6글자가 같으면 match 1씩 증가시킨다.


하지만 여기서 문제가 발생하는데 lotto를 맞추기 위해서 중복을 신경쓰지 않는다는 것이다.

만일 lotto 번호가 123456이고, 사용자의 입력이 222222이 가능하다 이러한 경우는 6개 모두 일치하는것으로 확인되어 

match가 6이 된다.



1
2
3
4
5
6
7
8
9
10
// calculate lotto score
    int match = 0, j = 0;
    for(i=0; i<6; i++){
        for(j=0; j<6; j++){
            if(lotto[i] == submit[j]){                                            
                match++;
            }
        }
    }
 
cs



만일 match가 6이면 lotto_pwn의 권한으로 "/bin/cat flag"를 출력하고, 

아니라면 "bad luck...\n"을 출력한다.



1
2
3
4
5
6
7
8
// win!
    if(match == 6){
        system("/bin/cat flag");                                                
    }
    else{
        printf("bad luck...\n");
    }
}    
cs



즉, lotto와 submit array를 비교하는데 있어서 사용자의 입력을 중복 체크하지 않아서

우리는 높은 확률로 lotto에 당첨될 수 있다.

1~45 사이의 ascii 문자를 입력하면 높은 확률로 성공 가능하다.

lotto로 선정된 6개의 byte에 +가 있다고 가정하고 공격을 진행한다.





위와 같이 높은 확률로 운좋게 lotto에 당첨되어 flag를 획득할 수 있다.


45개의 문자중 6개의 순서를 모두 맞추는 것보다 45개의 중 6개의 문자 하나를 맞추는 확률이 훨씬 더 높기 때문이다.