수행했던 프로젝트에서 K8s 환경에서 위협 행위에 대한 탐지를 위해 eBPF 기반 탐지도구인 Tetragon을 사용했다. 당시 프로젝트 수행 기간, 수행 요소 등을 종합적으로 고려 했을 때 여유롭게 스터디를 할 시간이 확보되지 않았다는 점과 Tetragon 탐지 고도화를 진행하면서 남았던 개인적인 아쉬움과 eBPF 기술에 대한 흥미를 위해 eBPF에 대해서 조금 더 깊이 공부 해보고자 포스트를 작성한다.
포스트는 eBPF 공식 문서를 기반으로 작성할 예정이다.
eBPF가 무엇인지부터 시작해서 어떤 방식으로 동작하는지에 대해 심층적으로 공부하고, Tetragon 정책 고도화까지 이어 가보려고 한다. 이번 포스트에선 eBPF가 무엇인지부터 알아보도록 하자!
KERNEL 프로젝트
https://github.com/Choseongyul/KERNEL
프로젝트에 대해 궁금하다면 해당 레포지터리 내용을 확인 해주세요!

공식 문서에서 설명하는 eBPF는 다음과 같다.
"eBPF는 운영 체제 커널과 같은 특별한 권한이 있는 환경에서 샌드박스 프로그램을 실행시킬 수 있는 리눅스 커널의 기술에서 기원한 혁신적인 기술입니다. 이는 커널 소스 코드를 바꾸거나 커널 모듈을 로드하지 않고도 기존 커널의 기능을 안전하고 효율적으로 확장시키는 것에 사용됩니다."
쉽게 말해서 eBPF는 커널 조작 없이, 사용자가 정의한 프로그램을 커널 레벨에서 안전하게 실행시킬 수 있는 기술을 의미한다.

제한적 권한으로 Kernel Level 위에서 동작하는 User Level 같은 경우 우리가 일반적으로 컴퓨터에 앱을 설치 및 운용하는 것처럼 자유도를 갖는다. 하지만 운영체제에서 커널은 컴퓨터라는 시스템에서 최고 권한을 가지며, 편의성보단 안정성과 보안을 더 우선시 해야하기 때문에 발전 속도가 상대적으로 더딜 수 밖에 없다.
eBPF는 샌드박스 프로그램을 커널 내부에서 실행할 수 있게 함으로써 문제를 해결하고 발전시켰다. 우리가 어떤 프로그램을 설계하고 실험할 때 가상머신을 통해 안전하게 격리된 상태에서 수행하는 것처럼 이러한 작업이 커널 레벨에서도 가능해졌다는 뜻이다.
그리고 커널 레벨에서 eBPF 프로그램을 이후 설명 할 JIT, Verifier Engine를 통해 선제적으로 검사함으로써 추가된 프로그램들이 안전하고 효율적으로 동작할 수 있도록 보장한다.

eBPF 프로그램은 상시로 동작하는 것이 아니라 Event Dirven 방식으로 동작한다. 이때 Hook은 eBPF 프로그램이 동작하는 지점을 의미한다. 즉, eBPF 프로그램은 Hook이라는 지점에 부착돼서 프로그램에 정의한 특정 이벤트가 발생하면 동작한다.

Hook은 System Call, Kernel 함수, 네트워크 이벤트 등 자유롭게 거의 모든 공간이 될 수 있다. 또한, 목적에 맞는 Hook이 없다면 kprobe(커널 프로브), uprobe(유저 프로브)를 정의하여 User, Kernel 레벨에 관계없이 자유롭게 부착 가능하다.
일반적으로 eBPF 프로그램은 직접 작성하지 않고, Cilium, bcc 등 eBPF 추상화를 지원하는 프로젝트를 사용하여 간접적으로 사용한다. 내가 진행한 프로젝트에서도 Cilium을 이용했다.
만약 앞서 언급한 eBPF 추상화 도구를 사용할 수 없다면 eBPF 프로그램은 직접 작성 해야한다.

특정 Hook이 확인되면 bpf 시스템 콜을 통해서 커널 내부로 로드된다. 이때 설정한 Hook point에 바로 부착되는 것이 아닌 Verifier로 검증 단계를 거치고, JIT Compiler를 통해 기계어로 컴파일 되어 부착된다.

설정한 Hook Point에 부착되기 전 권한, 정상 동작 여부, 종료 여부를 검사하게 된다. 이후 JIT(Just In Time) Complier를 거쳐 기계어로 변환되어 커널레벨에서 실행 가능한 명령어 집합으로 최종적으로 eBPF 프로그램으로써 동작하게 된다.

이렇게 부착된 eBPF 프로그램을 통해 수집된 정보를 공유 및 저장할 수 있는 공간이 필요한데 이를 eBPF Map이 수행한다. 이를 이용하여 필요한 정보를 가지고 올 수 있다. 서비스로 따지면 DB라고 생각하면 된다.
또한 eBPF 프로그램뿐만 아니라 User Level에서 어플리케이션에서도 Map에 저장된 정보에 접근할 수 있다. eBPF Map은 Hash table, 배열, LRU, ring buffer 등 다양한 자료구조로 구현될 수 있고, 단일 CPU 코어뿐만 아니라 전체에 대해서도 다양한 유형의 eBPF Map을 사용할 수 있다.

앞서 eBPF Program을 이용해 커널 내에서 다양한 이벤트를 관측한다고 했다. 이는 Kernel 함수의 값을 이용해서 이루어지는데, 만약 eBPF Program이 커널 함수를 직접 호출한다면 커널 버전 변화에 따라 함수 호출 방식이 달라져 특정 커널 버전에 종속될 수 있다. 커널 버전에 따라 함수 이름이 바뀌거나, 함수 호출시 필요한 파라미터 변화 등이 있을 수 있기 때문이다. 따라서 Kernel에서 제공하는 Helper 함수를 API로 활용해서 Kernel 함수를 호출한다.

Tail Call은 현재 실행중인 eBPF Program 외 다른 eBPF Program을 실행하거나 현재 실행 컨텍스트를 변경할 수 있도록 하는 기능으로 execve()와 동작 방식이 유사하다.
앞서 언급한 커널 레벨에서 안정성을 eBPF도 마찬가지로 최우선적으로 고려하여 설계되었다. 안정성을 위해 eBPF는 다음 단계를 따른다.
특별한 권한 요구
리눅스 커널에 eBPF 프로그램을 로드하려는 모든 프로세스는 특별한 권한을 가진 모드(root)에서 실행, CAP_BPF 권한을 가져야 한다.
검증
앞서 언급한 Verifier를 통해 정상 동작 여부, 정상 종료 여부, 복잡성, 메모리 영역 접근 등을 검사한다.
Hardening
eBPF 프로그램은 해당 eBPF 프로그램이 특별한 권한이 있는 프로세스 또는 그렇지 않은 프로세스에서 로드 되었는지를 확인한다. 먼저 eBPF 내 커널 메모리는 Read Only로 생성되어 조작이 확인될 시 크래시한다. 그 다음 메모리 마스킹을 통해 제한된 영역에 접근하도록 하고, 코드에 존재하는 상수를 모두 가려 JIT 스프레이와 같은 eBPF 프로그램의 메모리 영역에 침입하여 상수로 위장한 채로 침투한 악성 코드를 실행과 같은 공격에 대응한다.
추상화 된 런타임 컨텍스트
eBPF Helper 함수를 이용하여 커널 메모리에 접근할 수 있도록 하여, 일관성 있는 데이터 접근과 해당 eBPF 프로그램이 접근 가능한 데이터만 접근할 수 있도록 보장한다.
이번 포스트는 eBPF에 대한 Overview 느낌으로 각 구성 요소에 대해 가볍게 알아봤다. 다음 포스트에선 eBPF Map을 시작으로 각 구성 요소에 대해 좀 더 심층적인 내용을 다뤄보도록 하겠다.