ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从人脸检测到LSB隐写:CTF杂项题完整解题思路

从人脸检测到LSB隐写:CTF杂项题完整解题思路 1. 看到X-man-A face这个名字我的第一反应是去抠字眼CTF杂项里最不缺的就是靠名字给提示的题有些题恨不得把答案写在题目名上。QCTF2018这道X-man-A face就是典型——拿到题目压缩包的时候里面只有一张图我当时盯着文件名看了很久。按CTF的惯例题目名里出现的关键词几乎都是解题线索所以我先做了个最基础的拆解。X-man、A、face这三个部分拆开看我当时的理解是X-man大概率指向X战警题材图片内容取自某个人物剧照或粉丝图A单独拎出来可能是冠词但也可能是ASCII编码、Alpha通道或者某种符号暗示face则是最直白的线索说明突破点会和人脸绑定。在题目没有附带任何说明的前提下我选择把face作为最高优先级线索先沿着人脸方向走。事实证明这种做法在CTF里非常有效。出题人起名字通常不是随便起的题目名里出现的名词要么是方向提示要么是干扰项但绝大多数情况下它是前者。比起去猜那些花里胡哨的编码方式先把明面上的词处理好能省掉大量无效操作。1.1 题名关键词的多种解读方向当然单靠直觉不够我还做了一些发散推演。X-man如果连着读发音上接近exam也许暗示这是一场关于检测的题目A也可能是指文件格式里的某个通道比如RGBA里的Alpha通道。不过这些猜测在后续检查中被逐一排除真正立住脚的还是人脸区域这个方向。这里我想多说一句解CTF题不要怕猜错。猜错只是浪费几分钟不猜才可能卡住一晚上。很多选手看到这种名字会习惯性去翻编码字典想着X-man是不是某种加密的名字反而忽略了最直观的画面线索。我一直强调杂项题的第一步永远是做减法——先用低成本的排查排除掉大部分可能性剩下的就是正确答案最可能存在的空间。1.2 初始文件排查老四样不能少拿到图片题我不管题目名怎么暗示第一步永远是那套固定动作file、binwalk、strings、exiftool。这不是走流程而是用最低成本排除藏了额外文件改了文件尾部EXIF里塞了备注这些常见小把戏。说句实在话很多选手一上来就开stegsolve翻通道翻了半天才发现自己根本不知道在找什么这就是排查顺序没做好。$ file face.jpg face.jpg: JPEG image data, JFIF standard 1.01, resolution (DPI), density 96x96 $ binwalk face.jpg DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- 0 0x0 JPEG image data, JFIF standard 1.01binwalk结果干净没有额外的压缩包或文件尾插数据strings也只翻到正常的JPEG注释和软件信息exiftool看了一圈没有任何可疑字段。也就是说这张图片从文件容器层面是很干净的接下来只能往像素层面找。这个结论很重要它把搜索范围从文件里藏东西直接缩小到像素里藏东西。1.3 stegsolve初探为什么人眼看不出来很多新人接触杂项题条件反射就是开stegsolve翻通道。这个习惯不能说错但容易变成无头苍蝇式的乱翻。我在这道题上也翻了大概十几分钟R、G、B单通道各bit plane色相饱和度亮度全部看了一遍没发现能一眼读出来的二维码或字符串。这里有个很重要的判断如果一张图在各通道和位平面里都看不出明显异常要么是隐写强度极低要么就是需要先做空间定位再提数据——也就是说得先找到信息藏在画面的哪个区域。后者被我忽略了直到我重新注意到题目名里那个face。说实话这个问题我做题时犯过不止一次。面对隐写题大脑默认的模型是信息铺满整张图很少认真想信息可能只藏在画面中某个物体内部。人脸类隐写专门反着来把信息揉进人脸区域利用皮肤纹理的天然噪声做掩护全图性的分析工具在这种题上基本失效。到这一步我把结论锁定为flag在图片的人脸区域内用某种位平面隐藏方式写入。下一步就是把人脸精确找出来。2. face就是解题的钥匙人脸检测定位隐藏区域2.1 为什么想到用人脸检测当时给的是张正面人像图人脸占了画面大约四分之一面积。如果出题人想在图片里藏信息完全可以选择全图LSB但那种做法会在低位平面里留下比较明显的噪点规律不用人脸检测也能靠肉眼或简单工具定位。可这道题在stegsolve里没有这种全局噪点特征更像是信息只集中在一个局部区域。那怎么确定局部在哪题目名已经说了face。直接用现成的人脸检测器把脸找出来比手工用画图工具抠图高效得多。另外一个容易被忽略的点是出题人为了让flag区域藏得更自然通常会选一张以人脸为主体的图让隐写产生的像素扰动集中在五官附近。人眼对皮肤区域的细微颜色变化不敏感但检测器可以稳定地返回人脸框坐标这就把到底在哪提取变成了一个计算机视觉问题。2.2 OpenCV人脸检测的实操细节人脸检测我用的是OpenCV内置的Haar Cascade分类器路径是cv2.data.haarcascades haarcascade_frontalface_default.xml。在OpenCV 4.x里这个路径可以直接用不需要额外下载XML文件算是比较省心的一种方式。import cv2 img cv2.imread(face.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) face_cascade cv2.CascadeClassifier( cv2.data.haarcascades haarcascade_frontalface_default.xml ) faces face_cascade.detectMultiScale( gray, scaleFactor1.05, minNeighbors8, minSize(80, 80) ) print(faces) # 输出形如 [[x, y, w, h]] 的列表这里有几个参数不是随便填的scaleFactor控制每级缩放比例越接近1.0越精确但越慢minNeighbors控制一个区域被检测到的邻域个数越大越不容易误检。对一张2000x1500的高清人像图我习惯用1.05到1.1配minNeighbors8左右既能框准人脸又不会把背景里的纹理误判成脸。我当时跑完返回了一组坐标比如x850, y400, w600, h600。拿到坐标之后有一个很容易犯的错直接把这个矩形区域的像素抠出来去提LSB结果发现信息边缘被切了。原因很简单default.xml框出来的是人脸框但额头到下巴的边界未必和隐写区域的边界完全对齐。所以我一般会额外扩张15到30个像素再裁切这个容错区间很关键。你可以理解为检测器给的是一个大概范围隐写区域可能略大于人脸框留出边缘才能保证数据完整。2.3 人脸区域出现的异常信号裁剪之后我把人脸区域单独拿出来用stegsolve打开翻到蓝色低位平面时看到一条明显的斜向色带仔细看形状居然是半个二维码的定位角。再回看全图这个色带完全被人脸皮肤的自然纹理掩盖了在没有定位的情况下肉眼几乎不可能发现它。从这个现象我们能反推出出题人的思路信息不是随便铺的而是以二维码形式按某种位平面覆盖规则写入到人脸区域的RGB值最低位上。皮肤区域的相邻像素本来就有自然波动加上最低位翻转后肉眼几乎不可察觉所以检测器加区域裁切成了唯一的破局方式。到了这里我已经能确定两个关键条件一是信息区域等于人脸框含少量容错二是隐藏方式为LSB位平面写入。剩下的就是写脚本把低位数据批量提取出来。3. 像素级LSB隐写提取从原理到完整脚本3.1 LSB隐写原理简要回顾LSBLeast Significant Bit隐写的本质非常简单每个像素的颜色值是一个0到255的整数转成二进制后最低位的1或0对最终颜色的影响只有不到1/255。肉眼很难分辨纯白和亮白的差别但机器可以在最低位上稳定地编码信息。具体到RGB三通道图片每个像素就有3个可用的最低位。如果我把一条消息的每个bit依次写到相邻像素的R、G、B分量最低位上整张图在视觉上几乎看不出任何变化但提取端只要把每个像素的RGB值跟1做按位与就能把消息还原。这也是CTF隐写题里最标准的类别之一。区别只在于有些题是顺序写入全图有些是只写入某通道有些像X-man-A face这样指定了空间区域。确认哪种情况直接决定了脚本的取数顺序。3.2 LSB提取脚本的逐步实现我对裁切后的人脸区域做了提取。为了先确认数据形态我先把RGB三通道的最低位分别取出来打印成黑白位图然后再把位流拼成字节写文件自动判断文件头。from PIL import Image import numpy as np img Image.open(face.jpg).convert(RGB) # 这里填入上一步人脸检测给出的坐标并手动扩展30像素 x, y, w, h 850, 400, 600, 600 x - 30 y - 30 w 60 h 60 region np.array(img.crop((x, y, x w, y h))) # 提取每个通道的LSB转成0/1矩阵 r_lsb region[:, :, 0] 1 g_lsb region[:, :, 1] 1 b_lsb region[:, :, 2] 1 # 单独保存位平面图方便肉眼确认异常 Image.fromarray((r_lsb * 255).astype(np.uint8)).save(r_lsb.png) Image.fromarray((g_lsb * 255).astype(np.uint8)).save(g_lsb.png) Image.fromarray((b_lsb * 255).astype(np.uint8)).save(b_lsb.png) # 按常见顺序先把三通道最低位合成一维流再每8bit拼成一个字节 bits np.stack([r_lsb, g_lsb, b_lsb], axis-1).flatten() bit_bytes bits.reshape(-1, 8) data_bytes np.packbits(bit_bytes.astype(np.uint8), axis1).tobytes() with open(extract.bin, wb) as f: f.write(data_bytes) print(extract.bin 写入完成, 共, len(data_bytes), 字节)跑完之后我先看三个位平面图r_lsb.png基本是随机噪声g_lsb.png和b_lsb.png里能看出整齐的块状结构。这种周期性块状结构几乎直接指向二维码因为二维码本身是黑白模块按规律排列转成位平面后自然呈现出规则的网格纹理。肉眼扫到这个形态时我心里已经有八九成把握了。3.3 提取过程中的常见坑用这个脚本的时候我踩过几个坑都是新手容易卡住的地方。第一个坑是数组维度的理解。PIL读进来的RGB图经过np.array之后shape是(H, W, 3)第3个维度的0、1、2对应R、G、B。有些教程会写成arr[..., :, 0]这是错的shape对不上。只要记住这个顺序写循环时就不容易乱。第二个坑是字节对齐。图片隐写不一定保证位数正好是8的倍数直接packbits可能在末尾出现错乱。虽然二维码这类结构化数据通常能自纠错但严谨起见我会先对bits做截断只取前len(bits)//8*8个位避免最后一位的填充导致解析异常。第三个坑更隐蔽LSB写入顺序除了常见的像素从左到右、从上到下每个像素RGB三通道依次取也可能是先全图走完R通道再走G通道再走B通道。我后面的排查里就遇到过类似情况虽然这次用第一个顺序就能提出来但建议所有写LSB脚本的人都把这两种模式都跑一遍再判断结果。我通常写一个小工具函数传入channel_order参数把RGB、RBG、GRB、GBR、BRG、BGR六种排列全部试一遍总有一种能读出有意义的数据。4. 二维码残缺修复与flag还原4.1 为什么提取出来的图像是碎的拿到了extract.bin之后file命令显示它并不是常见的PNG或JPEG开头反而像一段裸的位图数据。这种情况通常有两种可能一是隐写前做了未加密的原始像素存储需要按宽高拼成图二是隐写的就是一段二进制方向不对需要旋转、镜像或换通道顺序重提。我当时的做法是先直接尝试把它当成位图裸数据。二维码的常见尺寸是29x29或33x33个模块如果每个模块在隐写时被放大成8x8像素整张二维码就是232x232或264x264左右。所以我把extract.bin按几种可能的宽高去reshape试了几轮在某个宽度参数下出图已经能看到三个角的回形方框也就是二维码的定位角。到这里基本可以确认提取方向正确只是图像存在一些破损和对比度问题。4.2 补齐二维码的三板斧这种把二维码弄碎、盖住一部分、或者对比度很低的题在CTF里太常见了。我总结过一套三板斧补角、补块、补色。第一斧补角二维码的三个定位角是解码器找形的关键缺一个基本扫不出来。好在LSB提取出来的往往只是对比度偏低定位角结构还在用图像处理把黑白值重新二值化即可。如果真缺角可以用画图工具把回形方框补上只要尺寸和位置对解码器就能重新定位。第二斧补块如果二维码中间缺一块可以直接按周围的方格对齐画上去。此时QR码自带的Reed-Solomon纠错能容忍最多约30%的码字损坏前提是容错级别为H且缺失区域别把某个定位角整个吞掉。以常见的33x33模块版本3二维码为例H级别下可以容忍约90个码字的损坏小块缺失完全不是问题。第三斧补色提取出来的位图经常是灰蒙蒙的直接扫描会失败。我一般会把灰度图转成二值图小于阈值的算黑大于阈值的算白。这一步看起来简单但阈值选多少很看手感。我会先统计直方图看看两个峰值分别在什么位置然后在两个峰值的中间取阈值效果比固定128要好得多。对这道题我在二值化修复之后用pyzbar直接识别成功输出的内容就是题目要的flag。整个过程并不复杂但每一步都是前面排查的必然结果。4.3 二维码识别工具链二维码识别我用过好几套方案在这做个对比方便大家按需选择工具使用难度成功率备注pyzbar低高依赖zbar库pip安装可能遇到dll问题OpenCV QRCodeDetector中中对缺损二维码效果偏弱手机微信/支付宝扫码低高适合人工修复后快速确认在线二维码解码网站低中不建议把flag相关图片传到陌生网站我的习惯是用pyzbar做批量识别遇到识别不了的再人工修复后上手机扫。手机上的二维码引擎在容错和光照适应上普遍做得不错很多时候电脑端读不出的残缺二维码手机一扫就能出结果。经过这套组合flag很快就被拿下了。5. 这类人脸隐写题目的通用套路总结5.1 出题人的思路还原复盘这道题出题人的设计逻辑其实很清晰选一张正面人脸图片作为载体让信息区域只存在于人脸框内然后用LSB把一张二维码写进去。这样整张图在stegsolve里不会像全图LSB那样一眼看出规律必须先在空间上定位才能提到正确数据。套路的精巧之处在于LSB本身不难难的是为什么信息恰好该在这块区域提取。这就是题目名的作用——face直接给出了空间坐标的提示。CTF里很多题都是这样算法不难难的永远是找对使用算法的位置。有了这个定位后面的提取和还原只是体力活。5.2 我的实操经验与避坑清单拿到图片题先做好题名关键词的拆解face、key、hint这类词直接就是解题目录。哪怕不确定也可以作为第一优先级去验证。文件层检查用file、binwalk、strings、exiftool四个命令足够不要一开始就陷进stegsolve里反复翻通道。空间隐写优先用人脸检测、物体检测定位别老想着全图分析。OpenCV的Haar Cascade在正面人像上非常好用dlib的HOG检测器作为备选也可以。裁切隐写区域时记得向外扩大约30像素的容错边防止信息边缘被切掉这一步能省下大量补数据的精力。提取LSB时通道顺序、位平面、区域三者都要做组合尝试把RGB排列和最低位/次低位全部轮一遍成功率会指数级提升。提取结果是裸数据时先file确认格式再按可能的宽高组合去reshape成图片别急着猜格式或丢掉数据。二维码修复的优先级是二值化、补定位角、用H容错级别判断可解码性。工具上pyzbar和手机扫码互补使用效率最高。5.3 还能怎么变着花样出题这类题在比赛里有很多变体我见过不少写出来方便大家举一反三把二维码旋转后再LSB写入提取后需要按方向旋转回去才能识别只取RGB中某一个通道写二维码提取时要选对通道其他通道全是干扰项把二维码拆散写进多个人脸区域需要先检测出所有脸再按顺序合并数据用DCT或DWT在频域隐藏图片这种依靠stegsolve翻不出来需要额外用变换域分析工具在LSB数据上叠加XOR加密提取后还要解一次密钥才能得到二维码。万变不离其宗核心永远是先定位再提取后还原。把X-man-A face从头到尾做一遍这类题以后再出现在赛场上你就知道该往哪个方向下手了。我个人也是从这种题目里慢慢摸透了杂项题的套路——别被题目名吓住也别被花哨的加密术语带走老老实实做文件排查、按线索定位、写脚本提取flag往往就在最朴素的那一步等着你。
返回列表