C#/C++:启动应用程序并处理其对系统的I/O调用

C#/C++: Launch an application and handle its I/O calls to the system

本文关键字:系统 调用 处理 C++ 启动 应用程序      更新时间:2023-10-16

我需要启动其他应用程序并处理它的I/O操作。因此,当它试图读取/写入文件时,我需要抓住它并更改路径。

这应该是可能的,因为有这样的程序(比如ModOrganizer)。

问题是我不想使用文件系统过滤器驱动程序。我不想让我的应用程序的用户安装这样的东西。

正如我所看到的,ModOrganizer通过多种方式实现了这一点,包括proxy.dllhookse.t.c。不知何故,它实现了几乎任何程序都可以从中启动的目标,并且ModOrganiz器将处理对特定目录的请求。

github上有源代码,但我真的不明白。这就是为什么我在这里问这个问题。

同样,ModOrganizer实现了这一,而无需对所有可能的程序进行反编译以了解注入位置。而且它不使用系统过滤器。

(请解释你的缺点。否则我将来该如何改进我的问题?)

您需要的是修改流程中每个模块的导入表。

看起来你提到的程序使用了与这里描述的类似的技术:https://www.codeproject.com/Articles/2082/API-hooking-revealed在">使用CreateRemoteThread()API函数注入DLL"一节下。但是,它不是使用远程线程,而是强制主程序线程完成它的工作(请参阅函数injectDLL):

https://github.com/TanninOne/modorganizer/blob/4a582e524dd012ed9d5fdb4f9c97aab22c8dac85/src/spawn.cpp

请注意,有一个标志CREATE_SUSPENDED传递给CreateProcess函数,这有助于它在主线程能够执行任何操作之前截断所有函数。

它没有修补导入表,而是插入在程序集中编写的存根(请参阅函数injectDLL):

https://github.com/TanninOne/modorganizer/blob/4a582e524dd012ed9d5fdb4f9c97aab22c8dac85/src/shared/inject.cpp

Jeffrey Richter在他的书"Windows via C/C++"中有一个关于如何修补导入表的好例子。你可以尝试找到并阅读一本完整的书,也可以在这里查看代码:https://github.com/lattesir/WindowsViaCPP/blob/master/22-LastMsgBoxInfoLib/APIHook.cpp

但是,对于任何反病毒软件来说,你的程序都可能看起来像病毒。

ModOrganizer使用远程线程注入在目标进程的上下文中运行自定义库(dll)。然后,图书馆继续从内部操纵这个过程,并获得对它的控制

这是最著名的技术之一,用户空间代码注入和在大多数进程中都能很好地工作。

注意:ModOrganizer不支持64位二进制文件,请参阅其代码中的以下异常:

throw windows_error("无法访问线程上下文。请注意,Mod Organizer不支持64位二进制文件!");

步骤1:获取HANDLE到目标线程&处理

与不同的线程交互&在机器上的进程中,您首先必须获得它们的HANDLE

ModOrganizer中获得该句柄的方法是bool spawn(..),更具体地说是以下行:

PROCESS_INFORMATION pi;
BOOL success =
::CreateProcess(nullptr,
commandLine,
nullptr, nullptr, // no special process or thread attributes
inheritHandles,   // inherit handles if we plan to use stdout or stderr reroute
CREATE_BREAKAWAY_FROM_JOB | (suspended ? CREATE_SUSPENDED : 0), // create suspended so I have time to inject the DLL
nullptr,          // same environment as parent
currentDirectory, // current directory
&si, &pi          // startup and process information
);

正如您所看到的,此行使用CreateProcess获取PROCESS_INFORMATION对象。我们现在可以从pi.hProcess提取进程HANDLE,从pi.hThread提取线程HANDLE

此外,该命令不仅获得目标进程的HANDLEs,而且将其置于暂停模式(CREATE_SUSPENDED)。这使我们能够随心所欲地操纵它,然后继续执行它。

步骤2:将您自己的dll注入目标进程

注入器方法是在src/shared/inject.cpp内部定义的void injectDLL(..)方法。

该方法的目标是将dllname指定的dll加载到目标进程processHandle内的目标线程threadHandle中。

让我们回顾一下重要的东西:

  • TParameters parameters;-这一行和下面的4行分配并设置TParameters结构的值,以包括我们要加载到目标进程中的dll文件的路径,以及我们将传递给该dll文件中的init函数的参数
  • ::LoadLibrary(__TEXT("kernel32.dll"))-将kernel32.dll加载到我们的流程空间中
  • ::GetProcAddress(k32mod, "LoadLibraryA")-获取kernel32.dllLoadLibraryA方法的地址
  • ::GetProcAddress(k32mod, "GetProcAddress")-获取kernel32.dllGetProcAddress方法的地址
  • ::VirtualAllocEx(..)-在目标进程内分配虚拟内存
  • ::WriteProcessMemory(.., &parameters, ..)-将我们之前创建的parameters结构写入我们在目标进程内分配的虚拟内存中
  • BYTE stubLocal[] = { .. }-生成在目标进程内加载dll的shell代码,然后使用parameters结构中的值调用dll内的Init函数
  • PBYTE stubRemote = reinterpret_cast<PBYTE>(::VirtualAllocEx(.., sizeof(stubLocal), ..))-在目标进程中分配虚拟内存,以容纳shell代码。保存我们在stubRemote中分配的内存段的地址
  • ::GetThreadContext(threadHandle, &threadContext)-获取目标进程的上下文,包括指向要执行的下一条指令的IP(指令指针)
  • ::WriteProcessMemory(.., reinterpret_cast<LPCVOID>(stubLocal), ..)-将shell代码写入我们在目标进程中分配的虚拟内存
  • threadContext.Eip = (ULONG)stubRemote;-将目标进程的IP设置为shell代码的位置
  • ::SetThreadContext(...)-恢复执行目标进程。执行在我们为IP设置的地址处恢复,从而执行我们注入的shell代码,该代码加载所需的dll文件并调用其中的Init函数。一旦我们的方法完成,它就会将执行返回到目标进程内的原始地址,就好像什么都没发生一样

此时,我们的库已在目标进程中完全加载,并有机会执行各种讨厌的挂钩。但是,我们加载的dll在目标进程中究竟做了什么?

步骤3:了解加载的dll

ModOrganizer实际上为注入的dll有单独的存储库:ModOrganizer hookdll

dll的主模块在dllmain.cpph中定义。本模块:

  • 定义所有挂钩方法:

    // hook declarations
    CreateProcessA_type CreateProcessA_reroute = CreateProcessA;
    CreateProcessW_type CreateProcessW_reroute = CreateProcessW;
    [..]
    GetModuleFileNameA_type GetModuleFileNameA_reroute = GetModuleFileNameA;
    GetModuleFileNameW_type GetModuleFileNameW_reroute = GetModuleFileNameW;
    
  • 定义Init函数,它是我们的shell代码在加载dll 后执行的方法

  • 调用InitHooks()方法,该方法负责初始化应用程序使用的所有挂钩


在这一点上,我希望您对实现目标所需的步骤和API有一个清晰的了解,并了解应该重用ModOrganizer代码中的哪些函数。

开发提示:我建议您编译自己的虚拟可执行文件,每秒将Hello world!打印到控制台,并在上面测试您的注射器。根据我自己的经验,开发此类工具需要时间和手术精度。当你的目标崩溃时,不要轻易吓退,并确保添加调试打印(如果你从注入的dll打印,你应该看到它打印到与Hello world!相同的控制台)。


64位系统

我看到一些链接说ModOrganizer的开发正在停止,将有一个新的mod管理器,由同一个开发团队制作,它将更加可定制,所以也许他们决定跳过对它的64位支持。

对于64位应用程序,完全可以实现这一点,但是,很难将其注入在64位系统内运行的32位应用程序中(使用WOW基础设施)。

为了将现有代码更改为支持64位,我认为步骤1可能不需要任何更改,步骤2至少需要对shell代码进行一些调整(汇编指令不同),步骤3(注入的dll)应该至少重新编译,并且一些挂接的API可能会更改。


更多技术和指南

除了ModOrganizer使用的技术外,这里还有一些教程和参考资料:

  • 使用CreateRemoteThread的Dll注入:非常相似,但它在远程进程中创建了一个加载Dll的线程。详细的教程可以在这里找到。

  • InjectProc项目(Github),支持多个注入实现作为参考。

  • EasyHook项目(Github)支持32位和64位DLL注入。

这通常是通过动态修补内存中的远程进程来实现的:

  1. 目标函数的前几个指令(通常为5字节)被无条件跳转到迂回函数所取代
  2. 来自目标函数的指令保存在蹦床函数中
  3. 迂回函数在检查和/或修改其参数之后调用目标函数

这有效地拦截了任意二进制函数;在您的情况下,它将是一个Win32 API调用。以下白皮书详细描述了这一过程:

https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/huntusenixnt99.pdf

最大的挑战是找出指令边界,并将其复制到蹦床功能中。因为这个过程既繁琐又繁琐,而且不同版本的操作系统以及32位和64位二进制文件之间的差异也不同,所以您可能不想自己做。碰巧微软(由上述白皮书的作者)创建并维护了一个库来做这类工作,称为迂回:

https://www.microsoft.com/en-us/research/project/detours/

迂回库的32位版本是免费的,但问题是,为了支持64位二进制文件,你必须购买一个商业许可证,我上次检查的成本是数千美元。然而,也有开源的替代方案。你自己也遇到过一个,InjectProc和MinHook就是其中之一。

出于演示的目的,让我们使用MinHook,因为它仍然是积极维护的,并且已经被证明可以在32位和64位二进制文件的Windows的多个迭代中工作。

https://github.com/TsudaKageyu/minhook

既然我们有了挂钩库,那么问题是我们如何将其引入目标流程,以便它能够从一开始就进行修补工作?这通常是通过一种称为dll注入的技术来实现的。这涉及到写入目标进程内存,这是可以的,因为它将是您进程的子进程(实际的目标函数补丁是在目标进程的上下文中完成的),但您的应用程序可能因此而被反病毒软件标记。远程进程中函数的补丁/挂接涉及几个步骤:

  1. 您的注入器应用程序启动远程进程
  2. 您的挂钩dll通过CreateRemoteThread加载到远程进程中
  3. 在初始化时,您的钩子dll通过将对目标函数的调用重定向到迂回函数来修补目标函数
  4. 迂回功能通过蹦床调用目标功能

让我们通过尝试挂接Notepad.exe中的ReadFile函数来进行演示,我们将把它作为进程的子进程来启动。

这是我们的挂钩dll:

#include <SDKDDKVer.h>
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <iostream>
#include "MinHook.h"
typedef BOOL (WINAPI *READFILE)(HANDLE, LPVOID, DWORD, LPDWORD, LPOVERLAPPED);
READFILE fpReadFile = NULL;
BOOL WINAPI DetourReadFile(HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped)
{
size_t size = sizeof(FILE_NAME_INFO)+sizeof(WCHAR) * MAX_PATH;
FILE_NAME_INFO *info = reinterpret_cast<FILE_NAME_INFO *>(malloc(size));
memset(info, 0, size);
info->FileNameLength = MAX_PATH;
GetFileInformationByHandleEx(hFile, FileNameInfo, info, (DWORD)size);
MessageBoxW(NULL, info->FileName, L"HOOK ReadFile", NULL);
free(info);
return fpReadFile(hFile, lpBuffer, nNumberOfBytesToRead, lpNumberOfBytesRead, lpOverlapped);
}
extern "C" BOOL APIENTRY DllMain(HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpvReserved)
{
switch (dwReason)
{
case DLL_PROCESS_ATTACH:
{
MH_Initialize();
MH_CreateHook(&ReadFile, &DetourReadFile, reinterpret_cast<void**>((LPVOID)&fpReadFile));
MH_EnableHook(MH_ALL_HOOKS);
}
break;
case DLL_PROCESS_DETACH:
MH_DisableHook(MH_ALL_HOOKS);
MH_Uninitialize();
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
}
return true;
}

ReadFile目标函数通过调用MH_Initialize、MH_CreateHook和MH_EnableHook与MiniHook进行修补。我们绕过的ReadFile所做的就是显示一个带有文件名的消息框,并调用原始ReadFile。

这是我们的主要注射器应用:

#include <SDKDDKVer.h>
#include <stdio.h>
#include <tchar.h>
#include <windows.h>
#include <iostream>
typedef DWORD(WINAPI *fp_NtCreateThreadEx_t)(
PHANDLE ThreadHandle,
ACCESS_MASK DesiredAccess,
LPVOID ObjectAttributes,
HANDLE ProcessHandle,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
BOOL CreateSuspended,
DWORD dwStackSize,
LPVOID Unknown1,
LPVOID Unknown2,
LPVOID Unknown3);
int _tmain(int argc, _TCHAR* argv[])
{
char* dllPath = ".\HookDll.dll";
void* pLoadLibrary = (void*)GetProcAddress(GetModuleHandleA("kernel32"), "LoadLibraryA");
STARTUPINFOA startupInfo;
PROCESS_INFORMATION processInformation;
ZeroMemory(&startupInfo, sizeof(startupInfo));
CreateProcessA(0, "notepad.exe", 0, 0, 1, CREATE_NEW_CONSOLE, 0, 0, &startupInfo, &processInformation);
void* pReservedSpace = VirtualAllocEx(processInformation.hProcess, NULL, strlen(dllPath), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(processInformation.hProcess, pReservedSpace, dllPath, strlen(dllPath), NULL);
HANDLE hThread = NULL;
fp_NtCreateThreadEx_t fp_NtCreateThreadEx = NULL;
fp_NtCreateThreadEx = (fp_NtCreateThreadEx_t)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtCreateThreadEx");
fp_NtCreateThreadEx(
&hThread, 
0x2000000, 
NULL, 
processInformation.hProcess, 
(LPTHREAD_START_ROUTINE)pLoadLibrary,
pReservedSpace, 
FALSE, 0, NULL, NULL, NULL);
WaitForSingleObject(hThread, INFINITE);
VirtualFreeEx(processInformation.hProcess, pReservedSpace, strlen(dllPath), MEM_COMMIT);
return 0;
}

它启动notepad.exe,通过NtCreateThreadEx将我们的钩子dll加载到其中(这在Windows 7及以上版本上有效,不确定以前版本的Windows),并让dll处理其余的工作。如果您需要dll将信息传达回启动器进程,那就另当别论了。