[하루한줄] CVE-2024-12085: rsync의 Uninitialized Stack Contents로 인한 Info Leak

URL

Target

  • rsync < 3.4.0

Explain

최근 구글 연구원들이 발견한 rsync의 취약점 6개가 공개되었습니다. 오늘은 그 중 CVE-2024-12085 취약점에 대해 공부해보려고 합니다.

CVE-2024-12085 취약점은 초기화하지 않은 스택 변수를 통해 메모리를 유출할 수 있는 취약점입니다.

/* sum2를 스택 위에 선언하지만, 아래에서 일부 길이만 채워질 수도 있음 */
char sum2[MAX_DIGEST_LEN];
...
int done_csum2 = 0;
...
if (!done_csum2) {
    map = (schar *)map_ptr(buf, offset, l);
    get_checksum2((char *)map, l, sum2);
    done_csum2 = 1;
}
/* 바로 아래와 같은 비교를 할 때, s->s2length가 실제 sum2가 계산된 길이보다 크면
   미초기화된 sum2의 나머지를 참조하게 됨 */
if (memcmp(sum2, sum2_at(s, i), s->s2length) != 0) {
    false_alarms++;
    continue;
}

sum2 버퍼를 충분히(또는 전혀) 초기화하지 않은 상태에서 memcmp(sum2, sum2_at(s, i), s->s2length) 비교를 수행해, s->s2length 값이 실제 체크섬 길이보다 크게 세팅될 경우(조작될 경우) sum2 배열의 미초기화 영역을 참조하게 됩니다. 이 과정에서 스택에 남아 있던 1바이트(또는 그 이상이 될 수 있다고 생각하는데 취약점 설명에는 1바이트만 leak된다고 합니다)가 비교 과정에서 밖으로 leak될 수 있습니다.

  1. char sum2[MAX_DIGEST_LEN];로 선언된 지역 배열이 전부 0으로 초기화되지 않습니다.
  2. get_checksum2() 함수가 실제로 해시를 계산해 sum2에 쓰는 길이는 해시 알고리즘에 따라 고정되거나 제한됩니다(예: 16바이트, 20바이트 등).

    • get_checksum2()는 다음 과정을 거칩니다:

      /* The "sum" buffer must be at least MAX_DIGEST_LEN bytes! */
      void get_checksum2(char *buf, int32 len, char *sum)
      {
          /* ... */
      
          switch (xfer_sum_nni->num) {
            case CSUM_XXH64:
                /* 8바이트만 씀 */
                SIVAL64(sum, 0, XXH64(buf, len, checksum_seed));
                break;
      
            case CSUM_XXH3_128:
                /* 16바이트만 씀 (low64 + high64) */
                XXH128_hash_t digest = XXH3_128bits_withSeed(buf, len, checksum_seed);
                SIVAL64(sum, 0, digest.low64);
                SIVAL64(sum, 8, digest.high64);
                break;
      
            case CSUM_MD5:
                /* md5_result()로 16바이트 작성 */
                md5_result(&m5, (uchar *)sum);
                break;
      
            /* ... */
          }
      }
      • 해시 알고리즘에 따라 고정된 길이 혹은 그에 준하는 길이의 데이터를 sum 버퍼에 쓴다. 예를 들어 XXH64면 8바이트, XXH3_128이면 16바이트, MD5나 MD4도 16바이트를 결과값으로 씁니다.
      • 그 뒤 나머지 바이트(버퍼의 최대 길이인 MAX_DIGEST_LEN에서 실제 해시 길이를 뺀 나머지)는 전혀 초기화하지 않습니다.
  3. s->s2length가 조작되거나 의도치 않게 실제 해시 길이보다 클 경우, memcmp(sum2, sum2_at(s, i), s->s2length) 호출 시 sum2 배열 중 계산된 해시가 쓰이지 않은 나머지 영역(미초기화 영역)을 비교하게 됩니다.
  4. 결국 스택에 남아 있던 데이터(최대 s2length - 해시 실제 길이만큼)가 유출될 가능성이 생깁니다.

해당 취약점은 함수 시작 부분에 sum2 변수를 초기화하는 코드를 추가하는 것으로 패치되었습니다.

@@ -147,6 +147,9 @@ static void hash_search(int f,struct sum_struct *s,
	int more;
	schar *map;

	+ // prevent possible memory leaks
	+ memset(sum2, 0, sizeof sum2);

	/* want_i is used to encourage adjacent matches, allowing the RLL
	 * coding of the output to work more efficiently. */
	want_i = 0;