윈도우용 네트워크 방화벽 개발하기

일반입력 :2004/10/14 09:15

김기홍 (필자)

리눅스 플랫폼에서는 많은 정보와 공개되어 있는 수많은 소스코드들을 참고하면 쉽고 빠르게 성능 좋은 방화벽을 개발할 수 있다. 하지만 윈도우 플랫폼에서 방화벽을 개발한다면 쉽게 그 한계를 느끼게 된다. 많은 부분이 공개된 코드들이 없을 뿐더러 NDIS, TDI 등의 커널 레벨에서 방화벽을 개발하려다 보니 어려운 부분이 한두 가지가 아니기 때문이다. 하지만 요즘 개인용 방화벽에 대한 요구가 많아지고 있는 시점이고 그 방화벽 개발의 방향이 윈도우 2000 이상이라면 어렵지 않게 개발할 수 있을 것이다.

윈도우 2000 DDK를 살펴보면 마이크로소프트는 새로운 타입의 네트워크 드라이버를 포함하고 있는데 바로 필터 훅 드라이버(Filter Hook Driver)이다. 이 드라이버를 이용하면 보내고 받는 각종 패킷(TCP, UDP, ICMP 등)을 제어할 수 있다. 반드시 모든 개발은 윈도우 2000 이상에서 해야 하며 그 이하 버전에서 사용되는 방화벽 개발은 이 문서에서 열외로 하겠다.

필터 훅 드라이버

글을 시작하면서 잠깐 언급했던 필터 훅 드라이버가 무엇인지 궁금해 하는 사람이 많을 것이다. 필터 훅 드라이버는 윈도우 2000 DDK에 소개되어 있다. 따지고 보면 이 필터 훅 드라이버라는 것은 완전 새로운 네트워크 드라이버를 말하는 것이 아니다. 운영체제가 기존에 가지고 있던 IP 필터 드라이버의 확장 형태로 볼 수 있다(이러한 확장 형태는 윈도우 2000 이상에서만 가지고 있다). 필터 훅 드라이버는 NDIS와 같은 네트워크 드라이버를 직접 개발하는 것을 말하는 것은 아니다. 단지 그냥 커널 레벨에서 작동하는 커널 레벨의 드라이버(커널 레벨에서 작동되는 일종의 프로그램)를 말하는 것이다.

필터 훅 드라이버는 Callback 함수를 가지고 있고 기존의 IP 필터 드라이버의 함수에 필터 훅 드라이버의 함수를 등록시켜서 IP 필터 드라이버 내부에 있는 함수가 호출될 때 우리가 지정한 필터 훅 드라이버의 CallBack 함수로 호출이 가능하게 하는 것이다. 그럼 기존의 IP 필터 드라이버에서 send와 receive 함수와 관계된 부분을 우리가 새롭게 만들 필터 훅 드라이버에 Callback 함수로 등록하게 되면 적당한 정보와 함께 비교하면서 차단할 지 허용할 지 결정하게 되는 것이다. 이 과정을 다시 한번 간단하게 정리해 보자.

[1] 커널 레벨의 필터 훅 드라이버를 만든다.

[2] 필터 함수를 등록하기 위해서는 IP 필터 드라이버의 포인터를 얻을 수 있도록 한다.

[3] IP 필터 드라이버의 포인터를 알았으면 우리는 특정한 IRP를 보내면서 필터 함수를 설치하여 사용할 수 있다. 그렇게 되면 IP 필터 드라이버를 통과하는 모든 데이터들이 우리의 필터 함수에도 통과하게 된다.

[4] 여기서 특정 정보와 비교해서 필터링할 수 있도록 한다.

[5] 필터링 과정이 끝난 후에 원 상태로 풀어주기 위해서는 NULL 포인터의 또 다른 함수를 등록시키거나 바꾸면 된다.

이와 같이 필터 훅 드라이버의 간단한 작동 원리를 알았다면 실질적으로 프로그램 소스코드를 보면서 이해해 보도록 하자.

커널 모드 드라이버 만들기

필터 훅 드라이버는 앞에서도 충분히 언급했듯 커널 모드 드라이버(Kernel Mode Driver)이다. 따라서 필터 훅 드라이버를 사용하려면 커널 모드 드라이버를 만들어야 하는 것은 분명한 사실이다. 그렇다면 커널 모드의 드라이버는 어떻게 만드는 것일까? 커널 모드의 드라이버를 제작하는 방법은 다른 책이나 참고문헌에 많이 실려 있으니 그것을 참조하도록 하고 여기서는 방화벽을 만들기 위해 필요한 최소한의 정보만 알면서 넘어갈 수 있도록 하자. 필터 훅 드라이버는 다음과 같이 기본적인 커널 모드 드라이버의 구조를 가진다.

◆ 우리가 만들 커널 모드의 드라이버 구조는 기본적으로 가져야 할 루틴들(디스패치, 로드, 언로드, create 등)을 포함하고 있고 애플리케이션과 통신하기 위한 심볼릭 링크들을 가지고 있어야 한다.

◆ 기본적인 루틴은 각종 IRP를 관리할 수 있어야 한다. 즉 각종 IOCTL들은 외부의 프로그램과 통신하기 위한 일종의 외부 함수들로서, 커널 모드 드라이버 내부에 미리 선언되어 있어야 한다. 또한 선언된 내용은 애플리케이션과도 연동하여 함께 작동될 수 있도록 한다.

◆ 기본적으로 필터링할 수 있는 함수를 커널 모드 드라이버에 등록할 수 있도록 한다.

그럼 이와 같은 대강의 구조를 알았다면 <리스트 1>의 소스와 주석을 보자.

필터링 함수 등록하기

<리스트 1>의 소스코드를 보면 SetFilterFunction이라는 함수를 호출하도록 되어 있다. 이 함수는 IP 필터 드라이버에 우리가 원하는 Callback 함수로 올 수 있도록 해당 내용을 등록하는 함수라고 볼 수 있다. 이 함수를 등록하는 과정은 다음과 같다.

[1] 반드시 IP 필터 드라이버의 포인터를 가지고 있어야 한다. 또한 IP 필터 드라이버가 설치되고 정상 작동이 되고 있어야 하며 우리가 원하는 커널 레벨의 드라이버보다 먼저 작동되어 있어야만 한다.

[2] 특별한 IRP를 만들어서 I/O 컨트롤 코드를 만들어줘야 한다. 또한 PF_SET_EXTENSION_HOOK_INFO 구조체를 만들어서 필터 함수에 필터링할 수 있는 정보를 보내줘야 한다.

[3] 디바이스 드라이버에 IRP를 보내준다.

하지만 여기서 문제점이 드러난다. 바로 단 하나의 필터 함수만 등록 가능하다는 것이다. 만약 같은 필터 함수를 사용하는 프로그램이 실행되어 있다면 우리의 프로그램은 실행되지 않을 것이다. <리스트 2>는 필터 함수를 등록하는 함수이다. 소스를 보면 알 수 있듯이 필터 함수를 등록하는 프로세스를 완료하게 되면 우리가 처음에 얻었던 디바이스 드라이버와 다른 포인터의 객체를 얻을 수 있다. 그리고 우리의 네트워크 필터 드라이버가 IRP 처리를 끝내면 이벤트를 일으킬 수 있도록 했다.

필터 함수

지금까지 어떻게 커널 모드의 드라이버를 제작하고 Hooking이라는 과정을 통해 어떻게 패킷을 필터링하는지, 또 기존의 커널 모드의 드라이버에 Hooking 함수를 등록하는지 알아보았다. 하지만 정말 중요한 ‘어떻게 들어오고 나가는 패킷을 차단하고 허용하는지’에 대한 본격적인 이야기는 하지 않았다. 우리가 등록한 필터 함수는 그 필터 함수의 결과 값에 따라 어떻게 해당 패킷을 처리할 지 결정하게 되는데 다음을 살펴보도록 하자.

typedef PF_FORWARD_ACTION (*PacketFilterExtensionPtr)(

// IP 패킷 헤더

IN unsigned char *PacketHeader,

// 헤더를 포함하지 않는 패킷

IN unsigned char *Packet,

// IP 패킷 헤더의 길이를 제외한 패킷 길이

IN unsigned int PacketLength,

// 장치 인덱스(몇 번째 장치인지)

// 받은 패킷에 대해서

IN unsigned int RecvInterfaceIndex,

// 장치 인덱스(몇 번째 장치인지)

// 보내는 패킷에 대해서

IN unsigned int SendInterfaceIndex,

// IP 주소 형태

// 장치가 받은 주소

IN IPAddr RecvLinkNextHop,

// IP 주소 형태

// 장치가 보낼 주소

IN IPAddr SendLinkNextHop

);

PF_FORWARD_ACTION은 다음과 같은 결과 값을 가질 수 있게 된다.

◆ PF_FORWARD

패킷을 정상적으로 처리하기 위해 시스템 상의 IP Stack에 값을 넣는다. 넣게 되면 해당 패킷은 처리하기 위한 애플리케이션으로 넘어가게 되며 해당 애플리케이션에서는 받은 정보를 가지고 적절한 처리를 하게 된다.

◆ PF_DROP

패킷을 드롭하게 된다. 시스템 상의 IP Stack에 해당 포인터를 넘겨주지 않고 폐기함으로써 애플리케이션은 해당 패킷을 받지 못하게 된다.

◆ PF_PASS

패킷을 그냥 통과시킨다. IP Stack에 넣지는 않지만 시스템 드라이버 내부는 통과하게 된다. 하지만 IP Stack에 값을 넣지 않았기 때문에 애플리케이션에서는 정상적인 패킷 데이터를 받지 못하는 것으로 나온다.

기본적으로 DDK에서 선언되어 있는 값은 이 3개이다. 물론 추가적인 값도 설정되어 있지만 여기서는 예외로 한다(더 많은 정보를 보고 싶으면 DDK를 설치하고 pfhook.h 파일을 살펴보면 된다). ICMP 패킷에 관련해선 PF_ICMP_ON_DROP 함수가 따로 있다. 이 함수는 ICMP 패킷에 대해서 따로 처리를 하는 부분이라고 볼 수 있다. 필터 함수를 보면 알 수 있듯이 패킷과 그 패킷의 헤더는 포인터 형태로 필터 함수를 통과하게 된다. 따라서 패킷의 헤더 정보를 변경하는 작업 또한 필터 함수에서 진행할 수 있게 되는 것이다.

기본적 방화벽 기능 구현

필터 훅 드라이버를 이용해서 네트워크 패킷 필터 드라이버를 작성할 수 있는 방법에 대해서 이야기해 보았다. 물론 강력하고 성능이 좋은 방화벽을 만들려면 더욱 더 깊은 커널 레벨에서 NDIS, TDI 등을 직접 건드려보면서 작성하는 것이 좋겠지만 기본적인 방화벽 기능을 하는 애플리케이션을 작성하고자 한다면 이 방법을 이용해도 충분히 가능할 것이다. 방화벽 작성에 필요한 모든 소스는 이야기하지 않았다. 하지만 가장 중요하다고 볼 수 있는 핵심 부분을 모두 이야기했고 이 기능을 충분히 이해하고 작성한다면 어느 정도 필요한 네트워크 방화벽은 만들 수 있게 될 것이다. @