【技術分享】LSB隱寫工具對比(Stegsolve與zsteg)
起因
很久很久以前,有一道送分題沒做出來,后來看writeup,只要zsteg就行了。
命令運行的結果
root@LAPTOP-GE0FGULA:/mnt/d# zsteg 瞅啥.bmp
[?] 2 bytes of extra data after image end (IEND), offset = 0x269b0e
extradata:0 .. ["x00" repeated 2 times]
imagedata .. text: ["r" repeated 18 times]
b1,lsb,bY .. "x00x8ExEE", data="x1EfxDEx9ExF6xAExFAxCEx86x9E"..., even=false>
b1,msb,bY .. text: "qwxf{you_say_chick_beautiful?}"
b2,msb,bY .. text: "i2,C8&k0."
b2,r,lsb,xY .. text: "UUUUUU9VUUUUUUUUUUUUUUUUUUUUUU"
b2,g,msb,xY .. text: ["U" repeated 22 times]
b2,b,lsb,xY .. text: ["U" repeated 10 times]
b3,g,msb,xY .. text: "V9XDR\d@"
b4,r,lsb,xY .. file: TIM image, Pixel at (4353,4112) Size=12850x8754
b4,g,lsb,xY .. text: "3"""""3###33##3#UDUEEEEEDDUETEDEDDUEEDTEEEUT#!"
b4,g,msb,xY .. text: """""""""""""""""""""DDDDDDDDDDDD""""DDDDDDDDDDDD*LD"
b4,b,lsb,xY .. text: "gfffffvwgwfgwwfw"
b1,msb,bY讀取到的flag,看的一臉懵逼,msb是啥?不是lsb隱寫么?bY的b又是啥?我用stegsolve怎么沒找到flag?
結論
兩個工具的一些參數在理解上有點疑問,因此查看了源碼。
Stegsolve的Data Extract功能,Bit Order選項MSBFirst和LSBFirst的區別,這個在掃描順序中說明
zsteg不理解參數更多
-c:rgba的組合理解,r3g2b3則表示r通道的低3bit,g通道2bit,r通道3bit,如果設置為rbg不加數字的,則表示每個通道讀取bit數相同,bit數有-b參數設置
-b:設置每個通道讀取的bit數,從低位開始,如果不是順序的低位開始,則可以使用掩碼,比如取最低位和最高位,則可以-b 10000001或者-b 0x81
-o:設置行列的讀取順序,xy就是從上到下,從左到右,xy任意有大寫的,表示倒序,不過栗子中有個bY令我費解,查看源碼知道對于BMP的圖片,可以不管通道,直接按字節讀取,就是b的意思了,b再順帶表示x,也就是bY的順序和xY是一樣的,Yb和Yx的順序是一樣的,但是b這個的讀取模式跟-c bgr -o xY好像是一樣的(因為看BMP圖片通道排列順序是BGR),不太理解專門弄個這個出來干嘛。
--msb和--lsb這個在組合順序中說明
掃描順序
行列順序
先說下行列的掃描順序
zsteg可以通過-o選項設置的8種組合(xy,xY,Xy,XY,yx,yX,Yx,YX),個人認為常用的就xy和xY吧
Stegsolve只有選項設置Extract By Row or Column,對應到zsteg的-o選項上就是xy和yx
字節順序
然后是字節上的掃描順序,因為是讀取的bit再拼接數據的,那么一個字節有8bit數據,從高位開始讀還是從低位開始讀的順序
Stegsolve:字節上的讀取順序與Bit Order選項有關,如果設置了MSBFirst,是從高位開始讀取,LSBFirst是從低位開始讀取
zsteg:只能從高位開始讀,比如-b 0x81,在讀取不同通道數據時,都是先讀取一個字節的高位,再讀取該字節的低位。對應到Stegsolve就是MSBFirst的選項。
組合順序
對于Stegsolve和zsteg,先讀取到bit數據都是先拿出來組合的,每8bit組合成一個字節,按照最先存放的Bit在低地址理解的話。
zsteg的--lsb和--msb決定了組合順序
--lsb:大端存放
--msb:小端存放
源碼片段,a內存儲的是讀取的Bit數據,所以msb是低地址的是低位,因此是小端存放。
if a.size >= 8
byte = 0
if params[:bit_order] == :msb
8.times{ |i| byte |= (a.shift< else
8.times{ |i| byte |= (a.shift<<(7-i))}
end
Stegsolve則是只有大端存放,即對應zsteg的—lsb,因為代碼中有個extractBitPos變量,初始值是128,每組合1bit,就右移一次,到0后循環。
源碼片段
private void addBit(int num)
{
if(num!=0)
{
extract[extractBytePos]+=extractBitPos;
}
extractBitPos>>=1;
if(extractBitPos>=1)
return;
extractBitPos=128;
extractBytePos++;
if(extractBytePos extract[extractBytePos]=0;
}
Stegsolve
了解一下Data Extract以及不同通道存儲圖片的隱寫
Data Extract
功能簡要說明
面板

配置選項后,是通過Preview按鈕進行數據的讀取,因此直接跟進該按鈕事件。
Bit Planes:選取通道要讀取的bit位。
Bit Plane Order:一個像素值包含多個通道,不同通道的讀取數據,Alpha一直是最先讀的,然后會根據該項的配置決定讀取順序。
Bit Order:讀取數據時,每次僅讀取1Bit,該項是控制讀取一個通道字節數時,讀取的方向,MSBFirst表示從高位讀取到低位,LSBFirst表示從低位讀取到高位。因此只有當通道勾選的Bit個數大于1時,該選項才會影響返回的結果。
代碼分析
文件:Extract.java
按鈕事件:
/**
* Generate the extract and generate the preview
* @param evt Event
*/
private void previewButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_previewButtonActionPerformed
generateExtract();
generatePreview();
}//GEN-LAST:event_previewButtonActionPerformed
跟進generateExtract(),存在內部調用,先列舉了另外兩個方法。
/**
* Retrieves the mask from the bits selected on the form
*/
/*讀取Bit Planes的配置,圖片getRGB會返回一個整型,如果存在alpha,那么范圍最大值就是0xffffffff,從高位至低位,每一個字節按順序對應為 A R G B,所以getMask就是獲取要獲取對應Bit的掩碼,存為this.mask,this.maskbits記錄是全部要讀取的Bit數。
*/
private void getMask()
{
mask = 0;
maskbits = 0;
if(ab7.isSelected()) { mask += 1<<31; maskbits++;}
if(ab6.isSelected()) { mask += 1<<30; maskbits++;}
if(ab5.isSelected()) { mask += 1<<29; maskbits++;}
if(ab4.isSelected()) { mask += 1<<28; maskbits++;}
if(ab3.isSelected()) { mask += 1<<27; maskbits++;}
if(ab2.isSelected()) { mask += 1<<26; maskbits++;}
if(ab1.isSelected()) { mask += 1<<25; maskbits++;}
if(ab0.isSelected()) { mask += 1<<24; maskbits++;}
if(rb7.isSelected()) { mask += 1<<23; maskbits++;}
if(rb6.isSelected()) { mask += 1<<22; maskbits++;}
if(rb5.isSelected()) { mask += 1<<21; maskbits++;}
if(rb4.isSelected()) { mask += 1<<20; maskbits++;}
if(rb3.isSelected()) { mask += 1<<19; maskbits++;}
if(rb2.isSelected()) { mask += 1<<18; maskbits++;}
if(rb1.isSelected()) { mask += 1<<17; maskbits++;}
if(rb0.isSelected()) { mask += 1<<16; maskbits++;}
if(gb7.isSelected()) { mask += 1<<15; maskbits++;}
if(gb6.isSelected()) { mask += 1<<14; maskbits++;}
if(gb5.isSelected()) { mask += 1<<13; maskbits++;}
if(gb4.isSelected()) { mask += 1<<12; maskbits++;}
if(gb3.isSelected()) { mask += 1<<11; maskbits++;}
if(gb2.isSelected()) { mask += 1<<10; maskbits++;}
if(gb1.isSelected()) { mask += 1<<9; maskbits++;}
if(gb0.isSelected()) { mask += 1<<8; maskbits++;}
if(bb7.isSelected()) { mask += 1<<7; maskbits++;}
if(bb6.isSelected()) { mask += 1<<6; maskbits++;}
if(bb5.isSelected()) { mask += 1<<5; maskbits++;}
if(bb4.isSelected()) { mask += 1<<4; maskbits++;}
if(bb3.isSelected()) { mask += 1<<3; maskbits++;}
if(bb2.isSelected()) { mask += 1<<2; maskbits++;}
if(bb1.isSelected()) { mask += 1<<1; maskbits++;}
if(bb0.isSelected()) { mask += 1; maskbits++;}
}
/**
* Retrieve the ordering options from the form
*/
/* 讀取Order setting的配置,主要就是rgbOrder的不同值對應的順序
*/
private void getBitOrderOptions()
{
if(byRowButton.isSelected()) rowFirst = true;
else rowFirst = false;
if(LSBButton.isSelected()) lsbFirst = true;
else lsbFirst = false;
if(RGBButton.isSelected()) rgbOrder = 1;
else if (RBGButton.isSelected()) rgbOrder = 2;
else if (GRBButton.isSelected()) rgbOrder = 3;
else if (GBRButton.isSelected()) rgbOrder = 4;
else if (BRGButton.isSelected()) rgbOrder = 5;
else rgbOrder = 6;
}
/**
* Generates the extract from the selected options
*/
private void generateExtract()
{
getMask();//獲取掩碼,每個像素值要獲取的對應Bit的掩碼,以及每個像素值獲取Bit的個數。
getBitOrderOptions();//獲取Order settings
int len = bi.getHeight() * bi.getWidth();//獲取總的像素點
len = len * maskbits; // 總的像素點*每個像素點獲取的Bit數=總的Bit數
len = (len +7)/8; // 總的Bit數轉換到總的字節數,+7是沒滿一個字節的Bit數也對應到一個字節。(極端點比如總的Bit數就1~7Bit,也是要轉為1字節,所以需要+7)
extract = new byte[len];//存儲讀取到的字節數據
extractBitPos = 128; // 每8個Bit組成一個字節數據,extractBitPos相當于權值,從128開始,因此讀取的每8Bit,先讀到的在高位。
extractBytePos = 0;
//System.out.println(bi.getHeight()+" "+bi.getWidth()+" "+len+" "+mask);
// 根據rowFirst參數來選擇讀取順序,調用extractBits讀取數據
if(rowFirst)
{
for(int j=0;j for(int i=0;i {
//System.out.println(i+" "+j+" "+extractBytePos);
extractBits(bi.getRGB(i, j));
}
}
else
{
for(int i=0;i for(int j=0;j extractBits(bi.getRGB(i, j));
}
}
讀取數據是extractBits,nextByte是讀取到的一個像素點的值,如果是lsbFirst(也就是選了Bitorder為LSBFirst,默認是MSBFirst),則是從低位從高位按順序讀取(每個通道選取2Bit以上才會有影響,如果只讀取1Bit則無所謂了)。
栗子:讀取alpha通道,lsbFirst,extract8Bits(nextByte,1<<24),掩碼是從24位開始,依次左移1位,左移8次;msbFirst,extract8Bits(nextByte,1<<31),掩碼是從31位開始,依次右移,右移8次。
/**
* Extract bits from the given byte taking account of
* the options selected
* @param nextByte the byte to extract bits from
*/
private void extractBits(int nextByte)
{
if(lsbFirst)
{
extract8Bits(nextByte,1<<24);
switch(rgbOrder)
{
case 1: //rgb
extract8Bits(nextByte,1<<16);
extract8Bits(nextByte,1<<8);
extract8Bits(nextByte,1);
break;
case 2: //rbg
extract8Bits(nextByte,1<<16);
extract8Bits(nextByte,1);
extract8Bits(nextByte,1<<8);
break;
case 3: //grb
extract8Bits(nextByte,1<<8);
extract8Bits(nextByte,1<<16);
extract8Bits(nextByte,1);
break;
case 4: //gbr
extract8Bits(nextByte,1<<8);
extract8Bits(nextByte,1);
extract8Bits(nextByte,1<<16);
break;
case 5: //brg
extract8Bits(nextByte,1);
extract8Bits(nextByte,1<<16);
extract8Bits(nextByte,1<<8);
break;
case 6: //bgr
extract8Bits(nextByte,1);
extract8Bits(nextByte,1<<8);
extract8Bits(nextByte,1<<16);
break;
}
}
else
{
extract8Bits(nextByte,1<<31);
switch(rgbOrder)
{
case 1: //rgb
extract8Bits(nextByte,1<<23);
extract8Bits(nextByte,1<<15);
extract8Bits(nextByte,1<<7);
break;
case 2: //rbg
extract8Bits(nextByte,1<<23);
extract8Bits(nextByte,1<<7);
extract8Bits(nextByte,1<<15);
break;
case 3: //grb
extract8Bits(nextByte,1<<15);
extract8Bits(nextByte,1<<23);
extract8Bits(nextByte,1<<7);
break;
case 4: //gbr
extract8Bits(nextByte,1<<15);
extract8Bits(nextByte,1<<7);
extract8Bits(nextByte,1<<23);
break;
case 5: //brg
extract8Bits(nextByte,1<<7);
extract8Bits(nextByte,1<<23);
extract8Bits(nextByte,1<<15);
break;
case 6: //bgr
extract8Bits(nextByte,1<<7);
extract8Bits(nextByte,1<<15);
extract8Bits(nextByte,1<<23);
break;
}
}
}
extract8Bits方法,針對每個通道是要單獨調用一次的,nextByte是讀取的一個像素點的數據,bitMask是對應通道的掩碼(根據extractBits方法的說明可知,如果是lsbFirst則是對應通道掩碼的最低位,msbFirst則是對應通道掩碼的最高位),在extract8Bits方法最后也有根據是lsbFirst的值選擇是左移還是右移,循環8次。
bitMask循環,與this.mask與,如果不為0,說明是要讀取的bit,此時就將nextByte與bitMask想與,把該bit的值存入extract
/**
* Examine 8 bits and check them against the mask to
* see if any should be extracted
* @param nextByte The byte to be examined
* @param bitMask The bitmask to be applied
*/
private void extract8Bits(int nextByte, int bitMask)
{
for(int i=0;i<8;i++)
{
if((mask&bitMask)!=0)
{
//System.out.println("call "+ mask+" "+bitMask+" "+nextByte);
addBit(nextByte & bitMask);
}
if(lsbFirst)
bitMask<<=1;
else
bitMask>>>=1;
}
}
addBit方法,num是讀取的像素值與相應bit的掩碼相與后的結果,如果不為0,表示那個Bit為1,否則為0,extractBitPos相當于權值,如果為1,就加extractBitPos,然后extractBitPos右移一位,如果為0就不需要加,但每次extractBitPos都是需要右移一位的,如果extractBitPos還是大于1的,說明還沒循環過8次,所以就return了,如果不大于1,說明8次了,那么重置extractBitPos為128,extractBytePos+1,新的字節extract[extractBytePos]的初始值為0。
/**
* Adds another bit to the extract
* @param num Non-zero if adding a 1-bit
*/
private void addBit(int num)
{
if(num!=0)
{
extract[extractBytePos]+=extractBitPos;
}
extractBitPos>>=1;
if(extractBitPos>=1)
return;
extractBitPos=128;
extractBytePos++;
if(extractBytePos extract[extractBytePos]=0;
}
不同通道讀取圖片
功能簡要說明
首先生成的圖片僅是黑白圖片,每個像素點的值根據讀取的bit位的值,如果為1設置為白色,如果為0設置為黑色。
代碼分析
打開圖片后,程序主界面上的<和>按鈕可以獲取不同通道的圖片,這里僅討論Alpha7~0,Red7~0,Green7~0,Blue7~0,也就是每個通道。
在StegSolve.java中定位到按鈕方法
private void forwardButtonActionPerformed(ActionEvent evt) {
if(bi == null) return;
transform.forward();
updateImage();
}
private void fileOpenActionPerformed(ActionEvent evt) {
JFileChooser fileChooser = new JFileChooser(System.getProperty("user.dir"));
FileNameExtensionFilter filter = new FileNameExtensionFilter("Images", "jpg", "jpeg", "gif", "bmp", "png");
fileChooser.setFileFilter(filter);
int rVal = fileChooser.showOpenDialog(this);
System.setProperty("user.dir", fileChooser.getCurrentDirectory().getAbsolutePath());
if(rVal == JFileChooser.APPROVE_OPTION)
{
sfile = fileChooser.getSelectedFile();
try
{
bi = ImageIO.read(sfile);
transform = new Transform(bi);
newImage();
}
catch (Exception e)
{
JOptionPane.showMessageDialog(this, "Failed to load file: " +e.toString());
}
}
}
主要方法定位到了Transform類,打開文件時初始化,參數是圖片的數據。
Transform.java
構造函數,originalImage記錄原始圖片數據,transform是轉換后的數據,先初始化為原始圖片數據,transNum的值對應不同的操作。
/*
* transforms
* 0 - none
* 1 - inversion
* 2-9 - alpha planes
* 10-17 - r planes
* 18-25 - g planes
* 26-33 - b planes
* 34 full alpha
* 35 full red
* 36 full green
* 37 full blue
* 38 random color1
* 39 random color2
* 40 random color3
* 41 gray bits
*/
Transform(BufferedImage bi)
{
originalImage = bi;
transform = originalImage;
transNum=0;
}
forward方法,,每次點擊一次按鈕,為加一次transNum,然后根據transNum的值去執行對應的操作。transNum值對應的操作除了注釋中的說明,也可以從getText方法中獲取,栗子:Alpha plane 0對應的transNum值為9
public void forward()
{
transNum++;
if(transNum>MAXTRANS) transNum=0;
calcTrans();
}
public String getText()
{
switch(transNum)
{
case 0:
return "Normal Image";
case 1:
return "Colour Inversion (Xor)";
case 2:
case 3:
case 4:
case 5:
case 6:
case 7:
case 8:
case 9:
return "Alpha plane " + (9 - transNum);
case 10:
case 11:
case 12:
case 13:
case 14:
case 15:
case 16:
case 17:
return "Red plane " + (17 - transNum);
case 18:
case 19:
case 20:
case 21:
case 22:
case 23:
case 24:
case 25:
return "Green plane " + (25 - transNum);
case 26:
case 27:
case 28:
case 29:
case 30:
case 31:
case 32:
case 33:
return "Blue plane " + (33 - transNum);
case 34:
return "Full alpha";
case 35:
return "Full red";
case 36:
return "Full green";
case 37:
return "Full blue";
case 38:
return "Random colour map 1";
case 39:
return "Random colour map 2";
case 40:
return "Random colour map 3";
case 41:
return "Gray bits";
default:
return "";
}
}
calcTrans方法,是一個switch方法,根據transNum的值調用方法,而我關心的不同通道獲取的圖片都是調用transfrombit方法,這里僅截取關心的
private void calcTrans()
{
switch(transNum)
{
case 2:
transfrombit(31);
return;
case 3:
transfrombit(30);
return;
case 4:
transfrombit(29);
return;
case 5:
transfrombit(28);
return;
case 6:
transfrombit(27);
return;
case 7:
transfrombit(26);
return;
case 8:
transfrombit(25);
return;
case 9:
transfrombit(24);
return;
case 10:
transfrombit(23);
return;
case 11:
transfrombit(22);
return;
case 12:
transfrombit(21);
return;
case 13:
transfrombit(20);
return;
case 14:
transfrombit(19);
return;
case 15:
transfrombit(18);
return;
case 16:
transfrombit(17);
return;
case 17:
transfrombit(16);
return;
case 18:
transfrombit(15);
return;
case 19:
transfrombit(14);
return;
case 20:
transfrombit(13);
return;
case 21:
transfrombit(12);
return;
case 22:
transfrombit(11);
return;
case 23:
transfrombit(10);
return;
case 24:
transfrombit(9);
return;
case 25:
transfrombit(8);
return;
case 26:
transfrombit(7);
return;
case 27:
transfrombit(6);
return;
case 28:
transfrombit(5);
return;
case 29:
transfrombit(4);
return;
case 30:
transfrombit(3);
return;
case 31:
transfrombit(2);
return;
case 32:
transfrombit(1);
return;
case 33:
transfrombit(0);
return;
default:
transform = originalImage;
return;
}
}
transfrombit方法,參數d基本就是讀取第dbit的數據,根據之前的說明 Alpha 7是getRGB的數據的最高位,第31bit,根據getText方法可以知道Aplpha 7對應的transNum值為2,再看calcTrans的case2就是調用transfrombit(31)。
private void transfrombit(int d)
{
transform = new BufferedImage(originalImage.getWidth(), originalImage.getHeight(), BufferedImage.TYPE_INT_RGB);
for(int i=0;i for(int j=0;j {
int col=0;
int fcol = originalImage.getRGB(i,j);
if(((fcol>>>d)&1)>0)//右移d個bit位,再取最低位,如果大于0表示對應Bit位為1,那么就設置對應像素值為0xffffff,也就是(255,255,255),對應白色,如果Bit位為0,則是設置為(0,0,0),對應為黑色
col=0xffffff;
transform.setRGB(i, j, col);
}
zsteg
跟進一下代碼執行流程,了解各個參數的意義。
入口
程序執行流程的文件
/bin/zsteg
/lib/zsteg.rb run方法
/lib/zsteg/cli/cli.rb run方法,這里會對參數解析,這里截取一些之后需要用到的參數,完整的自行看源碼吧,解析完參數后,主要是最后的動態方法調用,@actions=[‘check’],因此動態調用check方法
def run
@actions = []
@options = {
:verbose => 0,
:limit => Checker::DEFAULT_LIMIT,
:order => Checker::DEFAULT_ORDER
}
optparser = OptionParser.new do |opts|
opts.banner = "Usage: zsteg [options] filename.png [param_string]"
opts.separator ""
opts.on("-c", "--channels X", /[rgba,1-8]+/,
"channels (R/G/B/A) or any combination, comma separated",
"valid values: r,g,b,a,rg,bgr,rgba,r3g2b3,..."
) do |x|
@options[:channels] = x.split(',')
# specifying channels on command line disables extra checks
@options[:extra_checks] = false
end
opts.on("-b", "--bits N", "number of bits, single int value or '1,3,5' or range '1-8'",
"advanced: specify individual bits like '00001110' or '0x88'"
) do |x|
a = []
x = '1-8' if x == 'all'
x.split(',').each do |x1|
if x1['-']
t = x1.split('-')
a << Range.new(parse_bits(t[0]), parse_bits(t[1])).to_a
else
a << parse_bits(x1)
end
end
@options[:bits] = a.flatten.uniq
# specifying bits on command line disables extra checks
@options[:extra_checks] = false
end
opts.on "--lsb", "least significant BIT comes first" do
@options[:bit_order] = :lsb
end
opts.on "--msb", "most significant BIT comes first" do
@options[:bit_order] = :msb
end
opts.on("-o", "--order X", /all|auto|[bxy,]+/i,
"pixel iteration order (default: '#{@options[:order]}')",
"valid values: ALL,xy,yx,XY,YX,xY,Xy,bY,...",
){ |x| @options[:order] = x.split(',') }
if (argv = optparser.parse(@argv)).empty?
puts optparser.help
return
end
@actions = DEFAULT_ACTIONS if @actions.empty?
argv.each do |arg|
if arg[','] && !File.exist?(arg)
@options.merge!(decode_param_string(arg))
argv.delete arg
end
end
argv.each_with_index do |fname,idx|
if argv.size > 1 && @options[:verbose] >= 0
puts if idx > 0
puts "[.] #{fname}".green
end
next unless @img=load_image(@fname=fname)
@actions.each do |action|
if action.is_a?(Array)
self.send(*action) if self.respond_to?(action.first)
else
self.send(action) if self.respond_to?(action)
end
end
end
rescue Errno::EPIPE
# output interrupt, f.ex. when piping output to a 'head' command
# prevents a 'Broken pipe - (Errno::EPIPE)' message
end
/lib/zsteg/cli/cli.rb check方法
def check Checker.new(@img, @options).check end
/lib/zsteg/checker.rb initialize方法,初始化一些成員變量,@extractor也是傳入了圖像數據的,通道判斷了圖片屬性是否有alpha通道。
def initialize image, params = {}
@params = params
@cache = {}; @wastitles = Set.new
@image = image.is_a?(ZPNG::Image) ? image : ZPNG::Image.load(image)
@extractor = Extractor.new(@image, params)
@channels = params[:channels] ||
if@image.alpha_used?
%w'r g b a rgb bgr rgba abgr'
else
%w'r g b rgb bgr'
end
@verbose = params[:verbose] || -2
@file_cmd = FileCmd.new
@results = []
@params[:bits] ||= DEFAULT_BITS
@params[:order] ||= DEFAULT_ORDER
@params[:limit] ||= DEFAULT_LIMIT
if@params[:min_str_len]
@min_str_len = @min_wholetext_len = @params[:min_str_len]
else
@min_str_len = DEFAULT_MIN_STR_LEN
@min_wholetext_len = @min_str_len - 2
end
@strings_re = /[x20-x7ernt]{#@min_str_len,}/
@extra_checks = params.fetch(:extra_checks, DEFAULT_EXTRA_CHECKS)
end
/lib/zsteg/checker.rb check方法,截取部分,會判斷圖片是否是bmp的,只有bmp的-o選項內才有b,如果設置為all也只是多了bY的選項,但是通過之后代碼分析是可以by yb Yb的。判斷order中是否有b用的是正則,因此大小寫一樣。接著數據讀取就到check_channels方法了。
def check
@found_anything = false
@file_cmd.start!
if@image.format == :bmp
case params[:order].to_s.downcase
when /all/
params[:order] = %w'bY xY xy yx XY YX Xy yX Yx'
when /auto/
params[:order] = %w'bY xY'
end
else
case params[:order].to_s.downcase
when /all/
params[:order] = %w'xy yx XY YX Xy yX xY Yx'
when /auto/
params[:order] = 'xy'
end
end
Array(params[:order]).uniq.each do |order|
(params[:prime] == :all ? [false,true] : [params[:prime]]).each do |prime|
Array(params[:bits]).uniq.each do |bits|
p1 = @params.merge :bits => bits, :order => order, :prime => prime
if order[/b/i]
# byte iterator does not need channels
check_channels nil, p1
else
channels.each{ |c| check_channels c, p1 }
end
end
end
end
if@found_anything
print "r" + " "*20 + "r" if@need_cr
else
puts "r[=] nothing :(" + " "*20 # line cleanup
end
if@extra_checks
Analyzer.new(@image).analyze!
end
# return everything found if this method was called from some code
@results
ensure
@file_cmd.stop!
end
/lib/zsteg/checker.rb check_channels方法,首先判斷是否設置了bit_order,沒設置則兩個都測試,之后就是區分兩種模式了,channels有值的,最后是去的color_extractor.rb,沒有值的去的byte_extractor.rb。
color_extractor模式,還要判斷channels指定的模式,是就rgb還是會單獨指定每個通道讀取多少Bit的。確定過每個像素讀取多少bit,然后乘以總的像素點除以8確認讀取字節數。
byte)extractor模式,nbits是-b參數指定的讀取bit數,乘以一行的字節數,再乘以高/8。
show_title title輸出當前模式
data = @extractor.extract p1讀取數據
def check_channels channels, params
unless params[:bit_order]
check_channels(channels, params.merge(:bit_order => :lsb))
check_channels(channels, params.merge(:bit_order => :msb))
return
end
p1 = params.clone
# number of bits
# equals to params[:bits] if in range 1..8
# otherwise equals to number of 1's, like 0b1000_0001
nbits = p1[:bits] <= 8 ? p1[:bits] : (p1[:bits]&0xff).to_s(2).count("1")
show_bits = true
# channels is a String
if channels
p1[:channels] =
if channels[1] && channels[1] =~ /AdZ/
# 'r3g2b3'
a=[]
cbits = 0
(channels.size/2).times do |i|
a << (t=channels[i*2,2])
cbits += t[1].to_i
end
show_bits = false
@max_hidden_size = cbits * @image.width
a
else
# 'rgb'
a = channels.chars.to_a
@max_hidden_size = a.size * @image.width * nbits
a
end
# p1[:channels] is an Array
elsif params[:order] =~ /b/i
# byte extractor
@max_hidden_size = @image.scanlines[0].decoded_bytes.size * nbits
else
raise "invalid params #{params.inspect}"
end
@max_hidden_size *= @image.height/8
bits_tag =
if show_bits
if params[:bits] > 0x100
if params[:bits].to_s(2) =~ /(1{1,8})$/
# mask => number of bits
"b#{$1.size}"
else
# mask
"b#{(params[:bits]&0xff).to_s(2)}"
end
else
# number of bits
"b#{params[:bits]}"
end
end
title = [
bits_tag,
channels,
params[:bit_order],
params[:order],
params[:prime] ? 'prime' : nil
].compact.join(',')
return if @wastitles.include?(title)
@wastitles << title
show_title title
p1[:title] = title
data = @extractor.extract p1
if p1[:invert]
data.size.times{ |i| data.setbyte(i, data.getbyte(i)^0xff) }
end
@need_cr = !process_result(data, p1) # carriage return needed?
@found_anything ||= !@need_cr
end
/lib/zsteg/extractor.rb 根據-o選項中是否包含b選擇不同模式
def extract params = {}
@limit = params[:limit].to_i
@limit = 2**32 if@limit <= 0
if params[:order] =~ /b/i
byte_extract params
else
color_extract params
end
end
在分類說明兩個模式的時候,先將一個方法拿出來做個說明,bit_indexes
bit_indexes
通過代碼可以知道,在掃描一個字節的時候,zsteg是固定的從高位掃描至低位的
def bit_indexes bits
if (1..8).include?(bits)
# number of bits
# 1 => [0]
# ...
# 8 => [7,6,5,4,3,2,1,0]
bits.times.to_a.reverse
else
# mask
mask = bits & 0xff
r = []
8.times do |i|
r << i if mask[i] == 1
end
r.reverse
end
end
byte_extract
/lib/zsteg/extractor/byte_extractor.rb data列表是用于存儲字節數據,a是用于存儲bit數據。
通過byte_iterator方法遍歷每個字節,會根據order參數是否有小寫b,決定x方向的正序還是倒序,是否有小寫y決定y方向的正序還是倒序。
根據x,y的值讀取到對應字節,然后根據bit_indexes獲取的bidx(注定只能高位至低位)去讀取對應Bit值
當a.size為8時,就會組成一個字節,根據bit_order的值決定a中的8bit數據是大端還是小端
msb是小端,lsb是大端。
module ZSteg
class Extractor
# ByteExtractor extracts bits from each scanline bytes
# actual for BMP+wbStego combination
module ByteExtractor
def byte_extract params = {}
bidxs = bit_indexes params[:bits]
if params[:prime]
pregenerate_primes(
:max => @image.scanlines[0].size * @image.height,
:count => (@limit*8.0/bidxs.size).ceil
)
end
data = ''.force_encoding('binary')
a = [0]*params[:shift].to_i # prepend :shift zero bits
byte_iterator(params) do |x,y|
sl = @image.scanlines[y]
value = sl.decoded_bytes.getbyte(x)
bidxs.each do |bidx|
a << value[bidx]
end
if a.size >= 8
byte = 0
if params[:bit_order] == :msb
8.times{ |i| byte |= (a.shift< else
8.times{ |i| byte |= (a.shift<<(7-i))}
end
#printf "[d] %02x %08bn", byte, byte
data << byte.chr
if data.size >= @limit
print "[limit #@limit]".gray if@verbose > 1
break
end
end
end
if params[:strip_tail_zeroes] != false && data[-1,1] == "x00"
oldsz = data.size
data.sub!(/x00+Z/,'')
print "[zerotail #{oldsz-data.size}]".gray if@verbose > 1
end
data
end
# 'xy': b=0,y=0; b=1,y=0; b=2,y=0; ...
# 'yx': b=0,y=0; b=0,y=1; b=0,y=2; ...
# ...
# 'xY': b=0, y=MAX; b=1, y=MAX; b=2, y=MAX; ...
# 'XY': b=MAX,y=MAX; b=MAX-1,y=MAX; b=MAX-2,y=MAX; ...
def byte_iterator params
type = params[:order]
if type.nil? || type == 'auto'
type = @image.format == :bmp ? 'bY' : 'by'
end
raise "invalid iterator type #{type}" unless type =~ /A(by|yb)Z/i
sl0 = @image.scanlines.first
# XXX don't try to run it on interlaced PNGs!
x0,x1,xstep =
if type.index('b')
[0, sl0.decoded_bytes.size-1, 1]
else
[sl0.decoded_bytes.size-1, 0, -1]
end
y0,y1,ystep =
if type.index('y')
[0, @image.height-1, 1]
else
[@image.height-1, 0, -1]
end
# cannot join these lines from ByteExtractor and ColorExtractor into
# one method for performance reason:
# it will require additional yield() for EACH BYTE iterated
if type[0,1].downcase == 'b'
# ROW iterator
if params[:prime]
idx = 0
y0.step(y1,ystep){ |y| x0.step(x1,xstep){ |x|
yield(x,y) if@primes.include?(idx)
idx += 1
}}
else
y0.step(y1,ystep){ |y| x0.step(x1,xstep){ |x| yield(x,y) }}
end
else
# COLUMN iterator
if params[:prime]
idx = 0
x0.step(x1,xstep){ |x| y0.step(y1,ystep){ |y|
yield(x,y) if@primes.include?(idx)
idx += 1
}}
else
x0.step(x1,xstep){ |x| y0.step(y1,ystep){ |y| yield(x,y) }}
end
end
end
end
end
end
color_extractor
/lib/zsteg/extractor/color_extractor.rb data列表是用于存儲字節數據,a是用于存儲bit數據。
通過coord_iterator方法遍歷每個字節,會根據order參數是否有小寫x,決定x方向的正序還是倒序,是否有小寫y決定y方向的正序還是倒序。
根據x,y的值讀取到對應字節,然后根據bit_indexes獲取的ch_masks(注定只能高位至低位)去讀取對應Bit值,只是還要根據channel的值,如果是單個字符,表示讀取的bit數是通過-b設置的,因此傳入params[:bits],否則就是2個字符,讀取第2個字符表示讀取的bit數。
當a.size為8時,就會組成一個字節,根據bit_order的值決定a中的8bit數據是大端還是小端 msb是小端,lsb是大端。
module ZSteg
class Extractor
# ColorExtractor extracts bits from each pixel's color
module ColorExtractor
def color_extract params = {}
channels = Array(params[:channels])
#pixel_align = params[:pixel_align]
ch_masks = []
case channels.first.size
when 1
# ['r', 'g', 'b']
channels.each{ |c| ch_masks << [c[0], bit_indexes(params[:bits])] }
when 2
# ['r3', 'g2', 'b3']
channels.each{ |c| ch_masks << [c[0], bit_indexes(c[1].to_i)] }
else
raise "invalid channels: #{channels.inspect}" if channels.size != 1
t = channels.first
if t =~ /A[rgba]+Z/
return color_extract(params.merge(:channels => t.split('')))
end
raise "invalid channels: #{channels.inspect}"
end
# total number of bits = sum of all channels bits
nbits = ch_masks.map{ |x| x[1].size }.inject(&:+)
if params[:prime]
pregenerate_primes(
:max => @image.width * @image.height,
:count => (@limit*8.0/nbits/channels.size).ceil
)
end
data = ''.force_encoding('binary')
a = [0]*params[:shift].to_i # prepend :shift zero bits
catch :limit do
coord_iterator(params) do |x,y|
color = @image[x,y]
ch_masks.each do |c,bidxs|
value = color.send(c)
bidxs.each do |bidx|
a << value[bidx]
end
end
#p [x,y,a.size,a]
while a.size >= 8
byte = 0
#puts a.join
if params[:bit_order] == :msb
8.times{ |i| byte |= (a.shift< else
8.times{ |i| byte |= (a.shift<<(7-i))}
end
#printf "[d] %02x %08bn", byte, byte
data << byte.chr
if data.size >= @limit
print "[limit #@limit]".gray if @verbose > 1
throw :limit
end
#a.clear if pixel_align
end
end
end
if params[:strip_tail_zeroes] != false && data[-1,1] == "x00"
oldsz = data.size
data.sub!(/x00+Z/,'')
print "[zerotail #{oldsz-data.size}]".gray if @verbose > 1
end
data
end
# 'xy': x=0,y=0; x=1,y=0; x=2,y=0; ...
# 'yx': x=0,y=0; x=0,y=1; x=0,y=2; ...
# ...
# 'xY': x=0, y=MAX; x=1, y=MAX; x=2, y=MAX; ...
# 'XY': x=MAX,y=MAX; x=MAX-1,y=MAX; x=MAX-2,y=MAX; ...
def coord_iterator params
type = params[:order]
if type.nil? || type == 'auto'
type = @image.format == :bmp ? 'xY' : 'xy'
end
raise "invalid iterator type #{type}" unless type =~ /A(xy|yx)Z/i
x0,x1,xstep =
if type.index('x')
[0, @image.width-1, 1]
else
[@image.width-1, 0, -1]
end
y0,y1,ystep =
if type.index('y')
[0, @image.height-1, 1]
else
[@image.height-1, 0, -1]
end
# cannot join these lines from ByteExtractor and ColorExtractor into
# one method for performance reason:
# it will require additional yield() for EACH BYTE iterated
if type[0,1].downcase == 'x'
# ROW iterator
if params[:prime]
idx = 0
y0.step(y1,ystep){ |y| x0.step(x1,xstep){ |x|
yield(x,y) if @primes.include?(idx)
idx += 1
}}
else
y0.step(y1,ystep){ |y| x0.step(x1,xstep){ |x| yield(x,y) }}
end
else
# COLUMN iterator
if params[:prime]
idx = 0
x0.step(x1,xstep){ |x| y0.step(y1,ystep){ |y|
yield(x,y) if @primes.include?(idx)
idx += 1
}}
else
x0.step(x1,xstep){ |x| y0.step(y1,ystep){ |y| yield(x,y) }}
end
end
end
end
end
end
結果
起因里的zsteg的參數現在都解釋過了,而用stegsolve沒有看到flag是因為8bit數據是按照大端模式組成的字節,而flag是需要以小端模式組成,所以當我選擇stegsolve來做題時,注定是拿不到flag了,都是時辰的錯。
然后bY其實和xY的結果是一樣的,只是要確定通道的排列方式,bmp按順序存的通道順序是bgr。
root@LAPTOP-GE0FGULA:/mnt/d# zsteg -c bgr -o xY --msb -b1 瞅啥.bmp
[?] 2 bytes of extra data after image end (IEND), offset = 0x269b0e
b1,bgr,msb,xY .. text: "qwxf{you_say_chick_beautiful?}"
再來看下stegsolve,首先知道-o是Y,因此圖片需要倒一下,所以手動修改bmp的高度為原值的負值,圖片就倒過來了。

選中的序列和flag的值,生成二進制序列對比一下,應是每8個bit都是倒序的。
#encoding:utf-8
from binascii import b2a_hex,a2b_hex
flag = "qwxf{you_say_chick_beautiful?}"
stegsolve = "8eee1e66de9ef6aeface869efac61696c6d6fa46a686ae2e9666ae36fcbe"
flag = bin(int(b2a_hex(flag),16))[2:]
stegsolve = bin(int(stegsolve,16))[2:]
def show(a,b):
if len(a) % 2 != 0:
a = '0'+a
if len(b) % 2 != 0:
b = '0'+b
for i in xrange(0,len(a),8):
print a[i:i+8]+" "+b[i:i+8]
show(flag,stegsolve)
自己看下結果吧。