rootkit是一种工具和技术,用于隐藏(潜在的恶意)模块,使其不被系统监控发现。很多人一听到“rootkit”这个词就会想到内核模式下应用的技术,比如IDT(中断描述符表)钩子,SSDT(系统服务调度表)挂钩直接内核对象操作),等等。但rootkits也以一种更简单的用户模式风格出现。它们不像内核模式那样隐秘,但是由于它们的实现简单,它们的传播范围更广。这就是为什么我们需要知道它们是如何工作的。在本文中,我们将对一个简单的userland rootkit进行案例研究,该rootkit使用API重定向技术,以便在流行的监控工具中隐藏自己的存在。

分析示例

01 fb4a4280cc3e6af4f2f0f31fa41ef9

//特别感谢@MalwareHunterTeam

rootkit代码

这个恶意软件是用。net编写的,没有混淆-这意味着我们可以很容易地通过反编译器来反编译它dnSpy

正如我们在代码中看到的,它连接了3个流行的监视应用程序:Process Explorer (procexp)ProcessHacker和Windows任务管理器(任务管理器):

hook_apps

让我们试着运行这个恶意软件dnSpy并观察它在Process Explorer下的行为。样品已经命名malware.exe.一开始它是可见的,就像其他过程一样:

before_processexp

但是在执行了钩子例程之后,它就从列表中消失了:

after_processexp

附加一个调试器到进程资源管理器,我们可以看到一些API函数,例如,NtOpenProcess以非典型的方式启动——从跳转到一些不同的内存页面:

nt_open_hooked

重定向指向注入的代码:

hooked_open_process

它被放置在添加的内存页面,具有完全访问权限:

inj_shellc_view

我们可以转储这个页面并在IDA中打开它,得到3个函数的视图:

功能

第一个函数的代码从偏移量0x60开始:

inj_shellc

前面的空格由一些其他数据填充,这些数据将在本文的第二部分中讨论。

Rootkit的实现

现在让我们看一下实现细节。正如我们前面看到的,钩子是在函数中执行的HookApplication

看看这个函数的开头,我们可以确认,rootkit的角色是在特定的API函数上安装内联钩子:NtReadVirtualMemoryNtOpenProcessNtQuerySystemInformation.这些函数从要用到

让我们来看看实现这样一个简单的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的形式包含了准备好的绕路函数:

shellcodes

shellcode存储为十进制数数组:

shellcode2

为了分析细节,我们可以将每个shell代码转储为二进制形式,并在IDA中加载它。的绕路函数的伪代码NtOpenProcess是:

那么,这个绕道函数是做什么的呢?非常简单的过滤:“如果有人问起恶意软件,告诉他们它不在那里。但如果有人问别的问题,就说实话。”

其他过滤器,应用于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