| 항목 | 내용 |
|---|---|
| CVE ID | CVE-2019-5736 |
| 영향 버전 | runc 1.0-rc6 이하 (Docker, containerd, CRI-O 등 runc를 쓰는 모든 컨테이너 런타임) |
| CVSS v3.1 | 8.6 (High) 전후 |
| 공개일 | 2019-02-11 |
| 발견자 | Adam Iwaniuk, Borys Popławski (Dragon Sector) |
runc 는 Docker/containerd/CRI-O 등 대부분의 컨테이너 런타임이 실제로 컨테이너 프로세스를
띄우는 데 쓰는 저수준 실행기다. CVE-2019-5736은 컨테이너 안에서 악의적인 프로그램을
실행할 수 있는 사용자가, 호스트에 설치된 runc 바이너리 자체를 덮어써서 컨테이너
탈출(container escape) 후 호스트에서 root로 임의 코드 실행까지 이어질 수 있는
취약점이다. 컨테이너 격리의 핵심 전제("컨테이너 안의 코드는 호스트에 영향을 못 준다")를
정면으로 깬 사례로 잘 알려져 있다.
리눅스에서 모든 프로세스는 /proc/self/exe 라는 심볼릭 링크를 갖는다. 이 링크는 그
프로세스를 실행시킨 실제 바이너리 파일을 가리킨다. 컨테이너 안에서 runc 가 컨테이너
프로세스에 합류(nsenter)할 때, runc 자신의 프로세스가 실행한 바이너리(즉 호스트의
runc 실행 파일)를 가리키는 /proc/self/exe 가 컨테이너의 마운트 네임스페이스 안에서도
그대로 보인다. 컨테이너는 격리된 파일시스템을 갖지만, 이 특정 경로만큼은 호스트의 실제
runc 바이너리를 향한 창구로 남아있는 셈이다.
리눅스는 원래 "지금 실행 중인 바이너리 파일"에는 쓰기를 허용하지 않는다(ETXTBSY, "text file busy"). 하지만 여기에는 틈이 있다: 파일을 경로가 아니라 파일 디스크립터(fd)를 통해 열면, 프로세스가 종료되기 직전의 짧은 순간에 쓰기가 가능해지는 경우가 있다. 공격자는 이 타이밍을 노려 별도 프로세스가 반복적으로 쓰기를 시도하게 만드는 방식(busy-loop)으로 이 좁은 창을 파고든다.
ELF 실행 파일이나 공유 라이브러리는 main() 이 호출되기 전에 자동으로 실행되는
"생성자" 함수를 등록할 수 있다(.init_array 섹션). LD_PRELOAD 로 공유 라이브러리를
미리 로드시키면, 그 라이브러리의 생성자 함수가 대상 프로그램의 main() 보다 먼저,
그리고 대상 프로그램과 같은 권한으로 실행된다.
공격은 대략 다음과 같은 순서로 진행된다.
- 컨테이너 이미지 안에, 인터프리터로
#!/proc/self/exe를 지정한 실행 스크립트를 넣어 둔다 (또는 이와 동등한 효과를 내는 바이너리를 준비한다). - 관리자(호스트 쪽 운영자)가
docker exec등으로 이 컨테이너에 다시 진입하면, 호스트의runc프로세스가 컨테이너의 네임스페이스에 합류(nsenter)해 대상 프로그램을 실행한다. - 이때 실행되는 것이 컨테이너 안의
/bin/bash같은 게 아니라,#!/proc/self/exe를 해석한 결과 호스트의 실제runc바이너일을 가리키는 경로가 실행 대상이 된다. - 공격자는
LD_PRELOAD로 자신의 공유 라이브러리를 지정해,runc프로세스가 시작되는 순간 그 라이브러리의 생성자 함수가 (호스트의runc와 같은 권한으로) 먼저 실행되게 만든다. - 이 생성자 함수 안에서, 공격자는
/proc/self/exe를O_PATH로 연 뒤/proc/self/fd/<N>경로를 통해 다시O_WRONLY로 열어 호스트의runc바이너리 자체에 쓰기를 시도한다. 별도 프로세스가 이 쓰기를 반복 시도(busy-loop)하다가,runc프로세스가 막 종료되어 "text busy" 보호가 풀리는 짧은 순간을 잡아 실제로 덮어쓰는 데 성공한다.
호스트 컨테이너 (공격자 통제)
┌─────────────────┐
│ /usr/bin/runc │◀──────────────┐
│ (호스트 바이너리) │ │ /proc/self/exe 가
└─────────────────┘ │ 이 파일을 그대로 가리킴
▲ │
│ 덮어쓰기 시도 │
│ (O_PATH → /proc/self/fd/N │
│ → O_WRONLY, busy-loop) │
│ │
┌───────┴─────────────────────────┴──┐
│ runc nsenter 로 컨테이너에 합류 │
│ LD_PRELOAD 로 주입된 공유 라이브러리 │
│ 생성자 함수가 runc 와 같은 권한으로 │
│ 실행되며 위 쓰기를 시도 │
└──────────────────────────────────────┘
결과: 호스트의 runc 바이너리 자체가 공격자 코드로 교체됨
→ 이후 그 호스트에서 runc 가 호출될 때마다 공격자 코드가 root 권한으로 실행
이 취약점은 "컨테이너 탈출"의 교과서적인 사례다. 컨테이너 자체의 취약점이 아니라 컨테이너
런타임(runc)이 호스트와 상호작용하는 방식의 허점이며, 성공하면 해당 호스트에서 실행되는
모든 컨테이너에 영향을 줄 수 있는 영속적인 백도어(호스트 runc 자체가 오염됨)로 이어진다.
수정 커밋(0a8e4117e7f715d5fbeef398405813ce8e88558b, "nsenter: clone /proc/self/exe to
avoid exposing host binary to container")은 근본적인 발상의 전환을 택했다 — "쓰기를
막는다" 가 아니라, 애초에 컨테이너에서 보이는 게 진짜 호스트 바이너리가 아니게 만든다.
runc 가 nsenter 로 컨테이너에 합류하기 직전에, memfd_create(2) (또는 대체 수단인
O_TMPFILE)로 메모리에만 존재하는 익명 파일을 만들어 자기 자신(runc 바이너리)의
내용을 그 안에 복사한 뒤, 그 메모리상의 복사본을 실행한다.
호스트 컨테이너 (공격자 통제)
┌─────────────────┐
│ /usr/bin/runc │ (원본, 디스크에 그대로 존재)
└─────────────────┘
│
│ nsenter 진입 직전:
│ memfd_create() 로 메모리 전용 파일 생성 후 자기 자신을 복사
▼
┌─────────────────────┐
│ memfd 익명 파일 │◀──────────────┐
│ (runc 의 복사본, │ │ /proc/self/exe 가
│ 디스크에 없음) │ │ 이제 이 복사본을 가리킴
└─────────────────────┘ │
▲ │
│ 공격자가 덮어쓰려 해도 │
│ 디스크의 원본 runc 는 │
│ 애초에 이 경로에 없음 │
│ │
┌───────┴─────────────────────────────┴──┐
│ 공격자의 LD_PRELOAD 생성자가 쓰기를 시도해도 │
│ 건드리는 건 매번 새로 만들어진 메모리 사본뿐 │
└─────────────────────────────────────────┘
결과: 공격에 성공해도 그 실행 한 번의 메모리 사본만 오염될 뿐,
디스크의 실제 runc 바이너리는 전혀 손상되지 않음
즉 "덮어쓰기를 막는" 게 아니라 "덮어써도 상관없는, 매번 새로 만들어지고 버려지는 사본만 노출시킨다"는 방식으로 공격의 목적(영속적인 호스트 바이너리 오염) 자체를 무력화했다. 후속 커밋들(예: environ 파싱 제거)은 같은 문제의 관련 공격 표면을 추가로 줄였다.
- Anatomy of CVE-2019-5736: A runc container escape! — AWS Compute Blog
- Breaking out of Docker via runC — Unit 42 (Palo Alto Networks)
- CVE-2019-5736: Escape from Docker and Kubernetes containers to root on host — Dragon Sector (발견자 원문)
- 수정 커밋 0a8e4117e7f715d5fbeef398405813ce8e88558b — opencontainers/runc
- CVE-2019-5736 — NVD
- "text busy 보호가 풀리는 짧은 타이밍을 busy-loop 으로 잡는" 정확한 구현(어떤 시스템 콜을 어떤 순서로 반복하는지)은 이번에는 2차 자료(AWS/Unit42 블로그) 요약 수준까지만 확인했고, 공개 PoC 코드 자체를 직접 열어 대조하지는 않았다(정적 분석 범위 유지).