1.前言
本篇文章主要是介绍Windows x64下加载shellcode的方案,shellcode也称为PIC本质上就是一段位置无关的十六进制汇编代码。我们要加载运行shellcode主要可以分为3个关键点,第1点shellcode的存放位置,第2点申请可执行内存将shellcode复制进去,第3点将程序控制流转移到shellcode执行,所以本文也是围绕这3个关键点进阐述。
2.ShellCode存放
shellcode主要有2种常用格式,我们可以在kali linux通过msfvenom -p windows/x64/messagebox TEXT='ShellCode Execute!' -f c
生成一个用于弹出MessageBox的shellcode并导出为C语言数组格式
第2种常用格式就是raw类型,我们通过msfvenom -p windows/x64/messagebox TEXT='ShellCode Execute!' -f raw -o msgbox.bin
就可以生成raw类型shellcode并导出到文件中
2.1 .data段存放
通过如下C语言数组的方式我们就可以将刚刚生成的shellcode存放到PE文件中的.data段中
#include <stdio.h>
#include <Windows.h>
unsigned char ShellCode[] = {/*shellcode存放*/};
int main()
{
printf("ShellCode Address:%#llx\n", ShellCode);
return 0;
}
可以看到此时shellcode的地址位于PE文件中的.data段中,.data段内存保护属性为RW
有一种方案我们也可以将.data段赋予执行权限,我们可以通过如下编译器预处理指令可以将.data段的内存保护属性更改为RWX,不过这种方案不推荐使用因为.data段拥有RWX内存保护属性是一个可疑指标
#pragma comment(linker,"/SECTION:.data,ERW")
我们使用vs自带的dumpbin工具查看.data段的区段头表,其实上述编译器预处理指令本质就是在区段头表中的Characteristics字段附加了可执行属性
2.2 .rdata段存放
只要通过const语句修饰C语言数组我们就可以将shellcode存放到PE文件中的.rdata段
#include <stdio.h>
#include <Windows.h>
const unsigned char ShellCode[] = {/*shellcode存放*/};
int main()
{
printf("ShellCode Address:%#llx\n", ShellCode);
return 0;
}
可以看到此时shellcode的地址在PE文件中的.rdata段中,.rdata段的内存保护属性为仅可读
和.data段类似.rdata段我们也可以通过如下编译器预处理指令附加可执行权限
#pragma comment(linker,"/SECTION:.rdata,ERW")
2.3 .rsrc段存放
我们在资源文件栏右键->添加->资源
之后会自动生成resource.h和以当前工程项目命名的.rc文件然后弹出添加资源的小窗口
接下来我们点击导入然后选择我们之前导出的shellcode文件,在资源类型填写这里可以填写为自定义字符这里我就填为RCBIN了
此时shellcode已经作为资源添加到PE文件中,我们可以看到资源视图下我们添加的RCBIN资源类型和自动生成的IDR_RCBIN1资源ID
在resource.h头文件中我们可以修改自动生成的资源ID和名称
之后我们可以通过调用一系列函数组合获取在.rsrc区段的shellcode地址和大小,具体代码实现将在后续小节给出。我们可以看到此时shellcode地址位于.rsrc区段,.rsrc区段的内存保护属性为只读
2.4 HTTP服务器存储
之前小节的介绍的shellcode存储方式都是在当前PE文件中,本小节我们将shellcode存储于HTTP服务器,这种将shellcode存储在PE文件之外的方式也被称为分离加载,这里HTTP服务器我通过Python3的python -m http.server 8888
搭建,从HTTP服务器下载数据的代码实现将在后续小节给出
3.可执行内存
3.1 寻找合法可执行权限内存
合法可执行权限内存也就是不通过win32 api动态申请可执行内存的情况下在本进程自然存在的拥有可执行权限的内存,我们可以使用ProcessHacker工具看到在当前进程中拥有合法可执行权限内存的一个就是当前进程的.text段还有当前进程加载到内存的dll的.text段,而且这2种可执行内存类型都为image并且内存保护属性都是RX
如果要利用本进程的.text段存放shellcode最简便的方法就是通过如下的编译器预处理指令将shellcode存放到本进程的.text段,由于.text段内存保护属性默认为RX所以我们可以跳过动态申请可执行内存的步骤
#pragma section(".text")
__declspec(allocate(".text")) unsigned char ShellCode[] = {/*shellcode存放*/};
如果要利用加载到本进程的dll的.text段存放shellcode,首先我们需要解析目标dll的PE结构得出.text位置和大小,并且由于.text段内存保护属性默认为RX属性,我们需要将目标dll的.text段内存保护属性改为RWX将shellcode覆盖后再修改回原内存权限,所以我们需要寻找shellcode的功能实现中不会加载的dll否则会产生异常,具体的代码实现将在后续小节中给出这里就通过一个示意图来表达
3.2 VirtualAlloc
通过VirtualAlloc函数直接申请RWX权限内存是最常用的动态申请可执行内存操作,在这里我们是先申请了RW保护属性的内存后续再通过VirtualProtect函数修改内存保护属性为RX这样更符合OPSEC
#include <stdio.h>
#include <Windows.h>
unsigned char ShellCode[] = {/*shellcode存放*/};
int main()
{
DWORD dwOldProtect = 0;
// 申请RW内存保护属性的虚拟内存
LPVOID lpShellCode = VirtualAlloc(NULL, sizeof(ShellCode), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// 内存分配失败
if (lpShellCode == NULL) {
return -1;
}
// 将shellcode复制到申请的虚拟内存中
RtlMoveMemory(lpShellCode, ShellCode, sizeof(ShellCode));
// 修改虚拟内存保护属性为RX
VirtualProtect(lpShellCode, sizeof(ShellCode), PAGE_EXECUTE_READ, &dwOldProtect);
printf("ShellCode Address:%#llx\n", lpShellCode);
// 释放内存
VirtualFree(lpShellCode, 0, MEM_RELEASE);
return 0;
}
我们使用ProcessHacker查看我们申请的虚拟内存保护属性为RX并且shellcode写入成功
VirtualAlloc函数本质上是一个stub最终会通过调用3环下的NtAllocateVirtualMemory函数通过syscall调用进入0环的代码实现执行
3.3 HeapCreate
我们可以调用HeapCreate函数在参数1传入HEAP_CREATE_ENABLE_EXECUTE创建一个RWX内存保护属性的堆内存
#include <stdio.h>
#include <Windows.h>
unsigned char ShellCode[] = {/*shellcode存放*/};
int main()
{
// 创建一个默认大小RWX内存保护属性的堆
HANDLE hHeap = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 0, 0);
if (hHeap == NULL) {
return -1;
}
// 分配堆内存
LPVOID lpShellCode = HeapAlloc(hHeap, HEAP_ZERO_MEMORY, sizeof(ShellCode));
if (lpShellCode == NULL) {
HeapDestroy(hHeap);
return -1;
}
// 复制shellcode到堆内存
RtlMoveMemory(lpShellCode, ShellCode, sizeof(ShellCode));
printf("ShellCode Address:%#llx\n", lpShellC