JA3, JA4: 당신의 TLS 핸드셰이크는 지문이다

TLS가 무엇을 암호화하기도 전에, 클라이언트는 자기가 무엇인지 정확히 밝힌다. 인사하는 그 특유의 방식이 곧 지문이다 — JA3와 JA4의 이야기는, 아무도 감출 생각을 하지 않았던 프로토콜의 부위에서 신원이 어떻게 새어 나오는지에 관한 이야기다.

브라우저가 HTTPS 사이트에 접속할 때 가장 먼저 보내는 것은 ClientHello라는 메시지인데, 이건 평문으로 나간다 — 키가 합의되기 전에, 단 한 바이트도 암호화되기 전에. 그럴 수밖에 없다. 암호화는 협상이고, 상대가 아직 읽지 못하는 언어로는 협상할 수 없으니까. 그래서 ClientHello는 내 클라이언트가 할 수 있는 모든 것의 평문 목록이다: 원하는 TLS 버전, 지원하는 암호 스위트, 쓰는 확장(extension), 아는 타원곡선. 메뉴판처럼 읽힌다.

여기서 중요해지는 부분. 모든 소프트웨어는 그 메뉴를 조금씩 다르게 짠다. 크롬은 특정한 암호 집합을 특정한 순서로, 특정한 확장 목록과 함께 내민다. 파이어폭스는 다른 집합을 내민다. curl은 훨씬 짧은 걸 내민다. 파이썬 requests 스크립트, Go 프로그램, C2 서버로 신호를 보내는 악성코드 — 저마다 빌드된 TLS 라이브러리가 새겨 넣은, 자기만의 별난 인사법이 있다. 즉 인사의 모양이 소프트웨어를 식별한다. 신원을 증명하기 전에, 뭔가를 요청하기 전에, 아무것도 암호화되기 전에.

이것이 TLS 핑거프린팅의 발상 전체이고, 보안에서 되풀이되는 한 가지 진실의 꽤 깔끔한 사례다: 사람은 무엇을 말하느냐만이 아니라 어떻게 말하느냐로 식별된다.

JA3: 인사를 해시하다

2017년, 세일즈포스의 엔지니어 셋 — John Althouse, Jeff Atkinson, Josh Atkins — 이 그 관찰을 실제로 매칭할 수 있는 무언가로 바꾸는 방법을 발표했다. 자기들 이니셜을 따 JA3라 불렀다.

방법은 거의 공격적일 만큼 단순하다. ClientHello에서 다섯 필드를 뽑는다 — TLS 버전, 암호 스위트 목록, 확장 목록, 타원곡선, 타원곡선 점 형식. 각각을 숫자 값으로 적고 쉼표로 잇는다. 클라이언트 능력의 길고 긴 지문 같은 문자열이 나온다. 그 전체에 MD5를 돌린다. 32자 16진수 해시가 떨어진다. 그게 JA3 지문이고, 그 필드들의 조합이 달라질 때마다 다른 지문이 나온다.

강력했던 이유는, 지문이 IP 주소나 쿠키나 클라이언트가 애플리케이션 계층에서 통제하는 무엇에도 묶이지 않는다는 점이었다. 지문은 소프트웨어의 속성이다 — TLS 라이브러리와 그 설정 방식의. 봇넷 운영자가 IP 만 개를 돌려 쓸 수는 있어도, 노드가 전부 같은 도구로 빌드됐다면 들어올 때 모두 똑같은 JA3 해시를 속삭였다. 악성코드 패밀리들은 놀랍도록 안정적이고 뚜렷한 지문을 가진 것으로 드러났는데, 작성자들이 TLS 스택을 좀처럼 건드리지 않았기 때문이다. 방어자는 알려진 악성 샘플에서 JA3를 하나 뽑아, 네트워크 전체에서 그와 일치하는 다른 모든 접속을 — 아무것도 복호화하지 않고 — 사냥할 수 있었다.

침입 탐지 시스템, 봇 탐지 서비스, CDN 엣지 로직 — 사방에서 채택됐다. 몇 년간 JA3는 “이 클라이언트가 자기 주장대로인가”의 조용한 일꾼이었다.

그것을 죽인 필드

JA3에는 결함이 있었고, 그건 한 단어에 숨어 있었다: 순서.

JA3는 확장을 ClientHello에 나타난 순서 그대로 읽고 그 순서로 해시했다. 같은 확장을 재배열하면 완전히 다른 MD5가 나온다. 여러 해 동안 이건 문제가 아니었다. 클라이언트가 확장을 고정된 순서로 보냈고, 그 순서가 지문을 특정하게 만드는 요소의 일부였으니까.

그런데 구글이 그 고정된 순서를 문제로 여겼다. 핑거프린팅 문제가 아니라 — 경직화(ossification) 문제로. 수백만 클라이언트가 늘 같은 순서로 필드를 보내면, 미들박스와 서버가 슬금슬금 그 순서에 의존하기 시작하고, 프로토콜이 굳어 버린다: 인터넷을 깨뜨리지 않고서는 더 이상 그걸 바꿀 수 없게 된다. 구글은 이미 GREASE(RFC 8701)로 같은 전투를 치렀다. 크롬이 일부러 무작위 예약값을 ClientHello에 쑤셔 넣어, 서버가 모르는 값도 견디도록 강제해 생태계를 유연하게 유지하는 방식이다. 확장 순서를 랜덤화하는 건 다른 관절에 놓는 같은 약이었다.

그래서 2023년 초, 크로미엄 110에 실려(108·109 빌드를 통해 좀 더 일찍 야생에 새어 나오면서) 크롬은 접속할 때마다 TLS 확장 순서를 섞기 시작했다. Fastly가 그 순간을 지켜봤다: 2023년 1월 20일 무렵부터, 옛날의 안정적인 JA3를 달고 도착하는 크롬 클라이언트의 비율이 절벽처럼 떨어졌다.

JA3에게 이 계산은 잔혹하다. 크롬의 ClientHello는 확장을 대략 열다섯 개 나르고, 열다섯 개를 배열하는 경우의 수는 15 팩토리얼 — 약 1조 3천억이다. 실질적으로, 요즘 크롬의 모든 접속은 이제 유일한 JA3 해시를 낸다. JA3를 특정하게 만든 바로 그 성질 — 순서에 대한 민감성 — 이, 순서가 무작위가 되는 순간 JA3를 산산조각 낸 것이다. 방문할 때마다 새 지문을 주는 핑거프린팅 기법은 더 이상 핑거프린팅 기법이 아니다.

JA4: 해시하기 전에 정렬하라

해법은 다시 John Althouse에게서 나왔다 — 같은 사람이, 이제 자기 회사 FoxIO에서 — 2023년에 JA4+ 스위트를 발표했다. JA4는 첫 버전이 어떻게 죽는지 이미 알고서 만드는 물건이다.

핵심 수리는 뒤늦게 보면 뻔하다: 해시하기 전에 목록을 정렬하라. JA4는 암호 스위트와 확장을 표준 숫자 순서로 놓고 그다음 해시한다. 이제 클라이언트가 어떤 순서로 보냈는지는 상관없다 — 섞였든 아니든, 같은 밑바탕 집합은 같은 지문으로 수렴한다. JA3를 증발시킨 크롬의 랜덤화는 JA4에는 아무 짓도 못 한다. 그리고 하는 김에, JA4는 어디서 나타나든 GREASE 값을 무시해서 그 무작위 예약 항목들이 결과를 오염시키는 것도 멈춘다.

JA4는 또 불투명한 덩어리이길 그만뒀다. MD5 뭉갬 하나 대신, JA4 지문은 a_b_c 모양의 사람이 읽을 수 있는 세 부분을 가진다. 실제 예를 보자:

t13d1516h2_8daaf6152771_e5627efa2ab1

첫 구간 t13d1516h2는 그대로 읽힌다: t는 TCP 위의 TLS(QUIC이면 q), 13은 TLS 1.3, d는 서버 이름(SNI)이 있음, 15개 암호 스위트, 16개 확장, 그리고 h2 — 첫 ALPN 값, 즉 클라이언트가 HTTP/2를 제안했다는 뜻이다. 둘째 구간은 정렬된 암호 목록의 잘린 SHA-256이고, 셋째는 정렬된 확장에 서명 알고리즘을 더한 것의 잘린 SHA-256이다. 부분이 분리돼 있으니 전체를 정확히 맞출 필요 없이 읽히는 접두사만으로, 혹은 암호 해시만으로 사냥할 수 있다. 따져 볼 수 있는 지문인 것이다.

그리고 JA4는 한 가족의 TLS 조각일 뿐이다. 스위트는 같은 발상을, 신원을 흘리는 연결의 다른 부위로 확장한다: 서버가 응답하는 방식은 JA4S, HTTP 요청의 모양은 JA4H, X.509 인증서의 필드는 JA4X, 지연 시간은 JA4L, TCP 계층은 JA4T. 통찰이 일반화된 것이다. 연결의 거의 모든 계층에는 저마다 어쩔 수 없는 억양이 있다.

그 밑에 깔린 정찰의 교훈

버전 번호를 걷어내면, TLS 핑거프린팅은 신원이 실제로 어떻게 새는가에 관한 진술이고, 인증서 투명성패시브 DNS가 다른 각도에서 하는 것과 같은 진술이다: 어떤 시스템에 관한 가장 오래가는 정보는, 그것이 어쩔 수 없이 내뿜는 배기가스에서 나온다.

ClientHello를 보내지 않고는 TLS 연결을 열 수 없고, 내 TLS 스택 특유의 방언을 드러내지 않고는 ClientHello를 보낼 수 없다. 벗겨낼 수 있는 헤더도, 지울 수 있는 쿠키도 아니다. 구조적이다. 관찰자는 나를 건드릴 필요도, 무엇을 복호화할 필요도, 내가 로그인하기를 기다릴 필요도 없다 — 식별 신호는 첫 음절에, 어쩔 수 없이, 평문으로 실려 있다.

그래서 뒤따른 군비 경쟁은 지극히 예측 가능하다. 반대편에 있다면 — 스크래퍼, 침투 테스터, 브라우저처럼 보여야 하는 자동화라면 — 내 Go 프로그램의 정직한 지문이 모든 CDN에 “봇”이라고 외치는 걸 원치 않는다. 그래서 uTLS나 curl-impersonate 같은 도구가 등장해, 클라이언트가 크롬의 정확한 인사를 암호 하나하나, 확장 하나하나 위조하게 해준다. 방어자의 응답은 모순을 찾는 것이다: 크롬의 JA4를 주장하면서 HTTP 행동이나 TCP 타이밍, JA4H가 다른 이야기를 하는 클라이언트를. 다른 소프트웨어의 억양을 걸칠 수는 있어도, 모든 계층의 이야기를 동시에 앞뒤 맞게 유지하기는 어렵다. 잘 만든 위조의 지문은, 한 곳에서는 너무 완벽하고 다른 곳에서는 틀렸다는 것이다.

분명히 짚어둘 한계가 있다. 핑거프린팅은 과대 포장되기 쉬우니까: TLS 지문은 사람이 아니라 소프트웨어를 식별한다. 수백만 명이 같은 크롬 빌드를 돌리며 지문을 공유한다. 이름이 아니라 범주다. “이건 진짜 크롬이다” 혹은 “이건 파이썬이 흉내 내는 것이다”라고 말해주며, 사람과 자동화를 가르는 데는 정말로 유용하다. 하지만 그 자체로 내가 누구인지 알려주진 않는다. 프라이버시 질문이 날카로워지는 건 지문이 더 넓은 표의 한 열이 될 때다 — 내 IP, 타이밍, 행동과 결합될 때, 전체는 부분의 합보다 더 식별력이 있다.

그러나 새겨둘 핵심 사실은 하나다. 요청을 보내기 한참 전에, 자물쇠가 닫히기 전에, 무엇이 비밀이 되기 전에, 내 클라이언트는 이미 인사하는 그 특유의 방식으로 자기를 소개해 버렸다. 늘 그래왔다. JA3와 JA4는 그 억양에 귀 기울이는 법을 배운 도구일 뿐이다.

토론 참여

← 블로그로 돌아가기