This blog was coauthored by Nithin Chenthur Prabhu and Abdelrhman Mohamed.
Notepad is a common tool for windows users to jot down important notes or messages quickly. Beyond text files, Notepad can also open files in various known and unknown formats. However, over the years, adversaries have exploited Notepad as part of their malicious strategies, sometimes even disguising their activities by masquerading malicious processes as Notepad.
Given this trend, it’s important to understand what resides in Notepad’s process memory region. Advanced Persistent Threats (APTs) like Cozy Bear, Double Dragon, and Lazarus have utilized Notepad for process injection, DLL sideloading, shellcode or payload injection, and process hollowing.
We will look into methods for extracting user data from Notepad’s process memory and analyzing it for signs of malicious behavior.
To being our tests, we’ll use a Windows 10 virtual machine as a test environment. We launched Notepad and typed: “Abdelrhman and Azr43LKn1ght”.
If necessary, we can close Notepad, as its process will remain in memory for a short time. The first step is to capture the workstation’s memory using tools like FTK Imager’s Memory Capture, DumpIt.exe, or Magnet RAM Capture.
If the memory dump is not in .dmp format, it must be converted for compatibility with WinDbg. Tools like Volatility and MemProcFS can assist with this conversion. Once the memory dump (e.g., <file>.dmp ) is ready, we can process to debug it using WinDbg to analyze the captured Notepad process for signs of malicious behavior.
Before diving into the analysis, let’s examine the structure of memory in Windows. The operating system uses the _EPROCESS structure to represent a process. Each process has its own linear address space, called the Virtual Memory Space or Virtual Address Space, which is completely isolated from other processes.
This address space includes several key components:
- The core process executable: The main binary file of the process.
- Loaded modules: A list of all associated Dynamic Link Libraries (DLLs) an shared libraries.
- Process stack: Memory used for function calls and local variables.
- Process heaps : Memory areas for dynamic memory allocation.
- Virtual Address Descriptors (VADs): Regions of memory allocated by the process.
The structure of _EPROCESS is:
nt!_EPROCESS +0x000 Pcb : _KPROCESS +0x438 ProcessLock : _EX_PUSH_LOCK +0x440 UniqueProcessId : Ptr64 Void +0x448 ActiveProcessLinks : _LIST_ENTRY +0x458 RundownProtect : _EX_RUNDOWN_REF +0x460 Flags2 : Uint4B +0x460 JobNotReallyActive : Pos 0, 1 Bit [SNIP] +0x510 Job : Ptr64 _EJOB +0x518 SectionObject : Ptr64 Void +0x520 SectionBaseAddress : Ptr64 Void +0x528 Cookie : Uint4B +0x530 WorkingSetWatch : Ptr64 _PAGEFAULT_HISTORY +0x538 Win32WindowStation : Ptr64 Void +0x540 InheritedFromUniqueProcessId : Ptr64 Void +0x548 OwnerProcessId : Uint8B +0x550 Peb : Ptr64 _PEB +0x558 Session : Ptr64 _MM_SESSION_SPACE [SNIP] +0x7d8 VadRoot : _RTL_AVL_TREE +0x7e0 VadHint : Ptr64 Void +0x7e8 VadCount : Uint8B +0x7f0 VadPhysicalPages : Uint8B +0x7f8 VadPhysicalPagesLimit : Uint8B [SNIP] +0xa00 DynamicEHContinuationTargetsTree : _RTL_AVL_TREE +0xa08 DynamicEHContinuationTargetsLock : _EX_PUSH_LOCK |
The Process Control Block (PCB), represented by the _KPROCESS structure, is located at the base of the _EPROCESS structure. The PCB contains essential fields like:
- DirectoryTableBase: Used for address translation.
- Total time spent in kernel mode and user mode: Tracks the process’s CPU’s usage.
- Process state: Indicated whether the process is running, waiting, or suspended.
Another critical field in _EPROCESS is ActiveProcessLinks, a doubly linked list that links all active processes on the machine. This list is fundamental to system process management, enabling tools like Task Manager and system APIs to navigate, access, and manage running processes effectively.
This structure is essential for operating system process management, as it facilitates efficient operations like process information retrieval and scheduling.
In 2010, a notorious malware called Prolaco exploited this system. It performed Direct Kernel Object Manipulation from user mode without any kernel space driver or rootkit. Prolaco achieved this by:
- Enabling SeDebugPrivilege using the ZwSystemDebugControl function.
- Accessing the first _EPROCESS object from PsInitialSystemProcess in the NT kernel module.
- Traversing the double linked list of _EPROCESS objects to locate its own malware process.
- Unlinking its process from the ActiveProcessLinks list by overwriting the flink (forward link) of next process and the blink (backward link) of previous process.
The PsActiveProcessHead is a global kernel variable that holds the address of the first process in the list of active processes.
The Process Environment Block (PEB) is a pointer in kernel mode that references an address in user mode. It holds critical information about a process, including:
- Pointers to the process's DLL lists.
- The current working directory.
- Command-line arguments.
- Environment variables.
- Details of heaps.
- Handles for standard input/output streams.
VadRoot is the root node of the VAD tree. The VAD tree holds detailed information about all memory segments allocated to a process. This includes the original access permissions which are read, write, or execute and whether a file is mapped to the memory region.
To begin analyzing the Notepad process, we first nee to locate the _EPROCESS block associated with notepad.exe. This can be achieved using the !process command in WinDbg:
!process 0 0 notepad.exe
The !process extension provides information about the specified process or all processes, including the _EPROCESS block. Here’s a breakdown of the command argument:
- First 0 argument specifies the process option, which in this case is set to 0, meaning "all process contexts."
- Second 0 argument specifies the "flags" parameter. A flag value of 0 means "minimal output."
We now know the _EPROCESS block’s address of notepad.exe is ffff8701845ce340
.process /r /p ffff8701845ce340
This command sets the debugger’s context to the specific process, notepad.exe. The /r switch tells WinDbg to reload the user-mode symbols and modules for the specified process. The /p switch forces the debugger to synchronize its process state with the target, halting the process execution to provide an accurate view of its memory and current state.
Before looking at the VAD tree, let’s first look at the PEB.
dx -id 0,0,ffff8701845ce340 -r1 ((ntkrnlmp!_PEB *)0xd22fcf8000) ((ntkrnlmp!_PEB *)0xd22fcf8000) : 0xd22fcf8000 [Type: _PEB *] [SNIP] [+0x030] ProcessHeap : 0x21f68490000 [Type: void *] [+0x038] FastPebLock : 0x7fff556590e0 [Type: _RTL_CRITICAL_SECTION *] [SNIP] [+0x0c0] CriticalSectionTimeout : {-25920000000000} [Type: _LARGE_INTEGER] [+0x0c8] HeapSegmentReserve : 0x100000 [Type: unsigned __int64] [+0x0d0] HeapSegmentCommit : 0x2000 [Type: unsigned __int64] [+0x0d8] HeapDeCommitTotalFreeThreshold : 0x10000 [Type: unsigned __int64] [+0x0e0] HeapDeCommitFreeBlockThreshold : 0x1000 [Type: unsigned __int64] [+0x0e8] NumberOfHeaps : 0x4 [Type: unsigned long] [+0x0ec] MaximumNumberOfHeaps : 0x10 [Type: unsigned long] [+0x0f0] ProcessHeaps : 0x7fff55657d40 [Type: void * *] [+0x0f8] GdiSharedHandleTable : 0x21f68790000 [Type: void *] [SNIP] |
From this, we understand that the process heap base is 0x21f68490000 and heap parameters are:
- HeapSegmentReserve = 0x100000 (1MB)
- HeapSegmentCommit = 0x2000 (8KB)
- HeapDeCommitTotalFreeThreshold = 0x10000 (64KB)
- HeapDeCommitFreeBlockThreshold = 0x1000 (4KB)
- NumberOfHeaps = 0x4 (4 heaps)
- ProcessHeaps = 0x7fff55657d40 (pointer to array of heap handles)
From this, we get the heap allocation offsets from ProcessHeaps address that points to the heap handles.
kd> db 0x7fff55657d40 00007fff`55657d40 00 00 49 68 1f 02 00 00-00 00 2a 68 1f 02 00 00 ..Ih......*h.... 00007fff`55657d50 00 00 5b f8 1e 02 00 00-00 00 06 6a 1f 02 00 00 ...i.......j.... |
From this, we get the heap allocation offsets:
0000021f68490000, 0000021f682a0000
0000021ef85b0000, 0000021f6a060000
Now, let’s look at the VAD tree for all memory allocations for our process. This can be accessed through the VadRoot field, located at offset 0x7d8 from the _EPROCESS structure.
kd> dx -id 0,0,ffff8701845ce340 -r1 (*((ntdll!_RTL_AVL_TREE *)0xffff8701845ceb18)) (*((ntdll!_RTL_AVL_TREE *)0xffff8701845ceb18)) [Type: _RTL_AVL_TREE] [+0x000] Root : 0xffff8701844d00a0 [Type: _RTL_BALANCED_NODE *] |
We can see that the root of the VAD tree points to the first node which is at address 0xffff8701844d00a0.
Let’s inspect the root node of the VAD tree.
kd> dt nt!_RTL_BALANCED_NODE 0xffff8701844d00a0 +0x000 Children : [2] 0xffff8701`841c36c0 _RTL_BALANCED_NODE +0x000 Left : 0xffff8701`841c36c0 _RTL_BALANCED_NODE +0x008 Right : 0xffff8701`844d0280 _RTL_BALANCED_NODE +0x010 Red : 0y1 +0x010 Balance : 0y01 +0x010 ParentValue : 1 |
This node has two child nodes, each accessible via pointers. One child node is located to the left of the current node, and the other is to the right, depending on their respective positions in memory. By traversing the tree through these child nodes, we can reach a node that represents a heap allocation. Here, we will check the VADFLAGS and the allocation offsets for more memory allocation details.
kd> dt nt!_MMVAD ffff8701841c38a0 +0x000 Core : _MMVAD_SHORT +0x040 u2 : <anonymous-tag> +0x048 Subsection : (null) +0x050 FirstPrototypePte : 0x00000000`00000001 _MMPTE +0x058 LastContiguousPte : (null) +0x060 ViewLinks : _LIST_ENTRY [ 0x00000400`00000000 - 0x00000000`00060001 ] +0x070 VadsProcess : 0xffff8701`841c3910 _EPROCESS +0x078 u4 : <anonymous-tag> +0x080 FileObject : (null) |
Now, we can look at the node’s core to determine the offset of allocation. This will give us the starting and ending virtual address space offset.
kd> dt nt!_MMVAD_SHORT ffff8701841c38a0 +0x000 NextVad : 0xffff8701`840e74b0 _MMVAD_SHORT +0x008 ExtraCreateInfo : 0xffff8701`841c59c0 Void +0x000 VadNode : _RTL_BALANCED_NODE +0x018 StartingVpn : 0x21f6a060 +0x01c EndingVpn : 0x21f6a06f +0x020 StartingVpnHigh : 0 '' +0x021 EndingVpnHigh : 0 '' +0x022 CommitChargeHigh : 0 '' +0x023 SpareNT64VadUChar : 0 '' +0x024 ReferenceCount : 0n0 +0x028 PushLock : _EX_PUSH_LOCK +0x030 u : <anonymous-tag> +0x034 u1 : <anonymous-tag> +0x038 EventList : (null) |
Now, let’s look into the VADFLAGS.
dx -id 0,0,ffff8701845ce340 -r1 (*((ntkrnlmp!_MMVAD_FLAGS *)0xffff8701841c38d0)) (*((ntkrnlmp!_MMVAD_FLAGS *)0xffff8701841c38d0)) [Type: _MMVAD_FLAGS] [+0x000 ( 0: 0)] Lock : 0x0 [Type: unsigned long] [+0x000 ( 1: 1)] LockContended : 0x0 [Type: unsigned long] [+0x000 ( 2: 2)] DeleteInProgress : 0x0 [Type: unsigned long] [+0x000 ( 3: 3)] NoChange : 0x0 [Type: unsigned long] [+0x000 ( 6: 4)] VadType : 0x0 [Type: unsigned long] [+0x000 (11: 7)] Protection : 0x4 [Type: unsigned long] [+0x000 (17:12)] PreferredNode : 0x0 [Type: unsigned long] [+0x000 (19:18)] PageSize : 0x0 [Type: unsigned long] [+0x000 (20:20)] PrivateMemory : 0x1 [Type: unsigned long] |
Lock = 0: The VAD is currently not locked for any operations.
LockContended = 0: No other threads are waiting to acquire a lock on this VAD.
DeleteInProgress = 0: The VAD is not being deleted.
NoChange = 0: The VAD can be modified.
VadType = 0: This is VadNone type, typical for normal memory allocations like heaps.
Protection = 4: This means PAGE_READWRITE protection - the memory can be read from and written to this heap.
PreferredNode = 0: No specific non-uniform memory access (NUMA) node preference for this memory.
PageSize = 0: Using standard system page size (typically 4KB).
PrivateMemory = 1: This is private memory, not shared with other processes.
Let’s look into the starting and ending offset of the heap allocation. StartingVpn is 0x21f6a060 and the EndingVpn is 0x21f6a06f. Since the page size is 4096 bytes (or 0x1000), the resulting address range will be 0x21f6a060000 - 0x21f6a06f000.
Now, let’s look into HEAP structure of our heap address.
0: kd> dt _HEAP 0x21f6a060000 ntdll!_HEAP +0x000 Segment : _HEAP_SEGMENT +0x000 Entry : _HEAP_ENTRY +0x010 SegmentSignature : 0xffeeffee +0x014 SegmentFlags : 2 +0x018 SegmentListEntry : _LIST_ENTRY [ 0x0000021f`6a060120 - 0x0000021f`6a060120 ] +0x028 Heap : 0x0000021f`6a060000 _HEAP +0x030 BaseAddress : 0x0000021f`6a060000 Void +0x038 NumberOfPages : 0xf +0x040 FirstEntry : 0x0000021f`6a060740 _HEAP_ENTRY +0x048 LastValidEntry : 0x0000021f`6a06f000 _HEAP_ENTRY +0x050 NumberOfUnCommittedPages : 5 +0x054 NumberOfUnCommittedRanges : 1 +0x058 SegmentAllocatorBackTraceIndex : 0 [SNIP] +0x070 Flags : 0x1002 +0x074 ForceFlags : 0 +0x078 CompatibilityFlags : 0 +0x07c EncodeFlagMask : 0x100000 +0x080 Encoding : _HEAP_ENTRY [SNIP] [+0x198] FrontEndHeap : 0x0 [Type: void *] [+0x1a0] FrontHeapLockCount : 0x0 [Type: unsigned short] [+0x1a2] FrontEndHeapType : 0x0 [Type: unsigned char] [+0x1a3] RequestedFrontEndHeapType : 0x0 [Type: unsigned char] [SNIP] |
The first entry in the heap is located at 0x21f6a060740, while the last valid entry is at 0x21f6a06f000, which is also the EndingVpn. Before that we will have to look into _HEAP_ENTRY of encoding of the first heap allocation.
dx -r1 (*((ntdll!_HEAP_ENTRY *)0x21f6a060080)) (*((ntdll!_HEAP_ENTRY *)0x21f6a060080)) [Type: _HEAP_ENTRY] [+0x000] UnpackedEntry [Type: _HEAP_UNPACKED_ENTRY] [+0x000] PreviousBlockPrivateData : 0x0 [Type: void *] [+0x008] Size : 0xfc0b [Type: unsigned short] [+0x00a] Flags : 0xdc [Type: unsigned char] [+0x00b] SmallTagIndex : 0xbd [Type: unsigned char] [+0x008] SubSegmentCode : 0xbddcfc0b [Type: unsigned long] [+0x00c] PreviousSize : 0xb3d [Type: unsigned short] [+0x00e] SegmentOffset : 0x0 [Type: unsigned char] [+0x00e] LFHFlags : 0x0 [Type: unsigned char] [+0x00f] UnusedBytes : 0x0 [Type: unsigned char] [+0x008] CompactHeader : 0xb3dbddcfc0b [Type: unsigned __int64] [+0x000] ExtendedEntry [Type: _HEAP_EXTENDED_ENTRY] [SNIP] |
Let’s look into the first _HEAP_ENTRY.
((ntdll!_HEAP_ENTRY *)0x21f6a060740) : 0x21f6a060740 [Type: _HEAP_ENTRY *] [+0x000] UnpackedEntry [Type: _HEAP_UNPACKED_ENTRY] [+0x000] PreviousBlockPrivateData : 0x0 [Type: void *] [+0x008] Size : 0xfc08 [Type: unsigned short] [+0x00a] Flags : 0xdd [Type: unsigned char] [+0x00b] SmallTagIndex : 0xbf [Type: unsigned char] [+0x008] SubSegmentCode : 0xbfddfc08 [Type: unsigned long] [+0x00c] PreviousSize : 0xb49 [Type: unsigned short] [+0x00e] SegmentOffset : 0x0 [Type: unsigned char] [+0x00e] LFHFlags : 0x0 [Type: unsigned char] [+0x00f] UnusedBytes : 0x10 [Type: unsigned char] [+0x008] CompactHeader : 0x10000b49bfddfc08 [Type: unsigned __int64] [+0x000] ExtendedEntry [Type: _HEAP_EXTENDED_ENTRY] [+0x000] Reserved : 0x0 [Type: void *] [+0x008] FunctionIndex : 0xfc08 [Type: unsigned short] [+0x00a] ContextValue : 0xbfdd [Type: unsigned short] [+0x008] InterceptorValue : 0xbfddfc08 [Type: unsigned long] [SNIP] |
Here, we use the CompactHeader from the encoding to calculate the heap allocation size. To do this, we perform an “Exclusive OR” with the second QWORD of the heap entry, which is 10000b49`bfddfc08. The result is 0x0003 as the last word.
When multiplied by 0x10, this value gives us 0x30, which is the size of the entire heap allocation. Adding this size to the starting address gives us: 0x21f6a060770.
0: kd> dq 0x21f6a060770 L2 0000021f`6a060770 00000000`00000000 00000b3e`bfdcfc09 |
Now, using the same process, we can get 0x20, which will give us the next heap allocation, 0x21f6a060790.
Now we can list all heap memory allocations of our process with !heap -s -v -a.
The -s flag provides a summary of all the heaps, including details like total size, number of segments, number of blocks, and other statistics. This flag is useful for getting an overall picture of the heap usage in the process.
The -v flag is used for more detailed output to the command, such as specific heap flags, debugging information, and metadata about each heap. This level of detail can help in understanding the state and properties of each heap in-depth.
The -a flag is used to display all memory blocks allocated in the heap, including their sizes, addresses, and other properties. This is useful for finding patterns, such as unusual memory blocks or potential injected code.
Now we can look into the heap we analyzed.
0000021f6a060740 0000021f6a060750 0000021f6a060000 0000021f6a060000 30 740 10 busy 0000021f6a060770 0000021f6a060780 0000021f6a060000 0000021f6a060000 20 30 0 free 0000021f6a060790 0000021f6a0607a0 0000021f6a060000 0000021f6a060000 20 20 10 busy |
This will be same as the size and allocations we calculated.
We also have to parse all the subsegments. The offset of LFH Heap will be in the heap’s FrontEndHeap, which is 0x21f69dc0000.
dt _LFH_HEAP 0x21f69dc0000 ntdll!_LFH_HEAP +0x000 Lock : _RTL_SRWLOCK +0x008 SubSegmentZones : _LIST_ENTRY [ 0x0000021f`6a064070 - 0x0000021f`6a064070 ] +0x018 Heap : 0x0000021f`6a060000 Void +0x020 NextSegmentInfoArrayAddress : 0x0000021f`69dc0f30 Void +0x028 FirstUncommittedAddress : 0x0000021f`69dc1000 Void +0x030 ReservedAddressLimit : 0x0000021f`69dc7000 Void +0x038 SegmentCreate : 9 +0x03c SegmentDelete : 0 +0x040 MinimumCacheDepth : 0 +0x044 CacheShiftThreshold : 0 +0x048 SizeInCache : 0 +0x050 RunInfo : _HEAP_BUCKET_RUN_INFO +0x060 UserBlockCache : [12] _USER_MEMORY_CACHE_ENTRY +0x2a0 MemoryPolicies : _HEAP_LFH_MEM_POLICIES +0x2a4 Buckets : [129] _HEAP_BUCKET +0x4a8 SegmentInfoArrays : [129] (null) +0x8b0 AffinitizedInfoArrays : [129] (null) +0xcb8 SegmentAllocator : (null) +0xcc0 LocalData : [1] _HEAP_LOCAL_DATA |
From here, we can go to the subsegment zone and look at the subsegment zone header.
dq 0x0000021f6a064070 0000021f`6a064070 0000021f`69dc0008 0000021f`69dc0008 0000021f`6a064080 00000000`00000009 00000000`00000000 |
Here, the fourth QWORD is 0x9, which tells us that there are a total of nine subsegments in this particular zone. Now, let’s look into the first subsegment that starts after the zone header, which is 0x20 bytes.
dt _HEAP_SUBSEGMENT 0x0000021f6a064090 ntdll!_HEAP_SUBSEGMENT +0x000 LocalInfo : 0x0000021f`69dc0cf0 _HEAP_LOCAL_SEGMENT_INFO +0x008 UserBlocks : 0x0000021f`6a063c60 _HEAP_USERDATA_HEADER +0x010 DelayFreeList : _SLIST_HEADER +0x020 AggregateExchg : _INTERLOCK_SEQ +0x024 BlockSize : 3 +0x026 Flags : 0 +0x028 BlockCount : 0x13 +0x02a SizeIndex : 0x2 '' +0x02b AffinityIndex : 0 '' +0x024 Alignment : [2] 3 +0x02c Lock : 1 +0x030 SFreeListEntry : _SINGLE_LIST_ENTRY |
Here, the UserBlocks start at 0x0000021f6a063c60, so let’s look at the header of the UserBlock.
dt _HEAP_USERDATA_HEADER 0x0000021f6a063c60 ntdll!_HEAP_USERDATA_HEADER +0x000 SFreeListEntry : _SINGLE_LIST_ENTRY +0x000 SubSegment : 0x0000021f`6a064090 _HEAP_SUBSEGMENT +0x008 Reserved : 0x0000021f`6a060780 Void +0x010 SizeIndexAndPadding : 0xa +0x010 SizeIndex : 0xa '' +0x011 GuardPagePresent : 0 '' +0x012 PaddingBytes : 0 +0x014 Signature : 0xf0e0d0c0 +0x018 EncodedOffsets : _HEAP_USERDATA_OFFSETS +0x020 BusyBitmap : _RTL_BITMAP_EX +0x030 BitmapData : [1] 0xffffffff`ffffffff |
The header is 0x40 bytes, so the first allocation will start at 0x0000021f6a063ca0. The total allocations will be 0x13 in count and 0x30 bytes in size, as mentioned in subsegment header.
We have all the allocations, but the heaps that need to be analyzed are the segment0x0 heaps, not the LFH and subsegments which will have user_flag.
Now, let’s look into a heap entry, its flags, and its significance.
HEAP_ENTRY_BUSY: Indicates if a memory block is currently in use (1) or free (0) - this is the fundamental allocation status flag.
HEAP_ENTRY_EXTRA_PRESENT: Signals that the heap block contains additional metadata beyond the standard header structure.
HEAP_ENTRY_FILL_PATTERN: Shows that the block is filled with a specific pattern, typically used for detecting memory corruption or buffer overflows.
HEAP_ENTRY_INTERNAL: Marks blocks that are reserved for the heap manager's internal use rather than application allocation.
HEAP_ENTRY_VIRTUAL_ALLOC: Identifies memory blocks that were allocated directly through VirtualAlloc instead of normal heap allocation.
HEAP_ENTRY_USER_FLAGS: This flag is used to indicate whether a memory block is marked for user-defined purposes, which can be helpful for debugging or tracking allocations in a more customized way. Reserved space for application-specific flags, allowing custom metadata to be associated with heap blocks.
HEAP_ENTRY_SETTABLE_FLAG1: First user-definable flag that can be used for custom memory management implementations.
HEAP_ENTRY_SETTABLE_FLAG2: Second user-definable flag available for custom memory tracking or debugging purposes.
Positions of bit 0 (0x1) - HEAP_ENTRY_BUSY (1=allocated, 0=free) 1 (0x2) - HEAP_ENTRY_EXTRA_PRESENT 2 (0x4) - HEAP_ENTRY_FILL_PATTERN 3 (0x8) - HEAP_ENTRY_INTERNAL 4 (0x10) - HEAP_ENTRY_VIRTUAL_ALLOC 5 (0x20) - HEAP_ENTRY_USER_FLAGS 6 (0x40) - HEAP_ENTRY_SETTABLE_FLAG1 7 (0x80) - HEAP_ENTRY_SETTABLE_FLAG2 |
We have many heaps for this process, but we need only those that have extra user_flag.
Now, let’s look at the heap contents. For this we can use db 0000021ef85b2ad0 L80.
Now, we have successfully extracted the desired information from memory! This method can be applied to any file opened in notepad.exe, making it straightforward to recover any file opened in notepad.exe from a memory image.
Now, let’s make a custom notepad-like application. This will provide better understanding of heap allocations.
#include <windows.h> const wchar_t g_szClassName[] = L"myWindowClass"; HWND hEdit; LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_CREATE: hEdit = CreateWindowEx( 0, L"EDIT", L"", WS_CHILD | WS_VISIBLE | WS_VSCROLL | WS_HSCROLL | ES_LEFT | ES_MULTILINE | ES_AUTOVSCROLL | ES_AUTOHSCROLL, 0, 0, 500, 500, hwnd, (HMENU)1, GetModuleHandle(NULL), NULL); break; case WM_SIZE: MoveWindow(hEdit, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE); break; case WM_CLOSE: DestroyWindow(hwnd); break; case WM_DESTROY: PostQuitMessage(0); break; default: return DefWindowProc(hwnd, msg, wParam, lParam); } return 0; } int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { WNDCLASSEX wc; HWND hwnd; MSG Msg; wc.cbSize = sizeof(WNDCLASSEX); wc.style = 0; wc.lpfnWndProc = WndProc; wc.cbClsExtra = 0; wc.cbWndExtra = 0; wc.hInstance = hInstance; wc.hIcon = LoadIcon(NULL, IDI_APPLICATION); wc.hCursor = LoadCursor(NULL, IDC_ARROW); wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); wc.lpszMenuName = NULL; wc.lpszClassName = g_szClassName; wc.hIconSm = LoadIcon(NULL, IDI_APPLICATION); if (!RegisterClassEx(&wc)) { MessageBox(NULL, L"Window Registration Failed!", L"Error!", MB_ICONEXCLAMATION | MB_OK); return 0; } hwnd = CreateWindowEx( WS_EX_CLIENTEDGE, g_szClassName, L"Notepad Clone", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 500, 500, NULL, NULL, hInstance, NULL); if (hwnd == NULL) { MessageBox(NULL, L"Window Creation Failed!", L"Error!", MB_ICONEXCLAMATION | MB_OK); return 0; } ShowWindow(hwnd, nCmdShow); UpdateWindow(hwnd); while (GetMessage(&Msg, NULL, 0, 0) > 0) { TranslateMessage(&Msg); DispatchMessage(&Msg); } return Msg.wParam; } |
Debugging the process and analyzing, we are able to get the heap allocation, which is:
0000026cb860edc0 0000026cb860edd0 0000026cb85f0000 0000026cb85f0000 60 80 20 busy extra user_flag |
Let’s do that on real malware with the malware sample of brbbot.exe.
1 Find Process _EPROCESS (core).
2 Set Debugger Process context.
3. Now, let’s look into all the heap allocations.
As this is an actual malware process without any user defined heap allocations, we need to check all heaps associated with the process, not just the user_flag or extra heaps. The first heap address is 0000000001300000, and last heap address is 00000000016f5fd0. It’s better to view all the contents of the particular heap allocation. To do this, we can use the WinDbg command: db 0000000001300000 L 80000.
Further analysis on the malware’s heap allocations shows that it maintains a network connection with the threat actor’s server. During its execution in the system’s background, the malware sends encrypted data to the attacker’s server.
Takeaways and Applications
In this blog post, we explored heap memory management within Windows, focusing on the structure and functionality of _HEAP_ENTRY and the significance of its various flags in determining the state of memory allocations. We learned:
- What is heap memory allocation in Windows and how it is managed.
- The structure of process memory in Windows.
- How to manually analyze heap allocation and subsegment allocations.
- The process of decoding _HEAP_ENTRY and understanding its associated flags and their uses.
- The role of heap protections and how to analyze user-defined and program-defined heap allocations.
The ability to manually parse heap allocations allows for deeper insights into application behavior. This understanding enables more informed decisions regarding reverse engineering and digital forensics and incident response (DFIR) analysis of malwares and other Windows applications.
As we continue to navigate the complexities of memory management, the insights gained from these structures will remain invaluable for enhancing Windows core analysis skills.
Explore more DFIR resources and stay ahead in the ever-evolving field. Join the SANS community today to stay connected to the latest insights and innovations.