리눅스 시스템 기초 · 입문편 — 본문의 "N편"은 입문 과정 번호다. 번호별 글과 전체 250편 구성은 통합 로드맵에서 확인할 수 있다.
리눅스 시스템 기초 43 / 50 · Part 5. 네트워크·보안·SOC
실습 환경: Rocky Linux 9 (10.0.0.200) · 소켓 fd·inode·ss출력은 실습 컨테이너의 실제 출력
이전 글: 42. IP / TCP / UDP / Port
42편에서 TCP 연결이 커널 안에서 어떻게 만들어지는지 봤다. 그런데 애플리케이션(sshd, httpd, 악성코드)은 그 연결을 어떻게 사용할까? 답은 소켓(Socket) 이다.
13편에서 소켓도 파일 종류의 하나(s)라고 했다. Linux에서는 네트워크 연결도 파일처럼 다룬다. 프로세스 입장에서 소켓은 파일 디스크립터(fd) 번호 하나 이고, 파일처럼 읽고 쓴다.
이 사실이 관제에서 중요한 이유는, 네트워크 연결을 프로세스에 연결 할 수 있기 때문이다.
| 종류 | 주소 형식 | 예 |
|---|---|---|
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편).
| 단계 | 서버 | 클라이언트 |
|---|---|---|
| 1 | socket() — 소켓 생성, fd 반환 | socket() |
| 2 | bind() — IP:포트 지정 | (생략 시 임시 포트 자동) |
| 3 | listen() — 대기 상태(LISTEN) | |
| 4 | accept() — 연결 1개마다 새 fd 반환 | connect() — handshake |
| 5 | recv()/send() | send()/recv() |
| 6 | close() | close() |
중요한 점: accept()는 대기 소켓과 별도로 연결마다 새 소켓 을 만든다. 그래서 sshd 하나가 여러 접속을 동시에 처리할 수 있다.
| 위치 | 내용 |
|---|---|
/proc/<PID>/fd/N | socket:[inode번호] — 프로세스가 가진 소켓 |
/proc/net/tcp, tcp6, udp, unix | 커널의 소켓 표 (주소·상태·inode, 16진수) |
ss -p, lsof -i | 위 둘을 inode로 연결 해서 "어떤 프로세스의 어떤 연결"로 보여줌 |

한 파이썬 프로세스 안에서 서버 소켓을 열고(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
| fd | inode | 역할 |
|---|---|---|
| 3 | 8600 | listen() 중인 대기 소켓 (LISTEN) |
| 4 | 8601 | 클라이언트 소켓 — 임시 포트 39000이 자동 할당됨 |
| 5 | 8602 | accept()가 반환한 새 연결 소켓 |
/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편) 이 표를 직접 읽을 수 있다.
# 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
아래 출력은 형식 설명용 예시다.
리버스 쉘 의심 프로세스
리버스 쉘은 공격 대상 서버가 공격자 쪽으로 먼저 연결 을 걸고, 그 연결에 쉘의 입출력을 붙이는 방식이다. 방화벽이 들어오는 연결은 막아도 나가는 연결은 허용하는 경우가 많아 자주 쓰인다. 소켓 관점에서는 이렇게 보인다.
$ 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가 가진다. 쉘이 직접 외부 소켓을 쥐고 있는 것 이 핵심 이상 신호다.
| 관찰 | 의미 |
|---|---|
| 쉘·인터프리터의 fd 0/1/2 = socket | 리버스 쉘 (T1059) |
| 웹 서버 워커가 외부로 ESTAB | 웹셸·RCE 후 외부 통신 |
| 알 수 없는 프로세스의 LISTEN | 백도어 대기 포트 (바인드 쉘) |
(deleted) 실행 파일 + 소켓 | 실행 후 삭제한 악성코드 (29편) |
쓰기 가능한 docker.sock 등 | 권한 상승 경로 |
ss와 /proc/net/tcp 결과 불일치 | ss 변조·루트킷 의심 (7·9편) |
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
[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 전사 검색
/proc/PID/fd/N -> socket:[inode].socket → bind → listen → accept(연결마다 새 fd). 클라이언트: socket → connect.ss -p·lsof -i는 /proc/PID/fd와 /proc/net/tcp를 inode로 짝지어 보여준다(실습에서 일치 확인).docker.sock 쓰기 권한 = 권한 상승 위험.다음 글 「44. ss와 ip 명령어」 에서는 지금까지의 개념을 명령어로 정리한다. ss -tulnp로 열린 포트와 담당 프로세스 를 확인하고, 필터 문법, 0.0.0.0/127.0.0.1 바인딩 판단, 외부 연결 헌팅, netstat과의 차이를 다룬다.