리눅스 시스템 기초 · 입문편 — 본문의 "N편"은 입문 과정 번호다. 번호별 글과 전체 250편 구성은 통합 로드맵에서 확인할 수 있다.

리눅스 시스템 기초 43 / 50 · Part 5. 네트워크·보안·SOC
실습 환경: Rocky Linux 9 (10.0.0.200) · 소켓 fd·inode·ss 출력은 실습 컨테이너의 실제 출력
이전 글: 42. IP / TCP / UDP / Port

1. 들어가며

42편에서 TCP 연결이 커널 안에서 어떻게 만들어지는지 봤다. 그런데 애플리케이션(sshd, httpd, 악성코드)은 그 연결을 어떻게 사용할까? 답은 소켓(Socket) 이다.

13편에서 소켓도 파일 종류의 하나(s)라고 했다. Linux에서는 네트워크 연결도 파일처럼 다룬다. 프로세스 입장에서 소켓은 파일 디스크립터(fd) 번호 하나 이고, 파일처럼 읽고 쓴다.

이 사실이 관제에서 중요한 이유는, 네트워크 연결을 프로세스에 연결 할 수 있기 때문이다.

  • "어떤 프로세스가 외부 IP와 통신 중인가?" → 소켓의 주인을 찾는다.
  • "이 bash는 정상 로그인 쉘인가, 리버스 쉘인가?" → bash의 fd가 무엇을 가리키는지 본다.

2. 핵심 개념

2-1. 소켓의 종류

종류주소 형식예
TCP 소켓 (SOCK_STREAM)IP:포트sshd, httpd
UDP 소켓 (SOCK_DGRAM)IP:포트DNS, syslog
Unix 도메인 소켓파일 경로 (/run/...sock)같은 서버 안 프로세스 간 통신 — systemd, D-Bus, docker.sock, mysql.sock
Raw 소켓—ping, 패킷 생성 도구 (root 또는 CAP_NET_RAW 필요)

Unix 도메인 소켓은 네트워크를 거치지 않지만, 파일 권한으로 접근을 통제한다. 예를 들어 /var/run/docker.sock에 쓰기 권한이 있으면 사실상 root 권한을 얻을 수 있어 권한 상승 점검 대상이다(20편).

2-2. 서버와 클라이언트의 System Call 흐름

단계서버클라이언트
1socket() — 소켓 생성, fd 반환socket()
2bind() — IP:포트 지정(생략 시 임시 포트 자동)
3listen() — 대기 상태(LISTEN)
4accept() — 연결 1개마다 새 fd 반환connect() — handshake
5recv()/send()send()/recv()
6close()close()

중요한 점: accept()는 대기 소켓과 별도로 연결마다 새 소켓 을 만든다. 그래서 sshd 하나가 여러 접속을 동시에 처리할 수 있다.

2-3. 소켓을 보는 세 곳

위치내용
/proc/<PID>/fd/Nsocket:[inode번호] — 프로세스가 가진 소켓
/proc/net/tcp, tcp6, udp, unix커널의 소켓 표 (주소·상태·inode, 16진수)
ss -p, lsof -i위 둘을 inode로 연결 해서 "어떤 프로세스의 어떤 연결"로 보여줌

3. 동작 원리

소켓 = 네트워크 연결을 가리키는 파일 디스크립터

한 파이썬 프로세스 안에서 서버 소켓을 열고(9000번), 스스로 접속한 뒤 accept()까지 수행한 실제 결과다.

pid=2016 listen_fd=3 client_fd=4 accepted_fd=5 peer=('127.0.0.1', 39000)

$ ls -l /proc/2016/fd          # 소켓만 발췌
3 -> socket:[8600]
4 -> socket:[8601]
5 -> socket:[8602]

$ ss -tanpe '( sport = :9000 or dport = :9000 )'      # 일부 필드 생략
State  Local Address:Port  Peer Address:Port Process
LISTEN     127.0.0.1:9000       0.0.0.0:*     users:(("python3",pid=2016,fd=3)) ino:8600
ESTAB      127.0.0.1:9000     127.0.0.1:39000 users:(("python3",pid=2016,fd=5)) ino:8602
ESTAB      127.0.0.1:39000    127.0.0.1:9000  users:(("python3",pid=2016,fd=4)) ino:8601
fdinode역할
38600listen() 중인 대기 소켓 (LISTEN)
48601클라이언트 소켓 — 임시 포트 39000이 자동 할당됨
58602accept()가 반환한 새 연결 소켓

/proc/PID/fd의 inode 번호와 ss의 ino: 값이 정확히 일치 한다. ss -p는 바로 이 방식으로 연결과 프로세스를 짝지어 보여준다.

커널의 원본 표 /proc/net/tcp는 16진수로 되어 있다. 실습 환경의 8080 대기 소켓(inode 7085)은 이렇게 보인다.

  sl  local_address rem_address   st ... uid  timeout inode
   3: 0100007F:1F90 00000000:0000 0A ...   0        0 7085

0100007F = 127.0.0.1(바이트 역순), 1F90 = 8080, st 0A = LISTEN. ss가 없는 최소 설치 환경이나, 명령어 변조가 의심될 때(7편) 이 표를 직접 읽을 수 있다.


4. 실습

# 1) 특정 프로세스의 소켓 fd
sudo ls -l /proc/$(pgrep -o sshd)/fd | grep socket

# 2) inode 로 연결 찾기
sudo ss -tanpe | grep 'ino:<inode>'

# 3) lsof 로 네트워크 파일
sudo lsof -nP -i                    # 전체
sudo lsof -nP -i TCP:22             # 포트
sudo lsof -nP -p <PID> | grep -E 'IPv4|IPv6|unix'

# 4) Unix 도메인 소켓
ss -xlp | head
ls -l /run/*.sock /var/run/docker.sock 2>/dev/null

# 5) 커널 표 직접 읽기 (LISTEN = 0A)
awk 'NR>1 && $4=="0A" {print $2, $10}' /proc/net/tcp

5. 결과 분석

아래 출력은 형식 설명용 예시다.

리버스 쉘 의심 프로세스

리버스 쉘은 공격 대상 서버가 공격자 쪽으로 먼저 연결 을 걸고, 그 연결에 쉘의 입출력을 붙이는 방식이다. 방화벽이 들어오는 연결은 막아도 나가는 연결은 허용하는 경우가 많아 자주 쓰인다. 소켓 관점에서는 이렇게 보인다.

$ sudo ls -l /proc/7902/fd
0 -> socket:[48213]
1 -> socket:[48213]
2 -> socket:[48213]

$ sudo ss -tanp | grep 'pid=7902,'
ESTAB 0 0 10.0.0.200:51844 203.0.113.50:4444 users:(("bash",pid=7902,fd=0),("bash",pid=7902,fd=1),("bash",pid=7902,fd=2))
단서해석
bash의 fd 0·1·2가 모두 같은 socket입력·출력·에러가 터미널(/dev/pts/N)이 아니라 네트워크 연결
연결 방향서버의 임시 포트 → 외부 IP의 4444 (서버가 먼저 연결)
부모 프로세스35편처럼 httpd → sh → bash라면 웹셸 경유

정상적인 SSH 로그인 쉘의 fd 0·1·2는 /dev/pts/0 같은 가상 터미널 을 가리키고, 소켓은 부모인 sshd가 가진다. 쉘이 직접 외부 소켓을 쥐고 있는 것 이 핵심 이상 신호다.


6. 보안 관점

관찰의미
쉘·인터프리터의 fd 0/1/2 = socket리버스 쉘 (T1059)
웹 서버 워커가 외부로 ESTAB웹셸·RCE 후 외부 통신
알 수 없는 프로세스의 LISTEN백도어 대기 포트 (바인드 쉘)
(deleted) 실행 파일 + 소켓실행 후 삭제한 악성코드 (29편)
쓰기 가능한 docker.sock 등권한 상승 경로
ss와 /proc/net/tcp 결과 불일치ss 변조·루트킷 의심 (7·9편)

7. SOC / 보안관제 활용

7-1. 소켓을 쥔 쉘 찾기

for p in $(pgrep -x 'bash|sh|dash|zsh|python3|perl'); do
  if sudo ls -l /proc/$p/fd/0 2>/dev/null | grep -q socket; then
    echo "[!] PID $p  $(ps -o user=,args= -p $p)"
    sudo ss -tanp | grep "pid=$p,"
  fi
done

7-2. 분석 흐름

[Alert]   EDR: bash 프로세스의 외부 네트워크 연결 (10.0.0.200 → 203.0.113.50:4444)
   ↓
[fd]      /proc/7902/fd/0,1,2 → socket:[48213] → 리버스 쉘 확정
   ↓
[트리]    pstree -aps 7902 → httpd → sh → bash (35편)
   ↓
[증적]    kill -STOP 7902 → ss·lsof·/proc 수집 (34편)
   ↓
[Response] 203.0.113.50 차단, 웹 취약점 조치, 같은 IOC 전사 검색

8. 핵심 정리

  • 소켓은 프로세스 입장에서 fd 하나 다. /proc/PID/fd/N -> socket:[inode].
  • 서버: socket → bind → listen → accept(연결마다 새 fd). 클라이언트: socket → connect.
  • ss -p·lsof -i는 /proc/PID/fd와 /proc/net/tcp를 inode로 짝지어 보여준다(실습에서 일치 확인).
  • Unix 도메인 소켓은 파일 권한으로 보호된다. docker.sock 쓰기 권한 = 권한 상승 위험.
  • 쉘의 fd 0·1·2가 외부 소켓 이면 리버스 쉘이다.

9. 다음 글

다음 글 「44. ss와 ip 명령어」 에서는 지금까지의 개념을 명령어로 정리한다. ss -tulnp로 열린 포트와 담당 프로세스 를 확인하고, 필터 문법, 0.0.0.0/127.0.0.1 바인딩 판단, 외부 연결 헌팅, netstat과의 차이를 다룬다.


참고 자료

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

0개의 댓글