티스토리 뷰

Security/pwnable.kr

3. input

곰국 Gomsoup 2017.02.28 07:33

개인적으로 재미있었던 문제. 한국어로 된 Writeup이 없어서 써본다.





언제나 그렇듯 안내되는 ssh로 접속하면 input 바이너리와 소스, flag가 보인다.

input.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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <arpa/inet.h>
 
int main(int argc, char* argv[], char* envp[]){
    printf("Welcome to pwnable.kr\n");
    printf("Let's see if you know how to give input to program\n");
    printf("Just give me correct inputs then you will get the flag :)\n");
 
    //argv
    if(argc != 100return 0;
    if(strcmp(argv['A'], "\x00")) return 0;
    if(strcmp(argv['B'], "\x20\x0a\x0d")) return 0;
    printf("Stage 1 clear!\n");
 
    //stdio
    char buf[4];
    read(0, buf, 4);
    if(memcmp(buf, "\x00\x0a\x00\xff"4)) return 0;
    read(2, buf, 4);
    if(memcmp(buf, "\x00\x0a\x02\xff"4)) return 0;
    printf("Stage 2 clear!\n");
    
    //env
    if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
    printf("Stage 3 clear!\n");
 
    //file
    FILE *fp = fopen("\x0a""r");
    if(!fp) return 0;
    if(fread(buf, 41, fp) != 1return 0;
    if(memcmp(buf, "\x00\x00\x00\x00"4return 0;
    fclose(fp);
    printf("Stage 4 clear!\n");
    
    //network
    int sd, cd;
    struct sockaddr_in saddr, caddr;
    sd = socket(AF_INET, SOCK_STREAM, 0);
    if(sd == -1){
        printf("socket error, tell admin\n");
        return 0;
    }    
    saddr.sin_family = AF_INET;
    saddr.sin_addr.s_addr = INADDR_ANY;
    saddr.sin_port = htons(atoi(argv['C']));
    if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
        printf("bind error, use another port\n");
        return 1;
    }
    listen(sd, 1);
    int c = sizeof(struct sockaddr_in);
    cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
    if(cd < 0){
        printf("accept error, tell admin\n");
        return 0;
    }
    if(recv(cd, buf, 40!= 4return 0;
    if(memcmp(buf, "\xde\xad\xbe\xef"4)) return 0;
    printf("Stage 5 clear!\n");
 
    // here's your flag
    system("/bin/cat/flag");
    return 0;
}
cs


전형적인 pwn문제. 총 5개의 Stage로 나뉘며 각각 argv, env, stdio, file io, socket 을 통해 통과해야한다.



1. Stage 1, 3



Stage 1과 3의 exploit 코드를 같이 짜기로 한다. 어차피 execve로 실행시킬때 환경변수와 Arguments들은 같이 들어가야한다.


먼저 Stage 1부터.


1
2
3
4
5
6
    //argv
    if(argc != 100return 0;
    if(strcmp(argv['A'], "\x00")) return 0;
    if(strcmp(argv['B'], "\x20\x0a\x0d")) return 0;
    printf("Stage 1 clear!\n");
 
cs


1. argv의 개수가 정확히 100개여야 한다
2. argv['A'] 에 "\x00"가 담겨있어야 한다. ASCII로 A는 65이므로 argv[65]에 있어야 함.
3. argv['B'] 에 "\x20\x0a\x0d"가 담겨있어야 한다. 마찬가지 이유로 argv[66]에 있어야 함.
4. argv[100]은 NULL이 담겨있어야함. 안그러면 execve 작동이 실패한다. 이걸로 삽질 좀 했음.

다음은 Stage 3다.

1
2
3
//env
if(strcmp("\xca\xfe\xba\xbe", getenv("\xde\xad\xbe\xef"))) return 0;
printf("Stage 3 clear!\n");
cs

별건 없다. "\xde\xad\xbe\xef"라는 이름의 환경변수 내용이 "\xca\xfe\xba\xbe"면 된다.
마찬가지로 마지막엔 NULL이 들어가야함.


해당 내용을 바탕으로 exploit 코드를 짜보자.

1
2
3
4
5
6
7
8
9
char *argv[101], *env[2];
 
for(int i=0; i<100; i++)
    argv[i] = "A";
 
argv['A'= "\x00";    argv['B'= "\x20\x0a\x0d";
argv[100= NULL
 
env[0= "\xde\xad\xbe\xef=\xca\xfe\xba\xbe"; env[1= NULL;
cs

완성.



2. Stage 2.


삽질의 핵심. 처음 보는 내용들이 나와 당황했다. 우선 주석에는 stdio로 입력을 받는데 내용에 입력받는 내용이 read 함수 두개 밖에 없다. 
read 함수의 arguments를 보면 각각의 File Descriptor는 stdin과 stderr를 가르키고 있다.
구글에서 이것 저것 검색하다가, Pipe를 이용한 IPC를 사용하는 방법이 나왔다. (http://unixwiz.net/techtips/remap-pipe-fds.html)



즉, 프로세스간 통신이 필요한 경우 리눅스 커널에 파이프를 제공받아 해당 파이프로 통신한다. 이때 하나의 파이프는 양방향 송수신이 불가하다. 해당 프로세스간 양방향 송, 수신이 필요하다면, 파이프 2개를 열어야 한다. 

우리는 양방향 송수신은 필요없지만, read 함수에서 stdin과 stderr 2개의 File Descriptor를 사용하고 있으므로 편의를 위해 2개의 파이프를 열어주자.


해당 사이트에 나와있는 예제 소스와 똑같이 exploit 코드를 짜준다.


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
int stdin_pipe[2= {-1-1};
int stderr_pipe[2= {-1-1};
pid_t childpid;
 
if(pipe(stdin_pipe) < 0 || pipe(stderr_pipe) < 0){
    printf("Can't create pipe!\n");
    exit(1);
}
 
childpid = fork();
if(childpid < 0){
    printf("Can't fork child!\n");
    exit(1);
}
 
if(childpid == 0){ // child process case
    write(stdin_pipe[1], "\x00\x0a\x00\xff"4);
    write(stderr_pipe[1], "\x00\x0a\x02\xff"4);
    close(stdin_pipe[1]); close(stderr_pipe[1]);
}
else//parent process case
    dup2(stdin_pipe[0], 0); dup2(stderr_pipe[0], 2);
    close(stdin_pipe[0]) close(stderr_pipe[0]);
    execve("/home/input2/input", argv, env);
}
cs


별 다른 내용은 없다. 다만 pipe의 경우 File Descriptor가 stdin과 stderr가 아니므로, dup2를 통해 강제로 지정하는 작업을 거친다.

이 후 execve를 통해 프로그램을 실행시키며, 이 때 아까 Stage 1, 3에서 작성된 argv와 env를 인자로 준다.




3. Stage 4



1
2
3
4
5
6
FILE *fp = fopen("\x0a""r");
if(!fp) return 0;
if(fread(buf, 41, fp) != 1return 0;
if(memcmp(buf, "\x00\x00\x00\x00"4return 0;
fclose(fp);
printf("Stage 4 clear!\n");
cs


간단한 File io문제. \x0a라는 이름을 가진 파일의 내용이 \x00\x00\x00\x00인지 확인한다. 따라서 해당 내용대로 파일을 작성해주면 될 것이다.



1
2
3
FILE *fp = fopen("\x0a""w");
fwrite("\x00\x00\x00\x00"41, fp);
fclose(fp);
cs


끝. 쉽다. 




4. Stage 5



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int sd, cd;
struct sockaddr_in saddr, caddr;
sd = socket(AF_INET, SOCK_STREAM, 0);
if(sd == -1){
    printf("socket error, tell admin\n");
    return 0;
}    
saddr.sin_family = AF_INET;
saddr.sin_addr.s_addr = INADDR_ANY;
saddr.sin_port = htons(atoi(argv['C']));
if(bind(sd, (struct sockaddr*)&saddr, sizeof(saddr)) < 0){
    printf("bind error, use another port\n");
    return 1;
}
listen(sd, 1);
int c = sizeof(struct sockaddr_in);
cd = accept(sd, (struct sockaddr *)&caddr, (socklen_t*)&c);
if(cd < 0){
    printf("accept error, tell admin\n");
    return 0;
}
if(recv(cd, buf, 40!= 4return 0;
if(memcmp(buf, "\xde\xad\xbe\xef"4)) return 0;
printf("Stage 5 clear!\n"); 
cs


Socket을 이용해 통신하는 방법을 묻는 문제. 다른건 서버를 열고 listen모드까지 맞추는거라 다 필요없고 10번과 23, 24번 라인이 핵심이다.

10번 라인에서 argv['C'] 에 담긴 내용이 서버의 포트가 된다. 따라서 아까 Stage 1,3를 풀때 한줄 더 추가해서 포트를 지정해줘야한다.

22,23번 라인에서 해당 내용을 전송받을 시 Stage는 클리어된다. 


서버가 짜여져 있으니 해당 내용에 맞춰 클라이언트를 짜주면 되는 문제.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sleep(2); // for wait server opening
int client_socket;
 
client_socket = socket(AF_INET, SOCK_STREAM, 0);
if(client_socket < 0){
    printf("Fail to create socket!\n");
    exit(1);
}
 
struct sockaddr_in    server_addr;
memset(&server_addr, 0sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(45454); // port
server_addr.sin_addr.s_addr = inet_addr("127.0.0.1"); // loopback
 
if(connect(client_socket, (struct sockaddr*)&server_addr, sizeof(server_addr)) < 0){
    printf("Connection Failed!\n");
    exit(1);
}
printf("Connected!!\n");
send(client_socket, "\xde\xad\xbe\xef"40);
close(client_socket);
cs


쉽다.




5. etc.


pwnable.kr은 /tmp 디렉토리에 개인용 디렉토리를 생성할 수 있도록 설정되어 있다. 이렇게 exploit 코드의 작성 및 컴파일이 필요할때는 mkdir를 통해 자신의 폴더를 만들어 사용하도록 하자.


물론 나도 코드 컴파일까지 완료하고 실행했는데




??????????????????????????????????????????????????









이것때문에 또 삽질했다. 알고보니 내가 프로그램을 실행한곳은 /tmp/gomsoup인데 해당 바이너리는 /home/input2에 있으니 flag를 읽어올 수 있을 리가 없었던것


심볼릭링크 주고 다시해보자..






//참고 문서


http://forum.falinux.com/zbxe/index.php?document_srl=419982&mid=C_LIB // UNIX에서 PIPE를 사용한 IPC

http://forum.falinux.com/zbxe/index.php?document_srl=412814&mid=C_LIB // process copy with fork();




'Security > pwnable.kr' 카테고리의 다른 글

3. input  (0) 2017.02.28
1. fd  (0) 2016.07.30
댓글
댓글쓰기 폼
공지사항
최근에 달린 댓글
Total
6,446
Today
5
Yesterday
49
링크
TAG
more
«   2018/11   »
        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  
글 보관함