이더넷 프레임 구조 분석 예제


이더넷 프레임 구조 분석 예제
Ethernet Frame 헤더 정보가 저장되는 구조체 포맷에 대해 설명 드리면서 예제 소스코드를 이용하여 어떻게 헤더 내용을 출력 할 수 있는지 확인해 보도록 하겠습니다. 이더넷 링크에서의 데이터 패킷은 이더넷 패킷으로 불리며, 페이로드로 이더넷 프레임을 전송하게 됩니다.

이더넷 프레임 구조

이더넷 프레임 구조

헤더 구조체

이더넷 프레임의 헤더구조는 간단합니다. 목적지와 출발지 MAC 주소와 이더넷 타입(type) 으로 구성됩니다.
MAC 주소의 크기는 목적지와 출발지가 각각 6Byte 이고 이더넷 타입의 크기는 2Byte크기를 갖습니다. 이더넷 타입은 목적에 따라 값이 달라지게 되는데 1500 이하의 값은 페이로드의 크기를 나타냅니다. 하지만 1536 값과 그 이상의 값은 이더넷 프레임이 운반하는 페이로드 데이터의 프로토콜을 나타냅니다.

아래의 이미지는 가장 많이 사용되는 이더넷2(Ethernet II) 프레임의 헤더 구조를 나타냅니다. 이더넷 프레임 헤더의 전체 크기는 14Byte 입니다.

목적지 MAC주소(6 Byte)출발지 MAC주소(6 Byte)이더넷 유형(2 Byte)
14 Byte

위의 내용을 구조체로 나나내면 다음과 같고, /usr/include/net/ethernet.h 파일에 구조체가 정의되어 있습니다.

/* 10Mb/s ethernet header */
struct ether_header
{
  u_int8_t  ether_dhost[ETH_ALEN];	/* destination eth addr	*/
  u_int8_t  ether_shost[ETH_ALEN];	/* source ether addr	*/
  u_int16_t ether_type;		        /* packet type ID field	*/
} __attribute__ ((__packed__));

/* Ethernet protocol ID's */
#define	ETHERTYPE_PUP		0x0200          /* Xerox PUP */
#define ETHERTYPE_SPRITE	0x0500		/* Sprite */
#define	ETHERTYPE_IP		0x0800		/* IP */
#define	ETHERTYPE_ARP		0x0806		/* Address resolution */
#define	ETHERTYPE_REVARP	0x8035		/* Reverse ARP */
#define ETHERTYPE_AT		0x809B		/* AppleTalk protocol */
#define ETHERTYPE_AARP		0x80F3		/* AppleTalk ARP */
#define	ETHERTYPE_VLAN		0x8100		/* IEEE 802.1Q VLAN tagging */
#define ETHERTYPE_IPX		0x8137		/* IPX */
#define	ETHERTYPE_IPV6		0x86dd		/* IP protocol version 6 */
#define ETHERTYPE_LOOPBACK	0x9000		/* used to test interfaces */

소스코드 빌드시 컴파일러는 실제 구조체에서 할당한 크기가 아닌, 실행 속도를 높이기 위해서 패딩을 더해서 구조체 크기가 할당되는 경우가 있습니다. 구조체 크기 만큼 사용 될 수 있도록 __attribute__ ((__packed__)) 라는 내용이 추가되었습니다. 그 외의 내용들은 어렵지 않게 이해하실 수 있으실 겁니다.

실행 예제

위의 구조체를 이용하여 실제 환경에서 테스트 해보도록 하겠습니다. 전체 예제 소스코드는 아래의 git 명령어로 다운 받으 실 수 있습니다. 소스코드를 빌드 할 수 있는 Makefile 과 소스코드, 헤더 파일등을 포함하고 있습니다.

$ git clone https://hiseon.me/reps/libpcap-tutorial.git

그리고 패킷을 스니핑 하는 예제는 아래의 내용을 참고 하실 수 있습니다.

libpcap 예제 기본 사용법

작성해 볼 함수는 패킷이 수신되었을 때, 헤더 구조를 출력하는 예제입니다. 소스코드는 src/ethernet.h 와 src/ethernet.c 파일 안에 구현되었습니다.

#include <sys/types.h>

void dump_ethernet_header(const u_char *pkt_data);
#include <stdio.h>
#include <arpa/inet.h>
#include <net/ethernet.h>

#include "src/ethernet.h"

void dump_ethernet_header(const u_char *pkt_data) {
  struct ether_header *header = (struct ether_header *)pkt_data;

  const char *name = NULL;

  u_int8_t *dmac = header->ether_dhost;
  u_int8_t *smac = header->ether_shost;
  u_int16_t type = ntohs(header->ether_type);

  switch (type) {
    case ETHERTYPE_IP:
      name = "IP";
      break;
    case ETHERTYPE_ARP:
      name = "ARP";
      break;
    default:
      name = "Unknwon";
      break;
  }

  printf("%02x:%02x:%02x:%02x:%02x:%02x => " \
      "%02x:%02x:%02x:%02x:%02x:%02x (%s) \n",
  smac[0], smac[1], smac[2], smac[3], smac[4], smac[5],
  dmac[0], dmac[1], dmac[2], dmac[3], dmac[4], dmac[5], name);
}

수신된 패킷 데이터를 struct ether_header 구조체로 변환하여 사용하고 있습니다.
변환시 중요한 점이 있는데 ntohs 라는 함수를 사용하여 네트워크 바이트 오더를 호스트 바이트오더로 변환하는 것입니다.

위의 내용은 단순히 IP와 ARP 페이로드의 헤더만 출력하는 내용입니다.

실행 결과

위해서 구현된 dump_ethernet_header 함수는 패킷이 수신되었을 경우, 호출될 수 있도록 구현되었습니다.
보다 자세한 내용은 실행 예제 소스코드 중 src/sniffing.c 파일의 내용을 참고해 주시기 바랍니다.

1 :  eth0
2 :  ppp0
3 :  Pseudo-device that captures on all interfaces
4 :  lo
5 :  docker0
6 :  Linux netfilter log (NFLOG) interface
7 :  Linux netfilter queue (NFQUEUE) interface
8 :  USB bus number 1
number : 1
00:d0:cb:7f:f4:4d => 00:16:3e:cc:52:b4 (IP) 
00:d0:cb:7f:f4:4d => 00:16:3e:cc:52:b4 (IP) 
00:16:3e:cc:52:b4 => 00:d0:cb:7f:f4:4d (IP) 
00:d0:cb:7f:f4:4d => 00:16:3e:cc:52:b4 (IP) 
00:d0:cb:7f:f4:4d => 00:16:3e:cc:52:b4 (IP) 
00:16:3e:cc:52:b4 => 00:d0:cb:7f:f4:4d (IP) 
00:d0:cb:7f:f4:4d => ff:ff:ff:ff:ff:ff (ARP)

( 본문 인용시 출처를 밝혀 주시면 감사하겠습니다.)