419 lines
9.7 KiB
C++
419 lines
9.7 KiB
C++
#include "Common.h"
|
|
#include "StageClient.h"
|
|
#include "HostSpawn.h"
|
|
#include "Paths.h"
|
|
#include "Logger.h"
|
|
#include "resource.h"
|
|
|
|
#include <sstream>
|
|
#include <vector>
|
|
|
|
static std::string ToUtf8(const std::wstring& w)
|
|
{
|
|
if (w.empty())
|
|
return std::string();
|
|
|
|
int need = WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), nullptr, 0, nullptr, nullptr);
|
|
|
|
if (need <= 0)
|
|
return std::string();
|
|
|
|
std::string out(need, '\0');
|
|
WideCharToMultiByte(CP_UTF8, 0, w.c_str(), (int)w.size(), out.data(), need, nullptr, nullptr);
|
|
return out;
|
|
}
|
|
|
|
static std::wstring FromUtf8(const std::string& s)
|
|
{
|
|
if (s.empty())
|
|
return std::wstring();
|
|
|
|
int need = MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), nullptr, 0);
|
|
|
|
if (need <= 0)
|
|
return std::wstring();
|
|
|
|
std::wstring out(need, L'\0');
|
|
MultiByteToWideChar(CP_UTF8, 0, s.c_str(), (int)s.size(), out.data(), need);
|
|
return out;
|
|
}
|
|
|
|
static const wchar_t* kPipeName = L"\\\\.\\pipe\\ArDrvStage";
|
|
static const wchar_t* kDllName = L"ardrvstage.dll";
|
|
|
|
bool StageSend(const std::wstring& request, std::wstring& response)
|
|
{
|
|
response.clear();
|
|
|
|
HANDLE pipe = CreateFileW(
|
|
kPipeName,
|
|
GENERIC_READ | GENERIC_WRITE,
|
|
0,
|
|
nullptr,
|
|
OPEN_EXISTING,
|
|
0,
|
|
nullptr);
|
|
|
|
if (pipe == INVALID_HANDLE_VALUE)
|
|
return false;
|
|
|
|
std::string bytes = ToUtf8(request) + "\n";
|
|
|
|
DWORD written = 0;
|
|
|
|
if (!WriteFile(pipe, bytes.data(), (DWORD)bytes.size(), &written, nullptr))
|
|
{
|
|
CloseHandle(pipe);
|
|
return false;
|
|
}
|
|
|
|
std::string reply;
|
|
char buf[4096]{};
|
|
DWORD read = 0;
|
|
|
|
while (ReadFile(pipe, buf, sizeof(buf), &read, nullptr) && read > 0)
|
|
{
|
|
reply.append(buf, read);
|
|
|
|
if (reply.find("<<") != std::string::npos)
|
|
break;
|
|
}
|
|
|
|
CloseHandle(pipe);
|
|
|
|
size_t end = reply.find("<<");
|
|
|
|
if (end != std::string::npos)
|
|
reply = reply.substr(0, end);
|
|
|
|
response = FromUtf8(reply);
|
|
|
|
while (!response.empty() && (response.back() == L'\n' || response.back() == L'\r'))
|
|
response.pop_back();
|
|
|
|
return !response.empty();
|
|
}
|
|
|
|
bool StageAlive()
|
|
{
|
|
std::wstring response;
|
|
|
|
if (!StageSend(L"PING", response))
|
|
return false;
|
|
|
|
return response.find(L"PONG") != std::wstring::npos;
|
|
}
|
|
|
|
// Writes the payload that is packed into this executable to the stage dir.
|
|
|
|
// Writes the payload that is packed into this executable to the stage dir when
|
|
// the on-disk copy differs (missing, old build, or truncated). Comparing the
|
|
// bytes keeps upgrades working: a new loader always replaces an older DLL.
|
|
static bool EmbeddStageCurrent(const std::wstring& target)
|
|
{
|
|
HRSRC res = FindResourceW(nullptr, MAKEINTRESOURCEW(IDR_STAGE_DLL), RT_RCDATA);
|
|
|
|
if (!res)
|
|
return false;
|
|
|
|
DWORD size = SizeofResource(nullptr, res);
|
|
|
|
if (size == 0)
|
|
return false;
|
|
|
|
HGLOBAL loaded = LoadResource(nullptr, res);
|
|
|
|
if (!loaded)
|
|
return false;
|
|
|
|
const BYTE* data = static_cast<const BYTE*>(LockResource(loaded));
|
|
|
|
if (!data)
|
|
return false;
|
|
|
|
// Compare against what is already on disk.
|
|
HANDLE existing = CreateFileW(
|
|
target.c_str(),
|
|
GENERIC_READ,
|
|
FILE_SHARE_READ,
|
|
nullptr,
|
|
OPEN_EXISTING,
|
|
0,
|
|
nullptr);
|
|
|
|
if (existing != INVALID_HANDLE_VALUE)
|
|
{
|
|
LARGE_INTEGER fileSize{};
|
|
bool same = false;
|
|
|
|
if (GetFileSizeEx(existing, &fileSize) && fileSize.QuadPart == (LONGLONG)size)
|
|
{
|
|
std::vector<BYTE> onDisk(size);
|
|
DWORD read = 0;
|
|
|
|
same = ReadFile(existing, onDisk.data(), size, &read, nullptr) &&
|
|
read == size &&
|
|
memcmp(onDisk.data(), data, size) == 0;
|
|
}
|
|
|
|
CloseHandle(existing);
|
|
|
|
if (same)
|
|
return true;
|
|
}
|
|
|
|
HANDLE h = CreateFileW(
|
|
target.c_str(),
|
|
GENERIC_WRITE,
|
|
0,
|
|
nullptr,
|
|
CREATE_ALWAYS,
|
|
FILE_ATTRIBUTE_NORMAL,
|
|
nullptr);
|
|
|
|
if (h == INVALID_HANDLE_VALUE)
|
|
return false;
|
|
|
|
DWORD written = 0;
|
|
BOOL ok = WriteFile(h, data, size, &written, nullptr);
|
|
CloseHandle(h);
|
|
|
|
if (!ok || written != size)
|
|
{
|
|
DeleteFileW(target.c_str());
|
|
return false;
|
|
}
|
|
|
|
Log(L"[stage] payload refreshed from resources (%lu bytes) -> %s", size, target.c_str());
|
|
return true;
|
|
}
|
|
|
|
std::wstring StageDllPath(const std::wstring& explicitPath)
|
|
{
|
|
if (!explicitPath.empty())
|
|
{
|
|
wchar_t abs[4096]{};
|
|
|
|
if (GetFullPathNameW(explicitPath.c_str(), _countof(abs), abs, nullptr))
|
|
return abs;
|
|
|
|
return explicitPath;
|
|
}
|
|
|
|
std::wstring staged = ArdrvStageDir() + L"\\" + kDllName;
|
|
|
|
// The packed payload is authoritative: if it differs from the file on disk,
|
|
// the file is replaced. An old stage DLL otherwise survives every upgrade.
|
|
if (EmbeddStageCurrent(staged))
|
|
return staged;
|
|
|
|
if (GetFileAttributesW(staged.c_str()) != INVALID_FILE_ATTRIBUTES)
|
|
return staged;
|
|
|
|
wchar_t module[MAX_PATH]{};
|
|
|
|
DWORD len = GetModuleFileNameW(nullptr, module, _countof(module));
|
|
|
|
if (len > 0 && len < _countof(module))
|
|
{
|
|
std::wstring dir = module;
|
|
size_t slash = dir.find_last_of(L"\\/");
|
|
|
|
if (slash != std::wstring::npos)
|
|
{
|
|
dir = dir.substr(0, slash);
|
|
std::wstring candidate = dir + L"\\" + kDllName;
|
|
|
|
if (GetFileAttributesW(candidate.c_str()) != INVALID_FILE_ATTRIBUTES)
|
|
return candidate;
|
|
}
|
|
}
|
|
|
|
return std::wstring();
|
|
}
|
|
|
|
// CreateRemoteThread(LoadLibraryW) inside the chosen host.
|
|
static bool InjectDllIntoProcess(DWORD pid, const std::wstring& dllPath)
|
|
{
|
|
HANDLE proc = OpenProcess(
|
|
PROCESS_CREATE_THREAD |
|
|
PROCESS_QUERY_INFORMATION |
|
|
PROCESS_VM_OPERATION |
|
|
PROCESS_VM_WRITE |
|
|
PROCESS_VM_READ,
|
|
FALSE,
|
|
pid);
|
|
|
|
if (!proc)
|
|
{
|
|
Log(L"[-] stage: OpenProcess(%lu) failed: %lu", pid, GetLastError());
|
|
return false;
|
|
}
|
|
|
|
const SIZE_T bytes = (dllPath.size() + 1) * sizeof(wchar_t);
|
|
|
|
LPVOID remote = VirtualAllocEx(proc, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
|
|
|
if (!remote)
|
|
{
|
|
CloseHandle(proc);
|
|
return false;
|
|
}
|
|
|
|
if (!WriteProcessMemory(proc, remote, dllPath.c_str(), bytes, nullptr))
|
|
{
|
|
VirtualFreeEx(proc, remote, 0, MEM_RELEASE);
|
|
CloseHandle(proc);
|
|
return false;
|
|
}
|
|
|
|
HMODULE kernel = GetModuleHandleW(L"kernel32.dll");
|
|
auto loadLibrary = reinterpret_cast<LPTHREAD_START_ROUTINE>(
|
|
GetProcAddress(kernel, "LoadLibraryW"));
|
|
|
|
if (!loadLibrary)
|
|
{
|
|
VirtualFreeEx(proc, remote, 0, MEM_RELEASE);
|
|
CloseHandle(proc);
|
|
return false;
|
|
}
|
|
|
|
HANDLE thread = CreateRemoteThread(proc, nullptr, 0, loadLibrary, remote, 0, nullptr);
|
|
|
|
bool ok = false;
|
|
|
|
if (thread)
|
|
{
|
|
WaitForSingleObject(thread, 10000);
|
|
|
|
DWORD exitCode = 0;
|
|
|
|
if (GetExitCodeThread(thread, &exitCode))
|
|
ok = exitCode != 0;
|
|
|
|
CloseHandle(thread);
|
|
}
|
|
|
|
VirtualFreeEx(proc, remote, 0, MEM_RELEASE);
|
|
CloseHandle(proc);
|
|
|
|
return ok;
|
|
}
|
|
|
|
HostInjectResult StageInject(
|
|
const std::wstring& dllPath,
|
|
const std::wstring& preferredImage,
|
|
bool requireSystem)
|
|
{
|
|
HostInjectResult result;
|
|
|
|
std::wstring dll = StageDllPath(dllPath);
|
|
|
|
if (dll.empty())
|
|
{
|
|
result.error = L"ardrvstage.dll not found next to the loader or in the stage dir";
|
|
return result;
|
|
}
|
|
|
|
std::vector<HostInfo> hosts = EnumerateHosts(requireSystem);
|
|
|
|
HostInfo* chosen = nullptr;
|
|
|
|
for (auto& h : hosts)
|
|
{
|
|
if (!h.injectable)
|
|
continue;
|
|
|
|
if (requireSystem && !h.systemToken)
|
|
continue;
|
|
|
|
if (!preferredImage.empty() &&
|
|
_wcsicmp(h.imageName.c_str(), preferredImage.c_str()) != 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
chosen = &h;
|
|
break;
|
|
}
|
|
|
|
if (!chosen)
|
|
{
|
|
result.error = hosts.empty()
|
|
? L"no host process found (need admin or SYSTEM)"
|
|
: L"no injectable host available";
|
|
return result;
|
|
}
|
|
|
|
if (!InjectDllIntoProcess(chosen->pid, dll))
|
|
{
|
|
result.error = L"LoadLibrary injection failed into " + chosen->imageName;
|
|
return result;
|
|
}
|
|
|
|
result.ok = true;
|
|
result.hostPid = chosen->pid;
|
|
result.hostSession = chosen->sessionId;
|
|
result.hostImage = chosen->imagePath;
|
|
result.method = StageAlive() ? L"stage-live" : L"stage-injected-no-reply";
|
|
|
|
GetProcessCreateTime(chosen->pid, result.creationTime);
|
|
|
|
HostTrack track;
|
|
track.pid = result.hostPid;
|
|
track.creationTime = result.creationTime;
|
|
track.payload = dll;
|
|
track.hostImage = result.hostImage;
|
|
|
|
SaveHostTrack(track);
|
|
|
|
Log(L"[stage] injected %s into %lu (%s)",
|
|
dll.c_str(),
|
|
result.hostPid,
|
|
result.method.c_str());
|
|
|
|
return result;
|
|
}
|
|
|
|
HostInjectResult StageEnsure(bool requireSystem)
|
|
{
|
|
HostInjectResult result;
|
|
|
|
if (StageAlive())
|
|
{
|
|
result.ok = true;
|
|
result.method = L"stage-already-live";
|
|
|
|
HostTrack track = LoadHostTrack();
|
|
|
|
if (track.Tracked())
|
|
{
|
|
result.hostPid = track.pid;
|
|
result.hostImage = track.hostImage;
|
|
result.creationTime = track.creationTime;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
return StageInject(L"", L"", requireSystem);
|
|
}
|
|
|
|
std::wstring FormatStageStatus()
|
|
{
|
|
std::wostringstream os;
|
|
|
|
std::wstring dll = StageDllPath();
|
|
|
|
os << L"alive=" << (StageAlive() ? 1 : 0)
|
|
<< L" dll=" << (dll.empty() ? L"(missing)" : dll);
|
|
|
|
HostTrack track = LoadHostTrack();
|
|
|
|
if (track.Tracked())
|
|
os << L" host_pid=" << track.pid << L" host=" << track.hostImage;
|
|
|
|
return os.str();
|
|
}
|
|
|