png文件格式分析


png文件格式分析

寫在前面

在寫這個東西寫到一半的時候,突然發現CTFWiki已經有PNG隱寫這篇相對正規的文章了。對PNG文件格式的分析網上相對比較多,所以分析的比較菜,表哥們輕噴。

PNG文件結構

PNG文件格式

PNG文件格式很簡單,對於一個PNG文件來說,主要是開頭固定的字節(又叫做文件頭,文件署名域,標識符等等)和三組以上的PNG數據塊按照特定的順序組成,其中,最基本的PNG至少包含以下部分:

文件頭		IHDR		IDAT		IEND

其中,文件頭的hex為:

89 50 4E 47 0D 0A 1A 0A

這一部分主要是考察對各類媒體類型(mime type)的識別比較多。重要的是,正確判斷一個圖片的文件格式十分重要,如 GIF 里面有幀信息,而JPG 里面卻沒有,PNG 圖片有通道信息,而 JPG 也沒有。單憑后綴進行錯判會導致處理的時候文件報錯。(補充:常見文件格式的文件頭)
同時由於文件種類多而復雜,通過背誦文件頭實現對應圖片文件格式便顯得十分困難。根據網上資料可知,python庫中存在imghdr模塊可以直接識別文件,同目錄下命令行里敲入指令:
python -m imghdr file1
即可識別,結果會返回文件格式。

PNG數據塊(Chunk)

PNG定義了兩種類型的數據塊:關鍵數據塊(critical chunk),是標准的數據塊;輔助數據塊(ancillary chunks),是可選的數據塊。關鍵數據塊定義了四個標准數據塊。分別是IHDR、PLTE、IDAT、IEND,后面我們會分別提一下這四塊。其中PLTE僅與索引彩色圖像有關,在黑白PNG圖片是作為可選數據塊存在的,但是本身是標准數據塊之一。
對於每一個數據塊,其數據結構又是統一的,每個數據塊由Length(4bytes,指定數據塊數據域的長度),Chunk Type Code(4bytes,數據塊類型碼,由ASCII字母組成), Chunk Data(數據塊數據,儲存按照CTC指定的數據),CRC(4byte,循環冗余檢測,檢測是否有錯誤的循環冗余碼)組成。CRC計算是通過CTC和CD得到的。CRC通過觸發以及余數原理進行錯誤偵測。附:《CRC(循環冗余校驗碼)簡介與實現解析》

同時這里提供一下正常圖片CRC校驗值的計算方法:

import zlib

with open('d0430db27b8c4d3694292d9ac5a55634.png','rb') as image_data:
    bin_data=image_data.read()
#截取待計算的字符串即IHDR中去除前四字節(length)和后四字節(CRC)
data = bytearray(bin_data[12:29])
#使用函數計算
crc32key = zlib.crc32(data)
print(hex(crc32key))

PNG數據塊結構

IHDR

文件頭數據塊IHDR(header chunk):包含PNG文件中儲存的圖像數據的基本信息。位於文件頭之后的第一個數據塊。文件頭數據塊是由13個字節組成的:

域名稱 字節數 說明
Width 4bytes 圖像寬度,以像素為單位
Height 4bytes 圖像高度,以像素為單位
Bit depth 1byte 圖像深度
Color Type 1byte 顏色類型
Compression method 1byte 壓縮方法(LZ77派生算法)
Filter method 1byte 濾波器方法
Interlace method 1byte 隔行掃描方法

圖像深度:
索引彩色圖像:1,2,4或8
灰度圖像:1,2,4,8或16
真彩色圖像:8或16
顏色類型:
0:灰度圖像, 1,2,4,8或16
2:真彩色圖像,8或16
3:索引彩色圖像,1,2,4或8
4:帶α通道數據的灰度圖像,8或16
6:帶α通道數據的真彩色圖像,8或16
隔行掃描方法:
0:非隔行掃描
1: Adam7(由Adam M. Costello開發的7遍隔行掃描方法)

其中我們關注的是前8字節內容,也就是Width和Height。通過對圖片高度和寬度的修改隱藏原本應該顯示的flag是最為常見的方法。比較常見的就是CRC爆破和簡單計算猜測原圖片寬度,在下面我可能會提一下。

PLTE

調色板數據塊 PLTE(palette chunk):它包含有與索引彩色圖像(indexed-color image)相關的彩色變換數據,它僅與索引彩色圖像有關,而且要放在圖像數據塊(image data chunk)之前。

顏色 字節 意義
red 1 0 = 黑色, 255 = 紅
green 1 0 = 黑色, 255 = 綠
blue 1 0 = 黑色, 255 = 藍

由於三原色的原因,調色板數據塊的長度應該是三的倍數,否則就是一個非法調色板。真彩色的 PNG 數據流也可以有調色板數據塊,目的是便於非真彩色顯示程序用它來量化圖像數據,從而顯示該圖像。這一塊考察的相對較少,所以暫時沒啥要提的。
但是也不是不能提一下。我們可以通過修改PLTE數據塊修改圖片的顏色。不過我搜到的資料中很少是對調色板修改,更多是對數據中的RGB進行修改。我個人偏向於這種修改是對IDAT塊的修改,而不是PLTE層面。

IDAT

圖像數據塊IDAT(image data chunk),這部分儲存實際的數據,而且在數據流中可以多個連續存在。

  • 儲存圖像像數數據
  • 數據留種可以包含多個連續順序的IDAT
  • 采用LZ77算法的派生算法進行壓縮
  • 可以用zlib解壓縮
  • 只有單個數據塊充滿時,才會填充下一個數據塊
    相對重要的時最后兩條,尤其是最后一條。IDAT單個數據塊不充滿表示此處的數據塊出現錯誤,而錯誤大多數時隱寫導致的,這也是我們獲取flag的一個小小的突破口。
    關於zlib解壓縮這一部分,PNG文件是“可選”zlib解壓縮,不代表只有zlib一種。zlib不是一個壓縮算法,我們說到“zlib壓縮數據”更多的一是是指代一種壓縮數據的存放格式。我們可以使用python提供的zlib庫對其進行處理。我嘗試了zlib字符串的解壓縮,在這里提供代碼以及一篇文章《關於zlib的解壓》
#導入zlib包
import zlib
#目標字符串
message = 'abcd1234'
#字符串壓縮
compressed = zlib.compress(message)
#字符串解壓
decompressed = zlib.decompress(compressed)

print 'original:', repr(message)
print 'compressed:', repr(compressed)
print 'decompressed:', repr(decompressed)

這一塊牽扯比較多的就是LSB隱寫相關的,還有數據插入。一般來說數據插入說明這個只是一個跳板,下面還有其他隱寫……這兩塊我們稍微提一下。
LSB 全稱 Least Significant Bit,最低有效位。PNG 文件中的圖像像數一般是由 RGB 三原色(紅綠藍)組成,每一種顏色占用 8 位,取值范圍為 0x00 至 0xFF,即有 256 種顏色,一共包含了 256 的 3 次方的顏色,即 16777216 種顏色。
而人類的眼睛可以區分約 1000 萬種不同的顏色,意味着人類的眼睛無法區分余下的顏色大約有 6777216 種。
LSB 隱寫就是修改 RGB 顏色分量的最低二進制位(LSB),每個顏色會有 8 bit,LSB 隱寫就是修改了像數中的最低的 1 bit,而人類的眼睛不會注意到這前后的變化,每個像素可以攜帶 3 比特的信息。
我們假設一個像素塊的RGB是(11011010,10010110,10010101),那么,我們修改它末尾的bit的時候,人眼看來色調基本上是沒有變化的。說白了,就是人類視覺冗余沒有辦法識別相近的色素塊導致信息的隱藏,LSB隱寫見過的兩個比較常見的方向有藏二維碼和ASCII碼。本人能力不足,在這里附上lsb隱寫教程

IEND

圖像結束數據 IEND(image trailer chunk):它用來標記 PNG 文件或者數據流已經結束,並且必須要放在文件的尾部。文件結尾的十二個字符看起來應該是:
00 00 00 00 49 45 4E 44 AE 42 60 82
IEND 數據塊的長度總是 00 00 00 00,數據標識總是 IEND 49 45 4E 44,因此,CRC 碼也總是 AE 42 60 82
這個地方遇到的是圖片疊圖片,也就是很粗暴的將兩張圖片的數據塊疊在一起,這樣的文件只能打開,在工具中是報錯的。但是在讀取數據的時候讀取到IEND讀取停止,此時只顯示第一張圖片。這樣相對粗暴的完成了隱寫的目的。

PNG圖片隱寫

文件類型判斷

CRC爆破

在這里CRC爆破我選用的是BUUOJ 大白。tweakpng中提示校驗碼與實際校驗碼出現差別,一般這種情況下是擅自修改寬高沒有修改校驗碼導致的。這個時候我們以原本校驗碼為准,進行CRC爆破。

  import struct
  import binascii
  from Crypto.Util.number import bytes_to_long
  
  img = open("dabai.png", "rb").read()
  
  for i in range(0xFFFF):
  
      stream = img[12:20] + struct.pack('>i', i) + img[24:29]
      crc = binascii.crc32(stream)
     #crc = binascii.crc32(stream) & 0xFFFF
     #if crc == 0x8e14dfcf:
     if crc == bytes_to_long(img[29:33]):
         print(hex(i))

(友情提醒,crypto庫安裝后運行代碼大概率還會出現無crypto庫的白給情況,減少白給人人有責)
高度遍歷 0~0xFFFF。此外,因為img[12:20]等輸出的結果是bytes, 所以這里需要利用struct對整型數據格式化, 將其打包為字節流。另外格式符i意味着4字節的整型。默認小端輸出加'改為大端輸出。
得出結果在010editor修改即可。
不過這種題目制作的時候或多或少有一點點bug,就是有的時候憑手感直接輸入高度或者寬度也可以觀察到隱藏的那部分圖片,這樣看來好像不用很正規的辦法也能解出來(?

未填充滿數據塊

隱藏數據塊與二維碼還有一些刪除錯誤數據塊就可以了的習題。

LSB隱寫

題目示例我選用的是BUUOJ的LSB,這道題比較簡單,而且是牽扯到二維碼。這個地方我們是stegsolve工具,這里附上Stegsolve用法
使用stegsolve打開,在Red plane 0、Grenn plane 0、Blue plane0通道發現圖片的上方好有東西,Analyse->Data Extract稍微調整至一下,發現這是一張圖片,Save Bin保存為flag.png。打開發現是二維碼,掃碼出flag。關於stegsolve通道問題

IEND文件疊文件

misc8是文件疊加文件的典型。我們先用binwalk進行檢查,發現其中疊加了一個png文件。分離圖片文件分為兩種,一種是用foremost或者binwalk直接分離,另外一種是WinHex手動分離。
第一種:在Linux中通過指令binwalk misc8.png查看,發現存在PNG文件。offset是3892,也就是說跳過前方3892塊偏移后,后方為我們所要的。
通過dd指令:
$ dd if=misc8.png of=misc8a.png skip=3892 bs=1
其中,if是目標文件,of是輸出文件,skip是跳過偏移數目,bs設置每次讀寫塊的大小為1字節 。
第二種:直接肉眼手動分離了。有些費時費力,不太建議用這種方法。png文件的IEND應該是00 00 00 00 49 45 4E 44 AE 42 60 82,我們在WinHex中通過肉眼發現確實是有多的png。通過16進制文本定位,在offset F2C處已經出現上述字符塊,而到結尾也存在同樣的字符塊。我們選中后半部分,編輯->復制選塊->至新文件,將文件后綴填好。打開發現flag。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM