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; |
Reconstruct the payload and output its first character. Code:
$o = [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.