CVE-2021-43267 번역

CVE
Analysis

라온화이트햇 핵심연구팀 조진호

CVE-2021-43267 번역

Summary

리눅스 커널 5.10-rc1 ~ 5.15사이에서 터지는 힙 취약점 버그. 커널의 TPIC(Transparent Inter Process Communication)에서 취약점이 발생한다.

Executive Summary

먼저 취약점을 찾은 사람은 CodeQL을 이용해 취약점을 찾았다고 한다.

import cpp

from FunctionCall fc // 모든 함수 호출 로부터
where fc.getTarget().getName() = "kmalloc" // kmalloc 찾기
and fc.getArgument(0).getType().getSize() = 2 // kmalloc의 첫번쨰 파라미터가 16bit int
select fc, fc.getLocation()

kmalloc으로 할당하는 힙 중에서 16bit int만 찾는 이유는 32비트나 64비트보다 오버플로우 하기 쉬울거라고 생각했다고 한다.

총 60개의 결과중에 특별한 결과물이 하나 있다. (net/tipc/crypto.c)

static bool tipc_crypto_key_rcv(struct tipc_crypto *rx, struct tipc_msg *hdr) // (1)
{
	struct tipc_crypto *tx = tipc_net(rx->net)->crypto_tx;
	struct tipc_aead_key *skey = NULL;
	u16 key_gen = msg_key_gen(hdr);
	u16 size = msg_data_sz(hdr);                                               // (2)
	u8 *data = msg_data(hdr);

/* ... */
	/* Allocate memory for the key */
	skey = kmalloc(size, GFP_ATOMIC);                                          // (3)
	if (unlikely(!skey)) {
		pr_err("%s: unable to allocate memory for skey\n", rx->name);
		goto exit;
	}

	/* Copy key from msg data */
	skey->keylen = ntohl(*((__be32 *)(data + TIPC_AEAD_ALG_NAME)));           // (4)
	memcpy(skey->alg_name, data, TIPC_AEAD_ALG_NAME);
	memcpy(skey->key, data + TIPC_AEAD_ALG_NAME + sizeof(__be32),
			skey->keylen);                                                    // (5)

	/* Sanity check */
	if (unlikely(size != tipc_aead_key_size(skey))) {                         // (6)
		kfree(skey);
		skey = NULL;
		goto exit;
	}
/* ... */
}

이 함수는 받은 데이터를 파싱하는거 처럼 보이며 (2)에서 얻은 크기로 (3)에서 skey를 만들고 (4)와 (5)에서 아무런 사이즈 검증 절차가 없다. 따라서 skey를 오버플로우 할 수 있다.

Linux TIPC Protocol

TIPC프로토콜은 클러스터링 환경에서 많은 수의 노드간의 통신에 주로 사용한다. 프로토콜 처리의 대부분은 리눅스 커널 모듈 패키지에서 실행되며 사용자에게서 로드될 때 소켓과 같이 사용한다. 또한 권한이 없는 사용자는 netlink call로 사용할 수 있다고 한다.

TIPC는 이더넷과 UDP같은 방식을 설정할 수 있는데 (UDP는 포트 6118)권한 없는 사용자는 net관련 권한이 없을 수 있으니 LPE에는 UDP를 사용하면 된다.

TIPC는 분리된 주소 형태를 가질 수 있다. 또한 사용자에게서 투명한 작동 방식을 가지고 있다고 하는데 이는 아마 모든 과정이 커널 내부에서 이루어져서 이렇게 말하는 것 같다. 메시지 파싱 부분도 커널 내부에서 하는데 이런 모양이다.

Untitled

여기서 exploit에 사용되는 부분은 Header sizeMessage size이다.

Header size가 헤더의 크기를 말하는 것이고 Message size가 헤더를 포함한 전체 TIPC메시지 크기이다.

이 두개의 값은 tipc_msg_validate에서 검증한다. (/net/tipc/msg.c)

bool tipc_msg_validate(struct sk_buff **_skb)
{
  struct sk_buff *skb = *_skb;
  struct tipc_msg *hdr;
  int msz, hsz;

/* ... */

  hsz = msg_hdr_sz(buf_msg(skb));
  if (unlikely(hsz < MIN_H_SIZE) || (hsz > MAX_H_SIZE))
      return false;

/* ... */

  hdr = buf_msg(skb);

/* ... */

  msz = msg_size(hdr);
  if (unlikely(msz < hsz))
      return false;
  if (unlikely((msz - hsz) > TIPC_MAX_USER_MSG_SIZE))
      return false;
  if (unlikely(skb->len < msz))
      return false;

  TIPC_SKB_CB(skb)->validated = 1;

  return true;
}

Message sizeHeader size보다 큰가? 를 검증하고 Header size의 크기도 검증하고 있다.

TIPC 취약점

새로운 tipc message type인 MSG_CRYPTO가 2020년에 나왔다.

struct tipc_aead_key {
	char alg_name[TIPC_AEAD_ALG_NAME];
	unsigned int keylen; 	/* in bytes */
	char key[];
 };

TIPC_AEAD_ALG_NAME는 32다. 이 메시지가 들어오면 TIPC 커널 모듈은 이 정보를 노드 스토리지에 저장해야한다.

/* Allocate memory for the key */
  skey = kmalloc(size, GFP_ATOMIC);
/* ... */

  /* Copy key from msg data */
  skey->keylen = ntohl(*((__be32 *)(data + TIPC_AEAD_ALG_NAME)));
  memcpy(skey->alg_name, data, TIPC_AEAD_ALG_NAME);
  memcpy(skey->key, data + TIPC_AEAD_ALG_NAME + sizeof(__be32),
         skey->keylen);

Header sizeMessage size는 위 validate에서 검증하지만 keylen을 만드는 MSG_CRYPTO와 TIPC_AEAD_ALG_NAME에 대해서는 검증을 하지 않는다. 공격자는 작은 Message size를 만들어 힙을 할당받고, keylen에 임의의 값을 넣어 힙 오버플로우 취약점을 트리거 할 수 있게된다.

Untitled

Exploitability

취약점은 로컬과 리모트 둘다 되며, 로컬에서 힙 컨트롤을 더 잘 할 수 있어서 로컬이 더 잘된다.

오버플로우를 할 떄 메시지 사이즈 체크 부분을 보면 메시지 크기가 패킷보다 큰지만 체크해서 20바이트 크기의 패킷을 만들고 메시지 크기를 10바이트로 해도 정상적으로 사용 가능함을 알 수 있다.

if (unlikely(skb->len < msz))
    return false;

Patch

net/tipc/crypto.c | 32 +++++++++++++++++++++-----------
 1 file changed, 21 insertions(+), 11 deletions(-)

 diff --git a/net/tipc/crypto.c b/net/tipc/crypto.c
 index c9391d38d..dc60c32bb 100644
 --- a/net/tipc/crypto.c
 +++ b/net/tipc/crypto.c
 @@ -2285,43 +2285,53 @@ static bool tipc_crypto_key_rcv(struct tipc_crypto *rx, struct tipc_msg *hdr)
   u16 key_gen = msg_key_gen(hdr);
   u16 size = msg_data_sz(hdr);
   u8 *data = msg_data(hdr);

 +   unsigned int keylen;
 +
 +   /* Verify whether the size can exist in the packet */
 +   if (unlikely(size < sizeof(struct tipc_aead_key) + TIPC_AEAD_KEYLEN_MIN)) {
 +       pr_debug("%s: message data size is too small\n", rx->name);
 +       goto exit;
 +   }
 +
 +   keylen = ntohl(*((__be32 *)(data + TIPC_AEAD_ALG_NAME)));
 +
 +   /* Verify the supplied size values */
 +   if (unlikely(size != keylen + sizeof(struct tipc_aead_key) ||
 +            keylen > TIPC_AEAD_KEY_SIZE_MAX)) {
 +       pr_debug("%s: invalid MSG_CRYPTO key size\n", rx->name);
 +       goto exit;
 +   }

     spin_lock(&rx->lock);
     if (unlikely(rx->skey || (key_gen == rx->key_gen && rx->key.keys))) {
	 pr_err("%s: key existed <%p>, gen %d vs %d\n", rx->name,
		rx->skey, key_gen, rx->key_gen);
 -        goto exit;
 +        goto exit_unlock;
     }
     /* Allocate memory for the key */
     skey = kmalloc(size, GFP_ATOMIC);
     if (unlikely(!skey)) {
	 pr_err("%s: unable to allocate memory for skey\n", rx->name);
 -        goto exit;
 +        goto exit_unlock;
     }

     /* Copy key from msg data */
 -   skey->keylen = ntohl(*((__be32 *)(data + TIPC_AEAD_ALG_NAME)));
 +   skey->keylen = keylen;
    memcpy(skey->alg_name, data, TIPC_AEAD_ALG_NAME);
    memcpy(skey->key, data + TIPC_AEAD_ALG_NAME + sizeof(__be32),
			skey->keylen);

 -   /* Sanity check */
 -   if (unlikely(size != tipc_aead_key_size(skey))) {
 -       kfree(skey);
 -       skey = NULL;
 -       goto exit;
 -   }
 -
    rx->key_gen = key_gen;
    rx->skey_mode = msg_key_mode(hdr);
    rx->skey = skey;
    rx->nokey = 0;
    mb(); /* for nokey flag */

 - exit:
 + exit_unlock:
    spin_unlock(&rx->lock);
 + exit:
    /* Schedule the key attaching on this crypto */
    if (likely(skey && queue_delayed_work(tx->wq, &rx->work, 0)))
	return true;
--
2.31.1

패치는 keylen을 검증하는 코드가 추가되었다.