Find Hidden Message Embedded in Image

Summary: Learn how to find a hidden message embedded in an image.

Microsoft Scripting Guy, Ed Wilson, is here. Today we have an extra post from Shane Nelson with a recap of an interesting challenge presented to us at our Charlotte PowerShell User Group.

Shane is an associate Windows system administrator for a large-scale healthcare provider in the southeast U.S. He has worked in IT for since 2008. Along with his team, he helps manage and support Windows servers and Microsoft Exchange.

Take it away, Shane…

Windows PowerShell MVP, Jim Christopher, presented us with a challenge: Find the hidden message that he embedded in the image he provided. The image was a 640x480 seemingly black rectangle. And because most of us in the group aren’t Windows PowerShell veterans like the Scripting Guy, Jim gave us two clues:

1. He showed us how to load an image into Windows PowerShell:

$image = [System.Drawing.Image]::FromFile('C:\powershell\stegan1.png')

2. He hinted that when we are look for a hidden message in an image, we need to examine the bits.

What are the “bits” of an image? The pixels!

So we needed to find a way to examine all the pixels in an image. Fortunately, our $image variable came with a GetPixel method, as seen from its Get-Member output.
When we called the GetPixel method, for instance on the first pixel (0,0), we saw its red, green, and blue values.
PS C:\powershell> $image.GetPixel(0,0)

R             : 0

G             : 0

B             : 0

A             : 255

IsKnownColor  : False

IsEmpty       : False

IsNamedColor  : False

IsSystemColor : False

Name          : ff000000

In this case, they were all zero, which indicates that this pixel was black. Now we needed to iterate through every pixel to see what was different. We did this by iterating through each pixel in a row and then iterating through each row (and in turn through each of its pixels).

for ($h=0;$h -lt $image.Height;$h++)


   for ($w=0;$w -lt $image.Width;$w++)





Here the inner for loop iterates through the objects in a row while the outer for loop iterates through the rows.

When we watched this output for a time, we noticed that some of the pixels had R=1 instead of R=0. This didn’t help us much, however, because the GetPixel output merely showed us screens and screens of the RGB values of each pixel. We wanted to see what was different.

We decided to create a map of the red pixels (the ones that were changed) so that we could let our human eyes notice any differences.

$pixelmap = @()

for ($h=0;$h -lt $image.Height;$h++)


   $line = ""

   for ($w=0;$w -lt $image.Width;$w++)


       $line += ($image.GetPixel($w,$h)).R


   $pixelmap += $line


Here we created a pixel map by concatenating each red value in a row to the $line variable and then concatenating each $line value to the $pixelmap variable.

Redirecting our pixel map to a text document with $pixelmap > pixelmap.txt, we were then able to open the text containing all of the R values, zoom out as far as we could, and see the hidden message:

Image of message

Happy Holidays, scripters!


Thanks, Shane!

I invite you to follow me on Twitter and Facebook. If you have any questions, send email to me at, or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson, Microsoft Scripting Guy

Comments (7)

  1. Jeff Hicks says:

    I'd love to get a copy of the original graphic. I'd also love to know how the message was embedded in the graphic.

  2. David Wyatt says:

    You could generate the graphic using PowerShell in just about the same way it was extracted, once you laid out the picture (possibly in a text file just like the one Shane displayed from his script's output.)

    Aside from the code to read your source file and loop over its contents, these are the only things you should need:

    $littleBitRed = [System.Drawing.Color]::FromArgb(1,0,0)

    $image = New-Object System.Drawing.Bitmap(640,480)

    $image.SetPixel(100, 0, $littleBitRed)

    # Many, many more calls to SetPixel()

    $image.Save("$pwdMySuperSecretBitmap.jpg", [System.Drawing.Imaging.ImageFormat]::Jpeg)

  3. I went to play and on this particular workstation [system.drawing.image] wasn't there.  If you have that issue just execute at the PowerShell prompt or in your script prior to playing



  4. David Wyatt says:

    Out of curiosity, is there any advantage to using the LoadWithPartialName method as opposed to Add-Type -AssemblyName System.Drawing , or are they both just doing the same thing?

  5. David Wyatt says:

    To satisfy my curiosity, I took a look at the Add-Type cmdlet with dotPeek this morning.  It uses Assembly.Load(), and contains a hard-coded table of partial names to strong names which are checked if the name you pass in doesn't work as-is.

    When working with any assembly in that table, there's no practical difference between Add-Type -AssemblyName and Assembly.LoadWithPartialName().  For third-party assemblies that you want to load from the GAC, you'd need to use the full strong name, or Assembly.LoadWithPartialName().

  6. Tony Rivas says:

    Really fun demo here. @Jeff Hicks use MSPaint, make a 640X480 image, use black (R: 0; G: 0; B: 0) color to fill background, add white text "Happy Holidays" using 135 pt Calibri font –> then switch text color to almost black (R: 1; G: 0;B: 0).  Save.

  7. says:

    Have a bless day and keep our god first and your life . And please keep me and my family and pray thank you.!!much love to all

Skip to main content