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", 52, 0); send(client_fd, "You : ", 6, 0); | 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, 256, 0); 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(-1, NULL, 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가 생성된 것을 확인할 수 있다.
'Wargame > LOB' 카테고리의 다른 글
해커스쿨 LOB nightmare 풀이, write-up (0) | 2018.10.23 |
---|---|
해커스쿨 LOB succubus 풀이, write-up (0) | 2018.10.12 |
해커스쿨 LOB zombie_assassin 풀이, write-up (0) | 2018.09.20 |
해커스쿨 LOB assassin 풀이, write up (0) | 2018.09.17 |
해커스쿨 LOB giant 풀이, write up (0) | 2018.09.16 |