一次CTF比赛的杂项解题遇到了一道和二维码相关的题目,但是利用扫描工具扫描不出来结果,随后查阅了二维码的相关资料,定位到了问题出在二维码的校验码上。需要读取二维码的纠错容错位,然后比对两个纠错容错码,最后修复二维码纠错位扫码找到结果。
二维码是一个仅支持校验和纠错的编码,对于纠错功能比较直观的感受就是遮挡了一部分非关键区块之后二维码还可以扫出结果。在计算机网络课程里面多多少少都会提到校验码、纠错码的概念,简单啰嗦一下。校验码是能发现信息代码中存在错误的编码算法,最常见的是CRC算法,在文件校验、网络校验中都有应用。纠错码除了能检查到错误还可以一定程度纠正错误情况保证不出意外的编码,海明码(Hamming Code)是常见的纠错码。
二维码纠错信息存储的位置
二维码纠错信息所在的位置(以25*25大小的二维码为例,不同版本有差异),第一个纠错串信息在左上角定位符附近:
(8, 0),(8, 1),(8, 2),(8, 3),(8, 4),(8, 5),(8, 7),(8, 8),(7, 8),(5, 8),(4, 8),(3, 8),(2, 8),(1, 8),(0, 8)
第二个纠错串信息在右下角定位符附近:
(24, 8),(23, 8),(22, 8),(21, 8),(20, 8),(19, 8),(18, 8),(8, 17),(8, 18),(8, 19),(8, 20),(8, 21),(8, 22),(8, 23),(8, 24)
注意,这里两个纠错字符串的信息必须一致才能进行纠错操作。测试很多工具扫码遇到两个纠错字符串不一致时会忽略,从而识别不出二维码内容。
二维码纠错容错等级
二维码的纠错等级,容错等级。纠错等级是指容错率的大小,按照容错率从小到大可分L(<7%),M(<15%),Q(<25%),H(<30%)。容错率也叫纠错率。纠错率指的就是二维码能被正常扫描时允许被遮挡的最大面积占总面积的比率,确保二维码在被遮挡部分面积后仍能被正常扫描。
纠容错等级 | 纠错串 |
---|---|
L0 | 111011111000100 |
L1 | 111001011110011 |
L2 | 111110110101010 |
L3 | 111100010011101 |
L4 | 110011000101111 |
L5 | 110001100011000 |
L6 | 110110001000001 |
L7 | 110100101110110 |
M0 | 101010000010010 |
M1 | 101000100100101 |
M2 | 101111001111100 |
M3 | 101101101001011 |
M4 | 100010111111001 |
M5 | 100000011001110 |
M6 | 100111110010111 |
M7 | 100101010100000 |
Q0 | 011010101011111 |
Q1 | 011000001101000 |
Q2 | 011111100110001 |
Q3 | 011101000000110 |
Q4 | 010010010110100 |
Q5 | 010000110000011 |
Q6 | 010111011011010 |
Q7 | 010101111101101 |
H0 | 001011010001001 |
H1 | 001001110111110 |
H2 | 001110011100111 |
H3 | 001100111010000 |
H4 | 000011101100010 |
H5 | 000001001010101 |
H6 | 000110100001100 |
H7 | 000100000111011 |
解题代码
图片本身有白色外边,先进行裁边,返回图片实际像素数量和一个信息编码位的比例。
def position(stdsize=25):
from PIL import Image
qr = Image.open('key.png')
x, y = qr.size
start = x
end = 0
for i in range(x):
for j in range(y):
if qr.getpixel((i, j)) != 1:
start = min(i, j, start)
end = max(i, j, end)
img2 = qr.crop((start + 1, start + 1, end + 1, end + 1))
proportion = (end - start) // stdsize
return img2, proportion
还原信息到当前二维码25*25大小的数组上,并输出当前二维码的两个纠错容错等级。
dic = {
"L0": "111011111000100",
"L1": "111001011110011",
"L2": "111110110101010",
"L3": "111100010011101",
"L4": "110011000101111",
"L5": "110001100011000",
"L6": "110110001000001",
"L7": "110100101110110",
"M0": "101010000010010",
"M1": "101000100100101",
"M2": "101111001111100",
"M3": "101101101001011",
"M4": "100010111111001",
"M5": "100000011001110",
"M6": "100111110010111",
"M7": "100101010100000",
"Q0": "011010101011111",
"Q1": "011000001101000",
"Q2": "011111100110001",
"Q3": "011101000000110",
"Q4": "010010010110100",
"Q5": "010000110000011",
"Q6": "010111011011010",
"Q7": "010101111101101",
"H0": "001011010001001",
"H1": "001001110111110",
"H2": "001110011100111",
"H3": "001100111010000",
"H4": "000011101100010",
"H5": "000001001010101",
"H6": "000110100001100",
"H7": "000100000111011"}
l1 = [
(8, 0),
(8, 1),
(8, 2),
(8, 3),
(8, 4),
(8, 5),
(8, 7),
(8, 8),
(7, 8),
(5, 8),
(4, 8),
(3, 8),
(2, 8),
(1, 8),
(0, 8)]
l2 = [
(-1, 8),
(-2, 8),
(-3, 8),
(-4, 8),
(-5, 8),
(-6, 8),
(-7, 8),
(8, -8),
(8, -7),
(8, -6),
(8, -5),
(8, -4),
(8, -3),
(8, -2),
(8, -1)]
def reshape(img2, sub_step):
sub_step = step
result = []
result2 = []
x, y = img2.size
for i in range(0, x, sub_step):
tmpl = []
tmpl2 = []
for j in range(0, y, sub_step):
tmpl.append(1 - img2.getpixel((j, i)))
tmpl2.append(img2.getpixel((j, i)))
result.append(tmpl)
result2.append(tmpl2)
f1 = ''.join([str(result[i[0]][i[1]]) for i in l1])
f2 = ''.join([str(result[i[0]][i[1]]) for i in l2])
for k, v in dic.items():
if v == f1:
print(f1, k)
if v == f2:
print(f2, k)
return result2
最后有两种办法,可以暴力枚举纠错容错等级或者用较大的纠错容错等级替换纠错容错位修复图片(测试用L4可以扫出来),最后调用zxing扫描输出结果。
def fix(result2):
import numpy as np
import matplotlib.pyplot as plt
import zxing
for a, b in enumerate(dic['L4']):
result2[l1[a][0]][l1[a][1]] = 1 - int(b)
result2[l2[a][0]][l2[a][1]] = 1 - int(b)
gray_array = np.array(result2)
plt.imshow(gray_array, cmap='Greys_r') # 转灰度图
plt.axis('off') # 不显示坐标轴
plt.imsave('final.png', gray_array, cmap='Greys_r')
new_qr = zxing.BarCodeReader()
pw = new_qr.decode('final.png')
pw = pw.parsed
print(pw)
a1, a2 = position()
a3 = liupi(a1, a2)
fix(a3)