Sqix

Linux에서 libtins 라이브러리 사용하기 - 2. libtins의 기능들 본문

libtins

Linux에서 libtins 라이브러리 사용하기 - 2. libtins의 기능들

Sqix_ow 2017. 9. 13. 05:05

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

<주의 : libtins를 활용하고자 하시는 분은 libtins 소개 및 설치 편을 읽고 오시기 바랍니다.>


libtins는 PDU Class, Sender, Sniffer 클래스, 주소를 나타내는 클래스 및 개발을 보다 쉽게 해 주는 helper function들을 준수하여 제작된 라이브러리입니다. 우선, PDU에 대해서 살펴보도록 하겠습니다.


1. PDU


PDU란, Protocol Data Unit의 약자로 OSI 모델에서의 정보처리 단위이자 특정 계층의 프로토콜 안에서 지정되는 데이터 단위를 의미합니다. TCP, UDP와 같은 Transport Layer에서는 Segment라고 하고, Network Layer에서는 Packet이라고 불리웁니다. C++에도 PDU Class가 존재하고, libtins의 각 PDU(ip,tcp,udp ...)들은 이 PDU Class를 상속받습니다. 


PDU Class에는 실제 프로토콜 데이터 단위의 크기, 프로토콜 유형을 검색할 수 있는 메서드가 포함되어 있습니다. 또한, Send 메서드를 통해 패킷을 편리하게 보낼 수 있도록 합니다. 또한, Stacking을 지원하는데, 이는 하나의 객체가 내부에 또 다른 PDU 객체를 가질 수 있음을 의미합니다. 간단하게 실제 패킷으로 예를 들면, 일반적인 통신에 사용되는 | Ethernet | IP | TCP | HTTP | 프레임은 Ethernet Header에 Payload로 IP, TCP, HTTP가 있고 IP 헤더에 Payload로 TCP, HTTP가 있습니다. 이처럼 tins에서는 Ethernet 클래스 안에 IP 클래스, IP 클래스 안에 TCP 클래스를 넣을 수 있는 형식으로 되어 있습니다. 직관적으로 프레임을 알 수 있어서 좋은 것 같습니다.


> Inner PDU


PDU 내부의 inner PDU는 PDU::inner_pdu() 함수를 통해서 설정할 수 있습니다. 소스 코드는 다음과 같습니다.


#include <iostream>

#include <tins/tins.h>


using namespace std;

using namespace Tins;


int main(){

        EthernetII eth;

        IP *ip = new IP();

        TCP *tcp = new TCP();


        ip->inner_pdu(tcp);


        eth.inner_pdu(ip);

}


여기서 PDU::inner_pdu() 함수는 주어진 PDU 타입의 파라미터를 호출자 클래스의 내부 PDU로 지정해 줍니다. 파라미터로 들어갈 PDU는 new 연산자를 이용해서 할당되어야 하며, inner_pdu를 사용한 후로 해당 PDU는 호출 객체의 자식 객체가 됩니다. 즉, 해당 객체의 삭제 및 수정은 부모 객체에 의해 이루어지게 됩니다. 여기서는 tcp가 ip의 자식 객체이고, ip가 eth의 자식 객체가 됩니다. 만약 eth 객체의 소멸자가 호출되면, ip, tcp 객체도 함께 삭제가 되므로 memory 누수를 막을 수 있습니다.


만약 소멸자에 의해 삭제되는 것을 막고자 하면, PDU::clone 함수를 이용하여 코드를 작성하면 됩니다. 이 함수는 이미 해당 객체에 스태킹된 자식 PDU들을 반환해 줍니다. 혹은, python의 scapy와 같이 나누기 연산자를 이용해서 PDU 스택을 쌓을 수 있습니다. 그 예는 다음과 같습니다.


#include <iostream>

#include <tins/tins.h>


using namespace std;

using namespace Tins;


int main(){

        EthernetII eth = EthernetII() / IP() / TCP();

        TCP *tcp = eth.find_pdu<TCP>();

        IP &ip = eth.rfind_pdu<IP>();

}


eth에 EthernetII, IP, TCP를 순차적으로 스태킹합니다. 만약, PDU가 필요하여 이를 따로 할당하고자 하면, 위의 소스코드처럼 2가지로 할당할 수 있습니다. 포인터 PDU를 반환하는 <pdu_type>.find_pdu<pdu_type>() 함수, Reference PDU를 반환하는 <pdu_type>.rfind_pdu<pdu_type>() 함수. 이렇게 두 가지 함수가 존재합니다.


2. Address Classes


IP, Hardware 주소는 IPv4Address, IPv6Address, HWAddress<>클래스를 이용하여 핸들링할 수 있습니다. 이러한 클래스 초기화는 std::string을 이용하여 작성할 수 있습니다. 다음 코드와 같이 말입니다.


#include <iostream>

#include <tins/tins.h>


using namespace std;

using namespace Tins;


int main(){

        IPv4Address lo("127.0.0.1");

        IPv4Address empty; // It means "0.0.0.0"


        IPv6Address lo_6("::1");


        cout << "Lo : " << lo << endl;

        cout << "Empty : " << empty << endl;

        cout << "Lo6 : " << lo_6 << endl;


        return 0;

}


Result

> Lo : 127.0.0.1
> Empty : 0.0.0.0
> Lo6 : ::1

이 클래스는 또한, unistd를 사용하는 생성자 역시 제공합니다. 

HWAddress<> 클래스 템플릿의 정의는 다음과 같습니다.

template<size_t n, typename Storage = uint8_t>
class HWAddress;

파라미터 n은 주소의 길이를 나타냅니다.  주로 네트워크 인터페이스에서는 6입니다. 파라미터 Stroage는 n의 유형을 나타냅니다. HWAddress 객체는 std::strings, c의 string, const Storage*, HWAddress를 이용하여 생성할 수 있습니다. 또한, ==연산자를 이용할 수 있으며, 주소를 이용한 iteration 연산이 가능합니다. (주소를 iteration 연산자로써 이용할 수 있다는 의미입니다.)

예를 들어, 이러한 선언이 가능합니다.

HWAddress<6> hw_addr("01:de:22:01:09:af");

그리고 다음처럼 iterator로써의 이용이 가능합니다.

for(auto i : hw_addr) {
cout << static_cast<int>(i) << endl;
}

3. Address Range Class

Libtins에서 지원하는 기능 중, Address Range를 사용하면 주소 대역 구분 등에 매우 용이합니다. 예를 들어, 192.168.1.1부터 192.168.1.255까지의 주소를 이용하고자 할 때, 다음과 같이 프로그램을 작성하여 Address Range Class를 이용할 수 있습니다.

IPv4Range range1 = IPv4Address::from_mask("192.168.1.1", "192.168.1.255");

기본 문법(default)을 이용하면 255.255.255.0까지의 범위를 적용합니다.

IPv4Range range2 = IPv4Address("192.168.1.1") / 24; // Range : 192.168.1.1 to 255.255.255.0

IPv6 역시 마찬가지입니다.

IPv6Range range3 = IPv6Address("dead::") / 120; (dead : 0000:0000:0000:0000:0000:0000:0000-00ff)

혹은 

IPv6Range range4 = IPv6Range::from_mask("dead::","ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00");

처럼 사용할 수 있습니다.

이러한 주소 범위를 이용하면, 특정 주소가 네트워크에 있는지 검사하여 해당 주소에 반복적으로 요청을 할 수 있습니다.
그를 위한 함수로는, <IPv4Range || IPv6Range class variable>.contains를 사용합니다. 예를 들어, ipv4의 범위를 192.168.1.1 ~ 192.168.1.254라고 지정하고, 그 안에 192.168.1.250이 있는지 확인하려면 다음과 같이 작성할 수 있습니다.

IPv4Range range = IPv4Address::from_mask("192.168.1.1", "192.168.1.255");

range.contains("192.168.1.250");

또한, 해당 변수를 iterator로 사용할 수 있습니다.

for(const auto &addr : range){
cout << addr << endl;
}

Network Address와 마찬가지로, HW address에도 이를 사용할 수 있는데, 이를 이용해서 OUI 주소를 통해 제조사를 판별하는 일 등을 할 수 있습니다.

auto range = HWAddress<6>("00:19:D1:00:00:00") / 24;

if(range.contains("00:19:d1:22:33:44")){
cout << "It's Intel's" << endl;
}

4. Network Interfaces

다음은 네트워크 인터페이스의 추상 클래스인 NetworkInterface 클래스입니다. 이는 기본적으로 인터페이스 이름을 파라미터에 넣는 것으로 생성할 수 있고, 예외적으로 IPv4의 경우, IP 주소를 통해서 생성할 수 있습니다. 예제는 다음과 같습니다.

NetworkInterface lo("lo"); // loopback
NetworkInterface ipv4(IPv4Address("127.0.0.1"));

또한, 다음 메소드를 이용해서 인터페이스의 이름을 검색할 수 있습니다.

NetworkInterface::name()

이 함수는 호출이 이루어질 때 마다 이름을 검색하기 때문에, 이름을 반환값으로 받아오는 것이 필요할 때 유용하게 쓸 수 있습니다.

5. Writing Pcap Files

Pcap 파일에 직접 패킷을 쓸 수도 있는데,  PacketWriter 클래스를 이용하여 이 작업이 가능합니다. PacketWriter 클래스를 선언하기 위해선 두 개의 파라미터가 들어갑니다. 첫 파라미터는 pcap 파일의 경로, 두 번째 파라미터는 Data Link type의 PDU입니다. PacketWriter::writer(<pcap file> , Data Link Class)가 바로 그것입니다. 이더넷 타입을 이용한다면 DataLinkType<EthernetII>()를 두 번째 인수로, 무선 인터페이스 타입을 이용한다면 DataLinkType<RadioTap>() 혹은 DataLinkType<Dot11>()을 두 번째 인수로 주면 됩니다. 예제는 다음과 같습니다.

PacketWriter writer1("../pcap_dumps/20170914_eth0dump01.pcap", DataLinkType<EthernetII>());
PacketWriter writer2("../pcap_dumps/20170914_blehdump01.pcap", DataLinkType<RadioTap>());

PacketWriter가 선언되고 나면, write 메소드를 이용해서 파일에 패킷을 작성할 수 있습니다. write 함수는 두 개의 메소드로 오버로드되어 있는데, 하나는 PDU의 begin, end를 각각 첫 번째, 두 번째 파라미터로 받아와 작성을 하고, 나머지 하나는 PDU의 주소를 받아와 작성을 하게 됩니다. 앞의 메소드는 해당 범위를 반복자의 처음과 끝으로 삼아 작성하게 됩니다 .이러한 오버로드를 통해서, PDU의 시작 주소을 이용해서 패킷을 작성하거나, 여러 개의 패킷을 PDU&를 이용하여 한 번에 작성할 수 있습니다. 즉, 다음과 같은 방법을 이용해도 잘 동작이 됩니다. vector<unique_ptr<PDU>>::iterator. vector를 사용하여 작성한 예제는 다음과 같습니다.

#include <tins/tins.h>
#include <vector>

using namespace std;
using namespace Tins;

int main(){
        PacketWriter writer1("../../pcap_dumps/20170914_eth0dump01.pcap",DataLinkType<EthernetII>());

        vector<EthernetII> vec(1,EthernetII("e4:42:a6:a3:1c:7c"));

        writer1.write(vec.begin(), vec.end());

        writer1.write(vec[0]);
}

6. Putting it all together

이번 챕터에서 작성하였던 내용을 기반으로, packet을 생성하여 전송하는 프로그램을 제작하여 보겠습니다.

/* Simple Pcap Packet Writer Program!
 * Written by Sqix
 * Reference : libtins.github.io/tutorial
 */

#include <tins/tins.h>
#include <cassert>
#include <iostream>
#include <string>

using namespace std;
using namespace Tins;

int main(){
        // using default interface, default gateway
        NetworkInterface iface = NetworkInterface::default_interface();

        // Info : Interface's IP, Broadcast, Hardware Address
        NetworkInterface::Info info = iface.addresses();

        // Set e4:42:a6:a3:1c:7c to Default Interface's Hardware Address
        EthernetII eth("e4:42:a6:a3:1c:7c", info.hw_addr);

        // Stacking IP frame on 'eth' PDU, and Set 192.168.0.1 to Default Interface's IP Address
        eth /= IP("192.168.0.1", info.ip_addr);

        // Stacking TCP frame on 'eth' PDU, and Set dst port as 13, src port as 15
        eth /= TCP(13,15);

        // Stacking Payload on 'eth' PDU which containing string "I'm a Payload!"
        eth /= RawPDU("I'm a Payload!");

        // Actual Packet Sender
        PacketSender Sender;

        // Send the Packet Through the Default Interface
        sender.send(eth, iface);
}

이상으로 2편, Basic Tutorial에 대한 포스팅을 마치겠습니다. 다음 포스팅은 Sniffing에 대한 내용이 될 것 같습니다.

읽어주셔서 감사합니다.


Comments