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”。