flawopen.com/Teardowns/cve-2015-3824-android-stagefright-tx3g-integer-overflow

● CVE-2015-3824 · CVSS v2 10.0 · 높음
보안 연구 · FlawOpen

CVE-2015-3824: Android libstagefright의 Stagefright tx3g 정수 오버플로

MPEG-4 자막 아톰 tx3g를 파싱하는 중 검사하지 않은 크기 덧셈이 한 바퀴 돌아 아주 작은 힙 버퍼를 할당했고, 조작된 동영상이 사용자 조작 없이 mediaserver의 메모리를 손상시킬 수 있었던 방식.

💡 알기 쉬운 설명 (ELI5)

현수막 길이를 적는 칸이 네 자리밖에 없는 인쇄소 주문서를 떠올려 보세요. 이미 9,998미터가 기록되어 있는데 5미터를 더 주문합니다. 직원이 합계를 네 칸에 적자 숫자가 한 바퀴 돌아 0003이 되고, 인쇄소는 3미터짜리 종이를 자릅니다. 그런데 인쇄기는 10,003미터 분량의 글자를 그 종이에 계속 찍어 내고, 잉크가 책상 위 다른 손님들의 주문서까지 번집니다. Stagefright는 메모리에서 똑같은 일을 했습니다. 크기의 합이 한 바퀴 돌아 작은 버퍼만 할당되었고, 그 안에 동영상 데이터가 모두 쓰였습니다.

핵심 개념 및 용어

Stagefright(libstagefright)
특권 프로세스 mediaserver 안에서 오디오·비디오 파일을 파싱하는 Android 네이티브 미디어 라이브러리.
MPEG-4 아톰
MP4 파일을 이루는 자체 설명형 블록으로, 선언된 크기와 네 글자 유형, 데이터로 구성된다. 파서는 선언된 크기를 믿고 읽을 양을 정한다.
tx3g
텍스트 트랙의 자막 서식을 담는 3GPP 타임드 텍스트 아톰.
정수 오버플로
계산 결과가 정수형의 범위를 넘어 작은 값으로 되돌아가는 현상. 예를 들어 SIZE_MAX + 2가 1이 된다.

근본 원인 분석 (Root Cause)

MPEG4Extractor::parseChunk()는 이미 저장된 서식 데이터의 크기를 공격자가 제어하는 tx3g 아톰의 chunk_size에 더하면서 오버플로를 검사하지 않았다. 한 바퀴 돈 합계로 힙 할당 크기가 정해졌고, 이어서 readAt()이 chunk_size 바이트 전체를 그 안에 복사했다.

단계별 공격 실행 흐름

Step 1

조작된 동영상이 전달된다

공격자는 tx3g 아톰의 chunk_size를 최댓값 근처로 선언한 MP4 파일을 MMS나 메신저로 보낸다.

Step 2

파싱이 자동으로 시작된다

Android 5.1 이하에서는 미디어 서비스가 미리 보기와 썸네일을 만들기 위해 받은 미디어를 자동으로 파싱했기 때문에 탭할 필요가 없었다.

Step 3

크기 계산이 한 바퀴 돈다

size + chunk_size가 오버플로되어 new[]가 몇 바이트짜리 버퍼만 할당한다.

Step 4

힙이 덮어써진다

readAt()이 chunk_size 바이트를 작은 버퍼에 복사해 mediaserver의 힙을 손상시키고, 공격자는 그 프로세스 권한으로 코드를 실행한다.

소스 코드 비교: 취약한 구현 vs 보안 패치

취약한 구현
// Simplified from MPEG4Extractor::parseChunk() in libstagefright, before the fix
case FOURCC('t', 'x', '3', 'g'):
{
    uint32_t type;
    const void *data;
    size_t size = 0;
    if (!mLastTrack->meta->findData(kKeyTextFormatData, &type, &data, &size)) {
        size = 0;
    }

    // chunk_size is read from the file. size + chunk_size can wrap around,
    // so a tiny buffer is allocated for a huge copy.
    uint8_t *buffer = new (std::nothrow) uint8_t[size + chunk_size];
    if (buffer == NULL) {
        return ERROR_MALFORMED;
    }
    if (size > 0) {
        memcpy(buffer, data, size);
    }
    // Copies the full chunk_size bytes into the undersized buffer.
    if ((size_t)mDataSource->readAt(*offset, buffer + size, chunk_size) < chunk_size) {
        delete[] buffer;
        return ERROR_IO;
    }
    mLastTrack->meta->setData(kKeyTextFormatData, 0, buffer, size + chunk_size);
    delete[] buffer;
    *offset += chunk_size;
    break;
}
보안 강화 패치
// Simplified from MPEG4Extractor::parseChunk() in libstagefright, after the fix
case FOURCC('t', 'x', '3', 'g'):
{
    uint32_t type;
    const void *data;
    size_t size = 0;
    if (!mLastTrack->meta->findData(kKeyTextFormatData, &type, &data, &size)) {
        size = 0;
    }

    // Reject any length whose sum would overflow before allocating.
    if (chunk_size < 0 || (uint64_t)chunk_size > SIZE_MAX - size) {
        return ERROR_MALFORMED;
    }

    uint8_t *buffer = new (std::nothrow) uint8_t[size + chunk_size];
    if (buffer == NULL) {
        return ERROR_MALFORMED;
    }
    if (size > 0) {
        memcpy(buffer, data, size);
    }
    if ((size_t)mDataSource->readAt(*offset, buffer + size, chunk_size) < chunk_size) {
        delete[] buffer;
        return ERROR_IO;
    }
    mLastTrack->meta->setData(kKeyTextFormatData, 0, buffer, size + chunk_size);
    delete[] buffer;
    *offset += chunk_size;
    break;
}

엔지니어링 및 시스템 보안 강화 체크리스트

출처