파일 · 권한 · 사용자 관리 02 / 50 · Part 1. Linux 파일 관리 기초
실습 환경: Rocky Linux 9.8 · Ubuntu 24.04.5 (Docker 격리 컨테이너, 테스트 계정analyst)
기초편 연계: 「리눅스 시스템 기초」 6. Linux 파일 시스템 구조 · 14. inode와 파일 시스템
01편에서 파일은 이름 + inode + 데이터 블록 으로 이루어진다는 것을 확인했다. 이번 글은 한 단계 아래로 내려가 다음 질문에 답한다.
마지막 질문은 운영 장애 질문이면서 동시에 침해사고 분석 질문이다. 실행 중인 악성 프로세스가 자기 실행 파일이나 로그를 지워도 열린 파일 디스크립터를 통해 흔적이 남기 때문 이다.
파일 시스템은 블록 장치(디스크 파티션)를 inode 테이블 + 데이터 블록 + 관리 정보 로 나누어 파일을 저장하는 규칙이다.
| 파일 시스템 | 주 사용처 | 특징 |
|---|---|---|
| xfs | Rocky/RHEL 계열 기본 | 대용량·병렬 I/O에 강함, 축소(shrink) 불가 |
| ext4 | Ubuntu 기본 | 가장 널리 쓰임, 생성 시 inode 수가 고정됨 |
| tmpfs | /dev/shm, /run, (일부 배포판) /tmp | 메모리 기반, 재부팅 시 삭제 |
| proc / sysfs | /proc, /sys | 디스크가 없는 가상 파일 시스템 (커널 정보) |
| overlay | 컨테이너 루트 | 여러 레이어를 합쳐 보여 줌 |
배포판 차이: Rocky Linux 9 설치 기본값은 xfs, Ubuntu 24.04 설치 기본값은 ext4 다. 이 실습은 Docker 컨테이너 안에서 진행했기 때문에 루트(
/)는overlay이고, inode 실습을 위해 별도의 ext4 볼륨 을/data에 연결했다.
프로세스가 open()을 호출하면 커널은 작은 정수 하나를 돌려준다. 이것이 파일 디스크립터다. 이후 read(3, ...), write(3, ...)처럼 이 번호로 파일을 다룬다.
| fd | 기본 연결 | 의미 |
|---|---|---|
| 0 | stdin | 표준 입력 |
| 1 | stdout | 표준 출력 |
| 2 | stderr | 표준 에러 |
| 3~ | open() 할 때마다 할당 | 열린 파일, 소켓, 파이프 |
/proc/<PID>/fd/ 디렉터리에서 어떤 프로세스가 무엇을 열고 있는지 를 파일처럼 볼 수 있다.

open("/data/app.log", O_WRONLY|O_APPEND) 호출rm은 파일 내용을 지우는 명령이 아니다. 내부적으로 unlink() 시스템 콜을 호출해 디렉터리에서 이름 하나를 제거 하고 inode의 링크 수를 1 줄인다. 커널은 다음 두 조건이 모두 만족될 때만 inode와 데이터 블록을 실제로 해제한다.
링크 수(이름 수) == 0 AND 이 inode를 연 fd 수 == 0
그래서 로그를 쓰고 있는 프로세스가 있는 상태에서 로그 파일을 rm 하면, 이름은 사라지지만 공간은 해제되지 않는다.
권한 검사는 open() 시점에 수행된다. 이미 열린 fd로 읽고 쓰는 동안에는 파일 권한을 다시 검사하지 않는다. 그래서 파일 권한을 바꿔도 이미 열어 둔 프로세스는 계속 접근할 수 있다. 권한 회수 후에는 관련 프로세스 재시작까지 확인해야 하는 이유다.
# 1) 마운트된 파일 시스템 종류 확인
df -hT / /data /dev/shm
df -i /data # inode 총량과 사용량
# 2) 파일 하나의 inode와 블록
cd /data && echo "log line" > app.log
stat -c 'inode=%i size=%s blocks(512B)=%b io_block=%o' app.log
# 3) 현재 쉘에서 fd 3 으로 파일 열기
exec 3>>app.log
ls -l /proc/$$/fd/0 /proc/$$/fd/1 /proc/$$/fd/3
echo "written via fd 3" >&3
cat app.log
exec 3>&- # fd 3 닫기
# 4) 열린 상태에서 삭제된 파일 재현
tail -f app.log > /dev/null & # 파일을 계속 열고 있는 프로세스
PID=$!; sleep 1
rm app.log
ls -l /proc/$PID/fd | grep app.log
kill $PID
| 명령 | 의미 |
|---|---|
df -T | 파일 시스템 종류 표시 |
df -i | inode 기준 사용량 |
exec 3>>file | 현재 쉘에 fd 3을 append 모드로 연결 |
>&3 | 출력을 fd 3으로 보냄 |
exec 3>&- | fd 3 닫기 |
$$ / $! | 현재 쉘 PID / 마지막 백그라운드 프로세스 PID |

텍스트 원본(실제 출력):
[analyst@rocky9-lab ~]$ df -hT / /data /dev/shm
Filesystem Type Size Used Avail Use% Mounted on
overlay overlay 252G 14G 29G 32% /
/dev/loop0 ext4 224M 8.0K 206M 1% /data
shm tmpfs 64M 0 64M 0% /dev/shm
[analyst@rocky9-lab ~]$ df -i /data
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/loop0 65536 10 65526 1% /data
[analyst@rocky9-lab ~]$ cd /data && echo "log line" > app.log
[analyst@rocky9-lab data]$ stat -c 'inode=%i size=%s blocks(512B)=%b io_block=%o' app.log
inode=11 size=9 blocks(512B)=8 io_block=4096
[analyst@rocky9-lab data]$ exec 3>>app.log
[analyst@rocky9-lab data]$ ls -l /proc/$$/fd/0 /proc/$$/fd/1 /proc/$$/fd/3
lr-x------ 1 analyst analyst 64 Sep 24 10:16 /proc/51/fd/0 -> /dev/null
l-wx------ 1 analyst analyst 64 Sep 24 10:16 /proc/51/fd/1 -> pipe:[33712]
l-wx------ 1 analyst analyst 64 Sep 24 10:16 /proc/51/fd/3 -> /data/app.log
[analyst@rocky9-lab data]$ echo "written via fd 3" >&3
[analyst@rocky9-lab data]$ cat app.log
log line
written via fd 3
[analyst@rocky9-lab data]$ exec 3>&-
[analyst@rocky9-lab data]$ tail -f app.log > /dev/null &
[analyst@rocky9-lab data]$ PID=$!; sleep 1
[analyst@rocky9-lab data]$ rm app.log
[analyst@rocky9-lab data]$ ls -l /proc/$PID/fd | grep app.log
lr-x------ 1 analyst analyst 64 Sep 24 10:16 3 -> /data/app.log (deleted)
[analyst@rocky9-lab data]$ kill $PID
| 출력 | 해석 |
|---|---|
overlay ... / | 컨테이너 루트는 overlay FS. 실제 VM이라면 Rocky는 xfs, Ubuntu는 ext4 로 보인다 |
/dev/loop0 ext4 ... /data | 실습용 ext4 볼륨 |
shm tmpfs ... /dev/shm | 메모리 기반 FS. 누구나 쓸 수 있는 경로라 공격자가 파일을 두는 위치로 자주 쓰인다 |
Inodes 65536 IUsed 10 | 이 볼륨은 파일을 최대 65,536개 만들 수 있다 (ext4는 생성 시 고정) |
size=9 blocks(512B)=8 io_block=4096 | 내용은 9바이트지만 4KiB 블록 1개(= 512B × 8) 를 차지한다 |
fd/0 -> /dev/null, fd/1 -> pipe:[...] | 이 실습은 스크립트로 실행했기 때문에 stdin은 /dev/null, stdout은 파이프다. 터미널에서 직접 실행하면 /dev/pts/0 같은 터미널 장치가 보인다 |
fd/3 -> /data/app.log | exec 3>>로 연 파일이 fd 3에 연결됨 |
3 -> /data/app.log (deleted) | 이름은 지워졌지만 tail 프로세스가 아직 inode를 열고 있다 |
마지막 줄이 이번 글의 핵심이다. 이 상태에서는 ls /data에 app.log가 보이지 않지만, 데이터는 디스크에 그대로 있고 cat /proc/<PID>/fd/3으로 내용을 읽을 수도 있다.
| 상황 | 공격/장애 시나리오 | 확인 방법 |
|---|---|---|
| 실행 후 자기 파일 삭제 | 악성코드가 /tmp/.x를 실행하고 바로 rm → 디스크 검색으로 안 보임 | ls -l /proc/*/exe 2>/dev/null \| grep deleted |
| 로그 삭제 | 공격자가 /var/log/secure 삭제 → rsyslog는 계속 삭제된 inode에 기록 | ls -l /proc/$(pidof rsyslogd)/fd |
| inode 고갈 | 작은 파일 대량 생성 → 용량이 남아도 로그·세션 파일 생성 실패 | df -i |
| 메모리 FS 악용 | /dev/shm에 페이로드 저장 → 재부팅하면 사라짐 | find /dev/shm -type f -ls |
주의: (deleted) 표시는 정상 상황에서도 흔하다. 패키지 업데이트 후 재시작하지 않은 서비스가 삭제된 옛 라이브러리를 열고 있는 경우가 대표적이다. 판단 기준은 어떤 프로세스가, 어떤 경로의 파일을, 언제부터 열고 있었는가다.
[Detection] EDR/auditd: /tmp 경로에서 실행된 프로세스, 이후 해당 파일 unlink
↓
[Evidence] /proc/<PID>/exe → /tmp/.cache/kworker (deleted)
↓
[보존] cp /proc/<PID>/exe /evidence/kworker.bin ← 프로세스 종료 전에
sha256sum /evidence/kworker.bin
↓
[IOC] 해시 → 위협 인텔리전스 조회, 연결 IP(ss -tnp) 수집
↓
[Response] 네트워크 격리 → 프로세스 종료 → 계정·지속성(cron, systemd) 확인
| 관제 포인트 | 설명 |
|---|---|
| SIEM | 파일 삭제는 기본 syslog에 남지 않는다. auditd의 unlink, unlinkat 규칙이나 EDR 이벤트가 있어야 수집된다 |
| 이상 행위 | 실행 파일이 /tmp, /dev/shm, /var/tmp에 있고 이미 삭제된 상태 |
| Evidence 순서 | 프로세스를 먼저 죽이면 /proc/<PID>/가 사라진다. 보존 → 종료 순서를 지킨다 |
| 용량 이상 | du 합계와 df 사용량이 크게 다르면 삭제된 열린 파일을 의심한다 |
| 실수 | 결과 | 올바른 방법 |
|---|---|---|
용량 확보를 위해 사용 중인 로그를 rm | 공간이 해제되지 않음 | : > app.log로 비우거나 logrotate 사용, 필요 시 서비스 재시작 |
의심 프로세스를 먼저 kill | /proc의 증거 소실 | 실행 파일·fd·네트워크 정보를 먼저 보존 |
df -h만 보고 여유 있다고 판단 | inode 고갈을 놓침 | df -i도 함께 확인 |
| 권한 변경 후 바로 안전하다고 판단 | 이미 열린 fd는 계속 사용 가능 | 관련 프로세스 재시작 여부 확인 |
| 컨테이너 결과를 VM과 동일하게 해석 | 루트 FS 종류가 다름(overlay) | 환경을 먼저 df -T로 확인 |
[ ] df -T 로 파일 시스템 종류를 확인했다
[ ] df -i 로 inode 사용량을 확인했다
[ ] /proc/$$/fd 에서 fd 번호와 연결 대상을 확인했다
[ ] 열린 상태에서 삭제된 파일이 (deleted) 로 표시되는 것을 재현했다
[ ] 파일 공간이 해제되는 두 조건(링크 0, 열린 fd 0)을 설명할 수 있다
[ ] 의심 프로세스 대응 시 "보존 → 종료" 순서를 이해했다
/proc/<PID>/fd/에서 확인할 수 있다.rm은 이름을 제거(unlink)할 뿐이고, 공간은 링크 수 0 + 열린 fd 0 일 때 해제된다.open() 시점에 한 번 수행된다. 이미 열린 fd에는 권한 변경이 즉시 영향을 주지 않는다.(deleted) 파일은 정상 업데이트에서도 생기므로 경로·프로세스·시점 으로 판단한다.다음 글 「03. Linux 디렉터리 구조 이해」 에서는 /, /etc, /var, /tmp, /usr, /opt가 각각 무엇을 저장하는지, 그리고 각 디렉터리의 기본 권한이 왜 그렇게 설정되어 있는지 를 Rocky와 Ubuntu 실제 출력으로 비교한다.