病毒木马植入模块成功植入用户计算机之后,便会启动攻击模块来对用户计算机数据实施窃取和回传等操作。通常植入和攻击是分开在不同模块之中的,这里的模块指的是DLL、exe或其他加密的PE文件等。只有当前植入模块成功执行后,方可继续执行攻击模块,同时会删除植入模块的数据和文件。模块化开发的好处不单单是便于开发管理,同时也可以减小因某一模块的失败而导致整个程序暴露的可能性。
本章介绍了3种常用的病毒木马启动技术,它包括:
q 创建进程API:介绍使用WinExec、ShellExecute以及CreateProcess创建进程。
q 突破SESSION 0隔离创建进程:主要通过CreateProcessAsUser函数实现用户进程创建。
q 内存直接加载运行:模拟PE加载器,直接将DLL和exe等PE文件加载到内存并启动运行。
在一个进程中创建并启动一个新进程,无论是对于病毒木马程序还是普通的应用程序而言,这都是一个常见的技术,最简单的方法无非是直接通过调用WIN32 API函数创建新进程。用户层上,微软提供了WinExec、ShellExecute和CreateProcess等函数来实现进程创建。
WinExec、ShellExecute以及CreateProcess除了可以创建进程外,还能执行CMD命令等功能。接下来,本节将介绍使用WinExec、ShellExecute以及CreateProcess函数创建进程。
运行指定的应用程序。
函数声明
UINT WINAPI WinExec(
_In_ LPCTSTR lpCmdLine,
_In_ UINT uCmdShow)
参数
lpCmdLine [in]
要执行的应用程序的命令行。如果在lpCmdLine参数中可执行文件的名称不包含目录路径,则系统将按以下顺序搜索可执行文件:
应用程序的目录、当前目录、Windows系统目录、Windows目录以及PATH环境变量中列出的目录。
uCmdShow [in]
显示选项。SW_HIDE表示隐藏窗口并激活其他窗口;SW_SHOWNORMAL表示激活并显示一个窗口。
返回值
如果函数成功,则返回值大于31。
如果函数失败,则返回值是以下错误值之一。
值 | 含 义 |
---|---|
0 | 系统内存或资源不足 |
ERROR_BAD_FORMAT | exe文件无效 |
ERROR_FILE_NOT_FOUND | 找不到指定文件 |
ERROR_PATH_NOT_FOUND | 找不到指定的路径 |
运行一个外部程序(或者是打开一个已注册的文件、目录,或打印一个文件等),并对外部程序进行一定程度的控制。
函数声明
HINSTANCE ShellExecute(
_In_opt_ HWND hwnd,
_In_opt_ LPCTSTR lpOperation,
_In_ LPCTSTR lpFile,
_In_opt_ LPCTSTR lpParameters,
_In_opt_ LPCTSTR lpDirectory,
_In_ INT nShowCmd)
参数
hwnd [in, optional]
用于显示UI或错误消息的父窗口的句柄。如果操作不与窗口关联,则此值可以为NULL。
lpOperation [in, optional]
指向以空字符结尾的字符串的指针,它在本例中称为动词,用于指定要执行的操作。常使用的动词有:
edit:启动编辑器并打开文档进行编辑。如果lpFile不是文档文件,则该函数将失败。
explore:探索由lpFile指定的文件夹。
find:在由lpDirectory指定的目录中启动搜索。
open:打开由lpFile参数指定的项目。该项目可以是文件也可是文件夹。
print:打印由lpFile指定的文件。如果lpFile不是文档文件,则该函数失败。
NULL:如果可用,则使用默认动词。如果不可用,则使用“打开”动词。如果两个动词都不可用,则系统使用注册表中列出的第一个动词。
lpFile [in]
指向以空字符结尾的字符串的指针,该字符串要在其上执行指定谓词的文件或对象。要指定一个Shell名称空间对象,传递完全限定的解析名称。如果lpDirectory参数使用相对路径,则lpFile不要使用相对路径。
lpParameters [in, optional]
如果lpFile指定一个可执行文件,则此参数是一个指向以空字符结尾的字符串的指针,该字符串指定要传递给应用程序的参数。如果lpFile指定一个文档文件,则lpParameters应该为NULL。
lpDirectory [in, optional]
指向以空终止的字符串的指针,该字符串指定操作的默认目录。如果此值为NULL,则使用当前的工作目录。如果在lpFile中提供了相对路径,请不要对lpDirectory使用相对路径。
nShowCmd [in]
指定应用程序在打开时如何显示标志。SW_HIDE表示隐藏窗口并激活其他窗口;SW_SHOWNORMAL表示激活并显示一个窗口。
返回值
如果函数成功,则返回大于32的值。如果该函数失败,则它将返回一个错误值,指示失败的原因。
创建一个新进程及主线程。新进程在调用进程的安全的上下文中运行。
函数声明
BOOL WINAPI CreateProcess(
_In_opt_ LPCTSTR lpApplicationName,
_Inout_opt_ LPTSTR lpCommandLine,
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ BOOL bInheritHandles,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCTSTR lpCurrentDirectory,
_In_ LPSTARTUPINFO lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation)
参数
lpApplicationName [in, optional]
要执行的模块的名称。lpApplicationName参数可以是NULL。要运行批处理文件,必须启动命令解释程序,并将lpApplicationName设置为cmd.exe。
lpCommandLine [in, out, optional]
要执行的命令行。lpCommandLine参数可以是NULL。在这种情况下,该函数使用由lpApplicationName指向的字符串作为命令行。如果lpApplicationName和lpCommandLine都不为NULL,则由lpApplicationName指向的以空字符结尾的字符串会指定要执行的模块,并且由lpCommandLine指向的以空字符结尾的字符串会指定命令行。
lpProcessAttributes [in, optional]
指向SECURITY_ATTRIBUTES结构的指针,用于确定是否可以由子进程继承返回的新进程对象的句柄。如果lpProcessAttributes为NULL,则不能继承句柄。
lpThreadAttributes [in, optional]
指向SECURITY_ATTRIBUTES结构的指针,用于确定是否可以由子进程继承返回的新线程对象的句柄。如果lpThreadAttributes为NULL,则不能继承句柄。
bInheritHandles [in]
如果此参数为TRUE,则调用进程中的每个可继承句柄都将由新进程来继承。如果该参数为FALSE,则不会继承句柄。
dwCreationFlags [in]
控制优先级和创建进程的标志。例如,CREATE_NEW_CONSOLE表示新进程将使用一个新控制台,而不是继承父进程的控制台。CREATE_SUSPENDED表示新进程的主线程会以暂停的状态来创建,直到调用ResumeThread函数时才运行。
lpEnvironment [in, optional]
指向新进程的环境块的指针。如果此参数为NULL,则新进程将使用调用进程的环境。
lpCurrentDirectory [in, optional]
指向进程当前目录的完整路径。该字符串还可以指定UNC路径。如果此参数为NULL,则新进程将具有与调用进程相同的当前驱动器和目录。
lpStartupInfo [in]
指向STARTUPINFO或STARTUPINFOEX结构的指针。STARTUPINFO或STARTUPINFOEX中的句柄在不需要时必须由CloseHandle关闭。
lpProcessInformation [out]
指向PROCESS_INFORMATION结构的指针,用于接收有关新进程的标识信息。PROCESS_INFORMATION中的句柄必须在不需要时由CloseHandle关闭。
返回值
如果函数成功,则返回值非零。
如果函数失败,则返回值为零。
直接调用WinExec函数创建进程,具体的实现代码如下所示。
BOOL WinExec_Test(char *pszExePath, UINT uiCmdShow)
{
UINT uiRet = 0;
uiRet = ::WinExec(pszExePath, uiCmdShow);
if (31 < uiRet)
{
return TRUE;
}
return FALSE;
}
在上述代码中,WinExec函数只有两个参数,第一个参数指定程序路径或者CMD命令行,第二个参数指定显示方式。若返回值大于31,则表示WinExec执行成功,否则执行失败。
直接调用ShellExecute函数创建进程,具体的实现代码如下所示。
BOOL ShellExecute_Test(char *pszExePath, UINT uiCmdShow)
{
HINSTANCE hInstance = 0;
hInstance = ::ShellExecute(NULL, NULL, pszExePath, NULL, NULL, uiCmdShow);
if (32 < (DWORD)hInstance)
{
return TRUE;
}
return FALSE;
}
ShellExecute函数不仅可以运行exe文件,也可以运行已经关联的文件。例如,可以打开网页、发送邮件、以默认程序打开文件、打开目录、打印文件等。若返回值大于32,则表示执行成功,否则执行失败。
直接调用CreateProcess函数创建进程,具体的实现代码如下所示。
BOOL CreateProcess_Test(char *pszExePath, UINT uiCmdShow)
{
STARTUPINFO si = { 0 };
PROCESS_INFORMATION pi;
BOOL bRet = FALSE;
si.cb = sizeof(si);
si.dwFlags = STARTF_USESHOWWINDOW; //指定wShowWindow成员有效
si.wShowWindow = uiCmdShow;
bRet = ::CreateProcess(NULL, pszExePath, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
if (bRet)
{
//不使用的句柄最好关掉
::CloseHandle(pi.hThread);
::CloseHandle(pi.hProcess);
return TRUE;
}
return FALSE;
}
与WinExec以及ShellExecute函数相比较而言,CreateProcess函数的参数更多,使用起来更复杂。我们着重关注以下5个参数:执行模块名称的参数lpApplicationName、执行命令行的参数lpCommandLine、控制进程优先级和创建进程标志的参数dwCreationFlags、指向STARTUPINFO信息结构的参数lpStartupInfo,以及指向PROCESS_INFORMATION信息结构的参数lpProcessInformation。若CreateProcess函数执行成功,则返回TRUE,否则返回FALSE。
程序分别调用WinExec函数、ShellExecute函数,以及CreateProcess函数来创建1.exe、2.exe以及3.exe进程,并以SW_SHOWNORMAL方式显示程序窗口。直接运行上述程序,程序提示1.exe、2.exe以及3.exe进程成功创建并运行,如图4-1所示。
图4-1 创建进程
本小节主要通过调用WinExec函数、ShellExecute函数,以及CreateProcess函数来创建进程,实现程序的关键是对函数参数的理解。其中,除了进程路径参数较为重要之外,窗口显示方式也值得注意。
对WinExec和ShellExecute函数设置为SW_HIDE方式可隐藏运行程序窗口,并且成功隐藏执行CMD命令行的窗口,对于其他程序窗口不能成功隐藏。而CreateProcess函数在指定窗口显示方式的时候,需要在STARTUPINFO结构体中将启用标志设置为STARTF_ USESHOWWINDOW,表示wShowWindow成员显示方式有效。然后将wShowWindow置为SW_HIDE隐藏窗口,创建方式为CREATE_NEW_CONSOLE创建一个新控制台,这样可以成功隐藏执行CMD命令行的窗口,而其他程序窗口则不能成功隐藏。
如果在一个进程中想要创建以隐藏方式运行的进程,即隐藏进程窗口,则可以通过SendMessage向窗口发送SW_HIDE隐藏消息,也可以通过ShowWindow函数设置SW_HIDE来使窗口隐藏。这两种实现方式的前提是已获取了窗口的句柄。
WinExec只用于可执行文件,虽然使用方便,但是一个老函数。ShellExcute可通过Windows外壳打开任意文件,非可执行文件自动通过关联程序打开对应的可执行文件,区别不大,不过ShellExcute可以指定运行时的工作路径。WinExec必须得到GetMessage或超时之后才返回,而ShellExecute和CreateProcess都是无需等待直接返回的。
用户层上,通常是利用WMI或者通过HOOK API来监控进程的创建。EnumWindows函数可以枚举所有屏幕上的顶层窗口,包括隐藏窗口。
病毒木马通常会把自己注入系统服务进程或是伪装成系统服务进程,并运行在SESSION 0中。处于SESSION 0中的程序能正常执行普通程序的绝大部分操作,但是个别操作除外。例如,处于SESSION 0中的系统服务进程,无法与普通用户进程通信,不能通过Windows消息机制进行通信,更不能创建普通的用户进程。
在Windows XP、Windows Server 2003,以及更老版本的Windows操作系统中,服务和应用程序使用相同的会话(SESSION)来运行,而这个会话是由第一个登录到控制台的用户来启动的,该会话就称为SESSION 0。将服务和用户应用程序一起在SESSION 0中运行会导致安全风险,因为服务会使用提升后的权限来运行,而用户应用程序使用用户特权(大部分都是非管理员用户)运行,这会使得恶意软件把某个服务作为攻击目标,通过“劫持”该服务以达到提升自己权限级别的目的。
从Windows VISTA开始,只有服务可以托管到SESSION 0中,用户应用程序和服务之间会进行隔离,并需要运行在用户登录系统时创建的后续会话中。如第一个登录用户创建 Session 1,第二个登录用户创建Session 2,以此类推。
使用不同会话运行的实体(应用程序或服务)如果不将自己明确标注为全局命名空间,并提供相应的访问控制设置,那么将无法互相发送消息,共享UI元素或共享内核对象。
虽然Windows 7及以上版本的SESSION 0给服务层和应用层间的通信造成了很大的难度,但这并不代表没有办法实现服务层与应用层间的通信与交互。微软提供了一系列以WTS(Windows Terminal Service,Windows终端服务)开头的函数,从而可以完成服务层与应用层的交互。
接下来,本节将介绍突破SESSION 0隔离,在服务程序中创建用户桌面进程。
检索控制台会话的标识符Session Id。控制台会话是当前连接到物理控制台的会话。
函数声明
DWORD WTSGetActiveConsoleSessionId(void)
参数
无参数
返回值
如果执行成功,则返回连接到物理控制台的会话标识符。
如果没有连接到物理控制台的会话(例如,物理控制台会话正在附加或分离),则此函数返回0xFFFFFFFF。
获取由Session Id指定的登录用户的主访问令牌。要想成功调用此功能,则调用应用程序必须在本地系统账户的上下文中运行,并具有SE_TCB_NAME特权。
函数声明
BOOL WTSQueryUserToken(
_In_ ULONG SessionId,
_Out_ PHANDLE phToken)
参数
SessionId [in]
远程桌面服务会话标识符。在服务上下文中运行的任何程序都将具有一个值为0的会话标 识符。
phToken [out]
如果该功能成功,则会收到一个指向登录用户令牌句柄的指针。请注意,必须调用CloseHandle函数才能关闭该句柄。
返回值
如果函数成功,则返回值非零,phToken参数指向用户的主令牌;如果函数失败,则返回值为零。
创建一个新的访问令牌,它与现有令牌重复。此功能可以创建主令牌或模拟令牌。
函数声明
BOOL WINAPI DuplicateTokenEx(
_In_ HANDLE hExistingToken,
_In_ DWORD dwDesiredAccess,
_In_opt_ LPSECURITY_ATTRIBUTES lpTokenAttributes,
_In_ SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
_In_ TOKEN_TYPE TokenType,
_Out_ PHANDLE phNewToken)
参数
hExistingToken [in]
使用TOKEN_DUPLICATE访问权限打开访问令牌的句柄。
dwDesiredAccess [in]
指定新令牌的请求访问权限。要想请求对调用者有效的所有访问权限,请指定MAXIMUM_ ALLOWED。
lpTokenAttributes [in,optional]
指向SECURITY_ATTRIBUTES结构的指针,该结构指定新令牌的安全描述符,并确定子进程是否可以继承令牌。如果lpTokenAttributes为NULL,则令牌获取默认的安全描述符,并且不能继承该句柄。
ImpersonationLevel [in]
指定SECURITY_IMPERSONATION_LEVEL枚举中指示新令牌模拟级别的值。
TokenType [in]
从TOKEN_TYPE枚举中指定以下值之一。
值 | 含 义 |
---|---|
TokenPrimary | 新令牌是可以在CreateProcessAsUser函数中使用的主令牌 |
TokenImpersonation | 新令牌是一个模拟令牌 |
phNewToken [out]
指向接收新令牌的HANDLE变量的指针。新令牌使用完成后,调用CloseHandle函数来关闭令牌句柄。
返回值
如果函数成功,则函数将返回一个非零值;
如果函数失败,则返回值为零。
检索指定用户的环境变量,然后可以将此块传递给CreateProcessAsUser函数。
函数声明
BOOL WINAPI CreateEnvironmentBlock(
_Out_ LPVOID *lpEnvironment,
_In_opt_ HANDLE hToken,
_In_ BOOL bInherit)
参数
lpEnvironment [out]
当该函数返回时,已接收到指向新环境块的指针。
hToken [in,optional]
Logon为用户,从LogonUser函数返回。如果这是主令牌,则令牌必须具有TOKEN_QUERY和TOKEN_DUPLICATE访问权限。如果令牌是模拟令牌,则必须具有TOKEN_QUERY权限。如果此参数为NULL,则返回的环境块仅包含系统变量。
bInherit[in]
指定是否可以继承当前进程的环境。如果该值为TRUE,则该进程将继承当前进程的环境;如果此值为FALSE,则该进程不会继承当前进程的环境。
返回值
如果函数成功,则函数将返回TRUE;如果函数失败,则返回FALSE。
创建一个新进程及主线程,新进程在由指定令牌表示的用户安全上下文中运行。
函数声明
BOOL WINAPI CreateProcessAsUser(
_In_opt_ HANDLE hToken,
_In_opt_ LPCTSTR lpApplicationName,
_Inout_opt_ LPTSTR lpCommandLine,
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ BOOL bInheritHandles,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCTSTR lpCurrentDirectory,
_In_ LPSTARTUPINFO lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation)
参数
hToken [in,optional]
表示用户主令牌的句柄。句柄必须具有TOKEN_QUERY、TOKEN_DUPLICATE和TOKEN_ASSIGN_PRIMARY访问权限。
lpApplicationName [in,optional]
要执行模块的名称。该模块可以基于Windows应用程序。
lpCommandLine [in,out,optional]
要执行的命令行。 该字符串的最大长度为32K个字符。 如果lpApplicationName为NULL,则lpCommandLine模块名称的长度限制为MAX_PATH个字符。
lpProcessAttributes [in,optional]
指向SECURITY_ATTRIBUTES结构的指针,该结构指定新进程对象的安全描述符,并确定子进程是否可以继承返回进程的句柄。如果lpProcessAttributes为NULL或lpSecurityDescriptor为NULL,则该进程将获得默认的安全描述符,并且不能继承该句柄。
lpThreadAttributes [in,optional]
指向SECURITY_ATTRIBUTES结构的指针,该结构指定新线程对象的安全描述符,并确定子进程是否可以继承返回线程的句柄。如果lpThreadAttributes为NULL或lpSecurityDescriptor为NULL,则线程将获取默认的安全描述符,并且不能继承该句柄。
bInheritHandles [in]
如果此参数为TRUE,则调用进程中的每个可继承句柄都由新进程继承;如果参数为FALSE,则不能继承句柄。请注意,继承的句柄具有与原始句柄相同的值和访问权限。
dwCreationFlags [in]
控制优先级和进程创建的标志。
lpEnvironment [in,optional]
指向新进程环境块的指针。如果此参数为NULL,则新进程将使用调用进程的环境。
lpCurrentDirectory [in,optional]
指向进程当前目录的完整路径。如果此参数为NULL,则新进程将具有与调用进程相同的当前驱动器和目录。
lpStartupInfo [in]
指向STARTUPINFO或STARTUPINFOEX结构的指针。用户必须具有对指定窗口站和桌面的完全访问权限。
lpProcessInformation [out]
指向一个PROCESS_INFORMATION结构的指针,用于接收新进程的标识信息。PROCESS_INFORMATION中的句柄必须在不需要时使用CloseHandle关闭。
返回值
如果函数成功,则函数将返回一个非零值;如果函数失败,则返回零。
由于SESSION 0的隔离,使得在系统服务进程内不能直接调用CreateProcess等函数创建进程,而只能通过CreateProcessAsUser函数来创建。这样,创建的进程才会显示UI界面,与用户进行交互。
在SESSION 0中创建用户桌面进程具体的实现流程如下所示。
首先,调用WTSGetActiveConsoleSessionId函数来获取当前程序的会话ID,即Session Id。调用该函数不需要任何参数,直接返回Session Id。根据Session Id继续调用WTSQueryUserToken函数来检索用户令牌,并获取对应的用户令牌句柄。在不需要使用用户令牌句柄时,可以调用CloseHandle函数来释放句柄。
其次,使用DuplicateTokenEx函数创建一个新令牌,并复制上面获取的用户令牌。设置新令牌的访问权限为MAXIMUM_ALLOWED,这表示获取所有令牌权限。新访问令牌的模拟级别为SecurityIdentification,而且令牌类型为TokenPrimary,这表示新令牌是可以在CreateProcessAsUser函数中使用的主令牌。
最后,根据新令牌调用CreateEnvironmentBlock函数创建一个环境块,用来传递给CreateProcessAsUser使用。在不需要使用进程环境块时,可以通过调用DestroyEnvironmentBlock函数进行释放。获取环境块后,就可以调用CreateProcessAsUser来创建用户桌面进程。CreateProcessAsUser函数的用法以及参数含义与CreateProcess函数的用法和参数含义类似。新令牌句柄作为用户主令牌的句柄,指定创建进程的路径,设置优先级和创建标志,设置STARTUPINFO结构信息,获取PROCESS_INFORMATION结构信息。
经过上述操作后,就完成了用户桌面进程的创建。但是,上述方法创建的用户桌面进程并没有继承服务程序的系统权限,只具有普通权限。要想创建一个有系统权限的子进程,这可以通过设置进程访问令牌的安全描述符来实现,具体的实现步骤在此就不详细介绍了。
// 突破SESSION 0隔离创建用户进程
BOOL CreateUserProcess(char *lpszFileName)
{
// 变量 (略)
do
{
// 获得当前Session Id
dwSessionID = ::WTSGetActiveConsoleSessionId();
// 获得当前会话的用户令牌
if (FALSE == ::WTSQueryUserToken(dwSessionID, &hToken))
{
ShowMessage("WTSQueryUserToken", "ERROR");
bRet = FALSE;
break;
}
// 复制令牌
if (FALSE == ::DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL,
SecurityIdentification, TokenPrimary, &hDuplicatedToken))
{
ShowMessage("DuplicateTokenEx", "ERROR");
bRet = FALSE;
break;
}
// 创建用户会话环境
if (FALSE == ::CreateEnvironmentBlock(&lpEnvironment,
hDuplicatedToken, FALSE))
{
ShowMessage("CreateEnvironmentBlock", "ERROR");
bRet = FALSE;
break;
}
// 在复制的用户会话下执行应用程序,创建进程
if (FALSE == ::CreateProcessAsUser(hDuplicatedToken,
lpszFileName, NULL, NULL, NULL, FALSE,
NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT,
lpEnvironment, NULL, &si, &pi))
{
ShowMessage("CreateProcessAsUser", "ERROR");
bRet = FALSE;
break;
}
} while (FALSE);
// 关闭句柄,释放资源 (略)
return bRet;
}
因为程序要实现的是突破SESSION 0隔离,所以,在系统服务程序中创建用户桌面进程。程序必须注册成为一个系统服务进程,这样才处于SESSION 0中。服务程序的入口点与普通程序的入口点不同,需要通过调用函数StartServiceCtrlDispatcher来设置服务入口点函数。对于创建服务程序的内容本书没有进行具体讲解,读者可以阅读配套的示例代码来理解该部分内容。同时,本书还开发了一个服务加载器ServiceLoader.exe(该加载器的源码可以在相应章节的配套示例代码中找到),它可将测试程序加载为服务进程。
在main函数中,设置服务入口点函数,使之成为服务程序,并在服务程序中调用上述封装好的函数进行测试。首先,以管理员身份运行服务加载器ServiceLoader.exe,这样服务加载器会将CreateProcessAsUser_Test.exe程序加载为服务进程,从而执行创建用户进程的代码。服务加载器提示创建和启动服务成功后,立即显示对话框和启动“520.exe”程序,而且窗口界面也成功显示,如图4-2所示。
图4-2 成功创建520.exe进程
然后,使用进程查看器ProcessExplorer.exe查看CreateProcessAsUser_Test.exe进程以及520.exe进程中的SESSION值,如图4-3所示,CreateProcessAsUser_Test.exe进程处于SESSION 0中,而520.exe处于SESSION 1中。
图4-3 SESSION信息
突破SESSION 0隔离创建用户进程,要求程序处于SESSION 0中,这样才会有效。创建服务程序时,需要在 main函数中设置服务程序入口点函数,这样才能成功地为程序创建系统服务。该程序实现的关键是调用CreateProcessAsUser函数。需要程序创建并复制一个新的访问令牌,并获取访问令牌的进程环境块信息。
由于本节介绍的方法并没有对进程访问令牌进行设置,所以创建出来的用户桌面进程是用户默认的权限,并没有继承系统权限。
可以通过挂钩CreateProcessAsUser函数监控进程创建。
有很多病毒木马都具有模拟PE加载器的功能,它们把DLL或者exe等PE文件从内存中直接加载到病毒木马的内存中去执行,不需要通过LoadLibrary等现成的API函数去操作,以此躲过杀毒软件的拦截检测。
这种技术当然有积极的一面。假如程序需要动态调用DLL文件,内存加载运行技术可以把这些DLL作为资源插入到自己的程序中。此时直接在内存中加载运行即可,不需要再将DLL释放到本地。
本节主要针对DLL和exe这两种PE文件进行介绍,分别剖析如何直接从内存中加载运行。这两种文件具体的实现原理相同,只需掌握其中一种,另一种也就容易掌握了。
要想完全理解透彻内存直接加载运行技术,需要对PE文件结构有比较详细的了解,至少要了解PE格式的导入表、导出表以及重定位表的具体操作过程。因为内存直接加载运行技术的核心就是模拟PE加载器加载PE文件的过程,也就是对导入表、导出表以及重定位表的操作过程。
那么程序需要进行哪些操作便可以直接从内存中加载运行DLL或是exe文件呢?以加载DLL为例介绍。
首先就是要把DLL文件按照映像对齐大小映射到内存中,切不可直接将DLL文件数据存储到内存中。因为根据PE结构的基础知识可知,PE文件有两个对齐字段,一个是映像对齐大小SectionAlignment,另一个是文件对齐大小FileAlignment。其中,映像对齐大小是PE文件加载到内存中所用的对齐大小,而文件对齐大小是PE文件存储在本地磁盘所用的对齐大小。一般文件对齐大小会比映像对齐大小要小,这样文件会变小,以此节省磁盘空间。
然而,成功映射内存数据之后,在DLL程序中会存在硬编码数据,硬编码都是以默认的加载基址作为基址来计算的。由于DLL可以任意加载到其他进程空间中,所以DLL的加载基址并非固定不变。当改变加载基址的时候,硬编码也要随之改变,这样DLL程序才会计算正确。但是,如何才能知道需要修改哪些硬编码呢?换句话说,如何知道硬编码的位置?答案就藏在PE结构的重定位表中,重定位表记录的就是程序中所有需要修改的硬编码的相对偏移位置。
根据重定位表修改硬编码数据后,这只是完成了一半的工作。DLL作为一个程序,自然也会调用其他库函数,例如MessageBox。那么DLL如何知道MessageBox函数的地址呢?它只有获取正确的调用函数地址后,方可正确调用函数。PE结构使用导入表来记录PE程序中所有引用的函数及其函数地址。在DLL映射到内存之后,需要根据导入表中的导入模块和函数名称来获取调用函数的地址。若想从导入模块中获取导出函数的地址,最简单的方式是通过GetProcAddress函数来获取。但是为了避免调用敏感的WIN32 API函数而被杀软拦截检测,本书采用直接遍历PE结构导出表的方式来获取导出函数地址,这要求读者熟悉导出表的具体操作原理。
完成上述操作之后,DLL加载工作才算完成,接下来便是获取入口地址并跳转执行以便完成启动。
具体的实现流程总结如下。
首先,在DLL文件中,根据PE结构获取其加载映像的大小SizeOfImage,并根据SizeOfImage在自己的程序中申请可读、可写、可执行的内存,那么这块内存的首地址就是DLL的加载基址。
其次,根据DLL中的PE结构获取其映像对齐大小SectionAlignment,然后把DLL文件数据按照SectionAlignment复制到上述申请的可读、可写、可执行的内存中。
接下来,根据PE结构的重定位表,重新对重定位表进行修正。
然后,根据PE结构的导入表,加载所需的DLL,并获取导入函数的地址并写入导入表中。
接着,修改DLL的加载基址ImageBase。
最后,根据PE结构获取DLL的入口地址,然后构造并调用 DllMain函数,实现DLL加载。
而exe文件相对于DLL文件实现原理唯一的区别就在于构造入口函数的差别,exe不需要构造DllMain函数,而是根据PE结构获取exe的入口地址偏移AddressOfEntryPoint并计算出入口地址,然后直接跳转到入口地址处执行即可。
要特别注意的是,对于exe文件来说,重定位表不是必需的,即使没有重定位表,exe也可正常运行。因为对于exe进程来说,进程最早加载的模块是exe模块,所以它可以按照默认的加载基址加载到内存。对于那些没有重定位表的程序,只能把它加载到默认的加载基址上。如果默认加载基址已被占用,则直接内存加载运行会失败。
// 模拟LoadLibrary加载内存DLL文件到进程中
LPVOID MmLoadLibrary(LPVOID lpData, DWORD dwSize)
{
LPVOID lpBaseAddress = NULL;
// 获取镜像大小
DWORD dwSizeOfImage = GetSizeOfImage(lpData);
// 在进程中申请一个可读、可写、可执行的内存块
lpBaseAddress = ::VirtualAlloc(NULL, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (NULL == lpBaseAddress)
{
ShowError("VirtualAlloc");
return NULL;
}
::RtlZeroMemory(lpBaseAddress, dwSizeOfImage);
// 将内存DLL数据按SectionAlignment大小对齐映射到进程内存中
if (FALSE == MmMapFile(lpData, lpBaseAddress))
{
ShowError("MmMapFile");
return NULL;
}
// 修改PE文件的重定位表信息
if (FALSE == DoRelocationTable(lpBaseAddress))
{
ShowError("DoRelocationTable");
return NULL;
}
// 填写PE文件的导入表信息
if (FALSE == DoImportTable(lpBaseAddress))
{
ShowError("DoImportTable");
return NULL;
}
//修改页属性, 统一设置成属性PAGE_EXECUTE_READWRITE
DWORD dwOldProtect = 0;
if (FALSE == ::VirtualProtect(lpBaseAddress, dwSizeOfImage, PAGE_EXECUTE_READWRITE, &dwOldProtect))
{
ShowError("VirtualProtect");
return NULL;
}
// 修改PE文件的加载基址IMAGE_NT_HEADERS.OptionalHeader.ImageBase
if (FALSE == SetImageBase(lpBaseAddress))
{
ShowError("SetImageBase");
return NULL;
}
// 调用DLL的入口函数DllMain,函数地址即为PE文件的入口点AddressOfEntryPoint
if (FALSE == CallDllMain(lpBaseAddress))
{
ShowError("CallDllMain");
return NULL;
}
return lpBaseAddress;
}
由于篇幅有限,对于如何修改重定位表、导入表以及遍历导出表等操作在此就不详细说明,读者直接阅读配套代码即可,在配套代码中均有详细的注释说明。
直接内存加载运行TestDll.dll文件,若成功执行TestDll.dll入口处的弹窗代码,弹窗提示则说明加载运行成功,如图4-4所示。
图4-4 TestDll.dll弹窗提示
这个程序对于初学者来说,理解起来比较复杂。但是,只要熟悉PE结构,这个程序理解起来就会容易得多。对于重定位表、导入表,以及导出表部分的具体操作并没有详细讲解。如果没有了解PE结构,那么理解起来会有些困难;如果了解了PE结构,那么就很容易理解该部分知识。
可以通过暴力枚举PE结构特征头的方法,来枚举进程中加载的所有模块,它与通过正常方法获取到的模块信息进行比对,从而判断是否存在可疑的PE文件。