一次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)