:文件逆序與LSB隱寫技術(shù)解析及Python實現(xiàn))
1. 項目概述在CTFCapture The Flag競賽的雜項Misc賽道上文件逆序與LSBLeast Significant Bit最低有效位隱寫是兩類非常經(jīng)典且高頻出現(xiàn)的題目類型。前者考驗選手對文件結(jié)構(gòu)、字節(jié)序和數(shù)據(jù)組織的理解后者則是對圖像隱寫術(shù)基礎(chǔ)原理的實戰(zhàn)檢驗。而“emoji解密”這個關(guān)鍵詞往往指向一種將二進制數(shù)據(jù)或文本信息編碼為emoji表情符號的趣味性加密方式在CTF中常作為LSB隱寫提取出的數(shù)據(jù)的后續(xù)處理環(huán)節(jié)。這篇文章我將從一個實戰(zhàn)者的角度手把手地帶你用Python解決這兩類問題。我不會只給你干巴巴的代碼而是會深入講解每一步背后的“為什么”分享我在實際解題中踩過的坑和總結(jié)的技巧。無論你是剛接觸CTF的新手還是想鞏固基礎(chǔ)的老兵相信這篇近萬字的實戰(zhàn)筆記都能讓你有所收獲。我們的目標很明確拿到一個包含隱寫信息的文件如圖片通過逆向分析和數(shù)據(jù)處理最終提取出隱藏的Flag。2. 核心原理與工具準備2.1 文件逆序不只是“[::-1]”那么簡單文件逆序題目的核心思想是打亂數(shù)據(jù)的正常存儲順序要求我們將其恢復。這不僅僅是簡單的字符串反轉(zhuǎn)[::-1]其形態(tài)多變字節(jié)級整體逆序整個文件的字節(jié)順序被完全顛倒。這是最基礎(chǔ)的形式用file_bytes[::-1]即可解決。按塊逆序文件被分割成固定大小的塊如每4字節(jié)、每16字節(jié)一塊塊內(nèi)順序正常但塊與塊之間的順序被顛倒。行逆序針對文本文件每一行文本的內(nèi)部字符順序正常但行與行之間的順序被顛倒。比特位逆序在字節(jié)內(nèi)部每個字節(jié)的8個比特位bit順序被顛倒。例如字節(jié)0b11010010(0xD2) 會變成0b01001011(0x4B)?;旌夏嫘蛞陨蠋追N方式的組合例如先按塊逆序再對每個塊進行字節(jié)逆序。為什么出題人愛考這個因為它能有效考察選手對數(shù)據(jù)在內(nèi)存/文件中存儲形式的理解以及對Python字節(jié)bytes和字節(jié)數(shù)組bytearray操作的熟練度。解題的關(guān)鍵在于仔細觀察文件頭尾。一個正常的PNG文件頭是89 50 4E 47如果逆序后變成了47 4E 50 89這就是一個強烈的逆序信號。必備工具010 Editor或HxD十六進制編輯器用于直觀查看文件原始字節(jié)判斷逆序類型。Python我們的主力武器open()函數(shù)以二進制模式’rb’讀取文件至關(guān)重要。2.2 LSB隱寫藏在像素里的秘密LSB隱寫是圖像隱寫中最基礎(chǔ)、最常見的技術(shù)。其原理利用了人類視覺系統(tǒng)對顏色細微變化的不敏感性。原理一張彩色圖像如PNG的每個像素由紅R、綠G、藍B三個通道組成每個通道通常用8位即0-255表示。修改每個通道值的最低1位甚至2-3位對圖像整體的視覺效果影響微乎其微。如何隱藏信息將秘密信息如Flag文本轉(zhuǎn)換為二進制比特流然后依次替換圖像像素RGB通道最低位的比特。如何提取信息讀取圖像每個像素RGB通道的值提取其最低位的比特然后將這些比特按順序拼接起來轉(zhuǎn)換回字節(jié)數(shù)據(jù)最終得到隱藏的信息。一個關(guān)鍵細節(jié)提取順序。信息比特被嵌入到像素中的順序至關(guān)重要。常見的順序有RGB, RGB, RGB...按像素順序先R通道最低位再G再B然后下一個像素。RRR…GGG…BBB…先提取所有像素的R通道最低位然后是所有G通道最后是所有B通道。自定義順序出題人可能指定任何順序如BGR或只使用R和G通道。工具與庫Pillow (PIL)Python圖像處理庫Image.open()和img.getpixel()/img.load()是我們讀取像素的主要方式。Stegsolve一款經(jīng)典的Java圖像隱寫分析工具可以方便地查看各個顏色通道的LSB平面是手動分析的神器。zsteg命令行工具能自動檢測并提取多種LSB隱寫支持多種順序和組合在CTF中極為常用。實操心得遇到一張可能藏有LSB隱寫的圖片我的第一反應是先用zsteg -a image.png掃一遍。它能快速嘗試多種常見的LSB提取方式很多時候能直接給出結(jié)果。如果zsteg無效再上Stegsolve手動分析或自己寫腳本。2.3 Emoji編解碼當Flag變成表情包Emoji解密通常不是一種獨立的加密而是將LSB提取出的二進制數(shù)據(jù)或文本進行了一次“可視化”或“混淆”編碼。常見玩法有Base64/Hex - Emoji映射將Base64編碼的每個字符如A-Z, a-z, 0-9, , /或十六進制的每個半字節(jié)0-F映射到一個特定的emoji上。你需要找到這個映射表。二進制 - Emoji映射將二進制比特流如01101001按固定長度如8位一組映射到代表該字節(jié)值的emoji。直接替換將Flag中的字母、數(shù)字、符號直接替換為形狀相似的emoji如o- ?,i- ??,flag- 。解題思路觀察emoji序列的規(guī)律。如果emoji種類固定且數(shù)量較少如8個很可能對應3位二進制或一個十六進制字符。如果emoji種類很多可能直接對應ASCII碼??梢試L試統(tǒng)計頻率或者尋找題目描述、文件名中的提示如“emoji encoding table”。3. 實戰(zhàn)演練文件逆序處理假設(shè)我們拿到一個文件reversed.bin用010 Editor打開發(fā)現(xiàn)文件頭尾是反的判斷為整體字節(jié)逆序。3.1 基礎(chǔ)字節(jié)逆序def reverse_file_bytes(input_path, output_path): 將整個文件字節(jié)順序反轉(zhuǎn) with open(input_path, rb) as f: data f.read() # 核心操作使用切片[::-1]反轉(zhuǎn)字節(jié)序列 reversed_data data[::-1] with open(output_path, wb) as f: f.write(reversed_data) print(f[] 字節(jié)逆序完成結(jié)果保存至 {output_path}) # 使用示例 reverse_file_bytes(reversed.bin, fixed.bin)注意事項處理大文件時一次性讀入內(nèi)存f.read()可能引發(fā)內(nèi)存不足。對于超大文件應分塊讀取和寫入def reverse_file_bytes_chunked(input_path, output_path, chunk_size1024*1024): 分塊處理大文件的字節(jié)逆序 import os file_size os.path.getsize(input_path) with open(input_path, rb) as fin, open(output_path, wb) as fout: # 從文件末尾開始按塊讀取并逆序?qū)懭?for start in range(file_size, 0, -chunk_size): end max(start - chunk_size, 0) fin.seek(end) chunk fin.read(start - end) fout.write(chunk[::-1]) # 對當前塊進行逆序 print(f[] 大文件字節(jié)逆序完成)3.2 按塊逆序與比特位逆序def reverse_file_blocks(input_path, output_path, block_size4): 按固定大小的塊進行逆序塊內(nèi)順序不變 with open(input_path, rb) as f: data f.read() # 將數(shù)據(jù)分割成塊 blocks [data[i:iblock_size] for i in range(0, len(data), block_size)] # 反轉(zhuǎn)塊順序 reversed_blocks blocks[::-1] # 重新拼接 reversed_data b.join(reversed_blocks) with open(output_path, wb) as f: f.write(reversed_data) print(f[] 按塊大小{block_size}逆序完成) def reverse_bits_in_bytes(input_path, output_path): 反轉(zhuǎn)每個字節(jié)內(nèi)部的比特位順序 with open(input_path, rb) as f: data f.read() # 預計算0-255每個字節(jié)反轉(zhuǎn)后的值提升效率 bit_reverse_table bytes.maketrans(bytes(range(256)), bytes([int(f{i:08b}[::-1], 2) for i in range(256)])) reversed_data data.translate(bit_reverse_table) with open(output_path, wb) as f: f.write(reversed_data) print(f[] 字節(jié)內(nèi)比特位逆序完成)3.3 綜合逆序與自動化嘗試在實際CTF中逆序方式可能未知。我們可以編寫一個腳本嘗試多種逆序組合并自動識別生成的文件是否有效例如通過檢查文件頭。import os import magic # 需要安裝python-magic庫用于識別文件類型 def try_multiple_reversals(input_path): 嘗試多種逆序方式并自動識別可能成功的文件 with open(input_path, rb) as f: original_data f.read() results [] # 1. 整體字節(jié)逆序 reversed_full original_data[::-1] results.append((full_reverse, reversed_full)) # 2. 嘗試不同的塊大小進行塊逆序 for block_size in [2, 4, 8, 16, 32, 64, 128]: blocks [original_data[i:iblock_size] for i in range(0, len(original_data), block_size)] reversed_blocks blocks[::-1] results.append((fblock_{block_size}_reverse, b.join(reversed_blocks))) # 3. 比特位逆序 bit_reverse_table bytes.maketrans(bytes(range(256)), bytes([int(f{i:08b}[::-1], 2) for i in range(256)])) bit_reversed original_data.translate(bit_reverse_table) results.append((bit_reverse, bit_reversed)) # 4. 組合先比特逆序再整體逆序 combo bit_reversed[::-1] results.append((bit_then_full_reverse, combo)) # 檢查并保存可能有效的文件 for name, data in results: # 簡單檢查是否為常見的可識別文件頭 if data[:4] in [b\x89PNG, b\xff\xd8\xff\xe0, bPK\x03\x04, b\x25PDF] or data[:3] bGIF: output_name ffixed_{name}.bin with open(output_name, wb) as f: f.write(data) # 使用magic庫進一步識別 try: file_type magic.from_buffer(data) print(f[] 嘗試 {name} 生成了可能有效的文件: {output_name} 識別為: {file_type}) except: print(f[] 嘗試 {name} 生成了可能有效的文件: {output_name}) else: # 也可以保存所有嘗試結(jié)果供手動檢查 pass避坑指南magic庫在Windows上安裝可能有些麻煩可以使用其替代品filemagic或純Python實現(xiàn)的puremagic。更簡單的方法是直接檢查特定魔數(shù)文件頭例如PNG的\x89PNG\r\n\x1a\nJPEG的\xff\xd8ZIP的PK\x03\x04。4. 實戰(zhàn)演練LSB隱寫提取假設(shè)我們有一張圖片secret.png懷疑其中含有LSB隱寫。4.1 使用zsteg進行快速掃描在終端中首先嘗試最快捷的方式# 安裝zsteg (需要Ruby環(huán)境) # gem install zsteg # 掃描圖片中的所有LSB隱寫可能性 zsteg -a secret.png # 提取特定通道和順序的數(shù)據(jù)例如提取RGB通道的LSB按行掃描 zsteg -e b1,rgb,lsb,xy secret.png extracted_data.bin # 查看提取出的文本如果隱藏的是文本 zsteg -e b1,r,lsb,xy secret.png --stringszsteg的參數(shù)解釋-a嘗試所有已知的隱寫方法。-e提取數(shù)據(jù)。b1每個通道提取1個比特最低位。rgb按R, G, B通道順序提取。lsb最低有效位。xy按行掃描X方向優(yōu)先。--strings嘗試以字符串形式輸出提取的數(shù)據(jù)。4.2 手動編寫Python提取腳本當zsteg無法直接提取或我們需要更精細的控制時就需要自己寫腳本。以下是幾種常見情況的提取腳本。情況一標準RGB順序每個通道取1位from PIL import Image import sys def extract_lsb_simple(image_path, output_path): 從圖片中提取LSB隱寫數(shù)據(jù)假設(shè)為RGB順序每個通道最低位 img Image.open(image_path) pixels img.load() width, height img.size binary_data for y in range(height): for x in range(width): r, g, b pixels[x, y][:3] # 忽略Alpha通道 # 提取每個顏色通道的最低有效位 binary_data str(r 1) binary_data str(g 1) binary_data str(b 1) # 將二進制字符串轉(zhuǎn)換為字節(jié) # 確保二進制字符串長度是8的倍數(shù) if len(binary_data) % 8 ! 0: binary_data binary_data[:-(len(binary_data) % 8)] byte_data bytearray() for i in range(0, len(binary_data), 8): byte int(binary_data[i:i8], 2) byte_data.append(byte) with open(output_path, wb) as f: f.write(byte_data) print(f[] LSB數(shù)據(jù)已提取到 {output_path}) # 嘗試打印開頭部分看是否是文本或已知文件頭 print(f 文件頭: {byte_data[:16].hex()}) # 使用 extract_lsb_simple(secret.png, extracted.bin)情況二自定義提取順序例如只提取R和G通道def extract_lsb_custom(image_path, output_path, channel_orderrg): 按自定義通道順序提取LSB。 channel_order: 字符串如 rg 表示只提取R和G通道bgr表示按B,G,R順序。 img Image.open(image_path) pixels img.load() width, height img.size channel_map {r: 0, g: 1, b: 2} binary_data for y in range(height): for x in range(width): pixel pixels[x, y] for ch in channel_order.lower(): if ch in channel_map: binary_data str(pixel[channel_map[ch]] 1) # 轉(zhuǎn)換為字節(jié) byte_data bytearray() # 處理可能不足8位的尾部 for i in range(0, len(binary_data) - 7, 8): byte int(binary_data[i:i8], 2) byte_data.append(byte) with open(output_path, wb) as f: f.write(byte_data) print(f[] 按順序 {channel_order} 提取的LSB數(shù)據(jù)已保存) print(f 文件頭: {byte_data[:16].hex()})情況三提取的數(shù)據(jù)可能包含文件如ZIP、PNGdef extract_lsb_and_auto_save(image_path): 提取LSB并嘗試自動識別和保存為文件 img Image.open(image_path) pixels img.load() width, height img.size binary_data for y in range(height): for x in range(width): r, g, b pixels[x, y][:3] binary_data str(r 1) binary_data str(g 1) binary_data str(b 1) # 轉(zhuǎn)換為字節(jié) data bytes(int(binary_data[i:i8], 2) for i in range(0, len(binary_data)-7, 8)) # 嘗試識別常見文件類型 if data.startswith(bPK\x03\x04): output_name extracted.zip print(f[] 提取到ZIP文件) elif data.startswith(b\x89PNG): output_name extracted.png print(f[] 提取到PNG圖像) elif data.startswith(b\xff\xd8): output_name extracted.jpg print(f[] 提取到JPEG圖像) elif bflag{ in data or bFLAG{ in data or bctf{ in data: # 可能是直接隱藏的文本 try: text data.decode(utf-8) print(f[] 提取到文本: {text[:100]}...) output_name extracted.txt except: output_name extracted.bin print(f[] 提取到未知數(shù)據(jù)已保存為bin文件) else: output_name extracted.bin print(f[] 未識別出已知格式已保存為bin文件) with open(output_name, wb) as f: f.write(data) return output_name4.3 處理提取出的數(shù)據(jù)Emoji解密示例假設(shè)我們通過LSB提取出了一段文本“ {th1s_1s_4_3m0j1_fl4g}”這顯然是經(jīng)過Emoji編碼的。步驟1觀察與映射觀察發(fā)現(xiàn)Flag格式flag{...}被編碼為 {...}。我們可以假設(shè)一個簡單的替換密碼f-l-a-g-{和}保持不變或也可能被替換這里假設(shè)沒有。步驟2編寫解碼腳本def decode_emoji_simple(emoji_text): 簡單的Emoji替換解碼 # 建立映射字典這里需要根據(jù)題目實際情況調(diào)整 emoji_to_char { : f, : l, : a, : g, # 可以繼續(xù)添加其他映射如數(shù)字、下劃線等 1: 1, # 假設(shè)數(shù)字1沒有被編碼 _: _, {: {, }: }, } decoded for char in emoji_text: decoded emoji_to_char.get(char, char) # 如果找不到映射保留原字符 return decoded # 使用 hidden_text {th1s_1s_4_3m0j1_fl4g} flag decode_emoji_simple(hidden_text) print(f[] 解碼后的Flag: {flag}) # 輸出: flag{th1s_1s_4_3m0j1_fl4g}更復雜的情況如果Emoji對應的是二進制或Base64。import base64 def decode_emoji_base64(emoji_text, emoji_map): Emoji映射到Base64字符集。 emoji_map: 一個字典如 {: A, : B, ...} 映射到標準的64個字符。 # 將Emoji序列轉(zhuǎn)換為Base64字符串 base64_str .join([emoji_map.get(e, ) for e in emoji_text]) # 嘗試Base64解碼 try: decoded_bytes base64.b64decode(base64_str) return decoded_bytes.decode(utf-8) except: return f解碼失敗或非Base64。Base64串: {base64_str} # 假設(shè)我們有一個映射表實際題目會給出或需要猜測 # 這里只是一個示例實際映射需要根據(jù)題目確定 example_emoji_map { : A, : B, : C, : D, # ... 假設(shè)映射到完整的A-Za-z0-9/ } # decoded decode_emoji_base64(emoji_sequence, example_emoji_map)5. 綜合實戰(zhàn)案例與問題排查讓我們模擬一個完整的CTF題目流程。題目描述附件是一個圖片文件challenge.png。提示“秘密藏在最不起眼的地方需要倒著看并用快樂的表情打開?!苯忸}步驟初步觀察用file命令和binwalk檢查binwalk顯示圖片末尾附加了數(shù)據(jù)。用dd或Python分離出附加數(shù)據(jù)extra.dat。文件逆序用010 Editor打開extra.dat發(fā)現(xiàn)文件頭像是某個正常文件的反轉(zhuǎn)。運行我們的try_multiple_reversals函數(shù)發(fā)現(xiàn)“整體字節(jié)逆序”生成了一個ZIP文件fixed_full_reverse.zip。解壓ZIP解壓該ZIP文件發(fā)現(xiàn)需要密碼。同時在ZIP的注釋或某個文本文件中發(fā)現(xiàn)一串Emoji“: ”。LSB提取回到原始圖片challenge.png。使用zsteg -a challenge.png掃描發(fā)現(xiàn)b1,rgb,lsb,xy通道提取出的數(shù)據(jù)開頭有PK\x03\x04說明隱藏了一個ZIP。用zsteg -e b1,rgb,lsb,xy challenge.png hidden.zip提取。Emoji解碼解壓hidden.zip得到一個文本文件hint.txt內(nèi)容就是那串Emoji“: ”。結(jié)合ZIP密碼的提示我們猜測Emoji對應字母。根據(jù)常見單詞嘗試“flag”的映射發(fā)現(xiàn)f, l, a, g拼出來是“flag”但順序不對。嘗試“galf”作為密碼不對。再嘗試“alfg”,“l(fā)fag”... 或者提示“倒著看”可能Emoji序列要反轉(zhuǎn)-galf。用galf作為密碼嘗試解壓fixed_full_reverse.zip成功得到flag.txt。常見問題與排查技巧問題現(xiàn)象可能原因排查方法zsteg提取出一堆亂碼沒有明顯文件頭。LSB提取的通道、順序或位平面不對。1. 用zsteg -a查看所有輸出尋找像PNG,JFIF,PK等關(guān)鍵字。2. 用Stegsolve打開圖片在Analyse - Data Extract界面手動勾選不同的通道Red 0, Green 0, Blue 0和Bit OrderLSB First / MSB First觀察預覽框。自己寫的腳本提取出的數(shù)據(jù)開頭是PK\x03\x04但無法用壓縮軟件打開。提取的二進制數(shù)據(jù)可能錯位了不是從正確的比特開始或者提取的比特數(shù)不對如用了2個LSB。1. 檢查腳本的提取順序是否與隱寫順序一致。嘗試RGB,BGR,RBG等不同順序。2. 嘗試偏移1-7個比特開始提取。寫一個循環(huán)嘗試從二進制字符串的不同起始位置開始解碼。3. 檢查是否每個通道只取了1個LSB。有時是取2個LSB低2位。文件逆序后文件類型識別正確但文件損壞。逆序的粒度不對??赡苁前磯K逆序且塊大小不對或者是比特逆序后再字節(jié)逆序等組合。1. 用010 Editor對比正常文件頭和逆序后文件的“尾部”原文件頭。分析規(guī)律。2. 嘗試我們try_multiple_reversals函數(shù)中的多種組合并用magic或file命令檢查生成的文件。Emoji解碼后不是可讀文本。映射關(guān)系錯誤或者Emoji編碼的不是直接文本而是Base64、Hex或二進制數(shù)據(jù)。1. 統(tǒng)計Emoji的種類數(shù)量。如果正好是16種可能對應十六進制0-F如果是64種左右可能對應Base64。2. 尋找題目描述、文件名、圖片屬性中的提示。3. 嘗試將Emoji序列直接轉(zhuǎn)換為Unicode碼點然后觀察規(guī)律。提取出的ZIP文件需要密碼且無提示??赡苁莻渭用?、CRC32爆破或字典爆破。1. 用010 Editor檢查ZIP文件目錄區(qū)的全局加密位第6個字節(jié)的bit0。如果是00 00但軟件仍提示加密可能是偽加密改為00 00。2. 如果壓縮文件很小8字節(jié)嘗試CRC32爆破。3. 使用rockyou.txt等字典進行爆破。一個實用的LSB提取調(diào)試腳本def debug_lsb_extraction(image_path, start_bit0, channelsrgb, bits_per_channel1): 用于調(diào)試LSB提取參數(shù) img Image.open(image_path).convert(RGB) pix img.load() w, h img.size binary_str for y in range(h): for x in range(w): r, g, b pix[x, y] if r in channels: binary_str format(r, 08b)[8-bits_per_channel:] # 取最低bits_per_channel位 if g in channels: binary_str format(g, 08b)[8-bits_per_channel:] if b in channels: binary_str format(b, 08b)[8-bits_per_channel:] # 從 start_bit 開始取 binary_str binary_str[start_bit:] # 嘗試以字節(jié)形式輸出并查找可打印字符或文件頭 for i in range(0, min(len(binary_str), 1024*8), 8): if i 8 len(binary_str): break byte_val int(binary_str[i:i8], 2) # 打印前128個字節(jié)的hex和可打印字符 if i 128*8: char chr(byte_val) if 32 byte_val 127 else . print(f{byte_val:02x} ({char}), end ) if (i//8 1) % 16 0: print() # 也可以嘗試直接寫文件用010 Editor查看 byte_data bytes(int(binary_str[j:j8], 2) for j in range(0, len(binary_str)-7, 8)) debug_name fdebug_lsb_{channels}_{bits_per_channel}bit_start{start_bit}.bin with open(debug_name, wb) as f: f.write(byte_data) print(f\n[] 調(diào)試數(shù)據(jù)已寫入: {debug_name}) print(f 文件頭: {byte_data[:8].hex()})這個腳本允許你靈活調(diào)整起始位、顏色通道和每個通道提取的比特數(shù)并將結(jié)果保存為文件方便用十六進制編輯器查看是解決“奇怪”LSB隱寫的利器。最后CTF中的文件逆序和LSB隱寫題目千變?nèi)f化但核心思路不變理解數(shù)據(jù)存儲格式細心觀察大膽假設(shè)小心驗證。多動手寫腳本多分析真實賽題你會逐漸形成自己的解題直覺和工具箱。記住工具zsteg,steghide,binwalk能幫你快速解決80%的常規(guī)題而剩下的20%難題則需要你深入理解原理并靈活編寫代碼。