前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Windows黑客编程技术详解 --第四章 木马启动技术(内含赠书福利)

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

作者头像
用户1631416
发布2019-05-14 10:55:19
3.6K0
发布2019-05-14 10:55:19
举报
文章被收录于专栏:玄魂工作室玄魂工作室

病毒木马植入模块成功植入用户计算机之后,便会启动攻击模块来对用户计算机数据实施窃取和回传等操作。通常植入和攻击是分开在不同模块之中的,这里的模块指的是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文件。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-05-05,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 玄魂工作室 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 4.1 创建进程API
    • 4.1.1 函数介绍
      • 1.WinExec函数
      • 2.ShellExecute函数
      • 3.CreateProcess函数
    • 4.1.2 实现过程
      • 4.1.3 测试
        • 4.1.4 小结
          • 安全小贴士
          • 4.2 突破SESSION 0隔离创建用户进程
            • 4.2.1 函数介绍
              • 1.WTSGetActiveConsoleSessionId函数
              • 2.WTSQueryUserToken函数
              • 3.DuplicateTokenEx函数
              • 4.CreateEnvironmentBlock函数
              • 5.CreateProcessAsUser函数
            • 4.2.2 实现原理
              • 4.2.3 编码实现
                • 4.2.4 测试
                  • 4.2.5 小结
                    • 安全小贴士
                    • 4.3 内存直接加载运行
                      • 4.3.1 实现原理
                        • 4.3.2 编码实现
                          • 4.3.3 测试
                            • 4.3.4 小结
                              • 安全小贴士
                              相关产品与服务
                              文件存储
                              文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
                              领券
                              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档