콘텐츠로 이동

TLS 지문(JA3·JA3S·JARM·JA4)과 TLS 1.3, HTTP(S) 분석 정리

이 페이지에서 다루는 것

암호화를 풀지 않고도 "어떤 프로그램이 연결했는가"를 알아내는 TLS 지문(Fingerprint) — JA3와 JA3S가 무엇을 모아 어떻게 계산하는지 한 줄씩, 왜 JA3가 2023년 이후 흔들렸고 JA4가 그 자리를 이어받았는지, 서버를 직접 두드려 보는 JARM과 그 위험. 이어서 TLS 1.3이 보안팀에 무엇을 빼앗아 갔는지(인증서, 수동 해독, 다운그레이드, 곧 SNI까지), 그리고 HTTP(S) 분석 전체를 정리하는 점검 순서. 마지막에 확인 문제와 해설이 있습니다.

표기

[보충] = 꼭 알아야 할 실무 보충, [정정] = 흔히 잘못 알려진 내용 정정, [변경됨]/[주의] = 최근 바뀐 점과 옛 방식대로 하면 위험한 점. 앞 페이지 HTTPS — TLS 해독, 해독 없이 보는 법, 인증서 읽기에 이어집니다.

한눈에 보는 핵심 8가지

  1. 프로그램마다 TLS 연결을 시작하는 첫 인사(ClientHello)의 모양이 조금씩 다르다. 그 모양을 요약한 값이 TLS 지문이다.
  2. JA3 = 클라이언트 지문(ClientHello), JA3S = 서버 지문(ServerHello). 둘 다 몇 개 필드를 이어 붙여 MD5로 만든 32자리 값.
  3. 지문은 User-Agent처럼 스스로 적는 값이 아니라 프로그램이 만들어질 때 정해지는 값이라 속이기가 더 어렵다(불가능하지는 않다).
  4. 쓰는 법: 회사 안에서 흔한 지문 = 정상 기준선, 드문 지문과 위협 정보의 악성 지문 목록을 찾는다.
  5. [변경됨] 2023년 Chrome이 확장 순서를 매번 섞기 시작해 JA3가 크게 흔들렸다. 확장을 정렬해서 계산하는 JA4가 지금의 표준이다.
  6. JARM은 수동 관찰이 아니라 서버에 직접 접속해 보는(능동) 지문이다. 의심 서버가 C2 서버인지 확인할 수 있지만 우리 존재를 드러낸다(OPSEC).
  7. TLS 1.3: 인증서가 암호화되고, 서버 개인키로 나중에 푸는 수동 해독이 불가능해지고(완전 순방향 비밀성 강제), 낮은 버전으로 끌어내리는 다운그레이드가 막힌다. 남은 SNI도 ECH로 가려지는 중.
  8. HTTP(S) 분석 정리: 필드 → 내용 → 출발지·목적지·인증서 → 빈도와 맥락 → 공격 도구 지식, 여러 방법을 겹쳐 쓴다.

왜 TLS 지문이 필요한가

앞 페이지에서 본 것처럼 TLS를 풀지 못하면 경로, 헤더, User-Agent가 모두 안 보입니다. 그런데 연결을 시작하는 첫 메시지(ClientHello)는 암호화되지 않습니다. 아직 키를 정하기 전이라 암호화할 수 없기 때문입니다.

ClientHello에는 "나는 이 TLS 버전을 쓸 수 있고, 이런 암호 방식들을 이 순서로 지원하고, 이런 기능(확장)들을 원한다"는 목록이 들어 있습니다. 이 목록은 프로그램이 어떤 TLS 라이브러리로, 어떤 설정으로 만들어졌는지에 따라 정해집니다. 그래서 Firefox, Chrome, Python requests, Tor 클라이언트, 그리고 어떤 악성코드는 각자 조금씩 다른 ClientHello를 보냅니다.

비유하면 악수하는 방식입니다. 이름표(User-Agent)는 아무거나 써 붙일 수 있지만, 손을 내미는 각도, 쥐는 힘, 흔드는 횟수는 몸에 밴 습관이라 바꾸기가 더 어렵습니다. TLS 지문은 이 "악수 습관"을 짧은 값으로 요약한 것입니다.

JA3와 JA3S

JA3가 모으는 다섯 가지

JA3는 ClientHello에서 다섯 가지를 이 순서로 꺼냅니다.

순서 필드 뜻
1 TLS Version ClientHello에 적힌 버전 번호(10진수). 771 = 0x0303 = TLS 1.2
2 Cipher Suites 지원하는 암호 방식 목록, 보낸 순서 그대로
3 Extensions 원하는 기능(확장) 목록, 보낸 순서 그대로
4 Elliptic Curves (Supported Groups) 키 교환에 쓸 수 있는 곡선·그룹 목록
5 EC Point Formats 타원 곡선 점 표현 방식

규칙은 이렇습니다. 각 목록 안의 값은 10진수로 바꿔 -로 잇고, 다섯 묶음은 ,로 잇습니다. 그리고 GREASE 값은 뺍니다. GREASE는 Chrome 등이 "모르는 값이 와도 서버가 고장 나지 않는지" 시험하려고 일부러 섞어 넣는 의미 없는 값(0x0a0a, 0x1a1a 같은 모양)이라, 매번 달라져서 지문에 넣으면 안 됩니다.

직접 계산해 보기

예를 들어 어떤 클라이언트가 이렇게 보냈다고 합시다(설명용으로 줄인 가상의 예).

TLS Version        : 0x0303                      -> 771
Cipher Suites      : 0x1301 0x1302 0x1303 0xc02b 0xc02f
                                                 -> 4865-4866-4867-49195-49199
Extensions         : 0 10 11 13 16 43 45 51      -> 0-10-11-13-16-43-45-51
Supported Groups   : 0x001d 0x0017 0x0018        -> 29-23-24
EC Point Formats   : 0                           -> 0
  • 0x0303 -> 771 — TLS 1.3 클라이언트도 호환성 때문에 이 칸에는 TLS 1.2(0x0303)를 적습니다. 진짜 TLS 1.3 지원 여부는 확장 43(supported_versions) 안에 있습니다. 그래서 JA3의 버전 칸은 대부분 771입니다
  • 4865-...-49199 — 0x1301~0x1303은 TLS 1.3 암호 방식, 0xc02b·0xc02f는 TLS 1.2용 ECDHE-GCM 방식
  • 0-10-11-13-16-43-45-51 — 0 = SNI, 10 = supported_groups, 11 = ec_point_formats, 13 = signature_algorithms, 16 = ALPN, 43 = supported_versions, 45 = psk_key_exchange_modes, 51 = key_share
  • 29-23-24 — 29 = X25519, 23 = P-256, 24 = P-384
  • 0 — uncompressed(압축 안 함)

이어 붙이면 JA3 원문 문자열이 됩니다.

771,4865-4866-4867-49195-49199,0-10-11-13-16-43-45-51,29-23-24,0

이 문자열의 MD5가 JA3 해시입니다.

$ echo -n "771,4865-4866-4867-49195-49199,0-10-11-13-16-43-45-51,29-23-24,0" | md5sum
0c55049b7c3967e9b529134ee3c1bcc7  -
  • echo -n — 끝에 줄바꿈을 붙이지 않고 출력(줄바꿈이 들어가면 해시가 완전히 달라짐)
  • | md5sum — 그 문자열의 MD5 값을 계산
  • 결과 0c55049b... — 이 32자리가 로그에 남는 ja3 값

글자 하나만 달라져도(예: 곡선 하나가 추가되면) 해시는 완전히 다른 값이 됩니다. 이 점이 장점이자 약점입니다(아래 [변경됨]).

JA3S — 서버 쪽 지문

서버가 대답하는 ServerHello로 만든 지문입니다. 서버는 목록이 아니라 하나를 골라 답하므로 필드가 셋입니다.

JA3S string : 771,49199,65281-0-11-35
JA3S hash   : a2c05b7498f29a65608835b44035c0c5
  • 771 — 서버가 답한 버전
  • 49199 — 서버가 고른 암호 방식 하나(0xc02f)
  • 65281-0-11-35 — 서버가 답에 넣은 확장들

핵심은 서버의 답은 클라이언트의 질문에 따라 달라진다는 점입니다. 같은 서버도 Firefox에게와 악성코드에게 다르게 답할 수 있습니다. 그래서 JA3S는 혼자보다 JA3 + JA3S 쌍으로 볼 때 힘이 셉니다. "이 악성코드(JA3)가 이 C2 서버 프로그램(JA3S)과 이야기할 때만 나오는 조합"을 잡을 수 있기 때문입니다.

Zeek 로그에서 보기

Zeek에 JA3 패키지를 설치하면 ssl.log에 필드가 추가됩니다.

ssl.log:
  ts=2026-11-04T09:12:40Z  id.orig_h=10.1.8.23  id.resp_h=198.51.100.44  id.resp_p=443
  version=TLSv12  server_name=cdn-assets-sync.example
  ja3=6f1a9e0c3b5d7e2a4c8b0d1e3f5a7b9c  ja3s=d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9
  • ja3 — 이 PC의 프로그램이 보낸 ClientHello 지문
  • ja3s — 서버가 돌려준 ServerHello 지문
  • (해시 값은 설명용 가상 값입니다)

SOC에서 쓰는 법

  1. 기준선 만들기: 회사 전체 ssl.log에서 ja3 값별 개수를 셉니다. 회사가 쓰는 브라우저·업데이트 도구·보안 에이전트의 지문이 위쪽을 차지합니다. 표준 PC 이미지에서 직접 재현해 보면 "이건 우리 Chrome", "이건 우리 백업 에이전트" 하고 이름을 붙일 수 있습니다
  2. 드문 지문 찾기: 전체에서 한두 대 PC에서만 나오는 지문은 살펴볼 가치가 있습니다(단, 개발자 도구나 특이한 업무 앱일 수도 있음)
  3. 악성 목록 대조: 위협 정보에서 받은 악성코드·공격 도구 지문과 맞춰 봅니다
  4. 다른 신호와 겹치기: 드문 지문 + 새로 등록된 도메인 + 일정한 주기 + SNI 없음처럼 여러 신호가 한 연결에 모이면 우선순위를 올립니다

[주의] 지문은 "프로그램"이 아니라 "TLS 라이브러리와 설정"을 가리킵니다. 같은 Python 라이브러리를 쓰는 정상 스크립트와 악성코드는 지문이 같을 수 있고, 수많은 프로그램이 운영체제의 TLS 라이브러리(Windows의 Schannel 등)를 함께 써서 같은 지문을 냅니다. 그래서 지문 하나만으로 "악성"이라 판정하지 않습니다. 또 공격자는 브라우저의 지문을 흉내 내는 도구(TLS 라이브러리를 Chrome처럼 보이게 만드는 것)를 쓸 수 있습니다.

[보충] 예전에 쓰던 "JA3 → User-Agent 조회 사이트"는 이제 없습니다. 한때 접속한 브라우저의 JA3를 계산해 보여 주고 User-Agent와 짝지어 주던 공개 사이트가 있었지만 지금은 운영되지 않습니다. 지금은 JA4 데이터베이스(ja4db)나 위협 정보 플랫폼, 직접 만든 회사 기준선을 씁니다.

[변경됨] JA3가 흔들린 이유와 JA4

무슨 일이 있었나. 2023년 초 Chrome(그리고 Chromium 기반 브라우저)이 ClientHello의 확장 순서를 연결할 때마다 무작위로 섞기 시작했습니다. 원래 목적은 서버나 중간 장비가 특정 순서에 의존해 고장 나는 것을 막는 것이었지만, 결과적으로 JA3는 "보낸 순서 그대로" 이어 붙이므로 Chrome 하나가 연결마다 다른 JA3를 내게 됐습니다. 기준선이 무너진 것입니다. JA3를 만든 개발자는 원래 소속을 떠나 새 방식(JA4)을 만들었고, 원래 JA3 저장소는 더 이상 관리되지 않습니다.

JA4는 무엇이 다른가. JA4는 목록을 정렬한 뒤 계산하므로 순서가 섞여도 같은 값이 나옵니다. 또 사람이 읽을 수 있는 앞부분을 붙여서, 해시만 보고도 대략적인 성격을 알 수 있습니다. 모양은 a_b_c 세 부분입니다. 위의 같은 예를 JA4로 만들면 이렇게 됩니다.

JA4 = t13d0508h2_e133e205ac38_2f59b3fc5483
부분 값 뜻
a (읽을 수 있는 부분) t 전송 방식: t = TCP, q = QUIC, d = DTLS
13 실제 지원하는 가장 높은 TLS 버전(supported_versions 확장 기준) → 1.3
d SNI 있음(도메인으로 접속). i면 SNI 없음 = IP로 직접 접속
05 암호 방식 개수(GREASE 제외)
08 확장 개수(GREASE 제외)
h2 ALPN 첫 값의 첫 글자와 끝 글자 → h2(HTTP/2). 없으면 00
b e133e205ac38 암호 방식 목록을 정렬해 SHA256을 만든 뒤 앞 12자리
c 2f59b3fc5483 확장 목록(SNI·ALPN 제외)을 정렬하고 서명 알고리즘 목록을 붙여 SHA256 앞 12자리

앞부분 a만 봐도 많은 걸 압니다. 예를 들어 t12i...00이면 "TLS 1.2까지만, SNI 없이 IP로 직접, ALPN 없음" — 요즘 브라우저와는 전혀 다른 모양이라 바로 눈에 띕니다. 세 부분이 나뉘어 있어서 부분만으로도 사냥할 수 있습니다. 공격 도구가 암호 방식 하나만 바꾸면 b만 바뀌고 a와 c는 그대로라, a_c 조합으로 계속 따라갈 수 있습니다.

JA4+ 가족. 같은 방식이 다른 영역으로 넓어졌습니다.

이름 무엇의 지문
JA4 TLS 클라이언트(ClientHello)
JA4S TLS 서버(ServerHello) — JA3S의 후속
JA4H HTTP 클라이언트(헤더 구성·순서·쿠키·언어) — 해독된 HTTP나 평문 HTTP에서
JA4X X.509 인증서가 어떻게 만들어졌는지(필드 구성) — 같은 도구로 찍어 낸 인증서 묶기
JA4SSH SSH 세션의 패킷 모양(대화형 셸인지, 파일 전송인지 등)
JA4T / JA4TS TCP 클라이언트·서버(창 크기, TCP 옵션) — 운영체제·장비 성격
JA4L 왕복 지연으로 본 "거리"

라이선스도 알아 둘 만합니다. JA4(클라이언트)는 BSD 3-Clause로 자유롭게 쓸 수 있고, 나머지 JA4+는 FoxIO License 1.1입니다. 조직 내부 업무와 학술용으로는 쓸 수 있지만, 이를 넣어 파는 제품은 별도 라이선스가 필요합니다.

어디서 볼 수 있나. Wireshark, Zeek(JA4+ 패키지), Arkime, Security Onion이 JA4+를 지원하고, Suricata는 작업 중입니다. Cloudflare, AWS CloudFront·WAF, Akamai, Zscaler, VirusTotal 등은 JA4를 제공합니다. Zeek에 패키지를 설치하면 이런 식으로 남습니다.

ssl.log:
  id.orig_h=10.1.8.23  id.resp_h=198.51.100.44  id.resp_p=443
  server_name=cdn-assets-sync.example  version=TLSv13
  ja4=t13d0508h2_e133e205ac38_2f59b3fc5483  ja4s=t130200_1301_a56c5b993250
  • ja4 — 클라이언트 지문(위에서 만든 값)
  • ja4s — 서버 지문. t13(TCP, TLS 1.3) 02(확장 2개) 00(ALPN 없음) _1301(고른 암호 방식) _...(확장 해시)
  • (ja4s 해시는 설명용 가상 값입니다)

JARM — 서버를 직접 두드려 보는 지문

JA3·JA4는 지나가는 트래픽을 관찰하는 수동(Passive) 방식입니다. JARM은 다릅니다. 분석가가 서버에 직접 접속해서, 일부러 다르게 만든 ClientHello 10개를 보내고 서버가 각각에 어떻게 답하는지(어떤 버전·암호 방식을 고르는지, 어떤 확장을 돌려주는지)를 모아 62자리 지문을 만듭니다.

$ python3 jarm.py c2-panel.example
Domain: c2-panel.example
Resolved IP: 203.0.113.91
JARM: 2ad2ad16d2ad2ad22c42d42d00042d58c7162162b6a603d3d90a2b76865b53
  • jarm.py <host> — JARM 도구가 그 서버에 10번 접속해 봄
  • 앞 30자리 — 10번의 답 각각에서 서버가 고른 암호 방식·버전을 3자리씩 요약
  • 뒤 32자리 — 서버가 돌려준 확장들을 모아 만든 해시
  • (지문 값은 설명용 가상 값입니다)

왜 쓰나. 서버의 TLS 답하는 방식은 서버 프로그램과 설정에 따라 정해집니다. 공격자의 C2 서버 프로그램은 기본 설정 그대로 쓰면 특유의 JARM을 냅니다. 그래서 의심 서버의 JARM을 알려진 C2 지문과 맞춰 보거나, 인터넷 스캔 데이터(Censys·Shodan 등)에서 같은 JARM을 가진 다른 서버를 찾아 공격자 인프라를 넓혀 볼 수 있습니다.

[주의] JARM은 우리를 드러냅니다(OPSEC). 직접 접속하는 순간 공격자 서버 로그에 우리 IP가 남고, 공격자는 "누가 우리 서버를 조사한다"는 걸 알 수 있습니다. 회사 IP와 회사 PC에서 절대 하지 않습니다. 필요하면 격리된 분석 환경과 출처를 숨긴 회선을 쓰거나, 이미 인터넷 스캔 업체가 수집해 둔 JARM 값을 조회하는 쪽이 안전합니다.

[주의] JARM 하나만으로 판정하지 않습니다. 많은 C2 서버가 Java나 Go, nginx 같은 흔한 TLS 스택 위에서 돌아가서, 같은 JARM을 정상 서버 수천 대가 공유하기도 합니다. JARM은 "후보를 좁히는" 도구이지, 단독 증거가 아닙니다.

TLS 1.3이 바꾼 것

TLS 1.3은 사용자 보안을 크게 높였지만, 보안팀의 기존 방법 여러 개를 무너뜨렸습니다. 하나씩 봅니다.

1. 인증서가 암호화된다

TLS 1.2에서는 서버 인증서가 평문으로 지나가서 앞 페이지처럼 Subject, Issuer, 기간을 읽을 수 있었습니다. TLS 1.3에서는 ServerHello 직후부터 바로 암호화되고, 인증서는 그 암호화된 부분 안에서 전달됩니다. 해독 없이 인증서를 읽는 방법은 TLS 1.3 연결에서 통하지 않습니다.

TLS 1.2                              TLS 1.3
ClientHello        (clear)           ClientHello        (clear: SNI, JA4)
ServerHello        (clear)           ServerHello        (clear: JA4S)
Certificate        (clear) ← 보임    {EncryptedExtensions}  (encrypted)
ServerKeyExchange  (clear)           {Certificate}          (encrypted) ← 안 보임
...                                  {Finished}             (encrypted)

2. 서버 개인키로 나중에 푸는 "수동 해독"이 불가능

TLS 1.2 이하에는 RSA 키 교환이라는 방식이 있었습니다. 이 방식에서는 세션 키가 서버의 개인키로 잠겨 전달되므로, 서버 개인키를 가진 장비(또는 Wireshark)는 녹화해 둔 트래픽을 나중에 풀 수 있었습니다. 자기 회사 웹서버로 들어오는 트래픽을 IDS가 옆에서 조용히 푸는 방식이 이것이었습니다.

TLS 1.3은 이 방식을 아예 없애고 완전 순방향 비밀성(Perfect Forward Secrecy, PFS)을 강제합니다. 연결할 때마다 양쪽이 일회용 키(ECDHE)를 새로 만들고 쓰고 버리기 때문에, 서버 개인키가 있어도 지나간 트래픽을 풀 수 없습니다. 비유하면 예전엔 마스터키 하나로 모든 금고를 열 수 있었는데, 이제는 금고마다 한 번 쓰고 녹여 버리는 열쇠를 씁니다.

남은 방법은 두 가지입니다.

  • 연결 가운데에 서서 직접 끝을 맺는 방식(능동 프록시·TLS 검사 장비) — 앞 페이지의 방식
  • 엔드포인트에서 세션 키를 꺼내는 방식 — 실험실이나 사고 분석에서는 브라우저가 세션 키를 파일로 남기게 하는 SSLKEYLOGFILE 환경 변수를 쓰고, 그 파일을 Wireshark에 넣어 풉니다. 서버 쪽은 로드밸런서처럼 TLS를 끝맺는 지점에서 로그를 남깁니다
# Lab/IR use only, on a machine you control
$ export SSLKEYLOGFILE=$HOME/tls-keys.log
$ firefox &
# Wireshark: Preferences > Protocols > TLS > (Pre)-Master-Secret log filename = ~/tls-keys.log
  • export SSLKEYLOGFILE=... — 이 변수가 있으면 Firefox·Chrome 등이 연결마다 세션 키를 그 파일에 적음
  • Wireshark에 그 파일을 지정하면 같은 시간에 캡처한 TLS 트래픽이 풀려서 HTTP 내용이 보임
  • [주의] 이 파일은 그 기간의 모든 암호화 통신을 여는 열쇠입니다. 분석이 끝나면 지우고, 업무 PC에 상시로 켜 두지 않습니다. 공격자가 이 변수를 몰래 설정해 키를 훔치는 경우도 있으므로, 사용자 환경 변수에 SSLKEYLOGFILE이 설정돼 있으면 오히려 조사 대상입니다

3. 검사 장비가 "중간에 빠지기"가 어려워진다

TLS 1.2 시절 일부 검사 장비는 평문으로 지나가는 서버 인증서를 보고 "이건 은행이니 풀지 말자"를 정한 뒤 연결에서 빠질 수 있었습니다. TLS 1.3에서는 인증서가 암호화되므로 그 판단을 인증서로 할 수 없습니다. 장비가 쓸 수 있는 건 ClientHello의 SNI 정도이고, 그보다 더 알려면 처음부터 끝까지 연결 가운데에 서 있어야 합니다. 한번 끼어들면 중간에 슬쩍 빠질 수 없습니다(이미 양쪽 키가 서로 다르게 정해졌기 때문).

4. 낮은 버전으로 끌어내리기(다운그레이드)가 막힌다

"TLS 1.3이 불편하니 강제로 TLS 1.2로 연결하게 만들자"는 꼼수도 막혔습니다. TLS 1.3을 지원하는 서버가 더 낮은 버전으로 답할 때는 ServerHello의 무작위 값 끝 8바이트에 특별한 표시(DOWNGRD라는 글자에 해당하는 값)를 넣고, TLS 1.3 클라이언트는 이 표시를 보면 "누가 중간에서 버전을 끌어내렸다"고 판단해 연결을 끊습니다.

5. 남은 SNI도 ECH로 가려진다

TLS 1.3에서도 ClientHello의 SNI는 아직 평문이라 접속하려는 도메인은 보입니다. 하지만 ECH(Encrypted Client Hello)가 2026년 RFC 9849로 표준이 되면서, 지원하는 사이트·브라우저에서는 진짜 SNI가 암호화되고 겉으로는 CDN의 공용 이름만 보이게 됩니다(ECH). 그러면 해독 없이 남는 건 IP·ASN, 포트, 양·시간·주기, 그리고 JA4 같은 지문 정도가 됩니다.

보이는 것 TLS 1.2 TLS 1.3 TLS 1.3 + ECH
IP·포트·양·시간 O O O
SNI(도메인) O O 겉 이름만(CDN)
클라이언트·서버 지문(JA4·JA4S) O O 일부(겉 ClientHello 기준)
서버 인증서 O X X
서버 개인키로 수동 해독 RSA 키 교환이면 O X X
HTTP 경로·헤더·본문 X(해독 없이) X X

HTTP(S) 분석 정리 — 점검 순서

HTTP와 HTTPS 공격은 "이 규칙 하나면 100% 잡힌다"는 게 없습니다. 하지만 악성 요청은 꼼꼼히 보면 거의 언제나 어딘가 이상한 점이 있습니다. 필드 값이 이상하거나, 요청의 성격이 이상하거나(수상한 곳에서 평범한 이름의 파일을 반복 요청), 필요 없는 곳에 인코딩이 쓰였거나, 자체 서명 인증서이거나 하는 식입니다. 그래서 정상 HTTP가 어떻게 생겼는지 몸에 익히는 것이 가장 중요합니다. 정상을 알아야 이상이 보입니다.

이 모듈 전체에서 배운 것을 점검 순서로 묶으면 이렇습니다.

  1. 요청·응답 필드 — 메서드, URL, Host, User-Agent, Referer, Cookie, 상태 코드, Content-Type, Server, X-Forwarded-For(요청 읽기, 응답)
  2. 요청·응답 내용 — 본문의 파일 형식(매직 바이트), Base64·인코딩, 다운로드된 파일의 정체(Base64, 파일과 빈도)
  3. 출발지·목적지와 신원 — 도메인 나이와 평판, IP·ASN, SNI, 인증서, TLS 지문(평판, 인증서, 이 페이지)
  4. 빈도와 맥락 — 몇 대가, 얼마나 자주, 어떤 주기로, 사용자가 그 시간에 무엇을 하고 있었나(비콘 분석)
  5. 공격 도구와 방법 알기 — 익스플로잇 킷, 웹셸, Cobalt Strike 프로필, ClickFix·AiTM 같은 최신 수법(익스플로잇 킷)

최고 수준의 공격 도구(예: 잘 꾸민 Cobalt Strike 프로필)는 이 모든 것을 정상처럼 보이게 만들 수 있습니다. 그래서 한 가지 방법에 기대지 않고 여러 방법을 겹쳐 쓰고, 경보를 기다리기만 하지 않고 먼저 찾아 나서는 위협 사냥(Threat Hunting)이 필요합니다.

[2026 현재] 그대로 유효한 것

"ClientHello는 평문이라 지문을 만들 수 있다", "지문은 기준선 + 드문 값 + 악성 목록으로 쓴다", "JARM 같은 능동 조사는 OPSEC 비용이 있다", "TLS 1.3은 인증서를 숨기고 수동 해독을 막고 다운그레이드를 막는다", "정상을 알아야 이상이 보이고, 여러 방법을 겹쳐야 한다"는 그대로입니다.

[변경됨] 바뀐 것

  • JA3 → JA4. Chrome의 확장 순서 무작위화(2023) 이후 JA3는 Chrome 계열에서 기준선으로 쓰기 어려워졌습니다. 원래 JA3 저장소는 관리가 끝났고, 개발자가 이어서 만든 JA4+가 Wireshark, Zeek, Arkime와 주요 CDN·클라우드 WAF에 들어가 사실상 표준이 됐습니다. 오래된 위협 정보의 JA3 목록은 지금도 쓸 수는 있지만(악성코드는 순서를 섞지 않는 경우가 많음), 새로 만드는 탐지는 JA4 기준으로 합니다.
  • JARM도 정체. 원래 JARM 도구는 활발히 관리되지 않고, 능동 TLS 서버 지문은 JA4+ 가족(JA4Scan-TLS·QUIC)과 인터넷 스캔 업체의 데이터로 옮겨 가는 중입니다.
  • 양자 내성 키 교환이 기본이 됐습니다. 주요 브라우저는 2024~2025년부터 기존 X25519에 양자 내성 알고리즘 ML-KEM을 섞은 하이브리드 키 교환(X25519MLKEM768)을 기본으로 씁니다. 그래서 ClientHello가 커져 패킷 여러 개로 나뉘어 가고, 이를 제대로 처리하지 못하는 오래된 IDS·프록시는 SNI나 지문을 놓치거나 연결을 깨뜨립니다. JA3로 계산하면 곡선 목록에 새 값(4588)이 붙어 해시가 바뀌므로, 오래된 JA3 기준선은 다시 만들어야 합니다.
  • ECH가 표준(RFC 9849, 2026)이 됐습니다. CDN 뒤의 사이트부터 SNI가 가려지고 있습니다. 보안 정책으로 ECH를 끄거나(관리 브라우저 정책, DNS의 HTTPS 레코드 차단) 검사 장비에서 처리하는 조직이 늘고 있습니다.
  • QUIC(HTTP/3)용 지문. JA4의 첫 글자 q처럼 QUIC 연결도 지문을 만듭니다. 다만 많은 조직은 여전히 UDP 443을 막아 TCP로 되돌립니다.

[주의] 지금 그대로 하면 틀리거나 위험한 것

  • JA3 해시 하나로 브라우저를 판정하거나 "Chrome인데 JA3가 매번 다르다 = 수상"이라고 보지 않습니다. 확장 순서 무작위화 때문에 정상입니다. JA4로 봅니다.
  • 지문만으로 악성 판정하지 않습니다. 공유 TLS 라이브러리, 브라우저 흉내 도구 때문에 오탐과 미탐이 모두 생깁니다. 다른 신호와 겹쳐 씁니다.
  • 회사 IP에서 의심 서버에 JARM·스캔을 하지 않습니다.
  • "TLS 1.2로 강제로 낮추면 다시 보인다"는 방법은 쓰지 않습니다. TLS 1.3은 다운그레이드를 감지하고 끊으며, 보안 수준도 떨어뜨립니다.
  • SSLKEYLOGFILE을 업무 PC에 상시로 켜 두지 않습니다. 사용자 환경에 이 변수가 설정돼 있다면 공격 흔적일 수 있으니 확인합니다.

확인 문제

문제 1

회사 전체 ssl.log에서 지난 7일 JA4 값을 세었더니 다음과 같습니다. 무엇을 먼저 조사하고, 왜 그렇습니까?

count   hosts  ja4                                   top server_name
412093  1830   t13d1516h2_8daaf6152771_02713d6af862  (many)
 98211  1822   t13d1517h2_8daaf6152771_b1ff8ab2d16f  (many)
  5120    44   t13d1715h2_5b57614c22b0_3d5424432f57  update.vendor-sync.example
  1440     1   t12i0806h2_9a4f0c1e7d22_6e0b8a2f1c3d  -
정답과 해설

마지막 줄(1440건, PC 1대)을 먼저 조사합니다.

  • 위 두 줄은 거의 모든 PC(1830·1822대)에서 나오는 값 → 회사 표준 브라우저 같은 기준선
  • 세 번째 줄은 44대가 한 업데이트 도메인에만 쓰는 값 → 특정 업무 도구일 가능성. 나중에 확인해 이름을 붙임
  • 네 번째 줄은 a 부분이 t12i0806h2:
    • t12 — TLS 1.2까지만. 요즘 브라우저는 1.3
    • i — SNI 없음 = IP로 직접 접속(top server_name도 -)
    • 0806 — 암호 방식 8개, 확장 6개로 브라우저보다 훨씬 단순
    • PC 1대에서만, 7일간 1440건 = 하루 약 206건, 대략 7분에 한 번 꼴로 일정

드문 지문 + SNI 없음 + 일정한 반복이 한 연결에 모였으므로 C2를 의심합니다. 다음: 그 PC와 목적지 IP 확인, 연결 주기 분석, 엔드포인트에서 연결한 프로세스 찾기(Sysmon Event ID 3 — 네트워크 연결, 어떤 프로그램이 어디로 연결했는지 기록), IP 평판과 인증서(TLS 1.2이므로 x509.log에 남아 있을 수 있음) 확인.

문제 2

동료가 "의심 IP 203.0.113.91이 C2 서버인지 JARM으로 바로 확인해 보자"며 업무 PC에서 jarm.py를 실행하려 합니다. 무엇이 문제이고, 어떻게 하는 게 맞습니까?

정답과 해설

문제: JARM은 서버에 직접 10번 접속하는 능동 조사입니다. 업무 PC와 회사 IP에서 하면 공격자 서버 로그에 우리 IP가 남고, 공격자는 조사당하고 있음을 알아채 인프라를 바꾸거나 흔적을 지우거나 행동을 서두를 수 있습니다(OPSEC 노출). 또 공격자 서버는 조사자의 접속에 악성 응답을 줄 수도 있습니다.

맞는 방법:

  1. 먼저 수동 데이터부터 — 인터넷 스캔 업체(Censys·Shodan 등)나 위협 정보 플랫폼에 이미 수집된 그 IP의 JARM·인증서·JA4S를 조회
  2. 능동 조사가 꼭 필요하면 격리된 분석 환경과 회사와 연결되지 않는 회선에서, 조직의 승인 절차를 따라 실행
  3. 결과 JARM이 알려진 C2 지문과 맞아도 단독 증거로 쓰지 않음 — 흔한 TLS 스택(Java·Go·nginx)을 쓰는 정상 서버와 겹칠 수 있음. 인증서, 도메인 나이, 회사 내 연결 패턴과 함께 판단

문제 3

팀장이 "TLS 1.3 트래픽이 늘었으니, 웹서버 개인키를 IDS에 넣어서 들어오는 HTTPS를 예전처럼 옆에서 조용히 풀자"고 합니다. 이 계획이 왜 통하지 않는지, 대신 무엇을 할 수 있는지 설명하세요.

정답과 해설

통하지 않는 이유: 예전 방식은 TLS 1.2 이하의 RSA 키 교환에서만 가능했습니다. 그때는 세션 키가 서버 개인키로 잠겨 전달됐기 때문에, 개인키를 가진 IDS가 복사본 트래픽을 풀 수 있었습니다. TLS 1.3은 RSA 키 교환을 없애고 완전 순방향 비밀성(PFS)을 강제합니다. 연결마다 일회용 키(ECDHE)를 새로 만들어 쓰고 버리므로, 서버 개인키가 있어도 옆에서 본 트래픽을 풀 수 없습니다. TLS 1.2라도 요즘은 대부분 ECDHE를 쓰므로 마찬가지입니다. "TLS 1.2로 낮추자"도 다운그레이드 보호 때문에 안 되고 보안만 약해집니다.

대신 할 수 있는 것:

  1. TLS를 끝맺는 지점에서 보기 — 로드밸런서·리버스 프록시·WAF가 TLS를 풀고 뒤쪽 서버로 넘기므로, 그 지점의 로그(URL, 헤더, 상태 코드)를 SIEM으로 보내거나 그 뒤쪽 평문 구간에 센서를 둠
  2. 웹서버 자체 로그와 엔드포인트 — 웹서버 접근 로그, WAF 로그, 서버의 EDR(웹서버 프로세스가 셸을 띄우면 Windows Event ID 4688 — 새 프로세스 생성 — 이나 Sysmon Event ID 1에 남음)
  3. 해독 없이 보이는 것 — 클라이언트 JA4, IP·ASN, 요청 양과 주기