Home
System Hacking
🔐

Dirty Frag Background (2) - XFRM, ESP와 ESN

Type
Research
날짜
2026/08/05
종류
Kernel
Linux
1 more property

여담

Copy Fail에서 authencesn의 4-byte scratch write를 이미 봤다. Dirty Frag의 xfrm-ESP variant는 같은 crypto sink에 AF_ALG가 아니라 IPsec ESP receive path를 통해 도달한다.
이 글에서는 AEAD/scatterlist의 일반 개념을 반복하지 않고, XFRM SA, ESP, ESN, UDP encapsulation이 어떤 역할을 하며 attacker가 어떻게 esp_input()까지 도달하는지만 본다.

XFRM과 ESP

XFRM은 Linux kernel의 IPsec transform framework다. packet에 암호화·인증 transform을 적용하고, 그 상태를 struct xfrm_state로 관리한다.
ESP(Encapsulating Security Payload)는 IPsec에서 payload confidentiality와 integrity를 제공한다. 수신 packet의 SPI와 destination을 이용해 SA(Security Association)를 찾고, SA에 지정된 key/algorithm으로 decrypt와 authentication을 수행한다.
UDP/IP packet -> XFRM state lookup -> ESP header SPI sequence number low 32 bits -> esp_input() -> Crypto API AEAD decrypt
Plain Text
복사
PoC는 transport mode, loopback, UDP encapsulation을 이용해 packet을 다시 로컬 kernel receive path로 돌려보낸다.

SA와 NETLINK_XFRM

SA에는 대략 다음 정보가 들어간다.
source/destination address
SPI
transport/tunnel mode
encryption/authentication algorithm과 key
replay window와 sequence state
ESN 사용 여부
userspace는 NETLINK_XFRM으로 SA를 등록한다. init user namespace에서 이 작업은 CAP_NET_ADMIN이 필요하다.
공개 PoC는 unshare(CLONE_NEWUSER | CLONE_NEWNET)로 새 user/net namespace를 만들고 그 namespace 안에서 CAP_NET_ADMIN을 얻는다. 따라서 unprivileged user namespace가 정책으로 막혀 있으면 ESP variant의 진입 조건이 차단될 수 있다.

ESN이 왜 4-byte 값의 source가 되는가

기본 ESP sequence number는 32-bit다. ESN(Extended Sequence Number)은 논리적으로 64-bit sequence를 사용하고, 상위 32-bit는 wire header에 그대로 실리지 않지만 authentication 계산에는 포함된다.
esp_input_set_header()는 decrypt 전에 ESN 상위 32-bit를 skb 앞쪽에 임시로 배치한다.
if (x->props.flags & XFRM_STATE_ESN) { esph = skb_push(skb, 4); *seqhi = esph->spi; esph->spi = esph->seq_no; esph->seq_no = XFRM_SKB_CB(skb)->seq.input.hi; }
C
복사
그 값은 SA의 replay ESN state에서 온다. PoC는 XFRMA_REPLAY_ESN_VAL.seq_hi를 chunk마다 바꿔 4-byte STORE 값을 제어한다.

UDP encapsulation으로 esp_input까지 가는 경로

NAT 환경에서는 ESP를 UDP 4500 안에 넣는 ESP-in-UDP를 사용한다. receive socket에 UDP_ENCAP_ESPINUDP를 설정하면 일반 UDP queue가 아니라 XFRM input으로 packet이 전달된다.
splice(pipe -> UDP send socket) -> sender skb frags[]에 file page P -> loopback RX -> udp_rcv() -> udp_queue_rcv_one_skb() -> xfrm4_udp_encap_rcv() -> xfrm_input() -> esp_input()
Plain Text
복사
공개 PoC의 sender skb 형태는 다음과 같다.
skb linear head: ESP header + IV frags[0]: { page = target file page-cache P, offset = target offset, size = 16 }
Plain Text
복사
linear 영역은 attacker가 만든 protocol header이고 paged frag는 read-only로 연 target file에서 온다.

esp_input의 fast path

취약한 esp_input()은 uncloned skb에 대해 다음 조건이면 skb_cow_data()를 건너뛴다.
if (!skb_cloned(skb)) { if (!skb_is_nonlinear(skb)) { nfrags = 1; goto skip_cow; } else if (!skb_has_frag_list(skb)) { nfrags = skb_shinfo(skb)->nr_frags + 1; goto skip_cow; } } err = skb_cow_data(skb, 0, &trailer);
C
복사
여기서 !skb_has_frag_list(skb)frags[]가 kernel-private하다는 뜻이 아니다. splice로 들어온 shared page가 있어도 frag_list는 비어 있을 수 있다.
결국 page P가 그대로 SGL이 되고 다음 요청이 만들어진다.
skb_to_sgvec(skb, sg, 0, skb->len); aead_request_set_crypt(req, sg, sg, elen + ivlen, iv); crypto_aead_decrypt(req);
C
복사
src와 dst가 같은 SGL이므로 crypto_authenc_esn_decrypt()의 4-byte scratch STORE가 page P로 향한다.

authentication 실패가 write를 되돌리지 못하는 이유

authencesn은 verification 전에 ESN byte rearrangement를 위해 dst에 4-byte STORE를 먼저 수행한다.
scatterwalk_map_and_copy(tmp + 1, dst, assoclen + cryptlen, 4, 1);
C
복사
그 뒤 authentication이 실패해 -EBADMSG를 반환해도 이미 실행된 memcpy()는 rollback되지 않는다. 그래서 올바른 authentication key 없이도 page cache modification이 남는다.

정리

xfrm-ESP variant의 핵심은 암호를 깨는 것이 아니다. splice로 만든 shared frag를 esp_input()이 private payload로 오판해 COW를 생략하고, attacker-controlled ESN high 32-bit가 기존 Copy Fail과 같은 authencesn scratch STORE를 통해 page cache에 기록되는 것이다.

Reference

본편으로 돌아가기