
David에 의해 발견되고 문서화된 CVE-2022-1015 및 1016의 스페인어 번역.
이 README.md는 David의 블로그의 번역입니다. David는 Linux 커널에서 CVE-1015와 1016을 발견했습니다. 원본 문서를 읽으려면 그의 웹사이트를 방문하세요.
그의 소셜 미디어는 다음과 같습니다:
2022년 4월 2일 게시됨
이러한 문제는 최신 Ubuntu 및 RHEL의 기본 구성에서 악용될 수 있습니다. CVE-2022-1015의 개념 증명(PoC)은 Arch Linux 커널 버전 5.16-rc3을 대상으로 작성했습니다.
이 문서는 Linux 커널의 기능과 보안에 대한 기본 지식을 가진 사람들을 대상으로 합니다. 네트워크 스택에 대한 지식이 없는 사람도 접근할 수 있도록 이 문서를 최대한 친근하게 만들려고 노력했습니다.
다음은 읽기 가이드입니다:
2월 중순, Google 보안 프로그램은 kCTF 현상금 프로그램을 계속 진행한다고 발표했으며, nsjail 샌드박스 내에서 권한 없는 프로세스에서 root 사용자로 권한을 상승시킬 수 있는 Linux 커널 익스플로잇에 대해 $31,337에서 $91,337에 이르는 현상금을 제공했습니다.
가난한 학생인 저는 당연히 이에 주목했습니다. 이것은 제가 처음으로 "실제 세계"의 취약점을 찾는 시도였지만, 팀과 함께 CTF를 즐기면서 보안 측면에서 Linux 커널에 익숙해졌습니다. 몇 시간이고 거의 진전이 없었지만(대신 Linux에 대한 지식은 더 늘었습니다) 결국 nf_tables 모듈에서 몇 가지 취약점을 찾을 수 있었습니다.
안타깝게도, 결국 이 모듈은 Google kCTF 규칙에 포함되지 않았음을 알게 되었습니다(따라서 이 두 취약점에 대해 현상금을 받지 못했습니다). 하지만 그래도 신고했고 CVE-2022-1015에 대한 LPE(로컬 권한 상승) 익스플로잇을 작성했습니다.
자, 당신은 Linux에서 취약점을 찾기로 결정했습니다. 이제 무엇을 할까요? Linux는 거대한 프로젝트이며, 나무만 보고 숲을 보지 못하기 쉽습니다(세부 사항에 너무 집중하여 정말 중요한 것을 간과하여 상황을 전체적으로 보지 못하는 경우). 더 나쁜 것은 많은 부분이 문서화되지 않아서 무슨 일이 일어나고 있는지 이해하기 위해 엄청난 양의 코드를 읽어야 한다는 점입니다.
저는 Linux의 보안 모델에 대한 상세한 관점을 얻으려고 노력하면서 시작했습니다. 버그를 찾는 것과 좋은 버그를 찾는 것은 별개의 문제입니다. 결국 모든 버그가 동등하게 생성되지는 않습니다:
FS_USERNS_MOUNT를 지정하는 vfe로, 이 경우 사용자 네임스페이스에서 마운트할 수 있습니다.CAP_SYS_ADMIN 또는 CAP_NET_ADMIN을 필요로 합니다.
/proc/config.gz에서 확인할 수 있습니다. 모듈은 내장(=y)되거나 별도로 컴파일되어 런타임에 로드(=m)될 수 있습니다./proc/modules 및 /proc/kallsyms를 사용할 수 있지만, 모듈이 커널에 동적으로 로드될 수 있으므로(예: request_module) 항상 신뢰할 수 있는 것은 아닙니다.이러한 제약 조건은 취약점을 찾을 수 있는 파일 시스템의 범위를 파악하는 데 도움이 됩니다. 원하는 대상에 대한 공격을 계획하는 데 시간을 투자하는 것이 좋은 생각이라고 생각합니다.
저는 위의 요점에 대해 이미 교훈을 얻었습니다. 앞서 언급했듯이 kCTF에서 제공한 인스턴스에는 nf_tables 모듈이 로드되어 있지 않았습니다. 처음부터 이를 알아챘더라면 실망을 피할 수 있었을 텐데요. 반면에, 제가 그것을 미리 알았다면 아마 지금 이 블로그를 읽고 있지 않을 테니, 결국 모든 것이 잘 된 것 같습니다.
COS, Google의 컨테이너 최적화 Linux 포크, 에 nf_tables가 없는 이유에 대한 설명은 여기와 여기에서 찾을 수 있습니다.
위에서 언급한 사항들을 평가한 후, 네트워크 소스 코드를 살펴보는 것이 최선의 시작 경로라고 결정했습니다. 거기에 있는 많은 흥미로운 기능들은 CAP_NET_ADMIN을 필요로 하지만, 앞서 말했듯이 이것은 실제로 문제가 되지 않습니다. 반대로, 저는 특별한 권한이 필요한 구성 요소들이 일반적으로 덜 안전하다고 의심합니다. 커널 개발자들이 잘못된 안전감을 가질 수 있기 때문입니다.
또한 더 알고 싶은 파일 시스템을 선택하기 위해 노력했습니다. 이렇게 하면 버그를 찾지 못하더라도 많은 흥미로운 것들을 배울 수 있습니다.
네트워크 파일 시스템을 많이 조사했지만 중요한 것은 찾지 못했습니다. net/ 하위 디렉토리를 탐색하다가 nf_tables 모듈을 발견했습니다. 약간 복잡해 보여서 시간을 내어 이에 대해 알아보기로 결정했습니다.
Netfilter(net/netfilter)는 커널에서 상당히 큰 네트워크 파일 서브시스템입니다. 간단히 말해, netfilter는 다른 모듈이 핸들러를 등록할 수 있는 네트워크 모듈을 통해 훅을 배치합니다. 훅에 도달하면 제어가 해당 핸들러로 위임되고, 핸들러는 해당 네트워크 패킷 구조에서 작동할 수 있습니다. 핸들러는 패킷을 수락, 드롭 또는 수정할 수 있습니다.
nf_tables API(net/netfilter/nf_tables_api.c)를 몇 시간 탐색하여 정확히 어떻게 작동하는지 알게 된 후, 사용자가 보낸 레지스터에 대한 논리적 검증을 살펴보기로 결정했고 의심스러운 동작을 발견했습니다. 제가 미쳐가고 있는지 아닌지 고민한 후, 찾은 취약점을 트리거하기 위해 작은 PoC(개념 증명)를 작성했습니다. 이 취약점은 OOB(out-of-bounds) 취약점으로, 스택 메모리를 읽고 쓸 수 있게 해줍니다.
커널 주소를 유출할 방법을 찾은 후, 프로그램 카운터를 제어하는 것은 매우 쉬웠습니다. 약간의 ROP(Return-Oriented Programming) 후에 root 권한 셸이 현실이 되었습니다.
표현식의 init 루틴이 넷링크 사용자 메시지에서 레지스터를 파싱해야 할 때마다, 소스 레지스터인지 대상 레지스터인지에 따라 nft_parse_register_load 또는 nft_parse_register_store 루틴이 호출됩니다. 몇 가지 주석을 추가했습니다:```c
int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len)
{
/* Given a netlink attribute and the length
* that is required to read the requested data,
* write a register index to `sreg` or return
* an error on failure. */
u32 reg;
int err;
reg = nft_parse_register(attr);
err = nft_validate_register_load(reg, len);
if (err < 0)
return err;
/* Write resulting index to the nft_expr.data structure. */
*sreg = reg;
return 0;
}
static unsigned int nft_parse_register(const struct nlattr attr) { / Convert a register to an index in nft_regs */
unsigned int reg;
/* Get specified register from netlink attribute */
reg = ntohl(nla_get_be32(attr));
switch (reg) {
/* If it's 0 to 4 inclusive,
* it's an OG 16-byte register and we need to
* multiply the index by 4 (4*4=16) */
case NFT_REG_VERDICT...NFT_REG_4:
return reg * NFT_REG_SIZE / NFT_REG32_SIZE;
/* Else we subtract 4, since we need to account
* for the OG registers above. */
default:
return reg + NFT_REG_SIZE / NFT_REG32_SIZE - NFT_REG32_00;
}
/* So supplied values of 1, 2, 3, 4 map to
* OG 16-byte registers, with indices 4, 8,
* 12, 16
* Supplied values of 5, 6, 7 overlap the verdict,
* 8,9,10,11 overlap with OG register 1
* 12,13,14,15 overlap with OG register 2
* etc. */
}
static int nft_validate_register_load(enum nft_registers reg, unsigned int len) { /* We can never read from the verdict register, * so bail out if the index is 0,1,2,3 */ if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE) return -EINVAL;
/* Invalid operation, bail out */
if (len == 0)
return -EINVAL;
/* If there would be an OOB access whenever
* `reg` is taken as index and `len` bytes are read,
* bail out.
* sizeof_field(struct nft_regs, data) == 0x50 */
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))
return -ERANGE;
return 0;
}
`*_store` 변종들은 거의 동일하지만, 일부 조건에서 *verbdict*에 쓰기를 허용한다는 점이 다릅니다.
마지막 검증을 검토한 후, 여기서 뭔가 정말 잘못되었습니다:```c
if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data))
이것은 정수 오버플로인 것 같지 않나요? reg에 어떤 값을 4배로 곱한 결과 len을 더할 때 오버플로가 발생하도록 할 수 있다면 조건을 만족시킬 수 있습니다.