
CVE-2020-25578 및 CVE-2020-25579: 2020년에 제가 발견한 일부 FreeBSD 정보 유출 버그입니다.
ufs_create를 감사하는 데 시간을 투자했지만 버그 0개 발견struct dirent 객체의 패딩 바이트를 통해 정보가 유출되는 이 커밋 발견msdosfs_readdir에 대한 패치가 불완전함을 발견. 한 인스턴스의 버그는 패치했지만 두 번째는 패치하지 않음.mqueuefs, autofs, smbfs, tmpfs에서 전체 8바이트 포인터를 유출할 수 있는 변형을 발견. PoC 작성 및 확인.위에서 언급했듯이, 원래 발견한 버그는 위 링크의 패치를 분석하던 중 msdosfs_readdir에서 발견했습니다.
FreeBSD에서 readdir을 호출하는 기본 흐름은 다음과 같습니다:
#include <dirent.h>
int main(void) {
struct dirent *dp;
DIR *dirp;
dirp = opendir("./somedir");
dp = readdir(dirp);
}
somedir이 위치한 파일 시스템에 따라 FreeBSD 커널의 여러 *_readdir 함수 중 하나가 호출될 수 있습니다.
위 패치는 dirent_terminate라는 함수를 추가하여, struct dirent 객체가 사용자 공간으로 반환되기 전(주로 uiomove 함수를 사용하여 수행)에 패딩 바이트와 구조체의 d_name 필드에 남은 바이트를 0으로 설정합니다. struct dirent의 정의는 여기 있습니다.
패치를 보면, 1562번째 줄에서 dirbuf 변수를 인자로 dirent_terminate가 호출되는 것을 볼 수 있습니다. 이후 uiomove가 호출되어 dirbuf의 내용을 사용자 공간으로 복사합니다. 그러나 이 코드 줄들은 이 if 문 블록 안에 있습니다. 이 if 문 위의 주석은 이 분기가 MSDOS 파일 시스템의 루트에서 readdir이 호출될 때만 실행된다고 설명하므로, 파일 시스템의 루트가 아닌 하위 디렉터리에서 readdir을 호출하면 이 if 문을 간단히 건너뛸 수 있습니다.
더 아래로 내려가면, 1691번째 줄에서 uiomove에 대한 또 다른 호출이 있습니다. 하지만 코드를 자세히 읽어보면 이 인스턴스에서는 dirent_terminate가 호출되지 않으며, 이는 패딩 바이트가 초기화되지 않은 상태로 남는다는 것을 의미합니다. 불행히도 d_name 필드는 이 함수의 시작 부분(여기)에서 0으로 설정되었기 때문에 더 큰 유출은 얻을 수 없습니다.
먼저 USB 드라이브가 없었기 때문에 MSDOS 파일 시스템을 마운트할 방법을 찾아야 했습니다. 다음이 작동합니다:
$ dd if=/dev/zero of=test.img bs=512 count=256000
$ sudo mdconfig -a -t vnode -f test.img
$ sudo newfs_msdos -s 131072000 /dev/md1 # My mdconfig returned md1
$ mkdir ./temp
$ sudo mount -t msdosfs /dev/md1 ./temp
$ mkdir ./temp/test_dir
PoC는 original_poc.c에서 찾을 수 있습니다. clang으로 컴파일한 후 위 명령어와 동일한 디렉터리에서 실행하면 유출된 바이트가 출력되는 것을 볼 수 있습니다.
이 버그의 변형을 찾기 시작했습니다. uiomove\(&.*,를 grep한 것 같으며 약 15-20개의 결과가 나왔고, 모두 수동으로 확인했습니다. 불행히도 변형 중 어느 것도 FreeBSD에 기본적으로 존재하지 않습니다(파일 시스템을 수동으로 활성화하거나 커널에 컴파일해야 합니다). 변형이 있는 함수는 다음과 같습니다:
mqfs_readdirtmpfs_dir_getdotdenttmpfs_dir_getdotdotdentsmbfs_readvdirautofs_readdir_one이 모든 함수에서 버그는 정확히 동일하므로, mqfs_readdir만 다루겠습니다.
struct dirent entry가 할당됩니다.dirent_terminate가 호출되어 구조체의 패딩 + d_name 필드를 0으로 설정합니다.vfs_read_dirent가 호출됩니다. 이 함수는 uiomove를 호출하여 구조체를 사용자 공간으로 복사합니다.지금까지는 모든 것이 괜찮아 보이죠? 꼭 그렇지만은 않습니다. 구조체의 모든 필드가 초기화되었는지 확인해야 합니다. 코드를 자세히 보면 d_off 필드가 초기화되지 않은 채 남아 있음을 알 수 있습니다. 이 필드의 타입은 off_t이며, 본질적으로 int64_t입니다. 구조체가 사용자 공간으로 복사될 때 이 필드에서 초기화되지 않은 데이터를 얻게 됩니다.
이 PoC는 모든 변형에 대해 동일하게 작동하며, 다른 파일 시스템에서 실행하기만 하면 됩니다. mqueuefs의 경우 다음을 수행합니다(먼저 mqueuefs를 활성화하거나 커널에 컴파일해야 함):
$ mkdir ./temp
$ sudo mount -t mqueuefs null ./temp
PoC 자체는 variants_poc.c에서 찾을 수 있습니다. clang으로 컴파일한 후 위 명령어와 동일한 디렉터리에서 실행하면 커널 포인터가 출력되는 것을 볼 수 있습니다(아마도 스택 포인터 하나와 코드 섹션/힙 포인터 하나일 것입니다. 확인하지 않았습니다).