检测 Windows 7 下的 SAM 文件损坏(第 1 部分)





5.00/5 (3投票s)
本文第一部分介绍了如何通过 Windows 服务将 DLL 注入 lsass 进程。
引言
本文介绍并解释了如何在 Windows 7 系统运行时访问 SAM 文件。这里的目标不是提供一种破解 Windows 系统的方法,而是为系统管理员检测 SAM 文件损坏提供解决方案。
有几种方法可以在系统运行时或关闭时从 SAM 文件中提取用户凭据。在本文中,我们选择了第一种方法。事实上,为了我们的目的,我们需要最新的凭据,以便将它们与之前保存的凭据进行比较,并在比较失败时发出警报。
此方法通过在本地安全机构子系统服务 (LSASS) 进程(俗称 lsass.exe)中执行 DLL 注入来工作。这是一种恶意软件用于在另一个进程中运行 DLL 的方式,从而为该 DLL 提供该进程的所有权限。哈希转储工具通常以 lsass.exe 为目标,因为它具有必要的权限级别以及对许多有用 API 函数的访问权限。
DLL 注入后,它使用未记录的 API 函数,如 SamIConnect、SamQueryInformationUser 和 SamIGetPrivateData,从 SAM 文件中提取哈希。
一旦哈希被转储,它们就会与之前保存的哈希进行比较。如果比较失败,系统会发出警报。

背景
我们在 Visual Studio 2013 下开发的应用程序由三个程序组成
- CheckSam.exe:包含一个 Windows 服务,该服务将 DLL 注入到 lsass进程中。我们选择使用 Windows 服务是受到 Vista 以来引入的“会话分离”保护的驱动。‘会话分离’确保包括服务在内的核心系统进程始终在会话 0 中运行,而所有用户进程都在不同的会话中运行。因此,在用户会话中运行的任何进程都无法将 DLL 注入到系统进程中,因为“CreateRemoteThread”无法跨会话边界工作。此外,服务在本地系统帐户的安全上下文中运行。此帐户与运行核心 Windows 用户模式操作系统组件的帐户相同,包括 Smss.exe、Csrss.exe、Lsass.exe 等。
- ChkSamConfig.exe:此程序提供了一个用户控制台界面来启动、停止和删除 CheckSam 服务。
- dump.dll DLL:此 DLL 包含负责从 SAM 文件中提取凭据哈希的代码。
ChkSamConfig.exe
编写服务程序的 Main 函数
CheckSam.exe 的主函数调用“StartServiceCtrlDispatcher”函数连接到服务控制管理器 (SCM) 并启动控制调度器线程。调度器线程循环,等待服务控制管理器中的传入控制请求,这些请求针对调度表(即“SvcMain”)中指定的服务。
// Communication API with SCM
#pragma comment(lib, "advapi32.lib")
// The service name
#define SVCNAME TEXT("checkSam")
void __cdecl _tmain(int argc, TCHAR *argv[])
{
	// if the first argument is "install", then install SVCNAME service
	if (lstrcmpi(argv[1], TEXT("install")) == 0)
	{
		InstallSvc();
		return;
	}
	
	// add SVCNAME into the DispatchTable
	SERVICE_TABLE_ENTRY DispatchTable[] =
	{
		{ SVCNAME, (LPSERVICE_MAIN_FUNCTION)SvcMain },
		{ NULL, NULL }
	};
	// This call returns when the service has stopped. 
	
	if (!StartServiceCtrlDispatcher(DispatchTable))
	{
		// logging To Do
		return;
	}
}
编写 ServiceMain 函数
“SvcMain”函数首先调用“RegisterServiceCtrlHandler”函数,将“CtrlHandler”函数注册为服务的 Handler 函数,并开始初始化。
接下来,“SvcMain”函数调用“ReportSvcStatus”函数来指示其初始状态为 SERVICE_START_PENDING。当服务处于此状态时,不接受任何控件。为了简化服务逻辑,建议服务在执行初始化时不要接受任何控件。
最后,“SvcMain”函数调用“SvcInit”函数来执行特定于服务的初始化并开始服务要执行的工作。
SERVICE_STATUS          gSvcStatus;
SERVICE_STATUS_HANDLE   gSvcStatusHandle;
VOID WINAPI SvcMain(DWORD dwArgc, LPTSTR *lpszArgv)
{
	// Register the handler function for the service
    	gSvcStatusHandle = RegisterServiceCtrlHandler(	SVCNAME, CtrlHandler);
	if (!gSvcStatusHandle)
	{
		// logging To-DO
		return;
	}
	// Set the  service type 
	gSvcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS;
	
	// Set the error code to return 
	//when an error occurs while the service is starting or stopping
	gSvcStatus.dwServiceSpecificExitCode = 0;
	// Report initial status to the SCM
	ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 3000);
	// Perform service-specific initialization and work.
	SvcInit(dwArgc, lpszArgv);
}
编写 SvcInit 函数
此函数向 SCM 报告运行状态,启动一个执行“Thread”函数的线程,使用“WaitForSingleObject”函数等待其结束,最后调用“ReportSvcStatus”函数指示服务已进入 SERVICE_STOPPED 状态,然后返回。
    VOID SvcInit(DWORD dwArgc, LPTSTR *lpszArgv)
{	
	// Report running status when initialization is complete.
	ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0);
	
	// Start the thread that will perform the main task of the service
	
	HANDLE hThread = CreateThread(NULL, 0, Thread, NULL, 0, NULL);
	// Check whether to stop the service.
	WaitForSingleObject(hThread, INFINITE);
	ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
}
一旦线程启动,它就执行“Thread”函数,该函数又调用注入代码。
“injection( )”函数首先调用“LsassId()”函数以获取 lsass 进程的 PID。PID 被传递给“OpenProcess”以获取 lsass 进程的句柄。使用此句柄,“VirtualAllocEx”和“WriteProcessMemory”分别分配空间并将 DLL 的名称(字符串“dump.dll”)写入目标进程。接下来,使用“GetProcAddress”获取指向“LoadLibrary”的地址。最后,使用三个重要参数调用“CreateRemoteThread”:到 lsass 进程的句柄(hp)、LoadLibrary 的地址(loadLibAddr)以及指向先前写入 lsass 进程的 DLL 名称的指针(baseAdd)。“CreateRemoteThread”允许具有适当权限的进程在远程进程中执行线程。
// The name of the DLL to inject
#define DLLTOINJECT  "C:\\dump.dll"
// The logging file name
#define LOGFILE  "C:\\checkSam.log
VOID injection()
{
	// create a log file
	FILE * log;
	
	DWORD pid; // pid of lsass process
	
	HANDLE hp = NULL; // a handle to lsass process
	
	LPSTR baseAdd = NULL; // base address of the allocated pages inside lsass process
	// open logging file 
	fopen_s(&log, LOGFILE, "w");
	// get pid of lsass process
	pid = LsassId();
	if (pid == 0){
		fprintf(log, "Can't find the pid of lsass process\n");
		fclose(log);
		return;
	}
	// try to open lsass process with all privileges
	hp = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
	if (hp == NULL){
		fprintf(log, "ERROR: (%d) while trying to 
		open lsass process (PID: %d)\n", GetLastError(), pid);
		fclose(log);
		return;
	}
	
	// create 
	baseAdd = (LPSTR)VirtualAllocEx(hp, NULL, 
	strlen(DLLTOINJECT), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	if (baseAdd == NULL)
	{
		fprintf(log, "ERROR:  (%d) while creating a vitual memory address 
		space inside lsass process (PID: %d)\n", GetLastError(), pid);
		fclose(log);
		return;
	}
	fprintf(log, "The base address inside lsass of %s is : %p\n", DLLTOINJECT, baseAdd);
	
	if (!WriteProcessMemory(hp, baseAdd, DLLTOINJECT, strlen(DLLTOINJECT), NULL)){
		fprintf(log, "ERROR : (%d) while trying to write inside 
		the committed address space in lsass process (PID: %d)\n", GetLastError(), pid);
		fclose(log);
		return;
	};
	// Get the real address of LoadLibraryW in Kernel32.dll
	PTHREAD_START_ROUTINE loadLibAddr = (PTHREAD_START_ROUTINE)GetProcAddress
	(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryA");
	if (loadLibAddr == NULL) {
		fprintf(log, "ERROR : (%d) while loading the loadLibrary function \n", GetLastError());
		fclose(log);
		return;
	}
	fprintf(log, "loadLibrary function is loaded at %p\n", loadLibAddr);
	if (!CreateRemoteThread(hp, NULL, 0, loadLibAddr, baseAdd, 0, NULL)){
		fprintf(log, "ERROR: (%d) occurred when attempting 
		to launch a thread in lsass process", GetLastError());
		fclose(log);
		return;
	}
	fprintf(log, "The DLL has been successfully injected in the lsass process...\n");
	//close the logging file
	fclose(log);
	
	return;
}
DWORD LsassId(){
	
	// Array that receives the list of process identifiers.
	DWORD ProcessList[1024];
	// The size of the ProcessList, in bytes
	DWORD cbNeeded;
	// The number of bytes returned in the ProcessList array
	DWORD cProcesses;
	
	TCHAR szProcessName[MAX_PATH] = TEXT("<unknown>");
	
	if (!EnumProcesses(ProcessList, sizeof(ProcessList), &cbNeeded))
	{
		return 0;
	}
	// Calculate how many process identifiers were returned.
		cProcesses = cbNeeded / sizeof(DWORD);
    // looking for the PID of lsass process
	for (unsigned int i = 0; i < cProcesses; i++)
	{
		if (ProcessList[i] != 0)
		{
			// Get a handle to the process.
			HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, 
			FALSE, 
			ProcessList[i]);
			// Get the process name.
			if (NULL != hProcess)
			{
				HMODULE hMod;
				DWORD cbNeeded;
				if (EnumProcessModules(hProcess, &hMod, sizeof(hMod),
					&cbNeeded))
				{
					GetModuleBaseName(hProcess, hMod, szProcessName,
						sizeof(szProcessName) / sizeof(TCHAR));
				}
			}
			// get the pid of lsass process if exists
			if (lstrcmp(szProcessName, TEXT("lsass.exe")) == 0)
			{				
				CloseHandle(hProcess);
				return ProcessList[i];
			}
		}
	}
	return 0;
}
ChkSamConfig.exe
此控制台程序允许用户启动和删除 checkSam 服务。
install、start 和 delete 函数共享相同的起始代码。它们首先调用“OpenSCManager”来打开 SCM 并获取句柄,然后它们获取所需服务的句柄。
    SC_HANDLE schSCManager;
	SC_HANDLE schService;
	SERVICE_STATUS ssStatus;
	// Get a handle to the SCM database. 
	schSCManager = OpenSCManager(
		NULL,                    // local computer
		NULL,                    // ServicesActive database 
		SC_MANAGER_ALL_ACCESS);  // full access rights 
	if (NULL == schSCManager)
	{
		printf("OpenSCManager failed (%d)\n", GetLastError());
		return;
	}
	// Get a handle to the service.
	schService = OpenService(
		schSCManager,       // SCM database 
		SvcName,          // name of service 
		DELETE);            // need delete access 
	if (schService == NULL)
	{
		printf("OpenService failed (%d)\n", GetLastError());
		CloseServiceHandle(schSCManager);
		return;
	}
	
	// start, stop, delete, ... code
	
	CloseServiceHandle(schService);
	CloseServiceHandle(schSCManager);
	
//...
安装服务函数
它首先获取服务的映像路径。为此,我们使用“GetModuleFileName”函数,并将第一个参数设置为 NULL。这表明“GetModuleFileName”检索当前进程可执行文件的路径。事实上,为了使代码尽可能简单,我们将 install 函数的代码包含在 checkSam 程序中。
...
TCHAR ServicePath[MAX_PATH];
if (!GetModuleFileName(NULL, ServicePath, MAX_PATH))
{
		printf("Cannot install service (%d)\n", GetLastError());
		return;
}
	
// open SCM database as presented above 
...	
// Create the service
schService = CreateService(
		schSCManager,              // SCM database 
		SVCNAME,                   // name of service 
		SVCNAME,                   // service name to display 
		SERVICE_ALL_ACCESS,        // desired access 
		SERVICE_WIN32_OWN_PROCESS, // service type 
		SERVICE_DEMAND_START,      // start type 
		SERVICE_ERROR_NORMAL,      // error control type 
		ServicePath,               // path to service's binary 
		NULL,                      // no load ordering group 
		NULL,                      // no tag identifier 
		NULL,                      // no dependencies 
		NULL,                      // LocalSystem account 
		NULL);                     // no password 
if (schService == NULL)
	{
		printf("CreateService failed (%d)\n", GetLastError());
		CloseServiceHandle(schSCManager);
		return;
	}
else printf("Service installed successfully\n");
// close handles
...
启动服务函数
在获取服务句柄后,我们检查服务的状态。如果服务正在运行,程序将返回。否则,我们等待服务退出“SERVICE_STOP_PENDING”状态,然后调用“StartService”函数启动服务。
    // ... see the ChkSamConfig source code
    
    // Attempt to start the service.
	if (!StartService(
		schService,  // handle to service 
		0,           // number of arguments 
		NULL))      // here dll name (LPCWSTR *)DllName
	{
		printf("StartService failed (%d)\n", GetLastError());
		CloseServiceHandle(schService);
		CloseServiceHandle(schSCManager);
		return;
	}
	else printf("Service start pending...\n");
	
	//... see the ChkSamConfig source code
删除服务函数
要删除服务,我们只需要获取对相关服务的句柄并调用“DeleteService”Windows 函数。
//...
// Delete the service.
	if (!DeleteService(schService))
	{
		printf("DeleteService failed (%d)\n", GetLastError());
	}
	else printf("Service deleted successfully\n");
//...
结论
在本文的第一部分,我们展示了如何将 DLL 注入到 lsass 进程。在本文的第二部分,我们将介绍如何创建 dump.dll、如何从 SAM 文件中提取哈希以及检测工作原理。
参考文献
- 
	Windows Internals, Sixth Edition, Part 1, Mark Russinovich, David A. Solomon, Alex Ionescu, Microsoft Press, 2012 
- 
	Practical Malware Analysis, The Hands-On Guide to Dissecting Malicious Software, Michael Sikorski and Andrew Honig, William Pollock, 2012 
- 
	The Art of Memory Forensics Detecting Malware and Threats in Windows, Linux, and Mac Memory, Michael Hale Ligh Andrew Case Jamie Levy AAron Walters, Wiley, 2014 
- https://developer.microsoft.com/fr-fr/windows/desktop/develop


