项目地址:https://github.com/HDRorz/WriteFileHook
我这个项目是用VS2015 Community创建的。项目里设置的编译目标平台是Win8.1,底层库设置的VS2015,估计低版本的VS打开项目里的C++项目应该会有问题,而且因为我系统是Win7(还没装Win7 SDK)(拿到电脑就是Win7 sp1了,懒得处理了),在调试和编译时有很多麻烦,VS2015在编译C++时还会添加很多私货,在注册COM组件时还需要dcomp.dll、IEShims.dll、ucrtbased.dll(Win10 SDK)这3个dll。
dcomp.dll(网上下的) C:\Program Files (x86)\Internet Explorer\IEShims.dll C:\Program Files (x86)\Windows Kits\10\bin\x86\ucrt\ucrtbased.dll C:\Program Files (x86)\Windows Kits\10\bin\x64\ucrt\ucrtbased.dll
然而加了这几个dll,我写的那2个COM组件还是跑不起来(软爹WCNM
Nektra.Deviare2还是挺好用的。源代码编译后,使用regsvr32注册DeviareCOM.dll和DeviareCOM64.dll两个COM组件。
然后把生成的Nektra.Deviare2.dll添加引用到需要使用Deviare2的项目。DvAgent.dll是给C++用的。deviare32.db和deviare64.db是数据库文件,似乎并用不到(或者说已经在用了,直接用了COM组件注册的地方读入了???以后再研究吧)。
Deviare2启动时内建了任务管理器,可以通过进程名称获得需要hook的那个进程。 然后注册API调用事件OnWriteFileCalled,添加WriteFile Hook,Hook模式为eNktHookFlags.flgOnlyPostCall & eNktHookFlags.flgRestrictAutoHookToSameExecutable,前面的意思是每次传给Windows前触发,后面的意思同进程内循环自动触发?
public WriteFileHooker(string proccessName) { _spyMgr = new NktSpyMgr(); _spyMgr.Initialize(); _spyMgr.OnFunctionCalled += new DNktSpyMgrEvents_OnFunctionCalledEventHandler(OnWriteFileCalled); GetProcess(proccessName); if (_process == null) { //TODO: 没有监听进程时怎么办 //Environment.Exit(0); throw new Exception("没找到进程" + proccessName); } NktHook hook = _spyMgr.CreateHook("Kernel32.dll!WriteFile", (int)(eNktHookFlags.flgOnlyPostCall & eNktHookFlags.flgRestrictAutoHookToSameExecutable)); hook.Hook(true); hook.Attach(_process, true); processHandle = WinApi.OpenProcess(WinEnum.PROCESS_WM_READ | WinEnum.PROCESS_DUP_HANDLE, false, _process.Id); } private bool GetProcess(string proccessName) { NktProcessesEnum enumProcess = _spyMgr.Processes(); NktProcess tempProcess = enumProcess.First(); while (tempProcess != null) { //Console.Out.WriteLine(tempProcess.Name); if (tempProcess.Name.Contains(proccessName) && tempProcess.PlatformBits > 0 && tempProcess.PlatformBits <= IntPtr.Size * 8) { _process = tempProcess; return true; } tempProcess = enumProcess.Next(); } _process = null; return false; }
API调用事件处理函数接受3个参数,参数3 hookCallInfo是API调用时的参数。我们只要处理参数3里的数据。参考WInApi的结构,WriteFile函数有5个参数,分别是文件句柄、写入缓冲区、写入字节数、实际写入字节数、不知道干啥用的某结构体指针(这里用不到)。通过hookCallInfo.Params().Next() 依次获取各个参数。
// //BOOL WINAPI WriteFile( // _In_ HANDLE hFile, // _In_ LPCVOID lpBuffer, // _In_ DWORD nNumberOfBytesToWrite, // _Out_opt_ LPDWORD lpNumberOfBytesWritten, // _Inout_opt_ LPOVERLAPPED lpOverlapped //); // /// <summary> /// WriteFile调用事件处理函数 /// </summary> /// <param name="hook"></param> /// <param name="process"></param> /// <param name="hookCallInfo"></param> private void OnWriteFileCalled(NktHook hook, NktProcess process, NktHookCallInfo hookCallInfo) { string strDocument = "Document: "; INktParamsEnum paramsEnum = hookCallInfo.Params(); INktParam hFile = paramsEnum.First(); INktParam lpBuffer = paramsEnum.Next(); INktParam nNumberOfBytesToWrite = paramsEnum.Next(); #region 看着官方示例写的 毛用没有 if (hFile.PointerVal != IntPtr.Zero) { INktParamsEnum hFileEnumStruct = hFile.Evaluate().Fields(); INktParam hFileStruct = hFileEnumStruct.First(); } Console.Out.WriteLine(lpBuffer.ReadString()); #endregion var h_file = QueryFileHandle(hFile.Address); ReadBuffer(lpBuffer.Address, nNumberOfBytesToWrite.Address); }
官方示例(一个打印机hook程序)里用了lpBuffer.ReadString()就能直接获得缓冲区内文本内容,然而这里不行,这里的lpBuffer 是一个指针。这时就需要另外神奇的操作的了。
我们在注册目标进程时还运行了
WinApi.OpenProcess(WinEnum.PROCESS_WM_READ | WinEnum.PROCESS_DUP_HANDLE, false, _process.Id);
OpenProcess这个WinApi赋予了我们当前进程读取目标进程内存和复制句柄的权限。获得授权之后我们就可以调用别的WinApi任意调♂戏目标进程了。
关于目标进程到底调用WriteFile写了些什么可以通过直接把内存读爆的方式就知道了。
/// <summary> /// 内存读爆 /// </summary> /// <param name="p_buffer"></param> /// <param name="p_size"></param> private void ReadBuffer(IntPtr p_buffer, IntPtr p_size) { byte[] _size = new byte[4]; int readedbtyes = 0; var result = WinApi.ReadProcessMemory(processHandle.ToInt32(), p_size.ToInt32(), _size, 4, ref readedbtyes); int size = WinApi.ToInt32(_size); byte[] _bufferpoint = new byte[PtrSize]; result = WinApi.ReadProcessMemory(processHandle.ToInt32(), p_buffer.ToInt32(), _bufferpoint, PtrSize, ref readedbtyes); int bufferpoint = WinApi.ToInt32(_bufferpoint); byte[] _buffer = new byte[size]; result = WinApi.ReadProcessMemory(processHandle.ToInt32(), bufferpoint, _buffer, size, ref readedbtyes); Console.Out.WriteLine(Encoding.Default.GetString(_buffer)); Console.Out.WriteLine(Encoding.UTF8.GetString(_buffer)); }
然而问题是现在还不知道目标进程写了哪个文件。因为把堆栈上的hFile指针的内存数据读出来的结果是7,7是啥意思根本不知道。还好还可以用WinDbg调试目标进程,使用
!handle
可以列出进程中所有的句柄。通过数数发现这个7的意思是进程中第7个文件句柄 。当然因为这是隔壁进程的句柄,直接通过GetFullPathName这个WinApi是没法获得文件路径的。这种时候又有软爹隐藏黑科技,用SYSTEMHANDLEINFORMATION(16)参数调用ZwQuerySystemInformation函数可以获得系统中所有的句柄信息。
然而系统中句柄太多,用.net的Pinvoke的方式去调用直接就炸了(见上篇)。我写了两个COM组件,然而却都用不了(应该是vs编译的原因)。只能用C++/CLI的方式在非托管的状态下用C++调用WinApi然后通过过滤进程pid来减少句柄数,最后直接封装成托管对象返回给.net。其中FileObject的ObjectType是28(妈的,一个个试出来的)。C++/CLI的语法真tmd绕,用的时候头都大了。
// WinApiReader.h #pragma once using namespace System; namespace WinApiReader { public ref class SystemHandle { public: //这里用了C++类型,在C++环境下使用会比较简单,到托管环境下会自动转换成托管类型 unsigned int ProcessId; unsigned char ObjectType; unsigned char Flags; unsigned short Value; unsigned int Address; unsigned int GrantedAccess; }; public ref class SystemHandleInfo { public: int Count; //没想到吧,数组是这么声明的 array<SystemHandle^> ^SystemHandles; }; public ref class WinApiReader { public: //没想到吧,还有[System::Runtime::InteropServices::OutAttribute]这种操作,这个可以实现C#中out关键字 int QueryProcessHandleInfo(int ProcessId, [System::Runtime::InteropServices::OutAttribute] SystemHandleInfo^% HandleInfo); }; }
// 这是主 DLL 文件。 #include "stdafx.h" #include <Windows.h> #include "WinApiReader.h" //int QueryProcessHandleInfo(int process) //WinApi用的结构体,句柄信息 typedef struct _SYSTEM_HANDLE { DWORD dwProcessId; BYTE bObjectType; BYTE bFlags; WORD wValue; PVOID pAddress; DWORD GrantedAccess; } SYSTEM_HANDLE; //WinApi用的结构体,句柄信息数组 typedef struct _SYSTEM_HANDLE_INFORMATION { ULONG Count; SYSTEM_HANDLE SystemHandles[]; } SYSTEM_HANDLE_INFORMATION; //NTSTAUS 长度不匹配,这里引用ntstaus.h会编译过不了(太迷了) #define STATUS_INFO_LENGTH_MISMATCH 0xC0000004L //ZwQuerySystemInformation函数指针 typedef unsigned long(*ZWQUERYSYSTEMINFORMATION)(int SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength); //获取目标进程的所有句柄 int WinApiReader::WinApiReader::QueryProcessHandleInfo(int ProcessId, [System::Runtime::InteropServices::OutAttribute] SystemHandleInfo^% HandleInfo) { HMODULE hNtDLL = LoadLibrary(L"NTDLL.DLL"); if (!hNtDLL) { return FALSE; } ZWQUERYSYSTEMINFORMATION ZwQuerySystemInformation = (ZWQUERYSYSTEMINFORMATION)GetProcAddress(hNtDLL, "ZwQuerySystemInformation"); if (ZwQuerySystemInformation == NULL) { return FALSE; } ULONG mSize = 0x8000; PVOID mPtr; ULONG status; //进程的堆内存 HANDLE hHeap = GetProcessHeap(); //这里用了一个循环来开辟一个堆内存空间用来存放函数调用结果(因为太♂大了) do { mPtr = HeapAlloc(hHeap, 0, mSize); if (!mPtr) return NULL; memset(mPtr, 0, mSize); status = ZwQuerySystemInformation(16, mPtr, mSize, NULL); if (status == STATUS_INFO_LENGTH_MISMATCH) { HeapFree(hHeap, 0, mPtr); mSize = mSize * 2; } } while (status == STATUS_INFO_LENGTH_MISMATCH); int allCount = ((SYSTEM_HANDLE_INFORMATION*)mPtr)->Count; int outArrLen = allCount / 10; PVOID pOutArr; int outArrSize = sizeof(SYSTEM_HANDLE) * outArrLen; pOutArr = HeapAlloc(hHeap, 0, outArrSize); memset(pOutArr, 0, outArrSize); SYSTEM_HANDLE* outArr = (SYSTEM_HANDLE*)pOutArr; //筛选出目标进程的所有句柄 int outCount = 0; SYSTEM_HANDLE item; for (int i = 0; i < allCount; i++) { item = ((SYSTEM_HANDLE_INFORMATION*)mPtr)->SystemHandles[i]; if (item.dwProcessId == ProcessId) { outArr[outCount].bFlags = item.bFlags; outArr[outCount].bObjectType = item.bObjectType; outArr[outCount].dwProcessId = item.dwProcessId; outArr[outCount].GrantedAccess = item.GrantedAccess; outArr[outCount].pAddress = item.pAddress; outArr[outCount].wValue = item.wValue; outCount++; } } /*PVOID pOutHandleInfo; int outTrueArr = sizeof(SYSTEM_HANDLE) * (outCount - 1); int outHandleInfoSize = sizeof(SYSTEM_HANDLE_INFORMATION) + outTrueArr; pOutHandleInfo = HeapAlloc(hHeap, 0, outHandleInfoSize); memset(pOutHandleInfo, 0, outHandleInfoSize); SYSTEM_HANDLE_INFORMATION* outHandleInfo = (SYSTEM_HANDLE_INFORMATION*)pOutHandleInfo; outHandleInfo->Count = outCount - 1; memcpy(outHandleInfo->SystemHandles, outArr, outTrueArr); ProcessHandleInfo = (ULONG*)pOutHandleInfo;*/ //手工装箱 HandleInfo = gcnew SystemHandleInfo(); HandleInfo->Count = outCount; HandleInfo->SystemHandles = gcnew array<SystemHandle^>(outCount); for (int i = 0; i < outCount; i++) { HandleInfo->SystemHandles[i] = gcnew SystemHandle(); HandleInfo->SystemHandles[i]->ProcessId = outArr[i].dwProcessId; HandleInfo->SystemHandles[i]->Address = (unsigned int)outArr[i].pAddress; HandleInfo->SystemHandles[i]->Flags = outArr[i].bFlags; HandleInfo->SystemHandles[i]->ObjectType = outArr[i].bObjectType; HandleInfo->SystemHandles[i]->GrantedAccess = outArr[i].GrantedAccess; HandleInfo->SystemHandles[i]->Value = outArr[i].wValue; } //释放堆内存 HeapFree(hHeap, 0, mPtr); HeapFree(hHeap, 0, pOutArr); return 1; }
C++/CLI生成的dll并不是COM组件,可以直接引用到项目中,using之后就可以直接调用了。拿到真实的hFile的handle地址后就可以用DuplicateHandle函数复制一个句柄到自己的进程空间中,之后可以用File系WinApi随意调♂戏了。
WinApi.DuplicateHandle(processHandle.ToInt32(), p_hfile.ToInt32(), hCurProcess, ref my_hfile, 0x80000000, false, 2);
/// <summary> /// 把远程file handle复制到本地进程 /// 然后获取文件名 /// </summary> /// <param name="p_hfile"></param> private unsafe void ReadFileInfo(IntPtr p_hfile) { IntPtr my_hfile = new IntPtr(-1); int hCurProcess = -1;//WinApi.GetCurrentProcess(); bool result; result = WinApi.DuplicateHandle(processHandle.ToInt32(), p_hfile.ToInt32(), hCurProcess, ref my_hfile, 0x80000000, false, 2); #region GetFileInformationByHandle 文件最基本信息 竟然没文件名 var fileinfo = new BY_HANDLE_FILE_INFORMATION(); result = WinApi.GetFileInformationByHandle(my_hfile.ToInt32(), ref fileinfo); #endregion #region GetFileInformationByHandleEx var fileinfo2 = new FILE_FULL_DIR_INFO(); int fileInfo2Type = (int)FileInformationClass.FileFullDirectoryInfo;//0xe; //本来想用开辟非托管内存的操作,传指针到winapi,后来发现那块内存有数据后用 //PtrToStructure转结构体报错,也许是操作问题,还不如直接传结构体 int size2 = Marshal.SizeOf(fileinfo2); IntPtr p_fileInfo2 = Marshal.AllocHGlobal(size2); Marshal.StructureToPtr(fileinfo2, p_fileInfo2, false); //调用失败 大概是LARGE_INTEGER 这个union的结构不对 result = WinApi.GetFileInformationByHandleEx(my_hfile.ToInt32(), fileInfo2Type, ref fileinfo2, size2); var fileinfo3 = new FILE_NAME_INFO(); int fileInfo3Type = (int)FileInformationClass.FileNameInfo;//0x2; int size3 = Marshal.SizeOf(fileinfo3) + 1000*2; IntPtr p_fileInfo3 = Marshal.AllocHGlobal(size3); Marshal.StructureToPtr(fileinfo3, p_fileInfo3, false); //\otherproject\WriteFileHook\Log4netLoger\bin\x86\Debug\Logs\debug.log20170901 result = WinApi.GetFileInformationByHandleEx(my_hfile.ToInt32(), fileInfo3Type, ref fileinfo3, size3); StringBuilder fn3 = new StringBuilder(1000); int len3 = WinApi.GetFullPathName(fileinfo3.FileName, 1000, fn3, null); //输出正常路径 //E:\otherproject\WriteFileHook\Log4netLoger\bin\x86\Debug\Logs\debug.log20170901 Console.Out.WriteLine(fn3); #endregion #region GetFinalPathNameByHandle StringBuilder fileName = new StringBuilder(1000); //获取的带设备路径 //\Device\HarddiskVolume4\otherproject\WriteFileHook\Log4netLoger\bin\x86\Debug\Logs\debug.log20170901 int size4 = WinApi.GetFinalPathNameByHandle(my_hfile.ToInt32(), fileName, 1000, 0x2); StringBuilder fn4 = new StringBuilder(1000); //会在前面在加个盘符,然而没去掉设备路径,超谐 //有专门根据设备路径获取盘符路径的方法 //E:\Device\HarddiskVolume4\otherproject\WriteFileHook\Log4netLoger\bin\x86\Debug\Logs\debug.log20170901 int len4 = WinApi.GetFullPathName(fileName.ToString(), 1000, fn4, null); Console.Out.WriteLine(fn4); #endregion #region ZwQueryInformationFile //GetFileInformationByHandleEx 内部调用的就是这个吧( var fileStatus5 = new IO_STATUS_BLOCK(); int ssize5 = Marshal.SizeOf(fileStatus5); IntPtr p_fileStatus5 = Marshal.AllocHGlobal(ssize5); Marshal.StructureToPtr(fileStatus5, p_fileStatus5, false); var fileinfo5 = new FILE_NAME_INFO(); int fileInfo5Type = (int)FileInformationClass.FileFullDirectoryInfo;//9; int size5 = Marshal.SizeOf(fileinfo5) + 1000*2; IntPtr p_fileInfo5 = Marshal.AllocHGlobal(size5); Marshal.StructureToPtr(fileinfo5, p_fileInfo5, false); int ret = WinApi.ZwQueryInformationFile(my_hfile.ToInt32(), ref fileStatus5, ref fileinfo5, size5, fileInfo5Type); //\otherproject\WriteFileHook\Log4netLoger\bin\x86\Debug\Logs\debug.log20170901 Console.Out.WriteLine(fileinfo5.FileName); #endregion }
这样就可以通过这么复杂的方式知道隔壁进程在什么时候向什么文件写什么内容。(我为什么要做这种偷窥的事情呢,因为很刺激啊(
后记:软爹wcnm