쓰레드를 사용한 포트 스캔 프로그램
출처 : http://beist.org/research/public/pthread-scan/index.html



by Beist Security Study Group
(http://beist.org)

Members of Beist Research Group : beist and anonymous people
Members of Beist Study Group : beist, dars21, obhacker, passion, p-jackpot, jacaranda, cina





요약: 쓰레드를 이용하여 프로그램을 병렬적으로 작성할 경우 원하는 기능을 보다 빠르게 수행할 수 있습니다. 본 문서는 쓰레드를 이용한 포트 스캔 프로그램을 작성하는 과정을 담았습니다. 이를 활용할 경우 쓰레드를 사용하지 않은 포트 스캔보다 훨씬 더 빠르게 스캔 작업을 수행할 수 있고 특히 광범위한 IP 대역을 스캔하거나 할 때 유용하게 사용될 수 있습니다.










1. 소개

 

쓰레드 생성은 프로세스 생성에 비해 시스템 자원을 많이 소비하지 않고 가볍게 동작하기 때문에 네트워크 프로그래밍에서 다중 접속을 처리 할 때나 프로세서 내의 동일 작업을 병렬적으로 처 리할 때 많이 사용되는 기법입니다.

Port Scan 작업은 목표 네트워크를 공략하기 위한 중요한 작업 중에 하나인데 네트워크 환경에 따라 Scan 작업이 굉장히 오래 걸리는 경우가 있을 수 있습니다 . 특히 큰 대역을 Scan할 때 등 아주 많은 Port를 검색할 때 문제가 될 수 있습니다. 본 문서는 이러한 문제점을 극복하기 위한 방안으로 쓰레드를 Port Scan에 도입하는 내용에 대해서 다룰 것입니다. 또한 본 문서는 Port Scan쓰레드 기능을 도입하여 Scan 시간을 낭비하지 않도록 하는 것이 목표이므로 많은 내용을 다루지 않고 Scan 작업에 직접적으로 관련된 쓰레드만을 설명할 것입니다.

 

 

 

 

2. 기술적인 내용

 

(1)  쓰레드란

 

쓰레드는 흔히 경량 프로세스, 세미 프로세스라 지칭하는데, 프로그램에서 수행하는 명령의 흐름을 동시에 처리하도록 하는 것이 가장 큰 특징입니다. 일반적으로 프로세스의 경우 프로그램 명령들이 단일적으로 혹은 순차적으로 처리되지만, 쓰레드를 사용하게 되면 프로그램 명령들을 병렬적으로 처리할 수 있기 때문에 매우 빠른 속도로 동작할 수 있게 됩니다. 예를 들어 수행해야 할 작업이 많은 프로그 램일 경우 쓰레드 사용 여부에 따라 처리 속도가 대략 스무 배 이상 차이가 날 수 있습니다. 이외에도 쓰레드 간에는 공유 메모리 공간을 갖고 있기 때문에 프로그램 구현이 편리하다는 장점이 있습니다.

쓰레드의 단점으로, 작업을 수행하는 도중, 동시에 특정 메모리에 접근했을 때 문제를 일으킬 수도 있습니다. 또 동작중인 여러 개의 쓰레드 중 하나라도 잘못된 연산을 하게 될 경우 운영체제에 의해 프로세스 전체가 죽게 됩니다. 그리고, 여러 명령이 동시에 실행되기 때문에 디버깅 작업이 어렵다는 단점이 있습니다.

 

(2)  쓰레드 API

 

본 문서에서 다루게 될 쓰레드 API POSIX 쓰레드인데 운영체제에 관계없이 사용될 수 있는 호환성이 좋은 라이브러리로 pthread(Posix Thread줄임말)입니다.

 

1)    쓰레스 생성  

 

#include<pthread.h>

 

int pthread_create(pthread_t *thread, pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);

 

thread : 쓰레드 식별자. 다른 쓰레드와 구분될 수 있는 ID할당받게 됩니다.

attr : 생성되는 쓰레드의 특성을 정하기 위해 사용되는데, 일반적으로 NULL을 사용합니다.

start_routine : 리턴 타입이 void*이고 인자도 void*인 함수 포인터로 쓰레드가 실행시킬쓰레드 함수입니다.

arg : 쓰레드에 의해 호출되는 함수(start_routine 포인터에 대입된 함수)전될되는 함수 인자 입니다.

 

이 함수에서 중요한 인자는 쓰레드 식별자(thread), 쓰레드 생성시 호출되는 함수(start_routine)입니다.

 

 

2) 쓰레드 기다림

 

#include<pthread.h>

 

int pthread_join(pthread_t th, void **thread_return);

 

th : 쓰레드가 종료할 때까지 기다리는 쓰레드 식별자입니다.

thread_return : 쓰레드가 종료시 반환하는 값에 접근할 수 있는 포인터입니다.

 

생성된 쓰레드가 종료될 때까지 기다리기 위한 함수입니다. 만약 프로세스가 쓰레드를 생성한 후에 쓰레드가 종료할 때까지 기다리지 않고 프로세스를 종료하면 쓰레드도 동시에 모두 종료되기 때문에 모든 쓰레드를 안전하게 실행시키기 위해서 반드시 사용되어야 하는 함수 입니다.

 

3) 쓰레드 식별자 반환

 

#include<pthread.h>

 

int pthread_self();

 

pthread_self 함수를 실행하면 해당 쓰레드의 식별자를 반환해줍니다.

 

 

 

4) 쓰레드 분리

 

#include<pthread.h>

 

int pthread_detach(pthread_t th);

 

th : 메인 쓰레드에서 분리할 쓰레드 식별자이고, 쓰레드가 분리되면 종료되면서 자원을 즉시 해제시켜 줍니다.

 

5) 쓰레드 종료

 

#include<pthread.h>

 

int pthread_exit(void *retval);

 

현재 실행 중인 쓰레드를 종료시킬 때 사용되는 함수입니다. 종료 후에 pthread_join함수를 통하여 모든 자원을 해제 시킵니다.

 

 

(3) 쓰레드 동기화 API

 

쓰레드는 하나의 프로세스에서 나뉘어진 것으로 데이터를 공유할 수 있다고 하였습니다. 여러 개의 쓰레드가 전역 변수에 접근하는 경우에 문제가 발생할 수 있는데 이것을 임계 영역(Critical Section)으로 볼 수 있습니다. 이 문서에서는 뮤텍스(mutex)를 사용하여 데이터 동시 접근 문제를 해결합니다. 뮤텍스를 지원하는 pthread의 함수를 알아보겠습니다.

 

#include<pthread.h>

 

int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr);

 

int pthread_mutex_lock(pthread_mutex_t *mutex);

 

int pthread_mutex_unlock(pthread_mutex_t *mutex);

 

int pthread_mutex_destroy(pthread_mutex_t *mutex);

 

 

 pthread_mutex_init : 뮤텍스를 사용하기 전에 초기화 과정을 거치기 위한 함수입니다. 첫 번째 인자에는 초기화하고자 하는 뮤텍스 포인터를 넘기고, 두 번째 인자에는 뮤텍스 속성(attribute)를 설정할 때 사용합니다. 속성을 사용하지 않을 때는 NULL포인터를 넘깁니다.

pthread_mutex_lock : 뮤텍스를 걸어 잠글 때(lock) 사용하는 함수입니다. 뮤텍스를 걸어 잠그면 후에 진입하는 쓰레드는 뮤텍스가 풀릴 때까지 대기하게 됩니다.

pthread_mutex_unlock : 뮤텍스를 풀어줄 때 사용하는 함수입니다. 뮤텍스를 잠그면 반드시 풀어줘야 다른 쓰레드들이 진입할 수 있습니다.

pthread_mutex_destroy : 더 이상 뮤텍스를 사용하지 않고 이와 관련된 리소스를 해제할 때 사용되는 함수입니다.

 

(4) 멀티 쓰레드 구현 예 (포트스캔 프로그램)

 

멀티 쓰레드 프로그래밍 모델은 네트워크 프로그래밍에서 채팅 서버 등, 다중 접속 처리를 위한 목적으로 많이 사용됩니다. 본 문서에서는 이것을 응용하여 목표 서버의 포트(65535) 중에 어떤 포트가 열려있는지 빠른 시간 안에 확인할 수 있는 포트스캔 프로그램을 제작하는 과정으로 예를 들어 설명하겠습니다 .

예제 프로그램인 포트스캔 프로그램에서 65535개의 포트를 병렬적으로 검색하기 위해 멀티 쓰레드를 사용합니다. 시스템 자원의 한계 문제점으로 인해 일반적으로 프로세스당 쓰레드 생성개수가 제한되어 있습니다. 또한 과도한 쓰레드를 생성할 경우 프로그램이 정상적으로 작동되지 않는 경우가 있는데 본 문서에서는 이 문제점을 해결하기 위해 쓰레드를 생성한 후, 쓰레드 생성개수(thread_cont)를 증가시키는 부분을 임계영역으로 지정하고 뮤텍스 잠금을 통해 제한된 수(1000)를 넘지 않도록 하고 있습니다. (1000개 수치는 시스템마다 유연하게 설정할 수 있습니다.)

메인 함수에 대한 내용은 생략하고 쓰레드를 생성하는 함수와 쓰레드에서 실행할 내용에 대해서만 설명하겠습니다. 메인 함수의 역할은 프로그램을 실행시킬 때 입력하는 명령 인수로부터(argument) 목표 시스템의 IP주소를 얻어와 thread_scan(쓰레드를 생성할 함수) 함수를 호출하는 것 입니다.

 

1

2 #define ENDPORT 65535

3 int thread_count; //쓰레드 생성 개수를 나타내는 변수

4 int dst_ip; //스캔대상 ip

5 int flag = 1;

6

7 void thread_scan()  //쓰레드를 생성할 함수

8 {

9            int i, start_port;

10          pthread_t thread_id[ENDPORT];   

11          void* t_return;

12          int value[ENDPORT];

13          struct sockaddr_in sock;

14      if(pthread_mutex_init(&mutex, NULL)) fprintf(stderr, "mutex init error\n");

15          printf("\nStart PortScan..........\n");

16          printf("port\tstate\n");

17          for(i=0, start_port = 0;i<ENDPORT;i++){      

18                        value[i]=++start_port;

19      pthread_mutex_lock(&mutex);

20          while(1){

21      if(thread_count < 1000){

22       if(pthread_create(&thread_id[i], NULL, tcpportscan, (void*)&value[i])<0)

23                                     fprintf(stderr, "create error\n");

24                                thread_count++;

25                                     break;

26          }

27          else{

28                                     usleep(10);

29                                     continue;

30          } // if(thread_count<1000)

31          } // while()

32          pthread_mutex_unlock(&mutex);

33          } //for

34          if(flag)

35                                     printf("All port close..........\n");

36 }

37

38

39  void *tcpportscan(void* arg) //쓰레드에서 실행할 함수

40  {

41          struct sockaddr_in dest; //소켓 어드레스

42          int sockfd; //소켓 디스크립트

43          int scanport;

44          int value;

45          int Error, ErrorLength;

46          struct timeval tv;

47          fd_set readfd, writefd;

48          pthread_t th;

49          th = pthread_self();

50          scanport= *(int*)arg;

51          memset((char*)&dest, 0, sizeof(dest));

52          dest.sin_family = AF_INET;

53          dest.sin_port = htons(scanport);

54          dest.sin_addr.s_addr = htonl(dst_ip);

55

56          if((sockfd=socket(AF_INET, SOCK_STREAM, IPPROTO_TCP))<0){

57                        fprintf(stderr, "소켓에러\n");

58                        close(sockfd);

59                        pthread_detach(th);

60          }

61          value = fcntl(sockfd, F_GETFL);

62          fcntl(sockfd, F_SETFL, value | O_NONBLOCK);

63          if(connect(sockfd, (struct sockaddr *)&dest, sizeof(dest)) == 0){

64                        printf("connect에러");                     

65                        close(sockfd);

66                        pthread_detach(th);

67          }

68          //select 타임아웃 설정

69          tv.tv_sec = 2; //대기시간 2초 설정

70          tv.tv_usec = 0;

71      //select에서 검사하는 디스크립트 설정

72          FD_ZERO(&readfd);

73          FD_ZERO(&writefd);

74          FD_SET(sockfd, &readfd);

75          FD_SET(sockfd, &writefd);

76

77          if(select(sockfd+1, &readfd, &writefd, NULL, &tv) <= 0) ;

78     else{

79        if(FD_ISSET(sockfd, &readfd) || FD_ISSET(sockfd, &writefd)){

80                        ErrorLength = sizeof(Error);

81     if(getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &Error, &ErrorLength) == 0) {

82                                     if(Error == 0){

83                 & nbsp;             flag = 0; //열린 포트 있다는 flag 설정

84                                      &nb sp;            printf("%d\topen\n", scanport);             

85                                     }                                     

86      }//getsock

87              }//fd_isset

88     }//else

89          close(sockfd);

90      thread_count--;

91      pthread_detach(th);

92 }

 

 

 

프로그램 해설

 

2 : 최대 포트의 할당 개수가 65535이므로 검색포트의 개수를 65535개로 지정하고 있습니다.

4~5행 : 모든 쓰레드에서 참조 할 수 있도록 스캔 대상 호스트 IP전역번수로 선언하고, 검색된 포트가 없을 때 메시지를 출력해주기 위한 flag를 선언합니다.

14 : 동기화에 필요한 뮤텍스를 사용하기 위해 pthread_init함수를 호출하고 있다.

17~33행 : for문을 통해 65535까지 port번호를 대입하는 과정을 반복합니다.

한 프로세스의 쓰레드 생성 개수가 제한되어 있기 때문에 무제한으로 쓰레드를 생성되는 것

을 방지하기 위해 뮤텍스를 사용합니다. for문 시작 부분에서 뮤텍스 잠금함수인 pthread

_mutex_lock함수를 호출하여 임계영역으로 설정하고 쓰레드 생성개수가 1000개 이하인지

체크합니다. 만약 쓰레드 생성개수가 1000개 이하이면 pthread_create함수를 이용하여

 쓰레드를 생성하고 세 번째 인자에 실행될 쓰레드 함수로 tcpportscan함수를 대입하였습

니다. 또한, tcpportscan함수에 할당될 인자로는 value배열의 값으로 대입하였습니다.

쓰레드를 생성하고 난 후에는 pthread_mutex_unlock함수를 이용하여 뮤텍스 잠금을 풀어

줍니다.

반면에 쓰레드 생성개수가 1000개를 넘게 되면 다른 쓰레드에서 쓰레드가 종료될 때까지

기다리도록 합니다.

34~35행 : 열려있는 포트가 없을 경우 출력해주는 메시지입니다.

39 : 쓰레드 마지막 부분에서 pthread_detach함수의 인자로 사용하기 위해 쓰레드 식별자를 얻어오는 pthread_self함수를 호출합니다.

50 : 쓰레드 실행할 함수(tcpportscan)를 호출할 때 넘어온 인자 값으로 port번호입니다.

56 : socket함수를 통해 소켓 파일디스크립터를 생성합니다.

59, 66, 92행 : 쓰레드를 종료하고 자원을 해제하기 위해 pthread_detach함수를 호출합니다.

61~62행 : fcntl는 소켓파일 지정자 함수로 F_GETFL를 통해 파일 지정자 플래그를 읽어오고, F_SETFLO_NONBLOCK 플래그를 지정하여 소켓이 블록킹되지 않도록 합니다.

63 : connect 함수를 통해 포트에 연결 요청합니다.

69~70행 :  select함수가 기다리는 타임아웃 시간(2초 설정)을 지정합니다.

72~75행 :  FD_ 시작하는 함수는 파일 디스크립터를 제어하기 위한 함수로, FD_ZERO를 통해 입력과 출력의 파일 디스크립터 테이블을 0으로 초기화 하고, FD_SET를 통해 소켓을 입력과 출력의 파일 디스크립터 테이블에 설정합니다.

77 : select함수를 통해 tv구조체 변수에 설정된 시간동안 파일 디스크립터 테이블을 검사합니다.

78~88행 : FD_ISSET매크로를 통해 다시 한번 소켓의 입,출력의 변화인지 확인합니다. 그리고, getsockopt함수에 SO_ERROR 옵션을 지정해 소켓 에러 상태를 반환해주는데, 반환 값이 0이고, Error변수에 0이 할당되면 에러가 없는 값이므로 정상적인 소켓의 입, 출력 변화라고 판단하고 열려있는 포트번호를 출력해줍니다. flag변수는 모든 포트가 닫혀있을 때의 출력문을 위한 변수입니다.

 

 

 

 

3. 마치는 말

 

지금까지 멀티 쓰레드에 대한 개념과 멀티 쓰레드 프로그래밍을 응용하여 포트 스캔 프로그램을 작성하는 방법을 살펴보았습니다. 예제 프로그램처럼 멀티 쓰레드의 장점을 잘 활용하여 네트워크 프로그래밍을 한다면 프로세스만으로 실행하는 프로그램보다 훨 씬 더 좋은 성능을 발휘할 수 있는 프로그램을 기대할 수 있습니다. 특히 광범위한 IP 대역을 Scan할 때 쓰레드를 사용하지 않는다면 굉장히 오랜 시간이 걸릴 것입니다.

멀티 쓰레드를 사용할 때는 항상 공유 메모리를 사용하는 것에 주의해야 하고, 동기 화 문제를 잘 고려해서 프로그래밍을 해야 안전하게 작동될 수 있습니다. 본 문서에서는 쓰레드에 대한 내용을 아주 기본적인 부분만 다루었기 때문에 쓰레드를 보다 능숙하게 사용하기 위해서는 디버깅과 실시간 스케줄링, 고급 동기화, 공유 메모리에 대해 더욱 깊은 이해가 필요합니다.

 






소스 첨부: pthread_scan.c
#include<stdio.h> #include<netinet/ip.h> #include<fcntl.h> #include<pthread.h> #define ENDPORT 65535 int dst_ip; int thread_count; int flag = 1; pthread_mutex_t mutex; void *tcpportscan(void* arg); void thread_scan(); int main(int argc, char* argv[]) { int start_ip; if(argc != 2) { fprintf(stderr, "사용법:%s IP주소\n", argv[0]); fprintf(stderr, "예) %s 192.168.0.1\n", argv[0]); exit(1); } dst_ip = ntohl(inet_addr(argv[1])); thread_scan(); } void thread_scan() { int i, start_port; pthread_t thread_id[ENDPORT]; int value[ENDPORT]; struct sockaddr_in sock; if(pthread_mutex_init(&mutex, NULL)) fprintf(stderr, "mutex init error\n"); printf("\nStart PortScan..........\n"); printf("port\tstate\n"); for(i=0, start_port = 0;i<ENDPORT;i++) { value[i]=++start_port; pthread_mutex_lock(&mutex); while(1) { if(thread_count < 1000) { if(pthread_create(&thread_id[i], NULL, tcpportscan, (void*)&value[i])<0) fprintf(stderr, "create error\n"); thread_count++; break; } else { usleep(10); continue; } } pthread_mutex_unlock(&mutex); } if(flag) printf("All port close..........\n"); } void *tcpportscan(void* arg) { struct sockaddr_in dest; int sockfd; int scanport; int value; int Error, ErrorLength; struct timeval tv; fd_set readfd, writefd; pthread_t th; th = pthread_self(); scanport= *(int*)arg; memset((char*)&dest, 0, sizeof(dest)); dest.sin_family = AF_INET; dest.sin_port = htons(scanport); dest.sin_addr.s_addr = htonl(dst_ip); if((sockfd=socket(AF_INET, SOCK_STREAM, IPPROTO_TCP))<0) { fprintf(stderr, "소켓에러\n"); close(sockfd); pthread_detach(th); } value = fcntl(sockfd, F_GETFL); fcntl(sockfd, F_SETFL, value | O_NONBLOCK); if(connect(sockfd, (struct sockaddr *)&dest, sizeof(dest)) == 0) { printf("connect에러\n"); close(sockfd); pthread_detach(th); } tv.tv_sec = 2; tv.tv_usec = 0; FD_ZERO(&readfd); FD_ZERO(&writefd); FD_SET(sockfd, &readfd); FD_SET(sockfd, &writefd); if(select(sockfd+1, &readfd, &writefd, NULL, &tv) <= 0) { } else { if(FD_ISSET(sockfd, &readfd) || FD_ISSET(sockfd, &writefd)) { ErrorLength = sizeof(Error); if(getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &Error, &ErrorLength) == 0) { if(Error == 0) { flag = 0; printf("%d\topen\n", scanport); } } } } close(sockfd); thread_count--; pthread_detach(th); }
신고
Posted by remos


티스토리 툴바