2023년 초, 구글 공개 리졸버에서 나가는 DNS 트래픽을 잡아 봤다면 버그처럼 보이는 무언가를 발견했을 것이다. wIKiPEDiA.oRG를 묻는 질의. GooGle.CoM. eXamPLE.nET. 고양이가 Shift 키 위를 밟고 지나간 듯한 들쭉날쭉한 대소문자. 버그가 아니었다. 방어책이었다 — 2008년에 처음 적힌 뒤, 표준으로 승격된 적 없이, 15년 만에 전 세계적으로 켜진. 아무의 협조도 필요 없다는 게 밝혀졌기 때문에.
이 수법에는 못생긴 이름이 붙어 있다: DNS 0x20, 또는 케이스 무작위화(case randomization). 나는 이걸 프로토콜 전체에서 가장 좋아하는 것 중 하나로 꼽는데, 정직한 의미에서의 ‘해킹’이기 때문이다 — DNS가 명세된 방식의 우연을 파고들어, 공짜로, 누구의 허락도 구하지 않고 실질적인 보안을 사들인다. 그리고 이게 왜 먹히는지에 관한 이야기는, 어떤 보안 수정은 퍼지고 어떤 것은 초안 속에서 썩는지에 관한 이야기이기도 하다.
이게 때우는 약점
모든 DNS 스푸핑 이야기가 시작되는 곳에서 출발하자: 2008년 여름, 그리고 댄 카민스키. 재귀 리졸버가 권한 서버에 “example.com의 주소가 뭐냐?”라고 물을 때, 응답을 질문에 맞추는 데 16비트 트랜잭션 ID를 쓴다. 16비트는 65,536가지 — 큰 숫자처럼 들리지만, 오프패스 공격자, 즉 당신의 트래픽을 볼 수 없고 눈 감고 쏘기만 하는 자가 추측한 ID를 실은 위조 응답 수천 개를 퍼부을 수 있고, 그중 하나가 진짜 응답보다 먼저 도착하면 캐시를 오염시켜 도메인 하나를 통째로 딴 데로 돌릴 수 있다는 걸 깨닫는 순간 얘기가 달라진다.
모두가 기억하는 해결책은 소스 포트 무작위화다. 질의는 이미 무작위 16비트 ID를 담고 있었는데, 이제 무작위 소스 포트에서도 나가게 되어 위조자는 둘 다 맞혀야 했다. RFC 5452가 이를 명문화했고, 리졸버들이 배포했고, 대략 16비트가 32비트로 바뀌었다. 위기는 관리됐다.
하지만 ‘관리’는 ‘해결’이 아니다. 32비트 추측은 더 어려울 뿐 불가능하지 않고, 그 무엇도 인증하지 않는다 — 그저 복권을 맞히는 비용을 올릴 뿐이다. 진짜 인증 답안은 레코드를 암호로 서명하는 DNSSEC인데, 20년이 넘도록 대부분의 존은 여전히 서명되지 않았다. 아무도 원치 않는 키 관리 부담을 요구하기 때문이다. 그래서 2008년의 질문은 이거였다: DNSSEC를 하염없이 기다리는 동안, 질의에서 공짜로 엔트로피를 더 짜낼 수 있을까?
명세 속의 우연
여기 그 우연이 있다. DNS 이름은 대소문자를 구분하지 않는다: example.com, EXAMPLE.COM, ExAmPlE.cOm은 모두 같은 곳으로 해석된다. RFC 1035가 1987년에 그렇게 못 박았다. 하지만 — 그리고 이게 하중을 견디는 핵심 디테일인데 — 대소문자는 보존된다. 권한 서버에 ExAmPlE.cOm을 물으면, 제대로 동작하는 서버는 조회를 대소문자 무시로 처리하면서도 답변에는 당신이 적은 그대로, 뒤섞인 대소문자 그대로 질문을 되돌려준다. 2006년에 발표된 RFC 4343은 “Domain Name System (DNS) Case Insensitivity Clarification”이라는 무미건조한 제목으로 바로 이 동작을 확정했다.
폴 빅시와 데이비드 다곤은 그걸 보고 공짜 엔트로피를 봤다. 대소문자가 보낸 그대로 돌아온다면, 대소문자 그 자체가 채널이다. 나갈 때 무작위로 섞어 버리면, 응답을 위조하는 공격자는 그것까지 맞혀야 한다.
“0x20”이라는 이름이 그 원리를 한마디로 담는다. ASCII에서 대문자와 소문자는 정확히 한 비트 차이다 — 값 32, 16진수로 0x20인 비트. A는 0x41, a는 0x61이고, 그 한 비트를 뒤집으면 대소문자가 뒤집힌다. 그래서 리졸버는 질의 이름을 글자 하나하나 훑으며 각 글자의 0x20 비트를 무작위로 토글한다. 알파벳 문자 하나하나가 공격자가 정확히 불러 맞혀야 할 동전 던지기 하나가 된다. 열 글자짜리 도메인은 약 10비트의 엔트로피를 더한다 — 트랜잭션 ID와 무작위 소스 포트 위에 쌓아서. draft-vixie-dnsext-dns0x20 인터넷 드래프트가 2008년 3월, 카민스키 소동의 한복판에서 이를 펼쳐 놓았다.
왜 끝내 표준이 되지 못했나
그 드래프트는 만료됐다. IETF에서 아무런 공식 지위가 없고, RFC가 된 적도 없다. 보통은 그게 영리한 아이디어가 죽는 지점이다.
0x20이 죽지 않은 이유는 아무의 허락도 필요 없기 때문이다. 배포에 뭐가 드는지 생각해 보라. 프로토콜 협상도, 새 레코드 타입도, 플래그 데이도, 상대편의 업그레이드도 아니다 — 리졸버가 홀로, 원래 보낼 수 있었던 질의를 보내고, 답변의 대소문자가 보낸 것과 일치하는지 확인하는 것뿐이다. 권한 서버는 0x20의 존재를 알 필요조차 없다. RFC 4343이 이미 시킨 일 — 대소문자를 보존하는 것 — 만 하면 된다. 그렇게 하면 리졸버는 공짜 스푸핑 저항을 얻는다. 응답 경로의 어떤 중간 장비가 대소문자를 재작성하면, 리졸버는 불일치를 알아채고 폴백할 수 있다.
그 “아무의 협조도 필요 없다”는 성질이, 퍼지는 수정과 그렇지 못한 수정을 가르는 전부다. DNSSEC는 모든 존 소유자가 서명해야 한다. 또 다른 값싼 스푸핑 방어인 DNS 쿠키는 권한 서버 측이 강제해야 하는데, 그 강제는 대개 기본값이 꺼짐이다. 반면 QNAME 최소화는 순수하게 리졸버 측 변경이라 다른 누구에게도 비용을 지우지 않았고 — 조용히 퍼져 어디에나 있게 됐다. 0x20은 그 운 좋은 두 번째 부류에 속한다. 설득할 위원회가 없다. 그냥 하면 된다.
그리고 측정 결과가 안전하게 그래도 된다고 말했다. 드래프트에 딸린 조지아텍 논문이 실제 인터넷을 확인한 결과, 권한 서버의 99.7% 이상이 뒤섞인 대소문자를 올바르게 보존했다. 그렇지 못한 꼬리 — 모든 걸 소문자로 바꾸거나, 더 나쁘게는 일관성 없는 대소문자를 돌려주는 고집 센 로드밸런서와 어플라이언스의 얇은 조각 — 가 바로 0x20을 의무화할 수 없는 이유다. 전 세계적으로 켜서 강제하면, 그 1%도 안 되는 몫에 대해 해석이 깨진다. 이게 이것이 해킹으로 남은 이유이기도 하다: 표준은 모두에게 통해야 하지만, 해킹은 거의 모두에게만 통하면 되고 나머지에 대해서는 우아하게 저하되면 된다.
조용히, 켜지다
그렇게 몇 년을 그 자리에 있었다 — Unbound에 use-caps-for-id로 구현됐고, 기본값은 꺼짐, 이를 소화 못 하는 서버를 위한 도메인별 예외 목록과 함께, 예외를 돌볼 각오가 된 운영자들이 배포했다. 별표가 붙은 좋은 아이디어.
그러다 2023년 1월, Google Public DNS가 자사 리졸버 전반에 케이스 무작위화를 켰다고 발표했다. 오늘날 구글 리졸버가 권한 서버로 보내는 UDP 질의의 대다수가 뒤섞인 대소문자로 나간다. 어떤 표준도 그걸 밀어붙이지 않았다. 어떤 의무도 없었다. 지구상에서 가장 큰 리졸버 중 하나가 위험은 감내할 만하고 이득은 공짜라고 측정하고는, 스위치를 올렸다 — 그리고 이 메커니즘이 일방적이기에, 그 단 하나의 결정이 다른 누구도 손 하나 까딱하지 않은 채 세계 DNS 조회의 막대한 몫을 지킨다.
내가 0x20을 좋아하는 건 그것이 자신이 무엇인지에 대해 정직하기 때문이다. DNSSEC인 척하지 않는다. 아무것도 인증하지 않는다. 당신의 질의를 볼 수 있는 온패스(on-path) 공격자는 대소문자를 회선에서 그대로 읽어 내고, 그러면 이 모든 게 증발한다. 앞선 소스 포트 무작위화와 마찬가지로, 이건 과속방지턱이다 — 더 많은 엔트로피, 더 어려운 복권, 그 이상은 아니다. 하지만 글자당 1비트가 들고, 조율이 전혀 필요 없으며, 실패해도 “이전보다 나빠지지 않음”으로 저하되는 과속방지턱이다. 그 조합은 드물다. 대부분의 보안 개선은 다른 모두가 먼저 움직이길 요구하고, 그래서 하염없이 기다린다. 이것은 우리가 이미 써도 됐던 대문자를 달라고 했을 뿐이고 — 1987년 명세 속의 우연에서 15년 치 공짜 보호를 뽑아냈다.