41. TCP 3-Way Handshake — 연결이 성립했다는 증거

changseop lee·6일 전

📚 네트워크 · 패킷 분석 › 01. TCP/IP 구조 이해 — 41편
이전 글: 40. TCP와 UDP 헤더 비교 — 신뢰성과 속도의 차이 · 다음 글: 42. TCP 상태 전이 — LISTEN부터 TIME_WAIT까지
참고(리눅스 시스템 기초): Socket 이해 — listen·accept·connect 소켓 흐름

1. 왜 알아야 하는가

관제 보고서에서 가장 조심해야 하는 표현 중 하나가 "접속했다"입니다. 방화벽 로그에 허용(allow) 이벤트가 있다고 해서 실제로 연결이 성립했다는 뜻은 아닙니다. 방화벽이 첫 SYN 패킷을 통과시킨 시점에 로그를 남기는 장비도 많기 때문입니다.

  • SYN만 있고 응답이 없다 → 연결 시도
  • SYN → RST가 돌아왔다 → 거부됨
  • SYN → SYN/ACK → ACK가 모두 있다 → 연결 성립
  • 성립 후 데이터 바이트가 오갔다 → 실제 통신 발생

이 네 가지는 영향도 판단에서 전혀 다른 결론으로 이어집니다. "외부 IP가 내부 서버 22번에 접속 시도"와 "외부 IP가 내부 서버 22번과 세션을 맺고 데이터를 주고받음"은 대응 우선순위가 다릅니다. 이번 글은 3-Way Handshake의 원리와, 연결이 성립했다는 증거를 어디서 어떻게 확인하는지를 다룹니다.


2. 핵심 개념

2-1. 세 단계가 약속하는 것

단계방향플래그담긴 정보의미
1클라이언트 → 서버SYN클라이언트 초기 시퀀스 번호(ISN) = x, MSS·Window Scale 등 옵션"연결하고 싶다, 내 번호는 x부터 시작"
2서버 → 클라이언트SYN, ACK서버 ISN = y, ACK = x+1, 서버 옵션"받았다(x+1 기다림), 내 번호는 y부터"
3클라이언트 → 서버ACKACK = y+1"네 번호도 받았다" → 양쪽 모두 ESTABLISHED

핵심은 양쪽이 서로의 시퀀스 번호를 확인했다는 것입니다. SYN 플래그 자체가 시퀀스 번호 1개를 소비하므로 응답 ACK는 ISN + 1이 됩니다. 시퀀스 번호와 ACK의 상세 동작은 11편에서 다룹니다.

ISN은 예측하기 어렵게 무작위에 가깝게 생성됩니다. 과거 ISN을 예측할 수 있던 시절에는 응답을 보지 않고도 위조 연결을 만들 수 있었기 때문입니다.

2-2. 서버의 두 대기열

서버 커널은 연결 요청을 두 단계로 관리합니다.

대기열들어가는 시점상태가득 차면
SYN 대기열 (미완성 연결)SYN 수신 후 SYN/ACK 전송SYN-RECEIVED (ss에서 SYN-RECV)새 SYN을 버리거나, SYN 쿠키 사용
Accept 대기열 (완성 연결)마지막 ACK 수신 후ESTABLISHED, 애플리케이션의 accept() 대기커널 카운터 ListenOverflows 증가, 연결 드롭

SYN Flood는 첫 번째 대기열을 미완성 연결로 채워 정상 사용자의 연결을 막는 공격입니다. Linux는 이 대기열이 넘칠 때 SYN 쿠키(net.ipv4.tcp_syncookies)로 상태를 저장하지 않고 SYN/ACK를 보내는 방식으로 대응합니다.

2-3. 첫 SYN에 대한 응답 유형

서버 측 상황돌아오는 것클라이언트 결과
포트 열림 (LISTEN)SYN/ACK연결 성립
포트 닫힘 (호스트는 살아 있음)RST/ACK즉시 Connection refused
방화벽 drop없음SYN 재전송 반복 후 타임아웃
방화벽 rejectRST 또는 ICMP 3/13 등 (설정에 따라)즉시 실패
호스트 없음 (같은 대역)없음 (ARP 실패)No route to host 또는 타임아웃

3. 동작 원리

클라이언트 192.168.10.10이 서버 192.168.10.20:22에 접속하는 흐름입니다. 괄호는 각 시점의 TCP 상태입니다.

3-Way Handshake 시퀀스
그림 1. SYN → SYN/ACK → ACK로 양쪽이 서로의 시퀀스 번호를 확인합니다

응답 유형 비교
그림 2. 응답 유형에 따라 '성립·거부·무응답'이 구분됩니다

클라이언트 192.168.10.10:51514                  서버 192.168.10.20:22
(CLOSED)                                         (LISTEN)
    │                                                │
    │ ① SYN  seq=x                                   │
    │ ─────────────────────────────────────────────→ │
(SYN-SENT)                                           │  SYN 대기열에 등록
    │                                                │ (SYN-RECEIVED)
    │ ② SYN/ACK  seq=y, ack=x+1                      │
    │ ←───────────────────────────────────────────── │
    │                                                │
    │ ③ ACK  seq=x+1, ack=y+1                        │
    │ ─────────────────────────────────────────────→ │
(ESTABLISHED)                                        │  Accept 대기열로 이동
    │                                                │ (ESTABLISHED)
    │                                                ↓
    │                                         애플리케이션 accept()
    │ ④ 데이터 (PSH/ACK) ... 실제 통신 시작          │
    ↓                                                ↓

응답이 오지 않으면 클라이언트 커널은 SYN을 간격을 늘려 가며 재전송합니다. 재전송 횟수는 net.ipv4.tcp_syn_retries(일반적인 기본값 6)로 정해지며, 이 때문에 drop 정책 대상에 접속하면 수십 초에서 2분가량 기다린 뒤 실패합니다. 서버 쪽 SYN/ACK 재전송 횟수는 net.ipv4.tcp_synack_retries(일반적인 기본값 5)입니다.

관제 관점에서 기억할 점은, 같은 SYN이 짧은 간격으로 여러 번 보이는 것은 재전송일 수 있다는 것입니다. 출발지 포트와 시퀀스 번호가 같으면 새로운 시도가 아니라 같은 시도의 재전송입니다.


4. 실제 명령어 / 실습

실습 환경: 본인 소유 Linux VM (Rocky Linux 또는 Ubuntu) 2대(192.168.10.10 클라이언트, 192.168.10.20 서버 예시, 서버에 sshd 동작 중). 인터페이스 이름은 ens33을 예시로 사용합니다(환경에 따라 다름).

# 도구 설치
sudo dnf install -y tcpdump nmap-ncat      # Rocky
sudo apt install -y tcpdump netcat-openbsd # Ubuntu

4-1. 성공한 Handshake 캡처

# [서버] SSH 포트의 처음 3개 패킷만 캡처 (-S: 시퀀스 번호를 절대값으로 표시)
sudo tcpdump -nn -S -i ens33 -c 3 'tcp port 22'

# [클라이언트] 연결만 확인하고 종료
nc -zv 192.168.10.20 22

4-2. 닫힌 포트(거부) 캡처

# [서버] 8081 포트 캡처 (이 포트에는 서비스가 없다고 가정)
sudo tcpdump -nn -i ens33 'tcp port 8081'

# [클라이언트]
nc -zv 192.168.10.20 8081

4-3. 플래그별 필터

# SYN만 켜진 패킷 = 연결 시작 요청
sudo tcpdump -nn -i ens33 'tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn'

# SYN과 ACK가 모두 켜진 패킷 = 서버의 수락 응답
sudo tcpdump -nn -i ens33 'tcp[tcpflags] & (tcp-syn|tcp-ack) == (tcp-syn|tcp-ack)'

# RST가 켜진 패킷 = 거부·중단
sudo tcpdump -nn -i ens33 'tcp[tcpflags] & tcp-rst != 0'

첫 번째 필터에서 & (tcp-syn|tcp-ack)로 SYN·ACK 두 비트만 떼어 낸 뒤 == tcp-syn으로 비교하므로, SYN은 켜져 있고 ACK는 꺼진 패킷만 남습니다. tcp-syn != 0만 쓰면 SYN/ACK까지 함께 잡힙니다.

4-4. 서버 측 상태와 카운터

# 미완성 연결(SYN-RECV) 수
ss -tan state syn-recv
# 클라이언트에서 응답을 기다리는 연결(SYN-SENT)
ss -tan state syn-sent

# LISTEN 소켓의 대기열: Recv-Q = 현재 accept 대기 수, Send-Q = 최대 backlog
ss -ltn

# SYN 쿠키·대기열 관련 설정
sysctl net.ipv4.tcp_syncookies net.ipv4.tcp_max_syn_backlog net.core.somaxconn
sysctl net.ipv4.tcp_syn_retries net.ipv4.tcp_synack_retries

# 누적 카운터 (-a: 누적값, -z: 0인 항목도 표시)
nstat -az TcpExtListenOverflows TcpExtListenDrops TcpExtSyncookiesSent

nstat는 iproute2 패키지에 포함되어 Rocky·Ubuntu 모두 기본 설치되어 있는 경우가 많습니다.

📷 [실습 화면 삽입] tcpdump -nn -S 'tcp port 22' 결과 — SYN, SYN/ACK, ACK 3줄과 seq/ack 값의 +1 관계


5. 결과 확인

tcpdump 출력 형식 예시(값은 환경마다 다름):

# 성공 (-S 절대 시퀀스)
IP 192.168.10.10.51514 > 192.168.10.20.22: Flags [S], seq 1180562114, win 64240, options [mss 1460,sackOK,TS val 1920311 ecr 0,nop,wscale 7], length 0
IP 192.168.10.20.22 > 192.168.10.10.51514: Flags [S.], seq 3021447785, ack 1180562115, win 65160, options [mss 1460,sackOK,TS val 88213 ecr 1920311,nop,wscale 7], length 0
IP 192.168.10.10.51514 > 192.168.10.20.22: Flags [.], ack 3021447786, win 502, options [nop,nop,TS val 1920312 ecr 88213], length 0

# 거부 (닫힌 포트)
IP 192.168.10.10.51520 > 192.168.10.20.8081: Flags [S], seq 2231907741, win 64240, options [...], length 0
IP 192.168.10.20.8081 > 192.168.10.10.51520: Flags [R.], seq 0, ack 2231907742, win 0, length 0
확인 포인트성공거부
두 번째 패킷 플래그[S.] (SYN/ACK)[R.] (RST/ACK)
두 번째 패킷의 ack클라이언트 seq + 1클라이언트 seq + 1 (받은 것은 확인하되 거부)
세 번째 패킷[.] ACK, ack = 서버 seq + 1없음
nc -zv 결과succeeded / Connected 계열 메시지Connection refused

-S를 빼면 tcpdump는 두 번째 패킷부터 시퀀스 번호를 상대값(ack 1)으로 표시합니다. 번호 관계를 확인할 때는 -S가 편합니다.

  • SYN → SYN/ACK → ACK 세 패킷을 캡처했다
  • ack 값이 상대방 seq + 1임을 확인했다
  • 닫힌 포트에서 RST/ACK가 돌아오는 것을 확인했다
  • SYN 전용 필터와 SYN/ACK 필터의 차이를 설명할 수 있다
  • tcp_syncookies 설정과 ListenOverflows 카운터를 확인했다

📷 [실습 화면 삽입] 닫힌 포트 캡처 — Flags [S] 다음 Flags [R.] 라인과 nc 의 Connection refused


6. 패킷 / 로그 분석

관찰정상일 수 있는 경우의심해야 하는 경우
SYN만 있고 응답 없음대상 서버 중지, 방화벽 drop 정책 (설정 오류 포함)내부 단말이 외부 특정 IP로 일정 간격 SYN 반복 → 차단된 C2 접속 재시도 가능성
SYN → RST 다수서비스 재시작 중 일시적 거부한 출발지가 여러 포트·여러 호스트로 SYN → 대부분 RST → 포트 탐색 (05. 네트워크 스캔 · 공격 징후 분석 시리즈에서 다룸)
서버에 SYN-RECV 다수순간 접속 폭주, 느린 클라이언트 네트워크수백~수천 개 지속 + 출발지 IP가 매우 다양 + ACK가 오지 않음 → SYN Flood
요청하지 않은 SYN/ACK 수신비대칭 라우팅으로 SYN은 다른 경로를 탄 경우여러 외부 서버에서 SYN/ACK 유입 → 누군가 내 IP를 출발지로 위조해 SYN을 보냄(backscatter)
Handshake 성립 후 데이터 없이 바로 종료헬스체크, 포트 연결 확인여러 포트에 대해 반복 → 연결 기반 탐색
# 출발지별 SYN 수 집계 (100개 캡처 후 상위 출발지)
sudo tcpdump -nn -i ens33 -c 100 'tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn' 2>/dev/null \
  | awk '{print $3}' | cut -d. -f1-4 | sort | uniq -c | sort -rn | head

# SYN-RECV 수 추이 (1초마다 갱신, -H: 헤더 제외)
watch -n1 'ss -Htan state syn-recv | wc -l'

위 awk 예시는 tcpdump 기본 출력에서 세 번째 필드가 출발지IP.포트라는 점을 이용합니다. 옵션에 따라 출력 필드 위치가 달라질 수 있으므로 먼저 원본 출력을 확인한 뒤 사용합니다.


7. 보안관제 관점

흔적이 남는 곳

위치확인할 수 있는 것
방화벽 세션 로그세션 생성 시점, 종료 사유, 송수신 바이트. 바이트 0 또는 패킷 1~2개인 세션은 성립하지 않았을 가능성
NetFlow/세션 요약세션의 누적 TCP 플래그 (SYN만 있는지, ACK까지 있는지)
Zeek conn.logconn_state 값으로 성립 여부 요약 (예: S0 시도만 있고 응답 없음, REJ 거부, SF 정상 성립·종료). 07. 네트워크 보안관제 통합 분석 시리즈에서 다룸
서버 애플리케이션 로그Handshake가 끝나고 애플리케이션이 accept()한 뒤에만 기록됨 (예: sshd 접속 로그)

한계와 오탐 주의

  • 방화벽 allow 로그 = 연결 성립이 아닙니다. 송수신 바이트, 세션 지속 시간, 종료 사유를 함께 봐야 합니다.
  • 반대로 Handshake가 성립했다고 공격이 성공했다는 뜻도 아닙니다. 인증 성공 여부는 애플리케이션 로그(SSH라면 SSH Brute Force 로그 분석)로 확인합니다.
  • SYN 쿠키가 동작 중이면 서버에 SYN-RECV 상태가 쌓이지 않을 수 있어, 상태 수만으로 SYN Flood를 판단하면 놓칠 수 있습니다. TcpExtSyncookiesSent 카운터와 커널 로그의 SYN flooding on port 메시지를 함께 확인합니다.
  • 패킷의 상세 필드(옵션, 윈도우 값)로 OS나 도구를 추정하는 분석은 03. Wireshark 패킷 분석 시리즈에서 다룹니다.

8. 핵심 정리

  • 3-Way Handshake는 SYN(x) → SYN/ACK(y, ack x+1) → ACK(ack y+1)로 양쪽이 서로의 시퀀스 번호를 확인하는 과정입니다.
  • 연결 성립의 증거는 세 패킷이 모두 존재하는 것이며, 실제 통신 여부는 그 뒤 데이터 전송으로 판단합니다.
  • 첫 SYN에 대한 응답이 SYN/ACK(열림), RST(닫힘), 무응답(drop)으로 갈리며, 이것이 방화벽·서버 상태를 알려줍니다.
  • 'tcp[tcpflags] & (tcp-syn|tcp-ack) == tcp-syn'은 연결 요청만, == (tcp-syn|tcp-ack)는 수락 응답만 걸러냅니다.
  • 서버의 SYN-RECV 누적, ListenOverflows·SyncookiesSent 카운터는 SYN Flood 판단 근거가 됩니다.
  • 방화벽 allow 로그만으로 "접속 성공"이라고 보고하지 않습니다.

다음 글: 09. TCP 연결 종료 — FIN과 RST의 차이

profile
코드에 숨겨진 위협을 읽고 AI로 보안의 미래를 설계합니다. 프론트엔드 개발 경험을 자산 삼아 더 견고하고 지능적인 보안 운영 시스템을 구축해 나가는 과정을 기록합니다

0개의 댓글