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