CVE-2026-31431 분석: Copy Fail 취약점과 루트리스 컨테이너
2026년 5월에 공개된 리눅스 커널 취약점(Copy Fail)이 루트리스(Rootless) 컨테이너 환경에서 어떻게 권한 상승 공격을 차단하는지 분석한 글입니다. 공격자의 셸코드(Shellcode)를 역분석하여 악의적인 ELF 파일이 시스템 권한을 탈취하는 과정과, 이를 eBPF와 UID 매핑을 통해 실시간으로 추적 및 방어하는 원리를 다룹니다. 보안 실무자들에게 컨테이너 아키텍처의 중요성과 취약점 대응 메커니즘을 보여주는 중요한 자료입니다.
CVE-2026-31431: Copy Fail vs. rootless containers 2026년 5월 4일
목차 소개 취약점 개요 셸코드(Shellcode) 분석 테스트 랩 구축 루트리스 Podman 설정 컨테이너 내부에서 익스플로잇(Exploit) 실행 익스플로잇 메커니즘 추적 루트리스 컨테이너가 권한 상승을 막아낸 이유 eBPF를 이용해 커널 동작 실시간 포착 uid_map 증명 결론
소개 이전에 게시된 SELinux MCS 및 GitLab Runner에 관한 글에서 저는 작업(Job)별 가상 머신(VM) 격리의 동기 부여 사례로 CVE-2026-31431("Copy Fail")을 잠시 언급한 바 있습니다. 그 글이 공개된 후 저는 주말을 이용해 실제로 익스플로잇을 실행해 볼 수 있는 테스트 랩을 구축하고, 시스콜(syscall) 수준에서 추적한 뒤, GNOME의 Runner에 배포한 루트리스(Rootless) Podman 아키텍처가 이 공격을 성공적으로 차단할 수 있는지 확인했습니다. 본 글은 셸코드를 역어셈블하는 과정부터 커널이 실시간으로 권한 상승을 거부하는 것을 관찰하는 전체 과정을 문서화합니다.
취약점 개요 근본적인 원인, scatterlist 메커니즘 및 공개 일정에 대한 전체 기술적 분석은 Theori의 훌륭한 분석 글(xint.io/blog/copy-fail-linux-distributions)을 참조하십시오. 이 블로그 글에서는 우선 공개된 익스플로잇에 포함된 셸코드를 분석한 다음, 루트리스 컨테이너 내부에서 이를 실행할 수 있는 랩을 구축하고 커널 수준에서 어떤 일이 발생하는지 추적할 것입니다.
셸코드 분석 취약점이 공개된 후 며칠 동안 많은 사람들이 셸코드가 실제로 무엇을 하는지 확인하지도 않고 자신의 시스템에서 익스플로잇을 실행하는 것을 보았습니다. 한 번도 코드를 검토하지 않은 GitHub 저장소의 압축된 바이너리를 실행하는 것은 결코 좋은 보안 관행이 아닙니다. 어떤 경우에는 권한 상승 과정에서 데이터를 유출하거나 백도어를 심어놓을 수도 있기 때문입니다. 그러므로 실행하기 전에 실제 셸코드에 무엇이 포함되어 있는지 살펴보겠습니다.
셸코드는 Python 익스플로잇 내에 압축되고 16진수(Hex)로 인코딩된 문자열로 포함되어 있습니다: 78daab77f57163626464800126063b0610af82c101cc7760c0040e0c160c301d209a154d16999e07e5c1680601086578c0f0ff864c7e568f5e5b7e10f75b9675c44c7e56c3ff593611fcacfa499979fac5190c0c0c0032c310d3
스크립트는 zlib.decompress()를 사용하여 이를 원시 바이트(Raw bytes)로 변환합니다. 페이로드를 추출하고 검사하기 위한 코드는 다음과 같습니다:
#!/usr/bin/env python3 import zlib
hex_str = "78daab77... (중략) ...10d3" compressed_bytes = bytes.fromhex(hex_str) raw_payload = zlib.decompress(compressed_bytes)
with open("shellcode.bin", "wb") as f: f.write(raw_payload)
print(f"Payload extracted: {len(raw_payload)} bytes")
추출된 바이너리에 file 명령어를 실행하면 예상대로 다음과 같은 결과를 확인할 수 있습니다: shellcode.bin: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked...
이것은 날것의(raw) 셸코드가 아니라 완전히 구성된 ELF 실행 파일입니다. 익스플로잇은 /usr/bin/su의 시작 부분을 이 작은 바이너리로 덮어씁니다. 운영체제가 su를 실행할 때 손상된 페이지를 페이지 캐시에서 로드하여 합법적인 유틸리티 대신 악의적인 ELF를 실행하게 됩니다.
표준 objdump -d shellcode.bin 명령은 익스플로잇 작성자가 ELF golfing(페이로드를 수십 바이트로 압축하기 위해 섹션 헤더를 제거하는 기술)을 사용했기 때문에 아무런 출력도 생성하지 않습니다. .text 섹션이 없으면 objdump는 동작을 중단합니다. 강제로 날것의 역어셈블을 수행하려면 다음과 같이 실행합니다: objdump -D -b binary -m i386:x86-64 shellcode.bin
처음 약 0x77바이트는 objdump가 어셈블리로 해석하려고 시도하는 ELF 헤더 데이터이며, 이는 의미 없는 add %al,(%rax) 명령어를 생성합니다. 실제 코드는 오프셋 0x78에서 시작합니다. 주석이 포함된 전체 역어셈블리는 다음과 같습니다:
setuid(0) 시스콜 (오프셋 0x78 ~ 0x7e): 78: 31 c0 xor %eax,%eax 79: 31 ff xor %edi,%edi 7c: b0 69 mov $0x69,%al 7e: 0f 05 syscall
xor %edi, %edi는 rdi를 0으로 설정합니다. 이는 시스콜의 첫 번째 인자입니다. mov $0x69, %al은 10진수로 105를 로드하며, 이는 리눅스 x64에서 setuid에 해당하는 시스콜 번호입니다. 마지막으로 syscall 명령어가 이를 실행합니다.