
서비스 · 프로세스 관리 16 / 50 · Part 2. 시그널·자원·세션
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정analyst)
방화벽 로그에서 서버가 낯선 IP의 4444번 포트로 접속한 기록이 발견되었다. 관제 요원이 가장 먼저 답해야 할 질문은 "그 연결을 만든 프로세스는 무엇인가?" 다. IP와 포트만으로는 대응할 수 없고, 프로세스를 알아야 실행 파일·계정·부모를 추적할 수 있다.
이번 글에서는 ss -p와 lsof -i로 포트·연결과 프로세스를 연결 하고, 도구 없이 /proc/net/tcp의 소켓 inode로 프로세스를 역추적하는 방법을 실습한다.
「리눅스 시스템 기초 43·44편」에서 소켓과 ss 명령을 네트워크 관점에서 다뤘다. 이번 글은 소켓을 가진 프로세스 를 찾는 데 집중한다.
15편에서 fd는 파일뿐 아니라 소켓도 가리킨다고 했다. 프로세스의 /proc/PID/fd에서 socket:[25235] 같은 링크가 보이는데, 괄호 안의 숫자가 소켓 inode 다. 커널의 연결 테이블(/proc/net/tcp)에도 같은 inode가 기록되어 있어 둘을 연결할 수 있다.
| 옵션 | 의미 |
|---|---|
-t / -u | TCP / UDP |
-l | LISTEN(대기) 소켓만 |
-n | 이름 변환 없이 숫자로 |
-p | 소켓을 가진 프로세스 (다른 사용자 것은 root만) |
-e | 확장 정보 (uid, inode, cgroup) |
state established | 상태 필터 |
'( sport = :22 )' | 포트 필터 표현식 |
| 명령 | 의미 |
|---|---|
lsof -i :8080 | 8080 포트 관련 모든 소켓 |
lsof -iTCP -sTCP:LISTEN | LISTEN 중인 TCP |
lsof -i @10.0.0.5 | 특정 원격 주소 |
lsof -nP -p PID -a -i | 특정 프로세스의 네트워크 소켓만 |

/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 권한 이 있어야 전체가 보인다.
# 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'



텍스트 원본(실제 출력):
[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 <->
| 관찰 | 의미 |
|---|---|
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, 0035 | 22번(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을 한 번에 확인할 수 있다 |
| 주제 | 내용 |
|---|---|
| 리버스 셸 | 내부 서버가 외부로 연결을 만들고 그 소켓을 가진 프로세스가 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를 실행하면 다른 계정의 프로세스가 빠진다. "프로세스 없음"은 권한 부족일 수 있다 |
[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]' |
| 실수 | 결과 | 예방 |
|---|---|---|
일반 계정으로 ss -tlnp | Process 열이 비어 오판 | root로 실행 |
-n 없이 실행 | DNS 조회로 느리고 서비스 이름으로 표시 | 항상 -n (lsof -nP) |
netstat만 사용 | 최신 배포판에 기본 미설치 | ss 사용 |
| LISTEN만 확인 | 외부로 나가는 연결(리버스 셸)을 놓침 | ESTABLISHED도 확인 |
/proc/net/tcp의 IP를 그대로 읽음 | 리틀 엔디언이라 뒤집어 읽어야 함 | 포트는 그대로 16진수 변환, IP는 바이트 역순 |
[ ] ss -tlnp 로 LISTEN 포트와 프로세스를 확인했다
[ ] ESTABLISHED 연결의 양쪽 소켓과 에페메럴 포트를 확인했다
[ ] lsof -i :PORT 결과와 /proc/PID/fd 의 socket inode 를 대조했다
[ ] /proc/net/tcp 에서 포트(16진수)와 상태(0A)를 읽었다
[ ] 소켓 inode 로 소유 프로세스를 역추적했다
[ ] Ubuntu 에서 systemd(PID 1)가 22번 소켓을 함께 가진 것을 확인했다
socket:[inode]로 커널의 연결 테이블과 연결된다.ss -tnp, lsof -nP -i로 포트·연결을 가진 프로세스를 찾는다. 전체를 보려면 root가 필요하다./proc/net/tcp + /proc/*/fd만으로도 역추적할 수 있어 도구 변조에 대응할 수 있다.다음 글 「17. 프로세스 자원 제한 (ulimit·limits.conf)」 에서는 프로세스가 열 수 있는 파일 수, 만들 수 있는 프로세스 수 같은 한도를 다룬다. Too many open files 오류를 재현하고, limits.conf는 로그인에만, 서비스는 LimitNOFILE로 설정해야 하는 이유를 확인한다.