Skip to content

Latest commit

 

History

History
240 lines (197 loc) · 12.4 KB

File metadata and controls

240 lines (197 loc) · 12.4 KB

nf_tables 레지스터 검증 정수 오버플로우로 인한 스택 OOB 쓰기 (CVE-2022-1015)

메타데이터

항목 내용
CVE ID CVE-2022-1015
영향 버전 Linux 5.12 ~ (도입: commit 345023b0db3) ~ 수정: commit 6e1acfa387b9 이전
CVSS v3.1 6.6 (Medium, NVD 기준) — AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:L/A:H (원저자는 코드 실행까지 시연)
공개일 2022-04-29 (NVD), 실제 공개 디스클로저는 2022-03-28 (oss-security)
발견자 David Bouman

개요

nftables(nf_tables)는 사용자가 netlink로 등록한 규칙(rule)을 커널 내부의 작은 "가상 머신" 처럼 표현식(expression) 단위로 실행한다. 각 표현식은 값을 레지스터라는 고정 크기 배열에 읽고 쓰며, 이 레지스터 번호도 사용자가 netlink attribute로 직접 지정한다. CVE-2022-1015는 이 레지스터 번호를 검증하는 nft_validate_register_load/nft_validate_register_store의 경계 검사가 32비트 정수 오버플로우에 취약해서, 사용자가 실제로는 레지스터 배열 범위를 훨씬 벗어나는 인덱스를 검사를 통과시킬 수 있는 취약점이다. unprivileged user/network namespace(CLONE_NEWUSER | CLONE_NEWNET)를 만들 수 있는 로컬 사용자가 이를 이용해 커널 스택의 리턴 주소를 덮어써 임의 코드 실행 및 권한 상승까지 이를 수 있다.

사전 지식

nftables 레지스터와 struct nft_regs

nftables 규칙 평가는 net/netfilter/nf_tables_core.cnft_do_chain()에서 이뤄지며, 표현식들이 값을 주고받는 저장 공간은 스택에 놓인 지역 변수다.

struct nft_regs {
	union {
		u32			data[NFT_REG32_NUM];   /* NFT_REG32_NUM == 20 */
		struct nft_verdict	verdict;
	};
};
unsigned int
nft_do_chain(struct nft_pktinfo *pkt, void *priv)
{
	...
	struct nft_regs regs;   /* 80바이트, nft_do_chain 스택 프레임 위에 존재 */
	...
}

즉 유효한 레지스터 인덱스는 data[0]data[19] (019, 총 80바이트) 뿐이고, 이 범위를 벗어난 인덱스로 regs.data[idx]에 접근하면 nft_do_chain()의 다른 지역 변수나 저장된 리턴 주소 등 인접한 스택 메모리를 침범한다.

레지스터 번호 체계: 레거시 128비트 레지스터와 32비트 레지스터

사용자 공간에서 레지스터를 지정하는 방식은 두 가지가 공존한다 (하위 호환 때문).

  • 레거시: NFT_REG_VERDICT(0), NFT_REG_1NFT_REG_4(14) — 128비트(16바이트) 단위 레지스터.
  • 신규: NFT_REG32_00(8)~NFT_REG32_15(23) — 32비트(4바이트) 단위 레지스터 16개.

커널은 netlink로 들어온 원시 레지스터 번호(reg, enum nft_registers 즉 사실상 int)를 nft_parse_register()에서 내부 배열 인덱스(0~19)로 변환한다.

static unsigned int nft_parse_register(const struct nlattr *attr)
{
	unsigned int reg;

	reg = ntohl(nla_get_be32(attr));
	switch (reg) {
	case NFT_REG_VERDICT...NFT_REG_4:
		return reg * NFT_REG_SIZE / NFT_REG32_SIZE;   /* 0,4,8,12,16 */
	default:
		return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00;  /* reg - 4 */
	}
}

취약점 분석

근본 원인 ①: default 분기가 "그 외 모든 값"을 무검증으로 통과시킴

nft_parse_register()default 분기는 원래 NFT_REG32_00(8)NFT_REG32_15(23)만 받도록 의도됐지만, switch 문에 그 범위를 명시하지 않아서 **04를 제외한 모든 reg 값** (음수 취급되는 큰 값 포함, enum 은 C 표준상 int)이 default 로 들어가 reg - 4 를 그대로 반환한다. 즉 이 시점에서는 사용자가 임의의 32비트 값을 골라 넣을 수 있고, 아직 아무 범위 검사도 없다.

근본 원인 ②: 경계 검사 자체가 32비트 곱셈 오버플로우에 취약

변환된 reg 값은 nft_validate_register_load/nft_validate_register_store에서 검사된다 (패치 전, net/netfilter/nf_tables_api.c).

static int nft_validate_register_store(const struct nft_ctx *ctx,
      enum nft_registers reg, const struct nft_data *data,
      enum nft_data_types type, unsigned int len)
{
	...
	default:
		if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE)  /* reg < 4 */
			return -EINVAL;
		if (len == 0)
			return -EINVAL;
		if (reg * NFT_REG32_SIZE + len >
		    sizeof_field(struct nft_regs, data))            /* reg*4+len > 0x50 */
			return -ERANGE;
		...
	}
}

발견자 David Bouman이 oss-security에 올린 원본 보고서(아래 "참고 자료" 링크)에 따르면, 문제는 enum nft_registers reg가 (C89 3.1.3.3에 의해 int로 취급되므로) 컴파일러가 32비트 값으로 다뤄서, reg * NFT_REG32_SIZE(4를 곱함) 연산이 32비트 안에서 오버플로우할 수 있다는 점이다. 원문이 제시한 실제 수치:

reg = 0xfffffff8, len = 0x40 일 때, reg * 4 + len 을 32비트로 계산하면 0xffffffe0 + 0x40 = 0x20 이 되어 0x50 보다 작다는 조건을 통과한다. 이때 reg의 최하위 바이트인 0xf8(248)이 그대로 "유효한" 레지스터 인덱스로 채택된다.

x86_64에서 GCC는 이 계산을 lea (%r8,%rsi,4),%eax (32비트 lea) 한 줄로 컴파일해 검사를 통과시키는데, 이후 실제로 저장되는 레지스터 번호(*sreg/*dreg, 타입 u8)는 reg의 하위 1바이트 0xf8 그대로다 — struct nft_regs.data[20]의 유효 범위(0~19)를 248이라는 값이 아득히 초과한다.

패치 전 (검사가 32비트 오버플로우로 우회됨)

  사용자 입력 reg = 0xfffffff8, len = 0x40
        │
        ▼
  ┌───────────────────────────────────────────┐
  │ 32비트 연산: reg*4 + len                    │
  │  0xfffffff8*4 = 0xffffffe0 (32비트 wrap)    │
  │  0xffffffe0 + 0x40 = 0x100000020 → 0x20     │  ← 다시 32비트로 잘림
  └───────────────────────────────────────────┘
        │  0x20 < 0x50(sizeof data) → 검사 통과! (실수로 안전하다고 판정)
        ▼
  실제 저장되는 인덱스 = (u8)(reg) = 0xf8 = 248   ← data[20] 범위를 훨씬 벗어남

  struct nft_regs regs;  (nft_do_chain 스택 프레임, 80바이트)
  ┌─────────────────────────────────┐
  │ data[0] ... data[19] (유효 80B) │▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
  └─────────────────────────────────┘▒ 인접 스택(다른 지역변수, 저장된 ▒
                    data[248] ────────▶▒ 리턴 주소 등)까지 침범해서 쓰기 ▒
                                       ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒

공격이 성립하는 경로

David Bouman의 보고서에 따르면 실제 익스플로잇은 다음 순서로 이뤄졌다.

  1. unprivileged user/network namespace를 만들어 root 없이 자신만의 nftables 규칙을 구성한다.
  2. 위와 같은 오버플로우 조합으로 검사를 우회하는 nft_payload 표현식을 등록해, 스택의 레지스터 영역 바깥(리턴 주소 부근 등)에 있는 데이터를 레지스터로 읽어온다.
  3. 특정 바이트 값보다 크면 패킷을 drop, 작으면 accept 하는 규칙을 추가로 걸어, 패킷이 드롭되는지 여부로 그 바이트 값을 한 비트씩 알아내는 이진 탐색 방식의 정보 유출을 수행해 커널 주소(KASLR 무력화 등)를 알아낸다.
  4. 같은 오버플로우 방식으로 이번엔 사용자가 보낸 패킷의 데이터를 OOB 인덱스를 통해 스택에 쓰는 nft_payload 표현식을 등록해, 리턴 주소를 ROP 체인으로 덮어써 커널 코드 실행 흐름을 탈취한다(원저자는 대안으로 verdict 레지스터, 즉 인덱스 0을 같은 방식으로 다시 가리켜 체인 포인터를 조작하는 경로도 언급했다).

수정 방법

수정 커밋 6e1acfa387b9("netfilter: nf_tables: validate registers coming from userspace.", Pablo Neira Ayuso)은 nft_parse_register()를 아예 다시 설계해서, 허용 목록(allow-list) 방식으로 바꿨다 — "이 외의 값은 전부 유효하다"가 아니라 "이 두 범위만 유효하고, 그 외에는 명시적으로 거부"로 뒤집은 것이 핵심이다.

static unsigned int nft_parse_register(const struct nlattr *attr, u32 *preg)
{
	unsigned int reg;

	reg = ntohl(nla_get_be32(attr));
	switch (reg) {
	case NFT_REG_VERDICT...NFT_REG_4:
		*preg = reg * NFT_REG_SIZE / NFT_REG32_SIZE;
		break;
	case NFT_REG32_00...NFT_REG32_15:          /* ← 새로 추가된 명시적 범위 */
		*preg = reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00;
		break;
	default:
		return -ERANGE;                     /* ← 그 외에는 전부 거부 */
	}

	return 0;
}

이 변경만으로도 nft_validate_register_load/store에 도달하는 reg 값 자체가 이미 019(정확히는 legacy 매핑을 거친 0,4,8,12,16 또는 32비트 레지스터 매핑을 거친 419)로 제한되므로, 뒤이어 나오는 reg * NFT_REG32_SIZE + len 계산에 애초에 오버플로우를 일으킬 만큼 큰 reg 값이 들어올 수 없게 된다. 후속 커밋 6c6f9f31ecd4nft_parse_register의 반환형을 unsigned int에서 int로 바꿔 -ERANGE(음수) 오류를 올바르게 전파하도록 정리했다.

패치 후 (허용 목록으로 원천 차단)

  사용자 입력 reg = 0xfffffff8
        │
        ▼
  ┌───────────────────────────────────────────┐
  │ switch(reg):                               │
  │   0..4        → 레거시 매핑                │
  │   8..23       → 32비트 레지스터 매핑        │
  │   그 외(0xfffffff8 포함) → -ERANGE 즉시 반환 │
  └───────────────────────────────────────────┘
        │
        ▼
  nft_validate_register_load/store 는 절대 호출되지 않음
  (오버플로우를 일으킬 만큼 큰 reg 자체가 이 지점을 통과할 수 없다)

  struct nft_regs regs;  (80바이트)
  ┌─────────────────────────────────┐
  │ data[0] ... data[19] (유효 80B) │   ← 이 범위 밖 인덱스는 애초에 발생 불가
  └─────────────────────────────────┘

참고 자료

미해결/불확실 지점

  • blog.dbouman.nl의 상세 익스플로잇 블로그 포스트는 접근은 됐으나, 자동 요약 도구가 반환한 일부 수치(정확한 스택 오프셋 등)가 직접 계산과 맞지 않아 신뢰할 수 없다고 판단해 본문에는 포함하지 않았다. 대신 발견자가 oss-security 메일링 리스트에 직접 올린 1차 보고서(위 참고 자료 첫 항목)의 수치만 사용했다. 필요하면 다음에 블로그 포스트 원문을 브라우저로 직접 열어 대조할 것.
  • CVSS 6.6(Medium)은 NVD가 매긴 점수이지만, 발견자 본인은 임의 코드 실행 및 권한 상승까지 시연했다고 보고서에 명시하고 있어 실질적 심각도는 이 점수보다 높을 수 있다.