刷CTFshow的MISC图片篇,前23题基本算是“看图说话”阶段,放大图片用肉眼观察边缘、对比度、色块就能应付过去。但到了24题往后,我突然发现这套题的出题思路换了:图片不再是“给你看的内容”,而是被当成一个文件容器、一串字节流、一组像素矩阵来出题。我一开始还抱着“再放大看看”的想法,连续几道题碰壁之后,才老老实实打开十六进制编辑器,把PNG的块结构、JPEG的文件尾、RGB各通道的位平面逐一过了一遍。这篇文章就把我从24题刷到51题的完整思路整理出来,核心就两条主线:文件结构分析,颜色通道隐写。1. 从“看图猜谜”到“拆文件”:这套题为什么突然变难了1.1 文件头是入场券,先确认拿到的到底是什么文件做图片题的第一步,从来都不是打开图片看,而是用十六进制方式看一眼文件头部。不同格式的图片在文件开头有固定的魔数(Magic Number),这几字节决定了文件的真实类型,扩展名反而可能是假的。我整理了一下这套题里最高频的几种格式,做题时直接对照:格式十六进制特征常规扩展名JPEGFF D8 FF.jpg / .jpegPNG89 50 4E 47 0D 0A 1A 0A.pngGIF47 49 46 38 39 61(GIF89a).gifBMP42 4D.bmp为什么先看文件头?我碰到过一道题,给的是一个扩展名叫.png的文件,图标也能正常显示,我傻乎乎地直接扔进Stegsolve观察通道,折腾半天没有结果。后来用file命令看了一眼,工具直接告诉我这是JPEG数据,把扩展名改成.jpg之后,图片下方多出一段此前被错误解析掉的内容。魔数就是文件的身份证,做题时先验明正身,能省下大量的弯路。1.2 文件尾不等于文件结束,图片后面能藏另一个文件文件结构这条线里,最容易忽略的是文件尾。图片查看器打开一张图,看到画面就认定“这就是全部”,但实际上从文件结构视角看,图片的结尾只代表“图像数据到此为止”,不代表“整个字节流到此为止”。JPEG的正常结尾是FF D9,PNG的结尾是IEND块,但这些标识之后依然可以追加数据。拼接文件是很常见的出题手法:把flag压缩成ZIP或RAR,再直接附加到图片文件末尾。这种文件用图片浏览器打开毫无异常,但用十六进制编辑器拉到文件尾,就能看到标志性的50 4B 03 04(ZIP文件头),或者直接看到一大段压缩包数据。我的处理习惯是:拿到图先不急着肉眼看,直接交给binwalk扫描一遍。这工具本质是在字节流里搜索各种已知文件的特征签名,能快速发现“图片尾部还跟了个压缩包”这类结构。确认有附加文件之后再用foremost或binwalk -e把嵌入的文件分离出来。分离出来的压缩包通常带有密码,密码提示往往就藏在图片文件的注释、属性或者文件名里,这部分下面会展开说。1.3 binwalk的误报问题,不能盲信扫描结果binwalk好用,但也不是万能的,我刷这套题期间被它带偏过至少两次。最有代表性的误报场景是:扫描一个完全正常的PNG图片,结果报告里出现一堆 “Zlib compressed data” 的条目。这个其实是PNG自身的IDAT块导致的。PNG的图像数据默认用zlib压缩,IDAT块里天然就是压缩流,binwalk会把每个IDAT块识别成zlib数据。但这不是隐写,也不是附加文件,只是PNG的正常组成部分。如果看到这类结果就提取,只会得到一堆残缺的图像压缩数据,什么也解不出来。正确的判断方式很简单:对于binwalk报告的每个可疑条目,先看它是什么类型、出现在哪个偏移位置。如果“Zlib compressed data”整体出现在文件中部且密集排列,大概率是PNG的IDAT;如果某个ZIP、RAR、7z的签名出现在文件末尾的附加区,才是有价值的线索。另外,binwalk -e导出结果后经常生成一堆嵌套子目录,我后来一般用foremost做提取,它的输出目录结构更干净。2. 颜色通道里的第二层画面:LSB、位平面与通道置换2.1 最低有效位为什么能藏东西“看不见”颜色通道这条线的底层原理,是数字图像的像素表示方式。大多数彩色图片用RGB三通道记录颜色,每个通道用一个8位整数表示,取值范围0到255。比如纯红色像素的RGB值是(255, 0, 0),二进制写出来是11111111 00000000 00000000。像素每个通道的8个bit里,越靠左的bit对颜色影响越大,叫高位;越靠右的影响越小,叫低位。最右边那一位就是最低有效位,简称LSB。把某个像素的红色通道从255改成254,反映在视觉上几乎无法分辨——255和254显示出来的红色,肉眼根本看不出差别。可这一个bit就空出来了,可以被用来携带信息。这一原理用现实生活来类比,就像一幅画的整体色调被人微调了1个单位,观感上完全不受影响,但这1个单位的差异里可以编码一整套完整的信息。一张1000×1000像素的图片,每个像素RGB共24个bit,每个bit都利用起来,可以藏接近3MB数据,容量相当可观。这也解释了为什么LSB隐写在图片隐写题里出现频率这么高。2.2 LSB隐写怎么识别:噪点、直方图和工具验证识别一张图是否被LSB隐写,有几个维度可以叠加判断。第一个是肉眼观察法:把图片放大后看纯色区域,如果表面不是均匀的色块,而是带有一层极细微的彩色噪点或颗粒感,就需要警惕——正常图片的平滑区域不该有这种随机噪声。第二个是位平面观察法,也是我后来最依赖的方式。把图片每个通道按bit拆开,一共能拆出24张单bit图。正常人眼接收的是图像谐波后的整体效果,但信息藏在LSB里时,拆出来的第0位平面会出现有规律的图案或文字轮廓,而正常图片的第0位平面上基本只是雪花噪点。用Stegsolve工具打开图片,切到Analyse -> Bit Planes,逐平面翻看,一旦某张低bit平面的内容出现清晰结构,就基本可以断定存在LSB隐写。确认之后用zsteg这类工具快速提取,很快就能拿到结果。需要注意的是,LSB隐写有多种数据排列方式:LSB还是MSB、行扫描还是列扫描、RGB三通道的顺序。工具里通常提供组合选项,提取失败就换一种组合多试几次,zsteg默认会遍历大部分常见组合,省去手动测试的麻烦。2.3 单通道查看与通道置换:别被混合后的画面蒙蔽还有一种常见玩法,信息并不藏在最低有效位,而是直接藏在某一个颜色通道里,或者干脆把通道做了置换。我在刷这套题的过程中遇到过一张图,整体看起来是一幅再普通不过的风景照,但切到Grey Bits模式、只显示蓝色通道的灰阶图之后,画面里浮现出一段若隐若现的文字。为什么单看一个通道会看到不一样的信息?因为正常图片的最终画面是三通道合成后的结果,信息如果只在R、G、B其中一个通道里被写入,会被其它两个通道的视觉信息淹没。但单独提取出这个通道,就等于去掉了干扰项。Stegsolve里的Grey Bits功能就是干这个用的,它可以把R/G/B任意一个通道单独以灰阶显示。我刷这套题时养成了一个习惯:每一张图都先R、G、B三个通道各切一遍,再看一眼青、品红、黄色三种合成效果,无异常才算过。通道的另一个坑是通道置换。有些题把R和B通道整体互换,图片颜色会变得非常诡异——天空变成紫色,草地变成洋红色,肉眼一眼就能看出不对。这时候不是在藏信息,而是需要通过交换通道把图片恢复成正常样子,恢复之后真正的图像内容才会显露出来。用Stegsolve的Colour inversion和通道组合功能都能处理。3. “图裂了”不等于没救:PNG结构修复的完整思路3.1 PNG不是一张图,而是一串带标签的数据块PNG图片的结构和一个带目录的文档很像。它由若干个数据块(chunk)组成,每个块都严格按照“长度-类型-数据-CRC”的格式排列。开头是固定的8字节签名,之后依次是:IHDR:头信息块,记录图片宽度、高度、位深、颜色类型IDAT:图像数据块,保存压缩后的像素数据IEND:结束块,标记文件结束其中IHDR块里最值得关注的是宽度和高度字段,各占4字节。很多题目会把高度改小,使图片在正常查看时只显示上半部分,把藏有线索的下半部分裁掉;或者反过来,把高度改大,让原本不存在的区域显示出来。还有的题直接把CRC字段改错,让图片无法正常打开,恢复CRC后图片才还原。3.2 改宽高必须重算CRC,否则依旧打不开我第一次手动改PNG高度时犯过一个错误:只把IHDR里的高度字节改大,然后保存,结果图片照样打不开。原因在于PNG每个数据块的CRC是校验块完整性的,IHDR块里记录的高度和CRC值不匹配,解析器就会认为图片数据损坏。CRC的计算范围是从块类型字段到数据字段结束,也就是IHDR四个字节加上后面13字节的数据。修改宽度或高度后,必须用同样的算法重新计算CRC并写回去。下面的脚本就是我当时用来修图的,核心就四步:定位IHDR块、改宽度/高度、用zlib算CRC、写回文件。import struct import zlib def fix_png_height(path, new_height, out='fixed.png'): with open(path, 'rb') as f: data = bytearray(f.read()) # PNG签名(8字节) + 块长度(4字节) + "IHDR"(4字节) + 宽度(4字节) + 高度(4字节) height_offset = 20 old_height = int.from_bytes(data[height_offset:height_offset+4], 'big') print(f'原高度: {old_height}') # 写入新高度 data[height_offset:height_offset+4] = struct.pack('>I', new_height) # CRC计算范围: "IHDR"类型(4字节) + IHDR数据(13字节) = data[12:29] crc = zlib.crc32(bytes(data[12:29])) & 0xffffffff data[29:33] = struct.pack('>I', crc) with open(out, 'wb') as f: f.write(data) print(f'已修复并保存为 {out}') # 使用示例 fix_png_height('question.png', 1000)这个脚本的核心逻辑就是块结构的实际操作。第一次跑通的时候我意识到,网上那些“改高度藏flag”的题,本质就是在考你能不能正确理解PNG格式的字段布局和CRC保护机制。3.3 修复类题目更完整的排查顺序后来我把这类题的排查顺序固定成一套流程,避免漏掉任何可能。先看图片能不能正常打开。能打开但显示不全,优先怀疑IHDR高度被改小,直接读IHDR块里的宽高值和文件实际尺寸,对比判断异常。打不开且提示CRC错误,先读IHDR的CRC字段,拿前17字节重新计算结果对比,确认是单纯损坏还是故意篡改。CRC本身也有可能暗藏信息,如果CRC和实际计算值不一致,可以尝试用修改后的CRC值本身去解码,有时候它就对应一段ASCII字符串。再往深一层,如果IHDR一切正常但图片依然打不开,问题可能出在IDAT块。IDAT是分段存储压缩数据的,某些题会把其中一段抽走或改坏,导致解压失败。这类题需要比对相邻IDAT块的长度规律,找出异常的块。修复IDAT比修改高度更费劲,通常需要结合像素数据的解压结果来判断,这套题里出现的概率相对低一些,但值得知道有这条路。4. 我把28道题拆成了三层分类,并沉淀出一套固定排查流程4.1 题型分层:先判断难度再决定投入产出比从24到51,这28道题给我的整体感觉是有明显梯度设计的。我习惯把它们粗暴地分成三大层:第一层:文件结构类。包括附加文件分离、文件头修复、扩展名伪装。这类题拿到手用file、binwalk、十六进制编辑器就能解决,重点考验细心程度。第二层:颜色通道类。包括LSB隐写、位平面隐藏、单通道信息、通道置换。这类题需要对像素原理有基本认识,配合Stegsolve和zsteg就能提取。第三层:组合类。先分离出嵌在图片里的文件,再对分离出的文件继续做通道分析或修复。这类题的坑在于,你做完第一步拿到一个中间文件,很容易误以为是终点,结果最终信息还在更深处。这套分类的价值在于,当我卡在一道题上时,可以先判断它最可能属于哪一层,而不是盲目把每一层的方法都试一遍。4.2 我的标准工具链和使用顺序刷到后半段,我已经把工具的使用顺序固定下来,尽量做到不重不漏。顺序工具作用场景使用时机1file确认真实文件类型拿到文件后第一时间2strings搜索可见字符串、注释、提示file确认类型后3binwalk/foremost扫描并分离附加文件strings没有发现时4十六进制编辑器人工查看文件头和尾、修复块结构怀疑文件损坏或隐藏数据5zsteg快速检测LSB类隐写确认是无异常图片后6Stegsolve位平面、单通道、颜色反转zsteg无结果时7Python脚本CRC修复、宽高爆破、数据分析工具解决不了时自定义处理我特别想强调strings的重要性。很多人做图片题上来就上Stegsolve,但strings能在几秒内扫出图片里所有可打印字符串。遇到过不止一次,flag就明晃晃地写在PNG的tEXt注释块里,或者藏在GIF的帧描述里,strings一跑直接出答案。省下来的时间足够多做两道题。4.3 做题节奏:每道题给自己设时间上限刷题有一个很现实的问题:卡题时间过长会严重消耗耐心,尤其是一套题连做几十道的时候。我给自己定的规则是:每道题的总耗时不超40分钟。前15分钟完成基础扫描和快速尝试,如果卡住,就把已经获得的线索和尝试过的方案记录在笔记里,跳过并继续后面的题。回头有空再用新思路重新看卡住的题。这样做的好处是保持了整体推进的节奏,而且很多题其实属于“工具对了就秒出答案,工具错了就毫无头绪”的类型。跳过换题不等于放弃,过段时间再看,大脑反而会跳出原来固化的思路。我在24到51这段里,有三四道题都是一天后回来翻笔记,几分钟内找到新角度解出来的。5. 这28道题带给我最深的几个复盘结论5.1 踩过的坑:重型工具永远排在最后刷这套题期间,我最大的一个误区是“工具越强越好”。有一道题,我拿到图直接扔进Stegsolve,把位平面翻了个遍、通道组合试了个遍,花了将近半小时,毫无发现。后来静下心用strings扫了一遍,发现flag就直接写在PNG的注释块里。这个教训特别深刻:Stegsolve这类工具适合处理颜色通道隐写问题,但一旦题目本质是文件结构层面的隐藏,它完全无能为力。工具选型错误等于白忙活。从那以后我给自己立了一条规矩:先从最基础的字符串扫描和文件类型确认开始,逐步升级到结构扫描、隐写检测、通道分析。顺序反了,时间就全浪费了。5.2 提取出的数据先别急着当终点处理很多题在提取阶段就会给你一个中间产物,比如一段Base64字符串、一长串十六进制、或者一个加密的压缩包。我第一次遇到这种情况时,以为提取出字符串就是终点,结果面对一串Base64一头雾水。后来才意识到,提取只是中间步骤,识别出这串数据的编码方式才是关键。Base64字符串的特征是末尾可能出现=填充符,字符集局限于A-Z、a-z、0-9、+、/。十六进制字符串的特征是只包含0-9和a-f,且长度为偶数。拿到一段编码数据,先判断是Base64还是Hex,解码后再看内容;如果解码出来还是乱码,可能还套了第二层编码。另外,如果分离出带密码的压缩包,密码往往就在前面的分析过程中——可能是文件名、图片属性,也可能是某段注释的谐音。5.3 现在养成的肌肉记忆:每张图先做四个固定动作把这28道题刷完之后,我面对任何一张图片题都会条件反射地执行四个固定动作,算是真正沉淀下来的肌肉记忆。第一动,file确认类型,不信扩展名。第二动,strings扫全量字符串,顺带关注文件名和扩展名的特殊性,文件名本身就是线索的情况遇到过不止一次。第三动,binwalk看有没有附加文件或隐藏结构。第四动,无异常后才进入颜色通道分析:先zsteg跑一遍LSB,再用Stegsolve看位平面和单通道。这四步做完,大多数图片题的命门基本都能找到。这套流程现在已经成为我刷其它方向题目的基础方法论。颜色通道的思路和音频隐写、二维码容错等方向是相通的,文件结构的思路更是后续几乎所有取证类题目的基本功。回头再看24到51这半程,它逼着我把对图片的认知从“画面”切换到了“数据和字节”,这个视角转换是最有价值的收获。
刷CTFshow的MISC图片篇,前23题基本算是“看图说话”阶段,放大图片用肉眼观察边缘、对比度、色块就能应付过去。但到了24题往后,我突然发现这套题的出题思路换了:图片不再是“给你看的内容”,而是被当成一个文件容器、一串字节流、一组像素矩阵来出题。我一开始还抱着“再放大看看”的想法,连续几道题碰壁之后,才老老实实打开十六进制编辑器,把PNG的块结构、JPEG的文件尾、RGB各通道的位平面逐一过了一遍。这篇文章就把我从24题刷到51题的完整思路整理出来,核心就两条主线:文件结构分析,颜色通道隐写。1. 从“看图猜谜”到“拆文件”:这套题为什么突然变难了1.1 文件头是入场券,先确认拿到的到底是什么文件做图片题的第一步,从来都不是打开图片看,而是用十六进制方式看一眼文件头部。不同格式的图片在文件开头有固定的魔数(Magic Number),这几字节决定了文件的真实类型,扩展名反而可能是假的。我整理了一下这套题里最高频的几种格式,做题时直接对照:格式十六进制特征常规扩展名JPEGFF D8 FF.jpg / .jpegPNG89 50 4E 47 0D 0A 1A 0A.pngGIF47 49 46 38 39 61(GIF89a).gifBMP42 4D.bmp为什么先看文件头?我碰到过一道题,给的是一个扩展名叫.png的文件,图标也能正常显示,我傻乎乎地直接扔进Stegsolve观察通道,折腾半天没有结果。后来用file命令看了一眼,工具直接告诉我这是JPEG数据,把扩展名改成.jpg之后,图片下方多出一段此前被错误解析掉的内容。魔数就是文件的身份证,做题时先验明正身,能省下大量的弯路。1.2 文件尾不等于文件结束,图片后面能藏另一个文件文件结构这条线里,最容易忽略的是文件尾。图片查看器打开一张图,看到画面就认定“这就是全部”,但实际上从文件结构视角看,图片的结尾只代表“图像数据到此为止”,不代表“整个字节流到此为止”。JPEG的正常结尾是FF D9,PNG的结尾是IEND块,但这些标识之后依然可以追加数据。拼接文件是很常见的出题手法:把flag压缩成ZIP或RAR,再直接附加到图片文件末尾。这种文件用图片浏览器打开毫无异常,但用十六进制编辑器拉到文件尾,就能看到标志性的50 4B 03 04(ZIP文件头),或者直接看到一大段压缩包数据。我的处理习惯是:拿到图先不急着肉眼看,直接交给binwalk扫描一遍。这工具本质是在字节流里搜索各种已知文件的特征签名,能快速发现“图片尾部还跟了个压缩包”这类结构。确认有附加文件之后再用foremost或binwalk -e把嵌入的文件分离出来。分离出来的压缩包通常带有密码,密码提示往往就藏在图片文件的注释、属性或者文件名里,这部分下面会展开说。1.3 binwalk的误报问题,不能盲信扫描结果binwalk好用,但也不是万能的,我刷这套题期间被它带偏过至少两次。最有代表性的误报场景是:扫描一个完全正常的PNG图片,结果报告里出现一堆 “Zlib compressed data” 的条目。这个其实是PNG自身的IDAT块导致的。PNG的图像数据默认用zlib压缩,IDAT块里天然就是压缩流,binwalk会把每个IDAT块识别成zlib数据。但这不是隐写,也不是附加文件,只是PNG的正常组成部分。如果看到这类结果就提取,只会得到一堆残缺的图像压缩数据,什么也解不出来。正确的判断方式很简单:对于binwalk报告的每个可疑条目,先看它是什么类型、出现在哪个偏移位置。如果“Zlib compressed data”整体出现在文件中部且密集排列,大概率是PNG的IDAT;如果某个ZIP、RAR、7z的签名出现在文件末尾的附加区,才是有价值的线索。另外,binwalk -e导出结果后经常生成一堆嵌套子目录,我后来一般用foremost做提取,它的输出目录结构更干净。2. 颜色通道里的第二层画面:LSB、位平面与通道置换2.1 最低有效位为什么能藏东西“看不见”颜色通道这条线的底层原理,是数字图像的像素表示方式。大多数彩色图片用RGB三通道记录颜色,每个通道用一个8位整数表示,取值范围0到255。比如纯红色像素的RGB值是(255, 0, 0),二进制写出来是11111111 00000000 00000000。像素每个通道的8个bit里,越靠左的bit对颜色影响越大,叫高位;越靠右的影响越小,叫低位。最右边那一位就是最低有效位,简称LSB。把某个像素的红色通道从255改成254,反映在视觉上几乎无法分辨——255和254显示出来的红色,肉眼根本看不出差别。可这一个bit就空出来了,可以被用来携带信息。这一原理用现实生活来类比,就像一幅画的整体色调被人微调了1个单位,观感上完全不受影响,但这1个单位的差异里可以编码一整套完整的信息。一张1000×1000像素的图片,每个像素RGB共24个bit,每个bit都利用起来,可以藏接近3MB数据,容量相当可观。这也解释了为什么LSB隐写在图片隐写题里出现频率这么高。2.2 LSB隐写怎么识别:噪点、直方图和工具验证识别一张图是否被LSB隐写,有几个维度可以叠加判断。第一个是肉眼观察法:把图片放大后看纯色区域,如果表面不是均匀的色块,而是带有一层极细微的彩色噪点或颗粒感,就需要警惕——正常图片的平滑区域不该有这种随机噪声。第二个是位平面观察法,也是我后来最依赖的方式。把图片每个通道按bit拆开,一共能拆出24张单bit图。正常人眼接收的是图像谐波后的整体效果,但信息藏在LSB里时,拆出来的第0位平面会出现有规律的图案或文字轮廓,而正常图片的第0位平面上基本只是雪花噪点。用Stegsolve工具打开图片,切到Analyse -> Bit Planes,逐平面翻看,一旦某张低bit平面的内容出现清晰结构,就基本可以断定存在LSB隐写。确认之后用zsteg这类工具快速提取,很快就能拿到结果。需要注意的是,LSB隐写有多种数据排列方式:LSB还是MSB、行扫描还是列扫描、RGB三通道的顺序。工具里通常提供组合选项,提取失败就换一种组合多试几次,zsteg默认会遍历大部分常见组合,省去手动测试的麻烦。2.3 单通道查看与通道置换:别被混合后的画面蒙蔽还有一种常见玩法,信息并不藏在最低有效位,而是直接藏在某一个颜色通道里,或者干脆把通道做了置换。我在刷这套题的过程中遇到过一张图,整体看起来是一幅再普通不过的风景照,但切到Grey Bits模式、只显示蓝色通道的灰阶图之后,画面里浮现出一段若隐若现的文字。为什么单看一个通道会看到不一样的信息?因为正常图片的最终画面是三通道合成后的结果,信息如果只在R、G、B其中一个通道里被写入,会被其它两个通道的视觉信息淹没。但单独提取出这个通道,就等于去掉了干扰项。Stegsolve里的Grey Bits功能就是干这个用的,它可以把R/G/B任意一个通道单独以灰阶显示。我刷这套题时养成了一个习惯:每一张图都先R、G、B三个通道各切一遍,再看一眼青、品红、黄色三种合成效果,无异常才算过。通道的另一个坑是通道置换。有些题把R和B通道整体互换,图片颜色会变得非常诡异——天空变成紫色,草地变成洋红色,肉眼一眼就能看出不对。这时候不是在藏信息,而是需要通过交换通道把图片恢复成正常样子,恢复之后真正的图像内容才会显露出来。用Stegsolve的Colour inversion和通道组合功能都能处理。3. “图裂了”不等于没救:PNG结构修复的完整思路3.1 PNG不是一张图,而是一串带标签的数据块PNG图片的结构和一个带目录的文档很像。它由若干个数据块(chunk)组成,每个块都严格按照“长度-类型-数据-CRC”的格式排列。开头是固定的8字节签名,之后依次是:IHDR:头信息块,记录图片宽度、高度、位深、颜色类型IDAT:图像数据块,保存压缩后的像素数据IEND:结束块,标记文件结束其中IHDR块里最值得关注的是宽度和高度字段,各占4字节。很多题目会把高度改小,使图片在正常查看时只显示上半部分,把藏有线索的下半部分裁掉;或者反过来,把高度改大,让原本不存在的区域显示出来。还有的题直接把CRC字段改错,让图片无法正常打开,恢复CRC后图片才还原。3.2 改宽高必须重算CRC,否则依旧打不开我第一次手动改PNG高度时犯过一个错误:只把IHDR里的高度字节改大,然后保存,结果图片照样打不开。原因在于PNG每个数据块的CRC是校验块完整性的,IHDR块里记录的高度和CRC值不匹配,解析器就会认为图片数据损坏。CRC的计算范围是从块类型字段到数据字段结束,也就是IHDR四个字节加上后面13字节的数据。修改宽度或高度后,必须用同样的算法重新计算CRC并写回去。下面的脚本就是我当时用来修图的,核心就四步:定位IHDR块、改宽度/高度、用zlib算CRC、写回文件。import struct import zlib def fix_png_height(path, new_height, out='fixed.png'): with open(path, 'rb') as f: data = bytearray(f.read()) # PNG签名(8字节) + 块长度(4字节) + "IHDR"(4字节) + 宽度(4字节) + 高度(4字节) height_offset = 20 old_height = int.from_bytes(data[height_offset:height_offset+4], 'big') print(f'原高度: {old_height}') # 写入新高度 data[height_offset:height_offset+4] = struct.pack('>I', new_height) # CRC计算范围: "IHDR"类型(4字节) + IHDR数据(13字节) = data[12:29] crc = zlib.crc32(bytes(data[12:29])) & 0xffffffff data[29:33] = struct.pack('>I', crc) with open(out, 'wb') as f: f.write(data) print(f'已修复并保存为 {out}') # 使用示例 fix_png_height('question.png', 1000)这个脚本的核心逻辑就是块结构的实际操作。第一次跑通的时候我意识到,网上那些“改高度藏flag”的题,本质就是在考你能不能正确理解PNG格式的字段布局和CRC保护机制。3.3 修复类题目更完整的排查顺序后来我把这类题的排查顺序固定成一套流程,避免漏掉任何可能。先看图片能不能正常打开。能打开但显示不全,优先怀疑IHDR高度被改小,直接读IHDR块里的宽高值和文件实际尺寸,对比判断异常。打不开且提示CRC错误,先读IHDR的CRC字段,拿前17字节重新计算结果对比,确认是单纯损坏还是故意篡改。CRC本身也有可能暗藏信息,如果CRC和实际计算值不一致,可以尝试用修改后的CRC值本身去解码,有时候它就对应一段ASCII字符串。再往深一层,如果IHDR一切正常但图片依然打不开,问题可能出在IDAT块。IDAT是分段存储压缩数据的,某些题会把其中一段抽走或改坏,导致解压失败。这类题需要比对相邻IDAT块的长度规律,找出异常的块。修复IDAT比修改高度更费劲,通常需要结合像素数据的解压结果来判断,这套题里出现的概率相对低一些,但值得知道有这条路。4. 我把28道题拆成了三层分类,并沉淀出一套固定排查流程4.1 题型分层:先判断难度再决定投入产出比从24到51,这28道题给我的整体感觉是有明显梯度设计的。我习惯把它们粗暴地分成三大层:第一层:文件结构类。包括附加文件分离、文件头修复、扩展名伪装。这类题拿到手用file、binwalk、十六进制编辑器就能解决,重点考验细心程度。第二层:颜色通道类。包括LSB隐写、位平面隐藏、单通道信息、通道置换。这类题需要对像素原理有基本认识,配合Stegsolve和zsteg就能提取。第三层:组合类。先分离出嵌在图片里的文件,再对分离出的文件继续做通道分析或修复。这类题的坑在于,你做完第一步拿到一个中间文件,很容易误以为是终点,结果最终信息还在更深处。这套分类的价值在于,当我卡在一道题上时,可以先判断它最可能属于哪一层,而不是盲目把每一层的方法都试一遍。4.2 我的标准工具链和使用顺序刷到后半段,我已经把工具的使用顺序固定下来,尽量做到不重不漏。顺序工具作用场景使用时机1file确认真实文件类型拿到文件后第一时间2strings搜索可见字符串、注释、提示file确认类型后3binwalk/foremost扫描并分离附加文件strings没有发现时4十六进制编辑器人工查看文件头和尾、修复块结构怀疑文件损坏或隐藏数据5zsteg快速检测LSB类隐写确认是无异常图片后6Stegsolve位平面、单通道、颜色反转zsteg无结果时7Python脚本CRC修复、宽高爆破、数据分析工具解决不了时自定义处理我特别想强调strings的重要性。很多人做图片题上来就上Stegsolve,但strings能在几秒内扫出图片里所有可打印字符串。遇到过不止一次,flag就明晃晃地写在PNG的tEXt注释块里,或者藏在GIF的帧描述里,strings一跑直接出答案。省下来的时间足够多做两道题。4.3 做题节奏:每道题给自己设时间上限刷题有一个很现实的问题:卡题时间过长会严重消耗耐心,尤其是一套题连做几十道的时候。我给自己定的规则是:每道题的总耗时不超40分钟。前15分钟完成基础扫描和快速尝试,如果卡住,就把已经获得的线索和尝试过的方案记录在笔记里,跳过并继续后面的题。回头有空再用新思路重新看卡住的题。这样做的好处是保持了整体推进的节奏,而且很多题其实属于“工具对了就秒出答案,工具错了就毫无头绪”的类型。跳过换题不等于放弃,过段时间再看,大脑反而会跳出原来固化的思路。我在24到51这段里,有三四道题都是一天后回来翻笔记,几分钟内找到新角度解出来的。5. 这28道题带给我最深的几个复盘结论5.1 踩过的坑:重型工具永远排在最后刷这套题期间,我最大的一个误区是“工具越强越好”。有一道题,我拿到图直接扔进Stegsolve,把位平面翻了个遍、通道组合试了个遍,花了将近半小时,毫无发现。后来静下心用strings扫了一遍,发现flag就直接写在PNG的注释块里。这个教训特别深刻:Stegsolve这类工具适合处理颜色通道隐写问题,但一旦题目本质是文件结构层面的隐藏,它完全无能为力。工具选型错误等于白忙活。从那以后我给自己立了一条规矩:先从最基础的字符串扫描和文件类型确认开始,逐步升级到结构扫描、隐写检测、通道分析。顺序反了,时间就全浪费了。5.2 提取出的数据先别急着当终点处理很多题在提取阶段就会给你一个中间产物,比如一段Base64字符串、一长串十六进制、或者一个加密的压缩包。我第一次遇到这种情况时,以为提取出字符串就是终点,结果面对一串Base64一头雾水。后来才意识到,提取只是中间步骤,识别出这串数据的编码方式才是关键。Base64字符串的特征是末尾可能出现=填充符,字符集局限于A-Z、a-z、0-9、+、/。十六进制字符串的特征是只包含0-9和a-f,且长度为偶数。拿到一段编码数据,先判断是Base64还是Hex,解码后再看内容;如果解码出来还是乱码,可能还套了第二层编码。另外,如果分离出带密码的压缩包,密码往往就在前面的分析过程中——可能是文件名、图片属性,也可能是某段注释的谐音。5.3 现在养成的肌肉记忆:每张图先做四个固定动作把这28道题刷完之后,我面对任何一张图片题都会条件反射地执行四个固定动作,算是真正沉淀下来的肌肉记忆。第一动,file确认类型,不信扩展名。第二动,strings扫全量字符串,顺带关注文件名和扩展名的特殊性,文件名本身就是线索的情况遇到过不止一次。第三动,binwalk看有没有附加文件或隐藏结构。第四动,无异常后才进入颜色通道分析:先zsteg跑一遍LSB,再用Stegsolve看位平面和单通道。这四步做完,大多数图片题的命门基本都能找到。这套流程现在已经成为我刷其它方向题目的基础方法论。颜色通道的思路和音频隐写、二维码容错等方向是相通的,文件结构的思路更是后续几乎所有取证类题目的基本功。回头再看24到51这半程,它逼着我把对图片的认知从“画面”切换到了“数据和字节”,这个视角转换是最有价值的收获。