Methods to hex edit binary files via Powershell
Solution 1:
You already have a byte array, so you could simply modify the bytes at any given offset.
$bytes = [System.IO.File]::ReadAllBytes("C:\OldFile.exe")
$offset = 23
$bytes[$offset] = 0xFF
$bytes[$offset+1] = 0xFF
$bytes[$offset+2] = 0xFF
[System.IO.File]::WriteAllBytes("C:\NewFile.exe", $bytes)
Solution 2:
Probably the way most idiomatic to PowerShell would be:
$offset = 0x3C
[byte[]]$bytes = Get-Content C:\OldFile.exe -Encoding Byte -Raw
$bytes[$offset++] = 0xFF
$bytes[$offset++] = 0xFF
$bytes[$offset] = 0xFF
,$bytes |Set-Content C:\NewFile.exe -Encoding Byte
Solution 3:
How can we specify an offset position into PowerShell to replace this sketchy
-replace
command.
Ansgar Wiechers' helpful answer addresses the offset question, and brianary's helpful answer shows a more PowerShell-idiomatic variant.
That said, it sounds like if you had a solution for replacing only the first occurrence of your search string, your original solution may work.
First-occurrence-only string replacement:
Unfortunately, neither PowerShell's -replace
operator nor .NET's String.Replace()
method offer limiting replacing to one occurrence (or a fixed number).
However, there is a workaround:
$hx = $hx -replace '(?s)123456(.*)', 'FFFFFF$1'
(?s)
is an inline regex option that makes regex metacharacter.
match newlines too.(.*)
captures all remaining characters in capture group 1, and$1
in the replacement string references them, which effectively removes just the first occurrence. (See this answer for the more information about-replace
and the syntax of the replacement operand.)-
General caveats:
If your search string happens to contain regex metacharacters that you want to be taken literally,
\
-escape them individually or, more generally, pass the entire search term to[regex]::Escape()
.If your replacement string happens to contain
$
characters that you want to be taken literally,$
-escape them or, more generally, apply-replace '\$', '$$$$'
(sic) to it.
However, as iRon points out, while the above generically solves the replace-only-once problem, it is not a fully robust solution, because there is no guarantee that the search string will match at a byte boundary; e.g., single-byte search string 12
would match the middle 12
in 0123
, even though there is no byte 12
in the input string, composed of bytes 01
and 23
.
To address this ambiguity, the input "byte string" and the search string must be constructed differently: simply separate the digits constituting a byte each with spaces, as shown below.
Replacing byte sequences by search rather than fixed offsets:
Here's an all-PowerShell solution (PSv4+) that doesn't require third-party functionality:
Note:
As in your attempt, the entire file contents are read at once, and to-and- from string conversion is performed; PSv4+ syntax
-
To construct the search and replacement strings as "byte strings" with space-separated hex. representations created from byte-array input, use the same approach as for constructing the byte string from the input as shown below, e.g.:
-
(0x12, 0x34, 0x56, 0x1).ForEach('ToString', 'X') -join ' '
->'12 34 56 1'
-
.ForEach('ToString', 'X')
is the equivalent of calling.ToString('X')
on each array element and collecting the results.
-
- If prefer each byte to be consistently represented as two hex digits, even for values less than
0x10
, (e.g.,01
rather than1
), use'X2'
, which increases memory consumption, however.
Also, you'll have to0
-prefix single-digit byte values in the search string too, e.g.:'12 34 56 01'
-
# Read the entire file content as a [byte[]] array.
# Note: Use PowerShell *Core* syntax.
# In *Windows PowerShell*, replace `-AsByteStream` with `-Encoding Byte`
# `-Raw` ensures that the file is efficiently read as [byte[]] array at once.
$byteArray = Get-Content C:\OldFile.exe -Raw -AsByteStream
# Convert the byte array to a single-line "byte string",
# where the whitespace-separated tokens are the hex. encoding of a single byte.
# If you want to guaranteed that even byte values < 0x10 are represented as
# *pairs* of hex digits, use 'X2' instead.
$byteString = $byteArray.ForEach('ToString', 'X') -join ' '
# Perform the replacement.
# Note that since the string is guaranteed to be single-line,
# inline option `(?s)` isn't needed.
# Also note how the hex-digit sequences representing bytes are also separated
# by spaces in the search and replacement strings.
$byteString = $byteString -replace '\b12 34 56\b(.*)', 'FF FF FF$1'
# Convert the byte string back to a [byte[]] array, and save it to the
# target file.
# Note how the array is passed as an *argument*, via parameter -Value,
# rather than via the pipeline, because that is much faster.
# Again, in *Windows PowerShell* use `-Encoding Byte` instead of `-AsByteStream`.
[byte[]] $newByteArray = -split $byteString -replace '^', '0x'
Set-Content "C:\NewFile.exe" -AsByteStream -Value $newByteArray
Solution 4:
As far as I can oversee the quest, there is no need to do any hexadecimal conversion on a byte stream to do a replacement. You can just do a replacement on a decimal value list (default string conversion) where the values are bounded by spaces (word ends), e.g.:
(I am skipping the file input/output which is already explained in the answer from @mklement0)
$bInput = [Byte[]](0x69, 0x52, 0x6f, 0x6e, 0x57, 0x61, 0x73, 0x48, 0x65, 0x72, 0x65)
$bOriginal = [Byte[]](0x57, 0x61, 0x73, 0x48)
$bSubstitute = [Byte[]](0x20, 0x77, 0x61, 0x73, 0x20, 0x68)
$bOutput = [Byte[]]("$bInput" -Replace "\b$bOriginal\b", "$bSubstitute" -Split '\s+')
In case you like to use hexadecimal strings (e.g. for the replace arguments), you can convert a hex string to a byte array as follows: [Byte[]]('123456' -Split '(..)' | ? { $_ } | % {[Convert]::toint16($_, 16)})
Note that this solution supports different $bOriginal
and $bSubstitute
lengths. In such a case, if you like to start replacing from a specific offset you might want to use the Select-Object
cmdlet:
$Offset = 3
$bArray = $bInput | Select -Skip $Offset
$bArray = [Byte[]]("$bArray" -Replace "\b$bOriginal\b", "$bSubstitute" -Split '\s+')
$bOutput = ($bInput | Select -First $Offset) + $bArray