서문
이 글은 Linux 루트킷에 대한 2부로 구성된 시리즈 중 1부입니다. 이 첫 번째 편에서는 루트킷의 분류, 진화, 커널을 파괴하는 데 사용하는 후킹 기법 등 루트킷의 작동 원리에 대한 이론에 초점을 맞춥니다. 2부에서는 방어적인 측면으로 전환하여 프로덕션 환경에서 이러한 위협을 식별하고 대응하는 실질적인 접근 방식을 다루는 탐지 엔지니어링에 대해 자세히 살펴봅니다.
루트킷이란 무엇인가요?
루트킷은 파일, 프로세스, 네트워크 연결, 커널 모듈 또는 계정과 같은 악성 활동을 숨기도록 설계된 은밀한 멀웨어입니다. 공격자의 주요 목적은 지속성과 회피로, 공격자는 서버, 인프라, 기업 시스템과 같은 고가치 표적에 대한 장기적인 액세스를 유지할 수 있습니다. 다른 형태의 멀웨어와 달리 루트킷은 즉시 목표를 달성하기보다는 탐지되지 않는 데 중점을 둡니다.
루트킷은 어떻게 작동하나요?
루트킷은 운영 체제를 조작하여 사용자와 보안 도구에 정보를 표시하는 방식을 변경합니다. 사용자 공간 또는 커널 내에서 작동합니다. 사용자 공간 루트킷은 LD_PRELOAD 또는 라이브러리 하이재킹과 같은 기술을 사용하여 사용자 수준 프로세스를 수정합니다. 커널 공간 루트킷은 가장 높은 권한으로 실행되어 커널 구조를 수정하고, 시스템 호출을 가로채거나, 악성 모듈을 로드합니다. 이러한 심층 통합은 강력한 회피 기능을 제공하지만 운영 위험을 증가시킵니다.
루트킷을 탐지하기 어려운 이유는 무엇인가요?
커널 공간 루트킷은 핵심 OS 기능을 조작하여 보안 도구를 무력화하고 사용자 영역의 가시성에서 아티팩트를 가릴 수 있습니다. 새로운 프로세스나 파일과 같은 명백한 지표를 피하면서 시스템에 최소한의 흔적만 남기는 경우가 많기 때문에 기존의 탐지가 어렵습니다. 루트킷을 식별하려면 메모리 포렌식, 커널 무결성 검사 또는 OS 수준 이하의 원격 분석이 필요한 경우가 많습니다.
루트킷이 공격자에게 양날의 검인 이유
루트킷은 은밀성과 제어 기능을 제공하지만 운영상의 위험을 수반합니다. 커널 루트킷은 커널 버전과 환경에 맞게 정밀하게 조정되어야 합니다. 메모리를 잘못 처리하거나 시스템 호출을 잘못 연결하는 등의 실수로 인해 시스템 충돌(커널 패닉)이 발생하여 공격자가 즉시 노출될 수 있습니다. 최소한 이러한 실패는 시스템에 원치 않는 관심을 끌기 때문에 공격자가 발판을 유지하기 위해 적극적으로 피하려고 하는 시나리오입니다.
커널 업데이트는 API, 메모리 구조 또는 시스템 호출을 변경하면 루트킷 기능이 손상되어 지속성이 취약해질 수 있다는 문제도 있습니다. 의심스러운 모듈이나 후크를 탐지하면 일반적으로 심층 포렌식 조사가 시작되는데, 이는 루트킷이 고도로 숙련된 표적 공격을 강력하게 나타내기 때문입니다. 공격자에게 루트킷은 고위험, 고보상 도구이지만 방어자에게는 이러한 취약성이 낮은 수준의 모니터링을 통해 탐지할 수 있는 기회를 제공합니다.
윈도우와 리눅스 루트킷
Windows 루트킷 에코시스템
윈도우는 루트킷 개발의 주요 초점입니다. 공격자는 커널 후크, 드라이버, 문서화되지 않은 시스템 호출을 악용하여 멀웨어를 숨기고 자격 증명을 도용하며 지속성을 확보합니다. 성숙한 연구 커뮤니티와 기업 환경에서의 광범위한 사용으로 DKOM, PatchGuard 우회, 부트킷과 같은 기술을 포함한 지속적인 혁신이 이루어지고 있습니다.
강력한 보안 도구와 Microsoft의 보안 강화 노력으로 공격자들은 점점 더 정교한 방법으로 공격하고 있습니다. Windows는 엔터프라이즈 엔드포인트와 소비자 디바이스에서의 지배력으로 인해 여전히 매력적입니다.
Linux 루트킷 생태계
Linux 루트킷은 역사적으로 주목을 덜 받아왔습니다. 배포판과 커널 버전이 파편화되면 탐지 및 개발이 복잡해집니다. 학술적 연구는 존재하지만 많은 도구가 구식이며 프로덕션 Linux 환경에는 전문 모니터링이 부족한 경우가 많습니다.
그러나 클라우드, 컨테이너, IoT, 고성능 컴퓨팅에서 Linux의 역할이 커지면서 점점 더 많은 관심을 받고 있습니다. 실제 Linux 루트킷은 클라우드 제공업체, 통신사 및 정부를 대상으로 한 공격에서 관찰되었습니다. 공격자가 직면한 주요 과제는 다음과 같습니다:
- 다양한 커널은 배포 간 호환성을 저해합니다.
- 가동 시간이 길어지면 커널 불일치 문제가 길어집니다.
- SELinux, AppArmor 및 모듈 서명과 같은 보안 기능은 난이도를 높입니다.
고유한 Linux 위협은 다음과 같습니다:
- 컨테이너 & Kubernetes: 컨테이너 이스케이프를 통한 새로운 지속성 벡터.
- IoT 디바이스: 최소한의 모니터링만 가능한 오래된 커널.
- 프로덕션 서버: 사용자 상호 작용이 없는 헤드리스 시스템으로 가시성이 떨어집니다.
Linux가 최신 인프라를 지배하는 상황에서 루트킷은 모니터링이 제대로 이루어지지 않고 있지만 점점 더 커지고 있는 위협입니다. Linux 전용 기술에 대한 탐지, 툴링 및 연구를 개선하는 것이 점점 더 시급해지고 있습니다.
Linux 루트킷 구현 모델의 진화
지난 20년 동안 Linux 루트킷은 기본적인 사용자 영역 기술에서 eBPF 및 io_uring 과 같은 최신 커널 인터페이스를 활용하는 고급 커널 상주 임플란트로 발전해 왔습니다. 이러한 진화의 각 단계는 공격자의 혁신과 방어자의 대응을 모두 반영하여 루트킷 설계를 더욱 은밀하고 유연하며 복원력을 높이는 방향으로 나아가고 있습니다.
이 섹션에서는 주요 특징, 역사적 맥락 및 실제 사례를 포함하여 그 진행 과정을 간략하게 설명합니다.
2000년대 초반: 공유 객체(SO) 유저랜드 루트킷
초기의 Linux 루트킷은 커널 수정 없이 사용자 공간에서 작동했으며, LD_PRELOAD 또는 셸 프로필 조작과 같은 기술을 사용하여 합법적인 바이너리에 악성 공유 개체를 주입했습니다. 이러한 루트킷은 opendir, readdir, fopen 과 같은 표준 libc 함수를 가로채서 ps, ls, netstat 과 같은 진단 도구의 출력을 조작할 수 있습니다 . 이 접근 방식은 배포하기 쉽지만 사용자 영역 후크에 의존하기 때문에 커널 수준 임플란트에 비해 은닉성과 범위가 제한적이며 간단한 재부팅이나 구성 재설정에 의해 쉽게 중단될 수 있습니다. 대표적인 예로는 파일과 연결을 숨기기 위해 libc 함수를 연결한 Jynx 루트킷(2009)과 공유 객체 주입과 커널 모드 기능(옵션)을 결합한 Azazel(2013)이 있습니다. 이러한 동적 링커 남용의 기본 기법은 2003년 Phrack 매거진 61호에서 자세히 설명한 바 있습니다.
2000년대 중반~2010년대: 로드 가능한 커널 모듈(LKM) 루트킷
방어자가 사용자 영역 조작을 탐지하는 데 능숙해지자 공격자는 로드 가능한 커널 모듈(LKM)을 통해 커널 공간으로 이동했습니다. LKM은 합법적인 확장 프로그램이지만, 악의적인 공격자는 이를 이용해 sys_call_table 을 후킹하거나 ftrace 을 조작하거나 내부 링크 목록을 변경하여 프로세스, 파일, 소켓, 심지어 루트킷 자체를 숨기는 등 전체 권한을 가지고 활동합니다. LKM은 심층적인 제어와 강력한 은폐 기능을 제공하지만, 보안이 강화된 환경에서는 상당한 감시를 받아야 합니다. 오염된 커널 상태, /proc/modules 의 목록 또는 특수 LKM 스캐너를 통해 탐지할 수 있으며 보안 부팅, 모듈 서명 및 Linux 보안 모듈(LSM)과 같은 최신 방어 수단으로 인해 점점 더 많이 방해를 받고 있습니다. 이 시기의 대표적인 예로는 자신을 숨길 수 있는 시스템 호출 후킹 LKM인 Adore-ng(2004+), 많은 배포판에서 여전히 작동하는 인기 후커인 Diamorphine(2016), 백도어 기능을 갖춘 최신 변종인 Reptile(2020) 등이 있습니다.
2010년대 후반: eBPF 기반 루트킷
공격자들은 점점 증가하는 LKM 기반 위협 탐지를 회피하기 위해 원래 안전한 패킷 필터링과 커널 추적을 위해 구축된 하위 시스템인 eBPF를 악용하기 시작했습니다. Linux 4.8 이상부터 eBPF는 syscall 후크, kprobes, 트레이스포인트 또는 Linux 보안 모듈 이벤트에 코드를 첨부할 수 있는 프로그래밍 가능한 인커널 가상 머신으로 발전했습니다. 이러한 임플란트는 커널 공간에서 실행되지만 기존 모듈 로딩을 피하므로 rkhunter 및 chkrootkit 과 같은 표준 LKM 스캐너와 보안 부팅 제한을 우회할 수 있습니다. /proc/modules 에 표시되지 않고 일반적인 모듈 감사 메커니즘에는 기본적으로 보이지 않으므로 배포하려면 CAP_BPF 또는 CAP_SYS_ADMIN (또는 드물게 권한이 없는 BPF 액세스 권한이 필요함)이 필요합니다. 이 시대는 eBPF 프로그램을 삽입하여 execve 과 같은 시스템 호출을 연결하는 개념 증명인 Triple Cross(2022)와 eBPF를 통해 은밀한 C2 채널을 구현하는 Boopkit(2022)과 같은 도구와 함께 이 주제를 탐구하는 수많은 Defcon 프레젠테이션에 의해 정의됩니다.
2025년대와 그 이후: io_uring 기반 루트킷(신규)
가장 최근의 진화는 Linux 5.1(2019)에 도입된 고성능 비동기 I/O 인터페이스인 io_uring 를 활용하여 프로세스가 공유 메모리 링을 통해 시스템 작업을 일괄 처리할 수 있도록 합니다. 성능을 위해 시스템 호출 오버헤드를 줄이도록 설계되었지만, 레드팀원들은 io_uring 을 악용하여 시스템 호출 기반 EDR을 회피하는 스텔스 유저랜드 에이전트 또는 커널 컨텍스트 루트킷을 만들 수 있음을 입증했습니다. 이러한 루트킷은 io_uring_enter 을 사용하여 파일, 네트워크 및 프로세스 작업을 일괄 처리함으로써 관찰 가능한 시스템 호출 이벤트를 훨씬 적게 생성하여 기존의 탐지 메커니즘을 무력화하고 LKM 및 eBPF의 제한을 피할 수 있습니다. 아직 실험 단계이긴 하지만 io_uring 을 사용하여 read, write, connect, unlink 과 같은 일반적인 시스템 호출을 은밀하게 대체하는 RingReaper (2025) 와 같은 사례와 ARMO 의 연구는 맞춤형 기기 없이 추적하기 어려운 향후 루트킷 개발에 매우 유망한 벡터임을 강조합니다.
Linux 루트킷 설계는 더 나은 방어에 대응하여 지속적으로 적응해 왔습니다. LKM 로딩이 더욱 어려워지고 시스템 호출 감사가 더욱 고도화됨에 따라 공격자들은 eBPF 및 io_uring 와 같은 대체 인터페이스로 눈을 돌리고 있습니다 . 이러한 진화에 따라 이제 더 이상 탐지에만 집중하는 것이 아니라 루트킷이 후킹 전략과 내부 아키텍처부터 시스템 핵심에 침투하는 데 사용하는 메커니즘을 이해하는 것이 중요합니다.
##루트킷 내부 및 후킹 기술
Linux 루트킷의 아키텍처를 이해하는 것은 탐지 및 방어를 위해 필수적입니다. 대부분의 루트킷은 두 가지 주요 구성 요소로 이루어진 모듈식 디자인을 따릅니다:
- 로더: 루트킷을 설치하거나 주입하며 지속성을 설정할 수 있습니다. 반드시 필요한 것은 아니지만, 루트킷을 배포하는 멀웨어 감염 체인에서 별도의 로더 구성 요소를 자주 볼 수 있습니다.
- 페이로드: 파일 숨기기, 시스템 호출 가로채기 또는 은밀한 통신과 같은 악의적인 작업을 수행합니다.
페이로드는 실행 흐름을 변경하고 은닉을 달성하기 위해 후킹 기술에 크게 의존합니다.
루트킷 로더 컴포넌트
로더는 루트킷을 메모리로 전송하고, 실행을 초기화하며, 많은 경우 지속성을 설정하거나 권한을 에스컬레이션하는 역할을 하는 구성 요소입니다. 익스플로잇, 피싱 또는 잘못된 구성을 통한 초기 액세스와 전체 루트킷 배포 사이의 간극을 메우는 역할을 합니다.
루트킷 모델에 따라 로더는 사용자 공간에서 완전히 작동하거나 표준 시스템 인터페이스를 통해 커널과 상호작용하거나 운영 체제 보호를 완전히 우회할 수 있습니다. 로더는 크게 멀웨어 기반 드로퍼, 유저랜드 루트킷 초기화 프로그램, 커스텀 커널 공간 로더의 세 가지 클래스로 분류할 수 있습니다. 또한 공격자가 insmod 과 같은 사용자 공간 도구를 통해 루트킷을 수동으로 로드할 수도 있습니다.
멀웨어 기반 드로퍼
멀웨어 드로퍼는 루트킷 페이로드를 다운로드하거나 압축을 풀고 실행하는 것이 유일한 목적인 경량 프로그램으로, 초기 액세스 후 배포되는 경우가 많습니다. 이러한 드로퍼는 일반적으로 사용자 공간에서 작동하지만 권한을 에스컬레이션하고 커널 수준 기능과 상호 작용합니다.
일반적인 기술은 다음과 같습니다:
- 모듈 주입: 악성
.ko파일을 디스크에 작성하고insmod또는modprobe을 호출하여 커널 모듈로 로드합니다. - 시스콜 래퍼:
init_module()또는finit_module()래퍼를 사용하여 시스콜을 통해 직접 LKM을 로드합니다. - 인메모리 주입:
ptrace또는memfd_create과 같은 인터페이스 활용 , 종종 디스크 아티팩트를 피할 수 있습니다. - BPF 기반 로딩:
bpftool,tc, 또는 직접bpf()와 같은 유틸리티를 사용하여 커널 트레이스포인트 또는 LSM 후크에 eBPF 프로그램을 로드하고 첨부합니다.
유저랜드 로더
공유 객체 루트킷의 경우 로더가 사용자 구성 또는 환경 설정을 수정하는 것으로 제한될 수 있습니다:
- 동적 링커 악용:
LD_PRELOAD=/path/to/rootkit.so을 설정하면 악성 공유 객체가 대상 바이너리가 실행될 때 libc 함수를 재정의할 수 있습니다. - 프로필 수정을 통한 지속성:
.bashrc,.profile또는/etc/profile과 같은 글로벌 파일에 사전 로드 구성을 삽입하면 세션 전반에서 계속 실행됩니다.
이러한 로더는 구현이 간단하지만 방어가 취약한 환경이나 다단계 감염 사슬의 일부로 여전히 효과적입니다.
커스텀 커널 로더
지능형 루트킷에는 표준 모듈 로딩 경로를 완전히 우회하도록 설계된 사용자 지정 커널 로더가 포함될 수 있습니다. 이러한 로더는 낮은 수준의 커널 인터페이스 또는 메모리 장치와 직접 상호 작용하여 루트킷을 메모리에 기록하므로 커널 감사 로그 또는 모듈 서명 검증을 회피하는 경우가 많습니다.
예를 들어, Reptile은 사용자 공간 바이너리를 로더로 포함하므로 insmod 또는 modprobe 을 호출하지 않고도 루트킷을 로드할 수 있지만, 모듈을 메모리에 로드할 때는 여전히 init_mod 시스콜에 의존합니다.
추가 로더 기능
멀웨어 로더는 단순한 초기화를 넘어 공격 체인의 다기능 구성 요소로 확장된 역할을 맡는 경우가 많습니다. 이러한 지능형 로더의 핵심 단계는 권한 상승으로, 이들은 주로 로컬 커널 취약점을 악용하여 기본 페이로드를 로드하기 전에 루트 액세스를 시도하며, 이는 "더티 파이프" 취약점(CVE-2022-0847)으로 대표되는 일반적인 수법입니다. 권한이 확보되면 로더는 트랙을 덮는 작업을 수행합니다. 여기에는 bash_history, 커널 로그, 감사 로그 또는 시스템의 기본 syslog 과 같은 중요한 파일에서 항목을 삭제하여 실행의 증거를 지우는 프로세스가 포함됩니다. 마지막으로 시스템 재시작 시 재실행을 보장하기 위해 로더는 systemd 단위, cron 작업, udev 규칙 또는 초기화 스크립트 수정과 같은 메커니즘을 설치하여 지속성을 보장합니다. 이러한 다기능 동작은 특히 복잡한 다단계 감염의 경우 단순한 "로더(" )와 본격적인 멀웨어의 구분을 모호하게 만드는 경우가 많습니다.
페이로드 구성 요소
페이로드는 스텔스, 제어, 지속성 등 핵심 기능을 제공합니다. 공격자가 사용할 수 있는 몇 가지 주요 방법이 있습니다. SO 루트킷이라고도 하는 사용자 공간 페이로드는 동적 링커를 통해 readdir 또는 fopen 같은 표준 C 라이브러리 함수를 탈취하여 작동합니다. 이를 통해 ls, netstat, ps 와 같은 일반적인 시스템 도구의 출력을 조작할 수 있습니다. 일반적으로 배포하기는 쉽지만 운영 범위가 제한되어 있습니다.
반면 커널 공간 페이로드는 전체 시스템 권한으로 작동합니다. /proc 에서 직접 파일과 프로세스를 숨기고, 네트워킹 스택을 조작하고, 커널 구조를 수정할 수 있습니다. 보다 현대적인 접근 방식은 시스템 호출 추적 지점 또는 Linux 보안 모듈(LSM) 후크에 첨부된 인커널 바이트코드를 활용하는 eBPF 기반 루트킷을 포함합니다. 이 키트는 트리 외부 모듈 없이도 스텔스를 제공하므로 보안 부팅 또는 모듈 서명 정책을 사용하는 환경에서 특히 효과적입니다. bpftool 같은 도구는 로딩을 단순화하여 탐지를 복잡하게 만듭니다. 마지막으로 io_uring기반 페이로드는 io_uring_enter (Linux 5.1 이상에서 사용 가능)을 통해 비동기 I/O 배칭을 악용하여 기존의 시스템 호출 모니터링을 우회합니다. 이를 통해 원격 측정 노출을 최소화하면서 파일, 네트워크, 프로세스를 은밀하게 운영할 수 있습니다.
Linux 루트킷 - 후킹 기법
이러한 필수 기반을 바탕으로 이제 대부분의 루트킷 기능의 핵심인 후킹에 대해 살펴봅니다. 후킹은 본질적으로 함수나 시스템 호출의 실행을 가로채고 변경하여 악의적인 활동을 숨기거나 새로운 동작을 삽입하는 것을 포함합니다. 루트킷은 정상적인 코드 흐름을 우회하여 파일과 프로세스를 숨기고, 보안 이벤트를 필터링하거나, 명백한 단서를 남기지 않고 시스템을 몰래 모니터링할 수 있습니다. 후킹은 사용자 영역과 커널 영역 모두에서 구현할 수 있으며, 공격자들은 수년에 걸쳐 기존 방법부터 최신 회피 기동까지 다양한 후킹 기법을 고안해냈습니다. 이 파트에서는 Linux 루트킷에서 사용하는 일반적인 후킹 기법에 대해 자세히 살펴보고, 각 기법을 예제와 실제 루트킷 샘플(예: Reptile, Diamorphine, PUMAKIT, 최근에는 FlipSwitch)을 통해 설명하여 작동 방식과 커널 진화에 따른 문제를 이해합니다.
후킹의 개념
높은 수준에서 후킹은 함수 또는 시스템 호출 호출을 가로채서 악성 코드로 리디렉션하는 행위입니다. 이를 통해 루트킷은 반환된 데이터 또는 동작을 수정하여 자신의 존재를 숨기거나 시스템 작동을 조작할 수 있습니다. 예를 들어, 루트킷은 디렉터리(getdents)에 있는 파일을 나열하는 시스템 호출을 연결하여 루트킷 자체 파일과 일치하는 파일 이름을 건너뛰게 하여 ls 과 같은 사용자 명령에 해당 파일을 "보이지 않게" 만들 수 있습니다.
후킹은 커널 내부에만 국한되지 않고 사용자 공간에서도 발생할 수 있습니다. 초기 Linux 루트킷은 악성 공유 개체를 프로세스에 주입하여 사용자 영역에서 완전히 작동했습니다. 동적 링커의 LD_PRELOAD 환경 변수를 사용하는 것과 같은 기술을 사용하면 루트킷이 사용자 프로그램의 표준 C 라이브러리 함수(예: getdents, readdir, fopen)를 재정의할 수 있습니다. 즉, 사용자가 ps 또는 netstat 과 같은 도구를 실행하면 루트킷에 삽입된 코드가 프로세스 또는 네트워크 연결을 나열하는 호출을 가로채 악성 코드를 필터링합니다. 이러한 유저랜드 훅은 커널 권한이 필요하지 않으며 구현이 비교적 간단합니다.
주목할 만한 예로는 수십 개의 libc 함수를 연결하여 프로세스, 파일, 네트워크 포트를 숨기고 백도어까지 활성화하는 사용자 모드 루트킷인 JynxKit(2012) 과 Azazel(2014)이 있습니다. 하지만 유저랜드 후킹은 탐지 및 제거가 쉽지 않고 커널 수준의 후크처럼 심층적인 제어 기능이 부족하다는 점에서 상당한 한계가 있습니다. 그 결과, 커널 후크는 낮은 수준에서 운영 체제와 보안 도구를 포괄적으로 속일 수 있기 때문에 복잡성과 위험성이 높음에도 불구하고 대부분의 최신 및 "야생" Linux 루트킷은 커널 공간 후킹으로 전환했습니다.
커널에서 후킹이란 일반적으로 커널이 특정 작업(예: 파일 열기 또는 시스템 호출)을 실행하려고 할 때 합법적인 코드 대신(또는 추가로) 루트킷의 코드를 호출하도록 커널 데이터 구조 또는 코드를 변경하는 것을 의미합니다. 수년에 걸쳐 Linux 커널 개발자들은 무단 수정을 방지하기 위해 더 강력한 보호 기능을 도입했지만 공격자들은 점점 더 정교한 후킹 방법으로 대응하고 있습니다. 아래에서는 커널 공간의 주요 후킹 기법을 살펴보고, 지금은 거의 사용되지 않는 오래된 방법부터 최신 커널 방어를 우회하는 최신 기법까지 살펴보겠습니다. 각 하위 섹션에서는 이 기술을 설명하고, 간단한 코드 예제를 보여주며, 알려진 루트킷에서의 사용 방법과 오늘날의 Linux 안전 장치에 따른 한계에 대해 논의합니다.
커널의 후킹 기법
인터럽트 디스크립터 테이블(IDT) 후킹
Linux에서 가장 초기의 커널 후킹 트릭 중 하나는 인터럽트 설명자 테이블(IDT)을 대상으로 하는 것이었습니다. 32비트 x86 Linux에서는 소프트웨어 인터럽트(int 0x80)를 통해 시스템 호출을 호출했습니다. IDT는 인터럽트 번호를 핸들러 주소에 매핑하는 테이블입니다. 루트킷은 0x80 에 대한 IDT 항목을 수정함으로써 커널의 자체 시스템 호출 디스패처가 제어권을 갖기 전에 시스템 호출 진입 지점을 탈취할 수 있습니다. 즉, 어떤 프로그램이 int 0x80 을 통해 시스템 호출을 트리거하면 CPU가 먼저 루트킷의 사용자 지정 핸들러로 이동하여 루트킷이 가장 낮은 수준에서 호출을 필터링하거나 리디렉션할 수 있습니다. 다음은 IDT 후킹의 단순화된 코드 예시입니다(설명용):
// Install the IDT hook
static int install_idt_hook(void) {
// Get pointer to IDT table
idt_table = get_idt_table();
// Save original syscall handler (int 0x80 = entry 128)
original_syscall_entry = idt_table[0x80];
// Calculate original handler address
original_syscall_handler = (void*)(
(original_syscall_entry.offset_high << 16) |
original_syscall_entry.offset_low
);
// Install our hook
idt_table[0x80].offset_low = (unsigned long)custom_int80_handler & 0xFFFF;
idt_table[0x80].offset_high =
((unsigned long)custom_int80_handler >> 16)
& 0xFFFF;
// Keep same selector and attributes as original
// idt_table[0x80].selector and type_attr remain unchanged
printk(KERN_INFO "IDT hook installed at 0x80\n");
return 0;
}
위의 코드는 인터럽트 0x80 에 대한 새 핸들러를 설정하여 시스템 호출 처리가 발생하기 전에 실행 흐름을 루트킷의 핸들러로 리디렉션합니다. 이를 통해 루트킷은 시스템 호출 테이블 수준 아래에서 시스템 호출 동작을 완전히 가로채거나 수정할 수 있습니다. IDT 후킹은 SuckIT와 같은 교육용 및 오래된 루트킷에서 사용됩니다.
IDT 후킹은 이제 대부분 과거의 기술입니다. int 0x80 메커니즘을 사용하는 구형 Linux 시스템(Linux 2.6 이전의 32비트 x86 커널)에서만 작동합니다. 최신 64비트 Linux는 소프트웨어 인터럽트 대신 sysenter/syscall 명령어를 사용하므로 0x80 에 대한 IDT 항목은 더 이상 시스템 호출에 사용되지 않습니다. 또한 IDT 후킹은 아키텍처에 따라 다르며(x86만 해당), x86_64 또는 기타 아키텍처를 사용하는 최신 커널에서는 효과적이지 않습니다.
시스콜 테이블 후킹
시스콜 테이블 후킹은 sys_call_table 으로 알려진 커널의 시스템 호출 디스패치 테이블을 수정하는 고전적인 루트킷 기법입니다. 이 표는 각 항목이 특정 시스템 호출 번호에 해당하는 함수 포인터의 배열입니다. 공격자는 이 표의 포인터를 덮어쓰면 getdents64, kill, read 와 같은 정상적인 시스템 호출을 악의적인 핸들러로 리디렉션할 수 있습니다. 아래에 예시가 나와 있습니다.
asmlinkage int (*original_getdents64)(
unsigned int,
struct linux_dirent64 __user *,
unsigned int);
asmlinkage int hacked_getdents64(
unsigned int fd,
struct linux_dirent64 __user *dirp,
unsigned int count)
{
int ret = original_getdents64(fd, dirp, count);
// Filter hidden entries from dirp
return ret;
}
write_cr0(read_cr0() & ~0x10000); // Disable write protection
sys_call_table[__NR_getdents64] = hacked_getdents64;
write_cr0(read_cr0() | 0x10000); // Re-enable write protection
이 예제에서 테이블을 수정하려면 먼저 커널 모듈이 테이블이 있는 메모리 페이지에서 쓰기 보호를 비활성화해야 합니다. 다음 어셈블리 코드(디아모르핀에서 볼 수 있음)는 write_cr0 함수를 더 이상 모듈로 내보내지 않더라도 CR0 제어 레지스터의 20번째 비트(쓰기 보호)를 지울 수 있는 방법을 보여 줍니다:
static inline void
write_cr0_forced(unsigned long val)
{
unsigned long __force_order;
asm volatile(
"mov %0, %%cr0"
: "+r"(val), "+m"(__force_order));
}
쓰기 방지가 비활성화되면 테이블의 시스템 호출 주소가 악성 함수의 주소로 바뀔 수 있습니다. 수정 후 쓰기 보호가 다시 활성화됩니다. 이 기법을 사용한 루트킷의 대표적인 예로는 Diamorphine, Knark, Reveng_rtkit 등이 있습니다. 시스콜 테이블 후킹에는 몇 가지 제한 사항이 있습니다:
- 커널 강화(2.6.25 이후) 숨기다
sys_call_table. - 커널 메모리 페이지가 읽기 전용으로 설정되었습니다(
CONFIG_STRICT_KERNEL_RWX). - 보안 부팅 및 커널 잠금 메커니즘과 같은 보안 기능은 CR0에 대한 수정을 방해할 수 있습니다.
가장 확실한 완화 조치는 x86-64 아키텍처에서 시스템 호출이 전송되는 방식을 근본적으로 변경한 Linux 커널 6.9에서 제공되었습니다. 버전 6.9 이전에는 커널이 sys_call_table 배열에서 핸들러를 직접 조회하여 시스템 호출을 실행했습니다:
// Pre-v6.9 Syscall Dispatch
asmlinkage const sys_call_ptr_t sys_call_table[] = {
#include <asm/syscalls_64.h>
};
커널 6.9부터는 스위치 문에서 syscall 번호가 적절한 핸들러를 찾아 실행하는 데 사용됩니다. sys_call_table 은 여전히 존재하지만 추적 도구와의 호환성을 위해서만 채워지며 더 이상 시스템 호출 실행 경로에 사용되지 않습니다.
// Kernel v6.9+ Syscall Dispatch
long x64_sys_call(const struct pt_regs *regs, unsigned int nr)
{
switch (nr) {
#include <asm/syscalls_64.h>
default: return __x64_sys_ni_syscall(regs);
}
};
이 아키텍처 변경으로 인해 커널 6.9 이상에서 sys_call_table 의 함수 포인터를 덮어쓰는 것은 시스템 호출 실행에 영향을 미치지 않으므로 이 기술은 완전히 효과가 없습니다. 이로 인해 시스콜 테이블 패치가 더 이상 실행 가능하지 않다고 생각했지만, 최근 FlipSwitch 기술을 발표하면서 이 벡터가 아직 죽지 않았음을 입증했습니다. 이 방법은 특정 레지스터 조작 가젯을 활용하여 커널 쓰기 보호 메커니즘을 일시적으로 비활성화함으로써 공격자가 최신 시스템 호출 경로의 "불변성(" )을 우회하고 이렇게 강화된 환경에서도 후크를 다시 도입할 수 있게 해줍니다.
플립스위치는 데이터 기반 sys_call_table 대신 커널의 새로운 시스콜 디스패처 함수인 x64_sys_call 의 컴파일된 머신 코드에 초점을 맞추고 있습니다. 이제 커널은 대규모 스위치 케이스 문을 사용하여 시스템 호출을 실행하기 때문에 각 시스템 호출에는 디스패처의 바이너리 내에 하드코딩된 call 명령어가 있습니다. 플립스위치는 x64_sys_call 함수의 메모리를 스캔하여 대상 시스콜의 특정 "서명", 일반적으로 0xe8 옵코드( CALL 명령어) 뒤에 원래의 합법적인 핸들러를 가리키는 4바이트 상대 오프셋을 찾습니다.
디스패처 내에서 이 호출 사이트가 식별되면 루트킷은 가젯을 사용하여 CR0 제어 레지스터의 쓰기 보호(WP) 비트를 지우고 커널의 실행 코드 세그먼트에 임시 쓰기 액세스 권한을 부여합니다. 그런 다음 원래의 상대 오프셋을 악의적인 공격자가 제어하는 함수를 가리키는 새 오프셋으로 덮어씁니다. 이렇게 하면" 스위치를 디스패치 시점에서 효과적으로 "뒤집어 커널이 최신 스위치 명령문 경로를 통해 대상 시스템 호출을 실행하려고 할 때마다 대신 루트킷으로 리디렉션됩니다. 이를 통해 6.9 커널의 아키텍처 강화에도 불구하고 안정적이고 정밀한 시스템 호출 차단이 가능합니다.
인라인 후킹 / 기능 프롤로그 패치 적용
인라인 후킹은 포인터 테이블을 통한 후킹의 대안입니다. 인라인 후킹은 테이블의 포인터를 수정하는 대신 대상 함수 자체의 코드를 패치합니다. 루트킷은 커널 함수의 시작(프롤로그)에 점프 명령을 작성하여 실행을 루트킷의 자체 코드로 전환합니다. 이 기술은 함수 핫패치 또는 Windows의 사용자 모드 후크가 작동하는 방식(예: 함수의 첫 바이트를 수정하여 우회로로 이동)과 유사합니다.
예를 들어 루트킷은 do_sys_open (열린 파일 시스템 호출 처리의 일부인)와 같은 커널 함수를 표적으로 삼을 수 있습니다. 루트킷은 do_sys_open 의 처음 몇 바이트를 악성 코드에 대한 x86 JMP 명령어로 덮어씌움으로써 do_sys_open 호출이 있을 때마다 루트킷의 루틴으로 이동하도록 합니다. 그러면 악성 루틴은 원하는 대로 실행할 수 있으며(예: 열려는 파일 이름이 숨겨진 목록에 있는지 확인하고 액세스 거부), 선택적으로 원본 do_sys_open 을 호출하여 숨겨진 파일이 아닌 파일에 대한 정상적인 동작을 계속할 수 있습니다.
unsigned char *target = (unsigned char *)kallsyms_lookup_name("do_sys_open");
unsigned long hook = (unsigned long)&malicious_function;
int offset = (int)(hook - ((unsigned long)target + 5));
unsigned char jmp[5] = {0xE9};
memcpy(&jmp[1], &offset, 4);
// Memory protection omitted for brevity
memcpy(target, jmp, 5);
asmlinkage long malicious_function(
const char __user *filename,
int flags, umode_t mode) {
printk(KERN_INFO "do_sys_open hooked!\n");
return -EPERM;
}
이 코드는 do_sys_open() 의 시작 부분을 JMP 명령어로 덮어쓰고 실행을 악성 코드로 리디렉션합니다. 오픈 소스 루트킷 Reptile은 KHOOK이라는 사용자 정의 프레임워크를 통해 인라인 기능 패치를 광범위하게 사용합니다(곧 설명할 예정입니다).
Reptile의 인라인 후크는 sys_kill 등의 함수를 대상으로 하여 백도어 명령(예: 프로세스에 특정 신호를 보내면 루트킷이 권한을 올리거나 프로세스를 숨기도록 트리거)을 가능하게 합니다. 또 다른 예로, 일부 후크에 인라인 패치를 적용한 Suterusu도 있습니다.
인라인 후킹은 취약하고 위험성이 높습니다. 함수의 프롤로그를 덮어쓰는 것은 커널 버전과 컴파일러 차이에 민감하고(따라서 후크는 종종 빌드별 패치나 런타임 디스어셈블리가 필요함), 명령어나 동시 실행이 제대로 처리되지 않으면 시스템이 쉽게 충돌할 수 있으며, 최신 메모리 보호(W^X, CR0 WP, 모듈 서명/잠금)를 우회하거나 커널 텍스트가 쓰기 가능하도록 만들기 위해 취약점을 악용해야 하기 때문이죠.
가상 파일 시스템 후킹
Linux의 VFS(가상 파일 시스템) 계층은 파일 작업을 위한 추상화를 제공합니다. 예를 들어 디렉터리(예: ls /proc)를 읽으면 커널은 결국 디렉터리 항목을 반복하는 함수를 호출합니다. 파일 시스템은 iterate_shared (디렉토리 내용 나열) 또는 파일 I/O를 위한 읽기/쓰기와 같은 작업에 대한 함수 포인터를 사용하여 자체 file_operations를 정의합니다. VFS 후킹은 이러한 함수 포인터를 루트킷이 제공하는 함수로 대체하여 파일시스템이 데이터를 표시하는 방식을 조작하는 것입니다.
기본적으로 루트킷은 VFS에 연결하여 디렉터리 목록에서 파일이나 디렉터리를 필터링하여 숨길 수 있습니다. 일반적인 방법은 디렉터리 항목을 반복하는 함수를 후크하고 특정 패턴과 일치하는 파일 이름을 건너뛰도록 하는 것입니다. 디렉터리의 file_operations 구조(특히 /proc 또는 /sys)는 악성 프로세스를 숨기는 데 종종 /proc/<pid> 아래에 항목을 숨기는 것이 포함되기 때문에 자주 공격 대상이 됩니다.
디렉터리 목록 함수에 대한 이 예제 훅을 살펴보세요:
static iterate_dir_t original_iterate;
static int malicious_filldir(
struct dir_context *ctx,
const char *name, int namelen,
loff_t offset, u64 ino,
unsigned int d_type)
{
if (!strcmp(name, "hidden_file"))
return 0; // Skip hidden_file
return ctx->actor(ctx, name, namelen, offset, ino, d_type);
}
static int malicious_iterate(struct file *file, struct dir_context *ctx)
{
struct dir_context new_ctx = *ctx;
new_ctx.actor = malicious_filldir;
return original_iterate(file, &new_ctx);
}
// Hook installation
file->f_op->iterate = malicious_iterate;
이 대체 기능은 디렉터리 목록 작업 중에 숨겨진 파일을 필터링합니다. 루트킷은 VFS 수준에서 후킹함으로써 시스템 호출 테이블이나 로우레벨 어셈블리를 변조할 필요 없이 파일 시스템 인터페이스에 피기백하기만 하면 됩니다. 한때 인기를 끌었던 Linux 루트킷인 Adore-NG는 파일과 프로세스를 숨기기 위해 VFS 후킹을 사용했습니다. 디렉터리 반복에 대한 함수 포인터를 패치하여 특정 PID 및 파일 이름에 대한 항목을 숨겼습니다. 다른 많은 커널 루트킷은 VFS 후크를 통해 자신 또는 아티팩트를 숨기는 유사한 코드를 사용합니다.
VFS 후킹은 여전히 널리 사용되고 있지만 버전 간 커널 구조 오프셋의 변경으로 인해 후킹이 끊어질 수 있는 한계가 있습니다.
F트레이스 기반 후킹
최신 Linux 커널에는 ftrace(함수 추적기)라는 강력한 추적 프레임워크가 포함되어 있습니다. Ftrace는 디버깅 및 성능 분석을 위한 것으로, 커널 코드를 직접 수정하지 않고도 거의 모든 커널 함수 진입 또는 종료에 후크(콜백)를 첨부할 수 있습니다. 런타임에 제어된 방식으로 커널 코드를 동적으로 수정하는 방식으로 작동합니다(주로 추적 핸들러를 호출하는 경량 트램폴린을 패치하는 방식으로). 중요한 것은 ftrace는 특정 조건(예: ftrace를 지원하는 커널을 빌드하고 debugfs 인터페이스를 사용할 수 있는 경우)이 충족되는 한 커널 모듈이 추적 핸들러를 등록할 수 있는 API를 제공한다는 점입니다.
루트킷은 덜 분명한 방식으로 훅을 구현하기 위해 ftrace를 악용하기 시작했습니다. 루트킷은 함수에 JMP 을 수동으로 작성하는 대신 커널의 ftrace 기계에 이를 대신 수행하도록 요청하여 훅을 '합법화'할 수 있습니다. 즉, 루트킷은 함수의 주소를 찾거나 페이지 보호 기능을 수정할 필요 없이 가로채려는 함수 이름에 대한 콜백을 등록하기만 하면 커널이 훅을 설치합니다.
다음은 ftrace를 사용하여 mkdir 시스템 호출 핸들러를 연결하는 간단한 예제입니다:
static int __init hook_init(void) {
target_addr = kallsyms_lookup_name(SYSCALL_NAME("sys_mkdir"));
if (!target_addr) return -ENOENT;
real_mkdir = (void *)target_addr;
ops.func = ftrace_thunk;
ops.flags = FTRACE_OPS_FL_SAVE_REGS
| FTRACE_OPS_FL_RECURSION_SAFE
| FTRACE_OPS_FL_IPMODIFY;
if (ftrace_set_filter_ip(&ops, target_addr, 0, 0)) return -EINVAL;
return register_ftrace_function(&ops);
}
이 훅은 sys_mkdir 함수를 가로채서 악성 핸들러를 통해 경로를 재지정합니다. KoviD, Singularity, Umbra와 같은 최신 루트킷은 ftrace 기반 훅을 활용했습니다. 이러한 루트킷은 다양한 커널 함수(시스템 호출 포함)에 ftrace 콜백을 등록하여 이를 모니터링하거나 조작합니다.
에프트레이스 후킹의 가장 큰 장점은 글로벌 테이블이나 패치된 코드에 뚜렷한 흔적을 남기지 않는다는 점입니다. 후킹은 합법적인 커널 인터페이스를 통해 이루어집니다. 훈련되지 않은 눈에는 모든 것이 정상으로 보입니다. sys_call_table 는 손상되지 않았고, 함수 프롤로그는 루트킷에 의해 수동으로 덮어쓰이지 않았습니다(ftrace 메커니즘에 의해 덮어쓰기 되지만 이는 추적이 활성화된 커널에서 흔히 발생하며 허용되는 현상입니다). 또한 ftrace 후크는 종종 즉석에서 활성화/비활성화할 수 있으며 수동 패치보다 본질적으로 덜 방해가 됩니다.
ftrace 후킹은 강력하지만 환경 및 권한 경계(커널 외부에서 사용하는 경우)에 의해 제약을 받습니다. 추적 인터페이스(debugfs) 및 CAP_SYS_ADMIN 권한에 액세스해야 하며, 네임스페이스, LSM 또는 보안 부팅 잠금 정책에 의해 UID 0 조차 제한되는 강화 또는 컨테이너화된 시스템에서는 사용하지 못할 수 있습니다. 보안상의 이유로 프로덕션 환경에서는 디버그 파일을 마운트 해제하거나 읽기 전용으로 설정할 수도 있습니다. 따라서 일반적으로 모든 권한이 있는 루트 사용자는 ftrace를 사용할 수 있지만, 최신 방어는 이러한 기능을 비활성화하거나 제한하는 경우가 많아 고도로 강화된 환경에서는 ftrace 기반 후크의 실용성이 떨어집니다.
K프로브 후킹
Kprobes는 디버깅 및 계측을 위한 또 다른 커널 기능으로, 공격자들이 루트킷 후킹을 위해 용도를 변경했습니다. K프로브를 사용하면 프로브 핸들러를 등록하여 런타임에 거의 모든 커널 루틴에 동적으로 침입할 수 있습니다. 지정된 명령어가 실행되려고 하면 kprobe 인프라는 상태를 저장하고 사용자 지정 핸들러로 제어권을 전송합니다. 핸들러가 실행되면(레지스터나 명령 포인터를 변경할 수도 있습니다) 커널은 원래 코드의 정상적인 실행을 재개합니다. 간단히 말해서, kprobes를 사용하면 핸들러가 있는 중단점처럼 커널 코드의 임의 지점(함수 입력, 특정 명령어 등)에 사용자 지정 콜백을 첨부할 수 있습니다.
악성 후킹에 kpro브를 사용하는 것은 일반적으로 함수가 어떤 작업을 수행하지 못하도록 하거나 정보를 수집하기 위해 함수를 가로채는 것을 포함합니다. 최신 루트킷의 일반적인 사용: 많은 중요한 심볼( sys_call_table 또는 kallsyms_lookup_name)이 더 이상 내보내지지 않기 때문에, 루트킷은 해당 심볼에 액세스할 수 있는 함수에 kprobe를 배포하여 이를 탈취할 수 있습니다. 케이프로브 구조 및 등록은 아래와 같습니다.
// Declare a kprobe targeting the symbol "kallsyms_lookup_name"
static struct kprobe kp = {
.symbol_name = "kallsyms_lookup_name"
};
// Function pointer type matching kallsyms_lookup_name
typedef unsigned long
(*kallsyms_lookup_name_t)(const char *name);
// Global pointer to the resolved kallsyms_lookup_name
kallsyms_lookup_name_t kallsyms_lookup_name;
// Register the kprobe; kernel resolves kp.addr
// to the address of the symbol
register_kprobe(&kp);
// Assign resolved address to our function pointer
kallsyms_lookup_name =
(kallsyms_lookup_name_t) kp.addr;
// Unregister the kprobe (only needed it once)
unregister_kprobe(&kp);
이 프로브는 kallsyms_lookup_name 의 심볼 이름을 검색하는 데 사용되며, 일반적으로 시스템 호출 테이블 후킹의 전구체입니다. 초기 커밋에는 없었지만 최근 디아모르핀 업데이트에서 이 기법을 사용했습니다. kallsyms_lookup_name 자체의 포인터를 잡기 위해 k프로브를 배치합니다(또는 알려진 함수에 k프로브를 사용하여 필요한 것을 간접적으로 가져옵니다). 마찬가지로 다른 루트킷은 임시 kprobe를 사용하여 심볼을 찾은 다음 완료되면 등록을 취소하고 다른 수단을 통해 후크를 수행합니다. K프로브는 주소 찾기뿐만 아니라 동작을 직접 연결하는 데에도 사용할 수 있습니다. 또는 j프로브(특수한 k프로브)로 함수를 완전히 리디렉션할 수도 있습니다. 그러나 기능을 완전히 대체하기 위해 kprobes를 사용하는 것은 까다롭고 일반적으로 수행되지 않으며, 기능을 일관되게 하이재킹하려면 패치를 적용하거나 ftrace를 사용하는 것이 더 간단하기 때문입니다. K프로브는 간헐적 또는 보조적 후킹에 자주 사용됩니다.
K프로브는 유용하지만 제한적입니다. 런타임 오버헤드가 추가되고 매우 뜨겁거나 제한된 로우 레벨 기능에 배치될 경우 시스템을 불안정하게 만들 수 있으므로(재귀 프로브는 억제됨) 공격자는 프로브 지점을 신중하게 선택해야 합니다. 또한 감사 가능하며 커널 경고를 트리거하거나 시스템 감사를 통해 기록될 수 있고 활성 프로브는 /sys/kernel/debug/kprobes/list 에서 볼 수 있으므로(예상치 못한 항목은 의심스러운 항목) 일부 커널은 kprobe/디버그 지원 없이 빌드될 수도 있습니다.
커널 훅 프레임워크
앞서 언급했듯이, 공격자는 Reptile 루트킷을 사용하여 훅을 관리하기 위해 상위 수준의 프레임워크를 만들기도 합니다. 커널 훅(KHOOK)은 인라인 패치의 지저분한 작업을 추상화하고 루트킷 개발자에게 더 깔끔한 인터페이스를 제공하는 프레임워크 중 하나(Reptile의 저자가 개발)입니다. 기본적으로 KHOOK은 후크할 함수와 대체 함수를 지정할 수 있는 라이브러리로, 커널 코드 수정을 처리하는 동시에 원래 함수를 안전하게 호출할 수 있는 트램폴린을 제공합니다. 예를 들어, 다음은 (Reptile의 사용법에 따라) KHOOK과 유사한 매크로를 사용하여 킬 시스콜을 후킹하는 방법의 예시입니다:
// Creates a replacement for sys_kill:
// long sys_kill(long pid, long sig)
KHOOK_EXT(long, sys_kill, long, long);
static long khook_sys_kill(long pid, long sig) {
// Signal 0 is used to check if a process
// exists (without sending a signal)
if (sig == 0) {
// If the target is invisible (hidden by
// a rootkit), pretend it doesn't exist
if (is_proc_invisible(pid)) {
return -ESRCH; // No such process
}
}
// Otherwise, forward the call to the original sys_kill syscall
return KHOOK_ORIGIN(sys_kill, pid, sig);
}
KHOOK은 인라인 함수 패칭을 통해 작동하며, 공격자가 제어하는 핸들러로 점프하여 함수 프롤로그를 덮어씁니다. 위의 예는 킬 신호가 0인 경우 sys_kill() 이 악성 핸들러로 리디렉션되는 방법을 보여줍니다.
KHOOK은 인라인 패치를 간소화하지만 커널 텍스트를 수정하여 점프 스텁을 삽입하므로 커널 잠금, 보안 부팅 또는 W^X 같은 보호 기능이 이를 차단할 수 있다는 단점도 그대로 이어받습니다. 또한 아키텍처 및 버전에 따라 달라지므로(일반적으로 x86으로 제한되며 커널 5.x 이상에서는 실패함) 빌드 간에 취약할 수 있습니다.
사용자 공간의 후킹 기술
사용자스페이스 후킹은 동적 링커를 통해 액세스하는 libc 계층 또는 기타 공유 라이브러리를 대상으로 사용자 도구에서 사용하는 일반적인 API 호출을 가로채는 기술입니다. 이러한 호출의 예로는 readdir, getdents, open, fopen, fgets, connect 등이 있습니다. 공격자는 대체 기능을 삽입하여 ps, ls, lsof, netstat 와 같은 일반 사용자 영역 도구를 조작하여 변경된 또는 "살균된" 보기를 반환할 수 있습니다. 이는 프로세스, 파일, 소켓을 숨기거나 악성 코드의 증거를 숨기는 데 사용됩니다.
이를 구현하는 일반적인 방법은 동적 링커가 심볼을 확인하는 방법을 반영하거나 프로세스 메모리를 수정하는 것과 관련이 있습니다. 이러한 방법에는 LD_PRELOAD 환경 변수 또는 LD_AUDIT 을 사용하여 악성 공유 객체(.so) 파일을 강제로 조기 로드하거나, ELF DT_* 항목 또는 라이브러리 검색 경로를 수정하여 악의적인 라이브러리의 우선순위를 지정하거나, 프로세스 내에서 런타임 GOT/PLT 덮어쓰기를 수행하는 것이 포함됩니다. 일반적으로 메모리 보호 설정(mprotect)을 변경하고 새 코드를 작성(write)한 다음 주입 후 원래 설정(restore)을 복원하는 과정을 거칩니다.
후킹된 함수는 일반적으로 정상 작동을 위해 dlsym(RTLD_NEXT, ...) 을 사용하여 실제 libc 심볼을 호출합니다. 그런 다음 숨기려는 대상에 대해서만 결과를 필터링하거나 변경합니다. readdir() 함수에 대한 LD_PRELOAD 필터의 기본 예는 다음과 같습니다.
#define _GNU_SOURCE // GNU extensions (RTLD_NEXT)
#include <dlfcn.h> // dlsym(), RTLD_NEXT
#include <dirent.h> // DIR, struct dirent, readdir()
#include <string.h> // strstr()
// Pointer to the original readdir()
static struct dirent *(*real_readdir)(DIR *d);
struct dirent *readdir(DIR *d) {
if (!real_readdir) // resolve original once
real_readdir =
dlsym(RTLD_NEXT, "readdir");
struct dirent *ent;
// Fetch next dir entry from real readdir
while ((ent = real_readdir(d)) != NULL) {
// If name contains the secret marker,
// skip this entry (hide it)
if (strstr(ent->d_name, ".secret"))
continue;
return ent; // return visible entry
}
return NULL; // no more entries
}
이 예에서는 실제 libc 이전에 확인된 라이브러리를 제공하여 readdir() 인프로를 대체하여 필터와 일치하는 파일명을 효과적으로 숨깁니다. 과거 사용자 모드 숨김 도구와 경량 '루트킷'은 프로세스, 파일, 소켓을 숨기기 위해 LD_PRELOAD 또는 GOT/PLT 패치를 사용했습니다. 또한 공격자는 커널 모듈 없이도 특정 서비스에 공유 개체를 삽입하여 표적 스텔스를 달성할 수 있습니다.
사용자 공간 개입은 악성 라이브러리를 로드하거나 주입되는 프로세스에만 영향을 미칩니다. 시스템 전체의 지속성에 취약합니다(서비스/단위 파일, 살균된 환경, setuid/정적 바이너리로 인해 복잡해집니다). 의심스러운 LD_PRELOAD/LD_AUDIT 항목, /proc/<pid>/maps 에서 예기치 않게 매핑된 공유 개체, 온디스크 라이브러리와 인메모리 가져오기 간의 불일치 또는 변경된 GOT 항목이 있는지 확인하면 커널 후크와 관련하여 간단하게 탐지할 수 있습니다. 무결성 도구, 서비스 수퍼바이저(시스템드), 간단한 프로세스 메모리 검사 등을 통해 이 기법이 노출되는 경우가 많습니다.
eBPF를 사용한 후킹 기술
보다 최근의 루트킷 구현 모델에는 eBPF(확장 버클리 패킷 필터)의 남용이 포함됩니다. eBPF는 권한이 있는 사용자가 바이트코드 프로그램을 커널에 로드할 수 있도록 하는 Linux의 하위 시스템입니다. 흔히 "샌드박스가 적용된 VM으로 설명되지만," 실제로 보안은 바이트코드가 거의 지연 시간이 없는 실행을 위해 네이티브 머신 코드로 JIT 컴파일되기 전에 정적 검증기를 통해 안전(무한 루프 없음, 불법 메모리 액세스 없음)을 보장하는 데 의존합니다.
공격자는 LKM을 삽입하여 커널 동작을 수정하는 대신 민감한 커널 이벤트에 첨부되는 하나 이상의 eBPF 프로그램을 로드할 수 있습니다. 예를 들어, execve 에 대한 시스템 호출 항목에 첨부되는 eBPF 프로그램을 작성하여(kprobe 또는 트레이스포인트를 통해) 프로세스 실행을 모니터링하거나 조작할 수 있습니다. 마찬가지로 eBPF는 프로그램 실행 알림과 같이 특정 동작을 방지하거나 숨기기 위해 LSM 계층에 후킹할 수 있습니다. 아래에 예시가 나와 있습니다.
// Attach this eBPF program to the tracepoint for sys_enter_execve
SEC("tp/syscalls/sys_enter_execve")
int tp_sys_enter_execve(struct sys_execve_enter_ctx *ctx) {
// Get the current process's PID and TID as a 64-bit value
// Upper 32 bits = PID, Lower 32 bits = TID
__u64 pid_tgid = bpf_get_current_pid_tgid();
// Delegate handling logic to a helper function
return handle_tp_sys_enter_execve(ctx, pid_tgid);
}
대표적인 공개 사례로는 트리플크로스와 붑킷이 있습니다. 트리플크로스는 지속성과 은닉을 위해 실행과 같은 시스템 호출을 연결하기 위해 eBPF를 사용하는 루트킷을 시연했습니다. Boopkit은 소켓 버퍼를 조작(원격 당사자가 조작된 패킷을 통해 루트킷과 통신할 수 있게 함)할 수 있는 eBPF 프로그램을 첨부하여 은밀한 통신 채널 및 백도어로 eBPF를 사용했습니다. 이 프로젝트는 개념 증명 프로젝트이지만 루트킷 개발에서 eBPF의 실행 가능성을 입증했습니다.
주요 장점은 eBPF 후킹을 위해 LKM을 로드할 필요가 없고 최신 커널 보호와 호환된다는 점입니다. eBPF를 지원하는 커널의 경우 이는 강력한 기술입니다. 그러나 이러한 기능은 강력하지만 제약도 있습니다. 로드하려면 높은 권한이 필요하고, 검증자의 안전 검사에 의해 제한되며, 재부팅 시 일시적(별도의 지속성 필요)이고, 감사/포렌식 도구로 발견될 가능성이 점점 더 커지고 있습니다. 특히 일반적으로 eBPF 툴링을 사용하지 않는 시스템에서 eBPF의 사용이 두드러지게 나타납니다.
io_uring을 사용한 회피 기술
io_uring 은 후킹에 사용되지는 않지만, 최근 루트킷이 사용하는 EDR 회피 기법에 추가되었기 때문에 언급할 가치가 있습니다. io_uring 는 비동기식 링 버퍼 기반 I/O API로, 프로세스가 최소한의 시스템 호출 오버헤드로 I/O 요청 일괄 처리(SQE)를 제출하고 완료(CQE)를 거둘 수 있게 해줍니다. 후킹 프레임워크는 아니지만, 그 설계는 시스템 호출/가시성 표면을 변경하고 공격자가 은밀한 I/O, 시스템 호출 회피 워크플로에 악용하거나 취약점과 결합하면 하위 계층에 후크를 설치하도록 유도하는 익스플로잇 프리미티브로 사용할 수 있는 강력한 커널 지향 프리미티브(등록된 버퍼, 고정 파일, 매핑된 링)를 노출합니다.
공격 패턴은 (1) 회피/성능 악용: 악성 프로세스가 io_uring 을 사용하여 대량의 읽기/쓰기/메타데이터 작업을 일괄적으로 수행하므로 기존의 시스템 호출별 탐지기가 더 적은 이벤트 또는 비정형 패턴을 탐지합니다. (2) 익스플로잇 활성화: io_uring 표면의 버그(링 매핑, 등록된 리소스)는 역사적으로 권한 상승의 벡터였으며, 공격자는 더 전통적인 방법으로 커널 후크를 설치할 수 있습니다. io_uring 는 또한 코드가 직접 연산을 전송하는 경우 일부 라이브러리 래퍼를 우회하므로 라이브러리 호출을 가로채는 유저랜드 후킹을 우회할 수 있습니다. 간단한 제출/회수 흐름은 아래에 설명되어 있습니다:
// Minimal io_uring usage (error handling omitted)
// io_uring context (SQ/CQ rings shared with kernel)
struct io_uring ring;
// Initialize ring with space for 16 SQEs
io_uring_queue_init(16, &ring, 0);
// Grab a free submission entry (or NULL if full)
struct io_uring_sqe *sqe =
io_uring_get_sqe(&ring);
// Prepare SQE as a read(fd, buf, len, offset=0)
io_uring_prep_read(sqe, fd, buf, len, 0);
// Submit pending SQEs to the kernel (non-blocking)
io_uring_submit(&ring);
struct io_uring_cqe *cqe;
// Block until a completion is available
io_uring_wait_cqe(&ring, &cqe);
// Mark the completion as handled (free slot)
io_uring_cqe_seen(&ring, cqe);
위의 예는 단일 또는 몇 개의 io_uring_enter syscall로 많은 파일 작업을 커널에 공급하는 제출 대기열을 보여 주며, 작업별 syscall 원격 측정을 줄입니다.
은밀한 데이터 수집 또는 높은 처리량 유출에 관심이 있는 공격자는 io_uring 으로 전환하여 시스템 호출 노이즈를 줄일 수 있습니다. io_uring 는 본질적으로 전역 훅을 설치하거나 다른 프로세스의 동작을 변경하지 않으며, 권한 상승과 결합되지 않는 한 프로세스 로컬입니다. io_uring syscall(io_uring_enter, io_uring_register)을 계측하고 비정상적인 패턴(비정상적으로 큰 배치, 등록된 파일/버퍼가 많거나 대량의 일괄 메타데이터 작업을 수행하는 프로세스 등)을 관찰하면 탐지가 가능합니다. 커널 버전 차이도 중요합니다: io_uring 기능은 빠르게 진화하므로 공격자의 기법은 버전에 따라 달라질 수 있습니다. 마지막으로 io_uring 는 실행 중인 악성 프로세스를 필요로 하기 때문에 방어자는 종종 이를 중단하고 링, 등록된 파일 및 메모리 매핑을 검사하여 오용을 발견할 수 있습니다.
결론
Linux의 후킹 기술은 단순히 테이블의 포인터를 덮어쓰는 것에서 먼 길을 걸어왔습니다. 이제 공격자들은 탐지하기 어려운 후크를 심기 위해 합법적인 커널 계측 프레임워크(ftrace, kprobes, eBPF)를 악용하는 것을 볼 수 있습니다. IDT 및 시스콜 테이블 패치부터 인라인 후크 및 동적 프로브에 이르기까지 각 방법에는 은닉성과 안정성 측면에서 고유한 절충점이 있습니다. 방어자는 이러한 모든 가능한 벡터를 알고 있어야 합니다. 실제로 최신 루트킷은 목표를 달성하기 위해 여러 후킹 기술을 결합하는 경우가 많습니다. 예를 들어, PUMAKIT은 직접 syscall 테이블 훅과 ftrace 훅을 사용하고, Diamorphine은 syscall 훅과 kprobe를 사용하여 심볼 숨기기를 우회합니다. 이러한 계층적 접근 방식은 탐지 도구가 시스템의 여러 측면을 검사해야 한다는 것을 의미합니다: IDT 항목, 시스템 호출 테이블, 모델별 레지스터(시스템 후크용), 함수 프롤로그의 무결성, 구조 내 중요 함수 포인터의 내용(VFS 등), 활성 ftrace 작업, 등록된 kprobes, 로드된 eBPF 프로그램 등 다양한 측면을 검사해야 합니다.
이 시리즈의 2부에서는 이론에서 실무로 넘어갑니다. 여기서 다루는 루트킷 분류법과 후킹 기술에 대한 이해를 바탕으로 실제 Linux 환경에서 이러한 위협을 식별하기 위한 탐지 엔지니어링, 실용적인 탐지 전략 구축 및 적용에 초점을 맞출 것입니다.
