전체 글

안드로이드 VPN 유출, NAT-T keepalive 오프로드로 'VPN 없는 연결 모두 차단' 우회

xXfacebook페이스북

'VPN 없는 연결 모두 차단'을 켠 안드로이드 기기에서 10초마다 패킷 하나가 VPN 밖으로 나갑니다. 2026년 7월 29일에 공개된 연구자 보고서가 밝힌 경로는, 일반 앱이 공개된 NAT-T 소켓 keepalive API만으로 VPN 정책 경계 밖의 물리 라우터에 고정 형식 UDP/4500 패킷을 도달시키는 것입니다. 이때 새는 것은 페이로드가 아니라 실제 IP와 온라인 여부, 타이밍이고 패킷 본문은 플랫폼이 고정한 1바이트입니다.

이 주장의 원천은 연구자 한 사람의 보고서입니다. Mullvad가 2026년 9월 10일에 그 내용을 요약했고 GrapheneOS 이슈 트래커에는 같은 내용이 2026년 8월 27일에 이슈로 올라와 있습니다. 이 글은 세 출처를 겹쳐 읽은 정리이고 말한 주체가 갈리는 자리는 그 기준을 그대로 옮깁니다.

일반 앱이 공개 NAT-T keepalive API로 10초마다 보내는 1바이트 UDP/4500 패킷이 VPN 정책 경계를 건너 물리 라우터에 도달하는 경로를 왼쪽에서 오른쪽으로 이은 흐름도. 폰 속 앱과 프레임워크의 소켓 keepalive, 차단 설정이 놓인 VPN 정책 경계, 별도 기기의 라우터가 순서대로 놓이고 경계를 통과하는 지점에 10초 간격이라는 표시가 붙는다.

권한 프롬프트 없이 차단 설정의 경계를 건너는 10초 간격 패킷이다.

무엇이 새로 발견됐나

설정 이름부터 고정합니다. 이 글은 논문 표기 그대로 "Block connections without VPN"으로 씁니다. Mullvad는 같은 설정을 "Block all connections…"로 조금 다르게 적었는데 Mullvad 문장을 인용할 때만 그 표기를 따릅니다. 설정이 뜻하는 대로 VPN을 거치지 않는 연결을 차단하는 자리이고 이번 보고는 그 차단이 우회됐다는 것입니다.

Mullvad는 "이 공격에 어떤 특별 권한도 필요로 하지 않는다"고 적었습니다. 루트도 ADB도 숨은 API도 아닙니다. 범위는 거꾸로 제한돼 있습니다. 나가는 패킷의 본문은 플랫폼이 고정한 1바이트라 임의 콘텐츠가 나가는 것이 아니고 달아나는 것은 실제 IP와 온라인 여부, 타이밍입니다. Mullvad 스스로도 표현을 "실제 IP가 인터넷에서 보이게 된다"는 수위에 두었습니다.

논문에는 저자 이해관계 고지도 있습니다. 삼성과 Nothing 기기에서 활성 슬롯을 관측한 도구는 저자가 파는 상용 앱 VPN Leak Guard가 찍은 것입니다. 이 글에서도 해당 관측치를 그 출처와 함께 씁니다.

공개 API만 쓴 공격

경로는 공개 API 세 번 호출입니다. IpSecManager의 openUdpEncapsulationSocket()으로 NAT-T 캡슐화 소켓을 열고 ConnectivityManager의 createSocketKeepalive(...)으로 네트워크와 소켓을 묶은 뒤 keepalive.start(10)으로 간격을 정합니다. 마지막 인자 10이 논문이 **"공개 최소 간격(public minimum interval)"**이라고 부르는 값입니다.

// 논문 PoC가 밟은 세 호출 — 전부 공개 API다
val socket = ipSecManager.openUdpEncapsulationSocket()
val keepalive = connectivityManager.createSocketKeepalive(...)
keepalive.start(10) // 10초 간격 — 논문이 "공개 최소 간격"이라고 부르는 값

논문의 실측에서는 이 경로가 그대로 물리 네트워크에 나타났습니다. Pixel 8 Pro(husky, Android 16, 보안 패치 2026-05-05)에 Mullvad 앱 2026.5를 두고 Always-on VPN과 "Block connections without VPN"을 모두 켠 상태에서, 별도 기기의 OpenWrt 라우터가 tcpdump로 10초 간격 1바이트 UDP를 잡았습니다. 관측점 자체가 의미를 갖습니다. 폰이 아니라 다른 기기의 라우터에서 패킷이 보였다는 것은 그 패킷이 이미 VPN 정책 경계를 넘어 물리 네트워크까지 나갔다는 뜻입니다. GrapheneOS 이슈에도 "10초마다 패킷을 보낸다"고 적혀 있습니다.

PoC가 선언한 권한은 INTERNET과 ACCESS_NETWORK_STATE 같은 일반 네트워킹 권한뿐입니다. 위험 런타임 권한 프롬프트도 PACKET_KEEPALIVE_OFFLOAD 권한도, JNI나 raw Binder도 쓰지 않았습니다. ADB와 루트는 연구 계측에 쓰였을 뿐 경로에서 요구되지 않습니다.

논문 부록의 대조 실험(A.2)은 차이를 수로 보여줍니다. VPN을 아예 끈 기준선에서는 라우터에 9패킷이 보였고 일반 UDP도 전부 관측됐습니다. VPN을 켜고 차단 설정을 끄면 라우터에 보이는 것은 6패킷의 keepalive뿐이고 일반 UDP는 tun0 경유로 바뀝니다. VPN과 차단 설정을 모두 켠 조건에서도 keepalive 6패킷은 라우터에 계속 잡혔습니다. 같은 차단 상태에서 일반 UDP를 보내면 라우터에 매칭 패킷이 0개였고 같은 조건의 keepalive는 18패킷이 관측됐습니다.

차단 설정을 켠 상태에서 일반 UDP는 라우터에 도달한 패킷이 0개인 반면 keepalive는 18패킷이 관측된 대조를 막대로 비교한 그림. 아래에는 조건별 관측으로 VPN을 끈 기준선 9패킷, VPN을 켜고 차단 설정을 끈 조건 6패킷, VPN과 차단 설정을 모두 켠 조건 6패킷이 나란히 놓인다.

차단 상태에서 일반 UDP는 0, keepalive는 18패킷이었다.

왜 막히지 않는가

논문은 이 자리를 소스 히스토리로 추적합니다. keepalive 오프로드가 처음 나온 2015년에는 권한 있는 플랫폼 동작이었습니다. 논문이 정리한 히스토리는 이렇습니다.

  • 2015 — 패킷 keepalive 오프로드 도입. 권한 있는 플랫폼 동작
  • 2019년 1월 — fd 기반 NAT-T keepalive 추가. 권한이 강제됨
  • 2019년 3월 — 공개 UdpEncapsulationSocket 경로가 이 fd 메서드로 이동
  • 2019년 4월 — IpSec 리소스 검증과 수명 잠금 추가
  • 2019년 5월 — 검증 스택 되돌림. 이유는 서비스 의존성과 교착 우려
  • 2019년 5월 — UID별·네트워크별 쿼터가 대체. 쿼터가 막는 것은 자원 고갈이지 기밀성이나 VPN 정책이 아님
  • 2023년 이후 — 같은 Binder 메서드에 automatic-on/off 등 필드 추가

커밋 ID는 논문이 2026년 5월 30일 AOSP Gitiles에서 다시 확인했다고 밝힙니다.

원인 규정도 논문의 표현을 따릅니다. 되돌림 자체를 실패로 보지 않습니다. 통제 없이 일반 앱 경로를 릴리스한 것이 검토·릴리스 거버넌스의 실패라는 규정이고 특정 개인에게 귀속하지 않습니다.

지금 이 경계가 실제로 걸러내는 것은 리소스 고갈입니다. 실측에서도 슬롯 하나를 수용한 뒤의 추가 요청이 자원 부족 오류로 거부됐습니다(다음 절). 요청이 들어왔을 때 그 소켓이 VPN 정책으로 막혀 있어야 하는지를 묻는 단계가 진입점에 없다는 것이 논문이 말하는 구조이고 그래서 근본 해결도 진입 시점의 fd·리소스 인증과 VPN 정책 확인으로 갑니다.

2015년부터 2023년 이후까지 keepalive 오프로드 소스의 변화를 왼쪽에서 오른쪽으로 나열한 연표. 2015년 권한 있는 플랫폼 동작, 2019년 1월 fd 기반 NAT-T keepalive 추가, 2019년 3월 공개 소켓 경로의 이동, 2019년 4월 리소스 검증 추가, 2019년 5월 검증 스택 되돌림과 쿼터 대체, 2023년 이후 같은 Binder 메서드에 필드 추가가 순서대로 놓이고 되돌림 지점이 강조돼 있다.

검증 스택은 2019년 5월에 되돌려지고 쿼터가 그 자리를 대신했다.

영향 범위와 대응 상태

논문과 같은 제목의 GrapheneOS 이슈는 영향 범위를 Android 12 이상의 대부분 기기로 적습니다. 근거는 취약한 수용 경로가 Android 12 이상 프레임워크의 공통 동작이라는 것이고 그렇다면 창은 2021년 Android 12 출시부터 열려 있습니다. 기기마다 갈리는 조건도 붙습니다. 앱이 보이는 Wi-Fi NAT-T keepalive 오프로드를 쓸 수 있는 슬롯이 있어야 합니다. Mullvad 블로그는 버전을 말하지 않습니다.

규모는 연구자의 추정입니다. Qualcomm QCA/FastConnect, Broadcom/Cypress, MediaTek, Unisoc, 삼성 엑시노스, Huawei 등 WLAN 펌웨어 패밀리 7개가 2021년 4분기~2026년 1분기 추정 Android/AOSP 파생 출하량의 91.24%를 차지하고 나머지 8.76%는 미확정 롱테일이라는 것입니다. 근거가 되는 센서스 노트는 미공개 저자 소유입니다.

실측된 기기는 3대이고 확인된 깊이는 저마다 다릅니다. Pixel 8 Pro가 패킷 캡처까지 확인된 유일한 기기입니다. 삼성 SM-F966B는 스크린샷 기준으로 keepalive 슬롯이 24시간 32분 유지됐고 측정 내내 전원이 켜져 있었습니다. Nothing A059는 활성 슬롯 1개 수용과 게이트웨이 수용 확인까지만 기록돼 있고 라우터 캡처, 간격, 리스 기간, 재부팅은 측정하지 않았습니다. Pixel에서는 슬롯 하나를 수용한 뒤 추가 시도가 ERROR_INSUFFICIENT_RESOURCES(-32)로 거부됐습니다. 삼성과 Nothing 관측치는 앞서 밝힌 대로 저자 상용 앱이 찍은 것입니다.

실측된 세 기기에서 각각 확인된 범위를 나란히 놓은 그림. Pixel 8 Pro는 별도 라우터에서 10초 간격 1바이트 패킷 캡처까지 확인한 유일한 기기이고, 삼성 SM-F966B는 슬롯이 24시간 32분 유지된 스크린샷이 근거이며, Nothing A059는 활성 슬롯 1개 수용 확인까지만 기록돼 있다.

패킷 캡처까지 간 기기는 Pixel 8 Pro 하나다.

논문이 스스로 밝힌 한계도 그대로 옮깁니다. 재부팅 후의 지속은 입증되지 않았다(not established)입니다. Pixel은 재부팅 경계에서 멈췄고 삼성은 전원을 켠 상태에서 측정했으며 Nothing은 측정하지 않았습니다. 셀룰러 방출은 미측정입니다. 소스 분석상 경로는 전송 매체를 가리지 않고 Android 16 호환성 자료는 셀룰러 슬롯 노출 가능성을 말하지만 실측된 Pixel은 셀룰러 방출 전에 자원 부족으로 멈췄고 실측된 방출은 전부 Wi-Fi입니다. Mullvad 설명의 "Wi-Fi 또는 셀룰러 칩으로 오프로드된다"는 문구를 실측 결과처럼 그대로 옮기지 않습니다. 남은 둘은 실측 기기가 3대뿐이라는 것과 패치된 빌드에서의 검증이 없다는 것입니다. 별개로 재부팅 없이 다시 잠긴 BFU 상태에서 살아남는 것은 GrapheneOS 이슈와 논문 양쪽에서 확인된 사실이고 재부팅 후 생존과는 다른 사실입니다.

이 API를 실제로 쓰는 앱이 많은지에 대해서도 논문이 양쪽을 봤습니다. 오픈소스 쪽은 F-Droid와 IzzyOnDroid의 서명 v1 인덱스 4,888개 패키지 체크아웃에서 서로 다른 git origin 문자열 4,679개를 뽑았고(다른 철자의 같은 업스트림은 병합하지 않음) 그 가운데 안드로이드 프레임워크 IPsec/IKE/NAT-T API 사용은 0건이었습니다. 이 0은 오픈소스 표본 안에서만 성립합니다. 사기업 VPN 클라이언트와 OEM 클라이언트, 사이드로드는 표본 밖입니다. 사유 쪽은 Play 스토어의 FortiClient VPN 누적 설치 3,995,326건과 SmartVPN 139,322건, 합계 4,134,648건을 들며 두 앱의 사유 APK에서 검증된 플랫폼 SDK 호출점 47개(FortiClient 37개, SmartVPN 10개)를 확인했습니다. 누적 설치수는 활성 사용자 수가 아니고 저자의 추정으로는 이 플랫폼 API에 의존하는 안드로이드 사용자가 최대 0.01%, 1만 명 중 1명 꼴입니다.

보고의 경과는 기록으로 남아 있습니다. 2026년 5월 15일에 보고가 들어가자 구글이 당일 트리아지를 하고 coordinated disclosure를 요청했습니다. 5월 19일에 구글은 정식 이슈 386376240의 중복으로 표기했고, 그 뒤 연구자가 공개 지침을 요청했습니다. 기록에는 fix-status 업데이트나 CVE 부여, 심각도 결정이 없습니다. 공개는 2026년 7월 29일에 시작됐습니다.

구글 대응에 관해 Mullvad가 적은 문장은 층위가 겹쳐 있습니다. Mullvad는 연구자에 따르면 이슈는 조치 없이 종료됐다고 전하고 그 이슈는 비공개라며 그 정보를 근거로 구글이 아무것도 하지 않을 것이라고 Mullvad 자신이 판단한다고 적습니다. 연구자의 주장, Mullvad의 전달, 이슈의 비공개, Mullvad의 판단이 한 문장에 이어져 있으므로 이 글에서는 그 위까지를 Mullvad의 말로 옮기고 그 이상을 단정하지 않습니다. Mullvad는 GrapheneOS가 수정 작업 중이라고도 적었는데 이는 Mullvad의 표현이고 이슈 자체는 2026년 9월 13일 기준 open 상태로 마지막 갱신은 2026년 9월 11일입니다.

지금 할 수 있는 것

Mullvad가 드는 이론적 해법은 정상 앱이 자체 keepalive로 슬롯을 채워 악성 앱의 수용을 막는 것입니다. 다만 Mullvad는 이 방법을 제공하지 않습니다. 이유는 두 가지입니다. Mullvad가 소유한 서버로 보내더라도 터널 밖으로 패킷을 보내는 행위 자체는 그대로 일어납니다. 그리고 악성 앱이 Mullvad 앱이 시작되기 전에 이미 유출을 시작했을 수 있어 동작을 보장할 수 없습니다.

라우터 단 VPN은 조건부입니다. 논문이 든 조건은 폰이 그 라우터를 유일한 인터넷 경로로 쓰는 것, 셀룰러와 대체 네트워크를 끄거나 별도로 차단하는 것, 라우터의 터널이 끊기면 fail closed로 동작하는 것입니다. 논문의 표현을 옮기면 안드로이드 VPN 강제에 대한 의존을 줄일 뿐 무조건적 보장은 아닙니다.

세 출처가 제시하는 대응은 앱 쪽 완화(제공되지 않음), 라우터 단 VPN(조건부), 플랫폼 수정의 세 가지로 모입니다. 차단 설정 자체가 우회된다는 것이 이 보고의 내용입니다.

연구자가 공개하고 그다음 단계인 벤더·배포판 대응이 별개로 풀리는 구도는 2026년 8월 27일에 정리한 AI 에이전트의 QEMU/KVM VM 탈출 — 0-day 3개와 Firecracker 결과와 같은 결입니다.

근거와 참고

이 글 공유하기xXfacebook페이스북