服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

浅析Pe2shellcode

日期: 来源:默安逐日实验室收集编辑:Zhuri

编者注:本文仅供学习研究,严禁从事非法活动,任何后果由使用者本人负责。


前言

众所周知,对shellcode免杀是很流行的技术,但是直接对exe的免杀方法相对稀缺,如果我们能将exe转为shellcode,然后用shellcode免杀那一套就会简单许多。所以shellcode转exe是一个很值得研究的课题。下面我将大概讲解一下pe2shellcode这个开源项目的思路。

Github项目地:

https://github.com/hasherezade/pe_to_shellcode。


代码分析

我们先看看它的项目结构,主要模块有Injector、Runshc、pe2shr。



1

Injector

该文件夹下存在一个main.cpp。



这是一个简单的远程shellcode注入的代码,用于将我们的恶意代码注入到其它进程。

关于进程注入这里就不赘述了,想学习进程注入的朋友们可以关注一下这位师傅(https://modexp.wordpress.com/)。


2

Runshc

主文件main.cpp。



这是个最基本shellcode加载器,可以看出来Injector和Runshc都是给PE转成shellcode之后测试用的,测试shellcode是否能成功运行。

然后就是libpeconv。



这个工具也是非常经典,拿来用和学习pe结构都特别好(https://github.com/hasherezade/libpeconv),这个工具可以用来处理解析PE文件,可以用以替代windows的Createprocess制作Runshc和pe2shr。很巧的是,pe2shr里面的stub.bin是作者用汇编实现的一个简易版的libpeconv。


3

pe2shr

然后就是文章主角pe2shc,利用了stub32.bin、stub64.bin和main.cpp。

stub32.bin和stub64.bin是一个作者用汇编写的一个peloader。



接着打开main.cpp看看作者怎么实现的pe2shellcode。



前面就是对参数的处理,比如获取需要处理的原生pe路径,设置输出的shellcode路径,然后就是步入is_supporte_pe函数。



可以看到有三点:

1、必须要有重定位表

2、不能是控制台应用程序

3、不能是.net程序,.net的pe结构不同之处在于pe头中的IMAGE_OPTIONAL_HEARDER:这个结构中的数据目录DataDirectory包括了映像文件中的CLR头的RVA和大小。而stub并没有处理.net形式PE的代码。

 

其实除了作者在代码里列出的三种pe无法执行之外,有些有tls表的pe以及某些使用了延迟重定位表的pe也是转换不了的(或者说转换了也运行不了),因为作者的stub并没有对这些表处理,后面讲到stub的时候就可以发现了。

 


接下来将原生pe加载进内存处理后再将更改后的shellcode另存为(或者叫它pe?)。

关键是处理这一步:shellcodeify函数。



可以看到大致逻辑是判断pe位数后获取资源里的资源段,这个资源段是pe2shc.exe里本身的资源,且x64和x86各有一套。



RCData里有存储着stub,resourceID 101是32位的stub,102是64位的stub。

这个stub其实就是作者用汇编写的一段PEloader,后面会详细讲。

接下来分析overwrite_hdr这个函数。




Raw是原生pe的大小,将最后调用的eax赋值成ImageBase+sizeof(原生pe),让eip跳到尾部的stub执行。

这里可以看到有一个redir_code,这个redir_code是用来覆盖原生pe头的,个人认为这个redir_code涉及的十分巧妙,我们来分析一下32位的redir_code。


redir_code32[] = "\x4D" //dec ebp   ‘M’   "\x5A" //pop edx ’Z‘   "\x45" //inc ebp   消除 dec ebp的影响   "\x52" //push edx 消除pop edx 的影响   "\xE8\x00\x00\x00\x00" //短call 下一条指令   "\x58" // pop eax  把这条指令地址弹到eax   "\x83\xE8\x09" // sub eax,9     eax-9指向的就是imageBase也就是加载进内存中pe的基址   "\x50" // push eax (Image Base)  将eax压到栈顶   "\x05" // add eax,   把shellcode地址放到eax当中   eax+ 原生PE的buffer大小就是shellcode的地址   "\x59\x04\x00\x00" // value   "\xFF\xD0" // call eax    将下一条指令压栈   "\xc3"; // ret                       == // pop eip  又回到最初的起点


 \x4D\x5A对应的是PE文件的魔术字段“MZ”,作者将MZ当成指令处理,CreateProcess底层函数MiVerifyImageHeader检测到文件没有某些字段便会抛出异常不去执行,这些字段就包括魔术字段,也就是说,作者很巧妙地让转成shellcode的文件仍然能当作正常的PE文件运行,这种手法让我想到了0x0C0C0C0C,既可以是地址也可以是opcode。

MZ字符当成指令在msf的stager里也用到过。



msf里是为了防止发生Bootstrap直接写入PE头部而导致破坏dos头的情况,这里就不作赘述了。

下图是windows针对PE头的必检字段。



有些安全研究员会更改这些非必检字段制作畸形pe,一旦edr或者杀软的校验逻辑与windows不一致,那就很可能将其当成非可执行文件从而达到绕过检测的目的,但并不会影响其在windows上的正常运行。


"\x45" //inc ebp"\x52" //push edx


接着这两条指令是为了平衡“M”,“Z”作为指令造成的影响,插在e_cblp上。


"\xE8\x00\x00\x00\x00" //call <next_line"\x58" // pop eax


然后是call 0,意思是call 下一条指令的地址,也就是call pop eax的地址。

pop eax 把该指令地址弹到eax里。


"\x83\xE8\x09" // sub eax,9"\x50" // push eax (Image Base)


sub eax ,9 就是让eax指向imageBase,把eax压栈。


"\x05" // add eax,"\x59\x04\x00\x00" // value


前面讲过是将eax赋值成尾部stub的地址。

我们拿mimikatz_x86转shellcode进行验证。



010editor打开 mimi86.exe,可以看到rdi_code覆盖了原生pe头,而"\x59\x04\x00\x00"也变成了\x00\x80\x09\x00。



转到该地址,发现这确实是stu32.bin。



"\xFF\xD0" // call eax"\xc3"; // ret      


接着最后两条指令跳到stub执行,然后ret 回到原来的地方。


stub32.bin

接着我们看一下stub32.bin,由于篇幅问题,我就不一个指令一个指令分析了。

可以看到asm文件包含了一个inc文件。



Inc文件里把一些固定值用伪指令equ对应起来。



像PAGE_EXECUTE_READWRITE equ 40h、lfanew equ 3ch,增强了代码的可读性。


1

获取kernel32地址

我们看看第一段。



很经典的shellcode写法,fs\gs寄存器寻找PEB基址,通过PEB找到ldr链,然后ldr链里存放着dll名称和地址,hldr_begin的功能就是寻找kernel32.dll的地址。


2

获取导出函数地址


获取IMAGE_DIRECTORY_ENTRY_EXPORT地址->从IMAGE_DIRECTORY_ENTRY_EXPORT获取函数名地址(edAddressOfNames)->根据RVA在AddressOfNames表中的排序序号,获取AddressOfNameOrdinals表中相同排序序号的值->获取函数地址(edAddressOfFunctions)。

本质上实现了一个GetProAddress的功能。如下是转成C语言之后的代码。


DWORD GetFuncAddress() {    DWORD dwProcAddress = 0;    PIMAGE_DOS_HEADER pDosHdr = (PIMAGE_DOS_HEADER)hModule;    PIMAGE_NT_HEADERS pNtHdr = (PIMAGE_NT_HEADERS)((DWORD)pDosHdr + pDosHdr->e_lfanew);    PIMAGE_EXPORT_DIRECTORY pImageExportDirectory = (PIMAGE_EXPORT_DIRECTORY)((DWORD)pDosHdr + pNtHdr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);    PDWORD pAddressOfFunc = (PDWORD)((DWORD)hModule + pImageExportDirectory->AddressOfFunctions);    PDWORD pAddressOfName = (PDWORD)((DWORD)hModule + pImageExportDirectory->AddressOfNames);    PWORD pAddressOfNameOrdinal = (PWORD)((DWORD)hModule + pImageExportDirectory->AddressOfNameOrdinals);    if ((DWORD)lpProcName & 0xFFFF0000)    {        DWORD dwSize = pImageExportDirectory->NumberOfNames;        for (DWORD i = 0; i < dwSize; i++)        {            DWORD dwAddrssOfName = (DWORD)hModule + pAddressOfName[i];            int nRet = strcmp(lpProcName, (char*)dwAddrssOfName);            if (nRet == 0)            {                WORD wHint = pAddressOfNameOrdinal[i];                dwProcAddress = (DWORD)hModule + pAddressOfFunc[wHint];                return dwProcAddress;            }        }        dwProcAddress = 0;    }    else    {        DWORD nId = (DWORD)lpProcName - pImageExportDirectory->Base;        dwProcAddress = (DWORD)hModule + pAddressOfFunc[nId];    }    return dwProcAddress;}


3

处理导入表


循环遍历DLL导入表中的DLL及获取导入表中的函数地址-> 获取导入表中DLL的名称并加载DLL->判断是否加载了dll没有的话则加载之->获取OriginalFirstThunk以及对应的导入函数名称表首地址->获取FirstThunk以及对应的导入函数地址表首地址-> 判断导出函数是序号导出还是函数名称导出->获取函数地址。

转成C大概就是这样:


ImportTable(){    PIMAGE_DOS_HEADER DosAddr = (PIMAGE_DOS_HEADER)BaseAddr;    PIMAGE_NT_HEADERS NtAddr = (PIMAGE_NT_HEADERS)(BaseAddr + DosAddr->e_lfanew);    PIMAGE_IMPORT_DESCRIPTOR pImportTable = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)DosAddr +                                                                       NtAddr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress);    char* DllName = NULL;    HMODULE DllAddr = NULL;    PIMAGE_THUNK_DATA lpImportNameArray = NULL;    PIMAGE_IMPORT_BY_NAME lpImportByName = NULL;    PIMAGE_THUNK_DATA lpImportFuncAddrArray = NULL;    FARPROC FuncAddress = NULL;    DWORD i = 0;    while (TRUE)    {        if (0 == pImportTable->OriginalFirstThunk)        {            break;        }        DllName = (char*)((DWORD)DosAddr + pImportTable->Name);        DllAddr = GetModuleHandleA(DllName);if (NULL == hDll){    hDll = LoadLibraryA(DllName);    if (NULL == hDll)    {        pImportTable++;        continue;    }}
       i = 0;        lpImportNameArray = (PIMAGE_THUNK_DATA)((DWORD)DosAddr + pImportTable->OriginalFirstThunk);        lpImportFuncAddrArray = (PIMAGE_THUNK_DATA)((DWORD)DosAddr + pImportTable->FirstThunk);        while (TRUE)        {            if (0 == lpImportNameArray[i].u1.AddressOfData)            {                break;            }            lpImportByName = (PIMAGE_IMPORT_BY_NAME)((DWORD)DosAddr + lpImportNameArray[i].u1.AddressOfData);            if (0x80000000 & lpImportNameArray[i].u1.Ordinal)            {                FuncAddress = GetProcAddress(DllAddr, (LPCSTR)(lpImportNameArray[i].u1.Ordinal & 0x0000FFFF));            }            else            {                FuncAddress = GetProcAddress(DllAddr, (LPCSTR)lpImportByName->Name);            }            lpImportFuncAddrArray[i].u1.Function = (DWORD)FuncAddress;            i++;        }        pImportTable++;    }    return TRUE;}


4

处理重定位


获取IMAGE_DIRECTORY_ENTRY_RELOC地址->计算需要修正的重定位项(地址)的数目(reSizeofBlock)->计算需要修正的重定位项的地址->把移动的距离在原地址加上去->转到下一个节进行处理直到所有节处理完毕。

转成C就是这样:


ProcessRelocation(){PIMAGE_DOS_HEADER DosAddr = (PIMAGE_DOS_HEADER)BaseAddr;PIMAGE_NT_HEADERS NtAddr = (PIMAGE_NT_HEADERS)(BaseAddr + DosAddr->e_lfanew);PIMAGE_BASE_RELOCATION LocAddr = (PIMAGE_BASE_RELOCATION)(BaseAddr + NtAddr->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress);while ((LocAddr->VirtualAddress + LocAddr->SizeOfBlock) != 0){WORD* LocAddrData = (WORD*)((PBYTE)LocAddr + sizeof(IMAGE_BASE_RELOCATION));int NumberOfReloc = (LocAddr->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);int i;for (i = 0; i < NumberOfReloc; i++){if ((DWORD)(LocAddrData[i] >> 12)==3){DWORD* pAddress = (DWORD*)((PBYTE)DosAddr + LocAddr->VirtualAddress + (LocAddrData[i] & 0x0FFF));DWORD dwDelta = (DWORD)DosAddr - NtAddr->OptionalHeader.ImageBase;*pAddress += dwDelta;}}LocAddr = (PIMAGE_BASE_RELOCATION)((PBYTE)LocAddr + LocAddr->SizeOfBlock);}


5

跳到OPE执行


bool GetEntry(){    PIMAGE_DOS_HEADER DosAddr = (PIMAGE_DOS_HEADER)BaseAddress;    PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(BaseAddress + DosAddr->e_lfanew);    char* Entry = (char*)(BaseAddress + pNt->OptionalHeader.AddressOfEntryPoint);    ((void(*)())ExeEntry();    return TRUE;}


可以看到stub32.bin其实就是一个PEloader。


总结

pe2shellcode并不是真正意义上的将pe文件转成shellcode,它其实更像个加壳器,利用尾部的“peloader”加载头部的原生pe,并且让转换后的pe仍然是个合法的pe。




相关阅读

  • 弈 - Codeql 自动运行和项目监控工具

  • 前言代码审计总是离不开一些神器,笔者常用 Codeql[1] 这款工具辅助挖洞。当我每写一个规则都需要对其它项目手动运行检查一遍,效率很低,再加上 lgtm[2] 的关闭,此项目诞生了 ---
  • 设计提效-Figma插件篇

  • 序言咦,同样是用Figma,为什么同桌小花早早下班,隔壁二狗岁月静好,而你却在Figma中焦头烂额?设计提速的秘诀就在于Figma中功能各异的插件。但当打开Figma插件库,海量插件让人头晕眼
  • 微软丨微信小冰正式回归……

  • 9月22日小冰公司与微软(亚洲)互联网工程院在北京联合举行了第九代小冰发布会宣布微信小冰正式回归微信公众号名称是 :AI 小冰微信公众号 ID:xiaobing-official关注 AI 小冰微信
  • constexpr

  • 前面介绍了模板这种编译期动作,关于编译期动作,有必要介绍下constexpr。在这之前有必要简单提一下constexpr与const的关系,两者字面上都表达常量的意思。主要的区别是:const修饰
  • 智慧书房丨共赴春日阅读(南湖篇)

  • 智慧书房共赴春日阅读(南湖篇)春日花静开,在如此美妙的季节,读一本好书才不算辜负。借此良机,快来看看家附近的智慧书房,约二三好友,开启一场春天的阅读之旅。市图书馆智慧书房开
  • 万字长文教你如何做出 ChatGPT

  • 作者:monychen,腾讯 IEG 应用研究员简单来说,ChatGPT 是自然语言处理(NLP)和强化学习(RL)的一次成功结合,考虑到读者可能只熟悉其中一个方向或者两个方向都不太熟悉,本文会将 ChatGPT
  • IcedID僵尸网络滥用谷歌 PPC服务分发恶意软件

  • 关键词IcedID僵尸网络、谷歌 PPC、恶意软件1. 概述在密切跟踪 IcedID 僵尸网络的活动后,趋势科技的研究人员发现其分发方法发生了一些重大变化。自 2022 年 12 月以来, 趋势科

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • 浅析Pe2shellcode

  • 编者注:本文仅供学习研究,严禁从事非法活动,任何后果由使用者本人负责。前言众所周知,对shellcode免杀是很流行的技术,但是直接对exe的免杀方法相对稀缺,如果我们能将exe转为shell
  • 云原⽣组件Nacos新型红队手法研究

  • 组件简介Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service的首字母简称,一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。Nacos 致力于帮
  • 花式反沙箱(上)

  • 编者注:本文仅供学习研究,严禁从事非法活动,任何后果由使用者本人负责。前言目前沙箱正成为判断恶意威胁的一种最快速和最简单的方式,因此反沙箱检测在实战中发挥越来越重要的作
  • 弈 - Codeql 自动运行和项目监控工具

  • 前言代码审计总是离不开一些神器,笔者常用 Codeql[1] 这款工具辅助挖洞。当我每写一个规则都需要对其它项目手动运行检查一遍,效率很低,再加上 lgtm[2] 的关闭,此项目诞生了 ---
  • K8S后渗透横向节点与持久化隐蔽方式探索

  • 前言通常在红蓝对抗中,我们可能会通过各种方法如弱口令、sql注入、web应用漏洞导致的RCE等方法获得服务器的权限;在当前云原生迅猛发展的时代,这台服务器很可能是一个容器,在后
  • Linux Capabilities利用总结

  • 前言Linux对于权限的管理,系统权限只有root才有,对于普通用户只有一些有限的权限;而对于普通用户如果想进行一些权限以外的操作,之前主要有两种方法:一是通过sudo提权;二是通过SUI