runc 1.0-rc7 釋出之際

TaoBeier發表於2019-03-31

在 18 年 11 月底時,我寫了一篇文章 《runc 1.0-rc6 釋出之際》 。如果你還不瞭解 runc 是什麼,以及如何使用它,請參考我那篇文章。本文中,不再對其概念和用法等進行說明。

在 runc 1.0-rc6 釋出之時,給版本的別名為 "For Real This Time",當時我們原定計劃是釋出 1.0 的,但是作為基礎依賴軟體,我們認為當時的版本還有幾個問題:

  • 不夠規範;
  • 釋出週期不明確;

為了給相關的 runtime 足夠的時間進行修正/升級,以及規範版本生命週期等,最終決定了釋出 runc 1.0-rc6

為何有 runc 1.0-rc7 存在

前面已經基本介紹了相關背景,並且也基本明確了 rc6 就是在 1.0 正式釋出之前的最後一個版本,那 rc7 為什麼會出現呢?

CVE-2019-5736

我們首先要介紹今年 runc 的一個提權漏洞 CVE-2019-5736

2019 年 2 月 11 日在 oss-security 郵件組正式批露該漏洞,攻擊者可以利用惡意容器覆蓋主機上的 runc 檔案,從而達到攻擊的目的;(具體的攻擊方式此處略過),注意不要輕易使用來源不可信的映象建立容器便可有效避免被攻擊的可能。

簡單補充下可能被攻擊的方式:

  • 執行惡意的 Docker 映象
  • 在主機上執行 docker exec 進入容器內

關於容器安全或者容器的執行機制,其實涉及的點很多,我在去年的一次線上分享 《基於 GitLab 的 CI 實踐》 有提到過 Linux Security Modules(LSM)等相關的內容,對容器安全感興趣的朋友可以對 LSM 多瞭解下。

不過本文主要看的是 runc 如何修復該漏洞的,以及後續產生的影響。

修復方式

// 對 memfd_create 系統呼叫做了個封裝 省略部分程式碼
#if !defined(SYS_memfd_create) && defined(__NR_memfd_create)
#  define SYS_memfd_create __NR_memfd_create
#endif
#ifdef SYS_memfd_create
#  define HAVE_MEMFD_CREATE
#  ifndef MFD_CLOEXEC
#    define MFD_CLOEXEC       0x0001U
#    define MFD_ALLOW_SEALING 0x0002U
#  endif
int memfd_create(const char *name, unsigned int flags)
{
	return syscall(SYS_memfd_create, name, flags);
}

// 一個簡單的只讀快取區
static char *read_file(char *path, size_t *length)
{
	int fd;
	char buf[4096], *copy = NULL;

	if (!length)
		return NULL;

	fd = open(path, O_RDONLY | O_CLOEXEC);
	if (fd < 0)
		return NULL;

	*length = 0;
	for (;;) {
		int n;

		n = read(fd, buf, sizeof(buf));
		if (n < 0)
			goto error;
		if (!n)
			break;

		copy = must_realloc(copy, (*length + n) * sizeof(*copy));
		memcpy(copy + *length, buf, n);
		*length += n;
	}
	close(fd);
	return copy;

error:
	close(fd);
	free(copy);
	return NULL;
}

// 將複製後的 fd 重賦值/執行
static int clone_binary(void)
{
	int binfd, memfd;
	ssize_t sent = 0;

#ifdef HAVE_MEMFD_CREATE
	memfd = memfd_create(RUNC_MEMFD_COMMENT, MFD_CLOEXEC | MFD_ALLOW_SEALING);
#else
	memfd = open("/tmp", O_TMPFILE | O_EXCL | O_RDWR | O_CLOEXEC, 0711);
#endif
	if (memfd < 0)
		return -ENOTRECOVERABLE;

	binfd = open("/proc/self/exe", O_RDONLY | O_CLOEXEC);
	if (binfd < 0)
		goto error;

	sent = sendfile(memfd, binfd, NULL, RUNC_SENDFILE_MAX);
	close(binfd);
	if (sent < 0)
		goto error;

#ifdef HAVE_MEMFD_CREATE
	int err = fcntl(memfd, F_ADD_SEALS, RUNC_MEMFD_SEALS);
	if (err < 0)
		goto error;
#else

	int newfd;
	char *fdpath = NULL;

	if (asprintf(&fdpath, "/proc/self/fd/%d", memfd) < 0)
		goto error;
	newfd = open(fdpath, O_RDONLY | O_CLOEXEC);
	free(fdpath);
	if (newfd < 0)
		goto error;

	close(memfd);
	memfd = newfd;
#endif
	return memfd;

error:
	close(memfd);
	return -EIO;
}

int ensure_cloned_binary(void)
{
	int execfd;
	char **argv = NULL, **envp = NULL;

	int cloned = is_self_cloned();
	if (cloned > 0 || cloned == -ENOTRECOVERABLE)
		return cloned;

	if (fetchve(&argv, &envp) < 0)
		return -EINVAL;

	execfd = clone_binary();
	if (execfd < 0)
		return -EIO;

	fexecve(execfd, argv, envp);
	return -ENOEXEC;
}

複製程式碼

省略掉了部分程式碼,完整程式碼可直接參考 runc 程式碼倉庫

整個的修復邏輯我在上面的程式碼中加了備註,總結來講其實就是:

  • 建立了一個只存在於記憶體中的 memfd ;
  • 將原本的 runc 拷貝至這個 memfd ;
  • 在進入 namespace 前,通過這個 memfd 重新執行 runc ; (這是為了確保之後即使被攻擊/替換也操作的還是記憶體中的這個只讀的 runc)

經過以上的操作,就基本修復了 CVE-2019-5736 。

影響

核心相關

在上面講完修復方式後,我們來看下會產生哪些影響。

  • 涉及到了系統呼叫 memfd_create(2)fcntl(2)

增加了系統呼叫,那自然就要看核心是否支援了。實際上,這些函式是在 2015 年 2 月(距這次修復整整 4 年,也挺有趣)被加入到 Linux 3.17 核心中的。

換句話說就是 凡是在此核心版本之前的系統,均無法正常使用該功能,對我們的影響就是,如果你在此版本核心之前的機器上使用了包含上述修復程式碼的 runc 或構建在其之上的 containerd、 Docker 等都無法正常工作

以 Docker 舉例:安裝 docker-ce-18.09.2 或 docker-ce-18.06.3 可避免受 CVE-2019-5736 影響,但如果核心版本較低,在執行容器時可能會有如下情況出現: (不同版本/核心可能出現其他情況)

[tao@moelove ~]# docker run --rm my-registry/os/debian echo Hello     
docker: Error response from daemon: OCI runtime create failed: container_linux.go:344: starting container
process caused "process_linux.go:293: copying bootstrap data to pipe caused \"write init-p: broken pipe\"": unknown.
複製程式碼
  • 解決辦法

    • 升級核心;這是最直接的辦法,而且使用一個新版本的核心也能省去很多不必要的麻煩:)
    • rancher 提供了一個 runc-cve 的 patch,可相容部分 3.x 核心的系統(我沒有測試過)
    • 如果你不升級 runc/containerd/Docker 等版本的話,那建議你 1. 將 runc 可執行程式放到只讀檔案系統上,可避免被覆蓋;2. 啟動容器時,啟用 SELinux; 3. 在容器內使用低許可權使用者或者採用對映的方式,但保證使用者對主機上的 runc 程式無寫許可權。

注意:

memfd_create 等相關係統呼叫,也被加入到了 Debian 3.16 和 Ubuntu 14.04 updates 中,當然也被反向移植到了 CentOS 7.3 核心 3.10.0-514 版本之後。 (Red Hat 給 CentOS 7.x 的 3.10 核心上反向移植了很多特性)

記憶體相關

從上面的說明中,也很容易可以看到, 記憶體的使用上會有所增加,不過之後已做了修復。這裡不再進行展開。

其他

偶爾可能觸發一些核心 bug 之類的(總之建議升級 :)

等待 rc8 釋出

上面已經介紹了 1.0-rc7 出現的主要原因 CVE-2019-5736;當然這個版本中也有一些新特性和一些 bugfix 不過不是本文的主要內容,不再贅述。

值得一提的是這次的版本命名:runc 1.0-rc7 -- "The Eleventh Hour" 後面這個別名其實來自於一部英劇,感興趣也可以去看看。

至於下個版本是不是會是 1.0 正式版呢?目前來看應該不是,有極大可能會發布 runc 1.0-rc8 做一些 bugfix,讓我們拭目以待。


可以通過下面二維碼訂閱我的文章公眾號【MoeLove】

TheMoeLove

相關文章