Back to Blog
LinuxSet-UID권한환경변수접근제어

0x0A. Set-UID와 환경 변수 공격

Linux 권한 체계의 핵심인 Set-UID 프로그램의 동작 원리와 환경 변수를 통한 공격 기법을 다룬다

Linux 보안 기초

사용자(User)

Linux에서 각 사용자는 고유한 User ID를 부여받는다. 이 정보는 /etc/passwd 파일에 저장된다.

etc/passwd 파일 구조

id 명령어로 현재 사용자의 실제(Real) UID와 유효(Effective) UID, 그룹 ID를 확인할 수 있다.

id 명령어 결과

사용자 추가는 두 가지 방법으로 가능하다:

  • /etc/passwd에 직접 추가
  • adduser 명령어 사용
사용자 추가

다른 사용자로 전환하려면 su 명령어를 사용한다.

사용자 전환

그룹(Group)

그룹은 여러 사용자를 묶어 권한을 일괄 관리하는 단위이다. 한 사용자는 여러 그룹에 속할 수 있으며, 기본(primary) 그룹은 /etc/passwd에 기록된다.

그룹 정보

그룹 관리 명령어:

그룹 관리

권한과 접근 제어

파일에 대한 접근 권한:

  • read (r): 파일 내용 읽기
  • write (w): 파일 내용 수정
  • execute (x): 프로그램/스크립트 실행

디렉토리에 대한 접근 권한:

  • read (r): 디렉토리 내용 나열 (ls)
  • write (w): 파일/하위 디렉토리 생성
  • execute (x): 디렉토리 진입 (cd)
권한 구조

chmod 명령어로 파일 권한을 변경한다.

chmod 사용법

umask는 새 파일의 기본 권한을 결정한다.

umask 값

주의: umask 연산은 XOR이나 단순 AND가 아니다. 보수(complement)와의 AND 연산이다.

umask 연산

**ACL (Access Control List)**은 전통적인 권한 모델과 공존하며, 개별 사용자/그룹에 세밀한 권한을 부여한다.

getfacl로 ACL 조회:

getfacl

setfacl로 ACL 설정:

setfacl

권한 상승 방법

Linux에서 권한이 필요한 명령을 실행하는 세 가지 방법:

  1. sudo
  2. Set-UID 프로그램
  3. POSIX Capabilities

sudo 사용

sudo(Super-user Do)는 슈퍼유저 권한으로 명령을 실행한다. 사용자는 /etc/sudoers에 등록되어야 한다.

sudoers 설정

Ubuntu 20.04에서는 root 계정이 잠겨 있어 직접 로그인이 불가능하다. root 쉘을 얻으려면:

  • sudo -s
  • sudo bash
  • sudo su
root 쉘 획득

권장사항: root 쉘에서 작업하는 것보다 개별 명령에 sudo를 붙여 실행하는 것이 안전하다.

다른 사용자 권한으로 명령 실행:

다른 사용자로 실행

POSIX Capabilities

root 권한을 세분화한 단위이다. man capabilities로 전체 목록을 확인할 수 있다.

주요 Capabilities:

  • CAP_CHOWN: 파일 UID/GID 변경
  • CAP_DAC_OVERRIDE: 파일 읽기/쓰기/실행 권한 검사 우회
  • CAP_DAC_READ_SEARCH: 파일 읽기 및 디렉토리 검색 권한 우회
  • CAP_NET_RAW: RAW/PACKET 소켓 사용

Capability 설정:

Capability 설정 1 Capability 설정 2

실제 사례 - Wireshark: 패킷 스니핑 도구인 Wireshark에서 GUI 부분은 일반 권한으로, 실제 스니핑을 담당하는 dumpcap은 특수 권한으로 실행된다.

Wireshark 권한

ping 명령어는 raw socket을 사용하므로 CAP_NET_RAW capability가 설정되어 있다.

ping capability

인증(Authentication)

사용자 신원을 확인하는 과정이다.

인증 방법의 세 가지 유형:

  • 지식 기반: 비밀번호 (something you know)
  • 소유 기반: ID 카드 (something you have)
  • 생체 기반: 지문 (something you are/do)

**다중 인증(MFA)**은 이들을 조합한다.

패스워드 파일

/etc/passwd의 각 항목은 사용자 계정 정보를 담지만, 비밀번호 자체는 여기에 저장되지 않는다.

passwd 파일

마지막 필드는 로그인 후 실행할 첫 번째 명령(쉘)을 지정한다.

로그인 쉘

Shadow 파일

비밀번호는 /etc/shadow에 별도 저장된다. /etc/passwd에서 분리한 이유는 보안 강화를 위해서이다.

shadow 파일 구조

해싱과 솔팅

비밀번호 저장 시 **솔트(salt)**를 추가하여 해시한다.

솔팅 과정

솔트의 목적은 **무차별 대입 공격(brute-force)**을 늦추는 것이다. Dictionary 공격, Rainbow Table 공격에 대응한다.

동일한 비밀번호를 가진 세 계정도 솔트가 다르면 해시 값이 달라진다.

솔트 효과

계정 잠금

비밀번호 필드에 무효한 값을 넣으면 계정이 잠긴다. Ubuntu의 root 계정이 이 방식으로 잠겨 있다.

계정 잠금

Set-UID 프로그램

비밀번호 딜레마

/etc/shadow 파일은 root만 읽을 수 있다.

shadow 권한

그렇다면 일반 사용자는 어떻게 자신의 비밀번호를 변경할 수 있을까?

비밀번호 변경 문제

Two-Tier 접근법

Two-Tier 구조

세분화된 접근 제어를 OS에 직접 구현하면 시스템이 지나치게 복잡해진다. 대신 OS는 **특권 프로그램(Privileged Programs)**이라는 확장을 통해 세밀한 제어를 구현한다.

특권 프로그램의 두 가지 유형:

  • Daemon: 백그라운드에서 실행되며 root 권한이 필요한 프로그램
  • Set-UID 프로그램: 특수 비트가 설정된 실행 파일로, 파일 소유자의 권한으로 실행된다

Set-UID 개념

Set-UID는 사용자가 프로그램 소유자의 권한으로 프로그램을 실행할 수 있게 한다.

passwd 프로그램 예시

모든 프로세스는 두 가지 UID를 가진다:

  • Real UID (RUID): 프로세스의 실제 소유자
  • Effective UID (EUID): 프로세스의 권한 (접근 제어에 사용)

일반 프로그램: RUID == EUID (실행한 사용자의 ID)

Set-UID 프로그램: RUID != EUID (RUID는 사용자, EUID는 프로그램 소유자)

파일 소유자를 root로 변경:

소유자 변경

Set-UID 비트 활성화 전:

Set-UID 전

Set-UID 비트 활성화 후:

Set-UID 후

Set-UID 프로그램은 일반 프로그램과 동일하지만, Set-UID 비트라는 특수 표시가 있다.

Set-UID 비트

실행 예시:

Set-UID 실행 예시

Set-UID의 보안성

Set-UID는 일반 사용자가 권한을 상승할 수 있게 하지만, sudo와는 다르다. 제한된 동작만 수행하도록 설계된다. 아이언맨 슈트처럼 정해진 기능만 사용 가능한 것이다.

모든 프로그램을 Set-UID로 만들면 위험하다:

  • /bin/sh를 Set-UID로 만들면? 무제한 root 쉘
  • vi를 Set-UID로 만들면? 모든 파일 수정 가능

Set-UID 프로그램의 공격 표면

공격 표면

사용자 입력을 통한 공격 (명시적 입력)

  • Buffer Overflow: 버퍼를 넘쳐 악성 코드 실행
  • Format String 취약점: 사용자 입력을 포맷 스트링으로 사용

시스템 입력을 통한 공격

  • Race Condition: 심볼릭 링크로 권한 있는 파일을 가리키게 함
  • 쓰기 가능한(world writable) 폴더 내 파일 조작

환경 변수를 통한 공격

환경 변수는 프로세스 동작에 영향을 주는 동적 값이다. 사용자가 프로그램 실행 전에 설정할 수 있다.

PATH 환경 변수 공격:

  • system() 함수는 /bin/sh를 먼저 호출한다
  • system("ls") 실행 시 쉘이 PATH를 사용해 ls를 찾는다
  • 공격자가 PATH를 조작하면 다른 프로그램이 실행될 수 있다

Capability Leaking

특권 프로그램이 실행 중 권한을 낮추는(downgrade) 경우가 있다.

su 프로그램 예시:

  1. EUID=root, RUID=user1로 시작
  2. 비밀번호 확인 후 EUID와 RUID 모두 user2로 변경

문제는 권한 낮추기 전에 특권 capability를 정리하지 않으면 누출된다는 것이다.

Capability Leaking 1 Capability Leaking 2

OS X 사례 (2015, OS X 10.10):

Dynamic linker(dyld)에 DYLD_PRINT_TO_FILE 환경 변수가 추가되었다. root 소유 Set-UID 프로그램에서 이 변수로 보호된 파일에 쓸 수 있었다. dyld가 파일 디스크립터를 닫지 않아 capability가 누출되었다.

외부 프로그램 호출

Set-UID 프로그램에서 외부 명령을 호출할 때 주의가 필요하다.

위험한 방식: system()

system() 함수는 가장 쉬운 외부 명령 호출 방법이지만 위험하다.

system() 취약점 코드

위 프로그램은 /bin/cat을 실행하려 하지만, root 소유 Set-UID이므로 모든 파일을 볼 수 있다. 다른 명령도 실행할 수 있을까?

system() 공격

안전한 방식: execve()

execve()는 코드(명령 이름)와 데이터를 명확히 분리한다. 사용자 데이터가 코드가 될 수 없다.

execve() 코드 execve() 실행

주의: execlp(), execvp(), execvpe()는 쉘 동작을 모방하므로 PATH 환경 변수 공격에 취약하다.

다른 언어에서의 위험

외부 명령 호출 위험은 C에만 국한되지 않는다:

  • Perl: open() 함수가 쉘을 통해 명령 실행
  • PHP: system() 함수
PHP 취약점 예시

공격 예시:

http://localhost/list.php?dir=.;date

서버에서 실행되는 명령: /bin/ls .;date

분리의 원칙 (Principle of Isolation)

코드와 데이터를 섞지 말 것

이 원칙 위반으로 발생하는 공격:

  • system() 코드 실행
  • Cross Site Scripting (XSS)
  • SQL Injection
  • Buffer Overflow

최소 권한의 원칙 (Principle of Least Privilege)

특권 프로그램은 필요한 권한만 가져야 한다. 권한이 불필요할 때는 일시적/영구적으로 비활성화한다.

Linux에서 seteuid(), setuid()로 권한을 조절할 수 있다.


환경 변수

환경 변수는 프로세스 실행 환경의 일부로, 프로세스 동작에 영향을 준다. Unix에서 도입되어 Windows에서도 채택되었다.

PATH 변수 예시: 전체 경로 없이 프로그램을 실행하면 쉘이 PATH를 사용해 프로그램 위치를 찾는다.

환경 변수 접근

main 함수에서 접근:

main에서 환경변수

전역 변수 사용 (더 안정적):

environ 전역변수

환경 변수 획득

두 가지 방법:

  1. fork(): 자식 프로세스가 부모의 환경 변수를 상속
  2. execve(): 메모리가 덮어씌워지므로 명시적으로 전달해야 함

execve()로 환경 변수 전달:

execve 환경변수 전달 코드

새 환경 변수 배열을 세 번째 인자로 전달한다.

execve 실행 1 execve 실행 2

환경 변수의 메모리 위치

envpenviron은 초기에 같은 위치를 가리킨다. 하지만:

  • envp: main 함수 내에서만 접근 가능
  • environ: 전역 변수

환경 변수가 추가되면 저장 위치가 힙으로 이동할 수 있어 environ은 변경되지만 envp는 변경되지 않는다.

환경변수 메모리

쉘 변수

쉘 변수와 환경 변수는 다르다. 쉘 변수는 쉘 내부에서 사용하는 변수이다.

쉘 변수 생성

/proc 파일 시스템

Linux의 가상 파일 시스템으로, 각 프로세스 디렉토리에 environ 파일이 있다.

  • /proc/932/environ: 프로세스 932의 환경 변수
  • strings /proc/$$/environ: 현재 쉘의 환경 변수 출력

env 명령어는 자식 프로세스에서 실행되므로 쉘 자체가 아닌 자식의 환경 변수를 출력한다.

쉘이 시작되면 환경 변수를 쉘 변수로 복사한다. 쉘 변수 변경은 환경 변수에 반영되지 않는다.

쉘 변수와 환경 변수

쉘 변수가 자식 프로세스의 환경 변수에 영향을 주는 과정:

자식 프로세스 환경변수

쉘에서 env를 실행하면 자식 프로세스가 생성된다:

env 명령어 실행

환경 변수의 공격 표면

사용자가 환경 변수를 설정할 수 있으므로, Set-UID 프로그램의 공격 표면이 된다. 숨겨진 환경 변수 사용이 특히 위험하다.

환경변수 공격 표면

Dynamic Linker를 통한 공격

링킹은 프로그램이 참조하는 외부 라이브러리 코드를 찾는 과정이다:

  • Static linking: 컴파일 시 링킹
  • Dynamic linking: 런타임에 링킹 (환경 변수 사용)
링킹 예시 코드

Static linking:

링커가 프로그램 코드와 라이브러리 코드를 결합한다. 정적 컴파일된 프로그램은 동적 프로그램보다 약 100배 크다.

Static linking

Dynamic linking:

런타임에 공유 라이브러리(Windows의 DLL)와 링킹된다.

Dynamic linking

ldd 명령어로 의존하는 공유 라이브러리를 확인할 수 있다:

ldd 명령어

위험성

Dynamic linking은 메모리를 절약하지만, 프로그램 코드 일부가 컴파일 시점에 결정되지 않는다. 사용자가 이 부분에 영향을 줄 수 있다면 프로그램 무결성이 훼손될 수 있다.

사례 1: 일반 프로그램

LD_PRELOAD는 링커가 먼저 검색할 공유 라이브러리 목록이다. 함수를 찾지 못하면 LD_LIBRARY_PATH에서 검색한다. 두 변수 모두 사용자가 설정 가능하다.

sleep 함수를 호출하는 프로그램:

sleep 프로그램

직접 구현한 sleep() 함수:

악성 sleep()

공유 라이브러리 생성 및 LD_PRELOAD 설정:

LD_PRELOAD 공격

사례 2: Set-UID 프로그램

위 기법이 Set-UID 프로그램에도 적용된다면 매우 위험하다.

Set-UID에 적용

직접 만든 sleep()이 호출되지 않았다. Dynamic linker의 방어 기능 때문이다. EUID와 RUID가 다르면 LD_PRELOADLD_LIBRARY_PATH를 무시한다.

검증 실험:

검증 준비 검증 결과

사례 3: OS X Dynamic Linker

OS X 10.10에서 DYLD_PRINT_TO_FILE 환경 변수가 보안 분석 없이 추가되었다.

Set-UID 프로그램에서 사용자가 보호된 파일에 쓸 수 있었고, 파일 디스크립터가 닫히지 않아 capability leak이 발생했다.

공격 예시:

  1. DYLD_PRINT_TO_FILE을 /etc/sudoers로 설정
  2. Bob 계정으로 전환
  3. echo 명령으로 /etc/sudoers에 쓰기
OS X 공격

외부 프로그램을 통한 공격

애플리케이션이 환경 변수를 직접 사용하지 않아도, 호출하는 외부 프로그램이 사용할 수 있다.

외부 프로그램 호출 방식:

  • exec() 계열 → execve() 호출: 프로그램 직접 실행
  • system() → execl() → execve() → /bin/sh: 쉘을 통해 실행

쉘 프로그램은 PATH 등 많은 환경 변수의 영향을 받는다.

외부 프로그램 공격 코드 외부 프로그램 공격 결과

공격 표면 비교:

  • system(): 쉘을 호출하므로 환경 변수 영향을 받음
  • execve(): 쉘을 호출하지 않으므로 더 안전

특권 프로그램에서 외부 명령 호출 시 execve()를 사용해야 한다.

애플리케이션 코드를 통한 공격

프로그램이 직접 환경 변수를 사용하면 신뢰할 수 없는 입력이 된다.

getenv 취약점 코드

위 프로그램은 getenv()로 PWD 환경 변수를 가져와 배열에 복사한다. 입력 길이를 검사하지 않아 버퍼 오버플로우가 발생할 수 있다.

PWD 조작

대응책

Set-UID 프로그램에서 환경 변수 사용 시 **적절한 검증(sanitization)**이 필요하다.

secure_getenv() 함수를 사용할 수 있다:

  • getenv(): 환경 변수 목록에서 검색하여 포인터 반환
  • secure_getenv(): 동일하지만 "secure execution"이 필요하면 NULL 반환

Secure execution 조건: 프로세스의 EUID와 RUID가 다를 때

Set-UID 방식 vs Service 방식

Set-UID vs Service

일반 사용자가 특권 작업을 수행하는 두 가지 접근법:

  • Set-UID 방식: 특수 프로그램을 실행해 일시적으로 root 권한 획득
  • Service 방식: 특권 서비스에 작업 요청

Set-UID의 공격 표면이 훨씬 넓다. 환경 변수가 주요 원인이다:

  • Set-UID: 환경 변수를 신뢰할 수 없음
  • Service: 환경 변수를 신뢰할 수 있음

Service 방식에도 다른 공격 표면이 존재하지만, Set-UID보다 안전하다고 평가된다.

이러한 이유로 Android OS는 Set-UID와 Set-GID 메커니즘을 완전히 제거했다.


HGU 전산전자공학부 고윤민 교수님의 24-2 컴퓨터 보안 수업을 듣고 작성한 포스트이며, 첨부한 모든 사진은 교수님 수업 PPT의 사진 원본에 필기를 한 수정본입니다.