检测 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