다섯째 날: pcap을 이용한 네트워크 패킷 캡쳐

저자

@luzluna - luz + luna

시작하며

네트워크상에서 작업을 하다보면 가끔씩은 정말 무엇이 어떻게 돌아가고 있는지 두 눈으로 확인하고 싶은 욕구가 (치?)밀어오를 때가 많곤합니다. 응용과 시스템 그리고 원격지 사이에서 그 누구도 잘못이 없어보일 때 범인을 찾으려면 역시 네트워크 패킷을 직접 확인하는 것보다 정확한 것은 없죠. tcpdump와이어샤크 같은 유명한 도구도 있지만 직접 펄 코드와 연동해서 디버깅하는 것이 훨씬 편리할 때가 많습니다. pcap 라이브러리tcpdump 팀이 제작한 라이브러리로 tcpdump와이어샤크등의 패킷 캡쳐 프로그램의 핵심이라고 할 수 있습니다. pcap을 사용하면 펄에서는 손쉽게 패킷 캡쳐를 펄 프로그램과 연동할 수 있습니다.

준비물

pcap 관련 라이브러리를 컴파일하고 NetPacket 모듈에 의존성이 있는 Net::Libdnet 모듈을 컴파일 하기 위해 설치해야 할 도구가 있습니다. 데비안 계열의 리눅스를 사용하고 있다면 다음 명령을 이용해서 패키지를 설치합니다.

$ sudo apt-get install libpcap-dev libdumbnet-dev

필요한 모듈은 다음과 같습니다.

직접 CPAN을 이용해서 설치한다면 다음 명령을 이용해서 모듈을 설치합니다.

$ sudo cpan Net::Pcap NetPacket

사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나 perlbrew를 이용해서 자신만의 Perl을 사용하고 있다면 다음 명령을 이용해서 모듈을 설치합니다.

$ cpan Net::Pcap NetPacket

NetPacket 모듈 설치

시스템에 따라 NetPacket 모듈 설치에 실패하는 경우도 있습니다. NetPacket 모듈 설치시 실패하는 경우는 대부분 Net::Libdnet 모듈 설치 실패로 발생합니다.

...
/home/askdna/perl5/perlbrew/perls/perl-5.18.1/bin/perl /home/askdna/.perlbrew/libs/perl-5.18.1@advent/lib/perl5/ExtUtils/xsubpp  -typemap /home/askdna/perl5/perlbrew/perls/perl-5.18.1/lib/5.18.1/ExtUtils/typemap -typemap typemap  Libdnet.xs > Libdnet.xsc && mv Libdnet.xsc Libdnet.c
cc -c  -I/usr/include -D_REENTRANT -D_GNU_SOURCE -fno-strict-aliasing -pipe -fstack-protector -I/usr/local/include -D_LARGEFILE_SOURCE -D_FILE_OFFSET_BITS=64 -O2   -DVERSION=\"0.98\" -DXS_VERSION=\"0.98\" -fPIC "-I/home/askdna/perl5/perlbrew/perls/perl-5.18.1/lib/5.18.1/x86_64-linux-thread-multi/CORE"   Libdnet.c
Libdnet.xs:37:18: fatal error: dnet.h: 그런 파일이나 디렉터리가 없습니다
#include <dnet.h>
                  ^
compilation terminated.
make: *** [Libdnet.o] 오류 1
...
Installing /home/askdna/.perlbrew/libs/perl-5.18.1@advent/lib/perl5/x86_64-linux-thread-multi/.meta/Net-IPv6Addr-0.2/install.json
Installing /home/askdna/.perlbrew/libs/perl-5.18.1@advent/lib/perl5/x86_64-linux-thread-multi/.meta/Net-IPv6Addr-0.2/MYMETA.json
-> FAIL Installing the dependencies failed: Module 'Net::Libdnet' is not installed
-> FAIL Bailing out the installation for Net-Packet-3.27.
7 distributions installed

이는 dnet 라이브러리가 최근 dumbnet으로 이름을 변경하며 헤더 파일의 이름이 달라져 발생하는 문제입니다. 관련 시스템 개발 라이브러리를 설치했는데도 dnet.h 헤더 파일을 찾지 못한다면 모듈을 약간 수정해주어야 합니다.

$ wget http://cpan.metacpan.org/authors/id/G/GO/GOMOR/Net-Libdnet-0.98.tar.gz
$ tar xvzf Net-Libdnet-0.98.tar.gz
$ cd Net-Libdnet-0.98
$ perl -pi.bak -e 's/#include <dnet.h>/#include <dumbnet.h>/' Libdnet.xs

제대로 변경되었는지 확인해보죠.

$ diff -urN Libdnet.xs.bak Libdnet.xs

다음처럼 변경되었다면 성공입니다.

--- Libdnet.xs.bak      2013-12-05 10:34:56.100728050 +0900
+++ Libdnet.xs  2013-12-05 10:36:01.688559294 +0900
@@ -34,7 +34,7 @@
 #include "XSUB.h"

 #include <stdio.h>
-#include <dnet.h>
+#include <dumbnet.h>

 #ifdef DNET_BLOB_H
 typedef blob_t              Blob;

빌드 및 설치를 진행합니다. 자신의 펄 환경에 따라 사용자 권한 또는 관리자 권한으로 모듈을 설치하세요.

$ perl Makefile.PL
$ make
$ make test
$ make install # 또는 sudo make install

pcap과 관리자 권한

다음은 패킷 캡쳐에 사용할 장치를 화면에 출력하는 간단한 프로그램입니다.

#!/usr/bin/env perl

#
# FILE: pcap.pl
#

use v5.14;
use strict;
use warnings;

use Net::Pcap;

my $err = q{};
my $dev = pcap_lookupdev(\$err);  # find a device

say $dev;

간단하죠? 실행해볼까요?

$ perl pcap.pl

$

어라! 명령줄에 아무 것도 출력되지 않는군요.

pcap 모듈은 네트워크 장비에 직접 접근하기 때문에 관리자 권한이 있어야 제대로 동작합니다. 관리자 권한으로 실행해보죠.

$ sudo perl pcap.pl
eth0
$

잘 되는군요! :-)

펄 경로와 관리자 권한

앞서 모듈 설치시에 잠깐 언급했던 것처럼 여러분이 사용자 계정으로 모듈을 설치하는 방법을 정확하게 알고 있거나 perlbrew를 이용해서 자신만의 Perl을 사용하고 있다면 지금쯤이면 꽤나 혼란스러울 것입니다. 아마도 다음과 같은 오류를 출력하며 간단한 예제 프로그램이 여전히 동작하지 않을테니까요. :)

$ sudo perl pcal.pl
Can't locate Net/Pcap.pm in @INC (you may need to install the Net::Pcap module) (@INC contains: /etc/perl /usr/local/lib/perl/5.18.1 /usr/local/share/perl/5.18.1 /usr/lib/perl5 /usr/share/perl5 /usr/lib/perl/5.18 /usr/share/perl/5.18 /usr/local/lib/site_perl .) at pcap.pl line 1.
BEGIN failed--compilation aborted at pcap.pl line 1.
$

놀라지 마세요. 모든 문제와 문제의 해답은 경고나 오류 메시지에 담겨져 있습니다. 관리자 권한으로 실행하기 위해 관리자 계정으로 로그인 하거나 sudo를 이용해서 프로그램을 실행하는 순간 지금까지 사용하던 펄이 아닌 관리자 계정의 펄을 사용하게 되기 때문입니다. 여러분이 시스템의 펄과는 별개의 펄을 사용하고 있었다면 당연히 모듈도 별개의 펄을 위해 설치했을 것입니다. 어쩌면 필요한 모듈을 찾지 못하는 것은 당연한 결과죠. 지금까지 사용하던 펄이 아닌 관리자 계정의 펄이 어떻게 다른지 직접 확인해보죠.

$ which perl
/home/user/perl5/perlbrew/perls/perl-5.18/bin/perl
$ sudo which perl
/usr/bin/perl
$

문제를 해결하기 위한 몇 가지 방법이 있는데 가장 간단한 방법부터 가장 정확한 방법까지 하나씩 짚어보죠.

관리자 권한으로 로그인, 펄 모듈 설치, 프로그램 실행

DON'T DO THAT!

이 방법은 쉽고 간단해서 마약과 같습니다. 부득이한 경우가 아니라면 추천하지 않습니다.

sudo로 실행시 펄의 절대 경로 지정

sudo로 실행 시 두 번째 인자로 perl을 입력하죠.

$ sudo perl pcal.pl

이 부분에 착안을 하면 관리자 권한으로 실행을 하되 자신의 계정에 설치했던 펄을 사용할 수 있도록 전체 경로를 지정하면 설치했던 모듈을 사용하는 데 지장이 없을 것입니다.

$ sudo /home/user/perl5/perlbrew/perls/perl-5.18/bin/perl pcap.pl
eth0
$

잘 되죠?

쉬뱅라인에 펄의 절대 경로 지정

그런데 매번 실행할때마다 저 복잡한 라인을 다 쓰려면 꽤나 번거롭습니다. 유닉스 계열에서 스크립트는 대부분 쉬뱅 라인을 지정합니다. 실행 권한이 있는 스크립트는 파일의 가장 첫 줄인 쉬뱅 라인을 읽어 자신을 실행시킬 프로그램을 찾습니다. 보통 확장성 있는 펄 프로그램을 작성하기 위해 쉬뱅 라인은 #!/usr/bin/env perl로 작성하는 것을 선호하지만 이 경우 펄의 정확한 절대 경로로 설정할 경우 sudo로 실행한다 하더라도 원하는 펄로 스크립트를 구동할 수 있습니다.

쉬뱅 라인을 바꿔보죠.

#!/home/user/perl5/perlbrew/perls/perl-5.18/bin/perl

#
# FILE: pcap.pl
#

use v5.14;
use strict;
use warnings;

use Net::Pcap;

my $err = q{};
my $dev = pcap_lookupdev(\$err);  # find a device

say $dev;

그리고 스크립트에 실행 권한을 줍니다.

$ chmod +x pcap.pl

실행해볼까요?

$ sudo ./pcap.pl
eth0
$

역시 잘 동작합니다. :)

pcap을 사용할 펄에 적절한 권한 추가

이번 방법은 조금 어렵지만 리눅스를 사용한다면 제일 확실한 방법입니다. 단 시스템과 파일의 권한과 pcap에 대해서 어느 정도 이해할(하려 노력할) 필요가 있습니다. :)

모든 문제의 시작은 pcap 라이브러리가 네트워크 장치에 접근할 때 관리자 권한이 필요하기 때문에 발생합니다. 더 정확히 표현하면 관리자 권한은 모든 장치에 접근할 수 있기 때문에 관리자 권한을 획득해서 프로그램을 실행하는 것은 종종 가장 간단한 해결책이며 유일한 해결책이기도 합니다. 다행히 pcap은 선택의 여지가 있는데 pcap을 사용할 바이너리에 적절한 권한을 부여한다면 사용자 권한이 없이도 pcap 관련 도구를 사용할 수 있습니다.

현재 사용하는 펄의 경로를 우선 확인합니다.

$ which perl
/home/user/perl5/perlbrew/perls/perl-5.18/bin/perl

상기 경로의 펄에 적절한 권한을 추가합니다.

$ sudo setcap cap_net_raw,cap_net_admin=eip /home/user/perl5/perlbrew/perls/perl-5.18/bin/perl

권한이 제대로 추가되었는지 확인합니다.

$ sudo getcap /home/user/perl5/perlbrew/perls/perl-5.18/bin/perl
/home/user/perl5/perlbrew/perls/perl-5.18/bin/perl = cap_net_admin,cap_net_raw+eip

setcapgetcap 유틸리티가 없을 경우 데비안 계열의 리눅스를 사용하고 있다면 다음 명령을 이용해서 패키지를 설치합니다.

$ sudo apt-get install libcap2-bin

파일 관련 권한 기능이 제대로 동작하려면 커널에 capability 기능이 포함되어 있어야 합니다. 리눅스 커널 2.6.33 이후의 경우 커널에 포함되어 있으며 이전 커널의 경우 CONFIG_SECURITY_FILE_CAPABILITIES 설정을 활성화 시키고 빌드되어 있어야 합니다.

해당 기능을 활성화하기 위해 부팅 커널의 매개변수 옵션에 file_caps=1을 추가합니다. 데비안의 경우 /etc/default/grub 파일에서 GRUB_CMDLINE_LINUX_DEFAULT 값을 조정합니다.

--- a/etc/default/grub
--- b/etc/default/grub
-GRUB_CMDLINE_LINUX_DEFAULT="quiet"
+GRUB_CMDLINE_LINUX_DEFAULT="quiet file_caps=1"

수정 후 다음 명령을 실행해서 grub 설정을 갱신합니다.

$ sudo update-grub

커널 매개변수 옵션을 변경했다면 재부팅 후 다시 프로그램을 실행합니다.

$ ./pcap.pl
eth0
$

관리자 권한이 없이도 pcap 관련 함수가 제대로 호출되어 결과를 반환합니다. 권한과 관련한 자세한 내용은 man capabilities 문서를 참조하세요. :)

실제 패킷 캡쳐해보기

먼 길을 돌아 Net::Pcap 모듈을 사용할 준비가 되었으니 이제 패킷을 캡쳐해봐야겠죠?

#!/usr/bin/env perl

#
# FILE: pcap.pl
#

use v5.14;
use strict;
use warnings;

use Net::Pcap;

my $err = q{};
my $dev = pcap_lookupdev(\$err);  # find a device
say $dev;

my $pcap = pcap_open_live( $dev, 1024, 1, 0, \$err );

pcap_loop($pcap, 3, \&process_packet, "just for the demo");

pcap_close($pcap);

sub process_packet {
    my ( $user_data, $header, $packet ) = @_;
    printf "%d/%d %d\n",$header->{caplen}, $header->{len}, length($packet);
}

앞의 예제는 패킷을 3번 캡쳐한 후 캡쳐한 자료의 길이, 헤더에 기록된 길이, 그리고 캡쳐된 데이터의 실제 길이를 출력하는 예제입니다. 실행해 볼까요?

$ ./pcap.pl
eth0
66/66 66
1024/1514 1024
60/60 60
$

필터 기능

너무 많은 자료는 사실 없는 것이나 마찬가지입니다. 원하는 패킷 정보만 추려내려면 필터 기능을 이용하면 됩니다.

#!/usr/bin/env perl

#
# FILE: pcap.pl
#

use v5.14;
use strict;
use warnings;

use Net::Pcap;

my $err = q{};
my $dev = pcap_lookupdev(\$err);  # find a device
say $dev;

my $pcap = pcap_open_live( $dev, 1024, 1, 0, \$err );

my $filter;
my $filter_str = "host advent.perl.kr";
pcap_compile( $pcap, \$filter, $filter_str, 1, 0 );
pcap_setfilter( $pcap, $filter );

pcap_loop($pcap, 10, \&process_packet, "just for the demo");

pcap_close($pcap);

sub process_packet {
    my ( $user_data, $header, $packet ) = @_;
    printf "%d/%d %d\n",$header->{caplen}, $header->{len}, length($packet);
}

새로워진 예제의 핵심은 다음 두 줄입니다.

pcap_compile($pcap, \$filter, $filter_str, 1, 0);
pcap_setfilter($pcap, $filter);

먼저 필터를 컴파일(pcap_compile)한 뒤 필터를 적용(pcap_setfilter)합니다. 예제의 host advent.perl.kr 필터는 advent.perl.kr 호스트와 통신하는 패킷을 캡쳐하라는 의미입니다.

필터를 제대로 사용하려면 필터 문법을 제대로 알아야 합니다. 다음은 유용하게 쓰일만한 필터 몇 가지 입니다.

host advent.perl.kr      # advent.perl.kr 과 통신하는 모든 패킷
dst host advent.perl.kr  # destination 이 advent.perl.kr 인 패킷
src host advent.perl.kr  # source 가 advent.perl.kr 인 패킷
port 80                  # port가 80인 패킷
dst port 80              # destination port 가 80인 패킷
src port 80              # source port 가 80인 패킷
len <= 10                # 10 바이트 이하인 패킷
len >= 10                # 10 바이트 이상인 패킷

and, or, ()을 조합하여 다양하게 변형이 가능합니다.

#
# ip 패킷이면서 192.168.7.0/24대역과 통신하지 않는 패킷
#
ip and not net 192.168.7.0/24

#
# snup 게이트웨이를 통과하면서 ftp 포트를 사용하는 패킷
#
gateway snup and (port ftp or ftp-data)

좀 더 복잡한 조합으로 사용하는 것 역시 가능합니다.

#
# http 접속중 SYN패킷만 모아서 보기
#
tcp[tcpflags] = tcp-syn and port http

더 자세한 필터 문법은 tcpdump 공식 문서를 참조하세요.

패킷 분석

CPAN의 NetPacket 모듈은 패킷 분석을 손쉽게 할 수 있게 도와줍니다. 패킷 분석 기능을 추가한 코드는 다음과 같습니다.

#!/usr/bin/env perl

#
# FILE: pcap.pl
#

use v5.14;
use strict;
use warnings;

use Net::Pcap;
use NetPacket::Ethernet qw(:strip);
use NetPacket::IP qw(:strip);
use NetPacket::TCP;

my $err = q{};
my $dev = pcap_lookupdev(\$err);
say $dev;
$dev = 'wlan0';

my $pcap = pcap_open_live( $dev, 1024, 1, 0, \$err );

my $filter;
my $filter_str = "host advent.perl.kr";
pcap_compile( $pcap, \$filter, $filter_str, 1, 0 );
pcap_setfilter( $pcap, $filter );

pcap_loop($pcap, 10, \&process_packet, "just for the demo");

pcap_close($pcap);

sub process_packet {
    my ( $user_data, $header, $packet ) = @_;

    my $ip_obj  = NetPacket::IP->decode( eth_strip($packet) );
    my $tcp_obj = NetPacket::TCP->decode( $ip_obj->{data} );

    printf(
        "%s:%d->%s:%d (%d) ",
        $ip_obj->{src_ip},  $tcp_obj->{src_port},
        $ip_obj->{dest_ip}, $tcp_obj->{dest_port},
        length( $tcp_obj->{data} )
    );

    print "FIN " if ( $tcp_obj->{flags} & FIN );
    print "SYN " if ( $tcp_obj->{flags} & SYN );
    print "RST " if ( $tcp_obj->{flags} & RST );
    print "PSH " if ( $tcp_obj->{flags} & PSH );
    print "ACK " if ( $tcp_obj->{flags} & ACK );
    print "URG " if ( $tcp_obj->{flags} & URG );
    print "ECE " if ( $tcp_obj->{flags} & ECE );
    print "CWR " if ( $tcp_obj->{flags} & CWR );
    print "\n";

    if ( length( $tcp_obj->{data} ) > 0 ) {
        print $tcp_obj->{data}, "\n";
        print "============\n";
    }
}

실행 후 advent.perl.kr 호스트에 웹브라우저나 curl, wget 등으로 접속하면 다음과 같은 결과를 화면에 출력합니다.

$ perl pcap.pl
eth0
221.143.48.32:80->192.168.25.37:45902 (0) SYN ACK
192.168.25.37:45902->221.143.48.32:80 (0) ACK
192.168.25.37:45902->221.143.48.32:80 (685) PSH ACK
GET /2013/2013-12-04.html HTTP/1.1
Host: advent.perl.kr
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:25.0) Gecko/20100101 Firefox/25.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://advent.perl.kr/2013/
Cookie: __utma=97638351.475138252.1385891323.1386204394.1386208284.13; __utmz=97638351.1385891323.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none); _fjtads1=443; _fjtad6=3:4T2T5T6T1T3; _fjpvnum1=4; _fjpermvid1=1386195821039-8972276113869514; __utmc=97638351; __utmb=97638351.1.10.1386208284
Connection: keep-alive
If-Modified-Since: Tue, 03 Dec 2013 20:57:50 GMT


============
221.143.48.32:80->192.168.25.37:45902 (0) ACK
221.143.48.32:80->192.168.25.37:45902 (157) PSH ACK
HTTP/1.1 304 Not Modified
Server: nginx/1.2.4
Date: Thu, 05 Dec 2013 02:11:56 GMT
Connection: keep-alive
Last-Modified: Tue, 03 Dec 2013 20:57:50 GMT


============
192.168.25.37:45902->221.143.48.32:80 (0) ACK
192.168.25.37:45905->221.143.48.32:80 (0) SYN
192.168.25.37:45906->221.143.48.32:80 (0) SYN
192.168.25.37:45907->221.143.48.32:80 (0) SYN
192.168.25.37:45908->221.143.48.32:80 (0) SYN

정리하며

대부분의 패킷 캡쳐와 관련한 기능은 궁극의 pcap 라이브러리가 다 처리합니다. 펄에서는 이 pcap 라이브러리를 바인딩한 Net::Pcap 모듈CPAN을 통해 배포되고 있어 간단하게 pcap을 사용할 수 있습니다. 성능 역시 pcap 라이브러리에 의존하기 때문에 C로 작성하는 것과 비교해 별반 차이가 없죠. 아마 여러분에게 닥친 상황이 복잡하면 복잡할수록 여러분은 다른 패킷 캡쳐 도구보다 Net::Pcap을 더 선호하게 될 겁니다. :-)

blog comments powered by Disqus