[UNIX 시스템 프로그래밍 second Edition - KEITH HAVILAND저] 연습문제 풀이입니다.


(저작권에 저촉 될 우려가 있어 문제 내용은 생략합니다.)


연습문제 2.1입니다.

간단한 문제로 따로 설명은 필요하지 않을 듯 합니다.


코드는 C++로 작성하였습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdlib.h>
#include <fcntl.h>
#include <stdio.h>
 
char *workfile="junk";
 
int main() {
    int filedes;
 
    if((filedes = open(workfile, O_RDWR)) == -1)
    {
        printf("Couldn't open %s\n", workfile);
        exit(1);
    }
    exit(0);
}
 



프로젝트 주제로 ALZip 프로그램을 Fuzzing을 시도했습니다.

결과부터 말하면 ALZip Fuzzing은 실패했습니다. 구현과정에서 얻은 점이 많이 내용을 남깁니다.



프로젝트 진행 사항입니다.


 - Fuzzing 대상 프로그램 : ALZip (v10.72)


 - 입력 파일 포맷 : zip

입력 파일 포맷의 경우 전세계에서 가장 많이 사용되는 zip 포맷을 사용하기로 하였습니다.

아직까지 zip 포맷의 확장 정의가 관리되고 있고, 공개 된 파일 포맷이기에 포맷 분석 및 프로그램 테스트가 용이하리라 판단되었습니다.



 - zip 파일 구조


 Zip 파일의 구조는 위 그림과 같이 나타 납니다.

앞에 여러개의 파일 Entry 부분은 Zip 형식으로 압축 된(deflate 알고리즘) 각각의 데이터를 의미하고 Local header는 각각의 데이터에 대한 설명을 기술합니다. Zip 파일 자체의 Header는 뒤에 있는 Central Directory 부분이며 그 중에서도 가장 끝 부분에 위치하는 end of Central Directory에서 Central Directory의 offset을 알아내고 Central Directory의 구조를 설명합니다. 이를 통해 Zip 파일의 경우 전체 Zip 아카이브를 읽지 않고 Central Directory 부분만을 읽어 파일 목록을 로드 할 수 있습니다.



 - zip 파일의 각 구조에 대한 설명

# Local File Header

Local file header는 파일의 0번부터 시작하며, 순차적으로 N번째 파일까지 이어집니다.

Local file header에는 Zip파일 내부에 존재하는 각 파일에 대한 정보들(파일명, 압축 정보, CRC-32, ...)을 가지고 있습니다.

Local file header뒤에는 압축에 암호화가 되어있는 경우와 암호화가 되어있지 않은 경우로 뒤에 위치하는 데이터가 다릅니다.

암호화가 되어 있는 경우에는 encryption header가 뒤에 온 후 file data가 오게 되며, 암호화가 되어 있지 않은 경우에는 바로 file data가 오게 됩니다.


# Central Directory

 Central Directory는 zip파일에 포함된 파일들의 정보를 가지고 있는 Local header의 정보(Local header의 offset, 파일명, 압축사이즈 등)를 가지고 있습니다.

End of Central Directory에서 얻은 offset을 통해 Central Directory들의 첫 시작 주소를 찾았다면, 이 또한 역시 4바이트를 읽어 signature(0x02014B50)와 비교하여 검증을 해볼 수 있습니다. 일치 하지 않는다면, 위에서 offset을 잘 못 찾았다고 볼 수 있습니다.


 Central Directory들은 Local file header와 달리 연속되어 존재합니다. 

첫 번째 파일의 Central Directory -> 두 번째 파일의 Central Directory -> ....... -> N 번째 파일의 Central Directory


 Central Directory 역시 파일명, extra field, file comment등의 가변 크기의 변수를 갖지만 사이즈를 가지고 있으므로, 읽는데 어려움은 없습니다. file comment의 끝에 바로 다음 Central Directory가 시작되기 때문에 다음 Central Directory의 offset이 따로 존재 하지 않는 것으로 추측 됩니다.

 Central Directory에서 local header offset을 얻었으면 비로소 실제 파일에 접근 할 수 있는 local header에 도달할 수 있습니다.


# End of central directory record

 End of Central Directory는 파일의 맨 마지막에 위치하고 있으며, 위에서 언급했듯 Central Directory의 정보(시작 offset, 전체 Central Directory 사이즈, ...)를 가지고 있습니다. 즉, Zip파일 구조를 뜯어보려면 제일 먼저 End of Central Directory를 찾으면서 시작해야 한다. 하지만 End of Central Directory의 크기는 file comment 때문에 가변적이고, 시작점을 단번에 찾기는 쉽지 않습니다. 

 file comment는 End of Central Directory의 맨 마지막에 위치하고, file comment를 제외한 상위 바이트들의 사이즈는 22바이트 입니다. 따라서 file comment가 존재하지 않는 경우 파일의 끝에서부터 22바이트 떨어진 지점부터 End of Central Directory가 시작된다. End of Central Directory가 시작되는지 확인하기 위해서는 4바이트를 읽어 signature(0x06054B50)와 일치하는지 체크하여 확인 할 수 있습니다.



 - zip 파일 헤더


Local file header

Offset

Bytes

Description

0

4

파일의 header 서명

4

2

압축 해제 시 필요한 버전

6

2

범용 비트 플래그

8

2

압축 방법

10

2

최종 수정 시간

12

2

최종 수정 날짜

14

4

CRC-32 (순환 중복 검사 에러 검출 시 사용)

18

4

압축 크기

22

4

원본 크기

26

2

파일 이름 길이 (n)

28

2

추가 필드 길이 (m)

30

n

파일 이름

30+n

m

추가 필드


Data descriptor

Offset

Bytes

Description

0

0/4

선택적 데이터 설명자 서명(생략 가능) = 0x08074b50

0/4

4

CRC-32

4/8

4

압축 크기

8/12

4

원본 크기

 

Central directory file header

Offset

Bytes

Description

0

4

중앙 디렉토리 파일 헤더 서명= 0x02014b50

4

2

만들어진 버전

6

2

압축 해제에 필요한 최소 버전

8

2

범용 비트 플래그

10

2

압축 방법

12

2

최종 수정 시간

14

2

최종 수정 날짜

16

4

CRC-32 (순환 중복 검사 에러 검출 시 사용)

20

4

압축 크기

24

4

원본 크기

28

2

파일 이름 길이 (n)

30

2

추가 필드 길이 (m)

32

2

파일 주석 길이 (k)

34

2

파일이 시작되는 디스크 번호

36

2

내부 파일 속성

38

4

외부 파일 속성

42

4

로컬 파일 헤더의 상대 오프셋.

46

n

파일 이름

46+n

m

추가 필드

46+n+m

k

파일 주석


End of central directory record (EOCD)

Offset

Bytes

Description

0

4

중앙 디렉토리 서명의 끝 = 0x06054b50

4

2

디스크 번호

6

2

중앙 디렉토리의 시작 디스크

8

2

디스크의 중앙 디렉토리 레코드 수

10

2

총 중앙 디렉터리 레코드 수

12

4

중앙 디렉토리 크기 (Byte)

16

4

중앙 디렉토리의 시작 오프셋, 아카이브 시작에 대한 상태 값

20

2

코멘트 길이 (n)

22

n

코멘트



 - 접근 방법

 접근 방법은 Mutation 방식과 Generation 방식을 모두 사용해 보기로 하였습니다.


 처음에는 zip 파일의 포맷을 분석한 것을 바탕으로 Generation 방식을 이용하였습니다.

zip 파일의 헤더는 zip 파일 자체의 헤더인 Central Directory 부분과 압축된 데이터에 해당하는 헤더인 Local 부분 헤더가 존재하고, 또 파일의 데이터 부분이 존재하여 세 부분을 모두 적절히 변환해 보았습니다.

 일단 zip 파일의 Local 헤더 부분을 변경하여 fuzzing을 시도하였습니다. 이때 Local헤더 부분 중 독립적인 부분(파일의 생성 날짜, 파일 이름, ...)을 수정할 때에는 zip파일을 열었을 때 그 값만 바뀌고 아무 오류없이 정상적으로 실행되었습니다. 특정 헤더를 수정하면(압축크기, 압축 원본 등) 파일이 손상되었다고 뜨면서 복구 할 지 여부를 물어 보았습니다. 이를 통해 압축된 데이터의 헤더를 수정하여도 zip 자체의 헤더가 손상되지는 않아 zip파일을 open하는데 영향을 주지 않는다는것을 확인하였습니다.

 그 후 zip 파일의 Central Directory 부분을 수정하여 fuzzing을 시도하였습니다. 역시나 몇몇 부분을 수정하면(수정 날짜, 코멘트, ...) 정상적으로 수행되었고, 다른 몇몇 부분(CRC-32, 헤더 서명, ...) 을 수행하였더니 open 에러로 실행이 불가함을 확인 할 수 있었습니다.

 마지막으로 zip 파일 내에 저장된 데이터의 데이터 부분을 일부 수정해 보았습니다. 이때는 zip 파일을 열었을 때 데이터가 손상 되었으니 복구 여부를 물어보았고, 복구 여부를 물어보지 않거나 복구하지 않은 경우에도 압축 해제 시 데이터가 손상되었다는 오류를 표시할 뿐 디버깅 화면으로 넘어가지는 않았습니다.


 위에서 Generation 방식을 통해 취약점을 찾지 못해 차선책으로 Mutation 방식을 구현하였고, 이를 위해 파일의 랜덤한 위치에 “ff ff ff ff” 값을 삽입하는 과정을 계속해서 반복하였습니다. 보통 10번 내외로 수정되면 파일의 헤더가 손상되었다는 오류 메시지가 뜨지만 수많은 반복에서도 디버깅 화면으로 넘어가는 경우는 발생하지 않았습니다.



 - 프로그램 소스 코드

(실행 방법은 주석으로 Comment 했습니다. 알집 파일의 경우 이스트소프트 사이트에서 다운 받으시길 바랍니다.)

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
97
98
99
/*
 # param (알집 프로그램 cmd input): 
            argv[1] - ALZip_Link "C:\Program Files (x86)\ESTsoft\ALZip\ALzip.exe"
            argv[2] - Test_option "/context" (생략 가능)
            argv[2 or 3] - Test_file "Test_file.zip"

 # see (참고사항)
            Windows.h 를 사용하므로 Windows 환경에서만 정상적으로 동작하며 다른 운영체제에서 실행되지 않을 수 있다.
            
 # todo (추가적으로 처리해야 할 사항)
            실행시 C드라이브에 직접 접근하므로 관리자 권한으로 실행하여야 한다.
            C드라이브에 프로그램 설치 및 테스트파일을 해당 경로에 저장해야 한다.
*/
 
#include <Windows.h>
#include <iostream>
#include <fstream>
#include <tchar.h>
#include <cstdlib>
 
using namespace std;
 
//cmd 문자열은 프로그램 경로와 전달할 인자(Test_file.zip)을 담고 있음
wchar_t cmd[] = L"C:\\Program Files (x86)\\ESTsoft\\ALZip\\ALzip.exe Test_file.zip";
 
int main(int argc, char* argv[]) {
    char buffer[4];    //zip 파일의 헤더를 변형할 값을 저장
 
    //파일의 길이 측정 (for문 안에서 중복 측정 방지)
    fstream testfile("C:\\Program Files (x86)\\ESTsoft\\ALZip\\Test_file.zip", ios::binary | ios::out | ios::in);
    if (!testfile.is_open()) {
        perror("error name : ");
        testfile.close();
        return -1;
    }
    testfile.seekg(0, ios::end);
    long long int length = testfile.tellg();        //파일의 길이를 저장
    testfile.close();
 
    //랜덤으로 zip파일의 값을 변경 / 삽입 - 1000회 반복
    for (int i = 0; i < 1000; i++) {
        //프로그램이 저장된 파일을 open (바이너리 형태, 읽기쓰기 전용)
        fstream testfile("C:\\Program Files (x86)\\ESTsoft\\ALZip\\Test_file.zip", ios::binary | ios::out | ios::in);
        if (!testfile.is_open()) {
            perror("error name : ");
            testfile.close();
            return -1;
        }
 
        //버퍼의 값을 "ff ff ff ff" 로 설정
        memset(buffer, 255sizeof(buffer));    
        
        //변경할 구간을 입력받음
        int change = rand() % length;            
        
        /*
        이 값을 설정하면 파일이 덮어쓰기 범위 안에만 있어 파일이 커지지 않음.
        if (change - 8 >= length)
            change -= 8;
        */
 
        //랜덤한 파일 위치에 접근하여 파일을 쓰고 close
        testfile.seekg(change, ios::beg);
        testfile.write(buffer, sizeof(buffer));
        testfile.flush();
        testfile.close();
    
        cout << "[" << i + 1 << "]" << "CreateProcess!" << endl;
        STARTUPINFO si;
        PROCESS_INFORMATION pi;
 
        ZeroMemory(&si, sizeof(si));
        si.cb = sizeof(si);
        ZeroMemory(&pi, sizeof(pi));
 
        //ALZip 프로그램 open (인자로 zip 파일 전달)
        if (!CreateProcess(NULL, cmd, NULLNULL, FALSE, 0NULLNULL&si, &pi)) {
            cout << "CreateProcess fail!" << endl;
            cout << "Error message: " << GetLastError() << endl;
            exit(0);
        }
 
        //cout << "\tSleep 1sec" << endl;
        Sleep(1 * 1000);
        
        //cout << "\tterminate process" << endl;
        TerminateProcess(pi.hProcess, 100);
        
        //cout << "\twait child process" << endl;
        WaitForSingleObject(pi.hProcess, INFINITE);
 
        //cout << "\tclose handles" << endl;
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
        cout << "\t" << i + 1 << "th test fin." << endl << endl;
    }
    return 0;
}
 
 




 - 실행 결과

 실행결과는 퍼징 과정에서 오류가 발생하지만 프로그램 자체에서 오류가 나타났음을 알리는 것을 확인 할 수 있었습니다. 

(fuzzing 영상의 경우 Mutation 방식으로 fuzzing을 구현하였고, 랜덤으로 파일의 값을 변경하면서 처음에는 오류가 발생하지 않다가 이후 오류가 발생하여 프로그램 내에서 오류가 나타났음을 알리는 알림창이 뜨는 것을 확인할 수 있습니다.)



 - 미발생 원인 추측

 zip 파일의 Local 헤더, Central Directory 헤더 부분을 각각 하나씩 수정해 보았고, 랜덤으로 데이터와 헤더를 바꾸면서 체크도 해 보았지만 프로그램 내에서 오류 창이 나타날 뿐 Debug 오류는 발생하지 않았습니다.

 이와 같이 오류가 발생하지 않은 이유로는 첫 번째로 zip 파일이 압축 과정을 수행하여 만들어지는 파일이라 압축 과정에서 오류가 발생할 확률이 높고, 오류가 발생하게 되면 압축 된 파일 모두를 못 쓰게 되는 치명적인 결과를 초래할 수 있어 이를 방지하기 위해 오류의 처리와 복구를 위한 많은 기능을 삽입하였다고 추측하였습니다. 이와 같은 이유 때문인지 알집에서는 조금만 파일이 손상되어도 오류 메시지를 출력하고 복구 여부를 확인하는 과정을 수행하였습니다.

 또 다른 이유로는 zip 파일을 단순히 open하는 과정은 open과 동시에 압축해제 하는 과정에 비해 오류가 나타나기 힘들고 탐지도 쉬울 것이라고 생각 되었습니다. 이 때문에 open과 동시에 압축해제를 수행하도록 실행할 때 open과 unzip 인자를 동시에 전달 하려고 하였지만 압축해제 인자를 전달 할 때는 ALZip 프로그램에서 파일의 이름을 암호화하여 전달 받는 것을 확인 할 수 있었습니다. ALZip 프로그램에서 퍼징이나 기타 취약점 공격을 방지하기 위해 이와 같은 조치를 취한 것으로 생각 되며, 암호화 규칙이 알려지지 않아 파일을 code 상에서 암호화하여 전달할 수 없었습니다.




 - 비고

Program Language : C++ Languege


Test environment : 

ㆍ OS : Windows 10 1703 (15063.674)

ㆍ IDE : Visual Studio Community 2017 v15.1 (26403.7) Release


Software Requirements : 

ㆍ “Windows.h” 헤더를 사용, Windows 이외의 환경에서 정상적인 동작을 보장할 수 없음.


Reference : 

ㆍ zip file format

https://www.mql5.com/en/articles/1971

https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip.html

https://en.wikipedia.org/wiki/Zip_(file_format)  

http://www.pkware.com/documents/casestudies/APPNOTE.TXT

https://www.iana.org/assignments/media-types/application/zip



'학교 수업 및 과제 > 컴퓨터 보안' 카테고리의 다른 글

2. 패턴 매칭 프로그램  (0) 2018.05.30

(윤성우 저, 'TCP/IP 소켓 프로그래밍' - 11장 관련 내용입니다)


책에 기술 된 Multiplexing 서버와 Multiprocessing 클라이언트를 일부 수정하여 채팅 프로그램을 작성하는 문제입니다.
다중 접속을 지원하고 다중 접속 시 카카오톡 단체채팅방처럼 한 사람의 대화를 모든 사람이 보도록 구현하였습니다.


아래는 Multiplexing 서버를 수정한 채팅 서버 코드입니다.
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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>
 
#define BUF_SIZE 100
void error_handling(char *buf);
 
int main(int argc, char *argv[])
{
    //소켓, timeout값, fd_set값 등을 저장할 변수 선언
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    struct timeval timeout;
    fd_set reads, cpy_reads;
    socklen_t adr_sz;
    int fd_max, str_len, fd_num, i;
    char buf[BUF_SIZE];
 
    //입력 포맷이 맞지 않으면 오류 출력 후 종료
    if(argc!=2) {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }
 
    //서버의 소켓을 생성
    serv_sock=socket(PF_INET, SOCK_STREAM, 0);
    
    //서버의 주소정보를 저장, 이때 네트워크 바이트 순서로 저장한다
    memset(&serv_adr, 0sizeof(serv_adr));
    serv_adr.sin_family=AF_INET;
    serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_adr.sin_port=htons(atoi(argv[1]));
    
    //bind 함수를 통해 소켓에 서버의 주소정보를 할당
    if(bind(serv_sock, (struct sockaddr*&serv_adr, sizeof(serv_adr))==-1)
        error_handling("bind() error");
 
    //연결 대기 상태로 진입
    if(listen(serv_sock, 5)==-1)
        error_handling("listen() error");
 
    //fd_set형 변수 reads의 모든 비트를 0으로 초기화
    FD_ZERO(&reads);
    //소켓 디스크립터 정보를 등록
    FD_SET(serv_sock, &reads);
    //소켓의 번호를 저장
    fd_max=serv_sock;
 
    while(1) {
        cpy_reads=reads;
        //타임아웃 시간 설정
        timeout.tv_sec=5;
        timeout.tv_usec=5000;
        
        //소켓 포함 모든 파일디스크립터를 대상으로 '수신된 데이터의 존재여부' 검사
            //오류가 발생했다면
        if((fd_num=select(fd_max+1&cpy_reads, 00&timeout))==-1) {
            break;
        }
            //Timeout이 발생했다면
        if(fd_num==0) {
            continue;
        }
            //변화가 발생했다면
        for(i=0; i<fd_max+1; i++) {
            if(FD_ISSET(i, &cpy_reads)) {
                // 연결요청이 발생했다면
                if(i==serv_sock) {
                    //연결요청 수락    
                    adr_sz=sizeof(clnt_adr);
                    clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
                    
                    //해당 연결을 감시하도록 FD_SET을 통해 설정
                    FD_SET(clnt_sock, &reads);
                    //연결을 통해 clnt_sock 값이 증가했다면 fd_max 재설정
                    //(연결종료 된 클라이언트 < 새로 연결된 클라이언트)이라면 
                    if(fd_max<clnt_sock)
                        fd_max=clnt_sock;
                    printf("connected client: %d \n", clnt_sock);
                }
                else {
                    //메세지를 읽어들임   
                    str_len=read(i, buf, BUF_SIZE);
                    //종료 요청이 왔다면 연결 종료, fd_set값 초기화, 해당 소켓 종료
                    if(str_len==0) {    
                        FD_CLR(i, &reads);
                        close(i);
                        printf("closed client: %d \n", i);
                    }
                    //메세지가 왔다면
                    else {
                        //write(i, buf, str_len);    // echo!
                        //메세지를 모든 사람에게 전송 (4가 첫번째 소켓)
                        for(int j=4; j < fd_max + 1; j++) { 
                            write(j, buf, str_len);
                        }
                    }
                }
            }
        }
    }
    close(serv_sock);
    return 0;
}
 
void error_handling(char *buf) {
    fputs(buf, stderr);
    fputc('\n', stderr);
    exit(1);
}




아래는 Multiprocessing 클라이언트를 수정한 채팅 클라이언트 코드입니다.
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
97
98
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
 
#define BUF_SIZE 100
#define NAME_SIZE 20
 
void error_handling(char *message);
void read_routine(int sock, char *buf);
void write_routine(int sock, char *buf);
 
char name[NAME_SIZE] = "[NULL]";
char buf[BUF_SIZE];
 
int main(int argc, char *argv[]) {
    //소켓, pid값 등을 저장할 변수선언
    int sock;
    pid_t pid;
    char buf[BUF_SIZE];
    struct sockaddr_in serv_adr;
    
    //입력 포맷이 맞지 않으면 오류 출력 후 종료
    if(argc!=4) {    
        printf("Usage : %s <IP> <port> <name>\n", argv[0]);
        exit(1);
    }
    //데이터를 형식에 맞추어 쓰드록 지정
    sprintf(name, "[%s]", argv[3]);
 
    //클라이언트 소켓 설정
    sock=socket(PF_INET, SOCK_STREAM, 0);  
    memset(&serv_adr, 0sizeof(serv_adr));
    serv_adr.sin_family=AF_INET;
    serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
    serv_adr.sin_port=htons(atoi(argv[2]));
    
    //connect함수를 통해 서버와 연결 시도
    if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
        error_handling("connect() error!");
 
    //fork 함수를 통해 입력, 출력을 처리하는 프로세스를 각각 만듦 
    pid=fork();
    if(pid==0)
        write_routine(sock, buf);
    else 
        read_routine(sock, buf);
 
    close(sock);
    return 0;
}
 
//서버로부터 메세지를 읽어들이는 함수
void read_routine(int sock, char *buf) {
    char total_msg[NAME_SIZE + BUF_SIZE];
    while(1) {
        //서버로부터 메세지를 읽어들임
        int str_len=read(sock, total_msg, NAME_SIZE + BUF_SIZE);
        
        //close를 받았다면 종료
        if(str_len==0)
            return;
 
        //데이터 출력
        total_msg[str_len]=0;
        fputs(total_msg, stdout);
        //printf("Message from server: %s", buf);
    }
}
 
//서버로 메세지를 전송하는 함수
void write_routine(int sock, char *buf) {
    char total_msg[NAME_SIZE + BUF_SIZE];
    while(1) {
        //입력값을 받아들임
        fgets(buf, BUF_SIZE, stdin);
        
        //만약 'q'나 'Q' 문자가 입력되면 종료 
        if(!strcmp(buf,"q\n"|| !strcmp(buf,"Q\n")) {    
            shutdown(sock, SHUT_WR);
            return;
        }
        
        //데이터를 형식에 맞추어 쓰드록 지정
        sprintf(total_msg, "%s %s", name, buf);
        
        //서버로 입력한 데이터 전송
        write(sock, total_msg, strlen(total_msg));
    }
}
 
void error_handling(char *message) {
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}



(윤성우 저, 'TCP/IP 소켓 프로그래밍' - 10장 관련 내용입니다)


Select 함수를 이용하여 주어진 서버와 통신하는 I/O 멀티플렉싱 클라이언트를 작성하는 문제입니다.


 - 일정 시간 내에 데이터를 입력하지 않을 경우 Time Out 메시지 출력

 - read_routine 함수와 write_routine함수 적절히 호출


아래는 주어진 서버(multi_serv.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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>
 
#define BUF_SIZE 100
void error_handling(char *buf);
 
int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;
    struct sockaddr_in serv_adr, clnt_adr;
    struct timeval timeout;
    fd_set reads, cpy_reads;
 
    socklen_t adr_sz;
    int fd_max, str_len, fd_num, i;
    char buf[BUF_SIZE];
    if(argc!=2) {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }
 
    serv_sock=socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0sizeof(serv_adr));
    serv_adr.sin_family=AF_INET;
    serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_adr.sin_port=htons(atoi(argv[1]));
    
    if(bind(serv_sock, (struct sockaddr*&serv_adr, sizeof(serv_adr))==-1)
        error_handling("bind() error");
    if(listen(serv_sock, 5)==-1)
        error_handling("listen() error");
 
    FD_ZERO(&reads);
    FD_SET(serv_sock, &reads);
    fd_max=serv_sock;
 
    while(1)
    {
        cpy_reads=reads;
        timeout.tv_sec=5;
        timeout.tv_usec=5000;
 
        if((fd_num=select(fd_max+1&cpy_reads, 00&timeout))==-1)
            break;
        
        if(fd_num==0)
            continue;
 
        for(i=0; i<fd_max+1; i++)
        {
            if(FD_ISSET(i, &cpy_reads))
            {
                if(i==serv_sock)     // connection request!
                {
                    adr_sz=sizeof(clnt_adr);
                    clnt_sock=
                        accept(serv_sock, (struct sockaddr*)&clnt_adr, &adr_sz);
                    FD_SET(clnt_sock, &reads);
                    if(fd_max<clnt_sock)
                        fd_max=clnt_sock;
                    printf("connected client: %d \n", clnt_sock);
                }
                else    // read message!
                {
                    str_len=read(i, buf, BUF_SIZE);
                    if(str_len==0)    // close request!
                    {
                        FD_CLR(i, &reads);
                        close(i);
                        printf("closed client: %d \n", i);
                    }
                    else
                    {
                        write(i, buf, str_len);    // echo!
                    }
                }
            }
        }
    }
    close(serv_sock);
    return 0;
}
 
void error_handling(char *buf)
{
    fputs(buf, stderr);
    fputc('\n', stderr);
    exit(1);
}




주어진 multi_server 코드에 맞는 echo_client를 구현하기 위해 아래와 같은 코드를 작성했습니다.


코드 설명입니다.

1. 먼저 서버와 통신하기 위한 기본적인 과정을 거칩니다. (socket 만들고 connect)

2. select함수를 사용하기 위해 초기화와 타임아웃 값 설정 과정을 거칩니다.

3. select함수를 이용하여 입력 디스크립터(0번)과 소켓 디스크립터를 감시하여 수신된 데이터의 존재 여부를 검사합니다.

4. 수신된 데이터의 없다면 "Time-out!"을 출력하고 수신된 데이터가 있다면 입력, 출력 여부에 맞게 처리해줍니다.


아래는 구현한 echo_client 코드입니다.

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
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <sys/select.h>
 
#define BUF_SIZE 30
void error_handling(char *message);
void read_routine(int sock, char *buf);
void write_routine(int sock, char *buf);
 
int main(int argc, char *argv[]) {
    int sock;
    char buf[BUF_SIZE];
    struct sockaddr_in serv_adr;
 
    //타이머를 계산하기 위해 timeval형 변수 timeout 설정
    struct timeval timeout;
    //fd_set형 변수 선언하여 파일 디스크립터 정보 등록
    fd_set reads, cpy_reads;
    //검사할 파일 디스크립터의 수
    int fd_max, fd_num;
 
    if(argc!=3) {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }
 
    sock=socket(PF_INET, SOCK_STREAM, 0);
    memset(&serv_adr, 0sizeof(serv_adr));
    serv_adr.sin_family=AF_INET;
    serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
    serv_adr.sin_port=htons(atoi(argv[2]));
 
    if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
        error_handling("connect() error!");
 
    //fd_set형 변수 reads의 모든 비트를 0으로 초기화
    FD_ZERO(&reads);
    //입력 및 소켓 디스크립터 정보를 등록
    FD_SET(0&reads);
    FD_SET(sock, &reads);
    //소켓의 번호를 저장
    fd_max = sock;
    while(1) {
        cpy_reads = reads;
        //타임아웃 시간 설정
        timeout.tv_sec=5;
        timeout.tv_usec=0;
        //소켓 포함 모든 파일디스크립터를 대상으로 '수신된 데이터의 존재여부' 검사
        fd_num=select(fd_max+1&cpy_reads, 00&timeout);
        //오류가 발생했다면
        if(fd_num==-1) {
            error_handling("select() error\n");
        }
        //Timeout이 발생했다면
        else if (fd_num == 0) {
            puts("Time-out!");
            continue;
        }
        //변화가 발생했다면
        else {
            //만약 입력 디스크립터에 변화가 발생했다면 (사용자로부터 데이터를 입력받았다면)
            if(FD_ISSET(0&cpy_reads)) {
                //입력된 내용을 서버로 전송하는 함수
                write_routine(sock, buf);
                //파일 디스크립터 정보 삭제
                FD_CLR(0&cpy_reads);
            }
            //만약 포트에 변화가 발생했다면(서버로부터 데이터를 받았다면)
            if(FD_ISSET(sock, &cpy_reads)) {
                //받은 데이터를 출력하는 함수
                read_routine(sock, buf);
                //파일 디스크립터 정보 삭제
                FD_CLR(sock, &cpy_reads);
            }
        }
    }
    close(sock);
    return 0;
}
 
void read_routine(int sock, char *buf)
{
    int str_len=read(sock, buf, BUF_SIZE);
    if(str_len==0)
        return;
 
    buf[str_len]=0;
    printf("Message from server: %s", buf);
}

void write_routine(int sock, char *buf)
{
    fgets(buf, BUF_SIZE, stdin);
    if(!strcmp(buf,"q\n"|| !strcmp(buf,"Q\n"))
    {
        shutdown(sock, SHUT_WR);
        exit(0);
    }
    write(sock, buf, strlen(buf));
}

void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}


(윤성우 저, 'TCP/IP 소켓 프로그래밍' - 4장 관련 내용입니다)


echo_client 코드가 주어질 때, 주어진 클라이언트의 IP Address와 Port 번호를 출력하는 Iterative Echo Server를 만드는 문제입니다.


아래는 echo_client 코드입니다.

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
 
#define BUF_SIZE 1024
void error_handling(char *message);
 
int main(int argc, char *argv[])
{
    int sock;
    char message[BUF_SIZE];
    int str_len;
    struct sockaddr_in serv_adr;
 
    if(argc!=3) {
        printf("Usage : %s <IP> <port>\n", argv[0]);
        exit(1);
    }
    
    sock=socket(PF_INET, SOCK_STREAM, 0);   
    if(sock==-1)
        error_handling("socket() error");
    
    memset(&serv_adr, 0sizeof(serv_adr));
    serv_adr.sin_family=AF_INET;
    serv_adr.sin_addr.s_addr=inet_addr(argv[1]);
    serv_adr.sin_port=htons(atoi(argv[2]));
    
    if(connect(sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
        error_handling("connect() error!");
    else
        puts("Connected...........");
    
    while(1
    {
        fputs("Input message(Q to quit): ", stdout);
        fgets(message, BUF_SIZE, stdin);
        
        if(!strcmp(message,"q\n"|| !strcmp(message,"Q\n"))
            break;
 
        write(sock, message, strlen(message));
        str_len=read(sock, message, BUF_SIZE-1);
        message[str_len]=0;
        printf("Message from server: %s", message);
    }
    
    close(sock);
    return 0;
}
 
void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}




주어진 echo_client 코드에 맞는 echo_server를 구현하기 위해 아래와 같은 코드를 작성했습니다.

(코드에서 해당되는 모든 부분에 주석으로 설명을 달았습니다. 아래 코드를 보고 이해가 안 된다면 가장 위에 언급된 책 4장을 참고하시기 바랍니다.)

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
//헤더 부분입니다.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
 
#define BUF_SIZE 1024
void error_handling(char *message);
 
//main함수
int main(int argc, char *argv[])
{
    int serv_sock, clnt_sock;        //서버와 클라이언트 소켓을 저장할 변수 선언
    char message[BUF_SIZE];            //메세지를 저장할 변수 선언
    int str_len, i;
 
    //서버와 클라이언트 주소정보를 저장할 구조체 변수 선언
    struct sockaddr_in serv_adr;
    struct sockaddr_in clnt_adr;
    socklen_t clnt_adr_sz;
 
  //입력값 올바르지 않다면 에러
    if(argc!=2) {
        printf("Usage : %s <port>\n", argv[0]);
        exit(1);
    }
 
    //서버의 소켓을 생성
    serv_sock=socket(PF_INET, SOCK_STREAM, 0);
    if(serv_sock==-1)
        error_handling("socket() error");
 
    //서버의 주소정보를 저장, 이때 네트워크 바이트 순서로 저장한다
    memset(&serv_adr, 0sizeof(serv_adr));
    serv_adr.sin_family=AF_INET;
    serv_adr.sin_addr.s_addr=htonl(INADDR_ANY);
    serv_adr.sin_port=htons(atoi(argv[1]));
 
    //bind 함수를 통해 소켓에 서버의 주소정보를 할당
    if(bind(serv_sock, (struct sockaddr*)&serv_adr, sizeof(serv_adr))==-1)
        error_handling("bind() error");
 
    //연결 대기 상태로 진입
    if(listen(serv_sock, 5)==-1)
        error_handling("listen() error");
 
    clnt_adr_sz=sizeof(clnt_adr);
 
    for(i=0; i<5; i++)
    {
        //클라이언트로 부터 연결 요청이 오면 수락한다. 이때 클라이언트의 주소 정보를 저장한다.
        clnt_sock=accept(serv_sock, (struct sockaddr*)&clnt_adr, &clnt_adr_sz);
        if(clnt_sock==-1)
            error_handling("accept() error");
        else {
            /*
            클라이언트의 IP를 출력하기 위해 구조체 clnt_adr의 인자 sin_addr에 저장된 IP주소를 참조.
            이때 32비트 정수 자료형으로 저장 된 IP 주소를 네트워크 바이트 순서로 바꾸어
            OOO.OOO.OOO.OOO 형태의 문자열로 출력하기 위해 inet_ntoa 함수를 이용한다.
            */
            printf("Client's IP : %s\n",inet_ntoa(clnt_adr.sin_addr));
 
            /*
            클라이언트의 Port번호를 출력하기 위해 구조체 clnt_adr의 인자 sin_port에 저장된
            Port번호를 참조. 이때 Big Endian 방식으로 저장 된 포트 번호를 네트워크 바이트 순서로
            정렬해서 반환하기 위해 ntohs 함수를 이용한다.
            */
            printf("Client's Port : %d\n",ntohs(clnt_adr.sin_port));
 
            printf("Connected client %d \n", i+1);
        }
        //버퍼를 다 읽어와 이를 출력한다. 클라이언트가 종료 요청시에 while문을 탈출한다.
        while((str_len=read(clnt_sock, message, BUF_SIZE))!=0)
            write(clnt_sock, message, str_len);
 
        //클라이언트 소켓의 종료를 받아들인다.
        close(clnt_sock);
    }
  //서버 소켓의 종료
    close(serv_sock);
    return 0;
}
 
//에러 처리
void error_handling(char *message)
{
    fputs(message, stderr);
    fputc('\n', stderr);
    exit(1);
}