2021年第一篇技术分析文章,有不足之处还请大家多多包涵。
进程替换技术(process hollowing)
通过对API的调用,实现文件不落地的一种技术。需要编译选项/DYNAMICBASE or /FIXED 其中一个为NO 目的是取消生成重定位表,这里已/DYNAMICBASE为例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 涉及的API使用情况和相关重定位的计算方式。
CreateProcess / / 以CREATE_SUSPENDED挂起状态创建进程
ZwUnmapviewOfSection / / 释放目标进程内存,解除内存映射
VirtualAllocEx / / 为恶意代码分配新空间
WriteProcessMenory / / 写入数据(文件头)
for (i = 0 ;i<NumberOfSection;i + + )
WriteProcessMenory / / 写入节区数据
SetThreadContext / / 设置线程执行位置
ResumeThread / / 重新启动主线程
重定位表存储表达式 = 2 * n + 4 + 4 字节
第一个 4 为VirtualAddress
第二个 4 为SizeofBlock
n为重定位表个数
重定位表主要修改FF15 / FF25类跳转和涉及取内存地址的区域
|
源码讲解
测试代码来源 github,HelloWorld和ProcessHollowing两个进程。
ProcessHollowing通过main函数代码可以发现CreateHollowedProcess函数是整个进程替换的核心,对其进行重点学习。
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 | int _tmain( int argc, _TCHAR * argv[])
{
char * pPath = new char[MAX_PATH];
GetModuleFileNameA( 0 , pPath, MAX_PATH);
pPath[strrchr(pPath, '\\' ) - pPath + 1 ] = 0 ;
strcat(pPath, "helloworld.exe" );
CreateHollowedProcess
(
"cmd" ,
pPath
);
system( "pause" );
VirtualFreeEx(
pProcessInfo - >hProcess,
pPEB - >ImageBaseAddress,
pSourceHeaders - >OptionalHeader.SizeOfImage,
MEM_DECOMMIT);
TerminateProcess(pProcessInfo - >hProcess, 0 );
delete pStartupInfo;
delete pProcessInfo;
delete pBuffer;
delete pContext;
delete []pPath;
return 0 ;
}
|
由于要进行进程替换,所以得先启动一个进程然后将这个进程""挖空",并填入需要写入的另外一个进程的内容,最后跳转到执行点执行,就完成了整个进程替换的过程。
通过前半部分的代码可以得知目前正在获取的cmd的进程信息,并且CreateProcessA的第六个参数决定了该进程将以挂起的形式创建。
通过PROCESS_INFORMATION结构中的hProcess来获取进程句柄,通过PEB结构获取进程的ImageBaseAddress结构。
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 | pStartupInfo = new STARTUPINFOA();
pProcessInfo = new PROCESS_INFORMATION();
CreateProcessA
(
0 ,
pDestCmdLine, / / cmd
0 ,
0 ,
0 ,
CREATE_SUSPENDED,
0 ,
0 ,
pStartupInfo,
pProcessInfo
);
if (!pProcessInfo - >hProcess)
{
printf( "Error creating process\r\n" );
return ;
}
pPEB = ReadRemotePEB(pProcessInfo - >hProcess);
PLOADED_IMAGE pImage = ReadRemoteImage(pProcessInfo - >hProcess, pPEB - >ImageBaseAddress);
printf( "Opening source image\r\n" );
|
PEB结构如下所示,由于PEB结构较大只留下部分结构信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | typedef struct _PEB {
BOOLEAN InheritedAddressSpace;
BOOLEAN ReadImageFileExecOptions;
BOOLEAN BeingDebugged;
BOOLEAN Spare;
HANDLE Mutant;
PVOID ImageBaseAddress;
PPEB_LDR_DATA LoaderData;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID SubSystemData;
PVOID ProcessHeap;
PVOID FastPebLock;
PPEBLOCKROUTINE FastPebLockRoutine;
PPEBLOCKROUTINE FastPebUnlockRoutine;
······
}
|
接下来使用CreateFileA的第二个参数GENERIC_READ,来判断获取HelloWorld文件是否存在。如果存在则获取文件大小并分配内存将其加载到内存中,pBuffer就是这段内存的头指针。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | HANDLE hFile = CreateFileA
(
pSourceFile, / / HelloWorld文件的位置
GENERIC_READ,
0 ,
0 ,
OPEN_ALWAYS,
0 ,
0
);
if (hFile = = INVALID_HANDLE_VALUE)
{
printf( "Error opening %s\r\n" , pSourceFile);
return ;
}
dwSize = GetFileSize(hFile, 0 );
pBuffer = new BYTE[dwSize];
DWORD dwBytesRead = 0 ;
ReadFile(hFile, pBuffer, dwSize, &dwBytesRead, 0 );
|
通过自写的GetLoadedImage来完成镜像文件的装载。
1 2 | PLOADED_IMAGE pSourceImage = GetLoadedImage((DWORD)pBuffer); / / 载入镜像
pSourceHeaders = GetNTHeaders((DWORD)pBuffer); / / 获取NT结构
|
来看一下具体的实现过程,通过DbgHelp提供的LOADED_IMAGE函数就能够完成对内存镜像文件的解析。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)dwImageBase;
PIMAGE_NT_HEADERS32 pNTHeaders = GetNTHeaders(dwImageBase);
PLOADED_IMAGE pImage = new LOADED_IMAGE();
pImage - >FileHeader =
(PIMAGE_NT_HEADERS32)(dwImageBase + pDosHeader - >e_lfanew);
pImage - >NumberOfSections =
pImage - >FileHeader - >FileHeader.NumberOfSections;
pImage - >Sections =
(PIMAGE_SECTION_HEADER)(dwImageBase + pDosHeader - >e_lfanew +
sizeof(IMAGE_NT_HEADERS32));
return pImage;
|
LOADED_IMAGE结构的具体定义如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | typedef struct _LOADED_IMAGE {
PSTR ModuleName;
HANDLE hFile;
PUCHAR MappedAddress;
PIMAGE_NT_HEADERS64 FileHeader;
PIMAGE_NT_HEADERS32 FileHeader;
PIMAGE_SECTION_HEADER LastRvaSection;
ULONG NumberOfSections;
PIMAGE_SECTION_HEADER Sections;
ULONG Characteristics;
BOOLEAN fSystemImage;
BOOLEAN fDOSImage;
BOOLEAN fReadOnly;
UCHAR Version;
LIST_ENTRY Links;
ULONG SizeOfImage;
} LOADED_IMAGE, * PLOADED_IMAGE;
|
紧接着进行相关节的取消映射,方便之后对该节进行相关写入操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | printf( "Unmapping destination section\r\n" );
HMODULE hNTDLL = GetModuleHandleA( "ntdll" );
FARPROC fpNtUnmapViewOfSection = GetProcAddress(hNTDLL, "NtUnmapViewOfSection" );
_NtUnmapViewOfSection NtUnmapViewOfSection =
(_NtUnmapViewOfSection)fpNtUnmapViewOfSection;
DWORD dwResult = NtUnmapViewOfSection
(
pProcessInfo - >hProcess,
pPEB - >ImageBaseAddress
);
if (dwResult)
{
printf( "Error unmapping section\r\n" );
return ;
}
|
接着通过VirtualAllocEx在cmd进程中分配内存,内存的起始地址有第二个参数决定,这样就完成了相关内存的分配过程。
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 | pRemoteImage = VirtualAllocEx / / 如果errorcode = 487 则可能是本程序编译选项 / DYNAMICBASE未设置为NO
(
pProcessInfo - >hProcess,
pPEB - >ImageBaseAddress,
pSourceHeaders - >OptionalHeader.SizeOfImage,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);
if (!pRemoteImage)
{
printf( "VirtualAllocEx call failed error=%d\r\n" ,GetLastError());
return ;
}
DWORD dwDelta = (DWORD)pPEB - >ImageBaseAddress -
pSourceHeaders - >OptionalHeader.ImageBase;
printf
(
"Source image base: 0x%p\r\n"
"Destination image base: 0x%p\r\n" ,
pSourceHeaders - >OptionalHeader.ImageBase,
pPEB - >ImageBaseAddress
);
|
由于复制的内容大同小异,这里针对.reloc也就是重定位节进行一个重点讲述,也是本次内容中最重要的部分。
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 | if (dwDelta)
for (DWORD x = 0 ; x < pSourceImage - >NumberOfSections; x + + )
{
char * pSectionName = ".reloc" ;
if (memcmp(pSourceImage - >Sections[x].Name, pSectionName, strlen(pSectionName)))
continue ;
printf( "Rebasing image\r\n" );
DWORD dwRelocAddr = pSourceImage - >Sections[x].PointerToRawData;
DWORD dwOffset = 0 ;
IMAGE_DATA_DIRECTORY relocData =
pSourceHeaders - >OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
while (dwOffset < relocData.Size)
{
PBASE_RELOCATION_BLOCK pBlockheader =
(PBASE_RELOCATION_BLOCK)&pBuffer[dwRelocAddr + dwOffset];
dwOffset + = sizeof(BASE_RELOCATION_BLOCK);
DWORD dwEntryCount = CountRelocationEntries(pBlockheader - >BlockSize);
/ /
PBASE_RELOCATION_ENTRY pBlocks =
(PBASE_RELOCATION_ENTRY)&pBuffer[dwRelocAddr + dwOffset];
for (DWORD y = 0 ; y < dwEntryCount; y + + )
{
dwOffset + = sizeof(BASE_RELOCATION_ENTRY);
if (pBlocks[y]. Type ! = 3 )
continue ;
DWORD dwFieldAddress =
pBlockheader - >PageAddress + pBlocks[y].Offset;
DWORD dwBuffer = 0 ;
ReadProcessMemory
(
pProcessInfo - >hProcess,
(PVOID)((DWORD)pPEB - >ImageBaseAddress + dwFieldAddress),
&dwBuffer,
sizeof(DWORD),
0
);
/ / printf( "Relocating 0x%p -> 0x%p\r\n" , dwBuffer, dwBuffer - dwDelta);
dwBuffer + = dwDelta;
BOOL bSuccess = WriteProcessMemory
(
pProcessInfo - >hProcess,
(PVOID)((DWORD)pPEB - >ImageBaseAddress + dwFieldAddress),
&dwBuffer,
sizeof(DWORD),
0
);
printf( "NewRelocaAddress=0x%x -> Newvalue=0x%x\r\n" , dwFieldAddress,dwBuffer);
num + + ;
if (!bSuccess)
{
printf( "Error writing memory\r\n" );
continue ;
}
}
}
break ;
}
printf( "total write %d relocationtable\r\n" ,num);
|
DataDirectory是一个结构体数组,结构体由VirtualAddress和Size组成。
1 2 3 4 5 6 | typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, * PIMAGE_DATA_DIRECTORY;
|
数组包含以下15种,这里用到的就是IMAGE_DIRECTORY_ENTRY_BASERELOC,即重定位表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | / / IMAGE_DIRECTORY_ENTRY_COPYRIGHT 7 / / (X86 usage)
|
其中CountRelocationEntries是一个宏,用于计算重定位的个数。
1 2 3 4 | (dwBlockSize - \
sizeof(BASE_RELOCATION_BLOCK)) / \
sizeof(BASE_RELOCATION_ENTRY)
|
除此之外PBASE_RELOCATION_ENTRY结构如下所示,前12位表示偏移量,后4位表示类型。
1 2 3 4 | typedef struct BASE_RELOCATION_ENTRY {
USHORT Offset : 12 ;
USHORT Type : 4 ;
} BASE_RELOCATION_ENTRY, * PBASE_RELOCATION_ENTRY;
|
接下来的部分会涉及到线程执行劫持,利用OpenThread打开目标线程,在获取目标线程的句柄后,恶意软件通过调用SuspendThread来将线程置于挂起模式。调用VirtualAllocEx和WriteProcessMemory来分配内存并执行代码注入的操作。然后调用GetThreadContext和GetThreadContext获取并设置线程的上下文,以将EIP寄存器设置到要执行恶意代码的地址,达到重启线程的作用。。
1 2 3 4 5 6 7 8 | 涉及的相关API函数使用顺序
OpenThread
SuspendThread
VirtualAllocEx
WriteProcessMemory
GetThreadContext
SetThreadContext
ResumeThread
|
注意的是pContext->Eax = dwEntrypoint;利用某个寄存器来完成入口点的赋值,利用SetThreadContext完成上下文设置,最后通过ResumeThread执行,代码如下所示。
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 42 43 44 45 46 47 48 49 50 51 | printf( "total write %d relocationtable\r\n" ,num);
DWORD dwEntrypoint = (DWORD)pPEB - >ImageBaseAddress +
pSourceHeaders - >OptionalHeader.AddressOfEntryPoint;
printf( "EntryPoint=0x%p\r\n" , dwEntrypoint);
printf( "Writing breakpoint\r\n" );
DWORD dwBreakpoint = 0xCC ;
if (!WriteProcessMemory
(
pProcessInfo - >hProcess,
(PVOID)dwEntrypoint,
&dwBreakpoint,
4 ,
0
))
{
printf( "Error writing breakpoint\r\n" );
return ;
}
pContext = new CONTEXT();
pContext - >ContextFlags = CONTEXT_INTEGER;
printf( "Getting thread context\r\n" );
if (!GetThreadContext(pProcessInfo - >hThread, pContext))
{
printf( "Error getting context\r\n" );
return ;
}
pContext - >Eax = dwEntrypoint;
printf( "Setting thread context\r\n" );
if (!SetThreadContext(pProcessInfo - >hThread, pContext))
{
printf( "Error setting context\r\n" );
return ;
}
printf( "Resuming thread\r\n" );
if (!ResumeThread(pProcessInfo - >hThread))
{
printf( "Error resuming thread\r\n" );
return ;
}
printf( "Process hollowing complete\r\n" );
}
|
自此,整个代码流程就讲解完成如有什么疑问,可自行查询相关资料或者留言告知我。
HelloWorld演示
接下来进行调试演示,通过调试发现Creating process的进程是cmd,并且为Suspended,符合之前的描述过程。
获取到cmd相关结构信息。
获取重定位的目标差。
由于HelloWorld.exe本身没有重定位表,所以调试的时候一直没有进入相关函数。
DLL调试
这里自己创建一个dll来替代HelloWorld。
通过调试发现dwEntryCount值为136,目前还没有搞明白是什么意思。
最后通过打印,发现一共找到了148个重定位函数,有些不可思议。
借助studype来看一下重定位表的情况,0x88+0xE=0x96=226,比输出的148大,通过内容发现有些是00,所以我选择相信有148个需要重定位的函数。
新的程序入口点为0xABB829A即将赋值给eax+0xb0这里。
通过另一个调试器附件进程,如果附加失败则在CreateProcess之后就附加即可。
成功弹出MessageBox,整体调试完毕。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工
作,每周日13:00-18:00直播授课
最后于 2021-1-4 09:20
被Risks编辑
,原因: 修改帖子主题