L2 Wireguard
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
Wireguard 소개
wireguard는 VPN 프로토콜 중 하나로 필수적인 기능만 구현되어있어 기존 ipsec 등의 vpn보다 훨씬 단순하고 직관적이다. wireguard protocol 메세지 유형은 다음 4가지 뿐이다. (출처: https://www.wireguard.com/papers/wireguard.pdf)
일반적인 흐름을 아주 단순화하면 Initiation -> Response -> Transport Data 다.
공격 방어가 필요한 경우 Cookie Reply 가 개입할 수 있는 구조다.
테스트 환경 구성
=== office-gw ===
Arch: x86_64
OS: Debian 13 (trixie)
Kernel: 6.12.107+deb13-amd64
WG tool: v1.0.20210914
=== home-gw ===
Arch: aarch64
OS: Debian 12 (bookworm)
Kernel: 6.12.95+deb12-arm64
WG tool: v1.0.20210914
=== raspberrypi ===
Arch: aarch64
OS: Raspbian 12 (bookworm)
Kernel: 6.18.33-v8+
WG tool: v1.0.20210914
=== fedora ===
Arch: x86_64
OS: Fedora 43 (Workstation Edition)
Kernel: 6.17.12-300.fc43.x86_64
WG tool: v1.0.20260223
L3 Wireguard 동작 확인
// office-gw
root@office-gw:/home/suunj# ping -c 3 10.1.1.1
PING 10.1.1.1 (10.1.1.1) 56(84) bytes of data.
64 bytes from 10.1.1.1: icmp_seq=1 ttl=64 time=0.971 ms
64 bytes from 10.1.1.1: icmp_seq=2 ttl=64 time=1.18 ms
64 bytes from 10.1.1.1: icmp_seq=3 ttl=64 time=1.23 ms
--- 10.1.1.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 0.971/1.124/1.225/0.110 ms
// home-gw
root@home-gw:/home/debian# ping -c 3 10.1.1.2
PING 10.1.1.2 (10.1.1.2) 56(84) bytes of data.
64 bytes from 10.1.1.2: icmp_seq=1 ttl=64 time=4.65 ms
64 bytes from 10.1.1.2: icmp_seq=2 ttl=64 time=1.36 ms
64 bytes from 10.1.1.2: icmp_seq=3 ttl=64 time=1.21 ms
--- 10.1.1.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 1.212/2.404/4.646/1.586 ms
// raspberry pi
root@raspberrypi:/home/jc3wrld999# ping -c 3 192.168.1.2
PING 192.168.1.2 (192.168.1.2) 56(84) bytes of data.
64 bytes from 192.168.1.2: icmp_seq=1 ttl=62 time=2.03 ms
64 bytes from 192.168.1.2: icmp_seq=2 ttl=62 time=1.78 ms
64 bytes from 192.168.1.2: icmp_seq=3 ttl=62 time=2.11 ms
--- 192.168.1.2 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2003ms
rtt min/avg/max/mdev = 1.780/1.975/2.113/0.141 ms
// 노트북
root@fedora:/home/suunj# ping -c 3 192.168.0.2
PING 192.168.0.2 (192.168.0.2) 자료의 56(84) 바이트.
64 바이트 (192.168.0.2에서): icmp_seq=1 ttl=62 시간=1.84 ms
64 바이트 (192.168.0.2에서): icmp_seq=2 ttl=62 시간=1.87 ms
64 바이트 (192.168.0.2에서): icmp_seq=3 ttl=62 시간=1.73 ms
--- 192.168.0.2 ping 통계 ---
3 패킷이 전송됨, 3 수신됨, 0% packet loss, time 2004ms
rtt min/avg/max/mdev = 1.730/1.813/1.872/0.060 ms
Ethernet frame, IP Packet 비교
기존 Wireguard는 L3 네트워크 인터페이스 이므로 송신 시 기본적으로 IP Packet을 전달받는다.
따라서 코드 곳곳에서 wg0으로 들어오는 데이터는 ipv4 또는 ipv6 packet이다는 전제를 가지고 있다.
이걸 Ethernet frame 전체를 전달하는 것으로 수정하기 위해선 해당 부분을 수정할 필요가 있다.
1) net_device 수정
기존 wireguard 의 net_device는 IP packet을 처리하는 L3 인터페이스로 구성되어있다.
Ethernet frame 전체를 전달하기 위해서는 먼저 wireguard 인터페이스가 Ethernet device로 동작하도록 net_device 의 설정을 변경해야한다.
기존 wireguard는 device를 생성하면서 struct net_device *dev를 준비한 뒤 wireguard에 맞게 여러 필드를 직접 설정한다.
ip link add dev wg0 type wireguard
wireguard 의 rtnl_link_ops 에는 다음과 같이 wg_set()이 setup callback으로 등록되어있다.
static struct rtnl_link_ops link_ops __read_mostly = {
.kind = KBUILD_MODNAME,
.priv_size = sizeof(struct wg_device),
.setup = wg_setup,
.newlink = wg_newlink,
};
따라서 wireguard 타입의 인터페이스를 생성하면 wg_setup()을 통해 net_device의 특성이 설정된다.
기존 wg_setup()을 보면 다음과 같이 Ethernet 장치가 아닌 L3 인터페이스에 맞는 설정을 사용하고 있다.
static void wg_setup(struct net_device *dev)
{
...
dev->netdev_ops = &netdev_ops;
dev->header_ops = &ip_tunnel_header_ops;
dev->hard_header_len = 0;
dev->addr_len = 0;
...
dev->type = ARPHRD_NONE;
dev->flags = IFF_POINTOPOINT | IFF_NOARP;
...
wg->dev = dev;
}
Ethernet device 로 동작하도록 하기 위해선 그 dev에 ether_setup(dev);를 호출하여 Ethernet device의 기본 설정을 넣어줘야한다.
이를 위해 L2 용 setup callback 인 tapwg_setup() 을 만들어주었다.
static void tapwg_setup(struct net_device *dev)
{
struct wg_device *wg = netdev_priv(dev);
...
ether_setup(dev);
dev->netdev_ops = &tapnetdev_ops;
dev->needed_headroom = DATA_PACKET_HEAD_ROOM;
dev->needed_tailroom = noise_encrypted_len(MESSAGE_PADDING_MULTIPLE);
...
dev->mtu = ETH_DATA_LEN - overhead - ETH_HLEN;
dev->max_mtu = ... - ETH_HLEN;
eth_hw_addr_random(dev);
...
wg->dev = dev;
}
ether_setup()은 net_device 에 Ethernet 장치의 기본 특성을 설정한다. 또한 Ethernet Header 가 추가되는 것을 고려하여 MTU 계산에 ETH_HLEN을 반영하고 eth_hw_addr_random() 을 이용해 인터페이스가 사용할 MAC 주소를 생성한다.
Ethernet device 는 MAC 주소를 사용하므로 L2 용 net_device_ops 에는 MAC 주소 설정 및 검증을 위한 operation 도 추가된다.
static const struct net_device_ops tapnetdev_ops = {
.ndo_open = wg_open,
.ndo_stop = wg_stop,
.ndo_start_xmit = wg_xmit,
.ndo_set_mac_address = eth_mac_addr,
.ndo_validate_addr = eth_validate_addr,
};
그리고 새로 만든 tapwg_setup() 을 사용하는 tapwireguard type을 등록한다.static struct rtnl_link_ops taplink_ops __read_mostly = {
.kind = "tapwireguard",
.priv_size = sizeof(struct wg_device),
.setup = tapwg_setup,
.newlink = wg_newlink,
};
tapwireguard 인터페이스를 생성한 뒤 상대방 IP로 ping을 수행해 보았다.
$ ping -c 2 10.3.0.2
PING 10.3.0.2 (10.3.0.2) 56(84) bytes of data.
From 10.3.0.1 icmp_seq=1 Destination Host Unreachable
From 10.3.0.1 icmp_seq=2 Destination Host Unreachable
--- 10.3.0.2 ping statistics ---
2 packets transmitted, 0 received, +2 errors, 100% packet loss, time 1010ms
pipe 2
통신은 실패했고 커널 로그에서는 다음과 같은 메세지가 출력되었다.$ dmesg | tail -5
[ 8240.261685] wireguard: Copyright (C) 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved.
[ 8305.474629] igc 0000:02:00.0 lan1: NIC Link is Up 1000 Mbps Full Duplex, Flow Control: RX/TX
[ 8479.290800] wireguard: tapwg0: Invalid IP packet
[ 8480.300330] wireguard: tapwg0: Invalid IP packet
[ 8481.324455] wireguard: tapwg0: Invalid IP packet인터페이스 통계에서도 송신과정에서 오류가 발생한 것을 확인할 수 있다.
$ ip -s link show tapwg0
7: tapwg0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1406 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
link/ether 02:ca:fe:f0:0e:01 brd ff:ff:ff:ff:ff:ff
RX: bytes packets errors dropped missed mcast
0 0 0 0 0 0
TX: bytes packets errors dropped carrier collsns
0 0 3 0 0 0
기존 wg_xmit 경로에서는 전달받은 skb가 정상적인 IP packet인지 검사한다.
$ grep -r "Invalid IP packet" .
grep: ./wireguard.ko: binary file matches
grep: ./device.o: binary file matches
./device.c: net_dbg_ratelimited("%s: Invalid IP packet\n", dev->name);
grep: ./wireguard.o: binary file matches
static netdev_tx_t wg_xmit(struct sk_buff *skb, struct net_device *dev)
{
...
if (unlikely(!wg_check_packet_protocol(skb))) {
ret = -EPROTONOSUPPORT;
net_dbg_ratelimited("%s: Invalid IP packet\n", dev->name);
goto err;
}
}
ip_tunnel_parse_protocol 에서 ipv4 또는 ipv6 packet인지 확인한다.static inline bool wg_check_packet_protocol(struct sk_buff *skb)
{
__be16 real_protocol = ip_tunnel_parse_protocol(skb);
return real_protocol && skb->protocol == real_protocol;
}
이는 기존 wireguard에서는 문제가 되지 않는다.
하지만 L2 wireguard 에서는 IPv4나 IPv6 뿐 아니라 다양한 프로토콜이 들어갈 수 있다.
따라서 Ethernet frame 자체를 터널링하려면 기존의 검사 부분을 적용하지 않도록 수정한다.
어차피 수신쪽에서 최소한 Ethernet Frame으로 성립할 수 있는 길이는 따로 처리하고 있다.
static void wg_packet_consume_data_done(struct wg_peer *peer,
struct sk_buff *skb,
struct endpoint *endpoint)
{
...
wg_timers_data_received(peer);
if (unlikely(!pskb_may_pull(skb, ETH_HLEN)))
goto dishonest_packet_size;
다만 이 검사는 wg_check_packet_protocol() 을 대체하는 검사는 아니다.
송신 측의 wg_check_packet_protocol() 은 기존 IP 전용 가정을 제거하기 위해 우회한 것이고, 수신 측의 pskb_may_pull() 은 Ethernet Header 에 접근 가능한지를 확인하기 위한 별도의 검증이다.
2) l2 peer 선택 경로 추가
다시 ping 을 보내면 에러는 발생하지 않지만 여전히 ping이 가지 않는다.
bpftrace 를 돌려보면 wg_allowedips_lookup_dst 에서 -126(ENOKEY) 에러가 발생함을 알 수 있다.
#include <linux/skbuff.h>
kprobe:wg_xmit {
$skb = (struct sk_buff *)arg0;
printf("\n[xmit] proto=0x%04x len=%d comm=%s\n",
bswap($skb->protocol), $skb->len, comm);
}
kretprobe:wg_xmit {
printf("[xmit] ret=%d\n", (int64)retval);
}
kprobe:wg_* /probe != "kprobe:wg_xmit"/ {
printf(" %s\n", probe);
}
#include <linux/skbuff.h>
kprobe:wg_allowedips_lookup_dst {
@in = 1;
$skb = (struct sk_buff *)arg1;
printf("[lookup_dst] enter protocol=0x%04x\n", bswap($skb->protocol));
}
kretprobe:wg_allowedips_lookup_dst {
@in = 0;
printf("[lookup_dst] exit ret=%p\n", retval);
}
kprobe:wg_*, kprobe:ip_*, kprobe:ipv6_*, kprobe:skb_*, kprobe:pskb_* /@in == 1/ {
printf(" %s\n", probe);
}
[lookup_dst] enter protocol=0x0806
[lookup_dst] exit ret=(nil)
[lookup_dst] enter protocol=0x0806
[lookup_dst] exit ret=(nil)
enter protocol=0x806->exit ret=(nil) 사이에 아무 함수도 불리지 않았다.
즉 lookup_dst 가 내부 lookup() 조차 안 부르고 바로 NULL을 리턴했음을 알 수 있다.
/* Returns a strong reference to a peer */
struct wg_peer *wg_allowedips_lookup_dst(struct allowedips *table,
struct sk_buff *skb)
{
if (skb->protocol == htons(ETH_P_IP))
return lookup(table->root4, 32, &ip_hdr(skb)->daddr);
else if (skb->protocol == htons(ETH_P_IPV6))
return lookup(table->root6, 128, &ipv6_hdr(skb)->daddr);
return NULL;
}
IP 기반 peer 선택 또한 IPv4, IPv6만 지원하는 것을 확인할 수 있다.
ARP, PPPoE 같은 L2 Frame 에는 이 방식으로 사용할 IPv4/IPv6 목적지 주소가 없다.
이제 기존 L3 기반 패킷 프레임을 분석해보면서 코드상에 어느 부분을 수정해야할지 확인해보자.
raspberry pi에서 다음과 같이 office-gw쪽으로 ping을 날리고 office-gw에서 tcpdump로 패킷을 확인하면
ping -c1 -s 1 192.168.1.1 # IP 20 + ICMP 9 = 29
ping -c1 -s 4 192.168.1.1 # 32 → 패딩 0
ping -c1 -s 5 192.168.1.1 # 33 → 48로 패딩
ping -c1 -s 20 192.168.1.1 # 48
ping -c1 -s 21 192.168.1.1 # 49 → 64
- UDP len 0x48=72 (8+64)
- UDP Header len : 8 byte
- Wireguard Data len : 64 byte
- WG type 0x04
- reveiver index 0xf325ec1c
/* receive.c */
PACKET_CB(skb)->keypair = (struct noise_keypair *)wg_index_hashtable_lookup(
wg->index_hashtable, INDEX_HASHTABLE_KEYPAIR,
((struct message_data *)skb->data)->key_idx, /* ← f325ec1c */
&peer);- counter 0x01
- encrypted data 32 byte (ip 29 byte + padding 3 byte)
- poly1305 tag 16 byte
Encrypted Data 에는 페이로드가 암호화되어있고 남은 부분은 패딩으로 처리되어있다.
- IPv4 (0100) / Header len = 5 (0101) = 20 byte
- TOS (0x00) / tot_len (0x1d = 29)
- TTL 63 (0x3f) / ICMP (0x01)
- src ip (c0a8 0002) / dst ip (c0a8 0101)
- ICMP 9 byte (0800 fe45 f9b8 0001 00)
하지만 Ethernet header엔 길이 필드가 없다. 그래서 페이로드 길이를 알려면 ethertype(0800)을 보고 그 뒤 IP 헤더의 001d를 읽어서 14 (dst MAC 6 byte + src MAC 6 byte + ethertype 2 byte) + 29 = 43을 계산해야한다.
if (peer->device->is_tap) {
if (unlikely(!pskb_may_pull(skb, ETH_HLEN)))
goto dishonest_packet_size;
skb->dev = dev;
skb->ip_summed = CHECKSUM_UNNECESSARY;
skb->csum_level = ~0;
skb->protocol = eth_type_trans(skb, dev);
len = skb->len; /* ← 여기가 패딩 포함 그대로 */
len_before_trim = skb->len;
...
skb->protocol = eth_type_trans(skb, dev);
skb_reset_network_header(skb); /* ip_hdr()가 맞게 잡히도록 */
len_before_trim = skb->len; /* 패딩 포함, 통계용 */
len = tapwg_payload_len(skb); /* 이더넷 헤더 뒤 실제 길이 */
if (unlikely(len > skb->len))
goto dishonest_packet_size;
if (unlikely(pskb_trim(skb, len)))
goto packet_processed;
static unsigned int tapwg_payload_len(struct sk_buff *skb)
{
switch (skb->protocol) {
case htons(ETH_P_IP):
if (!pskb_may_pull(skb, sizeof(struct iphdr)))
return skb->len;
return ntohs(ip_hdr(skb)->tot_len);
case htons(ETH_P_IPV6):
if (!pskb_may_pull(skb, sizeof(struct ipv6hdr)))
return skb->len;
return sizeof(struct ipv6hdr) + ntohs(ipv6_hdr(skb)->payload_len);
case htons(ETH_P_ARP):
return sizeof(struct arphdr) + 20; /* Ethernet/IPv4 ARP = 28 */
default:
return skb->len; /* 모르는 건 패딩 못 뗌 */
}
}
VPN L2 확장
1) net_device 를 Ethernet 방식으로 변경
2) Ethernet frame 통신 확인
실제 Ethernet frame이 wg_xmit()으로 들어오는 것을 확인해보자.
ARP 의 경우 Invalid IP Packet 에러 발생
문제되는 부분은 기존 wireguard 는 ip 전용이기 때문에 protocol check에서 ipv4/ipv6가 아닌 프로토콜은 드롭시킴
3) protocol 처리 수정 (L2 protocol 까지 고려하여 xmit 경로 코드 수정)
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
이 블로그의 인기 게시물
ELF 구조부터 로딩까지
ELF format ELF(Executable and Linking Format)는 실행 가능한 코드를 위한 표준화된 파일 형식이다. ELF는 주로 실행 파일(ET_EXEC)이나 공유 라이브러리(ET_DYN) 에 사용된다. 현재 거의 모든 현대적인 Unix 계열 운영체제는 ELF 의 이식성, 표준화된 기능, 처음부터 깔끔하게 설계된 구조 덕분에 ELF 형식을 지원한다. ELF 표준의 실제 버전은 1.2 이다. ELF 표준을 다루는 여러 문서가 있으며 자세한 내용은 Tool Interface Standard, Executeable and Linking Format, Version 1.2 를 참고하면 된다. ELF 바이너리 형식은 다음 두가지 요구사항을 모두 만족하도록 설계되었다. linker : 일반적으로 컴파일 과정에서 사용된다. loader : 일반적으로 프로그램 실행 시점에서 사용된다. 이 두가지 요구사항을 만족하기 위해 ELF 바이너리 파일 내부의 데이터를 설명하는 서로 구별되는 두가지 인터페이스가 포함되었다. 이 두 인터페이스는 서로 의존하지 않는다. ELF headers ELF 파일은 최소한 ELF header를 반드시 포함해야 한다. ELF 헤더에는 바이너리 파일의 내용을 어떻게 해석해야 하는지에 대한 정보와 바이너리를 설명하는 다른 구조체들이 파일의 어디에 위치하는지에 대한 정보가 들어있다. ELF 헤더는 파일 내부의 offset 0에서 시작하며 다음과 같은 형식을 가진다. #define EI_NIDENT (16) typedef struct { unsigned char e_ident[EI_NIDENT]; Elf32_Half e_type; Elf32_Half e_machine; Elf32_Word e_version; Elf32_Addr e_entry; Elf32_Off e_phoff; Elf32_Off e_shoff; } Elf32_Ehdr; [e_ident] e...
VM escape - QEMU의 메모리를 읽는 두 가지 방법
악성코드가 VM을 탈출하여 호스트 머신에서 코드를 실행할 수 있을까? KVM(Kernel-based Virtual Machine) 은 사용자 공간 프로그램에 완전 가상화 인프라를 제공하는 커널 모듈이다. 이를 통해 수정되지 않은 Linux 또는 Windows 이미지를 실행하는 여러 가상 머신을 구동할 수 있다. KVM 의 사용자 공간 구성요소는 메인라인 QEMU(Quick Emulator)에 포함되어있으며 특히 device emulation을 담당한다. 개발 환경 여기서 타겟으로 잡은 취약점은 이미 오래전 패치되었기 때문에 QEMU 소스에서 해당 취약점 수정 직전의 커밋으로 checkout 해야한다. QEMU 메모리 배치 게스트에 할당된 물리 메모리는 실제로 QEMU의 가상 주소 공간 내에서 mmap 된 private 영역이다. 게스트의 물리 메모리를 할당할 때 PROT_EXEC 플래그가 활성화되지 않는다는 점에 주목하는 것이 중요하다. 게스트 프로세스 +--------------------+ 가상 주소 공간 | | +--------------------+ | | \__ 페이지 테이블 \__ \ \ | | 게스트 커널 +----+--------------------+----------------+ 게스트 물리 메모리 | | | | +----+--------------------+--------...
댓글
댓글 쓰기