📚 네트워크 · 패킷 분석 › 03. Wireshark 패킷 분석 — 119편
이전 글: 118. Ping Packet 분석 · 다음 글: 120. UDP Header 분석
참고(01. TCP/IP 네트워크 구조 이해): 40. TCP와 UDP 헤더 비교 — 신뢰성과 속도의 차이 · 41. TCP 3-Way Handshake — 연결이 성립했다는 증거 · 45. TCP 연결 종료 — FIN과 RST의 차이 · 46. 시퀀스 번호·ACK·윈도우 — TCP가 데이터를 빠짐없이 전달하는 방법 — TCP 동작 개념은 이 글들에서 다뤘습니다.
관제에서 보는 트래픽의 상당 부분은 TCP 위에서 동작합니다(HTTP, HTTPS, SSH, SMB, DB 등). 방화벽 로그가 "허용/차단"과 포트를 알려 준다면, pcap의 TCP 헤더는 그 연결이 실제로 어떻게 진행되었는지를 알려 줍니다.
tcp.stream)tcp.len)이 글은 TCP 헤더의 각 필드가 Wireshark에서 어떤 이름과 값으로 보이는지, 특히 Wireshark가 계산해서 보여 주는 값과 패킷에 실제로 기록된 값의 차이에 집중합니다. 연결 수립 과정 자체의 패킷 분석은 121편(121. TCP 3-Way Handshake 분석)에서 다룹니다.

그림 1. TCP 헤더 위치별 Wireshark 필드명
| 헤더 항목 | Wireshark 필드 | 실제 값 / 계산 값 | 분석 시 의미 |
|---|---|---|---|
| Source Port | tcp.srcport | 실제 | 출발지 포트 (tcp.port는 양쪽 중 하나) |
| Destination Port | tcp.dstport | 실제 | 목적지 포트 |
| (세션 번호) | tcp.stream | 계산 | Wireshark가 연결마다 붙이는 0부터 시작하는 번호 |
| Sequence Number | tcp.seq | 계산(상대값) | 연결 시작 기준 상대 번호 |
| Sequence Number (raw) | tcp.seq_raw | 실제 | 헤더에 기록된 32비트 원래 값 |
| Acknowledgment Number | tcp.ack / tcp.ack_raw | 계산 / 실제 | 다음에 받기를 기대하는 번호 |
| (다음 시퀀스) | tcp.nxtseq | 계산 | 이 세그먼트 다음에 올 시퀀스 번호 |
| Header Length | tcp.hdr_len | 실제(바이트 환산) | 20이면 옵션 없음, 그 이상은 옵션 포함 |
| Flags | tcp.flags, tcp.flags.*, tcp.flags.str | 실제 | 제어 비트 |
| Window | tcp.window_size_value | 실제 | 헤더에 기록된 16비트 값 |
| (계산된 윈도우) | tcp.window_size | 계산 | Window Scale을 적용한 실제 수신 가능 크기 |
| (스케일 배수) | tcp.window_size_scalefactor | 계산 | -1 = 알 수 없음(핸드셰이크 미캡처), -2 = 스케일 미사용 |
| (데이터 길이) | tcp.len | 계산 | 이 세그먼트의 페이로드 바이트 수 |
| Checksum | tcp.checksum | 실제 | 기본적으로 검증하지 않음(115편 오프로딩 참고) |
| Urgent Pointer | tcp.urgent_pointer | 실제 | URG가 설정될 때만 의미 |
| Flag | Wireshark 필드 | 비트 값 | 의미 |
|---|---|---|---|
| FIN | tcp.flags.fin | 0x001 | 보낼 데이터 없음, 정상 종료 요청 |
| SYN | tcp.flags.syn | 0x002 | 연결 시작, 초기 시퀀스 번호 동기화 |
| RST | tcp.flags.reset | 0x004 | 연결 즉시 중단·거부 |
| PSH | tcp.flags.push | 0x008 | 받은 데이터를 바로 애플리케이션에 전달 |
| ACK | tcp.flags.ack | 0x010 | tcp.ack 값이 유효함 |
| URG | tcp.flags.urg | 0x020 | 긴급 포인터 유효 |
| ECE / CWR | tcp.flags.ece, tcp.flags.cwr | 0x040 / 0x080 | ECN 혼잡 알림 관련 |
tcp.flags.str은 설정된 Flag를 ·······A··S·처럼 문자로 보여 주는 필드라 사람이 읽기 좋고, tcp.flags는 0x012 같은 숫자라 조합 비교에 편합니다. 예약 비트 영역의 필드 이름은 Wireshark 버전에 따라 바뀐 적이 있으므로(4.2 기준 tcp.flags.ae, tcp.flags.res), 사용하는 버전의 상세 트리에서 확인 후 씁니다.
| 옵션 | Kind | Wireshark 필드 | 의미 |
|---|---|---|---|
| MSS | 2 | tcp.options.mss, tcp.options.mss_val | 한 세그먼트에 받을 수 있는 최대 데이터 크기 |
| Window Scale | 3 | tcp.options.wscale, tcp.options.wscale.shift, tcp.options.wscale.multiplier | 윈도우 값에 곱할 배수(2의 shift 제곱) |
| SACK Permitted | 4 | tcp.options.sack_perm | 선택적 확인응답 사용 가능 |
| SACK | 5 | tcp.options.sack_le, tcp.options.sack_re | 받은 데이터 구간(왼쪽·오른쪽 끝) |
| Timestamps | 8 | tcp.options.timestamp.tsval, tcp.options.timestamp.tsecr | 송신 시각 값 / 상대 값 되돌려주기 |
| NOP | 1 | tcp.option_kind == 1 | 옵션 정렬용 패딩 |

그림 2. 플래그 비트 배치와 자주 보는 조합의 16진 값
TCP의 초기 시퀀스 번호(ISN)는 무작위에 가까운 32비트 값입니다. 사람이 읽기 어렵기 때문에 Wireshark는 기본적으로 연결에서 처음 본 값을 0으로 두고 상대값을 보여 줍니다(tcp.relative_sequence_numbers: TRUE).
패킷에 실제 기록된 값 Wireshark 기본 표시
tcp.seq_raw = 1000000 (SYN) → tcp.seq = 0
tcp.seq_raw = 1000001 → tcp.seq = 1
tcp.seq_raw = 1000001, len=21 → tcp.seq = 1, tcp.nxtseq = 22
↓
다른 캡처 파일(센서 pcap vs 서버 pcap)과 비교할 때
→ 상대값은 파일마다 기준점이 달라 비교 불가
→ tcp.seq_raw 로 비교해야 같은 패킷인지 확인 가능
캡처가 연결 중간부터 시작되면 Wireshark는 SYN을 보지 못했으므로 처음 본 패킷을 기준으로 상대값을 만들고, Window Scale도 알 수 없어 tcp.window_size_scalefactor가 -1로 표시됩니다. 이 경우 tcp.window_size는 실제 수신 가능 크기가 아니라 헤더 값 그대로입니다.
tcp.stream으로 연결 단위 묶기Wireshark는 (출발지 IP, 출발지 포트, 목적지 IP, 목적지 포트) 조합이 같은 패킷을 하나의 연결로 보고 tcp.stream 번호를 붙입니다. 분석할 때는 개별 패킷보다 tcp.stream 단위로 "무슨 일이 있었는지"를 요약하는 것이 효율적입니다. 같은 포트 조합이 짧은 시간 안에 재사용되면 Wireshark가 새 연결로 나누기도 하고(tcp.analysis.reused_ports 표시), 반대로 오래된 연결과 섞여 보일 수도 있습니다.
실습 환경: 본인 소유 Linux VM (Rocky Linux 또는 Ubuntu). 인터페이스 이름은 ens33을 예시로 사용합니다. tshark 설치는 112편과 같습니다(Rocky: sudo dnf install -y wireshark-cli, Ubuntu: sudo apt install -y tshark).
# 터미널 1: TCP만 캡처
sudo tshark -i ens33 -f "tcp" -w /tmp/pcap11-tcp.pcapng
# 터미널 2 (본인 VM 또는 본인 소유 실습 서버 대상)
curl -sI http://example.com > /dev/null # 정상 연결 + 데이터
timeout 2 bash -c '</dev/tcp/<실습 서버 IP>/8081' ; echo "rc=$?" # 닫힌 포트 연결 시도
두 번째 명령은 bash의 /dev/tcp 기능으로 TCP 연결만 시도하는 방법입니다. 대상 포트가 닫혀 있으면 SYN에 대해 RST가 돌아오고, 방화벽이 조용히 버리면 응답 없이 시간이 초과됩니다. 호스트 PC에서 VM으로 SSH 접속을 하고 있다면 그 트래픽도 함께 캡처됩니다.
📷 [실습 화면 삽입] Wireshark 상세 창의 Transmission Control Protocol 트리 (Flags와 Options를 펼친 상태)
F=/tmp/pcap11-tcp.pcapng
# (1) 헤더 핵심 필드
tshark -r $F -T fields -E header=y \
-e frame.number -e tcp.stream -e tcp.srcport -e tcp.dstport \
-e tcp.seq -e tcp.seq_raw -e tcp.ack -e tcp.len -e tcp.flags -e tcp.flags.str \
-e tcp.window_size_value -e tcp.window_size -e tcp.window_size_scalefactor | head -20
# (2) SYN, SYN/ACK에 실린 옵션
tshark -r $F -Y "tcp.flags.syn == 1" -T fields -E header=y \
-e frame.number -e ip.src -e tcp.flags.str -e tcp.options.mss_val \
-e tcp.options.wscale.shift -e tcp.options.timestamp.tsval -e tcp.option_kind
# (3) 상대 시퀀스 번호 끄고 보기 (실제 헤더 값)
tshark -r $F -o tcp.relative_sequence_numbers:FALSE -T fields \
-e frame.number -e tcp.seq -e tcp.ack | head
# (4) 연결별 데이터 양 요약 (페이로드 합계)
tshark -r $F -Y "tcp.len > 0" -T fields -e tcp.stream -e tcp.len \
| awk '{b[$1]+=$2} END {for (s in b) print s, b[s]}' | sort -n
# (5) Flag 조합 분포
tshark -r $F -T fields -e tcp.flags.str | sort | uniq -c | sort -rn
📷 [실습 화면 삽입] 닫힌 포트 연결 시도에서 [SYN] → [RST, ACK]가 보이는 패킷 목록
아래는 형식 예시(값은 환경마다 다름) 입니다.
frame stream srcport dstport seq seq_raw ack len flags flags.str win_value win scale
13 0 51000 22 0 1000000 0 0 0x0002 ··········S· 64240 64240
14 0 22 51000 0 5000000 1 0 0x0012 ·······A··S· 65160 65160
15 0 51000 22 1 1000001 1 0 0x0010 ·······A···· 502 64256 128
16 0 51000 22 1 1000001 1 21 0x0018 ·······AP··· 502 64256 128
17 1 51001 8081 0 777 0 0 0x0002 ··········S· 64240 64240
18 1 8081 51001 1 0 1 0 0x0014 ·······A·R·· 0 0 -2
502 × 128 = 64256옵션 형식 예시:
frame ip.src flags.str mss_val wscale.shift tsval option_kind
13 192.168.10.20 ··········S· 1460 7 12345 2,4,8,1,3
확인 체크리스트
tcp.stream 번호로 연결 단위를 구분했다tcp.seq(상대값)와 tcp.seq_raw(실제값)의 차이를 확인했다tcp.len > 0인 패킷으로 실제 데이터가 오간 연결을 구분했다tcp.window_size_value와 tcp.window_size의 차이와 scale 값(-1, -2)의 의미를 설명할 수 있다| 관찰 | 정상일 수 있는 경우 | 의심해야 하는 경우 |
|---|---|---|
| SYN 후 RST/ACK | 서비스 중지, 잘못된 포트 설정 | 한 출발지가 여러 포트에 SYN → 대부분 RST/ACK (포트 스캔, 05. 네트워크 스캔 · 공격 징후 분석 시리즈에서 다룸) |
| SYN 후 응답 없음 | 방화벽의 조용한 차단(drop), 패킷 손실 | 대량의 응답 없는 SYN이 한 서버로 집중 |
데이터 없는 연결 (tcp.len이 모두 0) | 헬스체크, 연결 확인 | 짧은 연결이 여러 포트·호스트로 반복 |
| SYN과 FIN이 동시에 설정 | (정상 통신에서는 사용되지 않음) | 조작된 패킷 → 스캔·회피 시도 가능성 |
Flag가 하나도 없는 패킷 (tcp.flags == 0x000) | (정상 통신에서는 사용되지 않음) | NULL 스캔 등 조작 패킷 |
FIN·PSH·URG 동시 설정 (0x029) | (정상 통신에서는 사용되지 않음) | Xmas 스캔 등 조작 패킷 |
| MSS 옵션 없는 SYN | 일부 특수 장비·스택 | 옵션이 거의 없는 SYN이 대량 발생 (도구가 만든 패킷 가능성) |
Window 0 (tcp.analysis.zero_window) | 수신 측 처리 지연 | 장시간 지속 → 서비스 성능 문제 확인 |
확인 필터 예시입니다.
# 비정상 Flag 조합
tshark -r $F -Y "(tcp.flags.syn == 1 && tcp.flags.fin == 1) || tcp.flags == 0x000 || tcp.flags == 0x029"
# SYN에 RST로 응답한 연결 (출발지·목적지 포트 요약)
tshark -r $F -Y "tcp.flags.reset == 1 && tcp.flags.ack == 1 && tcp.seq == 1" \
-T fields -e ip.src -e tcp.srcport -e ip.dst | sort | uniq -c | sort -rn
# 출발지별로 SYN을 보낸 목적지 포트 수
tshark -r $F -Y "tcp.flags.syn == 1 && tcp.flags.ack == 0" -T fields \
-e ip.src -e tcp.dstport | sort -u | awk '{c[$1]++} END {for (s in c) print c[s], s}' | sort -rn
# MSS 옵션이 없는 SYN (Wireshark 전문가 정보)
tshark -r $F -Y "tcp.options.mss.absent"
두 번째 필터의 tcp.seq == 1 조건은 상대 시퀀스 번호를 전제로 한 것이라, 캡처가 연결 도중부터 시작되었거나 상대값 표시를 끈 경우에는 결과가 달라질 수 있습니다. 필터 결과는 반드시 해당 tcp.stream을 열어 앞뒤 패킷과 함께 확인합니다.
흔적이 남는 곳
ss 명령으로 확인할 수 있습니다(ss와 ip 명령어, 16. 프로세스와 네트워크 소켓 참고).한계와 오탐 주의
tcp.seq, tcp.ack, tcp.stream, tcp.window_size, tcp.len은 Wireshark가 계산한 값입니다. 캡처가 불완전하면(연결 중간부터 시작, 패킷 유실) 계산 결과도 부정확해질 수 있으므로, 증거로 인용할 때는 tcp.seq_raw 같은 실제 값을 함께 적습니다.tcp.stream(연결)이며, 포트(tcp.srcport, tcp.dstport)와 Flag로 연결의 성격을 파악합니다.tcp.seq·tcp.ack는 Wireshark가 만든 상대값이고, 헤더의 실제 값은 tcp.seq_raw·tcp.ack_raw입니다. 여러 캡처를 비교할 때는 raw 값을 씁니다.tcp.len은 페이로드 크기로, 실제 데이터가 오갔는지 판단하는 가장 간단한 기준입니다.tcp.window_size는 Window Scale을 적용한 계산값이며, 핸드셰이크가 캡처되지 않으면 scale이 -1(알 수 없음)로 표시됩니다.tcp.options.* 필드로 읽습니다.다음 글: 12. 3-Way Handshake 패킷 분석 (예정)