Windows Shellcode 学习笔记——通过 VisualStudio 生成 shellcode
0x00 前言
shellcode 是一段机器码,常用作漏洞利用中的载荷(也就是 payload)
在渗透测试中,最简单高效的方式是通过 metasploit 生成 shellcode,然而在某些环境下,需要定制开发自己的 shellcode,所以需要对 shellcode 的开发作进一步研究
0x01 简介
编写 Shellcode 的基本方式有 3 种:
- 直接编写十六进制操作码
- 采用 C 或者 Delphi 这种高级语言编写程序,编译后,对其反汇编进而获得十六进制操作码
- 编写汇编程序,将该程序汇编,然后从二进制中提取十六进制操作码
本文将介绍如何通过 Visual Studio 编写 c 代码来生成 shellcode,具体包含以下三部分内容:
- 利用 vc6.0 的 DEBUG 模式获取 shellcode
- 测试 Shellcode 自动生成工具——ShellcodeCompiler
- 使用 C++编写(不使用内联汇编),实现动态获取 API 地址并调用,对其反汇编可提取出 shellcode
0x02 利用 vc6.0 的 DEBUG 模式获取 shellcode
注:本节参考爱无言的《挖 0day》附录部分
测试系统:
Windows XP
1、编写弹框测试程序并提取汇编代码
代码如下:
#include "stdafx.h"
#include <windows.h>
int main(int argc, char* argv[])
{
MessageBoxA(NULL,NULL,NULL,0);
return 0;
}
在 MessageBoxA(NULL,NULL,NULL,0);
处,按 F9 下断点
debug 模式按 F5 开始调试,跳到断点
按 Alt+8
将当前 C 代码转为汇编代码,如图
00401028 mov esi,esp
0040102A push 0
0040102C push 0
0040102E push 0
00401030 push 0
00401032 call dword ptr [__imp__MessageBoxA@16 (0042528c)]
call 是一条间接内存调用指令,实际使用需要真正的内存地址
按 Alt+6
打开查看内存数据的 Memory 窗口,跳到位置 0x0042528c
,如图
0042528C EA 07 D5 77 00 00 00 ..誻...
取前 4 字节,倒序排列(内存中数据倒着保存): 77D507EA
call 命令的实际地址为 0x77D507EA
MessageBoxA 函数位于 user32.dll 中,调用时需要提前加载 user32.dll
2、编写内联汇编程序并提取机器码
新建工程,使用内联汇编加载上述代码:
#include "stdafx.h"
#include <windows.h>
int main(int argc, char* argv[])
{
LoadLibrary("user32.dll");
_asm
{
push 0
push 0
push 0
push 0
mov eax,0x77D507EA
call eax
}
return 0;
}
编译执行,成功弹框
在 push 0 处按 F9 下断点,F5 进入调试模式跳至断点处
按 Alt+8
将当前 VC 代码转为汇编代码,如图
12: push 0
0040103C push 0
13: push 0
0040103E push 0
14: push 0
00401040 push 0
15: push 0
00401042 push 0
16: mov eax,0x77D507EA
00401044 mov eax,77D507EAh
17: call eax
00401049 call eax
接着提取上述代码在内存中的数据,如图
范围是 0040103C - 0040104A
注:call eax 的地址为 00401049,表示起始地址,完整代码的长度需要+1
按 Alt+6
打开查看内存数据的 Memory 窗口
跳到 0x0040103C,内容如下:
0040103C 6A 00 6A 00 6A 00 6A 00 B8 EA 07 D5 77 FF D0 j.j.j.j.戈.誻..
截取 0040103C - 0040104A 的内容如下:
6A 00 6A 00 6A 00 6A 00 B8 EA 07 D5 77 FF D0
这段机器码就是接来下要使用的 shellcode
3、编写加载 shellcode 的测试程序
#include "stdafx.h"
#include <windows.h>
int main(int argc, char* argv[])
{
LoadLibrary("user32.dll");
char shellcode[]="\x6A\x00\x6A\x00\x6A\x00\x6A\x00\xB8\xEA\x07\xD5\x77\xFF\xD0";
((void(*)(void))&shellcode)();
return 0;
}
成功执行 shellcode
注:由于 Win7 系统引入了 ASLR 机制,因此我们不能在 shellcode 中使用固定的内存地址,上述方法在 Win7 下不通用
0x03 Shellcode 自动生成工具——ShellcodeCompiler
下载地址:https://github.com/NytroRST/ShellcodeCompiler
特点:
- c++开发
- 开源工具
- 借助 NASM
- 可实现封装 api,转换为 bin 格式的 shellcode 和 asm 汇编代码
实际测试:
Source.txt 内容如下:
function MessageBoxA("user32.dll");
function ExitProcess("kernel32.dll");
MessageBoxA(0,"This is a MessageBox example","Shellcode Compiler",0);
ExitProcess(0);
cmd 下运行:ShellcodeCompiler.exe -r Source.txt -o Shellcode.bin -a Assembly.asm
注:ShellcodeCompiler.exe 和文件夹 NASM 放于同级目录
执行后 shellcode 保存在 Shellcode.bin 文件中
为便于测试生成的 shellcode,可在生成过程中加入 -t
参数执行一次 shellcode
我参考 ShellcodeCompiler 的代码将其执行 shellcode 的功能提取出来,实现了读取文件并加载文件中的 shellcode,完整代码如下:
#include <windows.h>
size_t GetSize(char * szFilePath)
{
size_t size;
FILE* f = fopen(szFilePath, "rb");
fseek(f, 0, SEEK_END);
size = ftell(f);
rewind(f);
fclose(f);
return size;
}
unsigned char* ReadBinaryFile(char *szFilePath, size_t *size)
{
unsigned char *p = NULL;
FILE* f = NULL;
size_t res = 0;
// Get size and allocate space
*size = GetSize(szFilePath);
if (*size == 0) return NULL;
f = fopen(szFilePath, "rb");
if (f == NULL)
{
printf("Binary file does not exists!\n");
return 0;
}
p = new unsigned char[*size];
// Read file
rewind(f);
res = fread(p, sizeof(unsigned char), *size, f);
fclose(f);
if (res == 0)
{
delete[] p;
return NULL;
}
return p;
}
int main(int argc, char* argv[])
{
char *szFilePath=argv[1];
unsigned char *BinData = NULL;
size_t size = 0;
BinData = ReadBinaryFile(szFilePath, &size);
void *sc = VirtualAlloc(0, size, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (sc == NULL)
{
return 0;
}
memcpy(sc, BinData, size);
(*(int(*)()) sc)();
return 0;
}
0x04 C++编写(不使用内联汇编),实现动态获取 API 地址并调用,对其反汇编可提取出 shellcode
对于 ShellcodeCompiler,最大的不足是使用了内联汇编,vc 在 64 位下默认不支持内联汇编,所以该方法无法生成 64 位 shellcode
注:delphi 支持 64 位的内联汇编,vc 在 64 位下虽然不能直接使用内联汇编,但是可以将程序段全部放到一个 asm 文件下进行编译
X64 上恢复 VS 关键字__asm 的方法可参照:http://bbs.pediy.com/showthread.php?p=1260419
那么,想要开发一个 64 位的 shellcode,最直接的方式就是不使用内联汇编,纯 c++编写,实现动态获取 API 地址并调用,最后对其反汇编进而得到 shellcode
好处如下:
便于调试,源代码的可读性大大增强
但是,我在网上并没有找到现成的代码,于是根据原理尝试自己实现
注:
1、编写 shellcode 需要实现以下步骤:
- 获取 kernel32.dll 基地址
- 定位 GetProcAddress 函数地址
- 使用 GetProcAddress 确定 LoadLibrary 函数地址
- 使用 LoadLibrary 加载 DLL 文件
- 使用 GetProcAddress 查找某个函数的地址(例如 MessageBox)
- 指定函数参数
- 调用函数
2、另一个参考资料:http://bbs.pediy.com/showthread.php?t=203140
参考资料通过 c++实现了加载一个第三方 dll
以此为参考进行修改,实现我们想要的功能:
实现动态获取 API 地址并调用
完整代码已上传至 github:https://github.com/3gstudent/Shellcode-Generater
特点:
- 支持 x86 和 x64
- 纯 c++实现,动态获取 GetProcAddress 和 LoadLibrary 函数的地址
编译前对 VisualStudio 做如下配置:
1、使用 Release 模式。近来编译器的 Debug 模式可能产生逆序的函数,并且会插入许多与位置相关的调用。
2、禁用优化。编译器会默认优化那些没有使用的函数,而那可能正是我们所需要的。
3、禁用栈缓冲区安全检查(/Gs)。在函数头尾所调用的栈检查函数,存在于二进制文件的某个特定位置,导致输出的函数不能重定位,这对 shellcode 是无意义的
接着在 IDA 下打开生成的 exe 获得机器码即可
0x05 补充
接下来研究的内容:在 X64 上恢复 VS 关键字__asm 后,如何获取 64 位 shellcode
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: Userland registry hijacking
下一篇: Covenant 利用分析
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论