부트캠프에서 배운 내용을 정리한 글입니다.


점검을 위해 금융기관 등 민감한 환경을 방문했을 때, 단순 분석을 위해 별도의 소프트웨어(특히 Python과 같은 런타임 환경)을 설치하는 방식은 바람직하지 않다. 해당 환경의 무결성을 해칠 우려가 있으며, 외부 의존성을 남기는 것 자체가 감사·운영 측면에서 리스크로 받아들여진다. 이러한 이유로, 필요하다면 분석 기능을 바이너리 형태로 패키징하거나, 이미 시스템에 내장된 도구를 활용하는 접근이 권장된다.
특히 다양한 리눅스 배포판을 다루는 현업에서는 POSIX 호환 셸(SH/Bash) 기반의 스크립트를 우선적으로 고려하는 것이 가장 현실적이다.

네트워크는 본질적으로 “메모리” 기반의 처리 구조를 갖는다. 들어오는 패킷은 OS에 도달하기 전에 방화벽이나 NIC 레벨 큐에서 선처리되고, 이 과정에서 룰과 정책과 매칭되어 필터링된다.
만약 방화벽 이후 추가적인 보안 레이어(예: Zero Trust 검증 단계)를 삽입하고자 한다면, 기존 OS 구조 내에서 단순히 기능을 ‘얹는’ 방식으로는 어렵다. 패킷 처리 경로 자체를 다시 설계해야 가능하며, 이는 상당한 난이도를 가진 작업이다.

파일 분석 측면에서는, 파일의 정체성은 확장자가 아니라 헤더(Signature) 에 의해 결정된다. DLL, SYS, EXE는 공통적으로 PE(Portable Executable) 구조를 기반으로 하며, 운영체제는 내부 헤더를 기준으로 파일을 해석한다.
단, Windows는 사용자 편의성을 위해 확장자를 실행 프로그램 연결의 기준으로 삼는데, 이로 인해 확장자를 조작하는 방식의 사회공학 공격이나 오용이 발생하기도 한다.

실제로 포렌식이나 CTF 문제에서는 파일 확장자를 의도적으로 제거하거나 잘못된 확장자를 부여하여, 분석자가 헤더 기반 식별 능력을 갖추고 있는지를 평가한다.


1. 리눅스 파일 입출력

리눅스 시스템은 모든 것을 파일로 다루는 철학을 가지고 있다. 하드디스크의 데이터, 디바이스 장치, 네트워크 소켓, 프로세스 간 통신까지 모두 파일 입출력의 형태로 처리된다.

파일 입출력은 리눅스 사용의 핵심이며, 이를 바탕으로 다양한 텍스트 처리 명령어를 활용하면 복잡한 데이터 가공과 분석을 단 몇 줄의 명령으로 자동화할 수 있다.

1.1 cut 명령어

구분자를 무엇으로 설정할 것인가?

cut 명령어의 한계

$ time cut -d " " -f1,3 data

time으로 명령어 처리시간을 보았을 때

cut 명령어는 데이터를 버퍼에 담지 않기 때문에 재가공하는 것이 어렵다. 또한 필드 구분자가 일정하지 않으면 사용이 매우 난감해진다. 다만 구분자가 의미없는 첫번째 필드를 볼 때는 cut 명령어의 속도와 편의성이 압도적이다. 두번째 세번째 부터는 조금 애매해진다.

스크립트에서는 쉘에서 처리하는 속도를 빠르게 하는 것이 최우선된다. 속도도 속도이지만, 시스템에 부하가 걸릴 수 있기 때문이다.

cut 예제

paste 명령어

diff 명령어

diff 명령어는 스크립트 명령어에 사용하기에는 적합하지 않다. 일상에서 사용하는건 괜찮다.

grep 명령어

-i옵션은 대소문자를 구분하지 않는다.

sort 명령어

공백을 제외한 구분자는 반드시 구분자를 명시해야 한다.

sed 명령어

awk 명령어

hjl3066@HCR-Zenbook:/mnt/c/LAB$ awk '$2 = 40{print $1,$2"살 입니다."}' data | head -2
hong 40살 입니다.
park 40살 입니다.

비교가 아닌 대입을 한다. $2 필드의 값을 40으로 변경하고 조건문에서는 항상 참으로 간주한다. 따라서 모든 줄에 대해 $2 가 40으로 바뀌고 출력된다.

hjl3066@HCR-Zenbook:/mnt/c/LAB$ awk '$2 == 49{print $1,$2"살 입니다."}' data
son 49살 입니다.

숫자 비교 연산자로 $2 가 정수 49와 같을 때만 조건이 참이 된다.

hjl3066@HCR-Zenbook:/mnt/c/LAB$ awk '$2 ~ /49/{print $1,$2"살 입니다."}' data
son 49살 입니다.

~ 는 정규표현식 연산자로 $2 필드 안에 49라는 문자열이 포함되어 있으면 조건이 참이 된다. 부분 문자열로 49만 포함되면 모두 매칭된다.

이건 crontab과의 연관성이 좋다.

세 번째 필드($3)를 하이픈(-)을 기준으로 분리해서 첫 번째 부분 phone[1] 만 출력하는 명령이다. 동작 원리는 다음과 같다.

  1. $3 필드를 -기호로 나누어 배열 phone에 저장한다.
  2. $1 필드와 분리된 전화번호의 앞자리를 출력한다.

예제

#1. 1000번 이상의 사용자를 사용자 이름과 UID를 출력하시오.

쉘 스크립트 작성 전 확인해야할 부분

  1. 쉘 스크립트 동작하는 os의 쉘에 따라 script는 영향을 받는다. 특히 기존 bash 환경에서 사용되었던 반복문 일부와 명령어 패키지가 가진 옵션이 다른 경우가 있다.
    a. bash → sh
    b. #!/bin/bash#!/bin/sh
    c. 명령어 패키지
    [user@lb ~]$ which ls
    alias ls=’ls —color=auto’
    /usr/bin/ls
    d. 사용중인 명령어가 옵션이 다르게 이용되거나 없으면(패키지가 다르다 보면 발생) binary 파일들 즉 sciprt에 사용되는 명령어를 directory에 모아두고 환경변수로 지정해야 하는 상황도 발생할 수 있다. find, awk
    e. container 환경은 기존 시스템의 환경을 그대로 올린 환경이 대부분이다.
  2. shell이 동작한느 언어 타입은 다양하다 그래서 sciprt 작성 시 초반에 LANG 환경 변수의 초기값을 잡아줘야 출력에 오류가 없다.
  3. 스크립트를 한 덩어리로 만들지 말고 점검 항목에 따라 함수화 해주어야 수정 시 해당 영역만 변경할 수 있다.
    a. if문을 예로 들었을 때 if문 자체를 길게 늘어트리거나 다중으로 사용하면 해당 메모리공간을 계속 잡고있기 때문에 굉장히 무거워진다.
    b. 필요하다면 awk를 사용해서 값을 넘기는게 더 효율적이다.
  4. 시스템 script 작성 시 시스템의 환경에서 따른 명령어가 전혀 다름을 확인하고 script 초반에 OS 별 정보를 확인해서 다른 동작을 하도록 case 구분 같은 것을 이용해야 한다.

명령어와 언어에 익숙해지도록 많이 다루는 것이 중요하다.

리눅스에 접속한 사용자 추적하기

UID기반으로 접속한 사용자를 추적할 수 있다. 하지만 더 좋은 방법은 터미널로 추적하는 것이다. 침해사고 분석 등에서 유용하게 사용되는 방법은 로그한 사용자의 계정이 접속한 터미널이다.

ps -ef | grep pts/1
[user@localhost ~]$ lastlog | grep -v "**Never logged in**"
Username         Port     From                                       Latest
root             tty1                                               Tue Sep 30 15:32:03 +0900 2025
user             pts/0    10.0.2.1                                  Thu Oct 30 17:05:29 +0900 2025
testuser         tty1                                               Thu Oct 30 17:14:30 +0900 2025
[user@localhost ~]$ last
user     pts/0        10.0.2.1         Thu Oct 30 17:05   still logged in
user     tty1                          Thu Oct 30 17:05   still logged in
reboot   system boot  5.14.0-570.39.1. Thu Oct 30 17:04   still running
user     tty1                          Thu Oct 30 16:08 - down   (00:01)
reboot   system boot  5.14.0-570.39.1. Thu Oct 30 15:03 - 16:10  (01:06)
user     pts/1        10.0.2.1         Tue Oct 21 13:52 - 13:52  (00:00)
user     pts/0        10.0.2.1         Tue Oct 21 13:52 - 17:06  (03:13)

실제로 testuser로 접속해 있지만 표시되지 않는다. 커널 취약점으로 자신의 터미널을 숨기는 작업을 하는 이상이 아니고서야 os 레벨에서 터미널을 숨길 수 있는 방법은 없다.

확실한 테스트를 위해 외부에서 testuser로 접속했다.

pts/1 tty로 접속한 testuser가 보이고 위의 명령어를 통해 [ ]를 확인할 수 있다.

TTY vs PTS 개념 및 관계

TTY

TTY는 원래는 물리 콘솔에 직접 연결된 입력 장치를 의미한다. Linux에서는 /dev/tty1, /dev/tty2 와 같은 로컬 콘솔이 바로 TTY다. 서버 현장에서 실제로 TTY로 접속하는 경우는 거의 없다. 직접 서버 앞에 앉아서 로그인해야 하기 때문이다.

TTY 세션이 등장한다는 건 “해당 시스템에 물리적으로 접근한 사람"이 존재한다는 뜻이다. 따라서 침해사고 포렌식에서는 내부자 행위 가능성, 또는 누군가 서버실 접근 권한을 악용했을 가능성까지 고려한다.

PTS

PTS (Pseudo Terminal Slave)는 원격 접속(SSH 등)에서 생성되는 가상 터미널이다. /dev/pts/0, /dev/pts/1 형태이며, 접속할 때마다 동적으로 번호가 증가한다. 대부분의 서버 운영 환경에서는 모든 관리 작업이 원격으로 이루어지므로 거의 모든 접속은 PTS라고 보면 된다.

SAN 스위치

SAN 스위치는 Fibre Channel 기반의 스토리지 네트워크 장비이며, 서버의 “콘솔 접속”을 의미하는 장치는 아니다.

물리 접근 또는 관리자용 원격 콘솔을 사용한 경우에만 TTY가 생성된다. 일반 사용자가 SAN 스위치를 통해 tty를 따는 일은 사실상 없다.

만약 침해사고 분석에서 tty 세션이 발견된다면?

  • 내부자 또는 물리 접근이 가능한 인력이 직접 로그인했을 가능성이 매우 크다. 외부 공격자가 tty 세션을 만드는 건 현실적으로 거의 불가능하다. 서버실 물리 환경, 콘솔 권한, 관리 네트워크 접근 등 여러 장치를 통과해야 하기 때문이다.
  • 공격자가 세션을 종료하는 순간부터는 로그와의 싸움이 된다. 그 이유는 다음과 같다.
    - 모든 프로세스를 기록할 수는 없다. 프로세스 생성/종료 이벤트는 초당 수백~수천 개 발생하는데 특히 대규모 서버에서는 로그 저장량이 폭발적으로 증가한다. 운영 환경에서는 디스크 용량, I/O 부담, 성능 문제 때문에 모든 이벤트를 기록할 수 없다.
    - 대부분의 로그는 노이즈며 실제 보안 분석에 필요한 로그는 전체의 0.1% 미만이다. 대부분은 서비스 동작, 주기성 잡업(cron), 정상적인 API 요청 같은 로그다. 그걸 전부 기록해두면 용량만 낭비하고 분석 난이도만 폭증한다.
  • 공격자는 흔적 지우기를 시도한다. (history 삭제, 로그 tampering, tmpfs 활용, 메모리 기반 payload 실행)

2. 쉘 스크립트

운영 보안은 ansible, 클라우드 보안은 쿠버네티스를 공부하면 매우 좋다.


변수를 내부에 넣을거면 반드시 더블쿼테이션을 사용해야 한다.

숙제

계정을 추적하기 위해 터미널 넘버에 집중해야 사용자 트래킹에 유리하다.

su 접속과 로그의 시간을 일치여부를 매칭하여 이 두 요소를 시각화하기. 왜냐하면 지금 su 접속과 1시간전 su 접속 기록이 미스매칭되면 안되기 때문.

su를 하면 남는 흔적(로그)를 활용하여 탐지 스크립트 작성.

힌트.

첫번째 목표 : 로그인, 로그아웃 감시

#!/bin/bash

read -p "감시할 사용자 이름을 입력하세요: " USER_TO_WATCH
echo "사용자 '$USER_TO_WATCH' 로그인/로그아웃 감시를 시작합니다."
echo "중지하려면 [Ctrl + C]를 누르세요."
echo

IS_LOGGED_IN=0

while true; do
        if who | grep -w "$USER_TO_WATCH" > /dev/null; then
                if [ $IS_LOGGED_IN -eq 0 ]; then
                        LOGIN_TIME=$(date "+%Y-%m-%d %H:%M:%S")
                        echo "[+] $USER_TO_WATCH 로그인 감지: $LOGIN_TIME"
                        IS_LOGGED_IN=1
                fi
        else
                if [ $IS_LOGGED_IN -eq 1 ]; then
                        LOGIN_TIME=$(date "+%Y-%m-%d %H:%M:%S")
                        echo "[+] $USER_TO_WATCH 로그아웃 감지: $LOGIN_TIME"
                        IS_LOGGED_IN=0
                fi
        fi
        sleep 5
done

두번째 목표 : su 추적

#!/bin/bash

read -p "감시할 사용자 이름을 입력하세요: " USER_TO_WATCH
echo "사용자 '$USER_TO_WATCH'의 로그인/로그아웃 및 su 감시를 시작합니다."
echo "중지하려면 [Ctrl + C]를 누르세요."
echo

IS_LOGGED_IN=0

LOG_FILE="/var/log/secure"
if [ ! -f "$LOG_FILE" ]; then
    LOG_FILE="/var/log/messages"
fi
echo "[DEBUG] 감시 중인 로그 파일: $LOG_FILE"

# 로그 감시 백그라운드
tail -n 0 -F "$LOG_FILE" | while read LINE; do
    if echo "$LINE" | grep -q "pam_unix(su"; then
        # su로 세션이 열린 경우
        if echo "$LINE" | grep -q "session opened for user $USER_TO_WATCH"; then
            SU_TIME=$(date "+%Y-%m-%d %H:%M:%S")
            echo "[!] su 명령을 통한 '$USER_TO_WATCH' 로그인 감지: $SU_TIME"
        fi

        # su로 세션이 닫힌 경우
        if echo "$LINE" | grep -q "session closed for user $USER_TO_WATCH"; then
            SU_TIME=$(date "+%Y-%m-%d %H:%M:%S")
            echo "[!] su 명령을 통한 '$USER_TO_WATCH' 로그아웃 감지: $SU_TIME"
        fi
    fi
done &

# 로그인/로그아웃 감시 루프
while true; do
    if who | grep -w "$USER_TO_WATCH" > /dev/null; then
        if [ $IS_LOGGED_IN -eq 0 ]; then
            LOGIN_TIME=$(date "+%Y-%m-%d %H:%M:%S")
            echo "[+] $USER_TO_WATCH 로그인 감지: $LOGIN_TIME"
            IS_LOGGED_IN=1
        fi
    else
        if [ $IS_LOGGED_IN -eq 1 ]; then
            LOGOUT_TIME=$(date "+%Y-%m-%d %H:%M:%S")
            echo "[-] $USER_TO_WATCH 로그아웃 감지: $LOGOUT_TIME"
            IS_LOGGED_IN=0
        fi
    fi
    sleep 5
done

문제 해결 과정은 다음과 같다.
/var/log/secure/var/log/messages 를 통한 로그 확인

Nov 3 02:25:37 localhost su[5903]: (to testuser) user on pts/1

초기 코드 작성:

# 로그 파일 결정
LOG_FILE="/var/log/secure"
if [ ! -f "$LOG_FILE" ]; then
    LOG_FILE="/var/log/messages"
fi
echo "[DEBUG] 감시 중인 로그 파일: $LOG_FILE"

# 로그 감시 백그라운드
tail -n 0 -F "$LOG_FILE" | while read LINE; do
    echo "[DEBUG] 새 로그 감지: $LINE"
    if echo "$LINE" | grep -q "su"; then
        echo "[DEBUG] su 관련 로그 발견"
        if echo "$LINE" | grep -q "(to $USER_TO_WATCH)"; then
            echo "[DEBUG] 대상 사용자 감지됨"
            SU_TIME=$(date "+%Y-%m-%d %H:%M:%S")
            TTY=$(echo "$LINE" | awk '{print $NF}')
            echo "[!] su 명령을 통한 '$USER_TO_WATCH' 전환 감지 ($TTY): $SU_TIME"
        fi
    fi
done &

su와 to USERNAME이 동시에 포함된 로그 탐지

작동되지 않음을 확인하고 다시 로그 확인

[user@localhost LAB]$ sudo tail -F /var/log/secure | grep "su"
Nov 3 02:30:12 localhost su[6287]: pam_unix(su-l:session): session opened for user testuser(uid=1001) by user(uid=1000)
Nov 3 02:30:32 localhost su[6287]: pam_unix(su-l:session): session closed for user testuser
Nov 3 02:30:35 localhost sudo[6012]: pam_unix(sudo:session): session closed for user root
Nov 3 02:31:54 localhost sudo[6947]: user : TTY=pts/0 ; PWD=/LAB ; USER=root ; COMMAND=/bin/tail -F /var/log/secure
Nov 3 02:31:54 localhost sudo[6947]: pam_unix(sudo:session): session opened for user root(uid=0) by user(uid=1000) 
Nov 3 02:32:02 localhost su[7001]: pam_unix(su-l:session): session opened for user testuser(uid=1001) by user(uid=1000) 
Nov 3 02:32:08 localhost su[7001]: pam_unix(su-l:session): session closed for user testuser

로그 포맷에서 다음을 확인

pam_unix(su-l:session): session opened for user testuser(uid=1001) by user(uid=1000)
pam_unix(su-l:session): session closed for user testuser

to testuser 패턴이 아닌 session opened/closed for user testuser 형식의 기록을 확인.

세번째 목표 : 감시 결과 파일로 저장과 예외처리

감시 결과를 파일로 저장하고 /var/log/secure의 로그 파일 형식이 아닌 /var/log/messages 로그 파일 형식에도 해당 코드가 적용되는지 안되면 코드를 어떻게 수정해야할지 고민해야 함.


※ 본 글은 비영리적 목적에 한해 자유롭게 이용 가능합니다. 단, 동일한 라이선스를 적용해야 하며, 상업적 이용은 금지됩니다.

0개의 댓글