0x00 Preface

---

Recently, I came across an interesting project on GitHub: Invoke-PSImage, which embeds PowerShell code as payload within the pixels of a PNG file (without affecting normal viewing of the original image). With just a single line of PowerShell code in the command line, the hidden payload within the pixels can be executed.

This is an application of steganography. I have previously introduced PNG steganography techniques in earlier articles, which can be referenced:

"Steganography Techniques – LSB Steganography in PNG Files"

"Steganography Techniques – Hiding Payloads Using PNG File Formats"

This article will analyze Invoke-PSImage based on my own insights, introduce its principles, address issues encountered during testing, learn programming techniques from the script, and propose my own optimization ideas.

Invoke-PSImage address:

https://github.com/peewpw/Invoke-PSImage

0x01 Introduction

---

This article will cover the following topics:

  • Script Analysis
  • Steganography Principles
  • Actual testing
  • Programming skills
  • Optimization ideas

0x02 Script analysis

---

1. Refer to the documentation

https://github.com/peewpw/Invoke-PSImage/blob/master/README.md

(1) Select 4 bits from the two colors of each pixel to store the payload

(2) Image quality will be affected

(3) Output format is png

2. Analyze the above description with reference to the source code

(1) The pixel uses RGB mode, selecting the lower 4 bits of the G and B color components (8 bits in total) to store the payload

(2) Since the lower 4 bits of both G and B are replaced, image quality will be affected

Additional note:

LSB steganography replaces the lowest 1 bit of the three RGB components, which is imperceptible to the human eye, allowing 3 bits of information to be stored per pixel

It is speculated that Invoke-PSImage chooses to store 8 bits per pixel for ease of implementation (8 bits = 1 byte), thus sacrificing image quality.

(3) The output format is PNG, which requires lossless compression.

PNG images use lossless compression (BMP images also use lossless compression), while JPG images use lossy compression. Therefore, in practical testing, when inputting a JPG image and outputting a PNG image, it will be found that the PNG image is much larger in size than the JPG image.

(4) Pay attention to the payload length; each pixel stores one byte, so the number of pixels must be greater than the length of the payload.

0x03 Steganography Principle

---

Refer to the source code for an example explanation (skipping the part about reading the original image).

1. Modify the RGB values of the pixels and replace them with the payload.

Code starting position:

https://github.com/peewpw/Invoke-PSImage/blob/master/Invoke-PSImage.ps1#L110

Make a simple modification to the for loop, assuming the need to read 0x73 and write it to the first pixel RGB(0x67,0x66,0x65).

(1) Read the payload.

Code:

$paybyte1 = [math]::Floor($payload[$counter]/16)

Explanation:

$payload[$counter]/16 means $payload[$counter]/0x10

i.e., take 0x73/0x10, take the quotient, equals 0x07

Therefore, $paybyte1 = 0x07

Code:

$paybyte2 = ($payload[$counter] -band 0x0f)

Explanation:

i.e., 0x73 & 0x0f, result is 0x03

Therefore, $paybyte2 = 0x03

Code:

$paybyte3 = ($randb[($counter+2)%109] -band 0x0f)

Explanation:

Used as random padding, $paybyte3 can be ignored

Note:

The original code compares the length of the payload with the pixel length of the image; extra pixels in the image are filled with random numbers in the same format

(2) Assign values to original pixels, adding payload

Original pixel is RGB(0x62,0x61,0x60)

Code:

$rgbValues[($counter*3)] = ($rgbValues[($counter*3)] -band 0xf0) -bor $paybyte1

Explanation:

i.e., 0x60 & 0xf0 | 0x07

Thus, $rgbValues[0] = 0x67

Code:

$rgbValues[($counter*3+1)] = ($rgbValues[($counter*3+1)] -band 0xf0) -bor $paybyte2

Explanation:

i.e., 0x61 & 0xf0 | 0x03

Thus, $rgbValues[1] = 0x63

Code:

$rgbValues[($counter*3+2)] = ($rgbValues[($counter*3+2)] -band 0xf0) -bor $paybyte3

Explanation:

Random number padding, can be ignored

In summary, the modification process for the new pixel is:

R: High bits unchanged, low 4 bits filled with random numbers

G: High bits unchanged, low 4 bits filled with the low 4 bits of the payload

B: High bits unchanged, low 4 bits filled with the high 4 bits of the payload

2. Read RGB to reconstruct the payload

Make a simple modification to the output: read and reconstruct the payload from the first pixel

Code to retrieve the 0th pixel:

$a = New-Object;
Add-Type -AssemblyName "System.Drawing";
$g = New-Object System.Drawing.Bitmap("C:\1\evil-kiwi.png");
$p = $g.GetPixel(0,0);
$p;

Reconstruct the payload and output its first character. Code:

$o = [math]::Floor(($p.B -band 15)*16) -bor ($p.G -band 15);
[math]::Floor(($p.B -band 15)*16) -bor ($p.G -band 15));

0x04 Actual Testing

---

Parameters Used:

Invoke-PSImage -Script .\test.ps1 -Image .\kiwi.jpg -Out .\evil-kiwi.png

test.ps1: Contains payload, e.g., "start calc.exe"

kiwi.jpg: Input image, pixel count must be greater than payload length

evil-kiwi.png: Output image path

After script execution, code to read the image, decrypt the payload, and execute it will be output

Actual demonstration omitted

0x05 Optimization Ideas

---

Based on previous analysis, replacing the lower 4 bits of two RGB components to store the payload may affect image quality to some extent. Refer to the principle of LSB steganography by replacing only the least significant bit of all three components, achieving an effect indistinguishable to the human eye

Of course, this method is merely an application of steganography and cannot bypass Win10's AMSI interception

Testing on Win10 systems also requires consideration of AMSI bypass techniques

0x06 Summary

---

This article analyzes the code of Invoke-PSImage, introduces the principles of encryption and decryption, discusses its advantages and disadvantages, proposes optimization ideas, and helps everyone better conduct learning and research.