freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

从0手工构造64位PE并手工进行加壳
2023-04-11 19:29:56
所属地 北京

1.前言

本文的目的是使用010Editor从0手工构造一个64位的PE文件(PE32+),主要功能是调用MessageBoxA函数弹框并且调用ExitProcess函数结束进程,之后使用010Editor对手工构造的64位的PE文件进行手工加壳,手工加壳的过程是使用异或加密原AddressOfEntryPoint(OEP),添加一个新区段并将PE文件的OEP改为新区段,新区段的功能主要就是先异或解密原OEP然后跳转到件原OEP执行。

2.环境

  • tools:010Editor

  • os:Windows10 20H2 19045

3.手工构造64位PE

PE文件格式简介

PE格式是由Unix中的COFF格式修改而来的,在Windows开发环境中PE格式也称为PE/COFF格式,可移植性可执行文件(Portable Executable,缩写为PE)是一种用于可执行文件、目标文件和动态链接库的文件格式,主要使用在32位和64位的Windows操作系统上,可移植性是指该文件格式的通用性,可用于许多种不同的操作系统和体系结构中。PE文件格式封装了Windows操作系统加载可执行程序代码时所必需的一些信息,这些信息包括动态链接库、API导入和导出表、资源文件和线程局部存储数据等等,PE文件格式在Windows下的扩展名有exe、scr、dll、sys、ocx等等,本文要构造的是扩展名为exe的64位PE文件。下图就是典型PE文件的主要结构
image

了解PE文件我们还需要了解一些知识点,ImageBase(基址)就是PE文件从硬盘加载到内存后的基地址,VA(虚拟地址)VirtualAddress就是PE文件加载到内存后的虚拟内存地址,RVA(相对虚拟地址)就是相对于PE文件ImageBase(基址)的偏移、FOA(文件偏移)就是PE文件在硬盘当中相对于文件头部的偏移。还有Windows下的数据存储方式,十六进制数字一般都是小端序存储(低字节存储在内存的低地址,高字节储在内存的高地址),字符串一般都是大端序存储(低字节存储在内存高地址,高字节存储在内存低地址)

3.1 DosHeader

我们来看微软在winnt.h头文件中定义的DosHeader的结构体,DosHeader一共占64个字节,其中我们需要关注的字段为e_magic和e_lfanew,e_magic固定值为0x4D5A(MZ),e_lfanew中存放的值为NtHeader结构的FOA
image

此为我们手工构造的DosHeader,重点就是2个字段e_magic为固定值0x4D5A(大端序存储方式),e_lfanew字段值填为了0x40,其他字段全部填为0
image

3.2 DosStub

我们查看一个使用Visual Studio编译的64位PE文件的DosStub,其主要特征就是有一串ASCII字符!This program cannot be run in DOS mode,DosStub主要是为了容MS-DOS而存在可有可无并不会影响PE文件的运行,所以我们可以直接将DosStub结构给删除,这样NtHeader就能直接紧跟在DosHeader后面,所以e_lfanew字段值我们填为了0x40
image

3.3 NtHeader

我们查看微软在winnt.h头文件中定义的NtHeader的结构体,可以看到NtHeader分为32位和64位,其中Signature为固定值0x5045(PE),FileHeader结构是相同的,主要不同就是OptionalHeader分为了32为和64位
image

这里我们先填入Signature,ASCII字符串是大端序存储所以为填为50 45 00 00
image

3.3.1 FileHeader

我们看看FileHeader结构体在winnt.h中的定义,FileHeader占20个字节,其中我们需要关注的重要字段为Machine、NumberOfSections、SizeOfOptionalHeader、Characteristics
image

Machine字段我们可以查看MSDN中的介绍一般32位PE文件值为0x014c,64位的PE文件值为0x8664
image

我们要构造的为64为PE文件所以Machine字段就填为64 86(小端序存储方式)
image

NumberOfSections字段为PE文件所拥有的Section数量,可以看到MSDN中告诉我们Windows下Section限制为96个,此字段要根据PE文件实际拥有的Section个数填写,按照我们的目标我们构造的PE文件应该拥有两个Section分别为.text和.rdata,其中.text用于存放可执行代码,.rdata用于存放导入表相关的数据
image

NumberOfSections字段我们填为2
image

TimeDateStamp字段为编译时间戳,此字段为编译器编译时填充,此字段不影响PE文件运行所以我们这里直接填充为0
image

PointerToSymbolTable字段为COFF符号表相关的,直接填0
image

NumberOfSymbols字段为符号表中的符号数,直接填0
image

SizeOfOptionalHeader字段为OptionalHeader结构的大小,我们知道OptionalHeader结构大小在32位PE和64位PE下是不相同的,在32位PE中为0xE0,64位为0xF0大小,我们要构造的为64位PE所以填为F0 00
image

Characteristics字段为PE文件的特征,MSDN上介绍了此字段可以设为的值
image

我们要构造64位的PE文件所以将此字段填为IMAGE_FILE_EXECUTABLE_IMAGE | IMAGE_FILE_LARGE_ADDRESS_AWARE也就是0x22
image

3.3.2 OptionalHeader

此为32位PE文件的OptionalHeader结构
image

此为64位PE文件的OptionalHeader结构,可以看到32位和64位PE文件的OptionalHeader字段并没有改变,只是有几个字段的类型改为了ULONGLONG(8字节)
image

接下来我们填写重要字段,首先Magic字段根据MSDN中的介绍我们64位PE应该将此值填写为IMAGE_NT_OPTIONAL_HDR64_MAGIC(0x20b)
image

小端序填为0B 02
image

MajorLinkerVersion到SizeOfUninitializedData一共5个字段共14个字节我们全部填为0,因为这些字段并不影响PE文件的运行
image

AddressOfEntryPoint字段也就是PE文件的入口函数的RVA,这里我们填为0x1000
image

BaseOfCode填0
image

ImageBase字段为PE文件在没有开启ASLR的情况下加载到内存后默认的基址,VC编译器生成的32位可执行程序默认基址一般为0x400000,64位默认为0x140000000
image

所以这里我们将ImageBase字段填为 00 00 00 40 01 00 00 00
image

SectionAlignment字段为PE文件加载到内存后的对齐方式,默认为系统页的大小4K(0x1000)
image

SectionAlignment字段我们填为00 10 00 00
image

FileAlignment字段为PE文件在硬盘中的对齐方式,一般默认为512(0x200)
image

FileAlignment字段填为 00 02 00 00
image

MajorOperatingSystemVersion字段为所需操作系统的主版本号,较新版本的VC编译器一般默认填为6(Windows Vista/Windows Server 2008),经过测试此值也可填为0并不影响PE文件运行
image

接下来的MinorOperatingSystemVersion、MajorImageVersion、MinorImageVersion字段共6个字节我们全部填为0
image

MajorSubsystemVersion字段为子系统的主版本号,此值我们填为6
image

MinorSubsystemVersion、Win32VersionValue字段一共6字节填为0
image

SizeOfImage字段为PE文件加载到内存后占用的虚拟内存空间大小,此值必须是SectionAlignment的倍数,这里我们就填为0x3000
image

SizeOfHeaders字段为PE文件中IMAGE_DOS_HEADER的e_lfanew字段到SectionHeaders结构结束位置的大小并且要为FileAlignment字段的整数倍
image

我们将SizeOfHeaders填为0x200
image

CheckSum填为0
image

Subsystem字段用来区分PE文件的类型是exe(CUL或GUI)还是dll、sys等等
image

这里我们填为IMAGE_SUBSYSTEM_WINDOWS_CUI(3)
image

DllCharacteristics字段为PE文件的DLL特征,可以决定PE文件是否开启重定位、SEH、DEP等等
image

我们不需要开启任何特性直接将DllCharacteristics字段填为0
image

接下来SizeOfStackReserve到LoaderFlags一共5个字段36个字节我们全部填为0
image

NumberOfRvaAndSizes字段为数据目录表的数量,一般默认为16个数据目录表,所以我们填为0x10
image

DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]字段是由IMAGE_DATA_DIRECTORY结构组成,每一个IMAGE_DATA_DIRECTORY结构结构都包含2个字段VirtuallAddress(表的RVA)和Size(表的大小),此结构的数量由NumberOfRvaAndSizes字段指定
image

16个数据目录表的默认排序为下,并且MSDN还给出了32位PE和64位PE下每个数据目录表相对于OptionalHeader头部的偏移量
image

16个数据目录表每个占8字节有16个表,所以我们插入128个字节,其中第二项的导入表信息我们需要填写,第一个字段为导入表的VirtualAddress(RVA),我们填写为0x2020,第二个字段为Size也就是导入表的大小,我们要导入两个函数MessageBoxA和ExitProcess分别位于USER32.dll和KERNEL32.dll中,所以需要2个导入表结构,一个导入表结构大小为20字节,我们需要两个就是40字节,导入表的结束标志也是20个字节的0,所以导入表结构大小为0x3C(60字节)
image

3.4 SectionHeaders

接下来的结构就是SectionHeaders了,我们需要2个SectionHeaders一个为.text代码段一个为.rdata导入表,Section中文一般称为节或者段
image

每个SectionHeaders为40个字节大小,我们可以查看MSDN上对SectionHeaders各个字段的详细介绍,我们主要关注其中的5个重要字段请看下小节
image

3.4.1 .text SectionHeaders

首先Name字段为段名称我们填写为.text
image

VirtualSize字段为加载到内存中的段的总大小,也就是我们.text代码段中实际的汇编代码大小,这里我们填为0x22(34字节)
image

VirtualAddress字段为PE文件加载到内存后.text段的RVA,此值需为SectionAlignment字段的倍数我们这里填为0x1000
image

SizeOfRawData字段为在硬盘中对齐后的大小此值需为FileAlignment字段的倍数,我们这里填为0x200
image

PointerToRawData字段为代码段的FOA也必须为FileAlignment字段的倍数,也填为0x200
image

PointerToRelocations到NumberOfLinenumbers的4个字段一共12字节不影响PE文件运行所以填为0
image

Characteristics字段为当前段加载到内存后的的权限,可在MSDN中查看所有可设置的标志,当前.text代码段需要可读可执行权限,所以我们将Characteristics字段设置为IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_CNT_CODE也就是0x60000020
image

3.4.2 .rdata SectionHeaders

Name字段填为.rdata
image

VirtuslSize字段为.rdata导入表的实际大小,填为0xB0
image

VirtualAddress字段要根据上一个段的VirtualAddress + SectionAlignment来填写,也就是0x1000(.text段VirtualAddress) + 0x1000(SectionAlignment字段) = 0x2000
image

SizeOfRawData字段填为0x200
image

PointerToRawData字段为.text段的PointerToRawData字段+SizeOfRawData字段,也就是0x200+0x200=0x400,所以我们将.rdata段的PointerToRawData字段填为0x400
image

PointerToRelocations到NumberOfLinenumbers的4个字段一共12字节不影响PE文件运行所以填为0
image

Characteristics字段设置为IMAGE_SCN_MEM_READ | IMAGE_SCN_CNT_INITIALIZED_DATA也就是0x40000040
image

3.5 Section

接下来就是区段数据了,.text区段的PointerToRawData字段为0x200,所以在SectionHeaders后面需要填104个字节的0,在偏移0x200处开始为.text区段
image

3.5.1 .text

从文件偏移0x200处开始添加0x200个字节的0,这0x200字节就是.text区段了
image

这就是我们.text段要添加的汇编代码一共34字节,主要功能就是调用MessageBoxA弹框并调用FatalExit(ExitProcess)函数退出进程,这段汇编代码其他部分都无需解释,主要就是在函数调用指令FF15 XXXXXXXX(CALL)的地址解析问题

0000000140001000 | 48:83EC 28          | sub rsp,28
0000000140001004 | 45:33C9             | xor r9d,r9d
0000000140001007 | 4D:33C0             | xor r8,r8
000000014000100A | 48:33D2             | xor rdx,rdx
000000014000100D | 33C9                | xor ecx,ecx
000000014000100F | FF15 EB0F0000       | call qword ptr ds:[<&MessageBoxA>]
0000000140001015 | 33C9                | xor ecx,ecx
0000000140001017 | FF15 F30F0000       | call qword ptr ds:[<&FatalExit>]
000000014000101D | 48:83C4 28          | add rsp,28
0000000140001021 | C3                  | ret

这里引用《逆向工程核心原理》一书中的内容,在x86下FF15(CALL)或FF25(JMP)后面的4字节为绝对地址(VA)指向IAT表的某个地址
image

在x64下虽然也是使用相同的指令,因为x64下地址变为了8个字节,如果直接填入VA的话4字节是不够的,但是x64下为了防止指令长度增加还是使用4字节地址,这4字节在x64下为相对地址(RVA),有一个计算公式:当前CALL/JMP指令地址 + RVA (IAT表RVA)+ 6(FF15/FF25指令长度),这里我们可以计算我们构造的PE文件的CALL MeessageBoxA函数指令的绝对地址(VA),0x14000100F + 0xFEB + 6 = 0x140002000,此地址也就是PE文件加载到内存后我们导入表中存放的MessageBoxA函数绝对地址(VA)
image

3.5.2 .rdata

接下来我们填写.rdata段也就是导入表,.rdata段是从0x400位置开始并且根据文件对齐方式0x200,我们从0x400位置开始填0x200个0代表导入表
image

3.6 ImportDescriptor

关于导入表我们要了解3个结构体,第1个为IMAGE_IMPORT_DESCRIPTOR结构,其中我们需要关注的字段是OriginalFirstThunk(指向首个IMAGE_THUNK_DATA结构的RVA)、Name(指向导入dll字符串的RVA)、FirstThunk(指向首个IAT的RVA)
image

第2个结构IMAGE_THUNK_DATA64,如果此结构的高位为1则说明函数是以序号方式导入,去除高位的值后就是要导入函数的导出序号了,因为我们构造的是函数名称导入方式所以此结构我们主要关注ForwarderString字段,此字段是指向INT(导入名称表)的RVA
image

第3个结构为IMAGE_IMPORT_BY_NAME也就是INT(导入名称表),Hint字段为导入的函数序号此值无意义有些编译器会将此值设为0,此结构我们主要关注Name字段(导入函数的ASCII字符串)
image

3.6.1 USER32.DLL

首先我们填入的7C 20 00 00 00 00 00 00为USER32.DLL中的导出函数MessageBoxA的IAT(导入函数地址表),在此PE文件被加载到内存中操作系统会将MessageBoxA的函数地址填充到此位置,IAT在硬盘中的值是指向INT(导入函数名称表的)RVA
image

我们将IAT的RVA转为FOA查看,0x207C - 0x2000(当前区段的RVA) + 0x400(当前区段的PointerToRawData) = 0x47C,我们查看0x47C位置就是IMAGE_IMPORT_BY_NAME结构了,前2个字节为Hint字段为导入函数的序号,后面紧跟着的就是导入函数MessageBoxA的ASCII字符串并以0结尾
image

接下来就是IMAGE_IMPORT_DESCRIPTOR结构,此结构的开始地址为我们在OptionalHeader中的IMAGE_DATA_DIRECTORY_ARRAY结构中的第2项Import的VirtualAddress字段指定,我们填的值是0x2020(RVA)转换为FOA就是0x2020 - 0x2000 + 0x400 = 0x420
image

OriginalFirstThunk字段为指向IMAGE_THUNK_DATA64的RVA,转换为FOA就是0x205C - 0x2000 + 0x400 = 0x45C,我们查看0x45C文件偏移处的IMAGE_THUNK_DATA64结构,我们需关注第一个字段ForwarderString此字段的值其实和IAT表的值相同都是指向了IMAGE_IMPORT_BY_NAME结构的RVA
image

接下来就是Name字段为0x208A,转为FOA为0x208A - 0x2000 + 0x400 = 0x48A,可以看到0x48A文件偏移处为USER32.dll字符串以00结尾
image

FirstThunk字段值为0x2000是指向首个IAT表的RVA,转换FOA为0x2000 - 0x2000 + 0x400 = 0x400,也就是我们最先介绍的IAT表,因为是64位所以占8个字节,在PE文件加载到内存后操作系统将会把MessageBoxA函数的地址填入此处
image

3.6.2 KERNEL32.DLL

接下来构造KERNEL32.dll中的导出函数ExitProcess的IAT表,这里和MessageBoxA的IAT表间隔了8字节的0,0x410偏移处填为0x2096,转为FOA之后0x2096 - 0x2000 + 0x400 = 0x496
image

查看0x496偏移处前2字节为Hint也就是导入函数的序号,之后紧跟着就是导入函数ExitProcess的ASCII字符串并以0结尾
image

接下来IMAGE_IMPORT_DESCRIPTOR结构,此结构的结束标志是20个字节的0(IMAGE_IMPORT_DESCRIPTOR结构大小)
image

首先OriginalFirstThunk字段值为0x206C指向IMAGE_THUNK_DATA64结构的RVA,转FOA后0x206C - 0x2000 + 0x400 = 0x46C,我们查看0x46C偏移处的IMAGE_THUNK_DATA64结构,我们只需关心第一个字段ForwarderString值为0x2096指向INT的RVA
image

Name字段值为0x20A4,转为FOA后0x20A4 - 0x2000 + 0x400 = 0x4A4,可以看到0x4A4处为KERNEL32.dll字符串
image

FirstThunk字段值为0x2010,转FOA为0x2010 - 0x2000 + 0x400 = 0x410,当PE文件被加载到内存后操作系统会将ExitProcess函数地址填充到此位置
image

3.7 添加字符串

此时我们双击运行PE已经可以正常弹窗了
image

使用x64dbg调试,可以看到导入表填充成功并且代码正常执行
image

虽然我们手工构造的PE文件可以正常运行并弹窗,但是为了美观还是要添加一些字符串,这里我修改了代码段的汇编代码并且将字符串放在了代码段,由于我们改动了代码段大小所以要将.text SectionHeader的VirtualSize改为0x3C
image

我们修改后的.text段汇编代码如下,主要修改了MessageBoxA函数的参数2(rdx)和参数3(r8)的传参使用lea指令传入我们添加在.text段尾部的字符串地址RVA

0000000140001000 | 48:83EC 28               | sub rsp,28                                         |
0000000140001004 | 45:33C9                  | xor r9d,r9d                                        |
0000000140001007 | 4C:8D05 22000000         | lea r8,qword ptr ds:[140001030]                    | 0000000140001030:"Hello"
000000014000100E | 48:8D15 21000000         | lea rdx,qword ptr ds:[140001036]                   | 0000000140001036:"x64 PE"
0000000140001015 | 33C9                     | xor ecx,ecx                                        |
0000000140001017 | FF15 E30F0000            | call qword ptr ds:[<&MessageBoxA>]                 |
000000014000101D | 33C9                     | xor ecx,ecx                                        |
000000014000101F | FF15 EB0F0000            | call qword ptr ds:[<&FatalExit>]                   |
0000000140001025 | 48:83C4 28               | add rsp,28                                         |
0000000140001029 | C3                       | ret                                                |

这里我们讲解一下lea r8指令的地址解析,Hello字符串的FOA为0x230,加载到内存后的绝对地址(VA)为0x230 - 0x200(.text段PointerToRawData) + 0x1000(.text段VA) + 0x140000000(ImageBase)= 0x140001030,在x64中由于是使用RVA(相对地址),所以0x140001030(字符串VA) - 0x140001007(lea r8指令地址VA) - 7(lea r8指令大小) = 22(字符串RVA),所以我们lea r8,0x140001030指令的OPCODE就为4C 8D 05 22 00 00 00了,lea rdx同理,注意由于我们添加了指令所以FF15 XXXXXXXX(CALL指令)的RVA也需要改变
image

我们双击运行PE文件可以看到现在字符串也已经正常显示了
image

4.手工加壳

壳一般分为压缩壳和加密壳,壳一般作为一个新区段添加到被加壳的PE文件,壳程序会先于原PE文件的OEP运行以实现解压缩、解密原PE文件的代码、数据,修复IAT、修复重定位等等后在跳转到原PE文件的OEP运行。我们这里主要模拟一个简易加密壳的功能,主要步骤就是在PE文件中添加一个新的.pack区段,并将PE文件的AddressOfEntryPoint改为新区段,新区段会将原PE文件的AddressOfEntryPoint也就是.text段解密然后跳转到.text段执行
image

接下来我们进行手工加壳,由于我们要添加区段所以将FileHeader结构的NumberOfSections字段加1改为3
image

接下来在SectionHeader后面的空字节位置添加新区段的SectionHeader,Name字段填为.pack,VirtualSize字段填为0x20,VirtualAddress字段填为0x3000,SizeOfRawData字段填为0x200,PointerToRawData字段填为0x600,Characteristics字段填为0x60000020(IMAGE_SCN_MEM_READ | IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_CNT_CODE)可读可执行的代码属性
image

之后在文件偏移0x600处添加0x200空字节作为我们的壳.pack区段
image

我们就模拟个简单的加密壳,将.text代码段的所有数据异或0x68,然后在.pack段解密并跳转到原OEP执行
image

由于异或解密操作时要修改.text区段内容,所以我们将.text区段的Characteristics添加IMAGE_SCN_MEM_WRITE(可写属性)也就是改为0xE0000020
image

接下来我们在.pack区段添加32个字节的汇编代码
image

汇编代码主要功能就是异或解密.text代码段,之后跳转到.text代码段执行

0000000140003000 | 49:BA 0010004001000000    | mov r10,pe64.exe.140001000                         | .text VA
000000014000300A | 48:31C9                   | xor rcx,rcx                                        |
000000014000300D | 41:80340A 68              | xor byte ptr ds:[r10+rcx],68                       | xor 0x68 decrypt .text
0000000140003012 | 48:FFC1                   | inc rcx                                            | index++ 
0000000140003015 | 48:83F9 3C                | cmp rcx,3C                                         | .text size
0000000140003019 | 75 F2                     | jne pe64.exe.14000300D                             | if(index != 0x3C)
000000014000301B | E9 E0DFFFFF               | jmp pe64.exe.140001000                             | jmp .text

之后修改OptionalHeader的AddressOfEntryPoint为壳区段的VA地址0x3000
image

修改OptionalHeader的SizeOfImage字段为0x4000
image

之后我们使用x64dbg进行调试,可以看到原OEP处0x140001000被异或加密后无意义汇编
image

通过异或0x68解密后的.text代码段可以看到汇编代码显示正常
image

跳转到.text代码段执行弹框正常
image

本篇文章构造的PE文件已经上传到github有需要的可以自取

5.总结

本篇文章我们通过使用010Editor从0手工构造了一个有2个导入函数的64位PE文件,主要功能就是调用函数MessageBoxA弹框并使用ExitProcess函数退出进程,之后又将将我们手工构造的64位PE文件进行手工加壳。PE文件格式是我们学习Windows下安全技术的基础,因为无论是Shellcode、免杀、脱壳、漏洞、恶意代码分析等等都和PE文件格式息息相关,本次我们通过手工的方式构造PE文件和手工加壳能够加深我们对PE文件的理解,为我们深入研究Windows安全打一个基础。

6.参考

《逆向工程核心原理》

https://bbs.kanxue.com/thread-226033.htm
https://learn.microsoft.com/zh-cn/windows/win32/debug/pe-format

# 加壳 # PE文件 # IAT
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录