본문으로 바로가기

해커스쿨 LOB xavius 풀이, write-up [미완]

category Wargame/LOB 2018. 10. 28. 16:56

xavius --- throw me away



문제 풀기 전에 아래의 명령어를 반드시 쳐야한다.



$export SHELL=/bin/bash2

$/bin/bash2




우선 xavius의 디렉터리를 확인한다.





death_knight라는 의심스러운 파일이 존재한다.

death_knight는 death_knight의 권한을 가지고 있고, setuid가 걸려있는 파일이다.


death_knight.c를 가지고 death_knight elf 파일을 만들었을 가능성이 있기 때문에, death_knight 파일을 본 뒤 어떤 취약점을 가지고 있는지 확인해보자.






지역 변수 buffer와 server_fd, client_fd를 선언한다.



1
2
3
char buffer[40];
 
int server_fd, client_fd;                                                            
cs



sockaddr_in 구조체 변수 server_addr, client_addr 두 개를 선언한다.



1
2
struct sockaddr_in server_addr;
struct sockaddr_in client_addr;                                                        
cs



sockaddr_in 구조체는 아래와 같다.


sockaddr_in은 IPv4 주소체계에서 사용하는 구조체이다.

sockaddr_in의 크기는 16 byte이다.



1
2
3
4
5
6
struct sockaddr_in{
    sin_family_t sin_family;        // 항상 AF_INET이다.
    unist16_t sin_port;             // 16-bit port number
    struct in_addr sin_addr;        // 32-bit IP 주소
    char sin_zero[8];               // sockaddr과 같은 크기를 유지하기 위한 zero padding(dummy)
}                                
cs



지역 변수 sin_size를 선언한다.



1
int sin_size;                                                                        
cs



socket() 함수는 소켓을 생성하여 반환하는 함수이다.


socket() 함수의 원형은 아래와 같다.


int socket(int domain, int type, int protocol);


int domain: 인터넷을 통해 통신할지, 같은 시스템 내의 프로세스끼리 통신할지 결정한다.




int type : 데이터의 전송 형태를 지정하며 아래 table의 값을 사용할 수 있다..





int protocol : 통신에 있어 특정 프로토콜을 사용하기 위한 변수이다. 주로 0을 사용한다.


socket() 함수가 성공해 socket을 만들면, 소켓 식별 번호를 return하고, 실패하면 -1을 반환한다.



IPv4 인터넷 프로토콜을 사용하고, TCP/IP 프로토콜을 통해 데이터를 전송하는 socket을 server_fd에 저장하고, 만일 socket 생성에 실패하면 error 내용을 출력하고 종료한다.



1
2
3
4
if((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == -1) {                            
        perror("socket");
        exit(1);
}
cs



htons() 함수는 네트워크에서 short 형의 변수를 host byte order에서 network에서 사용하는 network byte order로 바꿔준다. network에서는 시스템과 달리 big-endian을 사용한다.


sin_port = htons(6666);             //port 6666이 할당된다.

sin_addr.s_addr = INADDR_ANY;    // 소켓이 동작하는 컴퓨터의 IP주소가 자동 할당된다. PORT만 일치하면 수신 가능하다.


1
2
3
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(6666);
server_addr.sin_addr.s_addr = INADDR_ANY;                                            
cs



bzero()함수를 이용해 server_add.sin_zero에 0x00을 8 byte padding 한다.



1
bzero(&(server_addr,sin_zero), 8);            // 8-byte padding                        
cs



bind() 함수는 생성된 소켓에 IP주소와 port 번호를 지정해주는 함수이다.


bind() 함수의 기본형은 아래와 같다.


int bind(int sockfd, struct sockaddr *myaddr, socklen_t addrlen);


int sockfd: 소켓 디스크립터


struct sockaddr *myaddr: AF_INET을 사용하는 경우 struct sockaddr_in을 사용하고, AF_UNIX인 경우 struct sockaddr을 사용한다.


socklen_t addrlen: myaddr의 구조체 크기


성공하면 return 0, 실패하면 return -1을 한다.



생성된 소켓에 주소, 프로토콜, 포트를 할당하고 실패하면 bind에 관한 error 내용을 출력하고 종료한다.



1
2
3
4
if(bind(server_fd, (struct sockaddr *)&server_addr, sizeof(strcut sockaddr)) == -1){
        perror("bind");
        exit(1);
}
cs



listen() 함수는 소켓을 통해 클라이언트 접속 요청을 기다리도록 설정한다. 

즉, 클라이언트를 대기하고 있는 상태이다.


listen() 함수의 기본형은 아래와 같다.


int listen(int s, int backlog);


ins sockfd : 소켓 디스크립터

int backlog : 대기 메세지 큐의 개수


성공하면 return 0, 실패하면 return -1을 한다.


클라이언트를 대기한다. 만일 listen() 함수가 실패하면 listen에 대한 error를 출력하고 종료한다.



1
2
3
4
if(lisetn(server_fd, 10== -1) {                                                    
        perror("listen");
        exit(1);
}
cs



sin_size에 sockaddr_in 구조체의 크기를 저장한다.



1
sin_size = sizeof(struct sockaddr_in);                                                
cs




accept() 함수는 클라이언트의 접속 요청을 받고, 클라이언트와 통신하는 전용 소켓을 생성한다.


accept() 함수의 기본형은 아래와 같다.


int accept (int sockfd, struct sockaddr *addr, socklen_t *addrlen);


int sockfd : 소켓 디스크립터


struct sockaddr *addr : client의 주소 정보를 가지고 있는 포인터이다.


socklen_t addrlen: struct sockaddr *addr 포인터가 가르키는 구조체의 크기



-1을 반환하면 실패, -1 이외의 값은 새로운 소켓 디스크립터의 번호이다.


클라이언트 접속을 요청할 때, 클라이언트와 통신하는 전용 소켓을 생성한다. 만일 실패하면 accept에 관한 실패 error를 출력하고 종료한다.



1
2
3
4
if((client_fd = accept(server_fd, (struct sockaddr *)&clinet_addr, &sin_size)) == -1) {
        perror("accept");
        continue;
}
cs




fork() 함수는 현재 실행하고 있는 프로세스를 복사해 똑같은 기능을 하는 프로세스를 만드는 함수이다.


fork() 함수의 원형은 아래와 같다.


pid_t fork(void);


실행에 실패하면 return -1, 성공하면 부모 프로세스에게 새로 생성된 자식 프로세스 PID가 반환되고, 자식 프로세스는 0이 반환된다.


send() 함수는 연결된 서버나 클라이언트로 데이터를 전송한다.


send() 함수의 원형은 아래와 같다.


int send(int sockfd, const void *msg, size_t len, int flags);


int sockfd: 소켓 디스크립터


void *msg: 전송할 데이터


size_t len: 데이터의 바이트 단위 길이


int flags: 아래의 옵션이 가능하다. 옵션을 사용하지 않으명 0이다.



return -1 일 때 실패했고, -1 이외의 수가 return 되면 실제 전송한 byte 수이다.


fork가 성공하면 아래와 같은 행위를 한다.


client에 "Death Knight : Not even death can save you from me!\n"를 52 byte 전송한다.

client에 "You: " 를 6 byte 전송한다.



1
2
3
if(!fork()){
        send(client_fd, "Death Knight : Not even death can save you from me!\n"520);
        send(client_fd, "You : "60);
cs



recv() 함수는 소켓으로부터 데이터를 수신한다.


recv() 함수의 원형은 아래와 같다.


int recv(int sockfd, void *buf, size_t len, int flags);


int sockfd : 소켓 디스크립터


void *buf : 수신할 버퍼 포인터 데이터


size_t len : 버퍼의 byte size


int flags : 아래의 옵션을 사용가능하다 사용하지 않으면 0으로 set 한다.


return -1이면 실패, -1 이외의 값을 return 하면 실제 수신한 byte의 수 이다.



close() 함수는 파일 디스크립터를 종료하는 함수이다.


close() 함수의 원형은 아래와 같다.


int close(int fd);


int fd: 파일 디스크립터


정상적으로 close 했으면 0을 return, close에 실패하면 -1을 반환한다.


client_fd를 종료하고, 반복문에 탈출한다.



1
2
3
4
        recv(client_fd, buffer, 2560);                                            
        close(client_fd);
        break;
}
cs



clinet_fd를 한번 더 종료한다. fork()에 의해 종료가 되지 않을수도 있다.


waitpid() 함수는 자식 프로세스가 종료될때까지 대가하는 함수이다. 특정 자식 프로세스가 종료될 때까지 대기한다.


waitpid() 함수는 아래와 같다.


pid_t waitpid(pid_t pid, int *status, int options);


pid_t pid: 감사할 자식 프로세스 ID로서 아래의 옵션을 갖는다.




int *status: 자식 프로세스의 종료 상태 정보


int options : 대기를 위한 옵션으로 아래와 같은 옵션을 갖는다.




정상적으로 작동되면 종료된 자식 프로세스 ID를 return 한다. 실패하면 -1을 return 한다. WNOHANG을 사용하고 자식 프로세스가 종료되지 않으면 0을 return 한다.


자식 프로세스가 종료되지 않아야하고, client를 종료한다.



1
2
3
        close(clinet_fd);
        while(waitpid(-1NULL, WNOHANG) > 0 );                                        
    }
cs



server_fd를 종료한다.



1
2
    close(server_fd);                                                                
}
cs




death_knight의 메모리 구조를 예상해보면 아래와 같다.






실제 변수가 할당될 때, gcc가 최적화를 위해 자동적으로 dummy를 생성할 수 있다.


gdb를 이용해 death_knight를 분석해보자. death_night를 gdb로 실행하는 도중 permission denied가 발생하면 cp 명령을 이용하여 파일을 복사하면 된다. 현재 user의 권한으로 똑같은 파일을 가질 수 있기 때문이다.





위와 같이 death_knight가 xavius의 권한으로 복사된 것을 확인할 수 있다.



dummy가 생성되었는지 gdb를 통해 확인할 수 있다.





dummy가 추가적으로 생성되지 않고 정확히 84(0x54) byte가 생성된 것을 확인할 수 있다.