Sqix

Linux에서 libtins 라이브러리 사용하기 - 4. IEEE 802.11 프로토콜 본문

libtins

Linux에서 libtins 라이브러리 사용하기 - 4. IEEE 802.11 프로토콜

Sqix_ow 2017. 9. 21. 06:41

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


원본인 libtins.github.io/tutorial 에서는 Packet 전송 -> TCP Streams -> IP, DHCP Protocols -> 802.11 순서로 진행되고 있긴 하지만, 제가 현재 필요한 것은 802.11이기 때문에 이를 먼저 공부하고 작성하게 되었습니다. 


1. IEEE 802.11?


libtins에서는 802.11 프로토콜을 지원합니다. 가장 대표적으로 이를 이용한 툴이 aircrack-ng입니다. 802.11 프로토콜은 기본적으로 Dot11 클래스를 이용하여 구현됩니다. 이 클래스에는 모든 프레임에서 공유하는 필드가 들어있습니다. 이는 상속을 통해서 전달됩니다.


2. Management Frames


이제 Dot11Management라는 Abstract Class를 이용하는 관리 프레임을 살펴봅시다. 해당 클래스에는 Tag Option을 검색하고 추가할 수 있는 helper method들이 포함되어 있습니다. 그 예로, Beacon Frame을 나타내는 클래스인 Dot11Beacon을 예로 들어 보겠습니다.


Dot11Beacon beacon;

beacon.addr1(Dot11::BROADCAST); // 이는 Broadcast beacon frame을 만든 것으로, ff:ff:ff:ff:ff:ff와 같습니다.

beacon.addr2("<source MAC address>"); // 임의의 MAC Address를 src로 설정합니다.

beacon.addr3(beacon.addr2()); // BSSID를 설정합니다.


beacon.ssid("<example ssid name>"); // SSID를 설정합니다.

beacon.ds_parameter_set(<channel number>); // 채널 number를 설정합니다.

beacon.supported_rates({ 1.0f, 5.5f, 11.0f }); // 지원되는 rate 리스트입니다.


beacon.rsn_information(RSNInformation::wpa2_psk()); // wpa2_psk로 암호화합니다.


이렇게 beacon 패킷이 만들어졌습니다. 여기서 주의할 점은, Dot11Beacon::supported_rates() 함수는 C++11 이상의 최신 C++ 버전에서만 사용이 가능합니다. 해당 메서드는 std::vector<float>을 파라미터로 사용하므로, vector의 initializer를 initializer_list에서 호출합니다. 만약 이를 이용하지 않고 직접 구현하고자 한다면, tmp vector를 만들어서 파라미터로 전달해야 합니다.


3. Data Frames


802.11에서 캡슐화 된 프로토콜에 대한 기본 개념을 익혀야 데이터 프레임을 처리할 수 있습니다. 암호화가 되어 있지 않다는 가정하에, TCP 패킷은 libtins에서 다음과 같이 구성됩니다.


Dot11Data -> SNAP -> IP -> TCP -> RawPDU


<Non-encrypted IEEE 802.11 Packet>


여기서 SNAP 클래스는 LLC + SNAP 캡슐화를 나타냅니다. 만약 실제로 패킷을 만들 경우, 이 부분을 보다 주의해서 보아야 합니다. 만약 스니핑이 이루어지는 동안 Layer를 찾고자 하면, PDU::find_pdu method를 사용해야 합니다. 만약 데이터가 암호화된 경우, RawPDU만 검색이 될 수도 있습니다. 패킷 생성은 다음과 같이 수행됩니다.


Dot11Data data = Dot11Data() / SNAP() / IP() / TCP() / RawPDU("Hello Tins!");


4. Data Frame Decrypting


libtins에서는 WEP(RC4), WPA, WPA2(AES, TKIP)에 적용된 암호화의 해독을 지원합니다. 단,  PSK 해독은 지원되지 않습니다.


5. Decrypting WEP encrypted frames


Crypto namespace에 있는 WEPDecrypter 클래스를 이용하여 WEP 암호 해독을 할 수 있습니다. 해당 타입의 객체를 만들고 (bssid, password) 튜플을 추가하면 패킷을 해독할 수 있습니다. 예제는 다음과 같습니다.


Crypto::WEPDecrypter decrypter;

decrypter.decrypter.add_password("<BSSID>", "<Password>"); // 해당 BSSID와 Password를 가진 AP에서 전송되는 패킷 복호화

Dot11Data some_data = generate_dot11data();

if(decrypter.decrypt(some_data)){

cout << "Data was successfully decrypted!" << endl;

}

else {

cout << "Decryption failed!" << endl;

}


꽤나 간단한 예제였습니다. 암호화 해독에 실패하는 경우도 있는데, Checksum이 잘못되어 이러한 일이 발생할 수도 있습니다. 이러한 경우, 데이터가 포함된 RawPDU가 제거되어 some_data.inner_pdu()는 nullptr가 되어버립니다. 암호 해독이 성공적으로 이루어진다면, RawPDU는 SNAP PDU로 대체되고, 그 뒤에 패킷이 위치합니다. TCP 패킷을 예로 들어 보겠습니다.


Dot11Data -> RawPDU

WEPDecrypter

Dot11Data -> SNAP -> IP -> TCP -> RawPDU


만약 이를 적용했다고 하더라도 데이터 프레임이 암호화되지 않았거나 BSSID가 주어지지 않는 경우에는 그대로 패킷이 남아있습니다. 체크섬이 유효하지 않거나 해독되지 않은 패킷이 제거되는 이유는 다음과 같습니다. Snippet에서 보여주는 상황은 일반적인 상황이 아닙니다. 암호를 해독하려면 pcap 파일 혹은 Network Interface에서 패킷을 가져와야 합니다. WEPDecrypter 클래스는 Sniffer와 Sniffer Callback function 사이에 존재하도록 설계되었습니다. 해당 작업은 DecrypterProxy 클래스 Template를 이용하여 수행 가능합니다. 다음 예제를 봅시다.


bool handler(PDU & pdu){ // 콜백 함수입니다.

// 이 곳에 handling 코드를 작성합니다.

return true;

}


...

// decrypter_proxy 클래스를 생성하는 코드입니다.

auto decrypt_proxy = make_wep_decrypter_proxy(&handler);

// 혹은 다음과 같이 생성할 수도 있습니다.

decrypt_proxy.decrypter().add_password("<BSSID>", "<Password>");


Sniffer sniffer("<interface name>", Sniffer::PROMISC);

// 이제 스니핑과 복호화를 할 수 있습니다.

sniffer.sniff_loop(decrypt_proxy);


위의 코드는 WEPDecrypter를 사용하는 일반적인 상황입니다. 코드를 분석해보면, 먼저 handler라는 handling function을 정의합니다. 그 후, decrypter proxy를 만듭니다. c++11 이후의 auto 지정자가 사용되었기 때문에, 굳이 DecryperProxy <bool(*)(PDU&), WEPDecrypter>와 같은 객체 유형을 작성하지 않아도 됩니다. 해당 프록시 클래스는 Sniffer::sniff_loop의 파라미터로 이용하기 위한 operator()를 구현합니다. 또한, WEPDecrypter를 저장합니다. 


WEPDecrypter는 DecrypterProxy::decrypter method를 이용하여 접근할 수 있습니다.동일한 <BSSID, Password>가 프록시 클래스가 보유하는 decrypter에 추가됩니다. 그 후, 스니퍼가 생성되어 <interface name>에 해당되는 패킷을 해독합니다. Proxy는 여기서 단순히 handler를 호출하는 대신, 제공된 BSSID에 의해 식별된 AP로부터 보내지는 데이터 프레임을 가로채어 내용을 해독합니다. 만약 Handler Callback이 실행될 때, PDU 파라미터가 해독되어야 하는 프레임이면 제공된 PDU 이외이 BSSID 프레임은 유지되며, 같은 콜백에서 처리를 할 수 있습니다. 


만약 체크섬이 유효하지 않다면 해당 패킷은 버려집니다. 이러한 디자인을 적용한다면, 새로운 Decrypter를 만들 때 Decrypting Chain을 적용할 수 있습니다.



6. Decrypting WPA2 encrypted frames


이번에는 WPA2입니다. tins에서는 마찬가지로 WPA2(AES || TKIP) 암호화 프레임 해독을 지원합니다. WPA2의 암호 해독은 RC4인 WEP보다 복잡합니다. 암호 해독을 하는 도중, 추후에 필요한 nonce가 교환되는 클라이언트와 AP 간의 Handshake가 발생합니다. 또한, PMK를 생성하기 위해서는 AP의 SSID가 필요하기 때문에 이를 넣어 주어야 합니다. Object는 해당 SSID를 찾는 비콘 프레임을 분석하여 해당 AP에 적용된 Handshake를 BSSID를 사용하여 필터링할 수 있도록 합니다. WPA2Decrypter는 이 Handshake를 찾아서 클라이언트와 주고받는 모든 패킷을 해독합니다. 예제는 다음과 같습니다.


bool handler(PDU &pdu){

// Not Encrypted PDU

return true;

}


...


auto decrypt_proxy = make_wpa2_decrypter_proxy(&handler);

decrypt_proxy.decrypter().add_ap_data("<PSK>", "<AP SSID>");


Sniffer sniffer("<Interface name>", Sniffer::PROMISC);

sniffer.sniff_loop(decrypt_proxy);


7. 802.11 encalsulation


네트워크 인터페이스 드라이버는 일반적으로 Capsulation된 형태를 이용합니다. 주로 RadioTap을 사용합니다. 드라이버가 RadioTap을 사용하면 패킷을 다음과 같이 보냅니다. 


RadioTap -> Dot11Data -> RawPDU


RadioTap 프로토콜은 802.11 Frame에 대한 추가 정보를 제공합니다. 신호 강도, Noise, 무결성을 위한 검사 시퀀스의 사용 여부 등을 알 수 있습니다. 대부분 동일한 value를 사용하기 때문에, 모든 옵션에 대한 정보를 제공할 필요는 없습니다. 예제는 다음과 같습니다.


#include <tins/tins.h>


using namespace Tins;


int main(){

RadioTap radio = RadioTap() / Dot11Beacon();

PacketWriter writer("<pcap file location>", PacketWriter::RADIOTAP);

writer.write(radio);

}


일부 드라이버는 캡슐화를 사용하지 않는데, tins에서는 아직 해당 부분을 지원하지 않고 있고, 추후 개발될 예정이라고 합니다.

Comments