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

1. 들어가며

ps -ef를 실행하면 TTY 열이 pts/0인 프로세스와 ?인 프로세스가 섞여 있다. ?는 제어 터미널이 없는 프로세스, 대부분은 데몬이다. sshd, crond, journald 같은 시스템 데몬이 여기에 속한다. 그런데 공격자가 남긴 백도어도 같은 모습이다.

이번 글에서는 10편에서 잠깐 본 세션(SID)·프로세스 그룹(PGID)·제어 터미널을 정리하고, 전통적인 데몬화(fork → setsid → fork) 를 직접 구현한다. 그리고 시스템 데몬과 사용자가 만든 데몬을 구분하는 기준을 세운다.


2. 핵심 개념

2-1. 계층 구조

세션 (SID)                 ← 로그인 한 번 = 세션 하나, 제어 터미널 하나
 └─ 프로세스 그룹 (PGID)    ← 셸의 작업(job) 하나, 파이프라인 하나
     └─ 프로세스 (PID)
용어의미확인
세션 리더세션을 만든 프로세스 (SID = PID)STAT의 s
제어 터미널세션에 연결된 터미널ps -o tty, tty
포그라운드 그룹터미널 입력·Ctrl+C를 받는 그룹ps -o tpgid, STAT의 +
감사 세션 ID로그인 시 PAM이 부여하는 번호/proc/PID/sessionid
logind 세션systemd-logind가 관리하는 세션loginctl list-sessions

2-2. 데몬의 조건

조건이유구현
제어 터미널 없음로그아웃 SIGHUP, 터미널 입력의 영향을 받지 않기 위해setsid()
세션 리더가 아님실수로 터미널을 열어도 제어 터미널이 되지 않게setsid 후 한 번 더 fork
부모는 init부모 셸이 기다리지 않게부모 즉시 exit → 고아화
작업 디렉터리 /마운트 해제를 막지 않기 위해chdir("/")
표준 입출력 분리닫힌 터미널에 쓰다 오류 나지 않게fd 0·1·2 → /dev/null
(umask 설정)파일 권한 예측 가능umask(0) 또는 지정 값

3. 동작 원리

세션 · 프로세스 그룹 · 데몬화 (double fork)

셸 ─fork─► python(A)
            A: fork() → 부모 A 는 exit          ← 셸은 즉시 프롬프트 복귀
               자식 B: setsid()                 ← 새 세션·새 그룹의 리더, 터미널 없음
                       fork() → B 는 exit
                         손자 C: chdir("/"), fd → /dev/null, 본 작업 실행
C: PPID 1 (B 가 죽어 고아), SID = B 의 PID, 세션 리더 아님

systemd 이전에는 모든 데몬이 이 절차를 스스로 수행했다. 지금은 systemd가 서비스를 시작할 때 이미 새 세션·빈 표준 입출력·cgroup을 준비해 주므로, 서비스 프로그램은 포그라운드로 그냥 실행 하면 된다(Type=simple, 25편).


4. 명령어 실습

# 1) 세션·그룹·터미널 확인
tty; echo "셸 PID=$$"
set -m; sleep 100 | cat &                     # 파이프라인 하나 = 프로세스 그룹 하나
ps -o pid,ppid,pgid,sid,tty,tpgid,stat,cmd -t $(tty | sed 's#/dev/##')
cat /proc/$$/sessionid; echo
loginctl list-sessions --no-pager

# 2) 데몬 만들기: fork → setsid → fork
python3 -c 'import os,sys,time
if os.fork(): sys.exit(0)
os.setsid()
if os.fork(): sys.exit(0)
os.chdir("/"); fd=os.open("/dev/null",os.O_RDWR); [os.dup2(fd,i) for i in (0,1,2)]
time.sleep(120)'
D=$(pgrep -n -u analyst python3); ps -o pid,ppid,pgid,sid,tty,stat,cmd -p $D
ls -l /proc/$D/cwd /proc/$D/fd

# 3) (Ubuntu) 시스템 전체의 터미널 없는 프로세스
ps -eo tty= | sort | uniq -c
ps -eo pid,ppid,sid,tty,user,cmd --sort=sid | awk '$4=="?" && $2==1' | head -8

5. 실행 결과

실제 실행 결과 — Rocky Linux 9.8 · analyst@rocky9-lab — 세션 · 프로세스 그룹 · 제어 터미널

실제 실행 결과 — Rocky Linux 9.8 · analyst@rocky9-lab — 데몬 만들기: fork → setsid → fork

실제 실행 결과 — Ubuntu 24.04.5 · root@ubuntu-lab — 시스템 전체: 터미널 없는 프로세스

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

[analyst@rocky9-lab ~]$ tty; echo "셸 PID=$$"
/dev/pts/0
셸 PID=1701
[analyst@rocky9-lab ~]$ set -m; sleep 100 | cat & sleep 0.2
[analyst@rocky9-lab ~]$ ps -o pid,ppid,pgid,sid,tty,tpgid,stat,cmd -t $(tty | sed 's#/dev/##')
    PID    PPID    PGID     SID TT         TPGID STAT CMD
   1701    1700    1701    1701 pts/0       1732 Ss   bash /tmp/run_19_1.sh
   1726    1701    1726    1701 pts/0       1732 S    sleep 100
   1727    1701    1726    1701 pts/0       1732 S    cat
   1732    1701    1732    1701 pts/0       1732 R+   ps -o pid,ppid,pgid,sid,tty,tpgid,stat,cmd -t pts/0
[analyst@rocky9-lab ~]$ cat /proc/$$/sessionid; echo
68
[analyst@rocky9-lab ~]$ loginctl list-sessions --no-pager
SESSION  UID USER    SEAT TTY   STATE  IDLE SINCE
     68 1000 analyst      pts/0 active no

1 sessions listed.
[analyst@rocky9-lab ~]$ kill %1
[analyst@rocky9-lab ~]$ python3 -c 'import os,sys,time
> if os.fork(): sys.exit(0)
> os.setsid()
> if os.fork(): sys.exit(0)
> os.chdir("/"); fd=os.open("/dev/null",os.O_RDWR); [os.dup2(fd,i) for i in (0,1,2)]
> time.sleep(120)'
[analyst@rocky9-lab ~]$ sleep 0.5; D=$(pgrep -n -u analyst python3); ps -o pid,ppid,pgid,sid,tty,stat,cmd -p $D
    PID    PPID    PGID     SID TT       STAT CMD
   1771       1    1770    1770 ?        S    python3 -c import os,sys,time if os.fork(): sys.exit(0) os.setsi
[analyst@rocky9-lab ~]$ ls -l /proc/$D/cwd /proc/$D/fd | grep -v total
lrwxrwxrwx 1 analyst analyst 0 Sep 24 11:51 /proc/1771/cwd -> /

/proc/1771/fd:
lrwx------ 1 analyst analyst 64 Sep 24 11:51 0 -> /dev/null
lrwx------ 1 analyst analyst 64 Sep 24 11:51 1 -> /dev/null
lrwx------ 1 analyst analyst 64 Sep 24 11:51 2 -> /dev/null
lrwx------ 1 analyst analyst 64 Sep 24 11:51 3 -> /dev/null
[analyst@rocky9-lab ~]$ kill $D
root@ubuntu-lab:~# ps -eo tty= | sort | uniq -c
     12 ?
      4 pts/0
root@ubuntu-lab:~# ps -eo pid,ppid,sid,tty,user,cmd --sort=sid | awk '$4=="?" && $2==1' | head -8
     25       1      25 ?        root     /usr/lib/systemd/systemd-journald
     67       1      67 ?        systemd+ /usr/lib/systemd/systemd-resolved
     80       1      80 ?        root     /usr/sbin/cron -f -P
     81       1      81 ?        message+ @dbus-daemon --system --address=systemd: --nofork --nopidfile --syst
     84       1      84 ?        root     /usr/lib/systemd/systemd-logind
     86       1      86 ?        daemon   /usr/sbin/atd -f
     94       1      94 ?        root     (agetty)
     95       1      95 ?        root     sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups

6. 결과 해석

관찰의미
tty → /dev/pts/0SSH가 할당한 가상 터미널(pseudo terminal)
bash PGID 1701 = SID 1701, STAT Ss셸이 세션 리더이자 자기 그룹의 리더
sleep 1726, cat 1727 모두 PGID 1726파이프라인의 두 프로세스가 하나의 그룹 이다. Ctrl+C를 누르면 둘 다 받는다
TPGID 1732 = ps의 PGID, STAT R+지금 포그라운드는 ps다. sleep·cat은 백그라운드라 +가 없다
/proc/$$/sessionid = 68, loginctl SESSION 68커널 감사 세션 ID와 logind 세션 번호가 같다. 인증 로그·감사 로그를 연결하는 키다
데몬 1771: PPID 1, PGID 1770, SID 1770, TTY ?새 세션(1770)에 속하지만 리더(1770)는 이미 종료 했다. 1771은 세션 리더가 아니라 터미널을 다시 얻을 수 없다
STAT S (s·+ 없음)세션 리더도 포그라운드도 아닌 순수 백그라운드 프로세스
cwd -> /, fd 0·1·2 → /dev/null데몬 정리 단계가 적용되었다. fd 3은 /dev/null을 연 원본 fd다
Ubuntu: ? 12개, pts/0 4개서버 프로세스 대부분은 터미널 없는 데몬이다
시스템 데몬들: PPID 1, SID = 자기 PIDsystemd가 시작한 서비스는 각자 자기 세션의 리더 로 실행된다

시스템 서비스(SID = PID, PPID 1, cgroup이 .service)와 double fork 데몬(SID ≠ PID, PPID 1, cgroup이 session-N.scope)은 이 세 값으로 구분된다.


7. 보안 관점

주제내용
백도어의 데몬화공격 도구는 double fork로 터미널·부모와 관계를 끊고 PPID 1로 숨는다. 모습은 시스템 데몬과 비슷하다
구분 기준사용자 계정 소유 · TTY ? · PPID 1 · SID ≠ PID · cgroup이 session-N.scope 또는 웹 서비스 → 사용자가 수동으로 데몬화한 프로세스
감사 세션 ID/proc/PID/sessionid는 데몬화해도 로그인 당시 값이 유지 된다. auditd 로그의 ses= 필드와 연결하면 어느 로그인에서 나왔는지 알 수 있다
터미널 없는 셸TTY ?인 bash -i, sh -i는 대화형 셸인데 터미널이 없다는 뜻 → 리버스 셸 전형 (44편)

8. 보안관제 관점

[Detection]  점검: 사용자 계정 소유 · TTY ? · PPID 1 프로세스
     ps -eo pid,ppid,sid,user,tty,lstart,args -ww | awk '$2==1 && $5=="?" && $4!="root"'
     ↓
[분류]       cat /proc/PID/cgroup
             → *.service           : 시스템이 관리하는 서비스 (정상 가능성)
             → session-N.scope      : 로그인 세션에서 수동 데몬화
     ↓
[추적]       cat /proc/PID/sessionid → N
             ausearch --session N / journalctl 의 세션 N 로그인 기록
     ↓
[Response]   업무 목적 확인 → 불명확하면 증거 수집 후 종료, 계정 점검
필드정상 서비스수동 데몬화
PPID11
SID= PID≠ PID (리더 종료)
cgroupxxx.servicesession-N.scope
sessionid4294967295 (로그인 없음)로그인 세션 번호

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

실수결과예방
systemd 서비스에서 프로그램이 스스로 데몬화systemd가 메인 PID를 잃어 "종료됨"으로 판단Type=simple로 포그라운드 실행, 또는 Type=forking + PIDFile=
TTY ?면 모두 시스템 데몬이라고 판단수동 데몬화 백도어를 놓침계정·SID·cgroup 확인
데몬의 표준 출력을 닫지 않음로그아웃 후 쓰기 오류(EIO)로 종료/dev/null 또는 로그 파일로
데몬이 cwd를 유지해당 파일 시스템 umount 불가chdir("/")

10. 실습 체크리스트

[ ] PID, PGID, SID, TTY, TPGID 를 한 화면에서 읽었다
[ ] 파이프라인이 하나의 프로세스 그룹인 것을 확인했다
[ ] /proc/PID/sessionid 와 loginctl 세션 번호를 대조했다
[ ] fork → setsid → fork 로 데몬을 만들었다
[ ] 데몬의 PPID 1, TTY ?, SID ≠ PID, cwd /, fd /dev/null 을 확인했다
[ ] 시스템 서비스와 수동 데몬을 SID·cgroup 으로 구분할 수 있다

11. 핵심 정리

  • 세션 ⊃ 프로세스 그룹 ⊃ 프로세스. 세션은 로그인 한 번, 그룹은 작업 하나다.
  • 데몬은 제어 터미널 없이 동작하며, 전통적으로 fork → setsid → fork로 만든다.
  • systemd 서비스는 스스로 데몬화하지 않고 포그라운드로 실행하는 것이 표준이다.
  • 수동 데몬은 PPID 1·TTY ?·SID ≠ PID·세션 scope cgroup으로 시스템 서비스와 구분된다.
  • /proc/PID/sessionid는 데몬화 후에도 남아 로그인 기록과 연결하는 단서가 된다.

12. 다음 편 예고

Part 2의 마지막 글 「20. 메모리 부족과 OOM Killer」 에서는 메모리가 부족할 때 커널이 프로세스를 골라 강제 종료하는 OOM Killer를 다룬다. oom_score·oom_score_adj로 누가 먼저 죽는지 확인하고, 메모리 한도를 넘긴 서비스가 OOM Killer에 종료되는 과정을 로그로 추적한다.


참고 자료


시리즈 이동

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

0개의 댓글