Sqix

Linux에서 libtins 라이브러리 사용하기 - 3. Packet Sniffing 본문

libtins

Linux에서 libtins 라이브러리 사용하기 - 3. Packet Sniffing

Sqix_ow 2017. 9. 21. 04:26
ddd


<작성자가 libtins를 사용하는 환경은 Ubuntu 16.04.2 LTS, Kernel version 4.10 환경입니다>


1. Sniffing 기초


스니핑은 Sniffing 클래스를 이용하여 가능합니다. 해당 클래스는 libpcap 문자열 필터를 이용이 가능하고, 네트워크 인터페이스에서 스니핑이 가능하게 해 주며, 전송되는 패킷을 해석하고, PDU 오브젝트를 제공하여 보다 쉬운 스니핑 기능을 제공합니다. 필터를 설정하면 두 가지의 패킷 검색 기능이 제공됩니다. 그 중 하나는 Sniffer::next_packet 입니다. 이 멤버함수를 이용한다면 필터를 이용해서 패킷 검색을 할 수 있습니다.


Sniffer sniffer("<interface name>"); // up to 64kb packet


sniffer.set_filter("ip src 192.168.0.1"); // source ip address가 192.168.0.1인 패킷으로 필터링


PDU *some_pdu = sniffer.next_packet(); // 패킷 검색


...


delete some_pdu;


2. Sniffer 구성


3.2 버전 이후에는 스니핑 세션에 영향을 미치도록 하기 위해서 스니퍼에 부여할 수 있는 매개 변수들이 작성된 클래스가 존재합니다. libpcap의 pcap_setfilter, pcap_set_promisc 등의 rapper라고 볼 수 있습니다. 예를 들어, Port 80에서 패킷을 캡쳐하고, 400바이트의 snapshot 길이를 가지며, promiscuous mode로 스니핑을 하고자 하면 다음과 같이 코드를 작성하면 됩니다.


SnifferConfiguration config;

config.set_filter("port 80");

config.set_promisc_mode(true);

config.set_snap_len(400);


Sniffer sniffer("<interface name>", config);


Packet Sniffing이 지연되는 경우가 종종 발생하는데, 이는 burst mode를 지원하는 1.5 버전 이상의 libpcap이 그 원인인 경우가 많습니다. 이러한 경우, immediate 모드를 적용하여 해결할 수 있습니다.


SnifferConfiguration::set_immediate_mode


3. Loop sniffing


Sniffer::next_packet과 달리 libpcap의 pcap_loop처럼 Sniffer 객체에서 루프를 이용하여 스니핑할 수 있는 함수가 존재합니다. 그 함수는 바로 Sniffer::sniff_loop 함수입니다. 해당 메서드는 Template functor를 파라미터로 사용하며, 다음 signature들 중 하나를 이용해 operator를 정의해야 합니다.


bool operator()(PDU&);

bool operator()(const PDU&);


// C++11 mode를 이용하는 경우

bool operator()(Packet&);

bool operator()(const Packet&);


Sniffer::sniff_loop를 호출하면 Sniffer 클래스가 바로 패킷을 처리합니다. functor는 처리가 완료된 패킷들을 파라미터로 하여 호출됩니다. 스니핑을 멈추기 위해서는 functor에서 false를 반환하도록 하면 됩니다. 그렇지 않다면, default인 true 값을 계속 반환하여 Sniffer::sniff_loop 함수가 계속 실행된 채로 유지되게 됩니다. 펑터 객체는 복사되어 생성되기 때문에, 이를 감안하여 코드를 작성하여야 합니다. 


템플릿 파라미터 유형의 객체와 멤버 함수에 대해서 포인터로 가리키고 있는 HandlerProxy를 반환하는 Helper Template Function이 존재합니다. 해당 객체는 필수 연산자를 구현하는데, 구현된 연산자는 파라미터로 가져온 객체 포인터를 사용하여 제공된 멤버 함수의 포인터로 호출 신호를 전달합니다.


#include <tins/tins.h>


using namespace Tins;


bool doo(PDU&){

return false;

}    


struct foo{

void bar(){

SnifferConfiguration config;

config.set_promisc_mode(true);

config.set_filter("ip src 192.168.0.100");

Sniffer sniffer("<interface name>", config);

// 헬퍼 함수를 이용해서 this->handle을 호출할 수 있습니다. 

// 만약 boost or C++11을 사용한다면 boost::bind or std::bind를 사용할 수 있습니다.

sniffer.sniff_loop(make_sniffer_handler(this, &foo::handle);

sniffer.sniff_loop(doo);

// 위의 두 가지 방식으로 sniffer_loop를 구현할 수 있습니다.

}


bool handle(PDU&){

return false;

}

};


int main(){

foo f;

f.bar();

}


이와 같이 Sniffer::sniff_loop을 이용하면 코드를 줄이면서 패킷을 처리할 수 있습니다.


위의 예제처럼 우리는 PDU&를 이용하여 IP 192.168.0.100에 보내지는 IP PDU를 스니핑하고 있습니다. 스니핑을 위해서는, 파라미터 안에 저장된 IP PDU를 검색해야 합니다. 다행스럽게, 전편에서 다뤘던 PDU의 특성 상 PDU Stack 전체에서 특정한 PDU 유형을 검색하고, 이에 대한 Reference를 반환하도록 요청할 수 있습니다. 따라서, 해당 PDU에서 우리가 원하는 패킷이 발생하지 않았다면, pdu_not_found 예외를 발생시켜 throw할 수 있습니다. 해당 코드 예제는 다음과 같습니다.


bool doo(PDU &some_pdu){

// IP PDU가 포함된 패킷인지 검사하는 함수. loop는 계속 진행됩니다.

const IP &ip = some_pdu.rfind_pdu<IP>();

std::cout << "Destination address: " << ip -> dst_addr() << std::endl;

return false;

}


void test(){

SnifferConfiguration config;

config.set_promisc_mode(true);

config.set_filter("ip src 192.168.0.100");

Sniffer sniffer("<interface name>", config);

sniffer.sniff_loop(doo);

}


패킷을 하나씩 가져오는 것 보다 loop를 더 잘 이용할 수 있는 방법은 예외처리입니다. Sniffer::sniff_loop는 functor body에 throw 되어진 pdu_not_found, malformed_packet을 모두 감지할 수 있습니다. 이는 개발을 할 때, PDU::rfind_pdu를 이용할 수 있다는 것을 의미하고 예외는 Sniffer에 계속해서 잡혀 처리할 수 있기 때문에 그러한 PDU가 발견되더라도 상관이 없다는 것입니다.


4. 반복자(iterator)를 이용한 Sniffing


loop를 이용하지 않고 패킷을 검색할 수도 있는데, iterator를 이용하는 방법이 바로 그것입니다. forward iterator를 반환하는 begin(), end()를 정의하여 패킷 스니핑 및 패킷 검색에 이용할 수 있습니다.


Sniffer s = . . . ;

for(auto &packet : s){

process(packet);

}


5. Packet Object


만약 Timestamp 객체와 PDU를 함께 저장해야 하는 경우, Packet 클래스를 사용할 수 있습니다. Packet 클래스에는 PDU와 Timestamp 객체가 함께 포함되어 있고, 복사 및 이동 연산을 할 수 있습니다. 예제는 다음과 같습니다.


#include <vector>

#include <tins/tins.h>


using namespace std;

using namespace Tins;


int main(){

vector<Packet> vt;


Sniffer sniffer("<interface name>");

while(vt.size() != 10){

 //next_packet은 임시적으로 변환된 PtrPacket을 리턴합니다.

vt.push_back(sniffer.next_packet());

}

for(const auto& packet : vt){

if (packet.pdu()->find_pdu<IP>()) { // IP PDU가 존재하는가?

cout << "At : " << packet.timestamp().seconds() << " - " << packet.pdu()->rfind_pdu<IP>().src_addr() << endl;

}    

}


return 0;

}


마찬가지로 sniffer::next_packet과 함께 패킷 객체 역시 사용이 가능합니다.


Sniffer sniffer("<interface name>");

unique_ptr<PDU> pdu ptr(sniffer.next_packet()); // PDU 포인터

Packet packet = sniffer.next_packet(); // 포인터를 이용할 필요가 없다.

// 만약 에러가 있는 패킷이라면, packet.pdu() == nullptr 로 찾아야 한다.

if(packet){

process_packet(packet);

}


Sniffer::sniff_loop에서 사용되는 Functor 객체에서도 패킷을 받아들일 수 있지만, 이는 C++11 모드로 컴파일 옵션을 주어야 합니다.


6. pcap 파일 읽어오기


간단하게 pcap 형식의 파일을 읽어올 수도 있습니다. FileSniffer 클래스는 열고자 하는 파일을 파라미터로 사용해서 해당 pcap 파일을 처리할 수 있도록 합니다. SnifferFileSniffer는 모두 next_pdu sniff_loop가 구현된 BaseSniffer에서 상속을 받아옵니다. 따라서, 위의 Sniffer 예제처럼 FileSniffer를 사용할 수 있습니다.


#include <tins/tins.h>

#include <iostream>

#include <stddef.h>


using namespace std;

using namespace Tins;


size_t counter(0); 


bool count_packets(const PDU&){

counter++;

return true;

}


int main(){

FileSniffer sniffer("<pcap file directory>");

sniffer.sniff_loop(count_packets);

cout << "There are " << counter << "packets in the pcap file" << endl;

}


7. 패킷 해석하기


이제 패킷을 스니핑하거나 pcap 파일을 읽어들이는 방법을 익혔으므로, 가져온 패킷을 해석해보도록 하겠습니다. 원본 패킷이 읽힐 때 마다 해당 패킷의 Data Link 유형의 객체(EthernetII, RadioTap 등)가 만들어집니다. 이러한 PDU들은 내부 플래그를 기반으로 PDU 유형을 감지하고 해당하는 PDU를 하위에 추가하는 작업을 하도록 합니다. 이 동작은 transport layer 계층을 제외하고 모든 PDU에서 수행됩니다. DNS 패킷을 예로 들면 다음과 같습니다.



EthernetII -> IP -> UDP -> RawPDU (dns data)


그 다음, RawPDU의 Payload를 활용하여 패킷을 해석할 수 있습니다.


bool handler(const PDU& pkt){

const UDP &udp = pkt.rfind_pdu<UDP>(); // udp pdu가 존재하는지 확인

if(udp.sport() == 53 || udp.dport() == 53) { // port가 53인지 확인

DNS dns = pkt.rfind_pdu<RawPDU>().to<DNS>(); // DNS 패킷의 해석 작업을 하는 코드. Throw되어도 Sniffer에서 catch.

for(const auto &query : dns.queries()) {

cout << query.dname() << endl;

}

}

return true;

}


DHCP 등과 같은, 이와 다른 프로토콜들에서도 같은 메커니즘이 적용됩니다. Application Layer에서는 이러한 해석이 효율이 떨어지기 때문에 이를 구현해 놓지 않았습니다. 

Comments