Introduction
The video stream in an AVI file is nothing more than a sequence of bitmaps. This article is about extracting these bitmaps and re-building the stream, in order to hide a message in the video.
Background
Before reading this article, you should have read at least part one, Steganography - Hiding messages in the Noise of a Picture. This one uses the application described in parts 1-3, but you don�t need the extended features to understand it.
Reading the Video Stream
The Windows AVI library is a set of functions in avifil32.dll. Before it is ready to use, it has to be initialized with AVIFileInit
. AVIFileOpen
opens the file, AVIFileGetStream
finds the video stream. Each of these functions allocates memory which has to be released.
[DllImport("avifil32.dll")]
public static extern void AVIFileInit();
[DllImport("avifil32.dll", PreserveSig=true)]
public static extern int AVIFileOpen(
ref int ppfile,
String szFile,
int uMode,
int pclsidHandler);
[DllImport("avifil32.dll")]
public static extern int AVIFileGetStream(
int pfile,
out IntPtr ppavi,
int fccType,
int lParam);
[DllImport("avifil32.dll")]
public static extern int AVIStreamRelease(IntPtr aviStream);
[DllImport("avifil32.dll")]
public static extern int AVIFileRelease(int pfile);
[DllImport("avifil32.dll")]
public static extern void AVIFileExit();
Now we are able to open an AVI file and get the video stream. AVI files can contain many streams of four different types (Video, Audio, Midi and Text). Usually there is only one stream of each type, and we are only interested in the video stream.
private int aviFile = 0;
private IntPtr aviStream;
public void Open(string fileName) {
AVIFileInit();
int result = AVIFileOpen(
ref aviFile,
fileName,
OF_SHARE_DENY_WRITE, 0);
result = AVIFileGetStream(
aviFile,
out aviStream,
streamtypeVIDEO, 0);
}
Before we start reading the frames, we must know what exactly we want to read:
- Where does the first frame begin?
- How many frames are available?
- What is the height/width of the images?
The AVI library contains a function for every question.
[DllImport("avifil32.dll", PreserveSig=true)]
public static extern int AVIStreamStart(int pavi);
[DllImport("avifil32.dll", PreserveSig=true)]
public static extern int AVIStreamLength(int pavi);
[DllImport("avifil32.dll")]
public static extern int AVIStreamInfo(
int pAVIStream,
ref AVISTREAMINFO psi,
int lSize);
With these functions we can fill a BITMAPINFOHEADER
structure. To extract the images, we need three more functions.
[DllImport("avifil32.dll")]
public static extern int AVIStreamGetFrameOpen(
IntPtr pAVIStream,
ref BITMAPINFOHEADER bih);
[DllImport("avifil32.dll")]
public static extern int AVIStreamGetFrame(
int pGetFrameObj,
int lPos);
[DllImport("avifil32.dll")]
public static extern int AVIStreamGetFrameClose(int pGetFrameObj);
Finally we are ready to decompress the frames...
int firstFrame = AVIStreamStart(aviStream.ToInt32());
int countFrames = AVIStreamLength(aviStream.ToInt32());
AVISTREAMINFO streamInfo = new AVISTREAMINFO();
result = AVIStreamInfo(aviStream.ToInt32(), ref streamInfo,
Marshal.SizeOf(streamInfo));
BITMAPINFOHEADER bih = new BITMAPINFOHEADER();
bih.biBitCount = 24;
bih.biCompression = 0;
bih.biHeight = (Int32)streamInfo.rcFrame.bottom;
bih.biWidth = (Int32)streamInfo.rcFrame.right;
bih.biPlanes = 1;
bih.biSize = (UInt32)Marshal.SizeOf(bih);
int getFrameObject = AVIStreamGetFrameOpen(aviStream, ref bih);
...
public void ExportBitmap(int position, String dstFileName){
int pDib = Avi.AVIStreamGetFrame(getFrameObject, firstFrame + position);
BITMAPINFOHEADER bih = new BITMAPINFOHEADER();
bih = (BITMAPINFOHEADER)Marshal.PtrToStructure(new IntPtr(pDib),
bih.GetType());
byte[] bitmapData = new byte;
int address = pDib + Marshal.SizeOf(bih);
for(int offset=0; offset<bitmapData.Length; offset++){
bitmapData[offset] = Marshal.ReadByte(new IntPtr(address));
address++;
}
byte[] bitmapInfo = new byte[Marshal.SizeOf(bih)];
IntPtr ptr;
ptr = Marshal.AllocHGlobal(bitmapInfo.Length);
Marshal.StructureToPtr(bih, ptr, false);
address = ptr.ToInt32();
for(int offset=0; offset<bitmapInfo.Length; offset++){
bitmapInfo[offset] = Marshal.ReadByte(new IntPtr(address));
address++;
}
...and store them in bitmap files.
Avi.BITMAPFILEHEADER bfh = new Avi.BITMAPFILEHEADER();
bfh.bfType = Avi.BMP_MAGIC_COOKIE;
bfh.bfSize = (Int32)(55 + bih.biSizeImage);
bfh.bfOffBits = Marshal.SizeOf(bih) + Marshal.SizeOf(bfh);
FileStream fs = new FileStream(dstFileName, System.IO.FileMode.Create);
BinaryWriter bw = new BinaryWriter(fs);
bw.Write(bfh.bfType);
bw.Write(bfh.bfSize);
bw.Write(bfh.bfReserved1);
bw.Write(bfh.bfReserved2);
bw.Write(bfh.bfOffBits);
bw.Write(bitmapInfo);
bw.Write(bitmapData);
bw.Close();
fs.Close();
}
The application can use the extracted bitmaps just like any other image file. If one carrier file is an AVI video, it extracts the first frame to a temporary file, opens it and hides a part of the message. Then it writes the resulting bitmap to a new video stream, and continues with the next frame. After the last frame the application closes both video files, deletes the temporary bitmap file, and continues with the next carrier file.
Writing to a Video Stream
When the application opens an AVI carrier file, it creates another AVI file for the resulting bitmaps. The new video stream must have the same size and frame rate as the original stream, so we cannot create it in the Open()
method. When the first bitmap arrives, we know the frame size and are able to create a video stream. The functions to create streams and write frames are AVIFileCreateStream
, AVIStreamSetFormat
and AVIStreamWrite
:
[DllImport("avifil32.dll")]
public static extern int AVIFileCreateStream(
int pfile,
out IntPtr ppavi,
ref AVISTREAMINFO ptr_streaminfo);
[DllImport("avifil32.dll")]
public static extern int AVIStreamSetFormat(
IntPtr aviStream, Int32 lPos,
ref BITMAPINFOHEADER lpFormat, Int32 cbFormat);
[DllImport("avifil32.dll")]
public static extern int AVIStreamWrite(
IntPtr aviStream, Int32 lStart, Int32 lSamples,
IntPtr lpBuffer, Int32 cbBuffer, Int32 dwFlags,
Int32 dummy1, Int32 dummy2);
Now we can create a stream...
private void CreateStream() {
AVISTREAMINFO strhdr = new AVISTREAMINFO();
strhdr.fccType = this.fccType;
strhdr.fccHandler = this.fccHandler;
strhdr.dwScale = 1;
strhdr.dwRate = frameRate;
strhdr.dwSuggestedBufferSize = (UInt32)(height * stride);
strhdr.dwQuality = 10000;
strhdr.rcFrame.bottom = (UInt32)height;
strhdr.rcFrame.right = (UInt32)width;
strhdr.szName = new UInt16[64];
int result = AVIFileCreateStream(aviFile, out aviStream, ref strhdr);
BITMAPINFOHEADER bi = new BITMAPINFOHEADER();
bi.biSize = (UInt32)Marshal.SizeOf(bi);
bi.biWidth = (Int32)width;
bi.biHeight = (Int32)height;
bi.biPlanes = 1;
bi.biBitCount = 24;
bi.biSizeImage = (UInt32)(this.stride * this.height);
result = Avi.AVIStreamSetFormat(aviStream, 0, ref bi, Marshal.SizeOf(bi));
}
...and write video frames.
public void Open(string fileName, UInt32 frameRate) {
this.frameRate = frameRate;
Avi.AVIFileInit();
int hr = Avi.AVIFileOpen(
ref aviFile, fileName,
OF_WRITE | OF_CREATE, 0);
}
public void AddFrame(Bitmap bmp) {
BitmapData bmpDat = bmp.LockBits(
new Rectangle(0, 0, bmp.Width, bmp.Height),
ImageLockMode.ReadOnly,PixelFormat.Format24bppRgb);
if (this.countFrames == 0) {
this.stride = (UInt32)bmpDat.Stride;
this.width = bmp.Width;
this.height = bmp.Height;
CreateStream();
}
int result = AVIStreamWrite(aviStream,
countFrames, 1,
bmpDat.Scan0,
(Int32) (stride * height),
0, 0, 0);
bmp.UnlockBits(bmpDat);
this.countFrames ++;
}
That's all we need to read and write video streams. Non-video streams and compression are not interesting at the moment, because compression destroys the hidden message by changing colours, and sound would make the files even larger - uncompressed AVI files are big enough! ;-)
Changes in CryptUtility
The
HideOrExtract()
method used to load all carrier images at once. This was no good from the beginning, and now it became impossible. From now on
HideOrExtract()
loads only one bitmap, and disposes it before loading the next one. The currently used carrier image - simple bitmap or extracted AVI frame - is stored in a
BitmapInfo
structure, which is passed around
by ref.
public struct BitmapInfo{
public Bitmap bitmap;
public int aviPosition;
public int aviCountFrames;
public String sourceFileName;
public long messageBytesToHide;
public void LoadBitmap(String fileName){
bitmap = new Bitmap(fileName);
sourceFileName = fileName;
}
}
Whenever MovePixelPosition
moves the position into the next carrier bitmap, it checks the field aviPosition
. If aviPosition
is < 0, it saves and disposes the bitmap. If aviPosition
is 0 or greater, it is a frame in an AVI stream. It is not saved to a file, but added to the open AVI stream. If the bitmap was a simple image or the last frame of an AVI stream, the method opens the next carrier file. If there are more frames in the AVI stream, is exports and loads the next bitmap.
Using the code
There are three new classes in the project:
AviReader
opens existing AVI files and copies frames to bitmap files.
AviWriter
creates new AVI files and combines bitmaps to a video stream.
Avi
contains the function declarations and structure definitions.
Thanks
Thanks to Shrinkwrap Visual Basic, for the "Visual Basic AVIFile Tutorial". The examples could not be easily converted to VB.NET or C#, but they gave me important hints on how to copy DIBs.
Thanks to Rene N., who posted an AviWriter
class in microsoft.public.dotnet.languages.csharp.