PyPy의 샌드박싱 기능¶
Warning
This describes the old, unmaintained version. A new version
sandbox-2 및 py3.6-sandbox-2 브랜치와 sandboxlib 저장소에서 진행 중입니다. 자세한 설명은 다음을 참고하십시오: https://mail.python.org/pipermail/pypy-dev/2019-August/015797.html
또한 python 3.7+에는 _thread 모듈이 필요하며, 이는 샌드박스 탈출 측면에서 고려해야 할 사항이 될 수 있다는 점에 유의하십시오.
소개¶
PyPy는 OS 수준의 샌드박싱(예: Linux의 SECCOMP)과 비슷한 수준의 샌드박싱을 제공하지만, 완전히 이식 가능한 방식으로 구현되어 있습니다. 이를 사용하려면 (신뢰할 수 있는 일반) 프로그램이 PyPy의 특수한 샌드박스 버전인 하위 프로세스를 실행합니다. 이 하위 프로세스는 신뢰할 수 없는 임의의 Python 코드를 실행할 수 있지만, 모든 입출력은 직접 수행되는 대신 stdin/stdout 파이프로 직렬화됩니다. 외부 프로세스는 파이프를 읽고 어떤 명령을 허용할지 결정하거나(샌드박싱), 심지어 명령을 다르게 재해석하기도 합니다(가상화). 잠재적 공격자는 하위 프로세스에서 임의의 코드를 실행시킬 수는 있지만, 외부 프로세스가 통제하지 않는 입출력은 실제로 전혀 수행할 수 없습니다. 사용되는 RAM의 양과 CPU 시간을 제한하기 위한 추가 장벽도 마련되어 있습니다.
이는 Python 언어 수준의 샌드박싱, 즉 공격자가 실행할 수 있는 Python 코드의 종류에 제한을 두는 것과는 매우 다르다는 점에 유의하십시오(이유가 궁금하십니까? pysandbox에 관한 글을 읽어 보십시오).
비교할 점이 하나 더 있습니다. 만약 대신 CPython을 특수한 가상화 C 라이브러리에 연결하려고 시도한다면, OS에 종속될 뿐만 아니라 안전하지도 않은 결과를 얻게 될 것입니다. CPython은 세그폴트를 일으킬 수 있기 때문입니다(그 방법은 여러 가지이며, 모두 정말, 정말 난해합니다). 충분한 노력을 들이면 공격자는 거의 모든 세그폴트를 취약점으로 바꿀 수 있습니다. PyPy가 생성하는 C 코드는 코드 생성기가 올바르기만 하다면 세그폴트를 일으킬 수 없습니다. 즉, 신뢰해야 하는 코드의 줄 수가 더 적습니다. 편집증적으로 신중한 사람을 위해 덧붙이자면, 샌드박싱을 적용하여 변환한 PyPy에는 보통 디버깅 버전에만 들어 있는 체계적인 런타임 검사(예를 들어 버퍼 오버플로에 대한 검사)도 포함되어 있습니다.
Warning
PyPy 쪽에서 해야 할 어려운 작업은 이미 끝났습니다 — 완전히 안전한 버전을 얻을 수 있습니다. 아직 실험 단계이고 다듬어지지 않은 부분은 일반 Python 인터프리터(CPython 또는 샌드박스가 적용되지 않은 PyPy)에서 이 샌드박스 PyPy를 사용하기 위한 라이브러리뿐입니다. 기여를 환영합니다.
Warning
PyPy2에서 테스트했습니다. PyPy3에서는 별도 수정 없이 바로 동작하지 않을 수 있습니다.
개요¶
PyPy의 변환 기능 중 하나는 샌드박싱 기능입니다. 이는 “완전 가상화”라는 의미에서의 “샌드박싱”이지만, OS 지원을 전혀 받지 않고 일반 C로 구현됩니다. 이는 두 프로세스 모델입니다. PyPy를 특수한 “pypy-c-sandbox” 샌드박스 실행 파일로 변환할 수 있으며, 이 실행 파일은 라이브러리 호출이나 시스템 호출을 전혀 하지 않는다는 의미에서 안전합니다. 대신 그러한 연산을 수행하려 할 때마다 연산 이름과 인자를 마셜링하여 stdout으로 보내고, 마셜링된 결과가 stdin으로 들어오기를 기다립니다. 이 pypy-c-sandbox 샌드박스 프로세스는 이러한 연산 요청에 응답하는 외부 “컨트롤러” 프로그램이 실행하도록 설계되었습니다.
샌드박스 프로그램인 pypy-c-sandbox는 변환 과정에 변환 단계를 하나 추가하여 얻습니다. 이 단계는 RPython 수준의 모든 외부 함수 호출을 마샬링/대기/언마샬링을 수행하는 스텁으로 바꿉니다. 샌드박스를 탈출하려는 공격자는 stdout에 쓰고 stdin에서 읽는 것 외에는 외부 함수 호출이 전혀 없는 C 프로그램 안에 갇히게 됩니다. (이론적으로는 세그폴트와 비슷한 상황을 악용하는 등의 방법으로 여전히 공격할 수 있습니다. 하지만 서론에서 설명했듯이 저희는 PyPy가 그러한 공격에 대해 상당히 안전하다고 생각합니다.)
외부 컨트롤러는 CPython이나 일반 PyPy에서 실행할 수 있는 평범한 Python 프로그램입니다. 컨트롤러는 하위 프로세스에 자신의 세계를 원하는 대로 꾸며 보여 줌으로써 어떤 가상화든 원하는 대로 수행할 수 있습니다. 예를 들어, 하위 프로세스는 자신이 파일 핸들을 사용한다고 생각하지만, 실제로 그 숫자들은 컨트롤러 프로세스가 만들어 낸 것이므로 실제 OS 수준의 파일 핸들일 필요가 전혀 없습니다(그리고 아마 그래서도 안 됩니다). 제가 구현한 데모 컨트롤러에는 숫자를 파일 유사 객체에 대응시키는 매핑이 있을 뿐입니다. 컨트롤러는 요청된 경로를 가상의, 완전히 임의로 구성한 디렉터리 계층 안의 어떤 파일이나 파일 유사 객체로 변환하는 방식으로 “os_open” 연산에 응답합니다. 파일 유사 객체는 사용되지 않은 3 이상의 아무 숫자를 키로 하여 매핑에 들어가고, 그 숫자가 하위 프로세스에 반환됩니다. “os_read” 연산은 하위 프로세스가 넘겨준 의사(pseudo) 파일 핸들을 컨트롤러 안의 파일 유사 객체로 다시 매핑한 다음, 그 파일 유사 객체에서 읽는 방식으로 동작합니다.
샌드박싱을 활성화한 상태로 RPython 프로그램을 변환하면 인덱스 범위를 벗어난 접근을 검사하는 온갖 종류의 C 수준 어서션을 활성화하는 특수 플래그도 함께 사용됩니다.
그런데 이미 눈치채셨겠지만, 이 방식은 우리가 변환하는 대상이 PyPy라는 사실과는 전혀 무관합니다. 어떤 RPython 프로그램이든 상관없습니다. 저는 JS 인터프리터에서도 이를 성공적으로 시험해 보았습니다. 이 컨트롤러가 “pypy_interact”라고 불리는 이유는 단지 샌드박스 실행 파일인 pypy-c-sandbox가 요구하는 파일 계층을 흉내 내기 때문입니다. 이 컨트롤러에는 /bin/lib/pypy1.2/lib-python과 /bin/lib/pypy1.2/lib_pypy 같은 (읽기 전용) 가상 디렉터리가 들어 있으며, 실행 파일이 /bin/pypy-c인 것처럼 동작합니다.
사용 방법¶
pypy repository의 사본을 받으십시오. pypy/goal 디렉터리에서 다음을 실행하십시오.:
../../rpython/bin/rpython -O2 --sandbox targetpypystandalone.py
일반 PyPy가 설치되어 있지 않다면 설치하는 것이 좋습니다. 변환 속도가 더 빠르기 때문입니다. 하지만 같은 명령줄 앞에 python을 붙여 실행할 수도 있습니다.
실행하려면 pypy/sandbox 디렉터리에 있는 샌드박스 도구를 사용하십시오.:
./pypy_interact.py /some/path/pypy-c-sandbox [args...]
pypy-c와 마찬가지로 인자를 전달하지 않으면 대화형 프롬프트가 나타납니다. 이론상 이 프롬프트에서는 해로운 작업을 하거나 기기의 임의 파일을 읽는 것이 불가능합니다. 스크립트를 인자로 전달하려면 스크립트를 그 모든 의존성과 함께 하나의 디렉터리에 넣은 다음, --tmp=DIR 옵션을 사용하여 pypy_interact가 이 디렉터리를 하위 프로세스의 가상 /tmp 디렉터리로 (읽기 전용으로) 내보내도록 해야 합니다. 예제:
mkdir myexported
cp script.py myexported/
./pypy_interact.py --tmp=myexported /some/path/pypy-c-sandbox /tmp/script.py
이 작업은 script.py가 신뢰할 수 없는 임의의 출처에서 온 경우에도, 예를 들어 HTTP 서버가 이 작업을 수행하는 경우에도 안전하게 할 수 있습니다.
사용하는 힙 크기를 제한하려면 pypy_interact.py에 --heapsize=N 옵션을 지정하십시오. --timeout=N 옵션을 사용하면 CPU 시간(실제 시간)도 제한할 수 있습니다.
모든 연산이 지원되는 것은 아닙니다. 예를 들어 os.readlink(‘…’)를 입력하면 컨트롤러가 예외를 내며 비정상 종료되고 하위 프로세스도 종료됩니다. 다른 연산은 “Fatal RPython error”를 내며 하위 프로세스를 곧바로 종료시킵니다. 이 중 어느 것도 보안 허점이 아닙니다. 더 중요한 점은, 대부분의 다른 내장 모듈이 활성화되어 있지 않다는 것입니다. 이에 대해 불만을 제기하기 전에 이 페이지의 모든 경고를 읽어 주십시오. 기여를 환영합니다.