Title:《LSB算法分析及實現》
Author: Hugu
Started Date: Oct. 10th. 2019.
Finished Date: Oct. 11th. 2019.
數字水印
數字水印(Digital Watermark)是一種應用計算機算法嵌入載體文件的保護信息,數字水印技術是一種基於內容的、非密碼機制的計算機信息隱藏技術,它是將一些標識信息(即數字水印)直接嵌入數字載體當中(包括多媒體、文檔、軟件等)或是間接表示(修改特定區域的結構),且不影響原載體的使用價值,也不容易被探知和再次修改,但可以被生產方識別和辨認。
常用的數字水印有很多種,LSB是其中一種簡單的隱寫算法。
LSB簡介
LSB全稱爲 Least Significant Bit(最低有效位),是一種常被用做圖片隱寫的算法(在CTF中經常見到她的身影)。LSB屬於空域算法中的一種,是將信息嵌入到圖像點中像素位的最低位,以保證嵌入的信息是不可見的,但是由於使用了圖像不重要的像素位,算法的魯棒性差,水印信息很容易爲濾波、圖像量化、幾何變形的操作破壞。
算法分析
PNG和BMP圖片中的圖像像素一般是有由RGB三原色組成(如圖1所示),每一種顏色佔用8位,取值範圍爲0x00~0xFF,既有種色值。而人類的眼睛可以區分約1000萬種不同的顏色,這就意味着人類眼睛無法區分的顏色還有600多萬。
當僅僅更改顏色分量的最低位時,人類的眼睛不能區分這前後的變化,LSB就是在該位置存放信息。如圖2所示。
實現步驟
- 將圖像文件中的所有像素點以RGB形式分隔開,並將各個顏色分量轉換成二進制表示
- 把每個顏色分量值的最後一位全部設置成0,對圖像得影響非常細微,不會影響圖像的顯示格式
- 信息嵌入:將水印字符轉化爲二進制字符串,並將這些信息依次填入顏色分量的最低位上,即可完成信息的嵌入
- 信息提取:將圖像像素的最低位依次提取出來,並進行拼接,即可得到原始信息
程序設計
嵌入信息框圖:
提取信息框圖:
文檔結構:
程序主界面:
主界面代碼:
using System;
using System.Windows.Forms;
namespace LSBAlgorithmDemo
{
public partial class MainFrm : Form
{
#region 定義全局變量
Form frm;
#endregion
#region 構造函數
public MainFrm()
{
InitializeComponent();
}
#endregion
#region 嵌入按鈕得Click事件
private void btnEnbed_Click(object sender, EventArgs e)
{
frm = new InfoEmbedmentFrm();
this.Hide();
if(frm.ShowDialog() == DialogResult.Cancel)
{
this.Show();
}
else
{
Application.Exit();
}
}
#endregion
#region 提取按鈕的Click事件
private void btnExtract_Click(object sender, EventArgs e)
{
frm = new InfoExtractionFrm();
this.Hide();
if (frm.ShowDialog() == DialogResult.Cancel)
{
this.Show();
}
else
{
Application.Exit();
}
}
#endregion
}
}
嵌入界面:
嵌入界面代碼:
using System;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
namespace LSBAlgorithmDemo
{
public partial class InfoEmbedmentFrm : Form
{
#region 定義全局變量
private string fileName;
private LSBHelper lsb;
private string msg = null;
private MemoryStream memStream = null;
#endregion
#region 構造函數
public InfoEmbedmentFrm()
{
InitializeComponent();
// 初始化界面
Form_Init(0);
}
#endregion
#region 設置顯示界面
/// <summary>
/// 設置顯示界面
/// </summary>
/// <param name="flag">0:初始化、下載之後;1:圖片加載之後;2:圖片嵌入之後
/// </param>
private void Form_Init(int flag)
{
if(flag == 0)//初始化、下載之後
{
// 設置界面顯示的起始位置
this.StartPosition = FormStartPosition.CenterScreen;
// PictureBox控件
pBoxDisplay.SizeMode = PictureBoxSizeMode.StretchImage;
// 兩個必須都要設置(因兩次的加載途徑不一樣)
pBoxDisplay.ImageLocation = null;
pBoxDisplay.Image = null;
// RichTextBox控件
rtbInfo.Clear();
rtbInfo.ReadOnly = true;
// Button控件
btnDownloadImage.Enabled = false;
btnEmbedInfo.Enabled = false;
btnLoadImage.Enabled = true;
btnLoadImage.Focus();
}
else if(flag == 1)//圖片加載之後
{
// RichTextBox控件
rtbInfo.Clear();
rtbInfo.ReadOnly = false;
rtbInfo.Focus();
// Button控件
btnDownloadImage.Enabled = false;
btnEmbedInfo.Enabled = true;
btnLoadImage.Enabled = true;
}
else if(flag == 2)//圖片嵌入之後
{
// Button控件
btnDownloadImage.Enabled = true;
btnEmbedInfo.Enabled = true;
btnLoadImage.Enabled = true;
btnDownloadImage.Focus();
}
}
#endregion
#region 加載圖片的事件
private void btnLoadImage_Click(object sender, EventArgs e)
{
// 再次單擊加載圖片按鈕時,回到初始化狀態
Form_Init(0);
// 設置打開對話框的標題
openImageDialog.Title = "請選擇一張位圖";
// 對文件格式進行篩選
openImageDialog.Filter = "bmp | *.bmp;*.BMP";
// 默認設置爲空
openImageDialog.FileName = "";
if (openImageDialog.ShowDialog() == DialogResult.OK)
{
// 存儲打開文件的全文件名
fileName = openImageDialog.FileName;
// 設置顯示圖片的樣式
pBoxDisplay.SizeMode = PictureBoxSizeMode.StretchImage;
// 加載要顯示的圖片
pBoxDisplay.ImageLocation = fileName;
// 設置顯示界面
Form_Init(1);
}
}
#endregion
#region 嵌入文本的事件
private void btnEmbedInfo_Click(object sender, EventArgs e)
{
// 已經在界面設置中代替
if (string.IsNullOrWhiteSpace(fileName))
{
MessageBox.Show("請選擇一張位圖", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
else if (string.IsNullOrWhiteSpace(rtbInfo.Text))
{
MessageBox.Show("請輸入要嵌入的文本信息", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
else
{
lsb = new LSBHelper();
if (lsb.EmbedInfo(fileName, rtbInfo.Text, out msg, out memStream))
{
pBoxDisplay.ImageLocation = null;
//pBoxDisplay.ImageLocation = "C:\\Users\\ss\\Desktop\\664204.bmp";
pBoxDisplay.Image = Image.FromStream(memStream);
MessageBox.Show(msg, "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
// 設置顯示界面
Form_Init(2);
}
else
{
MessageBox.Show(msg, "提示", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}
#endregion
#region 下載圖片按鈕觸發的事件
private void btnDownloadImage_Click(object sender, EventArgs e)
{
if (memStream != null)
{
saveImageDialog.Filter = "位圖(*.bmp)|*.bmp";
if (saveImageDialog.ShowDialog() == DialogResult.OK)
{
FileStream fs = null;
BinaryWriter bw = null;
try
{
fs = new FileStream(saveImageDialog.FileName, FileMode.Create, FileAccess.Write);
bw = new BinaryWriter(fs);
// 指針回位(每次操作之前都要先操作一下)
memStream.Position = 0;
while (memStream.Position != memStream.Length)
{
// ReadByte()強制轉換成Int32類型
byte write = Convert.ToByte(memStream.ReadByte());
bw.Write(write);
}
MessageBox.Show("圖片下載成功", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
//設置顯示界面
Form_Init(0);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
finally
{
fs.Close();
bw.Close();
}
}
}
}
#endregion
}
}
提取界面:
提取界面代碼:
using System;
using System.IO;
using System.Windows.Forms;
namespace LSBAlgorithmDemo
{
public partial class InfoExtractionFrm : Form
{
#region 定義全局變量
private string fileName;
private MemoryStream memorystream = null;
private string info = null;
private string msg = null;
#endregion
#region 構造函數
public InfoExtractionFrm()
{
InitializeComponent();
// 初始化展示界面
Form_Init(0);
}
#endregion
#region 界面設置操作
/// <summary>
/// 界面設置操作
/// </summary>
/// <param name="flag">
/// 0:初始化
/// 1:提取之後
/// </param>
private void Form_Init(int flag)
{
if(flag == 0)
{
// 設置界面顯示位置
this.StartPosition = FormStartPosition.CenterScreen;
// 設置PictureBox控件
pBoxDisplay.SizeMode = PictureBoxSizeMode.StretchImage;
pBoxDisplay.ImageLocation = null;
// 設置RichTextBox控件
rtbInfo.Clear();
rtbInfo.ReadOnly = true;
// 設置Button控件
btnLoadImage.Enabled = true;
btnExtractInfo.Enabled = false;
}
else if(flag == 1)
{
// 設置RichTextBox控件
rtbInfo.Clear();
// 設置Button控件
btnLoadImage.Enabled = true;
btnExtractInfo.Enabled = true;
}
}
#endregion
#region 加載圖片
private void btnLoadImage_Click(object sender, EventArgs e)
{
Form_Init(0);
// 設置打開對話框的標題
openImageDialog.Title = "請選擇一張已嵌入信息的位圖";
// 對文件格式進行篩選
openImageDialog.Filter = "bmp | *.bmp;*.BMP";
openImageDialog.FileName = "";
if (openImageDialog.ShowDialog() == DialogResult.OK)
{
// 存儲打開文件的全文件名
fileName = openImageDialog.FileName;
// 設置顯示圖片的樣式
pBoxDisplay.SizeMode = PictureBoxSizeMode.StretchImage;
// 加載要顯示的圖片
pBoxDisplay.ImageLocation = fileName;
Form_Init(1);
}
}
#endregion
#region 提取信息
private void btnExtractInfo_Click(object sender, EventArgs e)
{
// 圖片是否加載
if (string.IsNullOrWhiteSpace(fileName))
{
MessageBox.Show("請選嵌入文字的位圖", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
else
{
try
{
LSBHelper lsb = new LSBHelper();
// 將圖像存入內存流中
using (FileStream fileReadStream = new FileStream(fileName, FileMode.Open, FileAccess.Read))
{
using (BinaryReader br = new BinaryReader(fileReadStream))
{
byte write;
memorystream = new MemoryStream();
for (int i = 1; i <= fileReadStream.Length; i++)
{
write = br.ReadByte();
memorystream.WriteByte(write);
}
// 回位操作
memorystream.Position = 0;
}
}
// 信息提取
if (lsb.ExtractInfo(memorystream, out info, out msg))
{
// 將提取到的信息展示在文本控件中
rtbInfo.Text = info;
}
else
{
MessageBox.Show(msg, "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
catch (Exception ex)
{
MessageBox.Show(ex.Message, "警告", MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
finally
{
// 釋放佔用的資源
memorystream.Dispose();
}
}
}
#endregion
}
}
LSBHelper代碼:
using System;
using System.IO;
namespace LSBAlgorithmDemo
{
public class LSBHelper
{
#region 定義全局變量
FileStream fileReadStream = null;
MemoryStream memStream = null;
BinaryReader br = null;
#endregion
#region 用於判斷圖片是否爲Bitmap格式
/// <summary>
/// 用於判斷圖片是否爲Bitmap格式
/// </summary>
/// <param name="imgFormat">圖片前幾個字節</param>
/// <returns>返回判斷結果</returns>
private bool IsBitmap(byte[] imgFormat)
{
// 判斷類型表示字節是否爲2
if (imgFormat.Length == 2)
{
// 判斷標識字節是否爲:42 4d
if (imgFormat[0] == 0x42 && imgFormat[1] == 0x4d)
{
// 是Bitmap
return true;
}
else
{
return false;
}
}
else
{
return false;
}
}
#endregion
#region 判斷文本信息能否寫入圖片中
/// <summary>
/// 判斷文本信息能否寫入圖片中
/// </summary>
/// <param name="streamSize">圖片流的字節個數</param>
/// <param name="infoSize">文本信息的字符個數</param>
/// <returns>返回判斷結果</returns>
private bool CanEmbed(long streamSize, long infoSize)
{
if (streamSize < (infoSize * 16 + 54))
{
return false;
}
else
{
return true;
}
}
#endregion
#region 判斷指定文件是否存在
/// <summary>
/// 判斷指定文件是否存在
/// </summary>
/// <param name="fullFileName">文件全名稱</param>
/// <returns>返回判斷結果</returns>
private bool IsFileExists(string fullFileName)
{
return File.Exists(fullFileName);
}
#endregion
#region 判斷圖像中是否寫入信息
private bool IsEmabed(MemoryStream memorystream)
{
// 復位
memorystream.Position = 0;
memorystream.Seek(54L, 0);
byte reader;
char ch;
int temp = 0;
int state = 0;
int index = 0;
while((index = memorystream.ReadByte()) > 0 && index != memorystream.Length)
{
temp = 0;
for (int k = 0; k < 16; k++)
{
reader = Convert.ToByte(memorystream.ReadByte());
temp += (reader & 0x01) << k;
}
ch = Convert.ToChar(temp);
if (ch == '#')
state++;
}
if (state == 2)
{
return true;
}
else
{
return false;
}
}
#endregion
#region 將待寫入圖片的信息轉成"# + info + #"的格式並儲存在tempChs字符數組中
/// <summary>
/// 將待寫入圖片的信息轉成"# + info + #"的格式並儲存在tempChs字符數組中
/// </summary>
/// <param name="info"></param>
/// <returns></returns>
private char[] GetTempChsFromInfo(string info)
{
// 將文本信息轉成Unicode字符值並存儲在char數組中
char[] infoToChs = new char[info.Length];
for (int i = 0; i < info.Length; i++)
{
infoToChs[i] = Convert.ToChar(info[i]);
}
// 將待寫入圖片的信息轉成指定格式並儲存在tempChs字符數組中
char[] tempChs = new char[(info.Length + 2) * 16];
// 前導符“#”:16位
for (int n = 0; n < 16; n++)
{
tempChs[n] = Convert.ToChar(0x0001 & ('#' >> n));
}
// 原主要信息
int k;
for (k = 1; k < infoToChs.Length + 1; k++)
{
for (int n = 0; n < 16; n++)
{
tempChs[16 * k + n] = Convert.ToChar(0x0001 & infoToChs[k - 1] >> n);
}
}
// 後導符“#”:16位
for (int n = 0; n < 16; n++)
{
tempChs[16 * k + n] = Convert.ToChar(0x0001 & '#' >> n);
}
return tempChs;
}
#endregion
#region 向Bitmap中嵌入信息
public bool EmbedInfo(string fullFileName, string info, out string msg, out MemoryStream memory)
{
// 判斷指定圖片是否存在
if (!IsFileExists(fullFileName))
{
msg = "請確認指定的圖片是否存在";
memory = null;
return false;
}
try
{
// 得到圖片的字節流讀取器
fileReadStream = new FileStream(fullFileName, FileMode.Open, FileAccess.Read);
br = new BinaryReader(fileReadStream);
// 讀取圖片的標識符
byte[] imgFormat = new byte[2];
imgFormat[0] = br.ReadByte();
imgFormat[1] = br.ReadByte();
br.BaseStream.Position = 0;
// 判斷指定圖片是否爲Bitmap格式
if (!IsBitmap(imgFormat))
{
msg = "請選擇Bitmap格式的圖片";
memory = null;
return false;
}
// 判斷指定圖片是否能夠寫下信息
if (!CanEmbed(fileReadStream.Length, info.Length))
{
msg = "該圖片無法寫下指定的全部信息,請選擇更大尺寸的Bitmap圖片";
memory = null;
return false;
}
// 將待寫入圖片的信息轉成指定格式並儲存在tempChs字符數組中
char[] tempChs = GetTempChsFromInfo(info);
// 使用LSB算法將整合後的文件流寫入內存流中
memStream = new MemoryStream();
byte write;
for (int i = 1, j = 0; i <= fileReadStream.Length; i++)
{
write = br.ReadByte();
// 前54字節不能動
if (i <= 54)
{
memStream.WriteByte(write);
}
else // 在數據區寫入信息
{
if (j < tempChs.Length)
{
memStream.WriteByte(Convert.ToByte((write & 0xfe) + tempChs[j]));
}
else
{
memStream.WriteByte(write);
}
j++;
}
}
memStream.Position = 0;
msg = "信息已成功嵌入圖片中";
memory = memStream;
return true;
}
catch (Exception ex)
{
msg = ex.Message;
memory = null;
return false;
}
finally
{
// 執行完畢之後一定要關閉創建的資源
fileReadStream.Close();
//memStream.Close();
br.Close();
}
}
#endregion
#region 提取Bitmap中的信息
public bool ExtractInfo(MemoryStream memorystream, out string info, out string msg)
{
#region 讀取圖片的標識符並判斷文件是否爲指定類型
// 讀取圖片的標識符
byte[] imgFormat = new byte[2];
imgFormat[0] = Convert.ToByte(memorystream.ReadByte());
imgFormat[1] = Convert.ToByte(memorystream.ReadByte());
memorystream.Position = 0;
// 判斷指定圖片是否爲Bitmap格式
if (!IsBitmap(imgFormat))
{
info = null;
msg = "請選擇Bitmap格式的圖片";
return false;
}
#endregion
#region 提取信息
// 回位
memorystream.Position = 0;
memorystream.Seek(54L, 0);
byte reader;
char ch;
int temp = 0;
int state = 0;
info = "";
while (Convert.ToChar(temp) != '#' || state != 2)
{
temp = 0;
for (int k = 0; k < 16; k++)
{
reader = Convert.ToByte(memorystream.ReadByte());
temp += (reader & 0x01) << k;
}
ch = Convert.ToChar(temp);
if (ch != '#' && state != 1)
{
info = null;
msg = "沒有嵌入文字信息或者信息已經被破壞";
return false;
}
if (ch != '#' && state == 1)
{
info += ch.ToString();
}
if (ch == '#')
state++;
}
msg = "成功";
return true;
#endregion
}
#endregion
}
}
注意點:
- 流每次讀取一次,位置就向後推進一定的位數,但可以使用Position屬性設置開始讀取的位置
- MemoryStream對象的ReadByte()方法將返回的字節強制轉換成Int32類型
結果展示
嵌入信息:
提取信息:
附錄
參考鏈接: