시스템으로 들어오는 패킷을 캡쳐하는 방식은 크게 세가지가 있다.
여기서는 유저레벨에서 할 수 있는 raw socket을 이용하여 패킷을 스니핑 해보겠다.
raw socket에 대한 개념은 따로 언급하지 않고 사용 방법에 대해서만 정리해보겠다.
먼저 네트워크 계층 까지만 다룰 수 있는 raw socket과 데이터 링크 계층까지 전부 다룰 수 있는 raw socket
이렇게 두가지 가 있다는 것을 이해하고 아래 코드를 살펴보겠다.
#include <stdio.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "pktfilter.c" //패킷을 뜯어서 터미널에 출력하는 함수가 정의된 파일.
int main(void)
{
int sock;
unsigned char buf[65536];
//domain, type, protocol
sock = socket(PF_INET, SOCK_RAW, IPPROTO_TCP);
if (sock < 0)
{
perror("socket error");
return -1;
}
while (1)
{
int rcv_bytes = recvfrom(sock, buf, sizeof(buf), 0, NULL, NULL);
if (rcv_bytes < 0)
break;
ip_handle((struct iphdr *)buf); //ip헤더부터 확인하면 된다.
}
close(sock);
return 0;
}
socket()을 호출 할 때,
domain은 Ipv4 인터넷 프로토콜로,
type은 SOCK_RAW로, raw network 프로토콜 접근을 제공한다.
protocoo은 TCP로 TCP만 스니핑 하겠다는 것이다.
또한 위 코드를 컴파일 한 후 관리자 권한으로 실행해야 소켓을 온전히 열 수 있다.
#include <stdio.h>
#include <sys/socket.h>
#include <linux/if_packet.h>
#include <net/ethernet.h>
#include <arpa/inet.h>
#include <unistd.h>
#include "pktfilter.c" //패킷을 뜯어서 터미널에 출력하는 함수가 정의된 파일.
int main(void)
{
int sock;
unsigned char buf[65536];
sock = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sock < 0)
{
perror("socket error");
return -1;
}
while (1)
{
int type;
int rcv_bytes = recvfrom(sock, buf, sizeof(buf), 0, NULL, NULL);
if (rcv_bytes < 0)
break;
eth_handle((struct ethhdr *)buf); //이더넷 헤더부터 확인해야 한다.
}
close(sock);
return 0;
}
위 코드와 앞선 코드의 차이점은 단 하나 소켓을 열 때 이다.
domain은 PF_PACKET으로 저수준의 패킷 인터페이스를 제공한다. 더 자세한 내용은 man packet을 참조하면 된다.
type은 똑같고,
protocol은 모든 것에 대해서이며, 여기서는 endian을 한 차례 변경해주어야 한다. 이는 이더넷 헤더에서 프로토콜 관련 필드가 2Bytes이기 때문이다.
끝으로 pktfilter.c 에 구현에 대한 내용이다.
#include <stdio.h>
#include <arpa/inet.h>
#include <linux/tcp.h>
#include <linux/udp.h>
#include <linux/ip.h>
#include <linux/if_ether.h>
#include <linux/if_arp.h>
struct ether_addr;
extern char * ether_ntoa(struct ether_addr *);
static void tcp_handle(struct tcphdr * tcp)
{
printf("-----TCP-----\n");
printf("%d -> %d\n", ntohs(tcp->source), ntohs(tcp->dest));
}
static void udp_handle(struct udphdr * udp)
{
printf("-----UDP-----\n");
printf("%d -> %d\n", ntohs(udp->source), ntohs(udp->dest));
printf("data length: %d\n", ntohs(udp->len));
}
static void ip_handle(struct iphdr * ip)
{
printf("-----NETWORK------\n");
printf("%s -> ", inet_ntoa(*(struct in_addr *)&ip->saddr));
printf("%s\n", inet_ntoa(*(struct in_addr *)&ip->daddr));
printf("total length: %d\n", ntohs(ip->tot_len));
switch (ip->protocol)
{
case IPPROTO_TCP:
tcp_handle((struct tcphdr *)(ip + 1));
break;
case IPPROTO_UDP:
udp_handle((struct udphdr *)(ip + 1));
break;
//more protocol...
}
}
static void arp_handle(struct arphdr * arp)
{
unsigned char * ptr = (unsigned char *)(arp + 1);
printf("-----ARP-----\n");
printf("%s\n", ntohs(arp->ar_op) == ARPOP_REQUEST ? "Request" : "Reply");
printf("shw: %s, sip: %s\n", ether_ntoa((struct ether_addr *)(ptr)),
inet_ntoa(*(struct in_addr *)(ptr + 6)));
printf("thw: %s, tip: %s\n", ether_ntoa((struct ether_addr *)(ptr + 6 + 4)),
inet_ntoa(*(struct in_addr *)(ptr + 6 + 4 + 6)));
}
static void eth_handle(struct ethhdr * eth)
{
printf("-----DATA-LINK-----\n");
printf("%s -> ", ether_ntoa((struct ether_addr *)eth->h_source));
printf("%s\n", ether_ntoa((struct ether_addr *)eth->h_dest));
switch (ntohs(eth->h_proto))
{
case ETH_P_ARP:
arp_handle((struct arphdr *)(eth + 1));
break;
case ETH_P_IP:
ip_handle((struct iphdr *)(eth + 1));
break;
//more protocol...
}
}
ether_ntoa()를 사용하려면 보통은 netinet/ether.h를 포함하면 되지만, 포함 했 을 시
패킷 헤더에 대한 정의를 담고 있는 헤더파일과 구조체 재정의로 인한 충돌로 인해 컴파일 에러가 발생해서 필요한 부분만 따로 작성하였다.
또 arp_handle()에서 arp op는 여러가지가 있지만 대표적으로 자주 사용되는 것이 주로 request, reply여서 간단하게 작성하였다.
'리눅스 커널 > 네트워크' 카테고리의 다른 글
Kernel hooking packet capture (0) | 2021.10.24 |
---|---|
Raw socket outgoing packet capture (0) | 2021.10.24 |
Ethernet & ARP Header (0) | 2021.10.24 |
IP & ICMP Header (0) | 2021.10.24 |
TCP & UDP Header (0) | 2021.10.24 |