Shellcode学习以及实现一个简易ShellcodeLoader

什么是 Shellcode

Shellcode 为16进制机器码,以其经常让攻击者获得 Shell 而得名。Shellcode 常常使用机器语言编写。 可在暂存器 eip 溢出后,塞入一段可让 CPU 执行的 Shellcode 机器码,让电脑可以执行攻击者的任意指令。

Shellcode is a small piece of code used as the payload in the exploitation of a software vulnerability.

例如 \x75 代表两个十六进制数,它会被翻译成汇编语言;
又例如:

这里 55 被翻译成了 push ebp
55这 2 个 16 进制转换成 8 位二进制,然后告诉 CPU 执行,CPU 识别发现是代码就会执行 push ebp

Local

本地 Shellcode 被攻击者使用,攻击者对计算机的访问权限有限,但可以利用该计算机上更高权限进程中的漏洞,例如缓冲区溢出。如果成功执行,Shellcode 将为攻击者提供与目标进程相同的更高权限的机器访问权限。

Remote

远程 Shellcode 执行成功,可被攻击者通过网络对计算机执行命令。

Download and Execute

这类 Shellcode 不会产生 Shell,计算机执行 Shellcode 后下载会产生 Shell 的软件/代码/脚本/Shellcode 执行获得 Shell。

Staged

When the amount of data that an attacker can inject into the target process is too limited to execute useful shellcode directly, it may be possible to execute it in stages. First, a small piece of shellcode (stage 1) is executed. This code then downloads a larger piece of shellcode (stage 2) into the process’s memory and executes it.

当注入的数据量有限制或者为了压缩体积时,采用分阶段的方法进行:

  1. 先执行一小段 Shellcode(人称小马),作用是将完整功能的 Shellcode 下载下来并执行(人称大马)
  2. 大马是具有完整功能的 Shellcode 或恶意程序,体积较大,功能较全

image.png
在 CobaltStrike 中的 Staged Payload:

使⽤ Wininet.dll 内函数通过 HTTP 接收 stage 然后反射加载

Stageless

执行即有相对完整的功能,也就是不分阶段。
在 CobaltStrike 中的 Stageless Payload:

不通过网络传输,直接加载 stage

无文件(内存马)和有文件

可以直接在内存中运行,不依赖硬盘文件运行,我们称之为无文件运行

比如利用 msfvenom 生成的 Shellcode,被翻译成汇编语言后,可以直接在内存中运行,生成的Shellcode 在成功运行后,我们在后续利用过程中可以做进程迁移,磁盘文件只是起到了一个触发作用,我们可以把这一段 Shellcode 迁移到其他进程上,此时触发文件可以直接删除,不会对木马进程造成影响。

如果木马程序与磁盘中的文件关联我们需要依托文件运行,我们称之为有文件(程序运行时我们会无法删除文件,只有关闭进程才能删除文件)

什么是 Shellcode 加载器

纯 Shellcode 是无法直接执行的,只有在 data 段的时候,才会被 CPU 识别成是代码,才有机会运行。因此需要一个加载器对 Shellcode 进行加载,并提供一定运行权限才能运行。

申请内存的方法

malloc 函数族:

  • virtualalloc
  • virtualallocex
  • realloc
  • calloc
  • sbrk

执行内存空间 Shellcode 的方法

  • 指针执行
  • 申请动态内存加载
  • 嵌入汇编加载
  • 强制类型转换
  • 汇编花指令

什么是PE文件

PE,(Portable Executable)是一种用于可执行文件目标文件动态链接库文件格式,主要使用在32位和64位的Windows操作系统上。Windows平台下的可执行文件的格式,我们称之为PE(Portable Executable)文件结构。

识别 PE 文件

你想要识别一个文件是不是PE文件,或者说是不是一个可执行文件,可以根据PE指纹来识别:首先你需要找到一个可以以16进制打开PE文件的工具(010 Editor),然后找到一个PE文件,用该工具打开PE文件,在文件的开始位置有一个0x5A4D(十进制:MZ),接着在0x003C位置向后有一个0x100,接着我们再去寻找0x100位置就会出现一个0x4550(十进制:PE),那么当你用这个方法可以顺利的走通整个流程找到PE,就表示这是一个PE文件,同样这也是一个PE指纹
image-20230816232635680
这里是Mac电脑看的,用的小端存储
image.png
如上示例中我使用的是 exe 后缀的文件,但即使不是 exe 后缀的文件,例如.sys.dll后缀的文件,实际上你通过这种方式会发现它们也是PE文件,所以我们不要只看后缀名来认定是不是PE文件,而要具体去看文件中的指纹
例如.acm, .ax, .cpl, .dll, .drv, .efi, .exe, .mui, .ocx, .scr, .sys, .tsp等后缀都是 PE 文件。

PE 文件的整体结构

image-20230816232612210

实现一个 ShellCode Loader

申请动态内存加载

申请一段动态内存,然后把 Shellcode 放进去,随后强转为一个函数类型指针,最后调用这个函数

1
2
3
4
5
6
7
8
9
10
#include <Windows.h>

int main()
{
char shellcode[] = "你的shellcode";

void* exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(exec, shellcode, sizeof shellcode);
((void(*)())exec)();
}

VirtualAlloc是一个Windows API函数,该函数的功能是在调用进程的虚地址空间,预定或者提交一部分页。
简单点的意思就是申请内存空间

VirtualAlloc 函数原形:

1
2
3
4
5
6
LPVOID VirtualAlloc{
LPVOID lpAddress, // 要分配的内存区域的地址
DWORD dwSize, // 分配的大小
DWORD flAllocationType, // 分配的类型
DWORD flProtect // 该内存的初始保护属性
};

其中:

  • LPVOID 代表没有任何类型的指针。
  • lpAddress:指定内存块的起始地址。如果该参数为 NULL,则系统会自动选择一个合适的内存地址
  • dwSize:指定要分配的内存块的大小(以字节为单位)。
  • flAllocationType:指定内存分配的类型。可以是下列值之一:
    • MEM_COMMIT:提交内存。
    • MEM_RESERVE:保留内存。
    • MEM_RESET:清除内存。
    • MEM_RESET_UNDO:撤销清除内存。
    • MEM_TOP_DOWN:从高地址向低地址分配内存。
  • flProtect:指定内存块的保护属性。可以是下列值之一:
    • PAGE_EXECUTE:允许执行代码。
    • PAGE_EXECUTE_READ:允许执行代码并读取数据。
    • PAGE_EXECUTE_READWRITE:允许执行代码、读取和写入数据。
    • PAGE_EXECUTE_WRITECOPY:允许执行代码、写入数据并复制。
    • PAGE_NOACCESS:禁止访问。
    • PAGE_READONLY:允许读取

memcpy函数原形:

1
void *memcpy(void *destin, void *source, unsigned n);

编译出来后执行 Shellcode 成功上线
image.png

或者创建堆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "Windows.h"

int main(int argc, char const *argv[])
{
unsigned char shellcode[] = "shellcode";

HANDLE HeapAddr = HeapCreate(HEAP_CREATE_ENABLE_EXECUTE, 0, 0);
void* exec = HeapAlloc(HeapAdd, HEAP_ZERO_MEMORY, sizeof(shellcode));

memcpy(exec, shellcode, sizeof shellcode);
((void(*)())exec)();

return 0;
}

HeapCreate 函数原型:

1
2
3
4
5
HANDLE HeapCreate(
[in] DWORD flOptions, //堆的句柄
[in] SIZE_T dwInitialSize,
[in] SIZE_T dwMaximumSize
);
  • flOptions:用于修改如何在堆栈上执行各种操作。

    • HEAP_CREATE_ENABLE_EXECUTE:堆中存放的内容是可以执行的代码。如果不设置,意味着堆中存放的是不可执行的数据。
    • HEAP_GENERATE_EXCEPTIONS:当堆分配内存失败时,会抛出异常。如果不设置,则返回NULL。
    • HEAP_NO_SERIALIZE:对堆的访问是非独占的,如果一个线程没有完成对堆的操作,其它线程也可以进程堆操作,这个开关是非常危险的,应尽量避免使用。
  • dwInitialSize:堆的初始大小,以字节为单位,该值向上舍入为系统页面的倍数,此值必须小于 dwMaximumSize, 若设为0,则提交一页。

  • dwMaximumSize:The maximum size of the heap, in bytes.

HeapAlloc函数原形:

1
2
3
4
5
DECLSPEC_ALLOCATOR LPVOID HeapAlloc(
[in] HANDLE hHeap,
[in] DWORD dwFlags,
[in] SIZE_T dwBytes
);
  • hHeap:堆的起始地址,一般是 HeapCreate 或 GetProcessHeap 函数的返回值。

  • dwFlags

    • HEAP_GENERATE_EXCEPTIONS
    • HEAP_NO_SERIALIZE
    • HEAP_ZERO_MEMORY
  • dwBytes:The number of bytes to be allocated.


Shellcode学习以及实现一个简易ShellcodeLoader
https://52hertz.tech/2023/08/16/shellcode/
作者
Ustin1an
发布于
2023年8月16日
许可协议