WIndows编程技术--启动技术

病毒木马植入模块成功植入用户计算机之后,便会启动攻击模块来对用户计算机数据实施窃取和回传等操作。当植入模块成功执行后,继续执行攻击模块,同时删除植入模块的数据和文件。

这里说的“启动技术”,不是程序的“自启动”,而是“启动”其他的程序,因为这是木马或病毒经常用到的技术。

本篇内容有点点难,主要涉及到重定位,篇幅也很长,慢慢理解看;源码放在网盘中。

篇内有三个内容:

1)WinExec、ShellExecute、CreateProcess创建进程API

2)Session 0 隔离的创建进程

3)内存加载运行

一、创建进程API

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

我这里不作介绍了,写过程序的都知道这三个怎么用。

二、Session 0 隔离的创建用户进程

从windows vista开始,只有服务可以托管到Session 0 中,用户程序和服务之间会进行隔离,并需要运行在用户登录系统时创建的后续会话中。如第一个登录用户创建Session 1,第二个登录用户创建Session 2,顺延类推。

使用不同会话运行的应用程序或服务,如果不将自己明确标注为全局命名空间,并提供相应的访问控制设置,那么将无法互相发送消息、共享UI或共享内核对象。

在此基础上,windows提供了一套以WTS开头的函数,从而打通服务层与应用层,完成两者间的交互。

1、函数介绍:

1)WTSGetActiveConsoleSessionId函数

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

参数:无;

返回值:如果执行成功,则返回连接到物理控制台的会话标识符。如果没有连接到物理控制台的会话(例如,物理控制台会话正在附加或分离),则此函数返回0xFFFFFFFF。

2)WTSQueryUserToken函数

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

BOOL WTSQueryUserToken(

_In_ ULONG SessionId,

_Out_ PHANDLE phToken)

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

第2个参数:如果该功能成功,则会收到一个指向登录用户令牌句柄的指针。请注意,必须调用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)

第1个参数:使用TOKEN_DUPLICATE访问权限打开访问令牌的句柄。

第2个参数:指定新令牌的请求访问权限。要想请求对调用者有效的所有访问权限,请指定MAXIMUM_ ALLOWED。

第3个参数:指向SECURITY_ATTRIBUTES结构的指针,该结构指定新令牌的安全描述符,并确定子进程是否可以继承令牌。如果lpTokenAttributes为NULL,则令牌获取默认的安全描述符,并且不能继承该句柄。

第4个参数:指定SECURITY_IMPERSONATION_LEVEL枚举中指示新令牌模拟级别的值。

第5个参数:从TOKEN_TYPE枚举中指定以下值之一。

含义

TokenPrimary

新令牌是可以在CreateProcessAsUser函数中使用的主令牌

TokenImpersonation

新令牌是一个模拟令牌

第6个参数:指向接收新令牌的HANDLE变量的指针。新令牌使用完成后,调用CloseHandle函数来关闭令牌句柄。

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

4)CreateEnvironmentBlock函数

检索指定用户的环境变量,然后可以将此块传递给CreateProcessAsUser函数。

BOOL WINAPI CreateEnvironmentBlock(

_Out_ LPVOID *lpEnvironment,

_In_opt_ HANDLE hToken,

_In_ BOOL bInherit)

第1个参数:当该函数返回时,已接收到指向新环境块的指针。

第2个参数:Logon为用户,从LogonUser函数返回。如果这是主令牌,则令牌必须具有TOKEN_QUERY和TOKEN_DUPLICATE访问权限。如果令牌是模拟令牌,则必须具有TOKEN_QUERY权限。如果此参数为NULL,则返回的环境块仅包含系统变量。

第3个参数:指定是否可以继承当前进程的环境。如果该值为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)

第1个参数:表示用户主令牌的句柄。句柄必须具有TOKEN_QUERY、TOKEN_DUPLICATE和TOKEN_ASSIGN_PRIMARY访问权限。

第2个参数:要执行模块的名称。该模块可以基于Windows应用程序。

第3个参数:要执行的命令行。该字符串的最大长度为32K个字符。如果lpApplicationName为NULL,则lpCommandLine模块名称的长度限制为MAX_PATH个字符。

第4个参数:指向SECURITY_ATTRIBUTES结构的指针,该结构指定新进程对象的安全描述符,并确定子进程是否可以继承返回进程的句柄。如果lpProcessAttributes为NULL或lpSecurityDeor为NULL,则该进程将获得默认的安全描述符,并且不能继承该句柄。

第5个参数:指向SECURITY_ATTRIBUTES结构的指针,该结构指定新线程对象的安全描述符,并确定子进程是否可以继承返回线程的句柄。如果lpThreadAttributes为NULL或lpSecurityDeor为NULL,则线程将获取默认的安全描述符,并且不能继承该句柄。

第6个参数:如果此参数为TRUE,则调用进程中的每个可继承句柄都由新进程继承;如果参数为FALSE,则不能继承句柄。请注意,继承的句柄具有与原始句柄相同的值和访问权限。

第7个参数:控制优先级和进程创建的标志。

第8个参数:指向新进程环境块的指针。如果此参数为NULL,则新进程将使用调用进程的环境。

第9个参数:指向进程当前目录的完整路径。如果此参数为NULL,则新进程将具有与调用进程相同的当前驱动器和目录。

第10个参数:指向STARTUPINFO或STARTUPINFOEX结构的指针。用户必须具有对指定窗口站和桌面的完全访问权限。

第11个参数:指向一个PROCESS_INFORMATION结构的指针,用于接收新进程的标识信息。PROCESS_INFORMATION中的句柄必须在不需要时使用CloseHandle关闭。

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

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

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

3、重点代码

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

BOOL CreateUserProcess(char *lpszFileName)

{undefined

BOOL bRet = TRUE;

DWORD dwSessionID = 0;

HANDLE hToken = NULL;

HANDLE hDuplicatedToken = NULL;

LPVOID lpEnvironment = NULL;

STARTUPINFO si = { 0 };

PROCESS_INFORMATION pi = { 0 };

si.cb = sizeof(si);

do

{undefined

// 获得当前Session ID

dwSessionID = ::WTSGetActiveConsoleSessionId();

// 获得当前Session的用户令牌

::WTSQueryUserToken(dwSessionID, &hToken);

// 复制令牌

::DuplicateTokenEx(hToken, MAXIMUM_ALLOWED, NULL,

SecurityIdentification, TokenPrimary, &hDuplicatedToken);

// 创建用户Session环境

::CreateEnvironmentBlock(&lpEnvironment,

hDuplicatedToken, FALSE);

// 在复制的用户Session下执行应用程序,创建进程

::CreateProcessAsUser(hDuplicatedToken,

lpszFileName, NULL, NULL, NULL, FALSE,

NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT,

lpEnvironment, NULL, &si, &pi);

} while (FALSE);

// 关闭句柄, 释放资源(略)

return bRet;

}

4、测试

因为程序要实现的是突破SESSION 0隔离,所以,在系统服务程序中创建用户桌面进程。程序必须注册成为一个系统服务进程,这样才处于SESSION 0中。服务程序的入口点与普通程序的入口点不同,需要通过调用函数StartServiceCtrlDispatcher来设置服务入口点函数。这里用到一个服务加载器ServiceLoader.exe,它可将测试程序加载为服务进程。

在main函数中,设置服务入口点函数,使之成为服务程序,并在服务程序中调用上述封装好的函数进行测试。首先,以管理员身份运行服务加载器ServiceLoader.exe,这样服务加载器会将CreateProcessAsUser_Test.exe程序加载为服务进程,从而执行创建用户进程的代码。服务加载器提示创建和启动服务成功后,立即显示对话框和启动“injectcalc.exe”程序,而且窗口界面也成功显示,如图所示。

然后,使用进程查看器查看CreateProcessAsUser_Test.exe进程以及injectcalc.exe进程中的SESSION值,如图所示,CreateProcessAsUser_Test.exe进程处于SESSION 0中,而injectcalc.exe处于SESSION 1中。

 

 

三、内存加载运行

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

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

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

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加载工作才算完成,接下来便是获取入口地址并跳转执行以便完成启动。

2、具体实现流程总结:

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

3、代码

// 模拟LoadLibrary加载内存DLL文件到进程中

// lpData: 内存DLL文件数据的基址

// dwSize: 内存DLL文件的内存大小

// 返回值: 内存DLL加载到进程的加载基址

LPVOID MmLoadLibrary(LPVOID lpData, DWORD dwSize)

{undefined

LPVOID lpBaseAddress = NULL;

// 获取镜像大小

DWORD dwSizeOfImage = GetSizeOfImage(lpData);

// 在进程中开辟一个可读、可写、可执行的内存块

lpBaseAddress = ::VirtualAlloc(NULL, dwSizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

::RtlZeroMemory(lpBaseAddress, dwSizeOfImage);

// 将内存DLL数据按SectionAlignment大小对齐映射到进程内存中

MmMapFile(lpData, lpBaseAddress);

// 修改PE文件重定位表信息

DoRelocationTable(lpBaseAddress);

// 填写PE文件导入表信息

DoImportTable(lpBaseAddress);

//修改页属性。应该根据每个页的属性单独设置其对应内存页的属性。

//统一设置成一个属性PAGE_EXECUTE_READWRITE

DWORD dwOldProtect = 0;

::VirtualProtect(lpBaseAddress, dwSizeOfImage, PAGE_EXECUTE_READWRITE, &dwOldProtect);

// 修改PE文件加载基址IMAGE_NT_HEADERS.OptionalHeader.ImageBase

SetImageBase(lpBaseAddress);

// 调用DLL的入口函数DllMain,函数地址即为PE文件的入口点IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint

CallDllMain(lpBaseAddress);

return lpBaseAddress;

}

4、测试

vs2019在生成x86程序进行测试时,弹出了窗口提示,加载Dll成功;

 

 

但在x64时,没有得到弹窗提示,显示测试失败。在vs里跟踪了一下,发现是“dwSizeOfImage = pNtHeaders->OptionalHeader.SizeOfImage;”这里报错“引发了异常: 读取访问权限冲突。 **pNtHeaders** 是 0x3DA74140”。

 

posted @ 2022-02-23 15:46  卖雨伞的小男孩  阅读(272)  评论(0编辑  收藏  举报