리눅스 시스템 기초 · 입문편 — 본문의 "N편"은 입문 과정 번호다. 번호별 글과 전체 250편 구성은 통합 로드맵에서 확인할 수 있다.
리눅스 시스템 기초 3 / 50 · Part 1. Linux 기본 구조
실습 환경: Rocky Linux 9 (10.0.0.200) · 비교: Ubuntu 22.04
이전 글: 2. Linux Kernel의 역할
1편에서 Shell은 "명령을 해석해 fork()와 execve()로 실행하는 프로그램"이라고 정리했다. 이번 글은 그 Shell, 특히 대부분의 Linux 서버에서 기본으로 쓰는 Bash 를 자세히 본다.
관제 관점에서 Bash를 알아야 하는 이유는 명확하다. 공격자가 서버에 침투해서 가장 먼저 얻으려는 것이 쉘(명령 실행 환경) 이고, 침투 후 남기는 흔적의 상당 부분이 Bash 설정 파일과 명령 이력 에 있다.
이번 글에서 답할 질문:
| Shell | 특징 | 주로 쓰이는 곳 |
|---|---|---|
sh | POSIX 표준 쉘 | 스크립트 호환성 기준 |
bash | sh 호환 + 이력·자동완성·배열 등 기능 확장 | RHEL·Ubuntu 로그인 쉘 기본값 |
dash | 가볍고 빠른 POSIX 쉘 | Ubuntu의 /bin/sh (시스템 스크립트용) |
zsh | 확장 기능, 플러그인 | macOS 기본, 개인 개발 환경 |
Rocky Linux의
/bin/sh는 bash를 가리키고, Ubuntu의/bin/sh는 dash를 가리킨다.ls -l /bin/sh로 확인할 수 있다. 같은 스크립트가 배포판마다 다르게 동작하는 원인이 되기도 한다.
/etc/passwd의 마지막 필드가 로그인 쉘이다.
roror:x:1000:1000:roror:/home/roror:/bin/bash
apache:x:48:48:Apache:/usr/share/httpd:/sbin/nologin
/sbin/nologin은 로그인을 거부하는 쉘이다. 서비스 계정은 원래 이렇게 설정되어 있어야 하며, 서비스 계정의 쉘이 /bin/bash로 바뀌어 있다면 점검 대상 이다.
| 구분 | 예 | 읽는 설정 파일 |
|---|---|---|
| 로그인 + 대화형 | SSH 접속, su - | /etc/profile → ~/.bash_profile 등 |
| 비로그인 + 대화형 | 로그인 후 bash 입력, 새 터미널 창 | ~/.bashrc |
| 비대화형 | bash script.sh, cron 작업 | (원칙적으로 대화형 설정 파일을 읽지 않음) |
| 원격 명령 | ssh host 'uptime' | 비대화형으로 실행 |

로그인 쉘은 /etc/profile을 먼저 읽고, 사용자 홈의 ~/.bash_profile, ~/.bash_login, ~/.profile 중 처음 발견한 하나만 실행한다. RHEL 계열은 ~/.bash_profile이 ~/.bashrc를 호출하고, ~/.bashrc가 다시 /etc/bashrc를 호출하는 구조다.
즉, 로그인할 때마다 여러 파일이 자동으로 실행된다. 이것이 편리함의 원천이자 공격자의 지속성(Persistence) 경로다.
Bash는 명령을 실행하기 전에 다음 순서로 확장(expansion)한다.
입력: echo ~/logs/{secure,messages} $USER $(date +%F) *.log
↓ 1. 중괄호 확장 ~/logs/secure ~/logs/messages
↓ 2. 틸드 확장 /home/roror/logs/...
↓ 3. 변수 확장 $USER → roror
↓ 4. 명령 치환 $(date +%F) → 2026-09-23
↓ 5. 산술 확장 $((1+1))
↓ 6. 단어 분리
↓ 7. 경로명 확장 *.log → 실제 파일 목록
실행
따옴표에 따라 확장 여부가 달라진다.
| 표기 | 변수·명령 치환 | 예 |
|---|---|---|
'...' 작은따옴표 | 하지 않음 | echo '$HOME' → $HOME |
"..." 큰따옴표 | 함 | echo "$HOME" → /home/roror |
로그 분석에서 grep에 정규식을 넘길 때 작은따옴표를 쓰는 이유가 이것이다. 쉘이 먼저 *나 $를 해석해 버리면 의도와 다른 패턴이 전달된다.
| 변수 | 의미 | 기본값(대체로) |
|---|---|---|
HISTFILE | 이력 파일 경로 | ~/.bash_history |
HISTSIZE | 메모리에 유지할 개수 | 1000 |
HISTFILESIZE | 파일에 저장할 개수 | 1000 |
HISTTIMEFORMAT | 실행 시각 기록 형식 | 없음 (시각 미기록) |
HISTCONTROL | 저장 제외 규칙 | ignoredups 등 |
중요한 사실 두 가지:
HISTTIMEFORMAT을 설정해야 시각이 남는다.# 1) 현재 쉘 확인
echo $SHELL # 로그인 쉘 (passwd에 지정된 값)
echo $0 # 현재 실행 중인 쉘 이름 (-bash 면 로그인 쉘)
ps -p $$ -o pid,ppid,cmd
cat /etc/shells
ls -l /bin/sh
# 2) 계정별 로그인 쉘 확인
awk -F: '{print $1, $7}' /etc/passwd | column -t
# 3) 명령어 종류 확인
type cd ls ll
alias
# 4) 시작 파일 확인
ls -la --time-style=full-iso ~/.bash* /etc/profile /etc/bashrc
ls -la /etc/profile.d/
# 5) history에 시각 기록 설정 (본인 계정)
echo 'export HISTTIMEFORMAT="%F %T "' >> ~/.bashrc
source ~/.bashrc
history | tail -5
아래 출력은 형식 설명용 예시다.
① echo $0
| 결과 | 의미 |
|---|---|
-bash | 앞에 -가 붙으면 로그인 쉘 |
bash | 비로그인 쉘 |
② awk -F: '{print $1, $7}' /etc/passwd
root /bin/bash
bin /sbin/nologin
apache /sbin/nologin
roror /bin/bash
사람이 쓰는 계정만 /bin/bash이고 나머지는 /sbin/nologin인 것이 정상 패턴이다.
③ history (HISTTIMEFORMAT 설정 후)
101 2026-09-23 10:12:03 sudo tail -n 20 /var/log/secure
102 2026-09-23 10:13:40 ss -tulnp
시각이 남으면 다른 로그(/var/log/secure, auditd)와 같은 타임라인에 올려 비교 할 수 있다. 관제 환경에서는 /etc/profile.d/에 전사 공통으로 설정하는 경우가 많다.
| 공격 기법 | 방법 | 흔적 |
|---|---|---|
| 시작 파일 지속성 | ~/.bashrc, /etc/profile.d/*.sh에 명령 삽입 → 로그인할 때마다 실행 | 파일 수정 시각 변경, 낯선 명령 줄 |
| alias 가로채기 | alias sudo='...'로 비밀번호 입력을 가로챔 | alias, type sudo 결과가 alias로 표시 |
| 이력 회피 | unset HISTFILE, HISTFILE=/dev/null, ~/.bash_history를 /dev/null 링크 | 이력 파일 크기 0, 심볼릭 링크 |
| 이력 삭제 | history -c, 파일 삭제 | 로그인 기록은 있는데 이력이 비어 있음 |
| 쉘 변경 | 서비스 계정 쉘을 /bin/bash로 변경 | /etc/passwd 변경, usermod 기록 |
여기서 중요한 점은 bash history는 증거로서 한계가 있다 는 것이다. 사용자가 쉽게 지우거나 끌 수 있기 때문이다. 그래서 관제에서는 history를 참고 자료로 보고, 사용자가 조작하기 어려운 auditd(커널 수준의 실행 기록) 나 SIEM으로 이미 전송된 로그 를 핵심 증거로 삼는다.
# 시작 파일 최근 변경 여부 (최근 7일)
sudo find /etc/profile /etc/profile.d /etc/bashrc -type f -mtime -7
sudo find /root /home -maxdepth 2 -name '.bash*' -mtime -7
# history 파일 이상 여부 (크기 0, /dev/null 링크)
sudo ls -la /root/.bash_history /home/*/.bash_history
# 로그인 가능한 쉘을 가진 계정
awk -F: '$7 !~ /(nologin|false)$/ {print $1, $7}' /etc/passwd
history에 의존하지 않고 커널 수준에서 명령 실행을 기록하려면 execve 감사 규칙을 사용한다(5편 System Call에서 자세히 다룬다).
# /etc/audit/rules.d/exec.rules
-a always,exit -F arch=b64 -S execve -F auid>=1000 -F auid!=unset -k user_exec
auid는 로그인할 때 부여되어 sudo·su를 거쳐도 바뀌지 않는 원래 사용자 ID 다. 그래서 "누가 root로 이 명령을 실행했는가"를 추적할 때 핵심 필드가 된다.
[Event] 로그인 성공(secure) 이후 history 비어 있음
↓
[IOC] ~/.bash_history → /dev/null 링크, ~/.bashrc 최근 수정
↓
[분석] auditd user_exec 이벤트로 실제 실행 명령 복원
↓
[판단] 의도적 흔적 삭제 → 침해 의심 등급 상향
↓
[대응] 시작 파일 원본 보존(해시) → 삽입된 명령 분석 → 계정 조치
/bin/sh는 dash라는 차이가 있다./etc/profile → ~/.bash_profile(또는 .bash_login, .profile 중 하나) → ~/.bashrc 순으로 설정을 읽는다.HISTTIMEFORMAT으로 보완한다.auid 기반 실행 기록 이 더 신뢰할 수 있다./sbin/nologin인지 확인한다.다음 글 「4. User Space와 Kernel Space」 에서는 1·2편에서 나온 두 공간의 경계를 메모리 관점에서 더 깊게 본다. 프로세스마다 독립된 가상 메모리가 어떻게 구성되는지, 커널 스레드는 어떻게 구분하는지, 그리고 커널 스레드처럼 이름을 위장한 악성 프로세스를 어떻게 찾아내는지 다룬다.