一.概述:
如果一个病毒文件被植入正在运行的进程中,我们想要清除它时系统总会提供无法删除;有时编辑文件的进程被意外中止而文件句柄没有正确释放,导致此文件无法进行改写操作。现在我们会使用Unlocker之类的小工具去解锁,但在编写程序的可能会需要把这些功能包含在自己的代码中,本文就是自己写代码实现”如何关闭已经被加载的DLL或是正在使用的文件”功能,使用文章中的方法能很方便的完成文件解锁功能。
按最初的想法准备在ring0中完成这些功能,但在查找资料的过程中发现既然我们能在ring3中做,为什么不让这些方法更通用一些呢。其实功能实现并不难,主要是前期从哪里入手比较麻烦。
我们知道无论是动态库或是文件在加载到进程中时,总会有一个指向它的指针,如果让进程释放这段指针,那么这些文件就不会被系统锁定。下面将是我们的需要实现文件解锁功能而分解出的步骤
1. 枚举当前系统所有进程;
2. 查找进程中打开的文件句柄和加载的动态库句柄;
3. 通知进程关闭这些句柄。
二.详细设计
2.1查找进程加模的动态库模块
Let’s go,我们来分步完成它吧。对于枚举当前系统所有进程,在这里就不给出代码了,相信实现的方法很多。下面的代码段完成查找指定进程加载动库信息的功能(在这里使用了Jeffrey在《Windows核心编程》一书中提供的CToolhelp类,用它可以完成进程加载信息的分析功能,感谢Jeffrey,我一直在使用它)
// 自定义结构,保存打开句柄的的信息
typedef struct _UNFILE_INFO {
int nFileType;
DWORD dwHandle;
char *strFileName;
} UNFILE_INFO, *PUNFILE_INFO;
//////////////////////////////////////////////////////////////////////////
// 通过PID号取得PID打开的文件句柄信息
//////////////////////////////////////////////////////////////////////////
void
GetModules(DWORD dwProcessID, CList<PUNFILE_INFO, PUNFILE_INFO> &plsUnFileInfo)
{
CToolhelp::EnableDebugPrivilege(TRUE);
CToolhelp th(TH32CS_SNAPALL, dwProcessID);
// 显示进程的详细资料
MODULEENTRY32 me = { sizeof(me) };
BOOL fOk = th.ModuleFirst(&me);
for (; fOk; fOk = th.ModuleNext(&me))
{
PVOID pvPreferredBaseAddr = NULL;
pvPreferredBaseAddr =GetModulePreferredBaseAddr(dwProcessID, me.modBaseAddr);
// 取得进程模块信息
PUNFILE_INFO pUnFileInfo = new UNFILE_INFO;
// 模块地址
pUnFileInfo->dwHandle = (DWORD)me.modBaseAddr;
// 模块类型
pUnFileInfo->nFileType = UNTYPE_DLL;
// 模块名称
pUnFileInfo->strFileName = new char[strlen(me.szExePath)+1];
memset( pUnFileInfo->strFileName, 0, strlen(me.szExePath)+1);
strcpy( pUnFileInfo->strFileName, me.szExePath);
// 保存打开的模块信息
plsUnFileInfo.AddTail( pUnFileInfo);
}
}
上面功能完成了枚举进程加载的模块功能,我们把得到的枚举信息加入了链表中,以备后面使用。
2.2枚举进程打开的文件信息
下面将分段说明如何枚举指定进程打开的文件句柄。在这里我们需要使用两个DDK中提供的函数:
NTSTATUS
ZwQuerySystemInformation(
IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
OUT PVOID SystemInformation,
IN ULONG SystemInformationLength,
OUT PULONG ReturnLength OPTIONAL
);
NTSTATUS
ZwQueryInformationFile(
IN HANDLE FileHandle,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID FileInformation,
IN ULONG Length,
IN FILE_INFORMATION_CLASS FileInformationClass
);
ZwQuerySystemInformation是个未公开函数,通过它的SYSTEM_INFORMATION_CLASS结构,我们能完成许多进程和线程的操作,下面是它的部分内容,在这时我们使用它的SystemHandleInformation(0x10),来完成文件句柄的操作。
typedef enum _SYSTEM_INFORMATION_CLASS {
SystemBasicInformation, // 0
SystemProcessorInformation, // 1
SystemPerformanceInformation, // 2
…
SystemHandleInformation, // 16
…
SystemSessionProcessesInformation // 53
} SYSTEM_INFORMATION_CLASS;
ZwQueryInformationFile可以根据参数FileInformationClass的不同值来返回不同的类型,在这里我们使用FileInformationClass=FileInformationClass来得到FILE_NAME_INFORMATION,从而得到文件句柄指向的文件名,下面是它的结构定义。
typedef struct _FILE_NAME_INFORMATION {
ULONG FileNameLength;
WCHAR FileName[1];
} FILE_NAME_INFORMATION, *PFILE_NAME_INFORMATION;
因为需要在Ring3上使用DDK提供的函数,我们需要导出这些两个函数,下面是导出函数的示例代码:
{
g_hNtDLL = LoadLibrary( "ntdll.dll" );
if ( !g_hNtDLL )
{
return FALSE;
}
ZwQuerySystemInformation =
(ZWQUERYSYSTEMINFORMATION)GetProcAddress( g_hNtDLL, "ZwQuerySystemInformation");
if( ZwQuerySystemInformation == NULL)
{
return FALSE;
}
ZwQueryInformationFile =
(ZWQUERYINFORMATIONFILE)GetProcAddress( g_hNtDLL, "ZwQueryInformationFile");
if( ZwQueryInformationFile == NULL)
{
return FALSE;
}
}
我们先来使用ZwQuerySystemInformation函数枚举系统中打开的所有文件的句柄,枚举出的信息将包含如下结构:
typedef struct _SYSTEM_HANDLE_INFORMATION {
ULONG ProcessId;
UCHAR ObjectTypeNumber;
UCHAR Flags;
USHORT Handle;
PVOID Object;
ACCESS_MASK GrantedAccess;
} SYSTEM_HANDLE_INFORMATION, *PSYSTEM_HANDLE_INFORMATION;
ObjectTypeNumber定义了句柄所属对像的类型,文件的ObjectType对不同的操作系统是不同的,所以需要先找到当前操作系统所定义的ObjectType值,这样才能在枚举出的众多句柄信息中找到哪些是文件句柄信息。通用的方法是我们CreateFile打开空设备NUL,记下它的句柄,用来比较。
UCHAR GetFileHandleType()
{
HANDLE hFile;
PSYSTEM_HANDLE_INFORMATION Info;
ULONG r;
UCHAR Result = 0;
hFile = CreateFile("NUL", GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, 0);
if (hFile != INVALID_HANDLE_VALUE)
{
Info = GetInfoTable(SystemHandleInformation);
if (Info)
{
for (r = 0; r < Info->uCount; r++)
{
if (Info->aSH[r].Handle == (USHORT)hFile &&
Info->aSH[r].uIdProcess == GetCurrentProcessId())
{
Result = Info->aSH[r].ObjectType;
break;
}
}
HeapFree(hHeap, 0, Info);
}
CloseHandle(hFile);
}
return Result;
}
现在知道了句柄的类型就可以枚举系统中打开的文件了。首先用句柄获取打开的文件名:
typedef struct _NM_INFO
{
HANDLE hFile;
FILE_NAME_INFORMATION Info;
WCHAR Name[MAX_PATH];
} NM_INFO, *PNM_INFO;
// 因为在在线程中取得文件名
DWORD WINAPI
GetFileNameThread(PVOID lpParameter)
{
PNM_INFO NmInfo = lpParameter;
IO_STATUS_BLOCK IoStatus;
int r;
NtQueryInformationFile(NmInfo->hFile, &IoStatus, &NmInfo->Info,
sizeof(NM_INFO) - sizeof(HANDLE), FileNameInformation);
return 0;
}
void CFileProcess::GetFileName(HANDLE hFile, PCHAR TheName)
{
HANDLE hThread;
PNM_INFO Info = new NM_INFO;
char namebuf[2000] = {0};
Info->Info = (PFILE_NAME_INFORMATION)namebuf;
Info->hFile = hFile;
Info->nRet = 0x00;
// 在对打开的句柄调用ZwQueryInformationFile时,调用线程会等待PIPE中的消息
// 如果PIPE中没有消息时,线程可能会永久挂起,所以使用一个等待超时来打开句柄
hThread = CreateThread(NULL, 0, GetFileNameThread, Info, 0, NULL);
if (WaitForSingleObject(hThread, 100) == WAIT_TIMEOUT)
TerminateThread(hThread, 0);
CloseHandle(hThread);
// 调用句柄失败
if( Info->nRet == 0x00)
{
delete Info;
return ;
}
// 加上盘符
wchar_t volume = GetVolumeName(hFile);
if (volume != L'!')
{
wchar_t outstr[1000] = L"A:";
outstr[0] = volume;
memcpy(&outstr[2], Info->Info->FileName, Info->Info->FileNameLength);
outstr[2+Info->Info->FileNameLength] = 0;
WideCharToMultiByte(CP_ACP, 0, outstr, 2+Info->Info->FileNameLength, TheName, MAX_PATH, NULL, NULL);
}
delete Info;
return ;
}
下面代码片段用来枚举打开的文件:
{
// 取得操作系统文件句柄定义类型
UCHAR ObFileType = GetFileHandleType();
// 取得系统中打开的文件句柄列表
PULONG buf = GetHandleList();
if (buf == NULL)
return ;
Info = (PSYSTEM_HANDLE_INFORMATION)&buf[1];
if (Info)
{
for (r = 0; r < buf[0]; r++, Info++)
{
if (Info->ObjectTypeNumber == ObFileType && Info->ProcessId == dwProcessID)
{
hProcess = OpenProcess(PROCESS_DUP_HANDLE, FALSE, Info->ProcessId);
if (hProcess)
{
// 复制句柄到当前进程中,这样方便对当前进程句柄取出文件名
// 因为共享一个FileObject,这两个文件句柄对像将由两个进程共享
if ( DuplicateHandle( hProcess, (HANDLE)Info->Handle,
GetCurrentProcess(), &hFile, 0, FALSE, DUPLICATE_SAME_ACCESS))
{
GetFileName(hFile, Name);
// 取得文件信息到链表中
if( strlen( Name)>0)
{
AfxTrace("PID=%d FileHandle %d FileName=%s", Info->ProcessId, Info->Handle, Name);
// 在这里加入你要保存的文件句柄信息
}
CloseHandle(hFile);
}
CloseHandle(hProcess);
}
}
}
}
}
2.3使用远线程方法关闭进程加载的模块和打开的文件
通过枚举进程加载的模块信息和打开的文件信息,我们已经得到系统中加载的DLL和文件名信息,以下是我们在链表中保存的信息:
{
DWORD ProcessID // 进程PID
BYTE nHandleType // 句柄类型是文件还是加载的DLL
Char strFileName[MAX_PATH] // 文件名
}
如果我们需要关闭正在打开的文件,只需要在这个链表中以文件名进行查找,即得到打开文件的进程PID。怎么才能通知别的进程关闭相应的句柄呢,我们通过跨进程去关闭句柄是很困难的,不如直接注入代码到需要的进程中,然后直接关闭句柄就行了。
需要再遍写一个DLL用来注入到进程中通知进程关闭文件句柄。这个DLL不需要做别的处理,只需要在加载的地方根据不同的句柄类型使用不同的方法进行关闭即可。下面是注入到进程的DLL执行的关闭句柄函数:
Void CloseHandel( PVOID pHandle, BYTE nHandleType)
{
If(HandleType = HANDLE_FILE)
FreeLibrary((HMODULE)pHandle);
else
CloseHandle((HANDLE)pHandle);
}
在DllMain中调用关闭句柄的函数:
DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
// Remove this if you use lpReserved
UNREFERENCED_PARAMETER(lpReserved);
if (dwReason == DLL_PROCESS_ATTACH)
{
DWORD nHandle=0;
BYTE nType=0;
TRACE0("MOPER.DLL Initializing!\n");
AfxMessageBox("MOPER.DLL Initializing!");
// 从碎甲程序所生成的文件句柄映射文件中取出要关闭的句柄信息
// 方法很多我在这里直接用共享文件实现的,就不写出代码了
GetHandle( nHandle, nType)
// 关闭句柄
CloseHandel((PVOID)nHandle, nType);
...
}
...
}
下面示例取出通过文件句柄链表中的PID,使用远程注入的方法向进程注入关闭句柄的DLL
{
HANDLE hRemoteProcess = OpenProcess(PROCESS_CREATE_THREAD | //允许远程创建线程
PROCESS_VM_OPERATION | //允许远程VM操作
PROCESS_VM_WRITE,//允许远程VM写
FALSE, dwProcessId);
if( hRemoteProcess == NULL)
return FALSE;
PTHREAD_START_ROUTINE pfnStartAddr = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle("Kernel32"), "LoadLibraryA");
int nLen = (strlen( pszLibFile)+1)*sizeof(char);
char * pszLibFileRemote = (char *) VirtualAllocEx( hRemoteProcess, NULL, nLen, MEM_COMMIT, PAGE_READWRITE);
if( pszLibFileRemote != NULL &&
pfnStartAddr !=NULL)
{
if( WriteProcessMemory(hRemoteProcess, pszLibFileRemote, (PVOID) pszLibFile, nLen, NULL))
{
HANDLE hThread = CreateRemoteThread( hRemoteProcess, NULL, 0, pfnStartAddr, pszLibFileRemote, 0, NULL);
if( !hThread)
{
WaitForSingleObject(hThread, INFINITE);
bRet = TRUE;
CloseHandle(hThread);
}
}
VirtualFreeEx(hRemoteProcess, pszLibFileRemote, 0, MEM_RELEASE);
}
CloseHandle( hRemoteProcess);
}
三.备注
对查找文件句柄的方法,参考了鄙人拙译(http://greatdong.blog.edu.cn)的“如何操作被占用文件”在此表示感谢。
使用了Jeffrey在《Windows核心编程》一书中提供的CToolhelp类,用它可以完成进程加载信息的分析功能,感谢Jeffrey。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)