Copy Fail 이후 6주 — 취약점 분석과 앞으로의 대응
Copy Fail 이후 6주 — 취약점 분석과 앞으로의 대응
n4c 활동을 하면서 분석한 Copy Fail과 그로부터 파생된 취약점들을 정리하고, 그 과정에서 느꼈던 점들을 함께 적어보았습니다.
2026년 4월 29일 Copy Fail이 공개된 이후 6주 동안, 리눅스 커널에서 page cache를 손상시키는 로컬 권한 상승 취약점이 다섯 건 공개되었습니다. 그중 네 건은 같은 유형으로 묶이며, 앞선 취약점을 수정한 패치가 다음 취약점의 조건이 된 경우가 두 차례 있었습니다.
DirtyFrag가 공개되고 그 패치를 우회하는 Fragnesia가 나오기까지 9일, 다시 DirtyClone까지 8일이 걸렸습니다. 배포판에 패치가 적용되기도 전에 해당 패치를 우회하는 변종이 공개되는 상황이 이어졌습니다.
1부와 2부에서는 이 6주 동안 확인된 사실을 정리하고, 3부와 4부에서는 이런 흐름이 이어질 경우 연구하는 쪽과 방어하는 쪽이 각각 무엇을 준비해야 할지를 다룹니다.
1부와 2부는 공개된 자료와 커밋을 확인한 내용이고, 3부와 4부는 검증된 결론이 아니라 개인적인 생각입니다. 아직 배우는 입장이라 부족한 부분이 있을 수 있습니다. 다른 의견이나 잘못된 내용이 있다면 알려주시면 감사하겠습니다.
1부. 6주 동안 무슨 일이 있었나
Copy Fail의 루트 코즈
4월 29일 공개된 Copy Fail(CVE-2026-31431)은 732바이트 분량의 파이썬 스크립트로 주요 배포판에서 root 권한을 획득합니다. 영향 범위는 커널 4.14부터 7.0-rc까지입니다.
공격 조건은 다음과 같습니다. 배포판별 오프셋이 필요 없고, 재컴파일도 필요 없으며, race condition이 아니므로 반복 시도가 필요 없습니다. 실패해도 시스템이 중단되지 않습니다.
루트 코즈를 보면 개별 커밋에는 문제가 없습니다. 서로 다른 시기에 들어간 세 가지 변경이 결합된 결과입니다.
2011년, IPsec의 확장 시퀀스 번호를 지원하기 위해 authencesn AEAD 템플릿이 추가됩니다. 이때 임시 버퍼를 별도로 할당하지 않고 호출자가 제공한 목적지 버퍼를 스크래치 공간으로 사용합니다. 당시에는 문제가 되지 않았습니다. 이 템플릿을 호출하는 것은 커널 내부의 xfrm 레이어뿐이었고, 그 버퍼도 커널이 직접 할당한 skb였습니다.
2015년, algif_aead가 AF_ALG를 통해 AEAD 연산을 사용자 공간에 노출합니다. 이 시점까지도 안전했습니다. 입력과 출력 scatterlist가 분리된 out-of-place 구조였기 때문에, splice()로 전달한 page cache 페이지는 읽기 전용인 소스 측에만 존재했습니다.
2017년, 성능 최적화를 위해 AEAD를 in-place로 변경합니다(72548b093ee3). 인증 태그 영역을 복사하지 않고 sg_chain()으로 출력 scatterlist 끝에 연결한 뒤 req->src = req->dst를 적용했습니다. 이 변경으로 splice()가 전달한 page cache 페이지가 쓰기 가능한 목적지 안에 포함됩니다.
세 변경이 결합되면 다음 경로가 만들어집니다.
읽기 전용 setuid 바이너리
↓ splice()
page cache 페이지
↓ AF_ALG
crypto scatterlist — sg_chain으로 목적지 끝에 연결
↓ authencesn 스크래치 쓰기
파일의 캐시된 사본에 4바이트 기록
HMAC 검증은 실패하고 recvmsg()도 EBADMSG를 반환합니다. 그러나 이미 기록된 4바이트는 되돌려지지 않습니다. 대상 파일과 오프셋, 기록할 값을 모두 통제할 수 있으므로 /usr/bin/su 같은 setuid 바이너리를 수정할 수 있습니다.
이 취약점은 디스크상의 파일을 변경하지 않습니다. 커널이 해당 페이지를 dirty로 표시하지 않으므로 writeback이 발생하지 않고, mtime도 변하지 않으며, VFS write 경로를 거치지 않으므로 로그도 남지 않습니다. 실행 시점에 실제로 읽히는 것은 page cache입니다. CISA는 5월 1일, 공개 이틀 만에 이 취약점을 KEV 목록에 추가했습니다.
세 커밋은 각각 독립적으로 검토했을 때 문제가 드러나지 않습니다. 취약점은 각 커밋이 전제하던 조건이 서로 어긋나는 지점에서 발생했습니다.
패치가 다음 취약점을 만든 경우
Copy Fail이 공개되던 무렵, 이미 별개의 취약점 두 건이 커널 메인테이너에게 보고되고 netdev에 패치까지 제출된 상태였습니다. 이후 6주간의 흐름은 다음과 같습니다.
| 날짜 |
이름 |
CVE |
위치 |
| 4/29 |
Copy Fail |
2026-31431 |
crypto / AF_ALG |
| 5/7 |
DirtyFrag |
2026-43284, 43500 |
xfrm-ESP, RxRPC |
| 5/13 |
Fragnesia |
2026-46300 |
XFRM ESP-in-TCP |
| 5/21 |
DirtyClone |
2026-43503 |
skb clone |
| 6/16~17 |
pedit COW |
2026-46331 |
net/sched |
Copy Fail과 DirtyFrag는 패치 우회 관계가 아닙니다. 한쪽은 crypto scatterlist, 다른 한쪽은 네트워크 skb fragment에서 발생합니다. 두 취약점이 공유하는 것은 상위의 문제입니다. 성능을 위해 파일 메모리에 대한 참조를 다른 서브시스템으로 넘기는데, 전달받은 쪽은 그것이 파일에 속한 메모리라는 정보를 갖고 있지 않다는 점입니다.
직접적인 우회 계보는 DirtyFrag 이후부터 시작됩니다. 5월 4일 메인라인에 반영된 수정(f4c50a4034e6)은 SKBFL_SHARED_FRAG 플래그를 splice된 UDP 패킷에 설정하고, in-place 복호화를 수행하는 코드가 그 플래그를 확인하면 COW를 먼저 수행하도록 했습니다.
9일 뒤, skb_try_coalesce()가 두 패킷을 병합하면서 이 플래그를 전달하지 않는다는 사실이 확인됩니다. Fragnesia입니다. 후속 패치는 Fixes: 태그에 DirtyFrag 수정 커밋 f4c50a4034e6을 명시합니다. 프리미티브도 4바이트 고정에서 임의 길이 기록으로 확장되었습니다.
다시 8일 뒤, 같은 문제가 __pskb_copy_fclone()에서도 확인됩니다. DirtyClone입니다. 남은 헬퍼들을 일괄 수정하는 광역 패치가 이미 제출된 상태였으나, 병합되기 전에 독립적으로 재발견되었습니다.
기존 플래그가 보안 경계가 된 경우
Fragnesia 패치의 Fixes: 태그에는 커밋 두 개가 명시되어 있습니다. 하나는 DirtyFrag 수정 커밋 f4c50a4034e6이고, 다른 하나는 2013년 커밋 cef401de7be8입니다.
이 플래그는 2013년부터 존재하던 것입니다. 본래 용도도 보안이 아니라 체크섬 정확성이었습니다. vmsplice()나 sendfile()로 들어온 프래그는 전송 도중 내용이 변경될 수 있으므로, TX 체크섬을 계산할 예정이라면 먼저 복사하라는 표시였습니다. 잘못되더라도 결과는 체크섬이 어긋나는 정도였습니다.
DirtyFrag 패치가 한 일은 플래그를 새로 만든 것이 아니라, 13년간 정확성 목적으로 쓰이던 플래그를 보안 판단의 근거로 사용하기 시작한 것입니다.
그리고 이 플래그는 누락 시 안전하지 않은 쪽이 기본값이 되는 구조입니다. skb를 복사하거나 병합하거나 분할하는 헬퍼는 커널에 수십 개가 있는데, 플래그를 옮기는 코드를 작성하지 않으면 값은 0이 됩니다. 컴파일러가 검사하지 않고 타입 시스템이 강제하지도 않으므로, 누락해도 아무 경고가 발생하지 않습니다.
Fragnesia와 DirtyClone은 그 수십 개 중 하나씩에서 확인된 사례입니다. 앞서 언급한 5월 16일 광역 패치는 __pskb_copy_fclone(), skb_shift(), skb_segment()와 GRO 관련 함수를 함께 수정 대상으로 포함했습니다.
2부. 9년간 발견되지 않은 이유와 지금 발견된 이유
자동화 도구가 탐지할 수 없는 유형이었습니다
AF_ALG는 syzkaller가 오랜 기간 테스트해 온 표면입니다. algif_aead 역시 syzbot 리포트가 꾸준히 제기되던 영역입니다. 그럼에도 9년간 발견되지 않았습니다.
그 이유는 메모리 안전성 버그가 아니기 때문입니다.
KASAN이 탐지하는 것은 유효하지 않은 메모리 접근입니다. 그런데 Copy Fail이 기록하는 대상은 유효하고, 정상적으로 매핑되어 있으며, 참조 카운트도 올바르게 관리되고 있는 페이지입니다. 기록해서는 안 되는 페이지일 뿐 접근 자체는 적법합니다.
따라서 sanitizer 관점에서는 탐지 대상이 아닙니다. 크래시가 없고, 리포트도 없으며, 퍼저 입장에서는 에러 코드를 반환하고 정상 종료한 실행입니다.
이 버그는 크래시를 일으키지 않고, 흔적을 남기지 않으며, 개별 코드 단위로는 정상입니다. 그런데 현재 사용되는 자동화 도구 대부분이 이 세 가지 중 하나에 의존합니다.
세 문장의 컨텍스트로 한 시간 만에 발견되었습니다
발견 과정은 공개되어 있습니다.
Theori 공개 문서를 살펴보면 이전 kernelCTF 작업을 통해 AF_ALG 공격 표면은 이미 파악되어 있었다고 나와있습니다. 그 과정에서 AF_ALG와 splice()를 조합하면 비특권 사용자가 page cache 페이지를 crypto 서브시스템에 전달할 수 있다는 점이 확인되었고, scatterlist에 포함된 페이지의 출처가 충분히 검토되지 않은 영역일 것이라는 가설이 세워집니다.
자동화 분석 도구에 컨텍스트로 제공된 것은 세 문장입니다.
이것은 리눅스 crypto 서브시스템이다. 사용자 공간 syscall에서 도달 가능한 모든 코드 경로를 검토하라. 한 가지 핵심 관찰: splice()는 읽기 전용 파일(setuid 바이너리 포함)의 page cache 참조를 crypto TX scatterlist에 전달할 수 있다.
약 한 시간 후 스캔이 완료되었고 Copy Fail이 최고 심각도로 보고되었습니다. 같은 스캔에서 다른 권한 상승 버그를 포함한 고심각도 취약점이 추가로 발견되었으나 아직 책임공개 절차가 진행 중이라고 합니다.
이 방식은 크래시를 탐지하는 것이 아니라 코드의 의미를 판단하는 방식입니다. 해당 메모리의 소유자가 누구인지는 실행을 통해 관찰되는 성질이 아니라 코드를 읽고 추론해야 하는 성질입니다.
Project Zero와 DeepMind의 Big Sleep 연구도 같은 내용을 다룹니다. 현재의 LLM은 완전히 개방된 탐색보다, 이미 발견된 버그를 출발점으로 삼는 variant analysis에 더 적합하다는 것입니다. Big Sleep이 발견한 SQLite 스택 버퍼 언더플로도 OSS-Fuzz와 프로젝트 자체 퍼징 인프라가 모두 놓친 버그였습니다.
3부. 앞으로 예상되는 변화
여기서부터는 확인된 사실이 아니라 제 추측입니다. 다만 이번 6주를 예외적인 사건으로 보기는 어렵다고 생각합니다.
대체된 것은 탐색이지 가설 수립이 아닙니다
Copy Fail의 발견 과정을 두고 AI가 취약점을 찾는다고 정리하는 것은 정확하지 않다고 생각합니다. 자동화 도구에 제공된 세 문장은 AF_ALG와 splice()의 상호작용을 이미 이해하고 있어야 작성할 수 있습니다. 그 문장이 없었다면 같은 스캔에서 Copy Fail이 최고 심각도로 올라왔을지는 알 수 없습니다.
달라진 것은 하나의 가설을 서브시스템 전체에 대해 검증하는 비용이 며칠에서 한 시간으로 줄었다는 점입니다. 이 변화가 앞으로 어떤 결과로 이어질지가 이 글의 나머지 내용입니다.
또 한 가지, DirtyFrag 패치처럼 기존 조건에 새로운 책임을 부여하는 수정은 코드 한 줄로 가능하지만, 그 시점부터 해당 조건을 유지해 온 모든 코드가 보안 관련 코드가 됩니다. 그리고 그에 대한 전수 검토는 대체로 함께 이루어지지 않습니다. Fragnesia와 DirtyClone이 9일과 8일 간격으로 이어진 배경에 이 구조가 있다고 봅니다.
취약점 사이의 간격이 줄어듭니다
패치가 공개되면 다음 정보가 함께 공개됩니다. 어떤 기능이 외부 입력과 연결되어 있는지, 개발 측이 무엇을 위험하다고 판단했는지, 새로 검사하는 조건이 무엇인지, 같은 규칙을 지켜야 하는 다른 코드가 어디에 있는지.
DirtyFrag 패치가 SKBFL_SHARED_FRAG를 신뢰하기 시작한 시점에 "그 플래그를 보존하지 않는 다른 헬퍼가 있는가"라는 질문이 만들어집니다. 이 질문이 있으면 검토 범위는 네트워크 스택 전체가 아니라 프래그를 다루는 함수 수십 개로 줄어듭니다. 그리고 그 수십 개를 검토하는 작업은 LLM이 비교적 잘 처리하는 형태입니다.
Fragnesia까지 9일, DirtyClone까지 8일이 걸린 이유가 여기에 있다고 봅니다. 이 검토 비용이 더 낮아지면 간격은 더 줄어듭니다.
패치 공개와 익스플로잇 사이의 시간이 짧아집니다
pedit COW가 이 부분을 보여줍니다.
이 수정은 본래 netdev에서 데이터 손상 버그 패치로, 보안 관련 언급이나 CVE 없이 공개적으로 진행되었습니다. 6월 16일에 CVE가 부여되었고 다음 날 동작하는 PoC가 공개되었습니다. 익스플로잇에 필요한 정보는 그보다 훨씬 이전부터 메일링 리스트에 공개되어 있었습니다.
기존에는 공개된 패치를 분석해 익스플로잇으로 연결하는 데 상당한 시간과 전문성이 필요했고, 그 시간이 방어 측에게 대응 시간으로 작용했습니다. 그 시간이 줄어들고 있습니다.
4부. 대응 방향
연구하는 입장에서
탐색보다 가설 수립의 비중이 커집니다. 코드를 넓게 검토하는 작업이 저렴해질수록, 어디를 봐야 하는지 판단하는 능력이 상대적으로 중요해집니다. Copy Fail을 찾아낸 것은 자동화 도구였지만, scatterlist에 들어간 페이지의 출처가 검토되지 않은 영역일 것이라는 가설은 도구가 만든 것이 아닙니다. 서브시스템에 대한 이해가 여전히 출발점입니다.
중복 발견을 전제해야 합니다. DirtyClone은 광역 패치가 이미 제출된 상태에서 독립적으로 재발견되었습니다. 탐색 비용이 낮아지면 동일한 버그를 여러 곳에서 동시에 발견하는 상황이 일반화됩니다. 개인 연구자 입장에서는 오랜 시간을 투자한 결과가 이미 보고되어 있을 가능성을 감안해야 합니다.
다만 Fragnesia와 DirtyClone의 발견은 SKBFL_SHARED_FRAG가 어떤 조건을 전제하는 플래그인지, 그리고 그 조건이 언제부터 보안 판단에 사용되기 시작했는지를 이해한 데에서 출발했습니다. 패치에서 규칙을 추출하고, 그 규칙이 코드로 강제되는지 확인하고, 같은 규칙에 의존하는 다른 코드를 찾는 작업은 아직 사람이 담당하는 영역으로 보입니다.
검증 부담은 커집니다. 탐색 결과를 실제 취약점으로 확정하는 것은 별개의 작업입니다. 해당 경로에 도달 가능한지, 필요한 입력과 상태를 만들 수 있는지, 단순 버그인지 보안 경계를 넘는 문제인지, 기존 CVE와 중복되지 않는지, 재현 가능한 PoC가 있는지를 확인해야 합니다. 재현과 구체적인 영향 없이 제출되는 리포트는 메인테이너의 검토 부담만 늘립니다.
방어하는 입장에서
CVE 단위 추적으로는 부족합니다. DirtyFrag 패치를 적용한 시스템도 Fragnesia에 취약했고, Fragnesia까지 적용해도 DirtyClone에 취약했습니다. JFrog는 리포트를 다음과 같이 정리했습니다.
DirtyFrag 취약점의 fix chain 전체가 적용되어야 시스템이 완전히 보호됩니다.
따라서 확인해야 할 것은 취약점 이름이 아니라 CVE-2026-43284, 43500, 46300, 43503 각각과 최종 수정이 반영된 커널 버전입니다. 초기 완화만 반영되고 후속 패치가 빠진 stable·LTS 브랜치가 특히 문제가 됩니다.
결과가 같아도 원인은 다를 수 있습니다. pedit COW는 동일하게 page cache를 손상시키지만 SKBFL_SHARED_FRAG 계열이 아닙니다. traffic-control의 act_pedit에서 COW 범위 계산이 typed key의 런타임 오프셋을 반영하지 못하는 별개 버그입니다. DirtyFrag 계열에 대응하기 위해 ESP 모듈을 차단해 두었더라도 이 취약점은 막히지 않습니다.
완화 조치가 실제로 적용되었는지 확인해야 합니다. 모듈 차단 방식의 완화는 해당 기능이 모듈이 아니라 커널에 내장된 환경에서는 오류 없이 실행되면서도 아무것도 차단하지 못합니다. 적용했다는 기록이 아니라 적용 결과를 확인해야 합니다.
탐지 기준을 점검해야 합니다. 이 계열은 디스크상의 파일을 변경하지 않으므로 온디스크 해시를 비교하는 무결성 검사나 패키지 검증으로는 탐지되지 않습니다. 설정 문제가 아니라 검사 대상이 다른 것입니다. 패키지 검증 결과에 이상이 없다는 사실이 손상되지 않았다는 근거가 되지 않는 경우가 존재합니다.
CVE 알림만으로는 놓치는 구간이 있습니다. pedit COW처럼 CVE가 부여되지 않은 채 수정이 진행되는 경우가 있습니다. 패치가 공개된 직후가 가장 위험한 구간이 되고 있다면, 병목은 취약점 발견이 아니라 패치 적용 속도 쪽으로 이동합니다.
공개된 PoC는 검증이 필요합니다. 이번 계열에서도 저장소에 올라온 상당수가 다른 코드를 이름만 바꾼 것이거나 동작하지 않았습니다. 판단 근거는 저장소가 아니라 커널 버전과 upstream 커밋의 존재 여부여야 합니다.
정리
이번 6주를 따라가면서 확인한 것과, 그로부터 생각하게 된 내용을 정리하면 다음과 같습니다.
취약점은 개별 커밋이 아니라 커밋 사이에 있을 수 있습니다. Copy Fail은 2011년, 2015년, 2017년의 변경이 결합된 결과였고 각각은 그 시점에서 문제가 없었습니다. 그리고 이런 유형은 커밋 단위 리뷰로는 발견되지 않습니다.
수정이 새로운 전제를 만들면 그 전제도 함께 검토해야 합니다. DirtyFrag 패치는 13년간 정확성 목적으로 쓰이던 플래그를 보안 판단의 근거로 사용하기 시작했지만, 그 플래그를 전파해 온 수십 개의 헬퍼는 함께 검토되지 않았습니다. Fragnesia와 DirtyClone은 그 결과입니다. 수정이 구조적인 강제를 만들었는지, 아니면 개발자가 지켜야 할 규칙을 하나 늘린 것인지는 구분할 필요가 있습니다.
현재의 자동화 도구는 이 유형을 탐지하지 못합니다. Copy Fail은 크래시를 일으키지 않고, 흔적을 남기지 않으며, 개별 코드 단위로는 정상입니다. 9년간 발견되지 않은 이유가 여기에 있습니다. 퍼징 결과에 이상이 없다는 사실이 해당 코드에 문제가 없다는 근거는 아닙니다.
반면 코드의 의미를 판단하는 방식은 이 유형에 효과가 있었습니다. 세 문장의 컨텍스트와 한 시간의 스캔으로 발견되었습니다. 다만 그 세 문장은 해당 서브시스템을 이해하고 있어야 작성할 수 있습니다. 낮아진 것은 탐색 비용이지 가설 수립의 난이도가 아닙니다.
따라서 앞으로 이런 사례는 더 자주 나올 것으로 예상합니다. 패치가 공개되면 어떤 조건이 새로 중요해졌는지가 함께 드러나고, 그 조건을 지키지 않는 코드를 찾는 작업은 검토 범위가 좁아 자동화에 적합합니다. Fragnesia까지 9일, DirtyClone까지 8일이 걸린 이유이며, 이 비용이 더 낮아지면 간격도 더 줄어들 것입니다. pedit COW처럼 CVE도 부여되지 않은 수정에서 다음 날 PoC가 나오는 경우도 함께 늘어날 것으로 봅니다.
연구하는 쪽에서는 가설 수립과 검증의 비중이 커집니다. 코드를 넓게 훑는 작업의 가치는 낮아지고, 어디를 봐야 할지 판단하는 능력과 결과를 실제 취약점으로 확정하는 능력이 남습니다. 동시에 중복 발견은 일반적인 상황이 될 것이므로, 최초 발견 여부만을 기준으로 삼기는 어려워질 것 같습니다.
방어하는 쪽에서는 추적 단위와 탐지 기준을 다시 잡아야 합니다. CVE 하나가 아니라 fix chain 전체를 확인해야 하고, 결과가 같은 취약점이라도 원인이 다르면 완화 조치가 적용되지 않습니다. 이 계열처럼 디스크상의 파일을 변경하지 않는 경우 온디스크 무결성 검사로는 탐지되지 않으므로, 기존 점검 항목이 실제로 무엇을 보고 있는지 확인할 필요가 있습니다. 그리고 병목은 취약점 발견이 아니라 패치 적용 속도 쪽으로 옮겨갑니다.
정리하면, 패치는 문제를 수정하는 코드인 동시에 같은 문제가 어디에 남아 있는지를 알려주는 정보이기도 합니다. 공격하는 쪽과 방어하는 쪽이 동일한 정보를 보고 있고, 그 정보를 해석하는 비용이 양쪽 모두에게 낮아졌습니다. 앞으로의 차이는 누가 먼저 해석하느냐에서 생길 것으로 보입니다.
부족한 글을 끝까지 읽어주셔서 감사합니다. 잘못 이해한 부분이나 보완이 필요한 내용이 있다면 알려주시면 감사하겠습니다.
참고 자료