CVE-2020-7460
라온화이트햇 핵심연구팀 조진호
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;
첫번째 루프로 copyin은 copy_from_user라고 생각하면 된다. 파라미터 생긴거도 똑같이 생겼다. 따라서 코드는 유저의 입력을 받는데, 이 입력은 특정 메시지의 길이다.

이 길이가 바운드를 넘지 않는지 체크를 하고 메모리 할당을 하고 난 뒤 길이를 넣는다. 여러 크기의 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[]; */
};
이 구조체는 컨트롤 메시지 헤더다.

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

첫번째 루프에서 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가지를 해야한다고 한다.
- 연속된
cmsghdr생성 - 한 스레드로
sendmsg()호출 - 다른 스레드로
cmsg_len을 증가
이를 이용하면 몇초 내에 커널패닉을 발생시킬 수 있다.
Primitive
레이스를 할 때 여러 스레드가 돌아가는데 언제까지 돌가야하는지 알 수 없다. 저대로 돌리기만 하면 멈추지 않는 오버플로우가 되어버린다. 이를 해결하기 위한 방법은로 unmap page를 이용했다.

위에서 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()가 호출된다. 정리하자면 아래와 같다.
PushMbuf()로 힙에 채워넣기PopMbuf()로 넣은mbuf중 절반정도 해제한다.- 레이스 취약점인 힙오버로 해제되지 않은
mbuf의ext_free를 덮는다. - 트리거 되었을 때
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