/* 转载请注明出处:http://www.cnblogs.com/Martinium/p/wechall_stegano.html */
最近迷上了 www.wechall.net 网站,里面都是些与计算机相关的题目挑战。题目又分很多类型,例如:加密与解密、隐写术、网络攻防、趣味编程、数学逻辑等。题目有的简单,有的很难,需要一些知识和技巧。与其他题目挑战的网站不同的是,在其他类似性质的网站注册的用户可以绑定到 WeChall 网站,然后 WeChall 提供排名信息,而且也分得很细,什么按总分全球排名、什么在自己国家的排名、什么解答某种语言网站题目的排名等。可以从解题的人数判断题目的难易程度,有兴趣的朋友可以去注册,解题中也能学到很多知识。国内的学校和公司也有举办网络攻防大赛。与玩 ACM 解题不同的是:ACM 提供给定的输入和输出,中间的黑盒部分需要你来完成,需要提供一种更好更快的算法;这类攻防挑战题目可能需要更多的计算机知识,从一堆垃圾数据中发现有用的信息、越过不同的障碍终于发现答案等。
注:Steganography 是隐写术,它与 Cryptography (密码术)是不同的。
题目1 stegano1 给了一张彩色的 BMP 图片,让你从中发现答案。
不管是图片、音频文件,还是二进制文件,首先都可以尝试以文本文件打开,查看是否存在可疑的字符串。文件太大了就不要这么做了,鼠标会转圈好久的。
图片很小,才 102 bytes。将图片文件拖到文本编辑器里,就可以看到答案了。Notepad++ 是 Windows 上最好的文本编辑器,没有之一。体积小巧,功能齐全。
Linux 上用 gedit 打开,或者使用 vim 命令,输入 :%!xxd 切换到二进制模式,答案显而易见。
xxd 是另外一个命令,以十六进制现实内容。再输入 :%!xxd -r 命令执行,切换到原来的摸样(r 是 reverse )。
注意 vim 的 -b 选项,是否以二进制模式打开。图片是二进制文件,所以要加此选项,否则处理会有问题。
还有一个命令 strings,来的更快,直接输出文件中可打印的字符,通常用来显示二进制中的字符串常量。
strings 命令的选项 -n VALUE 或 --bytes=VALUE 选项控制输出连续字符的最短长度,这个的默认值 VALUE 是 4。
还有 -t RADIX 或 --radix=RADIX 选项以后也会用上的,用来显示每个字符串的偏移量。C++ 的 static 字符串变量需要这个偏移量索引字符串。
你我都是做题的,但是怎么出题呢?破解别人的软件,写一个序列号注册机相对容易。但是如何写一个加壳、混淆的难以破解的程序就有点难度了,毕竟建房子拆房子简单。
我们需要了解一下 BMP 文件格式,BMP 图片很占空间,没有压缩,或者用 RLE (Run-Length Encoding)压缩一下,体积大所以很少在网络上传播。 BITMAPFILEHEADER,BITMAPINFOHEADER,
const char* buffer = "put your message here"; FILE *file = fopen(path, "wb"); if (!file) { fprintf(stderr, "can't open %s ", path); return false; } BITMAPFILEHEADER file_header; BITMAPINFOHEADER info_header; file_header.bfType = (WORD)('B'|'M'<<8); // Windows BMP file tag file_header.bfSize = sizeof(BITMAPFILEHEADER)+sizeof(BITMAPINFOHEADER)+len; file_header.bfReserved1 = 0; file_header.bfReserved2 = 0; file_header.bfOffBits = sizeof(BITMAPFILEHEADER)+sizeof(BITMAPINFOHEADER); info_header.biSize = sizeof(BITMAPINFOHEADER); info_header.biWidth = width; info_header.biHeight = height; info_header.biPlanes = 1; info_header.biBitCount = 24; info_header.biCompression = 0; // BI_RGB, An uncompressed format. info_header.biSizeImage = len; // size in bytes. This may be set to zero for BI_RGB bitmaps info_header.biXPelsPerMeter = 0; info_header.biYPelsPerMeter = 0; info_header.biClrUsed = 0; info_header.biClrImportant = 0; fwrite(&file_header, sizeof(file_header), 1, file); fwrite(&info_header, sizeof(info_header), 1, file); fwrite(buffer, len, 1, file); fclose(file);
这样写,太容易被发现了。我们可以通过对字符串先加密再隐写,或者用循环移位、异或操作增加难度。
知道 BMP 格式支持颜色索引模式(GIF、PNG 格式也支持),图片里会有一个调色板用来存储使用的颜色值,相当于一个数组存储 RGB 值,然后图片只用数组索引值找到对应的颜色并显示出来,于是你也可以这样隐藏你的信息到图片中。我们用两种颜色作图,在白纸写黑字,存储为索引模式图,于是调色板会有两个值 #000000 和 #FFFFFF,颜色只需一位 0/1 就可以表示黑白颜色了(查表法从24位压缩到1位,这个压缩是可观的,其实还可以用 RLE 行程编码对图片继续无损压缩)。将数组中的 #000000 值篡改成 #FFFFFF,也就是黑色改成白色,其他数据不用动,保存后用再打开图片看发现,字消失了!写一些想说的话,然后把图片发给暗恋对象,对方会说你怎么发了一张空白图片,于是留给对方想象的空间。说了这么多,自己动手尝试一下吧~
题目2 Stegano Attachment
答案在图片中吗?不,答案在附件(attachment)里。等你做出了这道题,就明白前面一句话的意思。有时候,题目会给出提示容易忽视的信息,而这个可能就是解题的关键。
稍微留心一下图片 http://www.wechall.net/challenge/training/stegano/attachment/attachment.php ,是不是很坏?后缀被人改成 PHP 网页后缀。用文本编辑软件打开,发现一堆乱码。不同文件会有不同的文件头,即不同的 Magic Number,如上面的 BMP 格式图片的 “BM” 标志,PNG 图片有八字节头 “x89
P
N
G
x1A
”,用 Visual Studio 调式 C++ 代码出现很多的“烫烫烫烫”,其实是编译期初始化栈空间为 “xCCxCCxCCxCC”(0xCC 对应 x86 的 INT 3 中断指令,你应该知道为什么这么做),编译 Java 文件生成的 .class 文件的文件头有 “CAFEBABE”。从 attachment.php 里我们看到了字符 "JFIF",应该就是 JPG 图片了。更好的方法是用 linux 上的 file 命令查看文件类型,文件名后缀随你怎么该。改成 .jpg 后缀后,打开图片后,是电影 Ghostbusters (1984) 的海报,我们追到了这里,电影我没看过,看介绍好像很不错的样子。一边下载一边解题吧~
martin@M2037:~/Desktop$ file attachment.php
attachment.php: JPEG image data, JFIF standard 1.01
我们稍微了解一下 JPEG 格式规范。JPEG 文件以 “xFFxD8” 打头,以 “xFFxD9” 结尾。JPEG/JFIF 文件会出现 “JFIF” 字符,JPEG/Exif 文件会出现 "Exif" 字符。
jpg 图片采用有损压缩,不支持 alpha 通道。编码时,颜色从 RGB 色彩空间转到 YUV 色彩空间,根据颜色分量的重要性和人眼的感知剔除不重要的信息,缩减采样 4:4:4 4:2:2 4:2:0,接着 分割图片成 8x8 子区域进行离散余弦变换,量化编码,支流系数用差分编码,交流系数用 RLE 编码。
我们发现图片是以 "xFFxD8" 开头的,但是结尾却不是 "xFFxD9",好像明白了什么。
我们用题目1学到的 strings 命令搜到了可疑字符串 solution.txt 和 solution.txtPK,恰恰是放文件最后的。
PK 字眼熟悉不?最常用的 ZIP 格式压缩文件头的 magic number 是 “PKx03x04”,而且文件中确实存在这四个字节。而且这四个字节的前面也恰好是 "xFFxD9"。想想之前的题目 attachment。看来,是谁把一个 ZIP 压缩文件追加到一个 JEPG 图像文件的后面了。我们需要提取出 ZIP 文件,答案离我们不远了。。
ZIP 文件的偏移量用题目1学到的 vim :%!xxd 搜索定位查看并计算,或者用 grep 命令搜索下。
我们需要复制从 0004f06 到最后 0004f8d (长度 0x88=136)到新的文件里去。
复制粘贴文件或者段落用 Ctrl+C、Ctrl+V,可是复制二进制文件流呢,也可以这么做吗?好像不行。
Notepad++ 不愧是最好用最实用的文本编辑器,选择菜单 Edit -> Paste Special,二级菜单中出现了 Copy|Cut|Paste Binary Cotent,接下来你知道怎么做啦~
或者使用 linux 的 dd 命令,还记得大学初学 linux 操作系统时,让记那么多命令和参数干嘛,现在就派上用场了。
如果你不嫌麻烦,甚至可以自己编写小段 C/C++ 代码,打开文件、复制文件、关闭文件。
martin@M2037:~/Desktop$ printf "%d
" 0x0004f06
20230
martin@M2037:~/Desktop$ dd if=attachment.jpg of=solution.zip skip=20230 bs=1 count=136
136+0 records in
136+0 records out
136 bytes (136 B) copied, 0.0021692 s, 62.7 kB/s
martin@M2037:~/Desktop$ unzip solution.zip
Archive: solution.zip
inflating: solution.txt
打开 solution.txt 文件,就是最终的答案了。
从中,我们学习了另外一种隐藏文件的方法,将一个文件附属到另外一个文件末尾,打开的时候只解析第一个文件,附属文件被忽略了。
将信息 message.txt 隐藏到图片 carrier.jpg 中(注意命令行中的先后顺序):
linux 终端下, cat carrier.jpg message >> mixed.jpg
Windows 控制台下,copy carrier.jpg /b + message.txt /a mixed.jpg
cat 是 concatenate 的简写,如果有多个序列文件(比如 file1.txt file2.txt file3.txt)合并,可以优雅地写成 cat file{1,2,3}.txt >> new.txt
/A /B 分别代表文本文件和二进制文件,详情请查看 copy /? 命令。
题3 LSB - The least significant bit
WeChall 网站使用了 cookie 和 session 来产生动态的答案,所以不同的人在每一次登录时,答案是不同的。我在这里贴出我的答案,你复制过去一般就是错误的答案,这样可以提防不动脑子只粘贴答案的人。
这道题目有链接提示,链接的颜色是 #EEE,接近背景色白色 #FFF,还是很明显看得出来的,看来作者没有有意隐藏这一提示。家里网没能打开这个链接,原因还没查出,公司网可以打开。steganabara 原来是一个 .jar 包,下载 steganabara.jar 文件并运行程序 java -jar steganabara-1.1.1.jar ,将隐藏信息的图片拖进来。似乎是很正常的一张图片,LED 数码管显示了作者的大名 Gizmore。查了一下,gizmor 英文是小发明的意思。当然,这只是作者创作题目的署名,与解题没有任何关系。解题偶尔能有额外的收获。
这张图片是 RGB 格式,没有 alpha 通道,随便勾选 RGB 某一通道的某一位,共有 3*8=24 种单项选择(复合选择暂未考虑进来,从简单到复杂嘛~)。点击菜单 Filter -> Bit Mask,从低位到高位一个一个勾选盲试的话,很容易试出答案来。思路是这样的,你的答案不一定是这样的。注意勾上 Amplify 选项,否则很难辨认。
借助事实分析和判断,点击菜单 Analyse -> Histogram 给出 RGB 各分量的柱状图,范围是 0~255,有 3 个连续分布图和 1 个离散分布(斑马带)图,可以怀疑出是哪个通道有问题。相邻柱子的间距是周期,可以推断出是哪个比特位被篡改,导致出现重复的模式。
LSB (least significant bit) 最低有效位,权重最小的位,也是修改最不容易引起注意的位;与之对应的是 MSB (most significant bit) 最高有效位,修改后很容易被察觉。上面之所以需要增强信号(Amplify) 就是因为隐藏的信息在 LSB 上不易被发觉。这个是图片与图片的操作,我想到了一种字符串与图片混合的办法。
将字符串信息(也就是需要隐藏的信息 ASCII 码值)拆开成比特位,分别藏在图像中的连续像素中的最低位。思路是读取承载信息的图片,然后把需要隐藏的信息以比特流的形式连续覆盖图片 RGB 值的最低位,为了进一步增加难度,可以只覆盖其中某一个分量,可以不用放最低位,如果比特位用完了,信息可以从头循环再覆盖,直到覆盖所有。例如此题采用的是 Blue 通道的第 4 位。
#include <stddef.h> #include <stdlib.h> #include <stdint.h> #include <string.h> #include <stdio.h> #include "libpng16/png.h" int main() { png_image image; memset(&image, 0, sizeof image); image.version = PNG_IMAGE_VERSION; image.format = PNG_FORMAT_RGB; // no alpha channel const char* message = "No 300 taels of silver buried here."; const int BITS = strlen(message) * CHAR_BIT; const char* original = "g2YMC634NSqoEhXe.png"; const char* stegano = "stegano.png"; if(png_image_begin_read_from_file(&image, original)) { size_t size = PNG_IMAGE_SIZE(image); png_bytep buffer = (png_bytep)malloc(size); if(buffer != NULL) { // blend text bits to image's least significant bit int bit = 0; for(size_t i = 0; i < size; ++i) { char test = (original[bit/8] & (1<<(bit%8))) ?1:0; buffer[i] = (buffer[i]&0xFE) | test; ++bit; if(bit >= BITS) bit = 0; // repeat message } if(png_image_finish_read(&image, NULL/*background*/, buffer, 0/*row_stride*/, NULL/*colormap for PNG_FORMAT_FLAG_COLORMAP*/)) { if(png_image_write_to_file(&image, stegano, 0/*convert_to_8bit*/, buffer, 0/*row_stride*/, NULL/*no colormap*/)) fprintf(stdout, "write to file %s successfully. ", stegano); else fprintf(stderr, "write %s: %s ", stegano, image.message); free(buffer); } else { fprintf(stderr, "read %s: %s ", original, image.message); /* This is the only place where a 'free' is required; libpng does * the cleanup on error and success, but in this case we couldn't * complete the read because of running out of memory. */ png_image_free(&image); } } else fprintf(stderr, "out of memory: %lu bytes ", (unsigned long)PNG_IMAGE_SIZE(image)); } else fprintf(stderr, "%s: %s ", original, image.message); return 0; }
to be continued...