題目資訊

Writeup


FIND MY SIZE

分類:Forensics

難度:Medium

我收到一個奇怪的檔案,你可以幫我看看這是怎麼回事嗎?


題目給了一個png圖片檔案。

很顯然的這張圖片的長、寬被改掉了。因此我們首要之務是找回原本正確的長、寬,並將檔案修復。

IHDR數據塊

每一個png檔案,都以89 50 4E 47 0D 0A 1A 0A開始,隨後由若干個數據塊所組成。數據塊分為以下幾種:(取自參考資料1.PNG - CTF Wiki)

數據塊符號 數據塊名稱 多數據塊 可選否 位置限制
IHDR 文件頭數據塊 第一塊
cHRM 基色和白色點數據塊 在 PLTE 和 IDAT 之前
gAMA 圖像γ數據塊 在 PLTE 和 IDAT 之前
sBIT 樣本有效位數據塊 在 PLTE 和 IDAT 之前
PLTE 調色板數據塊 在 IDAT 之前
bKGD 背景顏色數據塊 在 PLTE 之後 IDAT 之前
hIST 圖像直方圖數據塊 在 PLTE 之後 IDAT 之前
tRNS 圖像透明數據塊 在 PLTE 之後 IDAT 之前
oFFs (專用公共數據塊) 在 IDAT 之前
pHYs 物理像素尺寸數據塊 在 IDAT 之前
sCAL (專用公共數據塊) 在 IDAT 之前
IDAT 圖像數據塊 與其他 IDAT 連續
tIME 圖像最後修改時間數據塊 無限制
tEXt 文本信息數據塊 無限制
zTXt 壓縮文本數據塊 無限制
fRAc (專用公共數據塊) 無限制
gIFg (專用公共數據塊) 無限制
gIFt (專用公共數據塊) 無限制
gIFx (專用公共數據塊) 無限制
IEND 圖像結束數據 最後一個數據塊

而每個數據塊又由4個部分組成(取自參考資料1.PNG - CTF Wiki):

名稱 字節數 說明
Length(長度) 4 bytes 指定數據塊中數據域的長度,其長度不超過(231-1)字節
Chunk Type Code(數據塊類型碼) 4 bytes 數據塊類型碼由 ASCII 字母(A - Z 和 a - z)組成
Chunk Data(數據塊數據) 可變長度 存儲按照 Chunk Type Code 指定的數據
CRC(循環冗餘檢測) 4 bytes 存儲用來檢測是否有錯誤的循環冗餘碼

其中IHDR的數據塊數據(Header Chunk)由13個bytes組成,包含了這個png檔案的基本資訊,其中就包含圖片的長度、寬度。具體結構如下(擷取自參考資料2.png格式分析与压缩原理 - 一只安静的猫):

名稱 字節數 說明
Width 4 bytes 影像寬度,以像素為單位
Height 4 bytes 影像高度,以像素為單位
Bit depth 1 byte 图像深度
ColorType 1 byte 顏色類型
Compression method 1 byte 壓縮方法
Filter method 1 byte 濾波器方法
Interlace method 1 byte 隔行掃描方法

註:部分內容有精簡過,有興趣者可見參考資料2.。

我們用題目的檔案來實際看一次

IHDR結構.png

  • 白色框框:文件開頭的89 50 4E 47 0D 0A 1A 0A

  • 黃色框框:IHDR的Length,為0d,換成十進位也就是13,即前述IHDR的數據長度為13 bytes

  • 橘色框框:IHDR的Chunk Type Code,即49 48 44 52ASCII解碼即為IHDR

  • IHDR的Chunk Data

    • 藍色框框:圖片寬度

    • 靛色框框:圖片長度

  • 紫色框框:CRC循環冗餘檢測

如果一個圖片的長、寬被更改了,但CRC沒改,那圖片顯示器就會發現錯誤,有些軟體就不會顯示圖片(但Windows 11內建的相片軟體會顯示)。此時,最簡單的方法便是對長、寬進行爆破,並每次自行計算CRC,看跟檔案中的CRC是否一樣,如果一樣就代表正確。

那我們該怎麼找回長、寬呢?此外,pngcheck的兩個warning是什麼意思呢?

過濾器

以下假設圖片長度為HH,寬度為WW,每個像素占BB個bytes,圖片數據流總共SS bytes。

png的圖片數據(在IDAT裡)是一行一行儲存的,每一行的開頭都有1 byte用來指定該行的過濾器類型(Filter Type)(取值為0, 1, 2, 3, 4),接著才是數據內容(Pixel Data)(長度為W×BW \times B bytes)。(詳見參考資料2.png格式分析与压缩原理 - 一只安静的猫中的5.过滤器)

註:並不是一行一個IDAT喔!

因此,我們有

S=(1+W×B)×HH=S1+W×BS=(1+W\times B)\times H\Rightarrow H=\frac{S}{1+W\times B}

所以知道WW就會知道HH,且1+W×B1+W\times B整除SS

因此此題的解法如下:

對寬度WW進行爆破,對於每個WW,可以將數據切成若干行,每行有1+W×B1+W\times B bytes。接著我們依序檢查是否每行的開頭第一個byte都是0, 1, 2, 3, 4的其中之一(因為過濾器類型(Filter Type)取值只能是0, 1, 2, 3, 4)。如果檢查到任一行不對,就代表這個寬度是錯誤的。

如此便能篩選出可能的WW,並以此算出HH,隨後用16進位編輯器修改,用肉眼看哪個是正確的。

我們剩下的最後一道檻是知道BB是多少(SS只要寫腳本提取出IDAT段內容看長度多少即可),不過由前面pngcheck跑出來的結果我們知道這是一張32-bit RGB+alpha的照片,因此B=32 bit8 bit=4B=\frac{32\space \rm{bit}}{8\space\rm{bit}}=4

Script

撰寫Script如下:

#!/usr/bin/python3
import zlib
import struct

# 讀取 IDAT 數據
with open('FIND_MY_SIZE.png', 'rb') as f:
    content = f.read()
    idat_start = content.find(b'IDAT') + 4
    idat_end = content.find(b'IEND') - 4
    # 提取數據並解壓
    raw_data = zlib.decompress(content[idat_start:idat_end])

# 爆破寬度 (Width)
# 32-bit RGB+alpha 代表一個像素 4 bytes
for w in range(700, 900):
    row_size = w * 4 + 1
    # 檢查每一行的第一個 byte (Filter Type) 是否都在 0-4 之間
    is_valid = True
    for i in range(0, len(raw_data), row_size):
        if raw_data[i] > 4:
            is_valid = False
            break
    if is_valid and len(raw_data) % row_size == 0:
        h = len(raw_data) // row_size
        print(f"可能的(寬度,高度): ({hex(w)}, {hex(h)})")

我操作檔案內容相關的Python不好,所以腳本基本上是Gemini生的

但腳本內容應該很好理解

執行之後輸出

可能的(寬度,高度): (0x34d, 0x17e)

我們用任何一個16進位編輯軟體(如果你用Windows,我推薦HxD,簡單明瞭)修改長、寬,然後再pngcheck我們修改完的檔案,讓pngcheck幫我們算出正確的CRC

得到CRC為5e72f106。再次用16進位編輯器修改即可。接著我們就能得到還原後的圖片:

於是Flag就是

ZLCSC{W:0x34d-H:0x17e}

完工!

參考資料

  1. PNG - CTF Wiki

  2. png格式分析与压缩原理 - 一只安静的猫