리눅스 시스템 기초 · 입문편 — 본문의 "N편"은 입문 과정 번호다. 번호별 글과 전체 250편 구성은 통합 로드맵에서 확인할 수 있다.
리눅스 시스템 기초 42 / 50 · Part 5. 네트워크·보안·SOC
실습 환경: Rocky Linux 9 (10.0.0.200) · handshake 캡처와ss상태는 실습 컨테이너의 실제 출력
이전 글: 41. Linux 네트워크 구조
41편에서 패킷이 NIC → IP → TCP/UDP → 소켓 → 프로세스로 전달되는 전체 경로를 봤다. 이번 글은 그중 IP(L3)와 TCP/UDP(L4) 를 자세히 본다.
관제 로그의 대부분은 결국 다음 다섯 값으로 요약된다.
출발 IP · 출발 포트 · 목적지 IP · 목적지 포트 · 프로토콜 (5-tuple)
방화벽 로그, IDS 경보, ss 출력, sshd 로그 모두 이 값들을 담고 있다. 이 값의 의미와 TCP 연결 과정을 알아야 "이 연결은 누가 시작했는가", "연결이 실제로 성립했는가"를 판단할 수 있다.
| 구분 | 대역 | 의미 |
|---|---|---|
| 사설 IP | 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 | 내부망. 인터넷에서 직접 라우팅되지 않음 |
| 루프백 | 127.0.0.0/8 | 자기 자신 |
| 문서용 | 192.0.2.0/24, 198.51.100.0/24, 203.0.113.0/24 | 예시·실습용 (이 시리즈의 외부 IP 예시) |
| 공인 IP | 그 외 | 인터넷 주소 |
/24 같은 CIDR 표기는 앞 24비트가 네트워크 부분이라는 뜻이다. 10.0.0.200/24는 10.0.0.0 ~ 10.0.0.255가 같은 네트워크다.
관제에서 출발지가 사설 IP인 공격 은 특히 중요하다. 외부 공격이 아니라 이미 내부에 들어온 공격자의 횡적 이동(Lateral Movement) 일 수 있기 때문이다(49·50편 시나리오의 10.0.0.128).
포트는 한 IP 안에서 어느 프로세스(서비스)로 보낼지 를 정하는 번호(0~65535)다.
| 범위 | 이름 | 예 |
|---|---|---|
| 0–1023 | 잘 알려진 포트 (root만 바인딩 가능) | 22 SSH, 25 SMTP, 53 DNS, 80 HTTP, 443 HTTPS |
| 1024–49151 | 등록 포트 | 3306 MySQL, 5432 PostgreSQL, 8080 대체 HTTP |
| 32768–60999 | 임시 포트 (Linux 기본) | 클라이언트가 접속할 때 자동 할당 |
cat /proc/sys/net/ipv4/ip_local_port_range로 임시 포트 범위를, /etc/services로 포트 이름을 확인할 수 있다. 포트 번호만으로 서비스를 단정하면 안 된다. 공격자는 443이나 53처럼 흔한 포트로 다른 프로토콜을 쓰기도 한다.
| 항목 | TCP | UDP |
|---|---|---|
| 연결 | 3-way handshake로 연결 수립 | 연결 없음 |
| 신뢰성 | 순서 보장, 손실 시 재전송 | 보장 없음 |
| 속도·부하 | 상대적으로 무거움 | 가벼움 |
| 용도 | SSH, HTTP(S), DB | DNS, NTP, syslog(514), 스트리밍 |
| 위조 | handshake 때문에 출발 IP 위조가 어려움 | 출발 IP 위조 쉬움 (반사·증폭 공격) |

실습 환경에서 curl http://127.0.0.1:8080/를 실행하며 tcpdump로 캡처한 실제 패킷이다(타임스탬프와 옵션 일부 생략).
$ sudo tcpdump -i lo -nn 'tcp port 8080'
127.0.0.1.55972 > 127.0.0.1.8080: Flags [S], seq 2106989631, win 65495, length 0
127.0.0.1.8080 > 127.0.0.1.55972: Flags [S.], seq 1862633251, ack 2106989632, length 0
127.0.0.1.55972 > 127.0.0.1.8080: Flags [.], ack 1, length 0
127.0.0.1.55972 > 127.0.0.1.8080: Flags [P.], seq 1:78, ack 1, length 77: HTTP: GET / HTTP/1.1
127.0.0.1.8080 > 127.0.0.1.55972: Flags [P.], seq 1:158, ack 78, length 157: HTTP: HTTP/1.0 200 OK
...
127.0.0.1.8080 > 127.0.0.1.55972: Flags [F.], seq 4614, ack 78, length 0
| 줄 | 의미 |
|---|---|
[S] | 클라이언트(임시 포트 55972)가 연결 요청. 초기 순서 번호 2106989631 |
[S.] | 서버가 수락(SYN+ACK). ack 2106989632 = 상대 번호 + 1 |
[.] | 클라이언트가 확인. 여기서 연결 수립(ESTABLISHED) |
[P.] | 데이터 전송 (HTTP 요청 77바이트 → 응답) |
[F.] | 서버가 먼저 종료 요청 (HTTP/1.0 서버라 응답 후 바로 닫음) |
tcpdump는 handshake 이후 순서 번호를 상대값 (ack 1, seq 1:78)으로 보여준다.
TCP 상태 전이. 연결 직후 ss로 본 실제 상태다.
$ ss -tan '( sport = :8080 or dport = :8080 )'
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 5 127.0.0.1:8080 0.0.0.0:*
TIME-WAIT 0 0 127.0.0.1:8080 127.0.0.1:38102
먼저 연결을 닫은 쪽 에 TIME-WAIT이 약 60초 남는다(늦게 도착하는 패킷을 처리하기 위함). 이번에는 서버가 먼저 닫았으므로 서버 포트 8080 쪽에 남았다.
| 상태 | 의미 | 관제에서 |
|---|---|---|
LISTEN | 접속 대기 | 열린 서비스 목록 (44편) |
SYN-SENT | 요청 보냄, 응답 대기 | 외부로 계속 SYN만 → 스캔·C2 연결 실패 |
SYN-RECV | SYN 받고 SYN+ACK 보냄 | 대량이면 SYN flood |
ESTABLISHED | 연결 수립 | 실제 통신 중 |
TIME-WAIT | 먼저 닫은 쪽이 대기 | 대량이면 짧은 연결이 많음 |
CLOSE-WAIT | 상대가 닫았는데 내가 안 닫음 | 대량이면 애플리케이션 버그 |
닫힌 포트에 SYN을 보내면 커널이 RST로 응답한다. 방화벽이 조용히 버리면(DROP) 응답이 없다. 포트 스캐너는 이 차이로 포트 상태(open / closed / filtered)를 판단한다.
# 1) 잘 알려진 포트 이름
grep -wE '^(ssh|http|https|domain|ntp|syslog)' /etc/services
# 2) 임시 포트 범위
cat /proc/sys/net/ipv4/ip_local_port_range
# 3) handshake 관찰 (터미널 2개)
sudo tcpdump -i lo -nn 'tcp port 8080' # 터미널 A
python3 -m http.server 8080 --bind 127.0.0.1 & # 터미널 B
curl -s -o /dev/null http://127.0.0.1:8080/
# 4) TCP 상태별 개수
ss -tan | awk 'NR>1 {print $1}' | sort | uniq -c
# 5) 닫힌 포트에 대한 응답 (RST) 관찰
sudo tcpdump -i lo -nn 'tcp port 9999' &
curl -s http://127.0.0.1:9999/ ; echo "exit=$?" # exit=7 (연결 거부)
# 6) UDP 대기 소켓
ss -ulnp
아래 출력은 형식 설명용 예시다.
① 포트 스캔 흔적 (tcpdump)
10.0.0.128.41022 > 10.0.0.200.21: Flags [S]
10.0.0.200.21 > 10.0.0.128.41022: Flags [R.] ← 닫힘
10.0.0.128.41022 > 10.0.0.200.22: Flags [S]
10.0.0.200.22 > 10.0.0.128.41022: Flags [S.] ← 열림
10.0.0.128.41022 > 10.0.0.200.22: Flags [R] ← 연결 완성 없이 끊음
10.0.0.128.41022 > 10.0.0.200.23: Flags [S]
...
같은 출발 포트에서 목적지 포트만 바뀌며 SYN 이 이어지고, 열린 포트에는 handshake를 끝내지 않고 RST로 끊는다. 전형적인 SYN 스캔 이다. 출발지가 사설 IP(10.0.0.128)이므로 내부 호스트가 이미 장악되었을 가능성을 함께 본다.
② 상태 분포
1520 SYN-RECV
38 ESTAB
6 LISTEN
SYN-RECV가 비정상적으로 많다 → handshake를 끝내지 않는 대량 요청(SYN flood) 의심. sysctl net.ipv4.tcp_syncookies(기본 1)가 켜져 있는지 확인한다.
| 관찰 | 의미 | 대응 |
|---|---|---|
| 한 IP → 여러 포트 SYN | 포트 스캔 (T1046) | 출발지 확인, 방화벽 차단 |
| 여러 IP → 한 포트 SYN 대량 | SYN flood | syncookies, 상위 장비 차단 |
| 내부 IP 출발 스캔 | 횡적 이동 준비 | 출발 호스트 침해 조사 |
| 서버 → 외부 비표준 포트 ESTAB | C2 통신 가능성 | 프로세스 식별 (44편) |
| 흔한 포트로 다른 프로토콜 | 탐지 회피 (예: 443으로 비 TLS) | 포트가 아닌 프로세스 로 판단 |
| UDP 출발 IP | 위조 가능 | UDP 로그의 IP는 신뢰도 낮음 |
SRC=10.0.0.128 DST=10.0.0.200 PROTO=TCP SPT=41022 DPT=23 FLAGS=SYN ACTION=DROP
| 필드 | 해석 |
|---|---|
| SRC / SPT | 누가 (임시 포트) |
| DST / DPT | 어디의 어떤 서비스 (23 = telnet) |
| FLAGS=SYN | 연결 시도 (성립 여부는 아직 모름) |
| ACTION=DROP | 차단됨 |
SYN만 있는 기록은 "시도"다. 실제 연결이 성립했는지는 ESTABLISHED 기록이나 서버 측 로그(sshd Accepted, 웹 로그)로 확인해야 한다.
[Alert] IDS: 10.0.0.128 → 10.0.0.0/24 다수 호스트 · 다수 포트 SYN (02:05)
↓
[판단 1] 사설 IP 출발 → 외부 공격이 아니라 내부 호스트의 스캔
↓
[서버] 10.0.0.200 에서 22번만 SYN+ACK → 노출 서비스는 SSH
↓
[후속] 02:10부터 secure 에 10.0.0.128 Failed password 급증 (49편)
↓
[Response] 10.0.0.128 격리·조사, 22번 접근을 관리망으로 제한 (46편)
SYN → SYN+ACK → ACK로 연결되고, FIN(정상)·RST(강제)로 끝난다.TIME-WAIT이 남는다(실습 확인). SYN-RECV 급증은 SYN flood.다음 글 「43. Socket 이해」 에서는 애플리케이션과 커널 네트워크를 잇는 소켓 을 다룬다. 소켓이 파일 디스크립터라는 의미(/proc/PID/fd의 socket:[inode]), socket → bind → listen → accept 흐름, 그리고 리버스 쉘 이 소켓 관점에서 어떻게 보이는지를 정리한다.