서비스 · 프로세스 관리 16 / 50 · Part 2. 시그널·자원·세션
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정 analyst)

1. 들어가며

방화벽 로그에서 서버가 낯선 IP의 4444번 포트로 접속한 기록이 발견되었다. 관제 요원이 가장 먼저 답해야 할 질문은 "그 연결을 만든 프로세스는 무엇인가?" 다. IP와 포트만으로는 대응할 수 없고, 프로세스를 알아야 실행 파일·계정·부모를 추적할 수 있다.

이번 글에서는 ss -p와 lsof -i로 포트·연결과 프로세스를 연결 하고, 도구 없이 /proc/net/tcp의 소켓 inode로 프로세스를 역추적하는 방법을 실습한다.

「리눅스 시스템 기초 43·44편」에서 소켓과 ss 명령을 네트워크 관점에서 다뤘다. 이번 글은 소켓을 가진 프로세스 를 찾는 데 집중한다.


2. 핵심 개념

2-1. 소켓도 파일 디스크립터다

15편에서 fd는 파일뿐 아니라 소켓도 가리킨다고 했다. 프로세스의 /proc/PID/fd에서 socket:[25235] 같은 링크가 보이는데, 괄호 안의 숫자가 소켓 inode 다. 커널의 연결 테이블(/proc/net/tcp)에도 같은 inode가 기록되어 있어 둘을 연결할 수 있다.

2-2. ss 주요 옵션

옵션의미
-t / -uTCP / UDP
-lLISTEN(대기) 소켓만
-n이름 변환 없이 숫자로
-p소켓을 가진 프로세스 (다른 사용자 것은 root만)
-e확장 정보 (uid, inode, cgroup)
state established상태 필터
'( sport = :22 )'포트 필터 표현식

2-3. lsof -i

명령의미
lsof -i :80808080 포트 관련 모든 소켓
lsof -iTCP -sTCP:LISTENLISTEN 중인 TCP
lsof -i @10.0.0.5특정 원격 주소
lsof -nP -p PID -a -i특정 프로세스의 네트워크 소켓만

3. 동작 원리

포트 → 소켓 inode → 프로세스 역추적

/proc/net/tcp 한 줄
   sl  local_address rem_address   st ... uid  ...  inode
    0: 00000000:0016 00000000:0000 0A     0         4506
         │      │                  │      │          └ 소켓 inode
         │      └ 포트 0x0016 = 22  │      └ 소유 UID 0 (root)
         └ IP 0.0.0.0 (리틀 엔디언 16진수)  └ 0A = LISTEN

find /proc/*/fd -lname 'socket:[4506]'  → 이 소켓을 가진 PID

ss -p와 lsof -i는 이 과정을 자동으로 해 준다. 모든 /proc/PID/fd를 읽어야 하므로 다른 사용자의 fd를 읽을 수 있는 root 권한 이 있어야 전체가 보인다.


4. 명령어 실습

# 1) (analyst) 사용자 프로세스가 연 포트와 연결
cd /tmp; python3 -m http.server 8080 --bind 127.0.0.1 > /dev/null 2>&1 &
W=$!; (sleep 20 | nc 127.0.0.1 8080 > /dev/null &)
ss -tlnp 'sport = :8080'
ss -tnp state established '( sport = :8080 or dport = :8080 )'
lsof -nP -i :8080
ls -l /proc/$W/fd | grep socket

# 2) (root) 모든 프로세스의 소켓
ss -tlnp
ss -tnp state established | head -5
lsof -nP -iTCP -sTCP:LISTEN

# 3) (Ubuntu, root) 도구 없이 역추적
awk 'NR==1 || $4=="0A"' /proc/net/tcp
L=$(awk '$4=="0A" && $2 ~ /:0016$/ {print $10; exit}' /proc/net/tcp)
find /proc/[0-9]*/fd -lname "socket:\[$L\]" 2>/dev/null
ps -o pid,user,cmd -p <찾은 PID들>
ss -tlnpe 'sport = :22'

5. 실행 결과

실제 실행 결과 — Rocky Linux 9.8 · analyst@rocky9-lab — 사용자 프로세스가 연 포트와 연결

실제 실행 결과 — Rocky Linux 9.8 · root@rocky9-lab — root 로 보면 모든 프로세스의 소켓이 보인다

실제 실행 결과 — Ubuntu 24.04.5 · root@ubuntu-lab — 소켓 inode 로 프로세스 역추적 (/proc/net/tcp)

텍스트 원본(실제 출력):

[analyst@rocky9-lab ~]$ cd /tmp; python3 -m http.server 8080 --bind 127.0.0.1 > /dev/null 2>&1 &
[analyst@rocky9-lab tmp]$ W=$!; sleep 1; (sleep 20 | nc 127.0.0.1 8080 > /dev/null &) ; sleep 0.5
[analyst@rocky9-lab tmp]$ ss -tlnp 'sport = :8080'
State    Recv-Q   Send-Q     Local Address:Port      Peer Address:Port   Process
LISTEN   0        5              127.0.0.1:8080           0.0.0.0:*       users:(("python3",pid=2168,fd=3))
[analyst@rocky9-lab tmp]$ ss -tnp state established '( sport = :8080 or dport = :8080 )'
Recv-Q    Send-Q       Local Address:Port         Peer Address:Port     Process
0         0                127.0.0.1:8080            127.0.0.1:42500     users:(("python3",pid=2168,fd=4))
0         0                127.0.0.1:42500           127.0.0.1:8080      users:(("nc",pid=2172,fd=3))
[analyst@rocky9-lab tmp]$ lsof -nP -i :8080
COMMAND  PID    USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
python3 2168 analyst    3u  IPv4  27483      0t0  TCP 127.0.0.1:8080 (LISTEN)
python3 2168 analyst    4u  IPv4  27486      0t0  TCP 127.0.0.1:8080->127.0.0.1:42500 (ESTABLISHED)
nc      2172 analyst    3u  IPv4  27706      0t0  TCP 127.0.0.1:42500->127.0.0.1:8080 (ESTABLISHED)
[analyst@rocky9-lab tmp]$ ls -l /proc/$W/fd | grep socket
lrwx------ 1 analyst analyst 64 Sep 24 11:51 3 -> socket:[27483]
lrwx------ 1 analyst analyst 64 Sep 24 11:51 4 -> socket:[27486]
[root@rocky9-lab ~]# ss -tlnp
State     Recv-Q    Send-Q       Local Address:Port       Peer Address:Port   Process
LISTEN    0         128                0.0.0.0:22              0.0.0.0:*       users:(("sshd",pid=31,fd=7))
[root@rocky9-lab ~]# ss -tnp state established | head -5
Recv-Q          Send-Q                     Local Address:Port                     Peer Address:Port           Process
0               0                             172.17.0.2:22                         172.17.0.1:38276           users:(("sshd-session",pid=2201,fd=4),("sshd-session",pid=2188,fd=4))
[root@rocky9-lab ~]# lsof -nP -iTCP -sTCP:LISTEN
COMMAND PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
sshd     31 root    7u  IPv4   4283      0t0  TCP *:22 (LISTEN)
[root@rocky9-lab ~]# pkill -u analyst -f http.server; pkill -u analyst -x nc; true
root@ubuntu-lab:~# awk 'NR==1 || $4=="0A"' /proc/net/tcp
  sl  local_address rem_address   st tx_queue rx_queue tr tm->when retrnsmt   uid  timeout inode
   0: 00000000:0016 00000000:0000 0A 00000000:00000000 00:00000000 00000000     0        0 4506 1 ffff88810308ef00 100 0 0 10 0
   1: 3600007F:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000   995        0 4498 1 ffff88810308ca00 100 0 0 10 5
   2: 3500007F:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000   995        0 4496 1 ffff88810308dc80 100 0 0 10 5
root@ubuntu-lab:~# L=$(awk '$4=="0A" && $2 ~ /:0016$/ {print $10; exit}' /proc/net/tcp); echo "22번 포트(0x0016) LISTEN 소켓 inode=$L"
22번 포트(0x0016) LISTEN 소켓 inode=4506
root@ubuntu-lab:~# find /proc/[0-9]*/fd -lname "socket:\[$L\]" 2>/dev/null
/proc/1/fd/75
/proc/95/fd/3
root@ubuntu-lab:~# ps -o pid,user,cmd -p $(find /proc/[0-9]*/fd -lname "socket:\[$L\]" 2>/dev/null | cut -d/ -f3 | sort -u)
    PID USER     CMD
      1 root     /sbin/init
     95 root     sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
root@ubuntu-lab:~# ss -tlnpe 'sport = :22'
State       Recv-Q      Send-Q             Local Address:Port             Peer Address:Port      Process
LISTEN      0           4096                     0.0.0.0:22                    0.0.0.0:*          users:(("sshd",pid=95,fd=3),("systemd",pid=1,fd=75)) ino:4506 sk:7 cgroup:/docker/4c05aed4f49649e9c00450c976b68c6f915f2a6a31a399c6f43398e6fad60cc1/system.slice/ssh.socket <->

6. 결과 해석

관찰의미
LISTEN 127.0.0.1:8080 users:(("python3",pid=1921,fd=3))python3(1921)이 fd 3으로 8080을 로컬 전용 으로 대기 중
ESTABLISHED 두 줄한 TCP 연결은 양쪽 끝에 소켓이 하나씩 있다. 서버 쪽 python3 fd 4, 클라이언트 쪽 nc fd 3
클라이언트 포트 52058운영체제가 임시로 배정한 에페메럴 포트 다
lsof -i :8080의 (LISTEN), (ESTABLISHED)ss와 같은 정보를 fd 번호·소켓 inode(DEVICE 25235)와 함께 보여 준다
/proc/1921/fd의 3 -> socket:[25235], 4 -> socket:[25238]lsof가 보여 준 inode와 일치한다. 소켓이 fd라는 증거
root ss -tlnp → sshd pid=31 만 LISTEN실습 서버에서 외부에 열린 포트는 22번 하나다
ESTABLISHED 172.17.0.2:22 ↔ 172.17.0.1:38276, sshd-session 2188·2201실습 명령을 실행 중인 SSH 연결 자체다. [priv]와 사용자 세션 두 프로세스가 같은 소켓을 공유 한다
Ubuntu /proc/net/tcp의 0016, 003522번(SSH)과 53번(127.0.0.53/54, systemd-resolved) 대기 소켓
inode 4506을 가진 fd가 PID 1과 PID 95 두 곳Ubuntu 24.04는 ssh.socket으로 systemd가 22번 포트를 먼저 열고 sshd에 넘겨준다(소켓 활성화, 37편)
ss -e 출력의 ino:4506 ... cgroup:.../ssh.socket확장 정보로 inode와 소속 unit을 한 번에 확인할 수 있다

7. 보안 관점

주제내용
리버스 셸내부 서버가 외부로 연결을 만들고 그 소켓을 가진 프로세스가 bash, sh, python이면 리버스 셸을 의심한다 (44편)
백도어 포트목록에 없는 LISTEN 포트, 특히 0.0.0.0에 바인딩된 높은 번호 포트는 백도어 후보다
바인딩 주소127.0.0.1은 로컬 전용, 0.0.0.0/[::]는 모든 인터페이스. 관리 도구가 0.0.0.0에 열려 있으면 노출이다
루트킷ss·netstat 바이너리가 변조되면 특정 포트를 숨길 수 있다. /proc/net/tcp 직접 확인과 외부에서의 포트 스캔 결과를 비교한다
권한일반 계정으로 ss -p를 실행하면 다른 계정의 프로세스가 빠진다. "프로세스 없음"은 권한 부족일 수 있다

8. 보안관제 관점

[Detection]  방화벽: 10.0.2.15 → 203.0.113.50:4444 ESTABLISHED
     ↓
[서버 확인]  ss -tnp state established '( dport = :4444 )'
             → users:(("bash",pid=3321,fd=0),("bash",pid=3321,fd=1),("bash",pid=3321,fd=2))
     ↓
[판단]       bash 의 표준 입출력(fd 0·1·2)이 외부 소켓 → 전형적인 리버스 셸
     ↓
[추적]       pstree -ps 3321 · ls -l /proc/3321/exe /proc/3321/cwd · lsof -p 3321
     ↓
[Response]   방화벽 차단 → 증거 수집 → 프로세스 종료 → 침투 경로 조사
확인명령
외부 연결 전체ss -tnp state established
대기 포트 기준선 비교ss -tlnp 결과를 저장해 두고 diff
특정 프로세스의 소켓lsof -nP -a -p PID -i
도구 없이/proc/net/tcp + find /proc/*/fd -lname 'socket:[INODE]'

9. 실무에서 자주 발생하는 실수

실수결과예방
일반 계정으로 ss -tlnpProcess 열이 비어 오판root로 실행
-n 없이 실행DNS 조회로 느리고 서비스 이름으로 표시항상 -n (lsof -nP)
netstat만 사용최신 배포판에 기본 미설치ss 사용
LISTEN만 확인외부로 나가는 연결(리버스 셸)을 놓침ESTABLISHED도 확인
/proc/net/tcp의 IP를 그대로 읽음리틀 엔디언이라 뒤집어 읽어야 함포트는 그대로 16진수 변환, IP는 바이트 역순

10. 실습 체크리스트

[ ] ss -tlnp 로 LISTEN 포트와 프로세스를 확인했다
[ ] ESTABLISHED 연결의 양쪽 소켓과 에페메럴 포트를 확인했다
[ ] lsof -i :PORT 결과와 /proc/PID/fd 의 socket inode 를 대조했다
[ ] /proc/net/tcp 에서 포트(16진수)와 상태(0A)를 읽었다
[ ] 소켓 inode 로 소유 프로세스를 역추적했다
[ ] Ubuntu 에서 systemd(PID 1)가 22번 소켓을 함께 가진 것을 확인했다

11. 핵심 정리

  • 소켓도 fd이며, socket:[inode]로 커널의 연결 테이블과 연결된다.
  • ss -tnp, lsof -nP -i로 포트·연결을 가진 프로세스를 찾는다. 전체를 보려면 root가 필요하다.
  • /proc/net/tcp + /proc/*/fd만으로도 역추적할 수 있어 도구 변조에 대응할 수 있다.
  • 연결의 소유 프로세스가 셸이고 fd 0·1·2가 소켓이면 리버스 셸이다.
  • 대기 포트 목록은 기준선으로 저장해 두고 변화를 비교한다.

12. 다음 편 예고

다음 글 「17. 프로세스 자원 제한 (ulimit·limits.conf)」 에서는 프로세스가 열 수 있는 파일 수, 만들 수 있는 프로세스 수 같은 한도를 다룬다. Too many open files 오류를 재현하고, limits.conf는 로그인에만, 서비스는 LimitNOFILE로 설정해야 하는 이유를 확인한다.


참고 자료


시리즈 이동

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

0개의 댓글