Editing with --set¶
--set edits the file in memory using the same write path as the GUI's cell editor. Anything you can double-click and change in a GUI list view, you can change from a script.
- Edits are applied in order, before the report is generated, so a report switch shows the edited values.
- Without
--save, nothing is written. Use this to preview an edit. - With
--save, the write is all or nothing.-owrites to a new file instead of the input. - The report is still printed, and with no section filter that means everything. When you only want the edit, add a narrow filter such as
--headersor redirect stdout (> /dev/null, or> NULon Windows). The edit and save messages go to stderr.
Address forms¶
| Form | Reaches | Value |
|---|---|---|
<FieldName> | Header, data directory and section fields | hex |
Cell:<Node>:up:<row>:<col> | Any upper list-view cell | hex, or text for name columns |
Cell:<Node>:down:<upperRow>:<row>:<col> | Any lower list-view cell (the rows shown after selecting upper row <upperRow>) | hex, or text |
Cell:ResEntry_Type@<typeIndex>:up\|down:… | Cells in resource type views | hex, or text |
Cell:…:dec | Any Cell: address | decimal |
RawOffset:<hexOffset>:<width> | Any bytes: 1–8 bytes written little-endian | hex |
RawBytes:<hexOffset> · RawBytes:rva:<hex> · RawBytes:va:<hex> | A byte string of any length, for code | hex bytes |
RawBits:<hexOffset>:<width>:<shift>:<count> | Only some bits of a 1–8 byte little-endian field | hex |
String:<hexOffset>:ascii\|unicode:<maxChars> | A string found by --strings, rewritten in place | text |
Field names¶
These are the names printed by --headers, --dirs and --sections:
ppee-cli --set FileHeader.TimeDateStamp=5F000000 --save -o out.exe in.exe > /dev/null
ppee-cli --set 'Section[0].Name=.code' --save -o out.exe in.exe > /dev/null
ppee-cli --set 'DataDirectory[6].Size=0' --save -o out.exe in.exe > /dev/null # drop the debug directory entry
Clear ASLR (DYNAMIC_BASE, 0x40) from DllCharacteristics
Cell: addresses¶
A Cell: address points at one cell of a GUI list view:
<Node>is the view's internal name (table below) or a number.<row>is the row's Indx value.<col>is the column index, where Indx is column 0. In field-style views, Member = 1, Value = 2 and Comment = 3.- Columns that PPEE derives itself, such as Demangled name or the "Imported functions" bar, can't be edited.
<Node> | GUI view |
|---|---|
DIR_Export | DIR_ENTRY_EXPORT |
DIR_Import | DIR_ENTRY_IMPORT (upper: modules; lower: functions) |
DIR_Resource | DIR_ENTRY_RESOURCE |
DIR_Exception | DIR_ENTRY_EXCEPTION |
DIR_Security | DIR_ENTRY_SECURITY |
DIR_BaseReloc | DIR_ENTRY_BASERELOC |
DIR_Debug | DIR_ENTRY_DEBUG |
DIR_TLS | DIR_ENTRY_TLS |
DIR_LoadConfig, LoadConfig_Safe_SEH, LoadConfig_Guard_Function, … | DIR_ENTRY_LOAD_CONFIG and its tables |
DIR_BoundImport | DIR_ENTRY_BOUND_IMPORT |
DIR_DelayImport | DIR_ENTRY_DELAY_IMPORT |
DIR_Net | DIR_ENTRY_COM_DESCRIPTOR |
CLR_Dir_Metadata, CLR_Dir_Metadata_Tables | .NET metadata root / #~ |
CLR_Dir_Metadata_Strings, _US, _GUID, _Blob | .NET heaps |
CLR_Dir_VTableFixups | VTableFixups |
Module, TypeRef, TypeDef, Field, Method, Param, MemberRef, CustomAttribute, … | .NET metadata tables (see below) |
Import module list (upper view): columns are 0 Indx · 1 Name RVA · 2 Name · 3 OriginalFirstThunk · …
$ ppee-cli --imports --set 'Cell:DIR_Import:up:0:2=msvcrX.dll' in.dll
applied 1 field edit(s)
msvcrX.dll (OFT=94A90 TimeDateStamp=0 FT=72540) - 78 function(s)
Import function list (lower view, after selecting module row 0): columns are 0 Indx · 1 OFT · 2 FT · 3 Hint · 4 Name · 5 Demangled name · 6 Ordinal
# Rename function 0 of module 0 (same length or shorter)
ppee-cli --set 'Cell:DIR_Import:down:0:0:4=wcscat_x' --save -o out.dll in.dll > /dev/null
# Set its hint using a decimal value
ppee-cli --set 'Cell:DIR_Import:down:0:0:3:dec=1281' --save -o out.dll in.dll > /dev/null
Names can't grow
Text is rewritten in place, so a new name must fit in the space of the old one. A shorter name is NUL-padded. A longer one is refused (--set failed: …), exactly as in the GUI, where the edit box warns while you type.
Finding row and column numbers
Open the file in the GUI, select the view, and count columns from Indx = 0. Right-click a cell and choose Follow in Hex View to see which bytes it covers.
.NET metadata tables¶
Metadata tables use the lower view, with the record as <upperRow>, the field as <row>, and column 3 for the value (a leftover from the original UI's column layout):
<field> counts the table's columns from 0, in ECMA-335 order. For TypeDef that is 0 Flags, 1 TypeName, 2 TypeNamespace, 3 Extends, 4 FieldList and 5 MethodList. Values are hex heap indexes or tokens, as shown in the GUI's Value column.
# Set the Flags of TypeDef record 1
ppee-cli --set 'Cell:TypeDef:down:1:0:3=00100001' --save -o patched.dll managed.dll > /dev/null
RawOffset:¶
Writes the value as <width> little-endian bytes at a file offset. Both the offset and the value are hex. The value is one integer, so 01020304 lands as 04 03 02 01; a value wider than <width> is refused, not truncated. For a byte string use RawBytes:.
$ ppee-cli --set RawOffset:4E:2=4142 --save -o out.dll in.dll > /dev/null
applied 1 field edit(s)
saved 'out.dll'
$ xxd -s 0x4e -l 2 out.dll
0000004e: 4241 BA
RawBytes:¶
Writes a hex byte string, in the order given, at a file offset, or at an RVA or VA. Spaces and commas are ignored, and ?? keeps the byte that is there.
At rva: and va: addresses every byte must lie in the headers or in one section's data in the file: never the memory-only tail of a section, never across into the next one. A range that does not fit is refused.
RawBits:¶
Writes only bits [shift, shift+count) of a <width>-byte little-endian field and keeps the field's other bits (a read-modify-write). Use it for flag fields and packed records. A value that doesn't fit in <count> bits is refused, not truncated.
$ xxd -s 0x4e -l 2 in.dll
0000004e: 5468 Th
$ ppee-cli --set RawBits:4E:2:0:4=F --save -o out.dll in.dll > /dev/null # bits 0-3 := 0xF
$ xxd -s 0x4e -l 2 out.dll
0000004e: 5f68 _h
$ ppee-cli --set RawBits:4E:2:4:4=A --save -o out.dll in.dll > /dev/null # bits 4-7 := 0xA
0000004e: a468 .h
$ ppee-cli --set RawBits:4E:2:0:4:dec=9 --save -o out.dll in.dll > /dev/null # decimal value
0000004e: 5968 Yh
$ ppee-cli --set RawBits:4E:2:0:4=1F --save -o out.dll in.dll # 0x1F needs 5 bits
--set failed: unknown field or bad value 'RawBits:4E:2:0:4'
not saved: an --set edit failed, nothing written
Optional suffixes:
| Suffix | Meaning |
|---|---|
:dec | The value is decimal instead of hex |
:unit=<n> | The value is counted in units of n: it must be a multiple of n, and value / n is stored. Used by records that store a scaled offset, such as the ARM64 kernel import call record in the load config |
$ ppee-cli --set 'RawBits:4E:2:0:8:unit=4=10' --save -o out.dll in.dll > /dev/null # hex 10 = 16, 16 / 4 = 4 stored
0000004e: 0468 .h
$ ppee-cli --set 'RawBits:4E:2:0:8:dec:unit=4=20' --save -o out.dll in.dll > /dev/null # decimal 20 / 4 = 5 stored
0000004e: 0568 .h
$ ppee-cli --set 'RawBits:4E:2:0:8:unit=4=9' --save -o out.dll in.dll # 9 is not a multiple of 4
--set failed: unknown field or bad value 'RawBits:4E:2:0:8:unit=4'
not saved: an --set edit failed, nothing written
Quote the whole argument in a shell, since it contains = twice. --set treats the first = that isn't part of :unit= as the separator between address and value.
RawOffset: accepts a trailing tag (:ts, :guardflags, :extdllchar). It only tells the GUI which picker to offer for the cell (a timestamp, GuardFlags bits or extended DLL characteristics); the write itself ignores it.
String:¶
Rewrites a string at a file offset (as listed by --strings), NUL-padding it to <maxChars>. unicode writes UTF-16LE.
$ ppee-cli --set String:4E:ascii:10=Hello --save -o out.dll in.dll > /dev/null
$ xxd -s 0x4e -l 12 out.dll
0000004e: 4865 6c6c 6f00 0000 0000 616d Hello.....am
Error handling¶
| Situation | Message (stderr) | Exit code | File written? |
|---|---|---|---|
All edits applied, no --save | applied N field edit(s) | 0 | No |
All applied, --save | applied N field edit(s) then saved '<path>' | 0 | Yes |
| An address is unknown, malformed, or its value doesn't fit | --set failed: unknown field or bad value '<ADDRESS>' | 1 | No |
An edit failed and --save was given | … not saved: an --set edit failed, nothing written | 1 | No |
| Could not write the output | failed to save '<path>' | 1 | No |
Batch patching example¶
#!/usr/bin/env bash
set -euo pipefail
for f in dist/*.exe dist/*.dll; do
ppee-cli --no-similarity --set FileHeader.TimeDateStamp=0 --save "$f" > /dev/null
done
Checksums and signatures
PPEE writes exactly the bytes you asked for. It leaves OptionalHeader.CheckSum as it is unless you add --set OptionalHeader.CheckSum=auto, which recomputes it after all the other edits. Any edit invalidates an Authenticode signature. Re-sign after patching if you need to.
Related: GUI editing · MCP patch_pe · All options
References¶
- Microsoft PE format specification, optional header fields: the fields you can edit, including CheckSum.
- SignTool (Microsoft): signs a file again after an edit, and verifies signatures.