[네트워크/보안] Website Fingerprinting이란? Website FingerPrinting 실습
2025. 5. 20. 00:05ㆍCS
Website Fingerprinting이 무엇인지 알아보고,
Website Fingerprinting이 Tor를 어떻게 우회하는지에 대해 간단하게 알아보도록 하겠습니다.
그리고 Website Fingerprinting을 Pycharm Jupyter NoteBook 환경에서
실습할 수 있는 있는 방법에 대해서 알아보겠습니다.
Pyshark를 통해서 패킷을 캡처하는 과정까지 실습하고,
이후 ML/DL로 모델 학습하는 과정은 데이터셋이 많이 필요하다는 한계가 있어
시뮬레이션 과정을 설명하도록 하겠습니다.
Tor가 기본적으로 제공하는 보호 기능
Tor는 사용자의 IP 주소와 접속 대상 웹사이트를 서로 분리하기 위해 다단계 중계 (onion routing) 를 사용합니다.
데이터는 3개의 릴레이 노드(Guard, Middle, Exit)를 거쳐 암호화되어 전송되므로, 각 노드는 전체 경로를 알 수 없으며 사용자의 IP나 목적지를 직접적으로 식별할 수 없습니다. 하지만 Tor를 통한다고 해서 완벽한 익명성이 보장되는 것은 아닙니다.
Website Fingerprinting(WF)이란?
- Website Fingerprinting은 공격자가 사용자의 암호화된 트래픽 패턴만 보고도 어떤 웹사이트를 방문했는지를 추측하는 사이드채널 공격입니다.
- 즉, Tor의 암호화 구조를 직접 깨는 것이 아니라, 암호화되어 있더라도 패킷의 크기, 순서, 타이밍 등을 통해 웹사이트를 식별하는 기법이에요.
“모든 택배 상자가 테이프로 포장돼 있고, 내용물은 안 보이지만, 상자의 크기와 무게, 배송 시간만으로 어떤 제품인지 유추할 수 있는 것과 같아요.”
어떻게 공격이 이루어질까?
- 공격자는 자신이 직접 Tor를 사용해 수많은 웹사이트에 접속하며 트래픽 패턴을 수집합니다.
- 이를 통해 각 웹사이트마다 고유한 패턴, 즉 "트래픽 지문" (fingerprint) 를 만들어요.
- 이후 공격자는 희생자의 실제 트래픽 패턴을 관찰하고,
- 수집된 지문 데이터와 비교해 어느 웹사이트를 방문했는지 머신러닝 등으로 예측합니다.
- 이 공격은 주로:
- Tor 네트워크의 입구(Gateway) 근처에서
- 혹은 ISP 수준에서 수행될 수 있어요.
WF이 가능한 이유는?
- Tor는 payload 내용(내용물) 은 암호화하지만,
- 패킷 크기,
- 패킷 간 시간 간격,
- 패킷 방향 (업로드인지 다운로드인지) 등은 숨기지 못하기 때문입니다.
이런 메타데이터가 공격자에겐 웹사이트를 식별하는 강력한 단서가 됩니다.
WF에 사용되는 기술
- 최근에는 단순한 통계 기반이 아니라 딥러닝 기반의 WF 모델이 활발히 연구되고 있어요.
- 예: ResNet, LSTM 등 사용
- 90% 이상의 정확도로 웹사이트 식별이 가능한 경우도 보고됨
- 즉, Tor는 전통적인 감청자로부터는 사용자를 잘 보호하지만, 패턴 기반 분석자에겐 완벽하지 않습니다!
라이브러리 설치

| selenium | 자동 브라우저 제어를 위한 라이브러리 (웹사이트에 자동 접속) |
| pyshark | .pcap 파일을 파싱하여 트래픽 패킷을 읽는 도구 (Wireshark 기반) |
| chromedriver | 크롬 브라우저를 selenium이 제어하기 위한 드라이버 |
| nest_asyncio | Jupyter 환경에서 asyncio 이벤트 루프 충돌을 방지 |
| --upgrade pip | pip 최신 버전으로 업그레이드 |
이벤트 루프 충돌 방지

- 실습과정에서 오류가 발생하였습니다.
- pyshark 내부적으로 asyncio를 사용하는데,
- Jupyter/PyCharm에서는 이미 이벤트 루프가 실행 중이어서 충돌 발생
- 따라서 이 문제를 기존 루프를 패치하여 문제를 해결하였습니다.
웹사이트 자동 접속 및 트래픽 생성

- Selenium이 크롬 브라우저를 자동으로 열고, 지정한 URL로 접속합니다.
- headless 옵션이 있기 때문에 실제로 화면은 보이지 않지만 트래픽은 생성됩니다.
- 이때 발생한 HTTPS 요청들은 tcpdump 또는 Wireshark를 사용해서 .pcap으로 캡처해둬야 합니다.
실제로 headless 옵션으로 화면은 보이지 않았지만, 이렇게 Dock에서 새로운 chrome 아이콘이 하나 뜨면서 코드가 제대로 실행되고 있음을 확인할 수 있었습니다.

tcpdump를 사용해 HTTPS 트래픽 캡처

| sudo | 관리자 권한으로 실행 (네트워크 인터페이스 접근은 루트 권한 필요) |
| tcpdump | 패킷 캡처 도구 |
| -i en0 | 네트워크 인터페이스 en0에서 트래픽을 감시 (보통 Mac의 Wi-Fi) |
| port 443 | HTTPS 트래픽만 필터링 |
| -w ~/Desktop/capture.pcap | 결과를 Desktop에 .pcap 형식으로 저장 |
- 처음에 Desktop에 저장하는 옵션을 넣지 않았더니 /Users/username 디렉토리에 읽기 형식으로 저장이 되어서, Desktop에 저장하는 옵션을 추가해주었습니다.
- Desktop에 쓰기 버전으로 저장하고, 이후 Pycharm 실습하는 디렉토리로 옮겨주었습니다.

| listening on en0... | en0 인터페이스에서 패킷 수집 중 |
| ^C | 수집 종료 (Ctrl + C로 중단됨) |
| 10012 packets captured | 실제로 저장된 패킷 수 (파일로 저장됨) |
| 10288 packets received by filter | 필터(port 443)에 의해 걸린 전체 패킷 |
| 0 packets dropped by kernel | 커널이 처리 중 놓친 패킷 없음 → 데이터 손실 없이 성공 |
캡처한 패킷 분석 (pyshark 사용)

- .pcap 파일에서 TLS 패킷들만 추출
- 패킷의 크기(length)와 방향성(+/-)을 조합해 시퀀스 형태의 feature 벡터 생성
- +: 내가 보낸 패킷, -: 내가 받은 패킷
웹사이트 핑거프린팅 머신러닝 학습 과정

현실적으로 수집이 어렵다는 점을 고려해, 여러분이 이미 캡처한 1개 세션만으로 설명을 진행하겠습니다.
1. 문제 정의
암호화된 HTTPS 트래픽만을 보고 사용자가 접속한 웹사이트가 무엇인지 분류한다.
- 입력: 패킷 시퀀스 (예: [+431, -210, -1024, +1490, ...])
- 출력: 웹사이트 클래스 (예: 0: github, 1: wikipedia)
2. 데이터 수집
- 실제 실험에서는 각 사이트마다 수십~수백 개의 세션을 수집해야하지만, 3개 웹사이트 × 각 2세션만 있다고 가정하도록 하겠습니다.
- 웹사이트 패킷 시퀀스 예시 (고정 길이 10으로 padding) 라벨
| github.com | [+400, -100, +300, 0, 0, 0, 0, 0, 0, 0] | 0 |
| github.com | [+350, -120, +280, -200, 0, 0, 0, 0, 0, 0] | 0 |
| wikipedia | [-300, +150, +500, 0, 0, 0, 0, 0, 0, 0] | 1 |
| wikipedia | [+200, -90, +100, -300, +90, 0, 0, 0, 0, 0] | 1 |
| naver.com | [+800, -250, -100, +50, 0, 0, 0, 0, 0, 0] | 2 |
| naver.com | [+900, -150, +50, -40, -100, 0, 0, 0, 0, 0] | 2 |
- → 이걸 X, y에 저장
X = [
[400, -100, 300, 0, 0, 0, 0, 0, 0, 0],
[350, -120, 280, -200, 0, 0, 0, 0, 0, 0],
[-300, 150, 500, 0, 0, 0, 0, 0, 0, 0],
[200, -90, 100, -300, 90, 0, 0, 0, 0, 0],
[800, -250, -100, 50, 0, 0, 0, 0, 0, 0],
[900, -150, 50, -40, -100, 0, 0, 0, 0, 0],
]
y = [0, 0, 1, 1, 2, 2]
3. 모델 학습
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
# 학습/검증 데이터 나누기
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33)
# 랜덤 포레스트 모델 정의 및 학습
model = RandomForestClassifier()
model.fit(X_train, y_train)
# 정확도 평가
accuracy = model.score(X_test, y_test)
print(f"정확도: {accuracy}")