리눅스 시스템 기초 · 입문편 — 본문의 "N편"은 입문 과정 번호다. 번호별 글과 전체 250편 구성은 통합 로드맵에서 확인할 수 있다.

리눅스 시스템 기초 35 / 50 · Part 4. 프로세스·서비스·리소스
실습 환경: Rocky Linux 9 (10.0.0.200) · 트리·고아·좀비 출력은 실습 환경 실제 출력
이전 글: 34. Linux Signal과 kill

1. 들어가며

31편에서 모든 프로세스에는 부모(PPID)가 있다고 했다. 이번 글은 그 관계를 이어 붙인 프로세스 트리 를 다룬다.

EDR 제품의 경보 화면을 열면 가장 먼저 보이는 것이 프로세스 트리다. 이유는 간단하다. 하나의 프로세스만 보면 판단하기 어렵지만, 누가 누구를 실행했는가 를 보면 공격 경로가 드러나기 때문이다.

  • bash가 curl을 실행했다 → 관리자 작업일 수 있다.
  • httpd가 sh를 실행했고, 그 sh가 curl을 실행했다 → 웹셸일 가능성이 높다.

같은 curl이라도 부모가 누구냐 에 따라 의미가 완전히 달라진다.


2. 핵심 개념

2-1. 프로세스가 태어나는 방식: fork와 exec

단계System Call일어나는 일
①fork() (실제로는 clone())부모를 복제해 자식 생성. 자식 PPID = 부모 PID
②execve()자식이 다른 프로그램으로 교체. PID는 그대로
③exit()자식 종료 → 커널이 부모에게 SIGCHLD (34편)
④wait()부모가 자식의 종료 상태를 회수 → 프로세스 정보 완전 삭제

쉘에서 ls를 입력하면 bash가 fork → 자식이 exec(/usr/bin/ls) → 종료 → bash가 wait 하는 순서다. 그래서 ls의 부모는 항상 그 쉘이다.

2-2. 트리의 뿌리

PID이름역할
0(idle / swapper)커널 내부
1systemd모든 사용자 공간 프로세스의 조상 (8편)
2kthreadd모든 커널 스레드의 부모 ([kworker/...] 등)

2-3. 고아와 좀비

구분발생 조건모습문제
고아(Orphan)부모가 먼저 종료PPID가 1(또는 subreaper)로 바뀜그 자체는 정상 현상. 단, 공격자가 부모를 끊는 데 이용
좀비(Zombie)자식은 끝났는데 부모가 wait() 안 함STAT Z, <defunct>자원은 거의 없지만 PID를 점유. 대량이면 부모 프로그램 버그

좀비는 이미 종료된 상태라 kill -9로도 없어지지 않는다. 부모가 wait 하거나, 부모가 종료되어 systemd가 회수 해야 사라진다.


3. 동작 원리

프로세스 트리로 정상과 의심을 구분한다

실습 환경에서 고아와 좀비가 생기는 과정을 직접 재현한 실제 출력이다.

== 정상 트리 ==
  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 확인) 프로세스를 찾아야 한다.


4. 실습

# 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

5. 결과 분석

아래 출력은 형식 설명용 예시다.

① 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은 따라다니기 때문에 좋은 교차 증거가 된다.


6. 보안 관점

부모 → 자식의미위험도
sshd → bash → sudo원격 관리자 작업정상 (계정·시간 확인)
crond → sh → 스크립트예약 작업스크립트 내용 확인 (38편)
httpd/nginx/php-fpm → sh/bash웹 애플리케이션이 쉘 실행높음 — 웹셸
java/tomcat → bash → curl/wget역직렬화 등 RCE 후 다운로드높음
mysqld/postgres → shDB 기능 악용높음
PPID 1 + 웹 계정 + /tmp 실행부모를 끊은 잔류 프로세스높음
대량 좀비부모 프로그램 결함가용성 이슈

MITRE ATT&CK에서 웹셸은 T1505.003, 명령 인터프리터 실행은 T1059.004로 분류된다. 탐지 규칙의 핵심은 "서버 프로세스가 인터프리터를 자식으로 가짐" 이다.


7. SOC / 보안관제 활용

7-1. 웹 서버 자식 쉘 헌팅

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)$/'

7-2. auditd로 트리 복원

auditd의 execve 기록에는 pid와 ppid가 함께 남는다. 프로세스가 이미 종료되어도 로그로 트리를 복원 할 수 있다(48편).

ausearch -m EXECVE -ts today -i | grep -E 'ppid=948|pid=7900'

7-3. 분석 흐름

[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편), 업로드 경로 실행 차단

8. 핵심 정리

  • 프로세스는 fork(복제) → exec(교체) → exit → wait(회수) 로 생성·종료된다.
  • PID 1(systemd)은 사용자 공간, PID 2(kthreadd)는 커널 스레드의 뿌리다.
  • 고아: 부모가 먼저 종료 → PPID 1(또는 subreaper). 좀비: 부모가 wait 안 함 → Z, kill로 제거 불가.
  • 판단 기준은 "무엇이 실행됐나"보다 "누가 실행했나" 다. 서버 프로세스 → 쉘은 높은 위험.
  • 부모 관계는 끊을 수 있지만 cgroup 은 남는다. auditd 로그로 종료된 트리도 복원한다.

9. 다음 글

다음 글 「36. systemd 구조 이해」 에서는 트리의 뿌리인 systemd를 다룬다. Unit의 종류와 파일 위치, 의존성·부팅 순서, cgroup으로 서비스를 묶는 방식, 그리고 공격자가 systemd 서비스로 지속성을 확보 하는 위치를 정리한다.


참고 자료

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

0개의 댓글