CVE-2020-7460

CVE

라온화이트햇 핵심연구팀 조진호

Summary

freebsd kernel TOCTOU 버그로 아래 ZDI문서를 정리한 것이다. 취약점 패치 로그는 3846ff841491a990212681f423ac8e26c39032b9이며 parent는 b14963930ff962f492bcb8705785ea1e481615a2이다.

https://www.thezdi.com/blog/2020/9/1/cve-2020-7460-freebsd-kernel-privilege-escalation

Vulnerability

취약점은 freebsd/sys/compat/freebsd32/freebsd32_misc.c 파일의 freebsd32_copyin_control라는 함수에 있다. 이 함수는 두개의 반복문으로 이루어져 있다.

// 
// ----------------------- FIRST LOOP ----------------------- 
// 
while (idx < buflen) {
    error = copyin(buf + idx, &msglen, sizeof(msglen));
    if (error)
        return (error);
    if (msglen < sizeof(struct cmsghdr)) 
        return (EINVAL); 
    msglen = FREEBSD32_ALIGN(msglen); 
    if (idx + msglen > buflen) 
        return (EINVAL); 
    idx += msglen; 
    msglen += CMSG_ALIGN(sizeof(struct cmsghdr)) - 
        FREEBSD32_ALIGN(sizeof(struct cmsghdr)); 
    len += CMSG_ALIGN(msglen); 
}

if (len > MCLBYTES) 
    return (EINVAL);

// 
// ALLOCATE KERNEL MEMORY 
// 
m = m_get(M_WAITOK, MT_CONTROL); 
if (len > MLEN) 
    MCLGET(m, M_WAITOK); 
m->m_len = len;

첫번째 루프로 copyincopy_from_user라고 생각하면 된다. 파라미터 생긴거도 똑같이 생겼다. 따라서 코드는 유저의 입력을 받는데, 이 입력은 특정 메시지의 길이다.

/assets/2020-10-01/2010ho.png

이 길이가 바운드를 넘지 않는지 체크를 하고 메모리 할당을 하고 난 뒤 길이를 넣는다. 여러 크기의 cmsg를 할당하면 위와 같은 그림의 메모리 상태가 된다.

// 
// ----------------------- SECOND LOOP ----------------------- 
// 
md = mtod(m, void *); 
while (buflen > 0) { 
    error = copyin(buf, md, sizeof(struct cmsghdr)); 
    if (error) 
        break; 
    msglen = *(u_int *)md; 
    msglen = FREEBSD32_ALIGN(msglen);

    /* Modify the message length to account for alignment. */ 
    *(u_int *)md = msglen + CMSG_ALIGN(sizeof(struct cmsghdr)) - 
        FREEBSD32_ALIGN(sizeof(struct cmsghdr));

    md = (char *)md + CMSG_ALIGN(sizeof(struct cmsghdr)); 
    buf += FREEBSD32_ALIGN(sizeof(struct cmsghdr)); 
    buflen -= FREEBSD32_ALIGN(sizeof(struct cmsghdr));

    msglen -= FREEBSD32_ALIGN(sizeof(struct cmsghdr));
    if (msglen > 0) { 
        error = copyin(buf, md, msglen); // <<-------- OVERFLOW 
        if (error) 
            break; 
        md = (char *)md + CMSG_ALIGN(msglen); 
        buf += msglen; 
        buflen -= msglen; 
    }
}

...

if (error)
		m_free(m);
else
		*mp = m;

그리고 mtod는 위에서 할당한 mbuf를 포인터로 바꾸는건데 이렇게 변환하고 난 md는 포인터로 사용할 수 있다. buflen은 함수에 파라미터로 들어온 컨트롤 메시지(cmsg)배열의 전체 크기이다. 따라서 남은 컨트롤 메시지가 있다면 copyin으로 포인터로 바꿨던 mbuf에 있는 값들을 읽어 위에서 적어놨던 len과 비교하려 에러가 난다면 에러를 반환한다.

struct cmsghdr {
    socklen_t cmsg_len;       /* data byte count, including hdr */
    int       cmsg_level;     /* originating protocol */
    int       cmsg_type;      /* protocol-specific type */ 
    /* u_char cmsg_data[]; */ 
};

이 구조체는 컨트롤 메시지 헤더다.

/assets/2020-10-01/2010ho1.png

에러가 아닌 정상적으로 작동을 해서 두번째 루프에서 길이를 변경시켰다고 한다면 이런 메모리 상태가 된다. 이 사이에서 취약점은 발생할 수 있다.

/assets/2020-10-01/2010ho2.png

첫번째 루프에서 m->len = len;으로 박아버리고 나서 두번째 루프에서 검사를 하고 에러를 하고 마지막에 free를 해버리기 때문에 힙 오버가 발생할 수 있다.

Trigger

트리거는 sendmsg를 이용하는데 이는 힙 스프레이에도 많이 사용하는거다.

ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);

struct msghdr { 
    void         *msg_name;       /* optional address */ 
    socklen_t     msg_namelen;    /* size of address */ 
    struct iovec *msg_iov;        /* scatter/gather array */ 
    int           msg_iovlen;     /* # elements in msg_iov */ 
    void         *msg_control;    /* ancillary data, see below */ 
    socklen_t     msg_controllen; /* ancillary data buffer len */ 
    int           msg_flags;      /* flags on received message */ 
};

여기서 msg_control가 사용자 버퍼가 시작되는 곳인데 이 주변에 cmsghdr이 위치한다고 한다. 트리거 하기 위해서는 다음 3가지를 해야한다고 한다.

이를 이용하면 몇초 내에 커널패닉을 발생시킬 수 있다.

Primitive

레이스를 할 때 여러 스레드가 돌아가는데 언제까지 돌가야하는지 알 수 없다. 저대로 돌리기만 하면 멈추지 않는 오버플로우가 되어버린다. 이를 해결하기 위한 방법은로 unmap page를 이용했다.

/assets/2020-10-01/2010ho3.png

위에서 copyin으로 데이터를 읽는데 unmapped page를 만나면 에러를 출력하므로 이를 이용해 EFAULT가 나오면 우리가 원하는 데이터를 덮고 그 뒤 unmapped page를 만났구나 라는 것을 알 수 있다.

Controlling RIP

먼저 할당되는 mbuf구조체를 보고 가자. mbuf는 freebsd에서 할당한 메모리를 관리하는 기본 유닛 단위라고 보면 된다. 네트워크 패킷이나 소켓 데이터들이 이거로 할당된다.

/* description of external storage mapped into mbuf, valid if M_EXT set */
struct m_ext {
	caddr_t	ext_buf;		/* start of buffer */
	void	(*ext_free)();		/* free routine if not the usual */
	u_int	ext_size;		/* size of buffer, for ext_free */
};

struct mbuf {
	struct	m_hdr m_hdr;
	union {
		struct {
			struct	pkthdr MH_pkthdr;	/* M_PKTHDR set */
			union {
				struct	m_ext MH_ext;	/* M_EXT set */
				char	MH_databuf[MHLEN];
			} MH_dat;
		} MH;
		char	M_databuf[MLEN];		/* !M_PKTHDR, !M_EXT */
	} M_dat;
};

여기서 볼건 ext_free라는 함수 포인터인데 특이하게 구조체가 해제 할 루틴을 구조체 내에 정의해놨다. 저걸 바꿀 수 있으면 rip를 돌릴 수 있다.

먼저 mbuf를 할당해 스프레이를 해야한다. 이 건 udp 클라/서버를 만들어서 sendto()를 호출하여 만들 수 있다. sendto()내에 있는 PushMbuf() 가 새로운 mbuf들을 할당한다. 그리고 recvfrom으로 받을 때 ext_free에 포인터가 있다면 들렸다가 해제한다. 이 과정에서는 PushMbuf()와 반대로 PopMbuf()가 호출된다. 정리하자면 아래와 같다.

Exploit

이제 rip제어까지 완료했다. rip제어에 성공했을 때 free에 들어가게 되니 첫번째 파라미터인 rdi는 mbuf의 위치가 된다. 이 익스에서는 rop를 이용해 트리거했고, cr4를 조작하여 SMEP, SMAP를 풀고 kaslr은 안걸려있는 환경에서 진행했다.

exploit code: https://github.com/thezdi/PoC/blob/master/CVE-2020-7460/CVE-2020-7460.c

CCE 2020 - Maetdol