| // Copyright 2020 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // This is an experimental tool which will inject a thread into a Chrome |
| // process (tested on the browser process) and run code to call |
| // HeapSetInformation with HEAP_OPTIMIZE_RESOURCES_CURRENT_VERSION. This |
| // tells Windows to trim unnecessary memory from the heaps in that process. |
| // |
| // This tool uses sketchy techniques such as copying memory from one |
| // executable to another (only works if the code is relocatable and has no |
| // external references), VirtualAllocEx, and CreateRemoteThread. This is not |
| // for production use. |
| // |
| // The bitness of this tool (32/64) must match that of the target process. |
| // This tool has only been tested on 64-bit processes. This tool only works |
| // when compiled with optimizations. |
| // |
| // Some error handling and resource cleanup is omitted in order to keep things |
| // simple. |
| |
| #include <Windows.h> |
| |
| // Psapi.h must come after Windows.h. |
| #include <Psapi.h> |
| |
| #include <inttypes.h> |
| #include <stdio.h> |
| |
| #include <vector> |
| |
| #ifdef _DEBUG |
| #error This code only works in optimized (release) builds. |
| // Non-optimized code may include references to global variables. The |
| // "#pragma clang optimize on/off" directives do not work, by design, in debug |
| // builds. They can only lower the optimization level, not raise it. |
| #endif |
| |
| #define ADDRESS_COOKIE reinterpret_cast<void*>(0x123456789ABCDEF0) |
| |
| // Function suitable for copying into another process and invoking with |
| // CreateRemoteThread. The function address is a placeholder. |
| DWORD WINAPI ShrinkHeapThread(LPVOID) { |
| auto pHeapSetInformation = |
| reinterpret_cast<decltype(&::HeapSetInformation)>(ADDRESS_COOKIE); |
| HEAP_OPTIMIZE_RESOURCES_INFORMATION info = { |
| HEAP_OPTIMIZE_RESOURCES_CURRENT_VERSION, 0x0}; |
| pHeapSetInformation(nullptr, HeapOptimizeResources, &info, sizeof(info)); |
| return 0; |
| } |
| |
| int main(int argc, char* argv[]) { |
| const bool verbose = false; |
| |
| // Verify that we have the correct signature for ShrinkHeapThread. |
| static_assert( |
| std::is_same<decltype(ShrinkHeapThread)*, PTHREAD_START_ROUTINE>::value, |
| "Callback function is wrong type."); |
| |
| // Copy the thread function's memory to a vector. |
| std::vector<unsigned char> raw_bytes; |
| auto* src = reinterpret_cast<uint8_t*>(&ShrinkHeapThread); |
| // Assume that the only 0xc3 byte we will encounter will be the ret |
| // instruction. |
| uint8_t ret = 0xc3; |
| while (*src != ret) { |
| raw_bytes.push_back(*src++); |
| } |
| raw_bytes.push_back(ret); |
| if (src[1] != 0xcc) { |
| printf("Didn't find int 3 after ret. Exiting.\n"); |
| return 1; |
| } |
| // This can trigger if incremental linking is enabled since then the function |
| // pointer will be to a JMP stub. |
| if (raw_bytes.size() > 1000) { |
| printf("Code size is suspiciously large - %zu bytes. Exiting.\n", |
| raw_bytes.size()); |
| return 1; |
| } |
| |
| // Update the function pointer address in the copy to match the current |
| // address of HeapSetInformation. This assumes that the address will be the |
| // same in all processes, which should be the case. |
| for (auto* scan = &raw_bytes[0]; /**/; ++scan) { |
| auto** scan_64 = reinterpret_cast<void**>(scan); |
| if (*scan_64 == ADDRESS_COOKIE) { |
| auto* pHeapSetInformation = reinterpret_cast<void*>(GetProcAddress( |
| GetModuleHandleA("kernel32.dll"), "HeapSetInformation")); |
| *scan_64 = pHeapSetInformation; |
| if (verbose) |
| printf("Found and updated HeapSetInformation.\n"); |
| break; |
| } |
| } |
| |
| if (argc < 2) { |
| printf("Usage: %s PID.\n", argv[0]); |
| printf( |
| "Injects code into the target process to call HeapSetInformation with " |
| "HEAP_OPTIMIZE_RESOURCES_CURRENT_VERSION.\n"); |
| printf( |
| "May need to be run from an administrator command prompt for some " |
| "processes.\n"); |
| return 1; |
| } |
| |
| // Get the PIDs from the command line. |
| for (int i = 1; i < argc; ++i) { |
| int PID; |
| if (sscanf(argv[i], "%d", &PID) != 1) { |
| printf("Error getting PID.\n"); |
| return 1; |
| } |
| |
| // Open the process. We'll leak the handle afterwards, but that's okay |
| // because this is a short-lived tool. |
| HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ | |
| PROCESS_VM_WRITE | PROCESS_VM_OPERATION | |
| PROCESS_CREATE_THREAD, |
| false, PID); |
| if (!hProcess) { |
| printf("Error from OpenProcess is %lx.\n", GetLastError()); |
| return 1; |
| } |
| |
| #ifdef _M_X64 |
| BOOL wow_64_process = FALSE; |
| if (!IsWow64Process(hProcess, &wow_64_process) || wow_64_process) { |
| printf("Specified process is 32-bit. Code injection will not work.\n"); |
| return 1; |
| } |
| #else |
| // Update this with remote-process bitness tests if x86 works. |
| #error This code is only tested on x64 and may cause failures on x86. |
| #endif |
| |
| PROCESS_MEMORY_COUNTERS_EX memory_before = {sizeof(memory_before)}; |
| GetProcessMemoryInfo( |
| hProcess, reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&memory_before), |
| sizeof(memory_before)); |
| |
| // Allocate memory in the other process. |
| void* p = VirtualAllocEx(hProcess, nullptr, raw_bytes.size(), |
| MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); |
| |
| if (verbose) |
| printf("Writing %zd bytes to process %d at address 0x%p.\n", |
| raw_bytes.size(), PID, p); |
| // Write to the remotely allocated memory. |
| SIZE_T bytes_written = 0; |
| if (!WriteProcessMemory(hProcess, p, &raw_bytes[0], raw_bytes.size(), |
| &bytes_written)) { |
| printf("Error is %lx.\n", GetLastError()); |
| return 1; |
| } |
| |
| if (verbose) |
| printf("Wrote %zd bytes.\n", bytes_written); |
| HANDLE hRemoteThread = CreateRemoteThread( |
| hProcess, nullptr, 0, reinterpret_cast<LPTHREAD_START_ROUTINE>(p), |
| nullptr, 0, nullptr); |
| if (!hRemoteThread) { |
| printf("Failed to inject thread in process %d. Error code is %lx.\n", PID, |
| GetLastError()); |
| return 1; |
| } |
| |
| if (verbose) |
| printf("Successfully injected thread into process %d.\n", PID); |
| WaitForSingleObject(hRemoteThread, INFINITE); |
| // Clean up the allocated memory after the thread exits. |
| VirtualFreeEx(hProcess, p, 0, MEM_RELEASE); |
| |
| PROCESS_MEMORY_COUNTERS_EX memory_after = {sizeof(memory_after)}; |
| GetProcessMemoryInfo( |
| hProcess, reinterpret_cast<PROCESS_MEMORY_COUNTERS*>(&memory_after), |
| sizeof(memory_after)); |
| |
| double MiB = 1024.0 * 1024.0; |
| printf( |
| " Commit for process %6d went from %8.3f MiB to %8.3f MiB (%7.3f MiB " |
| "savings).\n", |
| PID, memory_before.PrivateUsage / MiB, memory_after.PrivateUsage / MiB, |
| (memory_before.PrivateUsage - memory_after.PrivateUsage) / MiB); |
| } |
| return 0; |
| } |