Step 158. 패킷 스니핑 심화 — 필터 두 얼굴과 대화 재조립

Step 158. 패킷 스니핑 심화 — 필터 두 얼굴과 대화 재조립

Level 2 — 보안 입문과 공격 스킬 기초 | 난이도 ★★★☆☆ | 예상 소요 시간 3시간

전제: Step 83~85(캡처, 필터, HTTP/HTTPS 비교)와 Step 157(평문 자격 증명 노출)을 마쳤다. tshark로 pcap을 읽을 수 있다.

  • 준비물: WSL 리눅스 + tshark, 파이썬 3 + Flask(평문 로그인 서버용), 터미널 2개.
  • ⚠️ 이 챕터의 실습은 내 랩·합법 플랫폼 전용입니다. 허가 없는 시스템에 적용하면 범죄입니다. 오늘 캡처의 범위는 내 컴퓨터 안(lo, 루프백)뿐입니다.
  • 실측 안내: 이 챕터의 모든 출력은 2026-09-09에 WSL 리눅스(TShark 4.2.2, Flask 3.1, curl 8.5)에서 실측했습니다.

지금까지는 "캡처하고, 필터 걸고, 패킷을 읽는" 기본기를 배웠습니다. 오늘은 분석가의 기술 세 개를 배웁니다 — 캡처 필터와 표시 필터의 구분(잡을 때 거르기 vs 잡은 뒤 보기), TCP 스트림 재조립(흩어진 패킷을 대화 하나로 꿰기), 대화 통계(누가 누구와 얼마나 말했는지 지도 그리기).

이 세 개가 갖춰지면 대용량 캡처가 두렵지 않습니다. 수만 패킷 속에서 "로그인 한 건"을 정확히 뽑아내고, 그 대화를 처음부터 끝까지 통째로 읽는 것 — 오늘 실습의 목표 지점입니다. 마지막에는 평문 HTTP 로그인을 일부러 일으켜, 비밀번호가 스트림 재조립 한 번에 통째로 읽히는 것을 확인합니다.


1. 학습 목표

이 챕터를 끝내면 다음을 할 수 있습니다:

  • 캡처 필터(BPF 문법)와 표시 필터(Wireshark 문법)의 차이를 설명하고 구분해 쓴다
  • tcp contains, tcp.flags 조합으로 내용·플래그 검색을 한다
  • Follow TCP Stream으로 흩어진 패킷을 하나의 대화로 재조립한다
  • Conversations 통계로 통신 지도를 그린다
  • 평문 로그인의 자격 증명을 스트림에서 추출하고, 이것이 의미하는 위험을 설명한다

2. 배경 지식 — 오늘의 도구와 개념

오늘의 도구 한눈에 보기

구분 내용
언어·환경 WSL 리눅스 터미널, tshark(명령줄 Wireshark), Flask 간이 서버
오늘의 명령 tshark -f(캡처 필터), -Y(표시 필터), -z follow,tcp,ascii,N, -z conv,tcp, tcp contains
필요한 개념 BPF 문법 vs 표시 필터 문법, TCP 스트림, 세션 재조립, Step 84의 플래그
오늘의 산출물 sniff.pcap + 분석 노트 — 특정 대화 1건의 완전한 재구성

2-1. 필터는 두 종류다 — 거르는 시점이 다르다

같은 "필터"라는 말이 Wireshark에서 둘로 갈립니다.

구분 캡처 필터 표시 필터
시점 패킷을 잡을 때부터 거른다 잡은 것 중에서 보여 줄 때 거른다
문법 BPF (tcp port 80, host 10.0.0.1) Wireshark (tcp.port==80, http)
tshark 옵션 -f "..." -Y "..."
용도 대용량에서 용량 절약 분석 중 시야 좁히기
주의 거른 패킷은 파일에 없음 — 되돌릴 수 없다 원본은 그대로 — 언제든 필터 해제

규칙은 이렇습니다 — "버려도 되는 것"만 캡처 필터로, 나머지는 표시 필터로. 캡처 필터는 강력하지만 거른 것은 영영 사라집니다.

2-2. TCP 스트림 — 패킷은 조각, 스트림은 대화

패킷 목록은 편지 봉투의 더미입니다. 실제 대화(HTTP 요청과 응답, 로그인 문답)는 여러 패킷에 쪼개져 실립니다. Follow TCP Stream은 같은 연결의 조각들을 순서대로 꿰어 "대화 전문"을 보여 줍니다 — 분석에서 가장 많이 쓰는 기능입니다. tshark에서는 -z follow,tcp,ascii,스트림번호가 이것에 해당합니다.

2-3. Conversations — 먼저 지도를 본다

패킷이 수만 개일 때 한 줄씩 읽는 것은 탐색이 아닙니다. Conversations(대화 통계)는 캡처 전체를 "누가 누구와, 패킷 몇 개, 몇 바이트"로 요약한 지도입니다. 이상한 대화(새벽의 대용량, 처음 보는 상대)를 여기서 먼저 찍고, 그 대화만 스트림으로 파는 것이 실무의 순서입니다.

2-4. 오늘의 무대 — 루프백 로그인 서버

안전한 실습을 위해 내 컴퓨터 안에서만 도는 평문 로그인 서버를 띄웁니다. 캡처 범위가 lo(루프백)뿐이라 남의 트래픽은 물리적으로 섞일 수 없습니다.


3. 따라 하기

3-1. 무대 설치 — 평문 로그인 서버와 캡처

WSL에 간이 서버(sniff_lab.py)를 만듭니다.

from flask import Flask, request
app = Flask(__name__)

@app.route("/")
def index():
    return "sniff-lab index page\n"

@app.route("/login", methods=["POST"])
def login():
    return f"login try: {request.form.get('username')} / {request.form.get('password')}\n"

if __name__ == "__main__":
    app.run(host="127.0.0.1", port=8000)

터미널 1에서 서버를 켜고, 터미널 2에서 캡처 필터를 걸어 잡습니다 — 캡처 필터(-f)의 첫 등장입니다.

tshark -i lo -f "tcp port 8000" -a duration:8 -w sniff.pcap

캡처가 돌고 있는 동안 터미널 3에서 세 가지 행동을 합니다 — 페이지 열람 2번, 로그인 1번.

curl -s http://127.0.0.1:8000/ > /dev/null
curl -s -X POST -d "username=admin&password=sup3r-secret-pw" http://127.0.0.1:8000/login > /dev/null
curl -s http://127.0.0.1:8000/ > /dev/null

출력 (2026-09-09 실측, 캡처 요약 일부):

    1 0.000000000    127.0.0.1 → 127.0.0.1    TCP 74 48258 → 8000 [SYN] ...
    4 0.000096420    127.0.0.1 → 127.0.0.1    HTTP 143 GET / HTTP/1.1
   16 0.005894011    127.0.0.1 → 127.0.0.1    HTTP 257 POST /login HTTP/1.1  (application/x-www-form-urlencoded)
   28 0.012230354    127.0.0.1 → 127.0.0.1    HTTP 143 GET / HTTP/1.1
   (총 36개 패킷)

읽는 법: 캡처 필터 덕분에 처음부터 8000번 포트의 대화만 파일에 들어왔습니다 — 다른 소음 패킷은 애초에 없습니다. 36개 패킷 안에 세 번의 연결(세 악수 세 벌)이 보입니다.

3-2. 표시 필터 1 — 새 연결만 세기

이제 잡은 파일에서 표시 필터(-Y)로 봅니다. 대화의 첫인사(SYN이고 ACK 아님)만:

tshark -r sniff.pcap -Y "tcp.flags.syn == 1 and tcp.flags.ack == 0"

출력 (2026-09-09 실측):

    1 0.000000000    127.0.0.1 → 127.0.0.1    TCP 74 48258 → 8000 [SYN] ...
   13 0.005835323    127.0.0.1 → 127.0.0.1    TCP 74 48272 → 8000 [SYN] ...
   25 0.012175168    127.0.0.1 → 127.0.0.1    TCP 74 48280 → 8000 [SYN] ...

읽는 법: 36개 중 3개 — 세 번의 연결 시작이 정확히 골라졌습니다. Step 84에서 배운 조합 필터가 그대로 쓰입니다. 표시 필터는 파일을 건드리지 않으니, 마음껏 걸었다 풀었다 하세요.

3-3. 표시 필터 2 — 내용으로 검색하기

"password라는 글자가 실린 패킷"을 직접 찾습니다.

tshark -r sniff.pcap -Y 'tcp contains "password"'

출력 (2026-09-09 실측):

   16 0.005894011    127.0.0.1 → 127.0.0.1    HTTP 257 POST /login HTTP/1.1  (application/x-www-form-urlencoded)

읽는 법: 36개 중 단 하나 — 로그인 요청 패킷만 걸렸습니다. tcp contains는 페이로드 바이트를 뒤지는 검색이라, "자격 증명이 어디 실렸나"를 찾는 정규 수사 기법입니다. 대용량 파일에서도 이 한 줄이 수사를 시작합니다.

본문 필드만 뽑아 보면:

tshark -r sniff.pcap -Y "http.request.method == POST" -T fields -e frame.number -e http.request.uri -e http.file_data

출력 (2026-09-09 실측):

16	/login	757365726e616d653d61646d696e2670617373776f72643d73757033722d7365637265742d7077

저 16진수를 디코딩하면 username=admin&password=sup3r-secret-pw입니다 — 다음 절에서 눈으로 확인합니다.

3-4. Follow TCP Stream — 대화 전문 재조립

16번 패킷이 속한 연결(스트림 1)을 통째로 꿰어 봅니다.

tshark -r sniff.pcap -q -z follow,tcp,ascii,1

출력 (2026-09-09 실측):

===================================================================
Follow: tcp,ascii
Filter: tcp.stream eq 1
Node 0: 127.0.0.1:48272
Node 1: 127.0.0.1:8000
191
POST /login HTTP/1.1
Host: 127.0.0.1:8000
User-Agent: curl/8.5.0
Accept: */*
Content-Length: 39
Content-Type: application/x-www-form-urlencoded

username=admin&password=sup3r-secret-pw
        173
HTTP/1.1 200 OK
Server: Werkzeug/3.1.8 Python/3.12.3
...
login try: admin / sup3r-secret-pw
===================================================================

읽는 법: 요청(191바이트)과 응답(173바이트)이 대화 순서대로 이어 붙었습니다. 그리고 보세요 — 아이디와 비밀번호가 평문 그대로, 요청 본문에 한 번, 서버의 응답 본문에 한 번 더 찍혔습니다. GUI Wireshark에서는 패킷 우클릭 → Follow → TCP Stream이 같은 결과를 창으로 띄웁니다 (화면 예시). 스트림 번호가 다르면 tcp.stream eq N의 N을 바꿔 가며 찾으면 됩니다.

3-5. Conversations — 통신 지도

캡처 전체의 대화 목록입니다.

tshark -r sniff.pcap -q -z conv,tcp

출력 (2026-09-09 실측):

TCP Conversations
Filter:<No Filter>
|       <-      | |       ->      | |     Total     |    Relative    | Duration |
| Frames  Bytes | | Frames  Bytes | | Frames  Bytes |      Start     |          |
127.0.0.1:48258 <-> 127.0.0.1:8000   5  532 bytes   7  547 bytes   12 1079 bytes  0.000000  0.0018
127.0.0.1:48272 <-> 127.0.0.1:8000   5  546 bytes   7  661 bytes   12 1207 bytes  0.005835  0.0015
127.0.0.1:48280 <-> 127.0.0.1:8000   5  532 bytes   7  547 bytes   12 1079 bytes  0.012175  0.0019

읽는 법: 세 개의 대화, 각 12패킷. 두 번째 대화(48272)만 바이트가 미묘하게 다릅니다(1207 vs 1079) — 로그인 데이터가 실린 그 대화입니다. 실무에서는 이 표에서 "바이트가 튀는 대화, 시간이 이상한 대화"를 먼저 찍습니다. 지도를 보고 나서 골목(스트림)으로 들어가는 순서입니다.

3-6. 문법 혼동 시연 — 캡처 필터에 표시 문법 넣기

두 문법을 섞으면 어떻게 되는지, 일부러 틀려 봅니다. 캡처 필터(-f)에 표시 필터 문법(tcp.port==80)을 넣으면:

tshark -i lo -f "tcp.port==80" -c 1

출력 (2026-09-09 실측):

tshark: Invalid capture filter "tcp.port==80" for interface 'Loopback'.

읽는 법: BPF 문법은 tcp port 80(== 없음), 표시 필터 문법은 tcp.port==80. 섞으면 캡처 시작 전에 이렇게 거절됩니다. 이 에러가 뜨면 문법을 잘못 골랐다는 뜻이니, 옵션이 -f인지 -Y인지부터 확인하세요. 마지막으로 습관 하나 — 오늘 만든 sniff.pcap은 지우지 말고 보관하세요. 캡처를 저장해 두고 나중에 다시 여는 것이 실무 분석의 기본 워크플로우입니다.


4. 미션과 연습문제

미션 — 대화 1건의 완전한 재구성

  1. 3-1의 서버와 캡처를 재현해 sniff.pcap을 만듭니다 (캡처 필터 사용 필수)
  2. 표시 필터로 로그인 패킷 1건을 찾습니다 — tcp contains "password" 또는 직접 만든 검색어
  3. 그 패킷이 속한 스트림을 follow,tcp,ascii로 재조립해, 요청·응답 전문을 노트에 인용합니다
  4. Conversations 출력에서 그 대화 행을 찾아 "어느 열이 로그인임을 암시하는가"를 적습니다
  5. 분석 노트 마무리 문단: "캡처 필터로 거르지 않았다면 무엇이 달라졌을까, 그리고 캡처 필터로 잘못 걸렀다면 무엇을 잃었을까"

연습문제

문제 1. 캡처 필터와 표시 필터의 차이를 "거르는 시점"과 "되돌릴 수 있는가" 두 축으로 설명해 보세요.

문제 2. tcp port 80tcp.port==80 중 각각이 어디에 쓰이는 문법인지, 그리고 섞어 쓰면 어떤 일이 생기는지 써 보세요.

문제 3. Follow TCP Stream이 "패킷 목록 보기"보다 강력한 이유를, HTTP 대화의 구조(요청-응답의 순서와 조각남)와 연결해 설명해 보세요.

문제 4. 3-4에서 비밀번호가 요청과 응답에 두 번 찍힌 것을 확인했습니다. 이 실험이 "평문 서비스에 같은 비밀번호를 쓰면 안 되는 이유"를 넘어, 왜 "서버가 비밀번호를 응답에 되돌려내면 안 되는지"까지 보여 주는지 설명해 보세요.


5. 모범 답안과 완료 기준

미션 모범 답안

3번의 인용 예 (2026-09-09 실측):

스트림 1 재조립 결과:
요청 → POST /login, 본문 username=admin&password=sup3r-secret-pw (평문)
응답 → 200 OK, 본문 "login try: admin / sup3r-secret-pw" (평문 재노출)

4번의 답 예: "Conversations에서 48272 대화의 Total이 1207바이트로 다른 대화(1079)보다 큼 — 로그인 폼 데이터(39바이트)와 응답 문구가 실려 바이트가 늘었다. 바이트 수의 미세한 차이가 ‘어느 대화에 내용이 실렸는가’의 단서가 된다."

5번의 문단 예: "캡처 필터 없이 잡았다면 lo의 다른 통신(내부 DNS 등)이 섞여 목표 대화를 찾는 비용이 커졌을 것이다. 반대로 캡처 필터를 tcp port 22처럼 잘못 걸었다면 로그인 패킷이 파일에 아예 남지 않아, 표시 필터로는 영영 되찾을 수 없었다. 캡처 필터는 삭제다 — 버려도 되는 것만 걸러야 한다."

검증하는 법: ① pcap이 실제로 만들어졌고 -f 필터가 쓰였는가. ② 스트림 전문 인용에 요청과 응답이 둘 다 있는가. ③ Conversations 행과 스트림이 같은 대화를 가리키는가(포트 번호로 대조). ④ 문단이 "복구 불가능성"에 닿았는가.

연습문제 해답

문제 1 해답. 캡처 필터는 패킷을 저장하기 전에 거르므로 걸러진 패킷은 파일에 존재하지 않습니다 — 되돌릴 수 없는 삭제입니다. 표시 필터는 이미 저장된 파일을 보여 줄 때만 거르므로 필터를 풀면 전부 다시 보입니다 — 되돌릴 수 있는 시야 조절입니다. 그래서 확실한 소음만 캡처 필터로, 분석용 좁히기는 표시 필터로 씁니다.

문제 2 해답. tcp port 80은 BPF 문법으로 캡처 필터(-f)에, tcp.port==80은 Wireshark 표시 필터 문법(-Y 또는 GUI 필터 창)에 쓰입니다. 섞어 쓰면 캡처 시작 전에 Invalid capture filter로 거절되거나(실측 확인), 표시 필터 쪽에서는 문법 오류 표시가 뜹니다. 옵션 문자(-f/-Y)를 보면 어떤 문법을 써야 하는지 알 수 있습니다.

문제 3 해답. 패킷 목록은 시간 순으로 모든 연결이 섞여 있어서, 한 대화의 요청과 응답이 목록 곳곳에 흩어집니다 — 게다가 큰 본문은 여러 패킷으로 조각납니다. Follow TCP Stream은 한 연결의 조각만 모아 번호 순으로 꿰므로, "클라이언트가 무엇을 보내고 서버가 무엇을 답했는가"라는 대화의 전체 이야기를 한 화면에서 읽을 수 있습니다. 분석의 단위가 패킷에서 대화로 올라가는 기능입니다.

문제 4 해답. 비밀번호 재사용의 위험과는 별개로, 서버가 받은 비밀번호를 응답 본문에 그대로 돌려내면 노출 지점이 두 배가 됩니다 — 요청을 놓쳐도 응답에서 읽히고, 로그·프록시·캡처 어디에든 두 번씩 남습니다. 비밀번호는 서버로 가는 길(요청)에만 존재해야 하고, 서버는 그것을 다시 출력하거나 저장해서도 안 됩니다(해시로만 검증). 오늘의 실험 서버는 그 "하면 안 되는 것"을 일부러 보여 주는 반면교사입니다.

완료 기준 체크리스트

  • [ ] 캡처 필터와 표시 필터의 차이(시점·문법·복구 가능성)를 설명할 수 있다
  • [ ] -f-Y에 맞는 문법을 구분해 쓴다
  • [ ] tcp contains로 내용 검색을 할 수 있다
  • [ ] Follow TCP Stream(-z follow,tcp,ascii,N)으로 대화를 재조립했다
  • [ ] Conversations(-z conv,tcp)로 대화 지도를 그렸다
  • [ ] 평문 자격 증명의 노출을 캡처로 확인하고 설명할 수 있다
  • [ ] 미션: 대화 1건의 완전한 재구성 노트 완성

6. 흔한 실수와 해결

벽 1. tshark: Invalid capture filter "tcp.port==80" 에러가 난다

원인: 캡처 필터 자리(-f)에 표시 필터 문법을 넣었습니다 (2026-09-09 실측 메시지).
해결: 캡처 필터는 BPF 문법 — tcp port 80(등호 없음). 표시 필터 문법(tcp.port==80)을 쓰고 싶다면 옵션을 -Y로 바꾸세요. 에러 메시지가 "capture filter"라고 말해 주는 것이 힌트입니다.

벽 2. 캡처 파일에 원하는 패킷이 없다

원인: 캡처 필터가 너무 좁았거나, 캡처 시작이 행동보다 늦었습니다. 걸러진 패킷은 복구되지 않습니다.
해결: 처음에는 필터 없이(또는 -i lo만으로) 넓게 잡고, 표시 필터로 좁히는 연습부터 하세요. 캡처 필터는 "뭘 잡을지 확실할 때"만. 그리고 캡처를 먼저 시작하고 행동(curl)하세요.

벽 3. follow의 스트림 번호를 모르겠다

증상: -z follow,tcp,ascii,0을 썼는데 다른 대화가 나옵니다.
원인: 스트림 번호는 0, 1, 2… 순서라, 목표 패킷이 몇 번 대화인지 알아야 합니다.
해결: 목표 패킷 번호에서 tshark -r sniff.pcap -Y "frame.number==16" -T fields -e tcp.stream으로 스트림 번호를 먼저 구하세요. 또는 -z conv,tcp의 대화 순서가 스트림 번호와 대응합니다.

벽 4. tcp contains가 아무것도 안 잡는다

원인: 찾는 글자가 실제로는 다르게 실렸습니다 — 대소문자, URL 인코딩(%40 등), 압축·암호화(HTTPS) 등.
해결: 먼저 http 필터로 대화를 눈으로 보고 실제 바이트를 확인하세요. HTTPS라면 내용 검색은 원천적으로 불가합니다 — 그것이 암호화의 본분입니다 (Step 85).

벽 5. tshark 경고 문구가 신경 쓰인다 — Running as user "root"...

원인: WSL에서 root로 실행해서 나는 표준 경고입니다. 캡처 도구가 패킷을 보려면 높은 권한이 필요해서 생기는 메시지로, 오늘 실습(루프백 전용)에서는 무시해도 되는 정보성 경고입니다.
해결: 출력을 깔끔하게 보려면 2>/dev/null로 stderr를 버리세요. 다만 실제 서버에서 root 상주 캡처는 피하는 것이 원칙입니다.


7. 정리

오늘의 개념

개념 한 줄 설명
캡처 필터 (BPF) 잡을 때부터 거르는 필터 — 버린 것은 복구 불가
표시 필터 잡은 것 중 보여 주는 필터 — 원본은 무사
tcp contains 페이로드 내용 검색 — 자격 증명 찾기의 시작
Follow TCP Stream 흩어진 조각을 대화 전문으로 재조립
Conversations 누가 누구와 얼마나 — 분석의 지도
세션 재구성 패킷 단위 분석에서 대화 단위 분석으로의 상승

오늘의 명령어·도구

명령·필터 하는 일
tshark -i lo -f "tcp port 8000" -w f.pcap 8000번만 골라 캡처 (BPF)
tshark -r f.pcap -Y "tcp.flags.syn==1 and tcp.flags.ack==0" 새 연결(첫인사)만 보기
tshark -r f.pcap -Y 'tcp contains "password"' 내용에 글자 검색
tshark -r f.pcap -q -z follow,tcp,ascii,1 스트림 1 대화 전문
tshark -r f.pcap -q -z conv,tcp 대화 통계 지도
-T fields -e tcp.stream 패킷의 스트림 번호 확인

명령어보다 중요한 감각

오늘의 실습은 겉으로는 도구 사용법이지만, 속으로는 "분석가의 시선 순서"를 배운 것입니다 — 지도(Conversations)를 보고, 골목(필터)으로 좁히고, 방 안(스트림)에 들어간다. 이 순서는 캡처가 백만 패킷이 되어도 같습니다. 그리고 마지막 장면 — 스트림 한 줄에 평문 비밀번호가 놓인 그 화면 — 을 기억하세요. 그 화면을 볼 수 있는 도구는 세상에 널렸고, 그래서 세상은 HTTPS를 기본값으로 바꿨습니다. 여러분은 오늘 그 기본값이 있기 전의 세상이 어땠는지를 눈으로 본 사람이 됐습니다.


전부 체크되면 Step 158 완료입니다. 사이드바의 체크박스를 눌러 진도를 저장하세요.