Windows黑客编程技术详解 --第四章 木马启动技术(内含赠书福利)

病毒木马植入模块成功植入用户计算机之后,便会启动攻击模块来对用户计算机数据实施窃取和回传等操作。通常植入和攻击是分开在不同模块之中的,这里的模块指的是DLL、exe或其他加密的PE文件等。只有当前植入模块成功执行后,方可继续执行攻击模块,同时会删除植入模块的数据和文件。模块化开发的好处不单单是便于开发管理,同时也可以减小因某一模块的失败而导致整个程序暴露的可能性。

本章介绍了3种常用的病毒木马启动技术,它包括:

q 创建进程API:介绍使用WinExec、ShellExecute以及CreateProcess创建进程。

q 突破SESSION 0隔离创建进程:主要通过CreateProcessAsUser函数实现用户进程创建。

q 内存直接加载运行:模拟PE加载器,直接将DLL和exe等PE文件加载到内存并启动运行。

4.1 创建进程API

在一个进程中创建并启动一个新进程,无论是对于病毒木马程序还是普通的应用程序而言,这都是一个常见的技术,最简单的方法无非是直接通过调用WIN32 API函数创建新进程。用户层上,微软提供了WinExec、ShellExecute和CreateProcess等函数来实现进程创建。

WinExec、ShellExecute以及CreateProcess除了可以创建进程外,还能执行CMD命令等功能。接下来,本节将介绍使用WinExec、ShellExecute以及CreateProcess函数创建进程。

4.1.1 函数介绍

1.WinExec函数

运行指定的应用程序。

函数声明

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

找不到指定的路径

2.ShellExecute函数

运行一个外部程序(或者是打开一个已注册的文件、目录,或打印一个文件等),并对外部程序进行一定程度的控制。

函数声明

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的值。如果该函数失败,则它将返回一个错误值,指示失败的原因。

3.CreateProcess函数

创建一个新进程及主线程。新进程在调用进程的安全的上下文中运行。

函数声明

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关闭。

返回值

如果函数成功,则返回值非零。

如果函数失败,则返回值为零。

4.1.2 实现过程

直接调用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。

4.1.3 测试

程序分别调用WinExec函数、ShellExecute函数,以及CreateProcess函数来创建1.exe、2.exe以及3.exe进程,并以SW_SHOWNORMAL方式显示程序窗口。直接运行上述程序,程序提示1.exe、2.exe以及3.exe进程成功创建并运行,如图4-1所示。

图4-1 创建进程

4.1.4 小结

本小节主要通过调用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函数可以枚举所有屏幕上的顶层窗口,包括隐藏窗口。

4.2 突破SESSION 0隔离创建用户进程

病毒木马通常会把自己注入系统服务进程或是伪装成系统服务进程,并运行在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隔离,在服务程序中创建用户桌面进程。

4.2.1 函数介绍

1.WTSGetActiveConsoleSessionId函数

检索控制台会话的标识符Session Id。控制台会话是当前连接到物理控制台的会话。

函数声明

DWORD WTSGetActiveConsoleSessionId(void)

参数

无参数

返回值

如果执行成功,则返回连接到物理控制台的会话标识符。

如果没有连接到物理控制台的会话(例如,物理控制台会话正在附加或分离),则此函数返回0xFFFFFFFF。

2.WTSQueryUserToken函数

获取由Session Id指定的登录用户的主访问令牌。要想成功调用此功能,则调用应用程序必须在本地系统账户的上下文中运行,并具有SE_TCB_NAME特权。

函数声明

BOOL WTSQueryUserToken(

_In_ ULONG SessionId,

_Out_ PHANDLE phToken)

参数

SessionId [in]

远程桌面服务会话标识符。在服务上下文中运行的任何程序都将具有一个值为0的会话标 识符。

phToken [out]

如果该功能成功,则会收到一个指向登录用户令牌句柄的指针。请注意,必须调用CloseHandle函数才能关闭该句柄。

返回值

如果函数成功,则返回值非零,phToken参数指向用户的主令牌;如果函数失败,则返回值为零。

3.DuplicateTokenEx函数

创建一个新的访问令牌,它与现有令牌重复。此功能可以创建主令牌或模拟令牌。

函数声明

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函数来关闭令牌句柄。

返回值

如果函数成功,则函数将返回一个非零值;

如果函数失败,则返回值为零。

4.CreateEnvironmentBlock函数

检索指定用户的环境变量,然后可以将此块传递给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。

5.CreateProcessAsUser函数

创建一个新进程及主线程,新进程在由指定令牌表示的用户安全上下文中运行。

函数声明

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关闭。

返回值

如果函数成功,则函数将返回一个非零值;如果函数失败,则返回零。

4.2.2 实现原理

由于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结构信息。

经过上述操作后,就完成了用户桌面进程的创建。但是,上述方法创建的用户桌面进程并没有继承服务程序的系统权限,只具有普通权限。要想创建一个有系统权限的子进程,这可以通过设置进程访问令牌的安全描述符来实现,具体的实现步骤在此就不详细介绍了。

4.2.3 编码实现

// 突破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;

}

4.2.4 测试

因为程序要实现的是突破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信息

4.2.5 小结

突破SESSION 0隔离创建用户进程,要求程序处于SESSION 0中,这样才会有效。创建服务程序时,需要在 main函数中设置服务程序入口点函数,这样才能成功地为程序创建系统服务。该程序实现的关键是调用CreateProcessAsUser函数。需要程序创建并复制一个新的访问令牌,并获取访问令牌的进程环境块信息。

由于本节介绍的方法并没有对进程访问令牌进行设置,所以创建出来的用户桌面进程是用户默认的权限,并没有继承系统权限。

安全小贴士

可以通过挂钩CreateProcessAsUser函数监控进程创建。

4.3 内存直接加载运行

有很多病毒木马都具有模拟PE加载器的功能,它们把DLL或者exe等PE文件从内存中直接加载到病毒木马的内存中去执行,不需要通过LoadLibrary等现成的API函数去操作,以此躲过杀毒软件的拦截检测。

这种技术当然有积极的一面。假如程序需要动态调用DLL文件,内存加载运行技术可以把这些DLL作为资源插入到自己的程序中。此时直接在内存中加载运行即可,不需要再将DLL释放到本地。

本节主要针对DLL和exe这两种PE文件进行介绍,分别剖析如何直接从内存中加载运行。这两种文件具体的实现原理相同,只需掌握其中一种,另一种也就容易掌握了。

4.3.1 实现原理

要想完全理解透彻内存直接加载运行技术,需要对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模块,所以它可以按照默认的加载基址加载到内存。对于那些没有重定位表的程序,只能把它加载到默认的加载基址上。如果默认加载基址已被占用,则直接内存加载运行会失败。

4.3.2 编码实现

// 模拟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;

}

由于篇幅有限,对于如何修改重定位表、导入表以及遍历导出表等操作在此就不详细说明,读者直接阅读配套代码即可,在配套代码中均有详细的注释说明。

4.3.3 测试

直接内存加载运行TestDll.dll文件,若成功执行TestDll.dll入口处的弹窗代码,弹窗提示则说明加载运行成功,如图4-4所示。

图4-4 TestDll.dll弹窗提示

4.3.4 小结

这个程序对于初学者来说,理解起来比较复杂。但是,只要熟悉PE结构,这个程序理解起来就会容易得多。对于重定位表、导入表,以及导出表部分的具体操作并没有详细讲解。如果没有了解PE结构,那么理解起来会有些困难;如果了解了PE结构,那么就很容易理解该部分知识。

安全小贴士

可以通过暴力枚举PE结构特征头的方法,来枚举进程中加载的所有模块,它与通过正常方法获取到的模块信息进行比对,从而判断是否存在可疑的PE文件。

原文发布于微信公众号 - 玄魂工作室(xuanhun521)

原文发表时间:2019-05-05

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券