📜 ⬆️ ⬇️

Continuing the story about environment variables, or substitute PEB

Tonight, for the first time I decided to register on Habré in order to unsubscribe ilya314 in this topic , but I was extremely surprised that I am unable to comment anything, because I am not an honorable habro user . Horrible.

Therefore, I decided to jot down a code in order to express my thoughts on the problem of data duplication in Sishny runtime from the PEB-a process.

Actually, there are several ways to solve the problem encountered by the author, the simplest of which is to abandon the getenv runtime library functions and use the kernel32.GetEnvironmentVariableW or kernel32.GetEnvironmentStringsW interfaces. But developing the topic further, I wanted to find environment variables and try to simply replace them for a specific process.

Let's look at an undocumented PEB ad with one eye (M $, of course, gives us a scrap of properties in five, but with proper use of Google or the debugger, everything falls into place):
')
typedef struct _PEB { BOOLEAN InheritedAddressSpace; BOOLEAN ReadImageFileExecOptions; BOOLEAN BeingDebugged; BOOLEAN Spare; HANDLE Mutant; PVOID ImageBaseAddress; PPEB_LDR_DATA LoaderData; PRTL_USER_PROCESS_PARAMETERS ProcessParameters; PVOID SubSystemData; PVOID ProcessHeap; PVOID FastPebLock; PPEBLOCKROUTINE FastPebLockRoutine; PPEBLOCKROUTINE FastPebUnlockRoutine; ULONG EnvironmentUpdateCount; PPVOID KernelCallbackTable; PVOID EventLogSection; PVOID EventLog; PPEB_FREE_BLOCK FreeList; ULONG TlsExpansionCounter; PVOID TlsBitmap; ULONG TlsBitmapBits[0x2]; PVOID ReadOnlySharedMemoryBase; PVOID ReadOnlySharedMemoryHeap; PPVOID ReadOnlyStaticServerData; PVOID AnsiCodePageData; PVOID OemCodePageData; PVOID UnicodeCaseTableData; ULONG NumberOfProcessors; ULONG NtGlobalFlag; BYTE Spare2[0x4]; LARGE_INTEGER CriticalSectionTimeout; ULONG HeapSegmentReserve; ULONG HeapSegmentCommit; ULONG HeapDeCommitTotalFreeThreshold; ULONG HeapDeCommitFreeBlockThreshold; ULONG NumberOfHeaps; ULONG MaximumNumberOfHeaps; PPVOID *ProcessHeaps; PVOID GdiSharedHandleTable; PVOID ProcessStarterHelper; PVOID GdiDCAttributeList; PVOID LoaderLock; ULONG OSMajorVersion; ULONG OSMinorVersion; ULONG OSBuildNumber; ULONG OSPlatformId; ULONG ImageSubSystem; ULONG ImageSubSystemMajorVersion; ULONG ImageSubSystemMinorVersion; ULONG GdiHandleBuffer[0x22]; ULONG PostProcessInitRoutine; ULONG TlsExpansionBitmap; BYTE TlsExpansionBitmapBits[0x80]; ULONG SessionId; } PEB, *PPEB; 

We are interested in the ProcessParameters property, which contains CommandLine; Environment; and other goodies that are duplicated in ring3 from ring0 and cached by Sishny runtime on the fly right from here. Probably, the runtime uses standard kernel32-> ntdll interfaces and you could just hook them up, but I decided to pull the PEB through the segment register and replace the data with arrogant memory. Just replace and see how Wind will react to this. Recently, I am going to AMD64, so we will compile it for this platform.

Thanks to the wonderful M $ solution, which sawed out the possibility of inline asm inserts for 64-bit platforms - we are waiting for an interesting quest to assign assembly functions to the project and linking with a separate object manager (you can write a full article about this, so I will not dwell on this, I will say just that I was tired of the order, until everything worked as it should).

 ; ; Utils.asm ; INCLUDE Utils.inc .code GetCurrentUserProcessParameters PROC mov rax, gs:[60h] mov rax, [rax + 20h]; ret; GetCurrentUserProcessParameters ENDP END 


By the way, it is worth noting that, unlike the 32-bit version of the Windows OS family, in 64-bit PEB is mapped by offset from the gs register, in the 32-bit version you can get it like this:

 __declspec(naked) PVOID GetCurrentUserProcessParameters() { __asm { mov eax, fs:[30h]; mov eax, [eax + 10h]; ret; } } 


Looking at PEB, it is easy to calculate the offset of ProcessParameters, for 32 bits. With an alignment of 4 bytes, it fits into 16 bytes. For 64 bits, taking into account 8-byte pointers and the first BOOLEANs aligned by 4 bytes, 28 + 4 bytes will be output. You can verify this by looking at the process memory using the debugger.

image

image

Now let's get together with the forces and substitute ProcessParameters in the PEB of our process, and more specifically, its environment variables. But first, let's look at the format for storing strings in the Environment box. M $ insists that the lines are VAR = VALUE and are separated by a zero byte, a sign of the end of the block is two zero bytes in a row. Let us see this with our own eyes and select the pitfalls:

 #include <windows.h> #include <stdio.h> #include <conio.h> #include "Utils.h" typedef struct _LSA_UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; } LSA_UNICODE_STRING, *PLSA_UNICODE_STRING, UNICODE_STRING, *PUNICODE_STRING; typedef struct _RTL_USER_PROCESS_PARAMETERS { ULONG MaximumLength; ULONG Length; ULONG Flags; ULONG DebugFlags; PVOID ConsoleHandle; ULONG ConsoleFlags; HANDLE StdInputHandle; HANDLE StdOutputHandle; HANDLE StdErrorHandle; UNICODE_STRING CurrentDirectoryPath; HANDLE CurrentDirectoryHandle; UNICODE_STRING DllPath; UNICODE_STRING ImagePathName; UNICODE_STRING CommandLine; PVOID Environment; ULONG StartingPositionLeft; ULONG StartingPositionTop; ULONG Width; ULONG Height; ULONG CharWidth; ULONG CharHeight; ULONG ConsoleTextAttributes; ULONG WindowFlags; ULONG ShowWindowFlags; UNICODE_STRING WindowTitle; UNICODE_STRING DesktopName; UNICODE_STRING ShellInfo; UNICODE_STRING RuntimeData; PVOID DLCurrentDirectory; } RTL_USER_PROCESS_PARAMETERS, *PRTL_USER_PROCESS_PARAMETERS; PVOID ReplacePEBEnvironmentTableAndAddValue(LPCWSTR Variable, LPCWSTR Value) { PRTL_USER_PROCESS_PARAMETERS ProcessParams; MEMORY_BASIC_INFORMATION MemoryInformation; PBYTE NewEnvironment; PWCHAR Token; size_t EnvironmentSize; if (!Variable || !Value || !*Variable || !*Value) return NULL; /*    RTL_USER_PROCESS_PARAMETERS  PEB   */ /*   Environment   */ /*     ,   ,    */ /*  L'\0' L'\0',   MSDN */ /*  4   */ ProcessParams = GetCurrentUserProcessParameters(); Token = (PWCHAR)ProcessParams->Environment; while (!(!*Token && !*(Token + 1))) ++Token; EnvironmentSize = (ULONG_PTR)Token - (ULONG_PTR)ProcessParams->Environment; /*      Environment   ,    */ /*  ,   */ MemoryInformation.AllocationProtect = PAGE_EXECUTE_READWRITE; VirtualQuery(ProcessParams->Environment, &MemoryInformation, sizeof(MEMORY_BASIC_INFORMATION)); /*       Environment +  */ /*          */ /*    */ /*  ,      Environment, */ /*       */ NewEnvironment = (PBYTE)VirtualAlloc(0, EnvironmentSize + 0x1000, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); if (NewEnvironment) { /*           */ /*  Var=Value (+ 2 widechar  L'='   ) */ DWORD OldProtect = PAGE_EXECUTE_READWRITE, OldProtect2 = PAGE_EXECUTE_READWRITE; size_t Size = (wcslen(Variable) + wcslen(Value)) * sizeof(WCHAR) + 2 * sizeof(WCHAR); PWCHAR EnvironmentString = malloc(Size); if (EnvironmentString) { /*         */ /*      */ PVOID OldEnvironment = ProcessParams->Environment; UINT EndOfEnvironment = 0; _snwprintf_s(EnvironmentString, Size / sizeof(WCHAR), _TRUNCATE, L"%ws=%ws", Variable, Value); memcpy(NewEnvironment, ProcessParams->Environment, EnvironmentSize); /*     -      MSDN */ *((PWCHAR)(NewEnvironment + EnvironmentSize)) = L'\0'; /*        */ /*    4   */ memcpy(NewEnvironment + EnvironmentSize + 2, EnvironmentString, Size - sizeof(WCHAR)); memcpy(NewEnvironment + EnvironmentSize + 2 + Size - sizeof(WCHAR), &EndOfEnvironment, 4); /*         */ VirtualProtect(NewEnvironment, EnvironmentSize + 0x1000, MemoryInformation.AllocationProtect, &OldProtect); /*     RTL_USER_PROCESS_PARAMETERS  PEB */ /*       */ /*  Environment block       */ VirtualProtect(ProcessParams, sizeof(RTL_USER_PROCESS_PARAMETERS), PAGE_EXECUTE_READWRITE, &OldProtect); ProcessParams->Environment = NewEnvironment; VirtualProtect(ProcessParams, sizeof(RTL_USER_PROCESS_PARAMETERS), OldProtect, &OldProtect2); /*           */ /*     Environment block */ free(EnvironmentString); return OldEnvironment; } VirtualFree(NewEnvironment, 0, MEM_RELEASE); } return NULL; } void RestorePEBEnvironmentTable(PVOID OriginalEnvironment) { PRTL_USER_PROCESS_PARAMETERS ProcessParams; DWORD OldProtect = PAGE_EXECUTE_READWRITE, OldProtect2; PVOID OldEnvironment; if (!OriginalEnvironment) return; /*  PEB          */ ProcessParams = GetCurrentUserProcessParameters(); VirtualProtect(ProcessParams, sizeof(RTL_USER_PROCESS_PARAMETERS), PAGE_EXECUTE_READWRITE, &OldProtect); OldEnvironment = ProcessParams->Environment; ProcessParams->Environment = OriginalEnvironment; VirtualProtect(ProcessParams, sizeof(RTL_USER_PROCESS_PARAMETERS), OldProtect, &OldProtect2); /*       ,   */ VirtualFree(OldEnvironment, 0, MEM_RELEASE); } void main(int argc, char *argv[]) { PVOID OriginalEnvironment; UNREFERENCED_PARAMETER(argc); UNREFERENCED_PARAMETER(argv); OriginalEnvironment = ReplacePEBEnvironmentTableAndAddValue(L"NewVar", L"NewValue"); if (OriginalEnvironment) { WCHAR Buff[1024] = {0}; if (GetEnvironmentVariableW(L"NewVar", Buff, sizeof(Buff))) wprintf_s(L"GetEnvironmentVariableW(): NewVar == %ws\n", Buff); printf_s("Restoring PEB Environment Table...\n"); RestorePEBEnvironmentTable(OriginalEnvironment); if (!GetEnvironmentVariableW(L"NewVar", Buff, sizeof(Buff))) printf_s("GetEnvironmentVariableW(): NewVar not found\n"); } _getch(); } 

Then the conclusion follows:

image

Return to the "native" table does not rebuild the cache. Thus, the author's problem with the dll is solved by using the appropriate kernel32 interfaces to work with the block of environment variables.

The most interesting thing is that this method will work even when data is being replaced in a third-party process, which can be shown later.

UPDATE: As CleverMouse correctly noted, working with environment variables through the C library does not carry a semantic load in a specific example, as in the case of the main function, the _environ cache is filled, and not _wenviron, so I suggest considering it as debug prints.

Forever yours
rwx64

Source: https://habr.com/ru/post/137479/


All Articles