서비스 · 프로세스 관리 03 / 50 · Part 1. 프로세스 기초
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (systemd로 부팅한 Docker 격리 컨테이너, 테스트 계정 analyst)

1. 들어가며

02편에서 모든 프로세스는 fork로 태어난다는 것을 확인했다. 그렇다면 모든 프로세스에는 부모가 있고, 부모의 부모를 계속 따라가면 하나의 뿌리 에 도달한다. 그 뿌리가 PID 1, 현대 Linux에서는 systemd 다.

이번 글에서는 PID와 PPID의 의미를 정리하고, SSH로 접속한 셸에서 PID 1까지 조상 체인을 직접 따라 올라가 본다. 관제에서 "이 프로세스는 누가 실행했나?"라는 질문의 답이 바로 이 트리에 있다.


2. 핵심 개념

2-1. PID와 PPID

용어의미특징
PID (Process ID)프로세스 고유 번호커널이 순서대로 할당, 종료 후 재사용 된다
PPID (Parent PID)나를 fork한 부모의 PID부모가 먼저 죽으면 새 부모(PID 1 등)로 바뀐다
PID 1커널이 부팅 후 처음 실행한 사용자 공간 프로세스systemd. 고아 프로세스 입양, 좀비 회수 담당
PID 0커널 내부의 idle 태스크ps의 PID 1 행에서 PPID가 0으로 보인다
PID 2kthreadd커널 스레드([kworker/...] 등)의 부모
pid_max할당 가능한 최대 PID + 1/proc/sys/kernel/pid_max

2-2. 트리를 보는 도구

명령어방향용도
pstree -p위 → 아래전체 트리를 PID와 함께
pstree -ps PID아래 → 위특정 프로세스의 조상 체인만
ps -ef --forest위 → 아래사용자·시각과 함께 들여쓰기 트리
pgrep -P PID -a아래특정 부모의 직계 자식 목록
/proc/PID/status의 PPid:한 단계 위스크립트로 조상을 따라갈 때

3. 동작 원리

SSH 로그인 셸의 실제 조상 체인 (Rocky Linux 9.8 실측)

SSH 접속 한 번에 프로세스가 여러 단계로 만들어지는 이유는 권한 분리(privilege separation) 때문이다.

systemd (PID 1)
  └─ sshd [listener]                    ← 22번 포트에서 접속 대기 (root)
       └─ sshd-session: analyst [priv]  ← 새 접속마다 fork, 인증 처리 (root)
            └─ sshd-session: analyst@pts/0 ← 인증 후 analyst 권한으로 낮춰 동작
                 └─ bash                ← 로그인 셸
                      └─ 내가 입력한 명령들

네트워크 데이터를 다루는 프로세스를 일반 사용자 권한으로 분리해 두면, 그 단계에서 취약점이 터져도 root 권한을 바로 얻지 못한다.

Rocky Linux 9.8의 OpenSSH는 세션 처리를 sshd-session이라는 별도 실행 파일 로 분리해 두었다. Ubuntu 24.04에서는 같은 역할이 sshd: analyst [priv], sshd: analyst@pts/0처럼 sshd 이름으로 보인다. 버전 차이일 뿐 구조는 같다.


4. 명령어 실습

# 1) PID 기본 정보
cat /proc/sys/kernel/pid_max
ps -o pid,ppid,user,cmd -p 1
echo "내 셸 PID=$$ PPID=$PPID"          # $$ = 현재 셸 PID, $PPID = 부모 PID

# 2) 전체 트리
pstree -p | head -12

# 3) 내 셸에서 PID 1까지 거슬러 올라가기 (스크립트 방식)
p=$$; while [ "$p" -ne 0 ]; do
  printf '%-6s %s\n' "$p" "$(tr '\0' ' ' < /proc/$p/cmdline | cut -c1-60)"
  p=$(awk '/^PPid/{print $2}' /proc/$p/status)
done

# 4) (Ubuntu) forest 출력, 조상 체인, 직계 자식
sleep 200 & bash -c 'sleep 200' &
ps -ef --forest | tail -8
pstree -ps $(pgrep -n sleep)
pgrep -P $$ -a

5. 실행 결과

실제 실행 결과 — Rocky Linux 9.8 · analyst@rocky9-lab — PID 1부터 이어지는 프로세스 트리

실제 실행 결과 — Ubuntu 24.04.5 · analyst@ubuntu-lab — ps --forest와 pstree -s

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

[analyst@rocky9-lab ~]$ cat /proc/sys/kernel/pid_max
4194304
[analyst@rocky9-lab ~]$ ps -o pid,ppid,user,cmd -p 1
    PID    PPID USER     CMD
      1       0 root     /usr/sbin/init
[analyst@rocky9-lab ~]$ echo "내 셸 PID=$$ PPID=$PPID"
내 셸 PID=309 PPID=308
[analyst@rocky9-lab ~]$ pstree -p | head -12
systemd(1)-+-agetty(55)
           |-atd(53)
           |-crond(54)
           |-dbus-broker-lau(56)---dbus-broker(57)
           |-sshd(64)---sshd-session(305)---sshd-session(308)---bash(309)-+-head(335)
           |                                                              `-pstree(334)
           |-systemd(159)---(sd-pam)(161)
           |-systemd-journal(22)
           `-systemd-logind(45)
[analyst@rocky9-lab ~]$ p=$$; while [ "$p" -ne 0 ]; do printf '%-6s %s\n' "$p" "$(tr '\0' ' ' < /proc/$p/cmdline | cut -c1-60)"; p=$(awk '/^PPid/{print $2}' /proc/$p/status); done
309    bash /tmp/run_03_1.sh
308    sshd-session: analyst@pts/0
305    sshd-session: analyst [priv]
64     sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
1      /usr/sbin/init
analyst@ubuntu-lab:~$ sleep 200 & bash -c 'sleep 200' &
analyst@ubuntu-lab:~$ sleep 0.3; ps -ef --forest | grep -v -e 'ps -ef' -e grep | tail -8
root         258      96  1 09:38 ?        00:00:00  \_ sshd: analyst [priv]
analyst      269     258  0 09:38 ?        00:00:00      \_ sshd: analyst@pts/0
analyst      270     269  0 09:38 pts/0    00:00:00          \_ bash /tmp/run_03_2.sh
analyst      271     270  0 09:38 pts/0    00:00:00              \_ sleep 200
analyst      272     270  0 09:38 pts/0    00:00:00              \_ sleep 200
analyst      276     270  0 09:38 pts/0    00:00:00              \_ tail -8
analyst      191       1  0 09:38 ?        00:00:00 /usr/lib/systemd/systemd --user
analyst      192     191  0 09:38 ?        00:00:00  \_ (sd-pam)
analyst@ubuntu-lab:~$ pstree -ps $(pgrep -n sleep)
systemd(1)───sshd(96)───sshd(258)───sshd(269)───bash(270)───sleep(272)
analyst@ubuntu-lab:~$ pgrep -P $$ -a
271 sleep 200
272 sleep 200
analyst@ubuntu-lab:~$ kill $(jobs -p) 2>/dev/null; pkill -u analyst -x sleep; true

6. 결과 해석

관찰의미
pid_max = 419430464비트 시스템에서 systemd가 설정하는 값(2²²). 예전 기본값 32768보다 훨씬 커서 PID 재사용 주기가 길다
PID 1의 PPID는 0PID 1은 커널이 직접 만든다. 그 위에는 사용자 공간 부모가 없다
pstree -p에서 bash(309)가 sshd(64) 아래에내 셸이 SSH 데몬의 후손이라는 것이 한눈에 보인다
systemd(159)---(sd-pam)(161)사용자가 로그인하면 사용자별 systemd --user 인스턴스가 따로 뜬다
조상 추적 결과 309 → 308 → 305 → 64 → 1/proc/PID/status의 PPid만으로 트리 전체를 복원할 수 있다
Ubuntu ps -ef --forest의 \_ 들여쓰기부모 아래에 자식이 들여쓰기되어, 사용자·시작 시각과 함께 트리를 읽을 수 있다
bash -c 'sleep 200'인데 트리에는 sleep만02편의 마지막 명령 exec 최적화 다. bash -c가 sleep으로 교체되어 중간 bash가 없다
pstree -ps → systemd(1)───sshd(96)───sshd(258)───sshd(269)───bash(270)───sleep(272)특정 프로세스 하나의 조상만 한 줄로 보여 준다. 관제 보고서에 붙이기 좋은 형식이다
pgrep -P $$ -a → 271, 272현재 셸의 직계 자식만 골라낸다

7. 보안 관점

주제내용
실행 주체 추적"누가 실행했나"는 UID만으로 부족하다. 같은 root라도 sshd 세션의 셸 에서 왔는지, cron 에서 왔는지, 웹 서버 에서 왔는지는 조상 체인이 알려 준다
PPID 1의 의미사용자 명령인데 PPID가 1이면 부모가 이미 종료된 고아(데몬화된) 프로세스 다. 공격 도구는 nohup, setsid, 이중 fork로 일부러 PPID 1이 되어 트리에서 출처를 숨긴다 (19편)
PID 재사용오래전 로그의 PID와 지금의 PID는 다른 프로세스일 수 있다. 시작 시각(ps -o lstart)을 함께 확인한다
네임스페이스컨테이너 안의 PID 1은 호스트에서는 다른 PID다. 컨테이너 탈출 분석 시 어느 PID 네임스페이스의 번호인지 를 구분한다

8. 보안관제 관점

[Detection]  의심 프로세스: /tmp/.x/kdevtmp (PID 4127, UID www-data)
     ↓
[조상 추적]  pstree -ps 4127
             systemd(1)───php-fpm(812)───php-fpm(2031)───sh(4120)───kdevtmp(4127)
     ↓
[판단]       웹 애플리케이션 프로세스(php-fpm)가 셸을 만들고 /tmp 실행 파일을 실행 → 웹 취약점 경유 침해
     ↓
[Evidence]   php-fpm 2031 의 처리 요청 시각 ↔ 웹 접근 로그, sh 4120 의 cmdline
     ↓
[Response]   웹 요청 출발지 차단, 취약 파라미터 확인, 프로세스·파일 수집 후 종료
정상 조상 체인 (예)조사 대상 조상 체인 (예)
systemd → sshd → sshd → bash → vimsystemd → nginx → sh → curl
systemd → crond → crond → sh → backup.shsystemd → crond → sh → /dev/shm/x
systemd → systemd --user → gnome-terminal → bashsystemd(1) → ./a.out (사용자 명령인데 PPID 1)

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

실수결과예방
의심 프로세스를 PID만 보고 바로 kill부모(실제 원인)가 다시 자식을 만든다먼저 pstree -ps로 부모를 확인하고 원인부터 제거
pstree가 없는 최소 설치 서버에서 당황트리 확인 불가ps -ef --forest 또는 /proc/PID/status 루프로 대체 (psmisc 패키지에 pstree 포함)
PID만 보고서에 기록나중에 재사용된 PID와 혼동PID + lstart + cmdline + 조상 체인
$PPID를 실시간 부모로 믿음셸 변수 $PPID는 셸 시작 시점 값이다현재 부모는 /proc/$$/status에서 확인

10. 실습 체크리스트

[ ] pid_max 값과 PID 1 의 PPID(0)를 확인했다
[ ] pstree -p 로 전체 트리에서 내 셸의 위치를 찾았다
[ ] /proc/PID/status 의 PPid 로 PID 1 까지 거슬러 올라갔다
[ ] sshd 권한 분리 구조(listener → priv → user)를 트리에서 확인했다
[ ] ps -ef --forest, pstree -ps, pgrep -P 의 차이를 이해했다
[ ] bash -c 마지막 명령이 exec 되어 중간 bash 가 없는 것을 확인했다

11. 핵심 정리

  • 모든 프로세스는 PPID를 따라가면 PID 1(systemd) 에 도달하는 하나의 트리를 이룬다.
  • pstree -ps PID는 조상 체인을, pgrep -P PID는 직계 자식을 빠르게 보여 준다.
  • SSH 접속은 listener → [priv] → 사용자 세션 → 셸로 권한을 나누어 fork된다.
  • 사용자 명령인데 PPID가 1이면 부모가 사라진 고아·데몬화 프로세스다.
  • 관제에서 "무엇이 실행됐나"보다 "누가(어떤 부모가) 실행했나" 가 침해 경로를 알려 준다.

12. 다음 편 예고

다음 글 「04. 프로세스 상태 (R·S·D·T·Z)」 에서는 ps의 STAT 열을 해석한다. 실행(R), 대기(S), 정지(T), 좀비(Z) 상태를 직접 만들어 보고, s, +, <, N 같은 보조 플래그가 무엇을 뜻하는지 확인한다.


참고 자료


시리즈 이동

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

0개의 댓글