Skip to content

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.

ppee-cli [report switches] --set ADDRESS=VALUE [--set ADDRESS=VALUE ...] [--save [-o OUT]] <file>
  • 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. -o writes 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 --headers or redirect stdout (> /dev/null, or > NUL on 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
$ ppee-cli --headers in.exe | grep DllCharacteristics
  OptionalHeader.DllCharacteristics        = 8160
$ ppee-cli --set OptionalHeader.DllCharacteristics=8120 --save -o noaslr.exe in.exe > /dev/null
applied 1 field edit(s)
saved 'noaslr.exe'

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):

Cell:<Table>:down:<record>:<field>:3=VALUE

<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.

$ ppee-cli --set 'RawBytes:rva:1000=90 90 ?? C3' --save -o out.exe in.exe > /dev/null

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

strip-timestamps.sh: make builds reproducible
#!/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