[Web Security] Chapter 5 - Shellshock

이한량·2025년 4월 11일
0

Network and Web Security

목록 보기
5/7

Environment Variables

환경 변수(Environment Variables)란, 동적으로 이름이 지정된 값들의 집합으로 프로세스가 실행되기 위한 환경의 일부가 된다.

많은 프로세스들의 동작은 환경 변수의 영향을 받으며, 프로세스는 환경 변수를 활용하여 해당 프로그램이 어떤 상태(경로, 버전 등)인지 확인할 수 있다.

예를 들어 가장 흔하게 사용되는 PATH 환경 변수에 대해서 살펴보자.

셸 프로그램이 명령어를 실행할 때 절대 경로가 제공되지 않을 경우, PATH변수를 사용하여 해당 명령어의 경로를 찾는다.

자식 프로세스가 생성될 때 환경 변수는 어떻게 될까?

  • fork() : 자식 프로세스는 부모 프로세스의 환경 변수를 상속한다.

  • execve() : 현재 실행중인 메모리 공간을 덮어쓰며 새로운 프로세스를 실행하는 함수이다. 기존 환경 변수는 모두 사라지며, execve() 함수의 매개변수로 새로운 환경 변수를 전달할 수 있다.

Shell Variables

Shell Variables는 셸 내부에서 사용하는 변수로, 셸은 사용자가 셸 변수를 생성/할당/삭제할 수 있도록 내장 명령어를 제공한다.

셸 변수와 환경 변수를 혼동할 수 있지만, 두 변수는 독립적인 변수이므로 유의해야 한다.

리눅스는 /proc이라는 가상 파일 시스템을 제공하며, 실행중인 프로세스 ID를 이름으로 하는 디렉토리가 존재한다.

예를 들면,/proc/932/environ와 같이 특정 PID의 하위 디렉토리에는 해당 프로세스가 사용하는 환경 변수 파일이 존재한다.

셸 프로그램이 시작되면 환경 변수를 자신의 셸 변수로 복사하며, 셸 변수와 환경 변수는 각각 독립적으로 존재하므로 서로의 값에 영향을 주지 않는다.

셸 변수를 변경하더라도 환경 변수 값이 변하지 않으며, 환경 변수 값을 변경하더라도 셸 변수는 변하지 않는다. 셸 변수는 셸 프로그램의 자식 프로세스의 환경 변수로 사용될 수 있다.

env 명령어를 실행할 때, 셸은 자식 프로세스를 생성한다. 셸 변수가 무조건 자식 프로세스의 환경 변수로 전달되는 것은 아니며, 이를 전달하기 위해 명령어(export)를 활용해야 한다.

Attack Surface

환경 변수는 사용자가 설정할 수 있고, 시스템 내부적으로 동작하기 때문에 환경 변수는 공격 표면이 된다.

신뢰성이 보장되지 않은 사용자 입력이 전달될 경우, 다음과 같이 여러가지 시스템 컴포넌트에 영향을 미칠 수 있다.

Attacks via Dynamic Linker

링크(Link)는 프로그램이 실행되기 위해 필요한 외부 라이브러리를 찾아서 연결하는 작업이다.

예를 들어, printf()와 같은 함수는 외부 라이브러리에 구현되어 있어 사용하기 위해선 링크가 이루어져야 한다.

링크 방식은 크게 정적 링크동적 링크로 나뉘며, 동적 링크는 프로그램 코드의 일부가 컴파일 단계에서 결정되지 않으므로 메모리를 절약할 수 있다는 장점이 존재한다.

반면 동적 링크의 경우 환경 변수를 활용하기 때문에 사용자가 환경 변수를 조작하여 프로그램의 무결성을 훼손할 수 있다.

  • Static Linking (정적 링크) : 컴파일 과정에 필요한 모든 라이브러리를 프로그램 내에 포함시켜 환경 변수 영향을 받지 않음

  • Dynamic Linking (동적 링크) : 프로그램이 실행되는 시점에 링크가 수행되며, 컴파일 시점에는 외부 라이브러리를 참조하지 않는다.

    • 환경 변수를 조작하여 참조하는 라이브러리를 변경 가능

    • ldd 명령어를 사용하여 프로그램이 어떤 공유 라이브러리를 필요로 하는지 확인할 수 있다.

Case Study 1

LD_PRELOAD는 링커가 가장 먼저 탐색할 공유 라이브러리 목록을 지정하는 환경 변수이다.

LD_PRELOAD에 포함되지 않은 외부 라이브러리는 LD_LIBRARY_PATH에 지정된 경로에서 탐색한다.

이 두 환경 변수는 모두 사용자에 의해 설정이 가능하므로, 사용자는 이를 악용하여 링크 과정(순서)을 제어할 수 있다.

이에 대한 대응책으로 동적 링커는 EUID(실행 사용자)와 RUID(실제 사용자)가 다를 경우, LD_PRELOAD와 LD_LIBRARY_RATH 환경 변수를 무시한다.

Case Study 2

Apple의 OS X 10.10에서 도입한 환경 변수 DYLD_PRINT_TO_FILE에서 발생한 보안 이슈에 대해 살펴보자.

이 환경 변수는 사용자가 디버깅 출력을 파일로 Redirection 할 수 있도록 파일명을 지정한다.

만약 이 환경 변수가 Set-UID 프로그램으로 실행된다면, 일반 사용자가 DYLD_PRINT_TO_FILE로 지정한 파일을 열고 쓸 수 있다.

이러한 보안 이슈를 Capability Leak(권한 누수)라고 하며, 안전하게 관리되어야 하는 권한이 환경 변수 등의 경로를 통해 일반 사용자에게 유출되는 문제이다.

Attacks via External Program

애플리케이션들은 외부 프로그램을 호출하기도 하며, 호출된 외부 프로그램은 환경 변수에 의존할 가능성이 높다.

외부 프로그램을 호출하는 대표적인 방법은 다음과 같다.

  • exec() 계열 : 직접 프로그램 실행

    • exec() 계열 함수는 셸을 거치지 않고 실행되기 때문에 상대적으로 안전
  • system() : 내부적으로 execl()을 호출 \rarr execl()execve()를 호출 \rarr 새로운 프로그램 실행

system() 함수를 통해 외부 프로그램을 호출할 경우, 셸을 거쳐 실행되기 때문에 환경 변수를 통한 공격이 가능하다.

system()에 비해, execve()셸을 거치지 않기 때문에 환경 변수의 영향을 적게 받는다.

Attacks via Library

외부 함수(라이브러리)를 많이 호출한다는 것은 그만큼 사용하는 환경 변수가 많아질 수 있다는 의미이며, 이는 그만큼 공격 표면이 증가함을 의미한다.

메시지를 출력할 때마다, 프로그램은 메시지 출력을 위한 표준 라이브러리 함수를 사용한다.

UNIX 시스템의 경우, libc 라이브러리의 gettext()catopen() 함수를 사용한다.

위 프로그램은 LANG, LANGUAGE, NLSPATH, LOCPATH, LC_ALL, LC_MESSAGES 환경 변수들에 의존한다.

위 환경 변수들은 사용자가 임의로 설정 가능하므로, 메시지 출력을 사용자가 제어할 수 있다.

Conectiva Linux 환경에선 이에 대한 대응책으로 Set-UID 프로그램에서 catopen(), catgets() 함수가 호출될 경우 NLSPATH 환경 변수를 명시적으로 검사하고 무시한다.

Attacks via Application Code

애플리케이션은 종종 직접적으로 환경 변수를 사용하기도 한다.

getenv() 함수 호출을 통해 셸 변수에 존재하는 PWD값이 프로그램에 전달되며, 해당 환경 변수를 통해 현재 디렉토리를 알아낼 수 있다.

PWD의 값은 셸 프로그램에서 전달되므로, 디렉토리를 변경할 때마다 셸은 이 값을 업데이트한다.

사용자가 이 셸 변수 값을 직접 변경할 수도 있다.

애플리케이션 코드를 통한 공격에 대한 대응책은 어떤 것이 있을까?

  • 환경 변수가 Set-UID 프로그램에서 사용될 경우, 환경 변수는 반드시 정제(Sanitize)되어야 한다.

  • 개발자는 getenv() 대신 secure_getenv()같은 함수를 사용하여야 한다.

    • getenv() : 환경 변수 목록을 검색해 해당 변수의 문자열 포인터를 반환하는 함수

    • secure_getenv() : 작동 방식은 동일하지만, 보안 실행(Secure execution)이 요구되는 상황에서 NULL을 반환한다.

보안 실행이란, 프로세스의 사용자 ID(EUID)와 실제 사용자 ID(RUID)가 일치하지 않는 경우를 의미한다.

Set-UID vs Service Approach

대부분의 OS에서 일반 사용자가 권한이 요구되는 작업을 수행할 수 있도록 두 가지 방식을 사용한다.

  • Set-UID : 일반 사용자가 일시적으로 직접 root 권한을 얻도록 하는 방식

    • 일반 사용자에게도 루트 권한을 부여하기 때문에 이를 악용하여 악성 라이브러리를 로드할 수 있다.
  • Service Approach : 일반 사용자가 직접 root 권한을 얻지 못하고, 신뢰된 서비스 프로세스에게 요청만 전달

    • Set-UID보다 공격 표면이 좁아 일반적으로 환경 변수를 신뢰할 수 있음

위와 같은 이유로, Android OS는 Set-UID, Set-GID 메커니즘을 완전히 제거하였음

Shellshock

Shell 프로그램이란, 운영체제에서 제공하는 Command-line interpreter이다. 즉, 셸은 사용자의 명령을 해석하여 커널에 전달하는 중개자와 같은 역할을 수행한다.

셸 프로그램은 사용자와 OS 사이의 상호작용을 위한 인터페이스를 제공하며, sh, bash, zsh 등이 대표적인 쉘 프로그램에 해당한다.

특히, bash 쉘은 리눅스 OS에서 가장 널리 사용되는 쉘 프로그램이다.

Shellshock는 bash가 환경 변수를 함수 정의로 변환하는 과정의 취약점을 악용한 공격이다. 따라서 Shellshock는 쉘 함수와 깊은 관련이 있다.

Passing Shell funcion to child process

자식 bash로 셸 함수를 전달하는 방법에 대해 알아보자.

export approach

foo()라는 함수를 쉘에서 직접 정의하여, export 명령어를 통해 자식 프로세스로 넘겨주는 예시이다.

export로 전달하는 함수는 환경 변수로 전달되며, 자식 프로세스가 bash를 실행할 때 bash 프로그램은 이 환경 변수를 다시 함수 정의로 변환한다.

Define an environment variable

foo라는 환경 변수에 함수 형태의 문자열을 넣어 자식 쉘의 환경 변수로 전달할 경우, 자식 쉘은 foo 환경 변수를 함수로 인식하고 실행한다.

이 방식은 부모 프로세스가 쉘 프로그램일 필요가 없으며, 일반적인 프로그래밍 언어나 C 프로그램으로도 가능한 방식이다.

bash가 자식 프로세스로 실행되기만 하면 환경 변수를 통해 함수가 등록된다.

Shellshock Vulnerability

부모 프로세스는 환경 변수를 사용해 자식 쉘 프로세스에게 함수 정의를 전달할 수 있다.

이때 Parsing logic의 취약점으로 인해, bash는 해당 환경 변수 안에 포함된 일부 명령어를 실행할 수 있다.

  • 위 예시의 경우, foo라는 환경 변수에 함수 정의와 명령어 echo "extra"를 포함

  • export 후 자식 프로세스에서 bash를 실행시킬 경우 "extra"가 출력

Vulnerability(취약점)이 발생하는 이유를 알아보기 위해 bash가 실행 될 때 환경 변수들을 파싱하는 코드를 살펴보자.

  • 환경 변수의 값이 () {로 시작할 경우, 이를 함수 정의로 간주하여 parse_and_execute() 함수를 통해 파싱

    • 이는 Line ALine B와 같이 파싱하는 코드이다.
  • 문자열이 진짜 함수 정의일 경우, 파싱만 진행하고 실행하진 않음

  • 문자열에 셸 명령어가 포함되어 있다면, 해당 명령어를 실제로 실행

    • 문자열 안에 악의적인 명령어가 포함된 경우에도 bash는 이를 실행

Shellshock를 위해 다음과 같은 두 가지 조건이 필요하다.

  • 대상 프로세스가 bash를 실행

  • 대상 프로세스가 신뢰성이 보장되지 않은 사용자 입력을 환경 변수를 통해 받음

profile
한량 극복 프로젝트

0개의 댓글