一, 什么是 PE 文件?
在 Windows 下所谓 PE 文件即 Portable Executable, 意为可移植的可执行的文件. 常见的. EXE,.DLL,.OCX,.SYS,.COM 都是 PE 文件. PE 文件有一个共同特点: 前两个字节为 4D 5A(MZ). 如果一个文件前两个字节不是 4D 5A 则其肯定不是可执行文件. 比如用 16 进制文本编辑器打开一个 ".xls" 文件其前两个字节为: 0XD0 0XCF; 打开一个 ".pdf" 其前两个字节为: 0X25 0X50.
PE 文件结构: DOS 头 + PE 头 + 节表 +.data/.rdata/.text. 而今天我们就来具体了解一下 PE 文件的 DOS 头和 PE 头的结构成员与部分成员的作用. 注意: 一个 exe 文件本身是一个 PE 文件, 但是由于包含 dll 库, 所以一个 exe 文件也是许多 PE 文件组成的 (包含多个 dll) 一个 PE 文件
1. DOS 头: 共 40H(64 字节)
DOS 头中声明用的寄存器(我们可以看到 e_ss,e_sp,e_ip,e_cs 还是 16 位的寄存器), 所以在 32 位 / 64 为系统中用到的只有两个成员了(第一个和最后一个):
e_magic: 判断一个文件是不是 PE 文件;
e_lfanew: 相对于文件首的偏移量, 用于找到 PE 头;
2. PE 头
PE 头分为标准 PE 头和可选 PE 头, 其同为 NT 结构的成员:
- //NT 头
- //pNTHeader = dosHeader + dosHeader->e_lfanew;
- struct _IMAGE_NT_HEADERS{
- 0x00 DWORD Signature; //PE 文件标识: ASCII 的 "PE"
- 0x04 _IMAGE_FILE_HEADER FileHeader;
- 0x18 _IMAGE_OPTIONAL_HEADER OptionalHeader;
- };
根据 DOS 头的 e_lfanew 成员我们就可以找到 NT 头, NT 头的第一个成员是 "PE"(0X50 0X45 0X00 0X00 四字节的签名, 可以在上图 00000100H 地址处观察), 后两个成员则分别是标准 PE 头 (_IMAGE_FILE_HEADER) 和可选 PE 头(_IMAGE_OPTIONAL_HEADER).
3. 几个重点的数据成员
(1) 文件对齐 (FileAlignment) 和内存对齐(SectionAlignment):
一个 PE 文件加载进内存中可能大于在硬盘上的大小, 并且无论是在内存中还是硬盘上, 都是是分块管理(分节), 一块和一块存储空间之间是空隙. 在硬盘上空隙有可能小于内存中空隙; 在内存中空隙较大(相较于硬盘). 而存在间隙的原因则是分块管理.
分块的一个原因是节省硬盘: 比如 notepad.exe, 由于是早期的程序, 当时硬盘容量比较小, 编译器在生成可执行文件时, 不仅要考虑效率问题使得内存对齐 / 文件对齐, 还需要设计成节省硬盘空间的结构. 所以这种结构遵循的对齐原则: 内存对齐 (1000H) 和硬盘对齐 (200H), 对齐的补充数据(0X0000) 便是间隙. 硬盘的对齐值较小, 补充间隙自然小, 因此同一个可执行程序在内存中可能比在硬盘上大. 但是现如今的硬盘空间更大, 所以编译器生成的可执行程序在硬盘上与内存中对齐方式都是 1000H. 统一对齐为 1000H 的目的依旧是提高效率.
而分块的另一个目的是节省内存空间, 比如同时在电脑上运行登录多个 QQ 账号, 就需要运行多次 QQ 可执行程序. 而代码段为只读数据需要一份即可, 数据段则需要为每个账号均开辟一份,, 多个 QQ 程序共享代码块, 单独使用数据块, 这样就节省了多份代码块的内存.(这些块是使用结构体来维护的, 分块即创建结构体).
(2) 镜像地址 / 基址 ImageBase 的作用:
FileBuffer 是磁盘上. exe 文件在内存中的一份拷贝, 但是 FileBuffer 无法直接在内存中运行, 必须经过 PE loader(装载器)装载以后成为 ImageBuffer.ImageBuffer 是 FileBuffer 的 "拉伸". 即 ".exe->FileBuffer->ImageBuffer"
.exe 首地址 (基址) 为 0
FileBuffer 首地址也为 0
ImageBuffer 首地址为 ImageBase
而真正的程序入口地址是: ImageBase + AddressOfEntryPoint(OEP)
一个 exe 文件默认镜像地址为 400000H(有可能不是, 总之有一个默认值), 如果一个 exe 文件中用到了多个 dll, 而 dll 文件作为一个 PE 文件, 其默认镜像地址也均是 400000H, 操作系统不会修改 exe 的镜像基址. 因为. exe 先被加载, 在. exe 中才加载的 dll 库, 由于 400000 已经被. exe 占用, 所以装载器会修改 dll 的镜像基址. 而采用 ImageBase + OEP 的目的也就是: 采用偏移地址的方式可以更方便地修改基址, 使得任何一个 dll 文件基址修改后程序依旧不会出错. 比如: dll 和 exe 基址有冲突, 本只需要将冲突的. dll 的文件基址修改为 600000H(假设是编译器为其分配的是 600000H); 如果不采用 "基址 + 偏移地址" 的方式, 而采用绝对地址, 那么要修改的就不是一个基址为 600000H 了, 而是 dll 中所有的地址统一加上 200000H(因为原来默认为 400000H).
二, 汇编基础知识
1. 寄存器
顾名思义, 寄存器就是暂时存储数据的地方, 寄存器被设计在 CPU 内部, 对于一个汇编程序员来说, CPU 中最主要的部分就是寄存器了. 寄存器是程序员能通过指令读写的部件, 程序员通过改变寄存器的值间接的控制 CPU
eax: 拓展累加寄存器;
ecx: 循环计数器;
edx: 数据寄存器;
ebx: 基址寄存器;
2. 堆栈
堆栈是连续的内存单元, 存取方式遵循 "先进后出" 原则, 栈是一种特殊的存储方式, 特殊在最先进入这个空间的数据却是最后出去的. 但是堆和栈不是同一个概念, 栈一般由编译器自动分配释放, 存储函数的参数值, 局部变量值等; 而堆, 一般由程序员分配释放, 程序结束时可能由 OS(系统)回收.
esp: 栈顶;
ebp: 栈底;
esi: 拓展目地指针;
edi: 拓展目地指针;
eip: 指令指针.
3. 汇编指令
汇编指令有 5 类:
数据传输指令: mov
逻辑计算指令: add
串操作指令: movs
控制转移指令: jmp
处理器控制指令: nop
其中 1,2,4 类指令对免杀有用.
4. 常用免杀汇编指令
mov ebp,9: 传送指令
push ebp : 进栈指令
pop ebp : 出栈指令
add esp,8 : 加法指令
sub esp,8 : 减法指令
inc ecx : 增量指令
dec ecx : 减量指令
jmp 00000001 : 无条件跳转指令
call 00000001 : 调用指令
xchg: 交换指令
pushad: 压栈 8 个寄存器
popad: 弹出 8 个寄存器(先进后出)
三, 免杀常用等价替换汇编指令修改方法
A 开头:
add 改 adc
ADD 改 ADC
ADD 1 改 sub -1
- add dword ptr ss:[ebp-130],edx ---------adc dword ptr ss:[ebp-130],edx
- ADD [EAX],CH----------------------------ADD [EAX],DH
- ADD [EAX],BH 0038 ----------------------ADD [EAX+40],AL 0040 40
- ADD [EAX+EAX*2+46],AL ------------------ADD [EAX+EAX*2+46],CL
- ADD [EAX+40],DL 0050 40 ----------------0058 40 ADD [EAX+40],DL
- ADD AH,CH 00EC -------------------------00F4 ADD AH,DH
- add dword ptr ss:[ebp-130],edx -------- adc dword ptr ss:[ebp-130],edx
C 开头:
CMP 改 SUB
call 复件_(4).004CF607 ----------------- push 复件_(4).004CF607
CMP DWORD PTR DS:[100170A4],0 -------------sub DWORD PTR DS:[100170A4],0
CALL --------- 看到了 CALL 跟随进去看 NOP 就可以把 CALL 的地址该成 NOP
方法 2-- 看下附近有没有 MOV 修该成 NOP 看下可以免杀不. 可以的话该 XOR
方法 3-- 看附近 jnz 跳转该下跳转的地址 / 可免杀不 /
CALL EAX |CALL EBX
比效指令 CMP: 看下是个比效指令 在看下 JNZ 条件转移指令
就是说 CMP 比效正确就跳那我们可以把 CMP 用 NOP 掉在把 JNZ 该成 JMP
不进行 CMP 比效
CMP ESI,1
call 改 jmp
D 开头:
DAA 组合的十进制加法调整指令 --------DAS 减法的十进制调整.
J 开头:
JE 改 JNB
JNZ 改 JNL
jnz 改 JB
JE 改 JNA
je 改 jb
jnz 改 jg
JS 改 jp
je 改 jle
jnz 改 jle
je 改 jge
JE 改 jnz
JE 改 JB
JNS 改 POP ECX
JNS 改 jnc-jnb
JNB 改 JGE
- jnb short fsg2_0.0040015D----------------ja short fsg2_0.0040015D
- JMP NEAR [1071c]---------------------JMP NEAR [1071B]
jnz--je-jmp 修改中要看下跳的地址是不是很重要说明[1]
JNZ 00874E85--MOV EAX,88B6D0 可以是该成 JE 00874E85--MOV EAX,88B6D0
L 开头:
LEA EBP,[ESP+10] 改 LEA EBP,[ESP+10]
M 开头:
MOVSX 改 MOVZX
MOV EBP,ESP 改 AND AH,CH
MOV [EBP-18],ESP 改 MOV [EBP-18],AH
MOV EAX,[ESP+10] 改 MOV EAX,[ESP+10]
MOV [ESP+10],EBP 改 MOV [ESP+10],EBP
mov [ebp-256], eax 改 adc [ebp-226], eax
MOV EDI,[EBP+10] 改 MOV EDI,[EBP+11]
MOV EBX,DWORD PTR DS:[ESI] 改 XOR EBX,DWORD PTR DS:[ESI]
- MOV EBP,ESP--------AND AH,CH
- MOV EBX,DWORD PTR DS:[ESI]---------XOR EBX,DWORD PTR DS:[ESI]
P 开头:
push 改 call
- PUSH EBX PUSH EDI
- PUSH ESI PUSH EAX
- PUSH EDI PUSH ESI
- PUSH EAX PUSH EBX
pop 改 nop
S 开头:
sbb 改 adc
sub 改 mov
SHL 改 SAL
SAR 改 SHR
- sub ebp,7---------- add ebp,-7
- sub ebx,eax----------sbb esi,ecx
- SBB ECX,DWORD PTR DS:[ESI+2]----------ADC ECX,DWORD PTR DS:[ESI+2]
- sub ebx,eax----------sbb esi,ecx
X 开头:
xor 改 sub
XOR [EAX],AL------- 改 --------MOV [EAX],AL
XOR EAX,EAX----- 改 -------OR EAX,EAX
来源: http://netsecurity.51cto.com/art/201903/593934.htm