복사 실패 및 더티프래그: Linux 페이지 캐시 버그

이 연구는 미묘한 페이지 캐시 손상 버그를 악용하여 루트 액세스에 대한 안정적인 경로를 생성하는 Linux 커널 권한 상승 취약점인 Copy Fail과 DirtyFrag를 분석합니다. 또한 Elastic Security Labs는 이러한 취약점에 대한 탐지 로직을 공개하고 있습니다.

서문

최근 Linux 커널 권한 에스컬레이션 취약점인 복사 실패(CVE-2026-31431) , 복사 실패 2, 더티프래그는 미묘한 페이지 캐시 손상 버그가 어떻게 루트에 대한 실질적이고 안정적인 경로가 될 수 있는지를 보여줍니다. 이러한 문제는 익스플로잇에 합법적인 커널 인터페이스, 로컬 실행, 짧은 개념 증명 코드가 포함되므로 방어자에게 특히 중요합니다. 복사 실패는 야생에서 악용되는 것으로 보고되었으며 CISA의 알려진 악용 취약점 카탈로그에 추가되었습니다.

이러한 위협을 완화하기 위해 Elastic Security Labs는 특정 개념 증명 구현과 일치하는 것이 아니라 이러한 취약점 주변의 익스플로잇 패턴에 초점을 맞춘 탐지 로직을 개발했습니다.

복사 실패

복사 실패는 Linux 커널의 authencesn 암호화 템플릿의 논리 버그입니다. 이 취약점은 AF_ALGsplice() 을 연결하여 읽기 가능한 파일의 페이지 캐시에 제어된 4바이트 쓰기를 생성합니다. 실제로 이렇게 하면 /usr/bin/su 같은 setuid 바이너리의 인메모리 보기가 손상되고 디스크의 파일을 변경하지 않고도 권한이 에스컬레이션됩니다. 이 공개 익스플로잇은 우분투, 아마존 리눅스, RHEL, SUSE에서 작동하는 732바이트의 Python 스크립트입니다.

DirtyFrag

DirtyFrag는 두 가지 페이지 캐시 쓰기 변형을 통해 동일한 버그 클래스를 네트워킹 스택으로 확장합니다. ESP 경로는 AF_NETLINK 을 통해 XFRM 보안 연결을 사용하여 연결된 페이지에서 제자리 암호화 작업을 수행하여 최소 루트 셸 ELF로 /usr/bin/su 을 덮어씁니다. RxRPC 폴백 경로는 AF_RXRPCpcbc(fcrypt) 과 함께 사용하여 /etc/passwd 을 손상시켜 루트의 비밀번호 필드를 지웁니다. 두 경로 모두 페이지 캐시 쓰기를 트리거하기 전에 네임스페이스 기능을 얻으려면 unshare(CLONE_NEWUSER | CLONE_NEWNET) 이 필요합니다.

DirtyFrag는 algif_aead 모듈에 의존하지 않으므로 복사 실패 완화 기능만 적용한 시스템도 여전히 노출될 수 있습니다.

탐지

이러한 취약점의 경우, 특정 익스플로잇 구현뿐만 아니라 기본 요소와 동작을 탐지하는 데 중점을 두었습니다. Copy Fail은 이미 여러 공개 구현(Python, Go, Rust, C, 메타스플로잇)이 있으며, DirtyFrag는 공개 C 개념 증명으로 출시됩니다. 특정 PoC만 탐지하려고 하면 방어자가 한 발 뒤처지게 됩니다.

시스템 호출 수준 프리미티브(감사)

Copy Fail과 DirtyFrag는 모두 커널 암호화 하위 시스템에 액세스하기 위해 socket(AF_ALG), 읽기 전용 파일 페이지를 네트워크 버퍼에 주입하기 위해 splice() 을 사용하여 제자리 암호화 작업으로 페이지 캐시가 손상되는 경우 이를 사용합니다. DirtyFrag는 AF_ALG 을 사용할 수 없는 경우 socket(AF_RXRPC) 을 폴백으로 추가로 사용합니다. 이러한 프리미티브는 a0 16진수 값 26 (AF_ALG) 또는 21 (AF_RXRPC), 루트 프로세스가 아닌 프로세스의 splice 호출을 통해 감사된 시스템 호출 감사( socket )를 통해 확인할 수 있습니다. 이를 초기 단계 신호로 사용하며, EQL 시퀀스를 통해 루트가 아닌 호출자로부터 유효한 아이디 0 를 얻는 최종 권한 상승 단계와 상호 연관시킵니다:

sequence with maxspan=60s
  [any where host.os.type == "linux" and    
   (
    (event.category == "process" and auditd.data.syscall == "socket" and auditd.data.a0 in ("26", "21")) or 
    (event.category == "process" and auditd.data.syscall == "splice") or 
    (event.category == "network" and event.action == "bound-socket" and data_stream.dataset == "auditd_manager.auditd" and ?auditd.data.socket.family == "38") 
    )  
   and user.id != "0"]  by process.pid, host.id, user.id with runs=10
  [process where host.os.type == "linux"  and event.action == "executed" and 
   (
     (user.effective.id == "0" and user.id != "0") or 
     (process.name in ("bash", "sh", "zsh", "dash", "fish", "ksh", "busybox") and 
      process.args in ("-c", "--command", "-ic", "-ci", "-cl", "-lc", "-bash", "-sh", "-zsh", "-dash", "-fish", "-ksh"))
    )] by process.parent.pid, host.id, user.id

Example of matches :

네임스페이스 생성(DirtyFrag 관련)

더티프래그의 익스플로잇 체인은 네임스페이스 기능을 얻기 위해 unshare(CLONE_NEWUSER | CLONE_NEWNET) 에도 의존합니다. 이 이벤트는 루트 프로세스 실행 또는 그 직후의 setuid(0) syscall과 연관되어 있습니다:

sequence by host.id, process.parent.pid with maxspan=30s
 [process where host.os.type == "linux" and 
  (
   (auditd.data.syscall == "unshare" and auditd.data.class == "namespace" and auditd.data.a0 in ("10000000", "50000000", "70000000", "10020000", "50020000", "70020000")) or 

   (process.name == "unshare" and  
    (process.args in ("--user", "--map-root-user", "--map-current-user") or process.args like ("-*U*", "-*r*")))
   ) and user.id != "0" and user.id != null]
 [process where host.os.type == "linux" and 
  user.id == "0" and user.id != null and 
  (
   process.name in ("su", "sudo", "pkexec", "passwd", "chsh", "newgrp", "doas", "run0", "sg", "dash", "sh", "bash", "zsh", "fish", 
                    "ksh", "csh", "tcsh", "ash", "mksh", "busybox", "rbash", "rzsh", "rksh", "tmux", "screen", "node") or 
   process.name like ("python*", "perl*", "ruby*", "php*", "lua*")
  )]

일반 SUID 바이너리 어뷰징(프로세스 실행 이벤트)

또한 프로세스 실행 이벤트만 사용하는 탐지 옵션도 평가했는데, 이는 감사된 시스템 호출 감사보다 더 많은 환경에서 활성화되는 경향이 있기 때문입니다. 두 익스플로잇의 공통적인 최종 단계는 su, sudo, pkexec, passwd, chsh 또는 newgrp 와 같은 SUID 바이너리의 인메모리 실행을 손상시키거나 영향을 주어 공격자가 제어하는 코드를 루트로 실행하도록 하는 것입니다.

탐지는 프로세스가 유효 UID 0으로 실행되고, 실제 사용자가 루트가 아니며, 부모 프로세스도 루트가 아니고, SUID 바이너리가 최소한의 인수로 실행되고, 부모 프로세스가 스크립팅 런타임, 셸 원라이너 또는 사용자 작성 가능 경로에서 실행되는 경우 의심스러운 실행을 찾습니다:

process where event.type == "start" and event.action == "exec" and (
  (process.user.id == 0 and process.real_user.id != 0) or
  (process.group.id == 0 and process.real_group.id != 0)
) and (
  (process.name == "su" and process.args_count <= 2) or
  (process.name == "sudo" and process.args_count == 1) or
  (process.name == "pkexec" and process.args_count == 1) or
  (process.name == "passwd" and process.args_count <= 2)
) and
(
  process.parent.name like (".*", "python*", "perl*", "ruby*", "lua*", "php*", "node", "deno", "bun", "java") or
  process.parent.executable like ("./*", "/tmp/*", "/var/tmp/*", "/dev/shm/*", "/run/user/*", "/var/run/user/*", "/home/*/*") or
  (
    process.parent.name in ("bash", "dash", "sh", "tcsh", "csh", "zsh", "ksh", "fish", "mksh") and
    process.parent.args in ("-c", "-cl", "-lc", "--command", "-ic", "-ci", "-bash", "-sh", "-zsh", "-dash", "-fish", "-ksh") and
    process.parent.args_count <= 4
  )
)

자식 프로세스가 생성되는 것에 의존하지 않고도 ES|QL을 사용하여 익스플로잇 활동을 선제적으로 추적할 수 있습니다. 복사 실패와 더티프래그는 모두 동일한 프로세스에서 인터리빙된 socket(AF_ALG)splice() 시스템 호출을 생성합니다. Copy Fail은 48 바이트를 쓰기 위해 192 바이트를 반복하고, DirtyFrag는 ESP 및 RxRPC 경로에서 비슷한 패턴을 따릅니다.

다음 쿼리는 이러한 시스템 호출을 프로세스별로 집계하고 AF_ALG 또는 AF_RXRPC 소켓과 splice 호출을 볼륨에서 결합하는 루트 프로세스가 아닌 모든 프로세스를 표시합니다:

FROM logs-auditd_manager.auditd-default*
| WHERE host.os.type == "linux" AND user.id != "0" AND
  (
    (event.category == "process" AND auditd.data.syscall == "socket" AND auditd.data.a0 IN ("26", "21")) OR
    (event.category == "process" AND auditd.data.syscall == "splice") OR
    (event.category == "network" AND event.action == "bound-socket" AND auditd.data.socket.family == "38")
  )
| EVAL
    is_af_alg   = CASE(auditd.data.syscall == "socket" AND auditd.data.a0 == "26", 1, 0),
    is_af_rxrpc = CASE(auditd.data.syscall == "socket" AND auditd.data.a0 == "21", 1, 0),
    is_splice   = CASE(auditd.data.syscall == "splice", 1, 0),
    is_bind_alg = CASE(event.action == "bound-socket" AND auditd.data.socket.family == "38", 1, 0)
| STATS
    socket_af_alg   = SUM(is_af_alg),
    socket_af_rxrpc = SUM(is_af_rxrpc),
    splice_count    = SUM(is_splice),
    bind_af_alg     = SUM(is_bind_alg),
    total_calls     = COUNT(*),
    first_seen      = MIN(@timestamp),
    last_seen        = MAX(@timestamp)
  BY host.name, user.name, process.executable, process.pid
| EVAL
    duration_seconds = DATE_DIFF("seconds", first_seen, last_seen),
    distinct_syscalls = CASE(
      socket_af_alg > 0 AND splice_count > 0 AND bind_af_alg > 0, "af_alg+splice+bind",
      socket_af_alg > 0 AND splice_count > 0, "af_alg+splice",
      socket_af_rxrpc > 0 AND splice_count > 0, "af_rxrpc+splice",
      socket_af_alg > 0, "af_alg_only",
      socket_af_rxrpc > 0, "af_rxrpc_only",
      splice_count > 0, "splice_only",
      "other"
    )
| WHERE total_calls >= 10 AND
  (socket_af_alg > 0 OR socket_af_rxrpc > 0) AND
  splice_count > 0
| SORT total_calls DESC
| LIMIT 50

감사 규칙:

감사 통합 구성에 다음 규칙을 추가하여 이러한 익스플로잇 기본 요소에 대한 가시성을 활성화할 수 있습니다:

-a always,exit -F arch=b64 -S socket -k socket_syscall
-a always,exit -F arch=b32 -S socketcall -k socket_syscall
-a always,exit -F arch=b64 -S splice -k splice-syscall
-a always,exit -F arch=b32 -S splice -k splice-syscall
-a always,exit -F arch=b64 -S bind -k socket_bound
-a always,exit -F arch=b32 -S bind -k socket_bound

탐지 규칙 :

완화

탐지는 강화 및 패치와 함께 이루어져야 합니다. 두 취약점 모두에 대한 기본 해결 방법은 배포 패치가 제공되면 Linux 커널을 업데이트하는 것입니다.

즉각적인 패치가 불가능한 경우, 표적 모듈 차단을 통해 공격 표면을 줄일 수 있습니다. 복사 실패의 경우 algif_aead 모듈을 비활성화하면 익스플로잇이 사용하는 AF_ALG AEAD 경로를 차단할 수 있습니다:

echo "install algif_aead /bin/false" > /etc/modprobe.d/copyfail.conf
rmmod algif_aead 2>/dev/null

DirtyFrag의 경우 영향을 받는 네트워킹 모듈을 비활성화하면 ESP와 RxRPC 익스플로잇 경로가 모두 차단됩니다:

printf 'install esp4 /bin/false\ninstall esp6 /bin/false\ninstall rxrpc /bin/false\n' > /etc/modprobe.d/dirtyfrag.conf
rmmod esp4 esp6 rxrpc 2>/dev/null

완화 조치를 적용한 후 페이지 캐시를 삭제하면 이전에 손상된 인메모리 페이지가 모두 삭제됩니다:

echo 3 > /proc/sys/vm/drop_caches

커널 모듈을 비활성화하면 영향을 받는 하위 시스템에 따라 IPsec VPN, 암호화 애플리케이션 또는 기타 서비스에 영향을 줄 수 있으므로 이러한 완화 조치는 프로덕션 배포 전에 스테이징 환경에서 테스트해야 합니다. 페이지 캐시를 삭제하면 일시적인 I/O 스파이크가 발생하므로 부하가 많을 때는 피해야 합니다.

권한이 없는 사용자 네임스페이스 생성을 제한하면 더티프래그 및 이와 유사한 익스플로잇에 대한 방어도 강화됩니다:

sysctl -w kernel.unprivileged_userns_clone=0

RHEL/Fedora에서는 user.max_user_namespaces=0 대신 사용하세요. 이 설정은 특정 컨테이너 런타임 및 브라우저 샌드박스와 같이 권한이 없는 네임스페이스에 의존하는 애플리케이션에 영향을 줄 수 있습니다. 신청하기 전에 호환성을 평가하세요.

참고 자료:

이 문서 공유하기