- Google은 eBPF verifier의 CVE-2023-2163을 Buzzer로 발견했고, 이 취약점이 로컬 권한 상승과 컨테이너 탈출에 악용될 수 있음을 확인함
- eBPF는 커널 기능을 런타임에 확장하지만 높은 권한에서 임의 바이트코드를 실행하므로, 로드 전 verifier 검증이 보안의 핵심 축이 됨
- 문제는 verifier의 path pruning이 실제로 다른 실행 경로를 이미 안전한 경로와 동등하다고 잘못 판단하면서 발생함
- 익스플로잇은 verifier가 0으로 본 레지스터가 런타임에는 다른 값을 갖는 차이를 이용해 eBPF 스택 포인터를 오염시키고, 임의 읽기/쓰기와 KASLR 우회로 이어짐
- 패치는 precise register에 영향을 주는 imprecise register까지 precise로 표시하는 방식이며, 같은 포인터 산술 퍼징 전략에서는 이후 추가 이슈가 발견되지 않음
eBPF verifier가 넓히는 커널 공격 표면
- eBPF는 복잡한 커널 모듈 없이도 Linux 커널 기능을 런타임에 확장할 수 있게 하는 기술임
- eBPF 프로그램은 커스텀 바이트코드로 작성되며, 특정 이벤트가 발생했을 때 실행되기 전에 안전성 검증을 거침
- 대표 예시는 특정 syscall 호출 시 실행되는 eBPF 프로그램임
- 높은 권한 수준에서 임의 코드를 실행할 수 있는 구조라서 커널의 공격 표면이 크게 늘어남
- 프로그램은 로드 전에 verifier를 통과해야 하며, verifier는 eBPF의 보안 가정이 충족되는지 검사함
- verifier 취약점이 악용되면 보통 로컬 권한 상승 또는 컨테이너 환경의 컨테이너 탈출로 이어짐
Buzzer와 포인터 산술 퍼징
- Google은 eBPF verifier 코드를 자동으로 감사하기 위해 Buzzer를 만들었음
- Buzzer는 구문적으로 유효한 eBPF 프로그램을 대량 생성하는 퍼저이며, 논리 버그를 유발하기 위한 전략을 설정할 수 있음
- CVE-2023-2163 발견에는 포인터 산술 전략이 사용됨
- 레지스터를 임의 값으로 초기화하는 헤더를 생성함
- 임의의 산술 및 점프 명령어 시퀀스를 생성함
- 임의 레지스터를 선택해 eBPF map element 포인터와 더하기 연산을 수행함
- 해당 element에 매직 값을 기록함
- 사용자 공간에서 기록된 값이 관찰되지 않으면 경계 밖 쓰기가 발생했을 가능성이 있음
- Buzzer가 발견한 CVE-2023-2163은 eBPF path pruning 로직에 있었고, 컨테이너 탈출과 로컬 권한 상승에 모두 사용할 수 있는 익스플로잇으로 이어짐
path pruning과 precise tracking 버그
- eBPF verifier는 프로그램이 안전하게 실행될 수 있는지 확인하기 위해 가능한 실행 경로를 시뮬레이션함
- 조건 분기에서 레지스터 값이 확실하지 않으면 verifier는 가능한 모든 상태의 실행을 따라가려 함
- 조건 점프가 많아질수록 실행 경로 수가 지수적으로 늘어나며, 커널에 프로그램을 로드하는 성능에도 악영향을 줌
- 이를 줄이기 위해 eBPF 개발자들은 path pruning을 도입함
- verifier가 특정 상태와 동등한 상태가 이미 안전하게 exit 명령에 도달한다고 보장할 수 있으면, 해당 경로를 더 탐색하지 않음
- 더 효율적인 pruning을 위해 precise tracking 개념이 함께 쓰임
- 레지스터가 포인터 산술 연산에 관여하거나 helper 함수에 상수로 전달되면 precise로 표시됨
- verifier는 해당 레지스터가 관련된 모든 상태를 탐색해야 함
- CVE-2023-2163에서는 r6의 정밀도에 영향을 주는 r9가 제대로 표시되지 않았음
- verifier는 r9가 r6의 preciseness에 기여하지 않는다고 가정함
- 이전 경로에서 r6로 안전하게 exit에 도달할 수 있다고 판단한 뒤, 나머지 상태를 동등하다고 보고 pruning함
- 런타임에는 1:2:4:6 경로가 실행됐고, 6에서 verifier가 0이라고 본 r6이 실제로는 다른 값을 가진 채 포인터 산술 연산에 사용될 수 있었음
임의 읽기/쓰기와 KASLR 우회
- 익스플로잇 코드는 Google security research repository에 공개됨
- 익스플로잇 개발에는 @chompie와 @_manfp의 작업이 중요하게 활용됨
- 전체 흐름은 먼저 임의 읽기/쓰기를 얻고, 이후 프로세스 credentials를 찾아 uid와
fs_struct포인터를 패치해 권한을 상승시키는 방식임 - 첫 단계에서는 오염된 레지스터 값을 1로 만듦
- 수동 분석에서 런타임 r6 값은
0x400이었고 verifier는 이를 0으로 봄 r6 >>= 10명령으로 원하는 값 1을 만들었음
- 수동 분석에서 런타임 r6 값은
- 이후
bpf_skb_load_bytes_relativehelper 함수로 eBPF 스택의 포인터를 오염시킴- verifier는
len이 8이라고 판단하지만, 실제 런타임에서는 r6 값 때문에len이 9가 됨 - 결과적으로 8바이트가 아니라 9바이트를 써서 스택 offset
-32값의 첫 바이트를 오염시킴
- verifier는
- 오염된 스택 포인터를 조작하면 eBPF map pointer를 누출할 수 있음
- 사용자 공간에서 R2와 R3 값을 읽고, R2가
0xBACA가 되는지 확인함 - 이 조건이 맞으면 R3가 map pointer 누출값이 됨
- eBPF map pointer 누출은 KASLR을 우회한 상태로 이어짐
- 사용자 공간에서 R2와 R3 값을 읽고, R2가
- 같은 전략으로 단일 바이트가 아니라 연속된 스택 영역 전체를 원하는 포인터로 덮어쓰면 임의 읽기/쓰기가 가능함
- verifier 관점에서는 BPF 스택 조작이지만, 실제로는 커널 메모리를 읽고 쓸 수 있음
map leak에서 root shell까지
- map pointer 누출 이후 익스플로잇은 Chompie의 익스플로잇과 크게 다르지 않으며, 일부 코드를 차용함
- 대략적인 절차는 다음과 같음
- kstrtab에서
init_pid_ns문자열을 반복적으로 검색함 - 해당 문자열을 참조하는 ksymtab 심볼을 찾아 init_pid_ns 구조체 주소를 얻음
- radix tree를 순회해
comm필드가 실행 중인 익스플로잇 실행 파일 이름과 일치하는 entry를 찾음- 컨테이너 안에서 실행 중이면 PID가 신뢰할 수 있는 휴리스틱이 아니므로 PID만 사용하지 않음
- uid를 0으로 패치하고
fs_struct포인터를 패치함 fs_struct를 PID 1이 참조하는 포인터와 같은 값으로 패치하면, 컨테이너 안에서 실행 중일 때 호스트 파일 시스템을 관찰할 수 있음system("/bin/bash")를 실행해 root shell을 얻음
- kstrtab에서
- GitHub에 공개된 코드는 특정 Linux 버전에서만 컨테이너 탈출로 동작함
- Ubuntu와 일부 배포판에서는 덮어쓴 데이터 구조의 offset이 달라 로컬 권한 상승으로만 동작함
- 어떤 Linux 배포판에서도 동작하게 하려면 코드 조정이 필요함
패치와 후속 검증
- CVE-2023-2163의 원인 분석과 패치는 kernel mailing list에서 확인할 수 있음
- 수정은 precise register에 영향을 준 연산의 imprecise register를 precise로 표시하는 방식임
- 이 수정이 eBPF verifier 성능에 영향을 주는지는 명확하지 않음
- 같은 포인터 산술 퍼징 전략을 계속 실행했지만 추가 이슈는 발견되지 않았음
- Buzzer는 계속 개발 중이며, 오픈소스 커뮤니티의 기여를 GitHub repo를 통해 받고 있음