windows黑客编程技术详解之启动技术

windows进程是怎么启动的呢?对于用户来说就是双击,那么这个双击实际上是由explorer进程调用CreateProcess api来创建了这个进程。又或者是通过命令行,cmd拉起。那么对于开发者来说,应该怎么绕开双击,或者命令行启动这些方式呢?下面介绍几种方法。

1 创建进程API

​ 代码怎么拉起一个进程呢,这个是有一些API的,如WinExec、ShellExecute、CreateProcess等函数。

/*WinExec 参数/返回值说明:
参数:
	lpCmdLine:命令行参数。
	 
	ShowCmd:外部程序的运行方式。其取值如下: 
	 
	  ----SW_HIDE 隐藏 
	 
	  ----SW_MAXIMIZE 最大化 
	 
	  ----SW_MINIMIZE 最小化,并把Z order顺序在此窗口之后(即窗口下一层)的窗口激活 
	 
	  ----SW_RESTORE 激活窗口并还原为初始化大小 SW_SHOW 以当前大小和状态激活窗口 
	 
	  ----SW_SHOW 用当前的大小和位置显示一个窗口,同时令其进入活动状态 
	 
	  ----SW_SHOWDEFAULT 以默认方式运行
	 
	  ----SW_SHOWMAXIMIZED 激活窗口并最大化 
	 
	  ----SW_SHOWMINIMIZED 激活窗口并最小化 
	 
	  ----SW_SHOWMINNOACTIVE 最小化但不改变当前激活的窗口 
	 
	  ----SW_SHOWNA 以当前状态显示窗口但不改变当前激活的窗口 
	 
	  ----SW_SHOWNOACTIVATE 以初始化大小显示窗口但不改变当前激活的窗口 
	
	  ----SW_SHOWNORMAL 激活并显示窗口,如果是最大(小)化,窗口将会还原。第一次运行程序 时应该使用这个值
   
返回值:
	函数成功,返回值>31。
 	函数失败,返回:
			--0 系统内存或资源不足 
 
  			--ERROR_BAD_FORMAT .EXE文件格式无效(比如不是32位应用程序) 
 
  			--ERROR_FILE_NOT_FOUND 指定的文件设有找到
 
  			--ERROR_PATH_NOT_FOUND 指定的路径没有找到
 		
*/
UINT WinExec(LPCSTR lpCmdLine,UINT  uCmdShow);

/*ShellExecute 参数/返回值说明:
参数:
	hWnd:用于指定父窗口句柄。当函数调用过程出现错误时,它将作为Windows消息窗口的父窗口。例如,可以将其设置为应用程序主窗口句柄,即Application.Handle,也可以将其设置为桌面窗口句柄(用GetDesktopWindow函数获得)。 
	lpOperation:用于指定要进行的操作
		edit:启动编辑器并打开文档进行编辑,如果lpFile不是文件,则该函数失败;
		open:执行由FileName参数指定的程序,或打开由FileName参数指定的文件或文件夹;
		print:打印由FileName参数指定的文件;
		explore:浏览由FileName参数指定的文件夹;
		find:在由lpDirectory指定的目录中启动搜索;
		
	lpFile:用于指定要打开的文件名、要执行的程序文件名或要浏览的文件夹名。 
	
	lpParameters:若FileName参数是一个可执行程序,则此参数指定命令行参数,否则此参数应为nil或PChar(0)。 
	
	lpDirectory:用于指定默认目录。 
	
	nShowCmd:同上

返回值:
	函数成功,返回值>32。
 	函数失败,返回其他值。
*/
HINSTANCE ShellExecute( HWND hwnd,LPCSTR lpOperation,LPCSTR lpFile,LPCSTR lpParameters,LPCSTR lpDirectory,INT nShowCmd);

BOOL CreateProcess(LPCWSTR pszImageName,LPCWSTR pszCmdLine,LPSECURITY_ATTRIBUTES psaProcess,LPSECURITY_ATTRIBUTES psaThread,BOOL fInheritHandles,DWORD fdwCreate,LPVOID pvEnvironment,LPWSTR pszCurDir,LPSTARTUPINFOW psiStartInfo,LPPROCESS_INFORMATION pProcInfo);

WinExec示例代码

bool WinExecTest(char * p_exe_path, UINT i_ui_show)
{
	UINT i_ret = 0;
	i_ret = WinExec(p_exe_path, i_ui_show);
	if (i_ret > 31)//大于31表示执行成功,否则失败
	{
		return true;
	}
	return false;
}

ShellExecute示例代码

bool ShellExcuteTest(wchar_t * p_exe_path, UINT i_ui_show)
{
	HINSTANCE hInstance = 0;
	hInstance = ::ShellExecute(NULL, NULL, p_exe_path, NULL, NULL, i_ui_show);
	if ((DWORD)hInstance > 32)//大于32表示执行成功
	{
		return true;
	}
	return false;
}

CreateProcess示例代码

bool CreateProcessTest(wchar_t * p_exe_path, UINT i_ui_show)
{
	STARTUPINFO si = { 0 };
	PROCESS_INFORMATION pi;
	bool b_ret = false;
	si.cb = sizeof(si);
	si.dwFlags = STARTF_USESHOWWINDOW;
	si.wShowWindow = i_ui_show;
	b_ret = ::CreateProcess(NULL, p_exe_path, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
	if (b_ret)
	{
		//关掉句柄
		::CloseHandle(pi.hThread);
		::CloseHandle(pi.hProcess);
		return true;
	}
	return false;
}

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

​ SESSION这个概念呢,实际上就是Windows将应用程序根据Windows账户分为了多个Session。比如Administor打开的进程可能属于Session1,而另一个账户Civilian打开的进程就属于Session2,另一个zhourui账户是SESSION3等等。那么SESSION 0呢是windows服务进程,如下所示,服务进程无法再与用户进程有交互式操作(UI 通信等),这就是Session隔离。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vyjQmoMi-1654776711765)(C:\Users\K\AppData\Roaming\Typora\typora-user-images\image-20220425103346347.png)]

​ 那么怎么服务层与应用层的进程怎么通信和交互呢?windows提供了一套WTS(Windows Terminal Service)的接口,可以进行通信和交互。

DWORD WTSGetActiveConsoleSessionId();

WTSGetActiveConsoleSessionId 检索控制台会话的标识符Session Id。微软描述

BOOL WTSQueryUserToken(
  [in]  ULONG   SessionId,
  [out] PHANDLE phToken
);

WTSQueryUserToken 获取Session Id指定的登录用户的主访问令牌。(https://docs.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtsqueryusertoken)

相关函数

BOOL DuplicateTokenEx(
  [in]           HANDLE                       hExistingToken,
  [in]           DWORD                        dwDesiredAccess,
  [in, optional] LPSECURITY_ATTRIBUTES        lpTokenAttributes,
  [in]           SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
  [in]           TOKEN_TYPE                   TokenType,
  [out]          PHANDLE                      phNewToken
);

DuplicateTokenEx 创建一个复制现有令牌的新访问令牌。此函数可以创建主令牌或模拟令牌。

BOOL CreateEnvironmentBlock(
  [out]          LPVOID *lpEnvironment,
  [in, optional] HANDLE hToken,
  [in]           BOOL   bInherit
);

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

BOOL CreateProcessAsUserW(
  [in, optional]      HANDLE                hToken,
  [in, optional]      LPCWSTR               lpApplicationName,
  [in, out, optional] LPWSTR                lpCommandLine,
  [in, optional]      LPSECURITY_ATTRIBUTES lpProcessAttributes,
  [in, optional]      LPSECURITY_ATTRIBUTES lpThreadAttributes,
  [in]                BOOL                  bInheritHandles,
  [in]                DWORD                 dwCreationFlags,
  [in, optional]      LPVOID                lpEnvironment,
  [in, optional]      LPCWSTR               lpCurrentDirectory,
  [in]                LPSTARTUPINFOW        lpStartupInfo,
  [out]               LPPROCESS_INFORMATION lpProcessInformation
);

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

实现原理

​ 由于Session0的隔离,使得在系统服务进程内不能直接调用CreateProess等函数创建进程,而是要通过CreateProcessAsUser函数来创建,这样创建的进程才会显示UI界面,与用户进行交互。

编码实现

//还有一些头文件和依赖
BOOL CreateUserProcess(wchar_t *p_file_nmae)
{
	BOOL bRet = FALSE;
	DWORD dwSessionID = 0;
	HANDLE hToekn;
	HANDLE hTokenDup = NULL;
	LPVOID pEnv = NULL;

	STARTUPINFOW si;
	PROCESS_INFORMATION pi;

	//获取当前session id
	dwSessionID = ::WTSGetActiveConsoleSessionId();
	do 
	{
		if (FALSE == ::WTSQueryUserToken(dwSessionID, &hToekn))
		{
			//WTSQueryUserToken error
			bRet = FALSE;
			break;
		}
		//复制令牌
		if (FALSE == ::DuplicateTokenEx(hToekn, MAXIMUM_ALLOWED, NULL, SecurityIdentification, TokenPrimary, &hTokenDup))
		{
			//DuplicateTokenEx error
			bRet = FALSE;
			break;
		}
		//创建用户会话环境
		if (FALSE == ::CreateEnvironmentBlock(&pEnv, hTokenDup, FALSE))
		{
			//CreateEnvironmentBlock error
			bRet = FALSE;
			break;
		}

		if (FALSE == ::CreateProcessAsUser(hTokenDup, p_file_nmae, NULL, NULL, NULL, FALSE, NORMAL_PRIORITY_CLASS | CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT, pEnv, NULL, &si, &pi))
		{
			//CreateProcessAsUser error
			bRet = FALSE;
			break;
		}
	} while (FALSE);

	//关闭句柄
	::CloseHandle(hToekn);
	::CloseHandle(hTokenDup);
	return bRet;
}

突破SESSION 0隔离创建用户进程-源码链接

执行ServiceLoader.exe可以发现,他将CreateProcessAsUser_Test.exe注册成了服务,
在这里插入图片描述
运行在Session 0下。在这里插入图片描述
继续执行,我们在这个CreateProcessAsUser_Test.exe服务中,拉起了一个test.exe,可以看到这个进程是在Session 1下。
在这里插入图片描述
至此,我们做到了突破Session 0隔离创建用户进程。

btw,我们可以通过hook CreatProcessAsUser来监控进程创建。

3 内存直接加载运行

​ 不通过LoadLibrary来加载DLL,不通过CreateProcess等API函数来加载exe,而是直接把DLL或者EXE等PE文件直接加载到内存中去执行。

实现原理

​ 内存直接加载运行技术的核心就是模拟PE加载器加载PE文件的过程,

​ 这边以加载DLL为例:

​ 首先就是把DLL文件按照映像对齐大小映射到内存中,切不可直接将DLL文件数据存储到内存中。根据PE知识可知是因为文件对齐粒度FileAlignment小于映像对齐粒度SectionAlignment。
然而,成功映射内存数据之后,在DLL中会存在硬编码数据,硬编码都是以默认的加载机制作为基址来计算的。但是由于DLL可以任意加载到其他进程空间,所以DLL的加载基址并非固定的(exe就是固定的,大家都知道进程是在虚拟空间,也就是_IMAGE_OPTIONAL_HEADER的ImageBase)。那么当如果DLL加载基址变了,那么就需要重新算偏移,重定位表记录的就是程序中所有需要修改的硬编码的相对偏移位置。
根据重定位表修改完硬编码数据之后,还要计算DLL调用别的库函数的地址。比如DLL调用了MessageBox,那么DLL如何知道MessageBox函数的地址呢?PE结构使用导入表记录所有引用的函数及函数地址。在DLL映射到内存后,需要更具导入表中的导入模块和函数名称来获取导入函数的地址。最简单的就是GetProcAddress函数来获取,但是调用Win32 AP可能会被杀毒软件检测到。

(顺便说下,为什么我们平时调用库函数只要依赖一下头文件就行了呢?因为我们在工程里面已经链接好了这个DLL,编译软件已经帮我们做好了,叫做隐式链接。那么这种LoadLibrary加载的DLL叫显式链接)

那么我们还可以采用直接遍历PE结构导出表的方式来获取函数地址。
完成上述操作之后,DLL加载工作才算完成,然后就是获取入口地址并跳转执行完成启动。

编码实现

// 模拟LoadLibrary加载内存DLL文件到进程中
// lpData: 内存DLL文件数据的基址
// dwSize: 内存DLL文件的内存大小
// 返回值: 内存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文件的入口点IMAGE_NT_HEADERS.OptionalHeader.AddressOfEntryPoint
	if (FALSE == CallDllMain(lpBaseAddress))
	{
		ShowError("CallDllMain");
		return NULL;
	}

	return lpBaseAddress;
}

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值