리눅스 시스템 기초 · 입문편 — 본문의 "N편"은 입문 과정 번호다. 번호별 글과 전체 250편 구성은 통합 로드맵에서 확인할 수 있다.
리눅스 시스템 기초 35 / 50 · Part 4. 프로세스·서비스·리소스
실습 환경: Rocky Linux 9 (10.0.0.200) · 트리·고아·좀비 출력은 실습 환경 실제 출력
이전 글: 34. Linux Signal과 kill
31편에서 모든 프로세스에는 부모(PPID)가 있다고 했다. 이번 글은 그 관계를 이어 붙인 프로세스 트리 를 다룬다.
EDR 제품의 경보 화면을 열면 가장 먼저 보이는 것이 프로세스 트리다. 이유는 간단하다. 하나의 프로세스만 보면 판단하기 어렵지만, 누가 누구를 실행했는가 를 보면 공격 경로가 드러나기 때문이다.
bash가 curl을 실행했다 → 관리자 작업일 수 있다.httpd가 sh를 실행했고, 그 sh가 curl을 실행했다 → 웹셸일 가능성이 높다.같은 curl이라도 부모가 누구냐 에 따라 의미가 완전히 달라진다.
| 단계 | System Call | 일어나는 일 |
|---|---|---|
| ① | fork() (실제로는 clone()) | 부모를 복제해 자식 생성. 자식 PPID = 부모 PID |
| ② | execve() | 자식이 다른 프로그램으로 교체. PID는 그대로 |
| ③ | exit() | 자식 종료 → 커널이 부모에게 SIGCHLD (34편) |
| ④ | wait() | 부모가 자식의 종료 상태를 회수 → 프로세스 정보 완전 삭제 |
쉘에서 ls를 입력하면 bash가 fork → 자식이 exec(/usr/bin/ls) → 종료 → bash가 wait 하는 순서다. 그래서 ls의 부모는 항상 그 쉘이다.
| PID | 이름 | 역할 |
|---|---|---|
| 0 | (idle / swapper) | 커널 내부 |
| 1 | systemd | 모든 사용자 공간 프로세스의 조상 (8편) |
| 2 | kthreadd | 모든 커널 스레드의 부모 ([kworker/...] 등) |
| 구분 | 발생 조건 | 모습 | 문제 |
|---|---|---|---|
| 고아(Orphan) | 부모가 먼저 종료 | PPID가 1(또는 subreaper)로 바뀜 | 그 자체는 정상 현상. 단, 공격자가 부모를 끊는 데 이용 |
| 좀비(Zombie) | 자식은 끝났는데 부모가 wait() 안 함 | STAT Z, <defunct> | 자원은 거의 없지만 PID를 점유. 대량이면 부모 프로그램 버그 |
좀비는 이미 종료된 상태라 kill -9로도 없어지지 않는다. 부모가 wait 하거나, 부모가 종료되어 systemd가 회수 해야 사라진다.

실습 환경에서 고아와 좀비가 생기는 과정을 직접 재현한 실제 출력이다.
== 정상 트리 ==
PID PPID STAT COMMAND
752 751 S \_ bash -c sleep 300 & echo "child_pid=$!"; wait
754 752 S | \_ sleep 300
$ kill -KILL 752 # 부모만 강제 종료
== 부모 종료 후 ==
754 1 S sleep 300 ← PPID 752 → 1 (고아가 되어 입양됨)
== 좀비 == (python fork 후 부모가 wait 하기 전)
PID PPID STAT COMMAND
774 773 Z python3
== wait 후 ==
PID PPID STAT COMMAND ← 회수되어 목록에서 사라짐
데스크톱 환경이나 컨테이너에서는 PID 1이 아닌 subreaper(예:
systemd --user, 컨테이너의 init)가 고아를 입양하기도 한다. 그래서 "PPID 1"보다는 "원래 부모와 끊어졌다" 는 사실에 주목한다.
공격자가 트리를 끊는 이유. 웹셸이 sh -c "nohup ./sysd &"를 실행하고 sh가 곧바로 종료하면, sysd는 고아가 되어 PPID 1로 바뀐다. 이제 ps --forest에서 httpd 아래가 아니라 systemd 바로 아래 에 보이므로, 트리만 보면 서비스처럼 보인다. 이를 흔히 이중 fork(double fork) 라고 부르며 데몬을 만드는 정상 기법이기도 하다. 그래서 PPID 1이면서 사용자 계정·웹 계정으로 실행 중 이고 systemd 서비스로 등록되지 않은(36편 cgroup 확인) 프로세스를 찾아야 한다.
# 1) 전체 트리
pstree -p | less # PID 포함
pstree -u # 계정이 바뀌는 지점 표시
ps -eo pid,ppid,user,args --forest | less
# 2) 특정 프로세스의 조상 경로 (위로)
pstree -aps <PID>
# 3) 특정 프로세스의 자손 (아래로)
pstree -ap <PID>
ps --ppid <PID> -o pid,user,args
# 4) 고아 후보: PPID 1 인데 사용자·웹 계정
ps -eo pid,ppid,user,etime,args | awk '$2==1 && $3!="root"'
# 5) 좀비와 그 부모
ps -eo pid,ppid,stat,comm | awk '$3 ~ /^Z/'
# 6) 어느 systemd 서비스 소속인가 (cgroup, 36편)
cat /proc/<PID>/cgroup
아래 출력은 형식 설명용 예시다.
① pstree -aps 7902
systemd,1
└─httpd,945 -DFOREGROUND
└─httpd,948 -DFOREGROUND
└─sh,7900 -c id;uname -a
└─(현재 프로세스) 7902
웹 서버 워커(apache 계정) → sh -c → 시스템 정보 수집 명령. 웹 서버는 정상 동작 중 쉘을 실행할 일이 거의 없으므로 웹셸을 통한 명령 실행 을 강하게 의심한다.
② cat /proc/7811/cgroup
0::/system.slice/httpd.service
PPID 1이라 서비스처럼 보였지만, cgroup은 여전히 httpd.service 에 속한다. 즉 httpd에서 파생된 프로세스 라는 뜻이다. 부모 관계는 끊을 수 있어도 cgroup은 따라다니기 때문에 좋은 교차 증거가 된다.
| 부모 → 자식 | 의미 | 위험도 |
|---|---|---|
sshd → bash → sudo | 원격 관리자 작업 | 정상 (계정·시간 확인) |
crond → sh → 스크립트 | 예약 작업 | 스크립트 내용 확인 (38편) |
httpd/nginx/php-fpm → sh/bash | 웹 애플리케이션이 쉘 실행 | 높음 — 웹셸 |
java/tomcat → bash → curl/wget | 역직렬화 등 RCE 후 다운로드 | 높음 |
mysqld/postgres → sh | DB 기능 악용 | 높음 |
PPID 1 + 웹 계정 + /tmp 실행 | 부모를 끊은 잔류 프로세스 | 높음 |
| 대량 좀비 | 부모 프로그램 결함 | 가용성 이슈 |
MITRE ATT&CK에서 웹셸은 T1505.003, 명령 인터프리터 실행은 T1059.004로 분류된다. 탐지 규칙의 핵심은 "서버 프로세스가 인터프리터를 자식으로 가짐" 이다.
for p in $(pgrep -d' ' -x 'httpd|nginx|php-fpm'); do
ps --ppid "$p" -o pid=,user=,args= 2>/dev/null
done | awk '$3 ~ /(^|\/)(sh|bash|dash|python[0-9.]*|perl|nc)$/'
auditd의 execve 기록에는 pid와 ppid가 함께 남는다. 프로세스가 이미 종료되어도 로그로 트리를 복원 할 수 있다(48편).
ausearch -m EXECVE -ts today -i | grep -E 'ppid=948|pid=7900'
[Alert] EDR: apache 계정의 sh 실행 (부모 httpd)
↓
[트리] pstree -aps → httpd(948) → sh -c "id;uname -a" → curl ... | bash
↓
[시각] sh 시작 시각 02:31:07 → access_log 02:31:07 POST /upload/img.php (25편)
↓
[파생] PPID 1 · apache · cgroup httpd.service 인 sysd(7811) 발견
↓
[판단] 업로드 취약점 → 웹셸 → 다운로드 → 채굴기 잔류
↓
[Response] 웹셸 파일 격리, sysd STOP → 증적 → KILL (34편), 업로드 경로 실행 차단
Z, kill로 제거 불가.다음 글 「36. systemd 구조 이해」 에서는 트리의 뿌리인 systemd를 다룬다. Unit의 종류와 파일 위치, 의존성·부팅 순서, cgroup으로 서비스를 묶는 방식, 그리고 공격자가 systemd 서비스로 지속성을 확보 하는 위치를 정리한다.