x86 support
picaro packs one architecture at a time. build.output.arch decides which:
arch | Stub target (Windows / cross) | Payload | windows import lib |
|---|---|---|---|
x64 (default) | x86_64-pc-windows-msvc / -gnu | PE32+ | windows_x86_64_msvc / _gnu |
x86 | i686-pc-windows-msvc / -gnu | PE32 | windows_i686_msvc / _gnu |
The rule: the stub and the payload share an architecture
Section titled “The rule: the stub and the payload share an architecture”Reflective loading runs the payload in the stub’s own process, so the two
must match: 32-bit code cannot run in a 64-bit process, and vice versa. A plan
whose payload does not match output.arch is rejected at build time, before
anything is encrypted or compiled:
build.output.arch is 'x64' but the payload is an x86 PE (machine 0x014c):the stub and the payload must share an architectureAn arch: x86 artifact is a 32-bit PE. On 64-bit Windows it runs under WOW64
and sees the WOW64 filesystem redirection (C:\Windows\System32\… resolves to
C:\Windows\SysWOW64\…, which is usually what you want — the 32-bit
svchost.exe and the 32-bit ntdll.dll). A 64-bit stub cannot run on
32-bit-only Windows.
Status
Section titled “Status”| Milestone | State | What it covers |
|---|---|---|
| M0 — foundations | done | PE32 parsing, schema/validation coupling, capability gating, i686 compiler, PE32 imports, x86 args, fixtures |
M1 — reflective_loading | done | PE32 mapper, 32-bit IMAGE_TLS_DIRECTORY, x86 TLS provisioning, x86 entry call |
M2 — process_hollowing | done | PE32 target parsing, CONTEXT32, Eip hand-off, fs:[0x2C] TLS slot, WOW64 target |
| M3 — preparations | done | anti_debug (x86 debug registers), patch_amsi, patch_etw, bouncer |
| M4 — x86 indirect syscalls | parked | needs a Heaven’s Gate thunk and 64-bit SSN resolution — a redesign, not a flag (see below) |
| M5 — .NET hosting | done | format: dotnet + dotnet_hosting: the stub hosts the CLR and loads the assembly from memory (x86 and x64) |
Verified end to end on Windows (32-bit payloads, under WOW64 on an x64 host):
packs_and_runs_an_x86_reflective_payload— a PE32 payload mapped into the 32-bit stub prints its marker and reads its ownthread_local;packs_and_runs_an_x86_hollowed_payload— a PE32 payload hollowed into the WOW64svchost.exewrites its marker, readstls=42and its exit code (42) comes back through the stub;x86_preparations_run_before_the_payload—anti_debug,patch_amsi,patch_etwandbouncerrun in a 32-bit stub before the payload;packs_and_runs_a_managed_payload— a 32-bit managed assembly runs on the CLR hosted by the 32-bit stub, receives the payload args, reportsx64=Falseand its exit code (42) comes back through the stub;packs_and_runs_sharphound— the real-world x86 assembly in the repository root packs and its output is identical to the unpacked one.
The foundations (M0) are unchanged from before: the schema accepts arch: x86,
the compiler builds a valid i686 stub, and the shared blocks are
architecture-aware:
engine::peparses PE32 and PE32+, andbuildenforces that the payload’s arch matchesoutput.arch.- The compiler is arch-parameterized: target triple, dependency set and
windows_i686_*/windows_x86_64_*import library. - The shared import resolver reads the image’s optional-header magic: 4-byte
thunks and
IMAGE_ORDINAL_FLAG32for PE32, 8-byte andIMAGE_ORDINAL_FLAG64for PE32+, with the matching IAT slot width. - The payload-argument block uses the right PEB offsets (
fs:[0x30],+0x10,+0x40on x86;gs:[0x60],+0x20,+0x70on x64). - Resources and
audithandle both layouts; the CI and the Docker image install and build the i686 target.
reflective_loading and process_hollowing declare
WindowsX64 | WindowsX86; so do anti_debug, patch_amsi, patch_etw and
bouncer. sleep_obfuscation stays x64-only, and an arch: x86 pipeline that
uses it is rejected at validation with a message naming the technique:
technique 'sleep_obfuscation' does not support build.output.arch 'x86' yet:it requires x64; the packer can compile an x86 stub, but this technique has nox86 fragmentHow the x86 fragments differ
Section titled “How the x86 fragments differ”reflective_loading.IMAGE_NT_HEADERS32, the hand-defined 32-bitIMAGE_TLS_DIRECTORY, 4-byte IAT slots andIMAGE_REL_BASED_HIGHLOWrelocations come from the shared mapper; the entry point is called through the x86 ABI (arguments on the stack — there is no register hand-off).process_hollowing. PE32 offsets in the payload parser (ImageBaseis au32at a different offset), the nativeCONTEXT32, the PEB’sImageBaseAddressat+0x08read fromEbx, the entry written toEip, and the thread’s TLS slot atfs:[0x2C]with 4-byte entries. The default target (System32\svchost.exe) is redirected by WOW64 toSysWOW64\svchost.exe.- TLS provisioning. Both techniques give a manually mapped image a real TLS
index, block and slot; the arrays differ per architecture (see the technique
READMEs and
docs/measurements.md). dotnet_hosting. The COM interfaces and vtable slots are the same on both architectures; what differs is theVARIANTlayout (16 bytes on x86, 24 on x64) and theInvoke_3ABI — the variant is passed by value on x86 and by reference on x64, and the fragment asserts the size it expects at build time.
Limitations
Section titled “Limitations”- Architecture must match. See the rule above; it is a hard build-time check.
syscalls.mode: indirectis x64-only, and M4 parked it deliberately. The x64 path uses asyscall; retgadget plus an x64global_asm!trampoline. A 32-bit process cannot executesyscallat all: it runs in compatibility mode, where the instruction is invalid, and the only way in is Heaven’s Gate — a far transfer to a 64-bit code segment (0x33) around a hand-written 64-bit thunk. On top of that the SSNs a 32-bit stub can read from the liventdllstubs are the WOW64 service numbers, which the kernel maps through a separate table; they are not the numbers the 64-bitsyscallpath expects. A real x86 implementation therefore needs the gate thunk and 64-bit SSN resolution (the 64-bitntdllis mapped above 4 GB and unreachable from 32-bit pointers), so it is a redesign of the syscall layer, not a per-architecture flag.mode: indirectis rejected onarch: x86instead of silently degrading. Becauseapi_resolution.mode: proxyrequires indirect, it is x64-only too.- .NET assemblies need CLR hosting (done, M5). A managed assembly’s entry
point calls
mscoree.dll!_CorExeMain, which starts the CLR for the process’s main module — the stub, not the mapped payload — so reflectively mapping a .NET assembly does not run it (that is now a build-time error). A managed payload declaresformat: dotnetand thedotnet_hostingexecution technique: the stub hosts the CLR (CLRCreateInstance→ICorRuntimeHost→_AppDomain.Load_3→GetEntryPoint→Invoke_3) and loads the assembly from memory. The assembly runs on the CLR of the stub’s bitness, so a32BITREQUIREDassembly needsarch: x86and an AnyCPU/IL-only one runs on either — checked at build time (SharpHound.exe, 32-bit .NET, is the acceptance case). - A mapped image’s TLS index can collide with a loaded module’s.
TlsAllocreturns the lowest free index and the index has to be reserved that way (seedocs/measurements.md); when it coincides with a module’s_tls_index, that module’s thread-locals in the affected thread are the payload’s template bytes. The stub logs the index underbuild.stub.debug. sleep_obfuscation(ekko) is x64-only. Its ROP chain is built on the x64 ABI (Rip/Rsp,Rcx–R9); an x86 port needs the cdecl/stack convention.process_hollowingneeds an arch-matching target. Under WOW64 theSystem32\svchost.exeliteral already resolves to the 32-bit one, so the default happens to work for an x86 stub — but it is implicit, and a target given as$arg.N$cannot be checked at build time.shellcodehas no architecture in its bytes, so it followsoutput.arch.- x86 SEH is stack-based (
fs:[0]chain), while x64 is table-based (.pdata/.xdata). x86 needs no unwind-table registration for a mapped image, but the x64 side does not register one either today, so payload exception unwinding inside a mapped image is unsupported on x64 and is a separate feature.IMAGE_DIRECTORY_ENTRY_EXCEPTIONis not meaningful in PE32. - TLS callbacks under process hollowing are not run (a never-started thread has no TLS array to point at) — unchanged by architecture.
dotnet_hostingisreflective_loading-only in practice. The CLR is hosted in the stub’s own process, and a pipeline has exactly one execution technique, soprocess_hollowinganddotnet_hostingcannot be combined;format: dotnetanddotnet_hostingimply each other at validation.