Overview
Raw Socket에 대한 특성과 Linux 환경에서 Packet을 변조해서 Raw Socket으로 전송하는 방법에 대해서 기술한다.
Raw Socket 이란
- Raw Socket은 특정 프로토콜용의 전송 계층 Formatting 없이 직접적으로 인터넷 프로토콜 패킷을 주고 받게 해주는 Socket이다.
- 사용자가 직접 헤더와 데이터를 조작하여 패킷을 전송해야 하기 때문에 각 헤더에 어떤 값을 입력해야 하는지 사전 정보가 필요하다.
- 일반 Socket은 OS 커널이 TCP/UDP/IP 헤더 정보를 추가하여 NIC에 전송한다.
예시 UDP 패킷의 구조
UDP 패킷의 경우에는 Ethernet header + IP header + UDP hedader + Payload 의 구조가 합쳐진 구조를 가지게 된다.
아래는 Wireshark로 UDP 패킷을 살펴보았을때의 이미지로 Raw Socket을 송신 시 해당 값들을 직접 다 채울 수 있어야 한다.



Raw Socket의 용도
- Network 패킷 탐지 프로그램의 테스트
- 의도적으로 패킷을 변조하여 전송시 탐지 프로그램에서 정확하게 탐지를 하는지 테스트 용도로 사용됨
- Network 프로토콜 구현
- 새로운 프로토콜을 테스트하거나 TCP/UDP 외 통신을 테스트하는 용도로 사용됨(DDS나 Someip 패킷을 직접 작성하여 전송이 가능)
UDP 패킷을 Raw Socket으로 전송하는 예제
사전 준비
- 현재 네트워크 환경에서 정상적으로 주고받는 UDP 패킷을 캡쳐하여 이 패킷의 내용을 바탕으로 SourceIP를 변조하여 다시 전송하는 Raw Socket을 생성한다.
- UDP 패킷은 위 Wireshark에서 확인한 예제 패킷 데이터를 사용한다.
00 00 00 00 00 00 00 00 00 00 00 00 08 00 45 00
00 2e 7c bc 40 00 40 11 c0 00 7f 00 00 01 7f 00
00 01 e3 82 1f 90 00 1a fe 2d 48 65 6c 6c 6f 2c
20 55 44 50 20 53 65 72 76 65 72 21
예제 소스
#include <iostream>
#include <string>
#include <cstdint>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/ethernet.h>
#include <netinet/ip.h>
#include <netinet/udp.h>
#include <netpacket/packet.h>
#include <net/if.h>
typedef struct
{
uint8_t destinationMac[6];
uint8_t sourceMac[6];
uint16_t etherType;
}__attribute__((packed)) EtherHeader;
typedef struct {
uint8_t headerLenAndIpVersion;
uint8_t typeOfService;
uint16_t totalLength;
uint16_t ipId;
uint16_t fragmentOffset;
uint8_t timeToLive;
uint8_t protocol;
uint16_t headerChecksum;
uint32_t ipSrc;
uint32_t ipDst;
}__attribute__((packed)) IPv4Header;
typedef struct
{
uint16_t portSrc;
uint16_t portDst;
uint16_t length;
uint16_t headerChecksum;
}__attribute__((packed)) UdpHeader;
uint8_t udp_packet[1024] = {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08, 0x00, 0x45, 0x00,
0x00, 0x2e, 0x7c, 0xbc, 0x40, 0x00, 0x40, 0x11, 0xc0, 0x00, 0x7f, 0x00, 0x00, 0x01, 0x7f, 0x00,
0x00, 0x01, 0xe3, 0x82, 0x1f, 0x90, 0x00, 0x1a, 0xfe, 0x2d, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x2c,
0x20, 0x55, 0x44, 0x50, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x21
};
// function created by Copilot
// IP 헤더의 체크섬은 모든 16비트 워드를 더한 후 1의 보수를 계산한다.
uint16_t calculate_ip_checksum(const void* vdata, size_t length) {
const uint8_t* data = reinterpret_cast<const uint8_t*>(vdata);
uint32_t acc = 0;
for (size_t i = 0; i + 1 < length; i += 2)
{
uint16_t word;
memcpy(&word, data + i, 2);
acc += ntohs(word);
}
if (length & 1)
{
acc += data[length - 1] << 8;
}
while (acc >> 16)
{
acc = (acc & 0xFFFF) + (acc >> 16);
}
return htons(~acc);
}
int main(int argc, char* argv[]) {
// Raw 소켓 생성
int sock = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ALL));
if (sock < 0) {
perror("socket");
return 1;
}
else
{
std::cout << "Socket created successfully" << std::endl;
}
// 링크 계층(Layer2) 패킷을 다루기 위해 소켓을 바인딩
struct sockaddr_ll sll;
memset(&sll, 0, sizeof(sll));
sll.sll_family = AF_PACKET;
sll.sll_protocol = htons(ETH_P_ALL);
// "lo" 인터페이스를 사용, 다른 인터페이스 사용 시 자신의 환경의 인터페이스 명이 어떻게 되어있는지 확인 후 변경(ex: eth0)
sll.sll_ifindex = if_nametoindex("lo");
if (bind(sock, (struct sockaddr*)&sll, sizeof(sll)) < 0)
{
perror("bind");
close(sock);
return 1;
}
// 원본 패킷에 각 헤더의 시작 주소를 설정하여 값을 수정 가능하도록 구현
EtherHeader* etherHeader = reinterpret_cast<EtherHeader*>(udp_packet);
IPv4Header* ipHeader = reinterpret_cast<IPv4Header*>(udp_packet + sizeof(EtherHeader));
UdpHeader* udpHeader = reinterpret_cast<UdpHeader*>(udp_packet + sizeof(EtherHeader) + sizeof(IPv4Header));
// IP 헤더의 source ip를 변경
ipHeader->ipSrc = inet_addr("127.0.0.2");
// IP 헤더의 체크섬 계산을 다시 해줘야 한다.
ipHeader->headerChecksum = 0; // 체크섬 계산을 다시 하기위해 기존 체크섬 값은 0으로 초기화
ipHeader->headerChecksum = calculate_ip_checksum(ipHeader, sizeof(IPv4Header));
// send할 패킷의 길이를 구하기 위해 IP 헤더의 totalLength에 Ethernet 헤더의 총 크기를 더한다.
uint16_t sendPacketLength = ntohs(ipHeader->totalLength) + sizeof(EtherHeader);
// 패킷 전송
if (sendto(sock, udp_packet, sendPacketLength, 0, (struct sockaddr*)&sll, sizeof(sll)) < 0)
{
perror("sendto");
close(sock);
return 1;
}
else
{
std::cout << "Packet sent successfully" << std::endl;
}
close(sock);
return 0;
}
빌드
g++ -o test main.cc
실행
관리자 권한이 필수이다.
sudo ./test
패킷 확인

원본 패킷과 비교했을때 Source Address 정보가 변경된 것을 확인 가능하고 덤으로 Header Checksum 또한 변경된 것을 확인 가능하다.
보통 UDP 통신을 할 때 목적지(Destination)의 주소와 포트정보를 설정하여 전송을 하게 되는데 Raw Socket을 통해서는 출발지(Source) 주소를 변경해서 패킷 전송이 가능한 것을 확인 가능하다.
'Network' 카테고리의 다른 글
| CAN(Controller Area Network) 통신 개요 (0) | 2025.09.27 |
|---|---|
| SOME/IP Protocol (0) | 2025.09.25 |
| Snort (5) | 2025.07.26 |
| pcap(Packet Capture, 패킷 캡처) (1) | 2025.07.18 |
| Netfilter (7) | 2025.07.16 |