ICHARM
【C++】将文件数据隐藏进BMP图片的像素中代码分析
【C++】将文件数据隐藏进BMP图片的像素中代码分析

最近哀差闷偶然看到一个把文件隐藏进Bmp图片文件像素里的代码,感觉十分的炫酷吧,特此拿来研究研究一番。

原理

首先要说的是:BMP图片必须是24位的,而且载体(bmp文件)最好远大于待隐藏文件,不然加密后的载体图片看起来有异常。

bmp字节码结构分析

下图是bmp文件的基本结构图:

 

http://www.icharm.me/wp-content/uploads/2015/12/sasdasdasdda.png

Windows对bmp文件头定义了如下的结构体:

typedef struct tagBITMAPFILEHEADER  {   
    UINT16 bfType;     
    DWORD bfSize;  
    UINT16 bfReserved1;  
    UINT16 bfReserved2;  
    DWORD bfOffBits; 
} BITMAPFILEHEADER;

下面是bmp文件的16进制字节码:
http://www.icharm.me/wp-content/uploads/2015/12/20151215205424.jpg
蓝色下划线前面画了4个框,分别对应着上面结构体中的5个变量:

  1. 0-1 (bfType):424Dh = ‘BM’表示这是Windows支持的位图格式。有很多声称开头两
    个字节必须为’BM’才是位图文件,从上表来看应为开头两个字节必须为’BM’才是Windows位图文件。
  2. 2-5 (bfSize):00282B36h = 2632502B = 2.6mB 表示该文件的大小
  3. 6-7,8-9 分别表示bfReserved1,bfReserved2。这两个是保留段,都为0
  4. A-D (bfOffBits):00000036h = 54 表示从位图头位置到保存位图数据位置需要偏移的字节数。这个在本程序中特别重要!

蓝色划线部分是 位图信息段和调色板信息段。需要注意的是:位图文件头是固定大小14字节,位图信息段是固定大小40字节。

bmp分很多位的类型,有24位,16位等,有些位类型的bmp文件是有调色板信息段的,而且调色板信息段大小不固定。在位图信息段储存有图片的尺寸等信息。

而在本程序中使用的是24位Bmp类型,而这种类型是没有调色板信息段的,所以偏移的字节数 = 位图文件头大小 + 位图信息段大小 =54。 所以在24位bmp中偏移量是固定的为54。

具体的有关bmp文件结构的分析可以看这里:bmp文件格式详解

蓝色划线后面的是位图实际数据,每三个字节表示一个像素的RGB三原色数据。

所以我们要做的就是,就是把需要把隐藏的数据每3个字节每3个字节的插进各个像素之间(即在bmp的字节码中每隔三个字节插入三个字节的需要隐藏的数据)。

WindowsAPI知识点

源码中涉及很多的WindowsAPI的操作,哀差闷对这方面还是十分陌生的。所以有必要来总结下。

CreateFile()函数 :如执行成功,则返回文件句柄。INVALID_HANDLE_VALUE表示出错,会设置GetLastError。即使函数成功,但若文件存在,且指定了CREATE_ALWAYS 或 OPEN_ALWAYS,GetLastError也会设为ERROR_ALREADY_EXISTS

参数类型及说明
函数声明HANDLE CreateFile(
    LPCTSTR lpFileName, //普通文件名或者设备文件名
    DWORD dwDesiredAccess, //访问模式(写/读)
    DWORD dwShareMode, //共享模式
    LPSECURITY_ATTRIBUTES lpSecurityAttributes, //指向安全属性的指针
    DWORD dwCreationDisposition, //如何创建
    DWORD dwFlagsAndAttributes, //文件属性
    HANDLE hTemplateFile //用于复制文件句柄
);
有关CreatFile函数的详细使用方法,可以查MSDN或者百度百科

GetFileSize()函数:如果函数调用成功,则返回值为文件大小的低位双字,lpFileSizeHigh返回文件大小的高阶双字。如果函数返回值为INVALID_FILE_SIZE,并且GetLastError函数返回值非NO_ERROR,则函数调用失败。(文件大小的低位双字即文件的实际大小,只有在文件的大小超过DWORD的范围时,即大于40G的时候才会用到返回的高阶双字lpFileSizeHigh)
参数类型及说明
DWORD WINAPI GetFileSize(
    HANDLE hFile,
    LPDWORD lpFileSizeHigh
);
hFile Long:文件的句柄。
lpFileSizeHigh Long:指定一个长整数,用于装载一个64位文件长度的头32位。如这个长度没有超过2^32个字节,则该参数可以设为NULL(变成ByVal)。

ReadFile()函数:从文件指针指向的位置开始将数据读出到一个文件中, 且支持同步和异步操作。

参数类型及说明

BOOL ReadFile(
HANDLE hFile, //文件的句柄
LPVOID lpBuffer, //用于保存读入数据的一个缓冲区
DWORD nNumberOfBytesToRead, //要读入的字节数
LPDWORD lpNumberOfBytesRead, //指向实际读取字节数的指针
LPOVERLAPPED lpOverlapped
//如文件打开时指定了FILE_FLAG_OVERLAPPED,那么必须,用这个参数引用一个特殊的结构。
//该结构定义了一次异步读取操作。否则,应将这个参数设为NULL
);

有关ReadFile函数的详细使用方法,可以查MSDN或者百度百科


SetFilePointer()函数用来移动文件指针,它和 Unix 中的 lseek() 函数以及 C 库中的 fseek() 函数是类似的。利用这个函数,可以处理那些长度超过4GB,但小于2^64字节的大型文件

参数类型及说明

DWORD SetFilePointer(
HANDLE hFile, // 文件句柄
LONG lDistanceToMove, // 偏移量(低位)
PLONG lpDistanceToMoveHigh, // 偏移量(高位)
DWORD dwMoveMethod // 基准位置FILE_BEGIN:文件开始位置 FILE_CURRENT:文件当前位置 FILE_END:文件结束位置
说明:移动一个打开文件的指针
 有关SetFilePointer函数的详细使用方法,可以查MSDN或者百度百科

WriteFile()函数:将数据写入一个文件。该函数比fwrite函数要灵活的多。也可将这个函数应用于对通信设备、管道、套接字以及邮槽的处理,返回值TRUE(非零)表示成功,否则返回零。会设置GetLastError
参数类型及说明
BOOL WriteFile(
HANDLE hFile,//文件句柄
LPCVOID lpBuffer,//数据缓存区指针
DWORD nNumberOfBytesToWrite,//你要写的字节数
LPDWORD lpNumberOfBytesWritten,//用于保存实际写入字节数的存储区域的指针
LPOVERLAPPED lpOverlapped//OVERLAPPED结构体指针
);
 有关WriteFile函数的详细使用方法,可以查MSDN或者百度百科

源码分析

// HideTextInBmp.cpp : 定义控制台应用程序的入口点。
//

#include "stdafx.h"
#include<iostream>
#include<windows.h>
#include<winsock.h>
using namespace std;


//读取文件内容的函数
//DWORD 表示一个32位无符号整形,windows下经常用来保存地址,在windows.h中有定义#define DWORD unsigned long
char * ReadFileContent(char * fileName, DWORD * fileSize){
	
	/*CreateFile函数详解 http://blog.sina.com.cn/s/blog_49851d030100096h.html    http://www.cppblog.com/yishanhante/articles/19545.html */
	HANDLE hfile = CreateFile(fileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE,0,OPEN_EXISTING,0,0);
	if (hfile == INVALID_HANDLE_VALUE){
		cout << "打开" << fileName << "文件失败!\n";
		return false;
	}
	DWORD dwRead;
	//获取要读取的文件大小
	//GetFileSize函数用来获取指定文件的大小,第一个参数为指定文件的句柄,第二个参数为一个高位的字节(当指定文件的大小超过4G(DWORD的范围)时才起到作用)
	DWORD dwSize = GetFileSize(hfile, &dwRead);
	*fileSize = (char)dwSize;

	//声明一个dwSize大小的指针数组
	char * buf = new char[dwSize];
	//初始化该指针数组,即把该指针数组的内存空间全部赋为0
	RtlZeroMemory(buf, sizeof(buf));
	//把文件内容读入进buf数组中
	ReadFile(hfile, buf, dwSize, &dwRead, 0);
	if (dwRead != dwSize){
		cout << "读取文件内容出错" << endl;
		return false;
	}
	//关闭文件句柄
	CloseHandle(hfile);
	return buf;

}

//写入文件的函数
bool SaveFile(char * fileName, char *buf, int len){
	HANDLE  hfile = CreateFile(fileName, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, NULL, NULL);
	if (hfile == INVALID_HANDLE_VALUE){
		cout << "打开" << fileName << "文件失败!" << endl;
		return false;
	}
	DWORD dwWrite;//保存写了多少字节到文件中去了
	SetFilePointer(hfile, 0, 0, FILE_BEGIN);
	WriteFile(hfile, buf, len, &dwWrite, 0);
	CloseHandle(hfile);
	return true;
}
bool Hide(char * SecretFileName, char * BmpFileName){
	DWORD bmpSize, secretSize;
	char * pBmp = ReadFileContent(BmpFileName, &bmpSize);	
	char * pSecretFile = ReadFileContent(SecretFileName, &secretSize);
	DWORD *lpFirstPx = (DWORD *)(pBmp + 10);
	cout << "第一个像素点的偏移量是:" << *lpFirstPx << endl;

	char * pCurrentBmp = pBmp + *lpFirstPx +3;
	char * pCurrentSecret = pSecretFile;
	//机密文件有多大,重要的参数
	*((DWORD *)pCurrentBmp) = secretSize;
	pCurrentBmp += 6;
	for (; pCurrentBmp < (pBmp + bmpSize) && pCurrentSecret<= pSecretFile; pCurrentBmp+=6){
		*pCurrentBmp = *pCurrentSecret;
		*(pCurrentBmp + 1) = *(pCurrentSecret + 1);
		*(pCurrentBmp + 2) = *(pCurrentSecret + 2);
		pCurrentSecret += 3;
	}

	SaveFile(BmpFileName, pBmp, bmpSize);
	delete[] pBmp;
	delete[] pSecretFile;
	return true;
}
bool Recovery(char * BmpFileName, char * SecretFileName){
	DWORD bmpSize;
	char * pBmp = ReadFileContent(BmpFileName, &bmpSize);
	
	DWORD *pFirstPoint = (DWORD *)(pBmp + 10);
	DWORD secretSize = *(DWORD *)(pBmp + *pFirstPoint + 3);
	cout << "已恢复" << secretSize << "字节的数据。" << endl;

	char * buf = new char[secretSize];
	char * pCurrentBmp = pBmp + *pFirstPoint +3 +6;
	for (int i = 0; pCurrentBmp < pBmp + bmpSize; pCurrentBmp += 6){
		
		buf[i] = *pCurrentBmp;
		buf[i + 1] = *(pCurrentBmp + 1);
		buf[i + 2] = *(pCurrentBmp + 2);
		i += 3;
	}
	SaveFile(SecretFileName, buf, secretSize);
	delete[] pBmp;
	delete[] buf;
	return true;
}
int _tmain(int argc, _TCHAR* argv[])
{
	//如果命令行传进来的参数小于3,则提示这个程序的使用方法。
	if (argc < 3){  
		cout << "Usage: " << argv[0] << " Encrypt secret_File_Name BMP_FileName\n";
		cout << "Usage: " << argv[0] << " Decrypt BMP_FileName secret_File_Name\n";
		return -1;
	}

	//strcmp函数:比较两个字符串,若相等则返回0
	//若第二个参数为Encrypt,则执行Hide(加密)函数。。。
	if (strcmp(argv[1], "Encrypt") == 0){
		Hide(argv[2], argv[3]);
	}
	else if(strcmp(argv[1], "Decrypt") ==0 ){
		Recovery(argv[2], argv[3]);
	}
	else{
		cout << "Invalid para!" << endl;
			return -1;
	}
	cout << "Done!" << endl;
	return 0;
}

总结

这个程序的原理还是比较简单的,虽然实用性不是很大。但从中学习到了WindowsAPI的一些操作方法。还是不错的。

其次这个程序还是有很大的优化空间的,首先载体bmp文件应该要有调色板的比较好,然后将数据一个字节的一个字节的写进每个像素的alpha通道。

在上面的程序中,并不是将待隐藏的文件数据插入进像素之间,而是直接覆盖像素。隔一个像素覆盖一个像素。这样对图片的破坏太大。

在这个程序的基础上可以做很多的优化,打造一款专业的文件隐藏工具。

发表评论

textsms
account_circle
email

ICHARM

【C++】将文件数据隐藏进BMP图片的像素中代码分析
最近哀差闷偶然看到一个把文件隐藏进Bmp图片文件像素里的代码,感觉十分的炫酷吧,特此拿来研究研究一番。 原理 首先要说的是:BMP图片必须是24位的,而且载体(bmp文件)最好远大于待…
扫描二维码继续阅读
2015-12-16