Dog Sniffing The Network: Monitor Mode To Wireless Frame Analysis

· omacs's blog

#toyprojects #wltools

# Contents

  1. Introduction
  2. Monitor Mode To Capture Wireless Frames
    1. Opening wireless adapters via system calls
    2. Ioctls of wireless adapters in Linux
    3. Capturing wireless frames in the air
  3. Wireless Frame Anaylsis
    1. Radiotap header
    2. IEEE 802.11 frame
  4. Conclusion
  5. Appendix
    1. How wireless ioctls work inside the kernel?
  6. References

# Introduction

와이파이 혹은 무선 랜 (Wireless LAN)상에서의 프레임을 캡처하는 프로그램을 작성하는 예제는 이미 많이 작성되어 있다. 그리고 이들은 대부분 libpcap이나 scapy, airodump-ng, airmon-ng 등을 사용한다. 물론, 이렇게 라이브러리를 사용하여 실습을 진행하는 것으로부터 많은 것을 배울 수 있다. 하지만 가능한 OS API와 표준 라이브러리만을 사용하여 구현해본다면, 보다 많은 것을 배울 수 있을 것이다.

이에 본 글에서는 외부 라이브러리의 사용을 최소화하여 무선 랜 프레임을 캡처하는 프로그램을 작성하기 위한 지식을 다룬다. 여기서 다루는 코드를 컴파일하고 실행하기 위해서는 우선 모니터 모드를 지원하는 무선 랜 어댑터가 있어야 한다: ipTIME N150UA2 (2.4GHz) [3], ipTIME A2000UA-4dBi (2.4 & 5GHz) [4]. 그리고 개발 환경으로는 우분투 22.04 [5] (리눅스 커널 버전: 6.5.0-15-generic)를 권장하며, sudo dmesg [6]를 사용할 수 있는 상태로 가정한다 (즉, 루트 권한도 사용할 수 있어야 한다).

본 글에서 사용하는 예제 코드들은 대부분 코드 조각이므로 완전한 예제 코드는 Wireless tool in C repo (URL: https://github.com/david232818/Wireless-tool-in-C)를 참고하라.

마지막으로, 당사자(송신인과 수신인)의 동의 없이, 그리고 통신비밀보호법, 형사소송법, 군사법원법의 규정에 의하지 않고 무선 랜 프레임을 캡처하는 것은 통신비밀보호법 제3조(통신 및 대화비밀의 보호) 등의 위반에 해당하며 제16조(벌칙) 등에 근거하여 법적 처벌을 받을 수 있다. 따라서 본 글의 내용은 모든 경우에 합법적이고, 연구 목적으로 사용되어야만 한다[11].

# Monitor Mode To Capture Wireless Frames

무선 랜 프레임을 캡처하기 위해서는 무선 랜 어댑터를 모니터 모드로 설정해야 하며, 이를 위해서는 해당 무선 랜 어댑터가 모니터 모드를 지원해야 한다. 이렇게 모니터 모드로 설정된 어댑터는 수신 가능한 범위에서 전송되고 있는 모든 패킷을 수신할 수 있다. 모니터 모드와 비슷하게 프레임을 캡처할 수 있는 프로미스큐어스 모드 (promiscuous mode)가 존재한다. 하지만 모니터 모드는 Access Point (AP)에 연결되지 않고도, 즉 공유기에 연결되지 않아도 프레임 캡처가 가능한 반면, 프로미스큐어스 모드는 공유기에 연결되어야 프레임 캡처가 가능하다[1, 2].

우분투가 설치된 컴퓨터에 무선 랜 어댑터를 연결하면 커널은 해당 어댑터의 드라이버로 등록된 커널 모듈을 로드한다. 이는 sudo dmesg 명령어로 확인할 수 있으며, 무선 랜 어댑터의 경우 해당 어댑터에 해당하는 무선 랜 인터페이스의 이름이 표시된다[6].

 1$ sudo dmesg
 2...
 3[91112.413237] usb 2-1: new high-speed USB device number 16 using ehci-pci
 4[91112.790635] usb 2-1: New USB device found, idVendor=148f, idProduct=7601, bcdDevice= 0.00
 5[91112.790650] usb 2-1: New USB device strings: Mfr=1, Product=2, SerialNumber=3
 6[91112.790659] usb 2-1: Product: 802.11 n WLAN
 7[91112.790669] usb 2-1: Manufacturer: MediaTek
 8[91112.790677] usb 2-1: SerialNumber: 1.0
 9[91113.133362] usb 2-1: reset high-speed USB device number 16 using ehci-pci
10[91113.502626] mt7601u 2-1:1.0: ASIC revision: 76010001 MAC revision: 76010500
11[91113.515830] mt7601u 2-1:1.0: Firmware Version: 0.1.00 Build: 7640 Build time: 201302052146____
12[91116.893475] mt7601u 2-1:1.0: Vendor request req:07 off:0730 failed:-110
13[91118.927602] mt7601u 2-1:1.0: EEPROM ver:0d fae:00
14[91119.633770] ieee80211 phy14: Selected rate control algorithm 'minstrel_ht'
15[91119.808372] mt7601u 2-1:1.0 wlx588694f7cb14: renamed from wlan0
16...
17$

위 로그에서 "wlx588694f7cb14"가 무선 랜 인터페이스의 이름이 된다.

# Opening wireless adapters via system calls

일반적으로 userland에서 디바이스를 제어하는 방법은 디바이스 파일을 열고 ioctl()의 command를 사용하는 것이다. 그럼 우리는 다음 두 가지를 알아야 한다:

그럼 먼저 무선 랜 인터페이스를 디바이스 파일과 같이 여는 방법을 알아보겠다. 그 방법은 바로 소켓을 여는 것이다. 즉, 디바이스 파일 기술자에 적용되는 ioctl() 연산이 네트워크 소켓에도 적용될 수 있다는 것이다[8].

 1#include <stdio.h>
 2#include <unistd.h>
 3#include <sys/types.h>
 4#include <arpa/inet.h>
 5#include <sys/socket.h>
 6#include <linux/if_ether.h>
 7
 8int open_wlan_if(void)
 9{
10    int fd;
11
12    fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
13    if (fd == -1)
14        fprintf(stderr, "Cannot open socket..\n");
15    return fd;
16}

그러나 이 소켓이 디바이스 파일처럼 그 디바이스를 특정하는 것은 아니다. 그래서 이 소켓 파일을 ioctl()에 전달하여 무선 랜 인터페이스를 제어하기 위해서는 해당 인터페이스를 특정하기 위한 정보를 커널에게 전달해주어야 한다. Linux ioctl()에는 command에 해당하는 데이터를 인자로 전달할 수 있다. 따라서 인터페이스를 특정하기 위한 정보를 이 인자에 포함시켜 ioctl()에 전달하면 된다[7].

이제 특정 무선 랜 인터페이스를 ioctl()로 제어하는 방법을 알아보겠다. Linux는 네트워크 디바이스를 설정하기 위한 표준적인 ioctls를 지원한다. 이는 모든 소켓 파일 기술자와 함께 사용될 수 있고, 다음과 같은 struct ifreq를 인자로 전달하여 사용하게 된다[9]:

 1struct ifreq {
 2    char ifr_name[IFNAMSIZ]; /* Interface name */
 3    union {
 4        struct sockaddr ifr_addr;
 5        struct sockaddr ifr_dstaddr;
 6        struct sockaddr ifr_broadaddr;
 7        struct sockaddr ifr_netmask;
 8        struct sockaddr ifr_hwaddr;
 9        short           ifr_flags;
10        int             ifr_ifindex;
11        int             ifr_metric;
12        int             ifr_mtu;
13        struct ifmap    ifr_map;
14        char            ifr_slave[IFNAMSIZ];
15        char            ifr_newname[IFNAMSIZ];
16        char           *ifr_data;
17    };
18};

이때 커널은 ifr_name[]으로 인터페이스를 식별하고, union structure에 해당하는 값을 가져오거나, 해당하는 값으로 설정한다. 위 코드의 union structure에서 무선 랜 어댑터를 모니터 모드로 바꾸기 위한 멤버는 ifr_flags이다. 이 멤버는 해당 디바이스 (여기서는 무선 랜 인터페이스)의 활성화된 플래그를 얻거나 (ioctl command: SIOCGIFFLAGS) 설정할 때 (ioctl command: SIOCSIFFLAGS) 사용된다. 이 멤버는 다음과 같은 값들에 대한 비트 마스크를 가질 수 있다[7, 9]:

IFF_UP            Interface is running.
IFF_BROADCAST     Valid broadcast address set.
IFF_DEBUG         Internal debugging flag.
IFF_LOOPBACK      Interface is a loopback interface.
IFF_POINTOPOINT   Interface is a point-to-point link.
IFF_RUNNING       Resources allocated.
IFF_NOARP         No arp protocol, L2 destination address not
                set.
IFF_PROMISC       Interface is in promiscuous mode.
IFF_NOTRAILERS    Avoid use of trailers.
IFF_ALLMULTI      Receive all multicast packets.
IFF_MASTER        Master of a load balancing bundle.
IFF_SLAVE         Slave of a load balancing bundle.
IFF_MULTICAST     Supports multicast
IFF_PORTSEL       Is able to select media type via ifmap.
IFF_AUTOMEDIA     Auto media selection active.
IFF_DYNAMIC       The addresses are lost when the interface
                goes down.
IFF_LOWER_UP      Driver signals L1 up (since Linux 2.6.17)
IFF_DORMANT       Driver signals dormant (since Linux 2.6.17)
IFF_ECHO          Echo sent packets (since Linux 2.6.25)

위 값들 중에서 여기서 관심 가지는 비트 마스크는 IFF_UP 뿐임을 기억하라. 이 값은 인터페이스 모드를 바꿀 때 잠시 인터페이스를 down시키고 다시 up시키는데 사용된다. 이를 C 코드로 작성하면 다음과 같다[9, 10]:

 1#include <stdio.h>
 2#include <unistd.h>
 3#include <sys/types.h>
 4#include <sys/ioctl.h>
 5#include <linux/if_ether.h>
 6
 7int if_down(const int fd, /* socket file descriptor */
 8          const char *ifname) /* interface name */
 9{
10    struct ifreq ifr;
11    
12    if (fd == -1)
13        return -1;
14
15    strncpy(ifr.ifr_name, ifname, IFNAMSIZ);    
16    ifr.ifr_flags = (short) 0xffff;
17    ifr.ifr_flags &= ~IFF_UP; /* interface down */
18    if (ioctl(fd, SIOCSIFFLAGS, &ifr) == -1) {
19        fprintf(stderr, "Interface down failed..\n");
20	return -1;
21    }
22    return 0;
23}
24
25int if_up(const int fd, /* socket file descriptor */
26          const char *ifname) /* interface name */
27{
28    struct ifreq ifr;
29    
30    if (fd == -1)
31        return -1;
32    
33    strncpy(ifr.ifr_name, ifname, IFNAMSIZ);    
34    ifr.ifr_flags = 0;
35    ifr.ifr_flags |= IFF_UP; /* interface up */
36    if (ioctl(fd, SIOCSIFFLAGS, &ifr) == -1) {
37        fprintf(stderr, "Interface up failed..\n");
38	return -1;
39    }
40    return 0;
41}

그럼 무선 랜 인터페이스 모드는 어디에 정의되어 있을까? 바로 include/uapi/linux/wireless.h에 정의되어 있다[9, 10].

1/* Modes of operation */
2#define IW_MODE_AUTO	0	/* Let the driver decides */
3#define IW_MODE_ADHOC	1	/* Single cell network */
4#define IW_MODE_INFRA	2	/* Multi cell network, roaming, ... */
5#define IW_MODE_MASTER	3	/* Synchronisation master or Access Point */
6#define IW_MODE_REPEAT	4	/* Wireless Repeater (forwarder) */
7#define IW_MODE_SECOND	5	/* Secondary master/repeater (backup) */
8#define IW_MODE_MONITOR	6	/* Passive monitor (listen only) */
9#define IW_MODE_MESH	7	/* Mesh (IEEE 802.11s) network */

그러나 struct ifreq에는 위 값들을 유의미하게 저장할 멤버가 없다. 즉, 위 값들을 가져오거나 설정하기 위해서는 다른 ioctl command를 사용해야 하고, 이에 해당하는 구조체가 필요하다. 그 command와 구조체는 include/uapi/linux/wireless.h에 정의되어 있으며, 각각 다음과 같다[10]:

 1/* ... */
 2
 3#define SIOCSIWMODE	0x8B06		/* set operation mode */
 4#define SIOCGIWMODE	0x8B07		/* get operation mode */
 5
 6/* ... */
 7
 8/*
 9 * This structure defines the payload of an ioctl, and is used
10 * below.
11 *
12 * Note that this structure should fit on the memory footprint
13 * of iwreq (which is the same as ifreq), which mean a max size of
14 * 16 octets = 128 bits. Warning, pointers might be 64 bits wide...
15 * You should check this when increasing the structures defined
16 * above in this file...
17 */
18union iwreq_data {
19	/* Config - generic */
20	char		name[IFNAMSIZ];
21	/* Name : used to verify the presence of  wireless extensions.
22	 * Name of the protocol/provider... */
23
24	struct iw_point	essid;		/* Extended network name */
25	struct iw_param	nwid;		/* network id (or domain - the cell) */
26	struct iw_freq	freq;		/* frequency or channel :
27					 * 0-1000 = channel
28					 * > 1000 = frequency in Hz */
29
30	struct iw_param	sens;		/* signal level threshold */
31	struct iw_param	bitrate;	/* default bit rate */
32	struct iw_param	txpower;	/* default transmit power */
33	struct iw_param	rts;		/* RTS threshold */
34	struct iw_param	frag;		/* Fragmentation threshold */
35	__u32		mode;		/* Operation mode */
36	struct iw_param	retry;		/* Retry limits & lifetime */
37
38	struct iw_point	encoding;	/* Encoding stuff : tokens */
39	struct iw_param	power;		/* PM duration/timeout */
40	struct iw_quality qual;		/* Quality part of statistics */
41
42	struct sockaddr	ap_addr;	/* Access point address */
43	struct sockaddr	addr;		/* Destination address (hw/mac) */
44
45	struct iw_param	param;		/* Other small parameters */
46	struct iw_point	data;		/* Other large parameters */
47};
48
49/*
50 * The structure to exchange data for ioctl.
51 * This structure is the same as 'struct ifreq', but (re)defined for
52 * convenience...
53 * Do I need to remind you about structure size (32 octets) ?
54 */
55struct iwreq {
56	union
57	{
58		char	ifrn_name[IFNAMSIZ];	/* if name, e.g. "eth0" */
59	} ifr_ifrn;
60
61	/* Data part (defined just above) */
62	union iwreq_data	u;
63};
64
65/* ... */

위 코드에서 ifr_name[] 역할을 하는 것은 ifrn_name[]이다. 그리고 struct iwreq_datamode 멤버를 통해 현재 모드 값을 가져오거나, 모드 값을 설정할 수 있다. 이를 C 코드로 작성하면 다음과 같다[10]:

 1#include <stdio.h>
 2#include <unistd.h>
 3#include <sys/types.h>
 4#include <linux/wireless.h>
 5
 6int iw_set_monitor_mode(const int fd, /* socket file descriptor */
 7                        const char *ifname) /* interface name */
 8{
 9    struct iwreq iwr;
10    
11    if (fd == -1)
12        return -1;
13
14    strncpy(iwr.ifr_ifrn.ifrn_name, ifname, IFNAMSIZ);
15    iwr.u.mode = IW_MODE_MONITOR;
16    if (ioctl(fd, SIOCSIWMODE, &iwr) == -1) {
17        fprintf(stderr, "Setting monitor mode failed..\n");
18	return -1;
19    }
20    return 0;
21}

지금까지 설명한 것을 종합하여 어댑터 모드를 설정하는 순서를 정리하면 다음과 같다:

1. Open socket file
2. Set interface flag to down
3. Set interface wireless mode flag to IW_MODE_MONITOR
4. Set inter face flag to up

위 순서에 대한 완전한 예제 코드는 Wireless tool in C의 prv_ieee80211_set_if_mode()를 참고하라.

# Capturing wireless frames in the air

이렇게 모니터 모드가 구성되었으므로 무선 랜 프레임을 캡처할 수 있다. 그 방법은 바로 read() system call을 호출하는 것이다[16].

 1#include <stdio.h>
 2#include <stdint.h>
 3#include <stddef.h>
 4#include <unistd.h>
 5
 6/*
 7 * admp_hexdump: hexdump function written by ccbrown;
 8 * thanks to ccbrown!!
 9 */
10void ccbrown_hexdump(const void* data, size_t size)
11{
12    char ascii[17];
13    size_t i, j;
14
15    ascii[16] = '\0';
16    for (i = 0; i < size; ++i) {
17	printf("%02X ", ((unsigned char *) data)[i]);
18	if (((unsigned char *) data)[i] >= ' ' 
19	    && ((unsigned char *) data)[i] <= '~') {
20	    ascii[i % 16] = ((unsigned char *) data)[i];
21	} else {
22	    ascii[i % 16] = '.';
23	}
24	if ((i + 1) % 8 == 0 || i + 1 == size) {
25	    printf(" ");
26	    if ((i + 1) % 16 == 0) {
27		printf("|  %s \n", ascii);
28	    } else if (i + 1 == size) {
29		ascii[(i + 1) % 16] = '\0';
30		if ((i + 1) % 16 <= 8) {
31		    printf(" ");
32		}
33		for (j = (i + 1) % 16; j < 16; ++j) {
34		    printf("   ");
35		}
36		printf("|  %s \n", ascii);
37	    }
38	}
39    }
40}
41
42int ieee80211_pcap(int fd) /* socket file descriptor */
43{
44    uint8_t buff[BUFSIZ];
45    ssize_t len;
46    
47    if (fd == -1)
48        return -1;
49    
50    while ((len = read(fd, buff, BUFSIZ)) >= 0)
51        ccbrown_hexdump(buff, len);
52    return 0;
53}

위 코드를 보면 무선 랜 프레임을 캡처하는 것은 매우 간단해보인다. 그러나 문제는 위 코드로 출력되는 값들의 의미를, 아스키 코드로 표현되는 것이 아니라면 전혀 알 수 없다는 것이다. 그래서 무선 랜 프레임을 분석해볼 필요가 있다.

# Wireless Frame Analysis

무선 랜 프레임에는 그 크기가 정해진 필드도 있지만 가변 필드도 존재한다. 그리고 이러한 가변 필드들은 무선 랜 프레임 파싱을 어렵게 만든다. 하지만 가변 필드에도 나름의 원칙이 있기 때문에 이를 기억한다면 파싱하는 것은 그다지 어렵지 않다. 그럼 Beacon frame (무선 랜 AP 또는 와이파이 공유기가 자신의 존재를 알리는 프레임)으로부터 SSID (무선 랜 AP의 이름 또는 와이파이 공유기 이름)를 파싱하는 것을 목표로, 시작해보겠다.

무선 랜 프레임은 일반적으로 radiotap header (de facto standard)와 IEEE 802.11 frame으로 구성된다. 여기서는 IEEE 802.11 frame에서 SSID만 추출할 것이기 때문에 radiotap header가 그다지 비중있게 다루어지지는 않지만, radiotap header는 신호 세기와 같은 유용한 정보들을 담고 있고 이를 파싱하는 것은 예제로 제공할 것이다[13, 14].

Radiotap header를 분석하기에 앞서서, 가변 필드의 구성 원칙을 알아보자. 무선 랜 프레임에서 가변 필드는 TLD 형식으로 구성된다. TLD 형식이란 Type (EID로 표현됨), Length, Data로 필드를 구성하는 형식을 의미한다. 이때 type은 해당 필드가 어떤 정보를 가지고 있는지, length는 해당 필드의 길이, data는 실제 데이터를 의미한다. 그럼 우리는 다음과 같이 가변 필드에 대한 구조체를 정의할 수 있다[12]:

1struct tld {
2    unsigned int type;
3    size_t len;
4    unsigned char data[];
5};

# Radiotap header

Radiotap header는 다음과 같이 구성된다[13]:

             [version][padding][length][present|extended][ ... ]
byte length:    1        1        2       4       4

위 그림에서 present 부분이 의미하는 것은 4 바이트 값인 present에 따라 extended가 될 수도 있고 안될 수도 있다는 것이다. Radiotap header에는 present 값에 따라 추가되는 필드들이 존재한다. 즉, present 값에 따라 시작 주소에서 추출하고자 하는 필드까지의 offset이 달라진다는 것이다[13].

이를 파싱하기 위해서는 present의 각 비트에 따른 길이 값을 테이블로 만들고 ([13]의 https://www.radiotap.org/fields/defined 참고) 인덱스 기반으로 offset을 구해야 한다. 이러한 방식으로 신호 세기 필드까지의 offset을 구하는 C 코드를 작성하면 다음과 같다[10, 13, 14]:

 1#include <stdint.h>
 2#include <stddef.h>
 3
 4struct radiotap_hdr {
 5    uint8_t ver; /* version */
 6    uint8_t pad; /* padding */
 7    uint16_t len; /* entire length of radiotap header */
 8    uint32_t present;
 9    uint8_t variable[]; /* payload */
10} __attribute__((__packed__));
11
12unsigned int radiotap_get_offset(uint64_t present,
13			        unsigned int bit_loc) /* bit location of
14				                         specific field */
15{
16    unsigned int offsettab[] = {
17	sizeof(uint64_t),	/* TSFT */
18	sizeof(uint8_t),	/* Flags */
19	sizeof(uint8_t),	/* Rate */
20	sizeof(uint16_t),	/* Channel */
21	sizeof(uint8_t) + sizeof(uint8_t) /* hop set + hop pattern */
22
23	/* [TODO] Add defined fields */
24    };
25    unsigned int offset;
26    uint64_t mask, i;
27
28    offset = 0;
29    for (i = 0; i < bit_loc; i++) {
30	mask = 1 << i;
31	if (mask & present)
32	    offset += offsettab[i];
33    }
34    return offset;
35}
36
37int get_antsig(void *buff, uint16_t len)
38{
39    unsigned int offset;
40    struct radiotap_hdr *header;
41    
42    header = buff;
43    offset = radiotap_get_offset(header->present, 5);
44    if (header->present & (1 << 31))
45        offset += sizoef(uint32_t);
46    return *(header->variable + offset);
47}

그럼 bit_loc은 어떻게 구할 수 있을까? 이것 또한 [13]의 https://www.radiotap.org/fields/defined 를 참고하여 해당 필드가 몇 번째 비트로 정의되어 있는지 알 수 있다.

이러한 radiotap header가 앞에 존재할 때 IEEE 802.11 frame의 시작 주소를 구하는 방법은 무엇일까? 바로 상기의 struct radiotap_hdrlen 멤버를 캡처된 데이터의 시작주소에 더하는 것이다. 이제 IEEE 802.11 frame을 분석할 차례다.

# IEEE 802.11 Frame Analysis

Generic한 무선 랜 프레임은 다음과 같이 구성된다[15]:

             [Frame  ][Duration][addr1][addr2][addr3][Sequence][addr4][ ... ][FCS]
              Control][ID      ][     ][     ][     ][Control ][     ][     ][   ]
byte length:    2        2         6      6      6      2         6    0-2312  4

Generic한 프레임이라는 것은 프레임의 종류에 따라 헤더의 구성이 달라질 수 있다는 뜻이다. 그리고 프레임의 종류는 위 헤더의 Frame Control 필드의 값에 따라 결정된다. 이 필드는 다시 여러 비트 필드로 나뉘어지는데, 여기서 관심을 가지는 필드는 type과 subtype이다. 전자는 프레임의 종류를 결정하고, 전자로부터 결정된 프레임 종류 내에서의 하위 분류를 나타낸다[15].

      [Protocl][Type ][Subtype ][To DS][From DS][More][Retry][Power][WEP][Rsvd]
      [Version][     ][        ][     ][       ][Flag][     ][Mgmt ][   ][    ]
bit   :  2        2         4       1       1       1     1     1     1    1
length

여기서 추출하고자 하는 정보는 beacon frame에서의 SSID이므로 beacon frame에 초점을 맞출 것이다. 먼저 frame control 필드의 type은 다음과 같은 값을 가질 수 있고,

@ 00: Management frame
@ 01: Control frame
@ 10: Data frame

management frame의 subtype은 다음과 같이 값에 따라 나뉘어진다[15]:

@ 0000: Association request frame
@ 0001: Association response frame
@ 0010: Reassociation request frame
@ 0011: Reassociation response frame
@ 0100: Probe request frame
@ 0101: Probe response frame
@ 1000: Beacon frame
@ 1001: ATIM
@ 1010: Association destroy frame
@ 1011: Authentication frame
@ 1100: Deauthentication frame

위 정보를 이용하여 캡처된 프레임이 management frame인지 확인하는 것을 C 코드로 작성하면 다음과 같다[10]:

 1#include <stdio.h>
 2#include <string.h>
 3#include <stdint.h>
 4#include <stddef.h>
 5#include <unistd.h>
 6#include <sys/types.h>
 7
 8struct admp_ieee80211_frame_control {
 9    uint16_t proto: 2,
10	    type: 2,
11	    subtype: 4,
12	    ds: 2,		/* [tods][fromds] */
13	    moreflag: 1,
14	    retry: 1,
15	    pwrmgmt: 1,
16	    moredata: 1,
17	    wep: 1,
18	    order: 1;
19} __attribute__((__packed__));
20
21struct admp_ieee80211_sequence_control {
22    uint16_t fragnum: 4,
23	    seqnum: 12;
24} __attribute__((__packed__));
25
26/*
27 * This header structure is for generic case. But in IEEE 802.11,
28 * header structure varies by frame categories.
29 */
30struct admp_ieee80211_mac_frame {
31    struct admp_ieee80211_frame_control fc;
32    uint16_t durid;		/* Duration / ID */
33    uint8_t addr1[6];
34    uint8_t addr2[6];
35    uint8_t addr3[6];
36    struct admp_ieee80211_sequence_control seqctl;
37    uint8_t addr4[6];
38    uint8_t variable[];
39} __attribute__((__packed__));
40
41struct admp_ieee80211_mac_trailer {
42    uint32_t fcs;
43} __attribute__((__packed__));
44
45/* ieee80211_parser: parse given buff as a generic frame */
46static int ieee80211_parser(void *buff, /* captured frame */
47	      	            ssize_t len) /* captured frame's length */
48{
49    struct admp_ieee80211_mac_frame *frame;
50
51    if (buff == NULL || ap == NULL || len < 0) {
52        fprintf(stderr, "Invalid arguments..\n");
53	return -1;
54    }
55    
56    frame = buff;
57    switch (frame->fc.type) {
58    case 0b00:
59	/* Management frame */
60	break;
61    case 0b01:
62	/* Control frame */
63	break;
64    case 0b10:
65	/* Data frame */
66	break;
67    default:
68	break;
69    }
70    return 0;
71}

이렇게 수신된 프레임이 management frame으로 확인되면, 헤더의 구성이 달라진다. Management frame, 그 중에서도 beacon frame의 헤더 구성은 다음과 같다[15]:

             [Frame  ][Duration][addr1][addr2][addr3][Sequence][ ... ][FCS]
              Control][ID      ][     ][     ][     ][Control ][     ][   ]
byte length:    2        2         6      6      6      2       0-2312  4


[ ... ]:     [Timestamp][Beacon  ][Capacity   ][  SSID  ][Option field]
             [         ][Interval][Information][        ][            ]
byte length:     8          2          3        variable    variable
                                                length      length
             <-----------------Required-----------------><--Optional-->

위 헤더의 가변 길이로 표시된 SSID와 Option field가 바로 가변 필드에 해당하는 부분이며, TLD 형식을 따른다. 여기서 상기에 설명한 TLD 구성 원칙을 기억하면서 beacon frame을 파싱하는 코드를 작성하면 다음과 같다[10, 12, 15]:

 1#include <stdio.h>
 2#include <string.h>
 3#include <stdint.h>
 4#include <stddef.h>
 5#include <unistd.h>
 6#include <sys/types.h>
 7
 8#define IS_X_IN_RANGE(min, x, max) (((x) >= (min)) && (x) < (max))
 9
10struct admp_ieee80211_mgmt_frame {
11    /* header */
12    struct admp_ieee80211_frame_control fc;
13    uint16_t durid;
14    uint8_t addr1[6];
15    uint8_t addr2[6];
16    uint8_t addr3[6];
17    struct admp_ieee80211_sequence_control seqctl;
18    uint8_t variable[];
19} __attribute__((__packed__));
20
21struct admp_ieee80211_beacon {
22    uint64_t tmstamp;		/* Timestamp */
23    uint16_t interval;		/* Beacon interval */
24    uint16_t capinfo;		/* Capability information */
25    uint8_t variable[];		/* Information Elements */
26} __attribute__((__packed__));
27
28struct admp_ieee80211_tlv {
29    uint8_t type;		/* EID */
30    uint8_t len;		/* Length */
31    uint8_t value[];		/* Data */
32} __attribute__((__packed__));
33
34/* ieee80211_beacon_parser: parse given frame as a beacon frame */
35static int
36ieee80211_beacon_parser(struct admp_ieee80211_mgmt_frame *frame,
37			    ssize_t len)
38{
39    unsigned int done;
40    struct admp_ieee80211_beacon *beacon;
41    struct admp_ieee80211_tlv *ie;
42
43    if (frame == NULL || ap == NULL || len < 0) {
44        fprintf(stderr, "Invalide argument..\n");
45	return -1;
46    }
47
48    beacon = (struct admp_ieee80211_beacon *) frame->variable;
49    ie = (struct admp_ieee80211_tlv *) beacon->variable;
50    done = 0;
51    while (len > 0 && done == 0) {
52	if (!IS_X_IN_RANGE(0, ie->len, len))
53	    break;
54	
55	switch (ie->type) {
56	case 0x00:
57	    /* SSID */
58	    strncpy(ap->ssid, (char *) ie->value, ie->len);
59	    break;
60	default:
61	    done = 1;
62	    break;
63	}
64	ie = (struct admp_ieee80211_tlv *) ((uint8_t *) ie + ie->len);
65	len -= ie->len;
66    }
67    return 0;
68}
69
70/* ieee80211_mgmt_parser: parse given buff as a management frame */
71static int ieee80211_mgmt_parser(void *buff, /* captured frame */
72					    ssize_t len) /* captured frame's length */
73{
74    struct admp_ieee80211_mgmt_frame *frame;
75
76    if (buff == NULL || ap == NULL || len < 0) {
77        fprintf(stderr, "Invalid arguments..\n");
78	return -1;
79    }
80    
81    frame = buff;
82
83    /* printf("%d\n", frame->fc.subtype); */
84    switch (frame->fc.subtype) {
85    case 0b0000:
86	/* Association request frame */
87	break;
88    case 0b1000:
89	/* Beacon frame */
90	ieee80211_beacon_parser(frame, len);
91	break;
92    default:
93	break;
94    }
95    return 0;
96}

# Conclusion

지금까지 가능한 linux kernel이 제공하는 system call과 C 언어의 표준 라이브러리가 제공하는 기능만을 사용하여 무선 랜 프레임을 캡처하는 프로그램을 작성하기 위한 지식을 알아보았다. 이는 무선 랜 인터페이스를 모니터 모드로 바꾸는 것으로 시작하여 무선 랜 프레임 중 beacon frame을 파싱하는 코드를 작성하는 것으로 끝을 맺었다. 비록 코드 조각으로 예제를 제공하였지만, 완전한 예제 코드를 이해하는 데에는 무리가 없을 것이라고 생각한다.

물론, 여기서 제공한 예제들은 (심지어 완전한 예제 코드라고 지칭한 것도) 한정적인 목적에만 부합하도록 작성되어 있다. 따라서 이들이 완전한 파서로 기능하기 위해서는 다른 radiotap header 필드, 다른 프레임들, 다른 TLD 형식 값들도 고려되어야 할 것이다.

# Appendix

# How wireless ioctls work inside the kernel?

리눅스 커널은 직접적으로 내부에 wireless ioctls를 구현하지 않는다. 즉, FreeBSD에서의 ieee80211_ioctl()과 같은 함수가 없다. 다만, 리눅스 커널은 이를 extensions의 형태로 구현한다. 이는 wireless extensions (wext)로 표현되고, 이에 대한 예시로는 Realtek wireless driver의 _rtl92e_wx_get_gen_ie()가 있다. 이 함수의 콜트레이스를 살펴보면 다음과 같다[10]:

ioctl() -> ... -> iw_handler_table() -> _rtl92e_wx_get_gen_ie()

그럼 위 콜트레이스에서 iw_handler_table()은 어떻게 _rtl92e_wx_get_gen_ie()를 호출할 수 있었을까?

상기에 무선 랜 인터페이스의 모드가 정의된 헤더 파일이 include/uapi/linux/wireless.h라고 설명하였다. 하지만 이 헤더 파일에는 무선 랜 인터페이스를 제어하기 위한 거의 모든 ioctls가 포함되어 있다. 이 헤더 파일에 정의된 ioctls의 일부를 보면 다음과 같다[10]:

 1/* ... */
 2
 3#define SIOCSIWSCAN	0x8B18		/* trigger scanning (list cells) */
 4#define SIOCGIWSCAN	0x8B19		/* get scanning results */
 5
 6/* ... */
 7
 8/* Encoding stuff (scrambling, hardware security, WEP...) */
 9#define SIOCSIWENCODE	0x8B2A		/* set encoding token & mode */
10#define SIOCGIWENCODE	0x8B2B		/* get encoding token & mode */
11
12/* ... */
13
14/* WPA : Generic IEEE 802.11 informatiom element (e.g., for WPA/RSN/WMM).
15 * This ioctl uses struct iw_point and data buffer that includes IE id and len
16 * fields. More than one IE may be included in the request. Setting the generic
17 * IE to empty buffer (len=0) removes the generic IE from the driver. Drivers
18 * are allowed to generate their own WPA/RSN IEs, but in these cases, drivers
19 * are required to report the used IE as a wireless event, e.g., when
20 * associating with an AP. */
21#define SIOCSIWGENIE	0x8B30		/* set generic IE */
22#define SIOCGIWGENIE	0x8B31		/* get generic IE */
23
24/* WPA : IEEE 802.11 MLME requests */
25#define SIOCSIWMLME	0x8B16		/* request MLME operation; uses
26					 * struct iw_mlme */
27/* WPA : Authentication mode parameters */
28#define SIOCSIWAUTH	0x8B32		/* set authentication mode params */
29#define SIOCGIWAUTH	0x8B33		/* get authentication mode params */
30
31/* ... */

위 헤더에 정의된 ioctls는 iw_handler에 의해 wireless driver 함수들과 연결된다. 이는 다음 코드로 확인할 수 있다[10]:

 1/* code path: /drivers/staging/rtl8192e/rtl8192e/rtl_wx.c */
 2#define IW_IOCTL(x) ((x) - SIOCSIWCOMMIT)
 3static iw_handler r8192_wx_handlers[] = {
 4	...
 5	[IW_IOCTL(SIOCSIWSCAN)] = _rtl92e_wx_set_scan,
 6	[IW_IOCTL(SIOCGIWSCAN)] = _rtl92e_wx_get_scan, 
 7	...
 8	[IW_IOCTL(SIOCSIWENCODE)] = _rtl92e_wx_set_enc,
 9	[IW_IOCTL(SIOCGIWENCODE)] = _rtl92e_wx_get_enc,
10	...
11	[IW_IOCTL(SIOCSIWGENIE)] = _rtl92e_wx_set_gen_ie,
12	[IW_IOCTL(SIOCGIWGENIE)] = _rtl92e_wx_get_gen_ie,
13	[IW_IOCTL(SIOCSIWMLME)] = _rtl92e_wx_set_mlme,
14	[IW_IOCTL(SIOCSIWAUTH)] = _rtl92e_wx_set_auth,
15	[IW_IOCTL(SIOCSIWENCODEEXT)] = _rtl92e_wx_set_encode_ext,
16};

정리하면, 무선 랜과 관련하여 정의된 ioctls는 커널 내부적으로 iw_handler를 통해 wireless driver 함수들과 매핑되어 있다. 따라서 상기에서와 같이 정의된 ioctls를 인자로 하여 ioctl()가 호출된다면, 사전에 매핑된 wireless driver 함수가 호출될 것이라고 볼 수 있다.

# References

  1. He-Jun Lu and Yang Yu, "Research on WiFi Penetration Testing with Kali Linux," Hindawi, vol. 2021, Article ID 5570001, 2021.
  2. Vladimir Leiv, "What is the difference between Promiscuous and Monitor mode in Wireless Networks?," 2013. [Online]. Available: https://security.stackexchange.com/questions/36997/what-is-the-difference-between-promiscuous-and-monitor-mode-in-wireless-networks, [Accessed Feb. 06, 2024].
  3. "ipTIME N150UA2", [Online]. Available: https://iptime.com/iptime/?page_id=11&pf=9&page=&pt=499&pd=1, [Accessed Feb. 06, 2024].
  4. "ipTIME A2000UA-4dBi", [Online]. Available: https://iptime.com/iptime/?page_id=11&pf=8&page=&pt=248&pd=1, [Accessed Feb. 06 2024].
  5. "Ubuntu 22.04.3 LTS (Jammy Jellyfish)," 2022. [Online]. Available: https://releases.ubuntu.com/jammy/, [Accessed Feb. 06, 2024].
  6. "dmesg(1) -- Linux manual page," 2023. [Online]. Available: https://man7.org/linux/man-pages/man1/dmesg.1.html, [Accessed Feb. 06, 2024].
  7. "ioctl(2) -- Linux manual page," [Online]. Available: https://man7.org/linux/man-pages/man2/ioctl.2.html, [Accessed Feb. 06, 2024].
  8. Jean Tourrilhes, "Wireless Extensions for Linux," 1997. [Online]. Available: https://hewlettpackard.github.io/wireless-tools/Linux.Wireless.Extensions.html, [Accessed Feb. 07, 2024].
  9. "netdevice(7) -- Linux manual page," [Online]. Available: https://man7.org/linux/man-pages/man7/netdevice.7.html, [Accessed Feb. 07, 2024].
  10. torvalds, "Linux kernel," 2024. [Online]. Available: https://github.com/torvalds/linux, [Accessed Feb. 07, 2024].
  11. "통신비밀보호법", 법무부(공공형사과) 및 과학기술정보통신부(통신자원정책과), 2022. [Online]. Available: https://www.law.go.kr/%EB%B2%95%EB%A0%B9/%ED%86%B5%EC%8B%A0%EB%B9%84%EB%B0%80%EB%B3%B4%ED%98%B8%EB%B2%95, [Accessed Feb. 07, 2024].
  12. Nico Waisman, "Anatomy of a Coffee Bean (Wireless Vulnerabilities in Linux Kernel)," 2019. [Online]. Available: https://securitylab.github.com/research/anatomy-of-a-coffee-bean-wireless-vulnerabilities-in-linux-kernel/, [Accessed Feb. 08, 20224].
  13. "Radiotap," [Online]. Available: https://www.radiotap.org/, [Accessed Feb. 08, 2024].
  14. 2N(nms200299), "[802.11] RadioTab Header(헤더) 분석", 2021. [Online]. Available: https://blog.naver.com/nms200299/222267279476, [Accessed Feb. 08, 2024].
  15. "802.11 MAC Frame, WLAN MAC Frame 802.11 MAC 프레임", 정보통신기술용어해설. [Online]. Available: http://www.ktword.co.kr/test/view/view.php?m_temp1=3352, [Accessed Feb. 07, 2024].
  16. ccbrown, "Compact C Hex Dump Function w/ASCII," [Online]. Available: https://gist.github.com/ccbrown/9722406, [Accessed Feb. 08, 2024].