AI 声明

本文由 AI 辅助整理,基于参考资料进行结构化编辑。

概述:Shellcode 是一段机器码,常作为漏洞利用的载荷。本文介绍通过 Visual Studio 编写 C 代码生成 Shellcode 的三种方法。

什么是 Shellcode

Shellcode 是一段可以直接执行的机器码,通常用作漏洞利用中的载荷(Payload)。在渗透测试中,最简单的方式是通过 Metasploit 生成,但在某些场景下需要定制开发。

编写 Shellcode 的三种方式

方式优点缺点
直接编写十六进制操作码精确控制难度高、易出错
高级语言编译后反汇编可读性好需要额外提取步骤
汇编程序提取高效、可控需要汇编知识

一、利用 VC6.0 DEBUG 模式获取 Shellcode

原理

通过调试器下断点,查看 C 代码对应的汇编指令,再从内存中提取机器码。

步骤

1. 编写测试程序并提取汇编代码

#include "stdafx.h"
#include <windows.h>
 
int main(int argc, char* argv[])
{
    MessageBoxA(NULL, NULL, NULL, 0);
    return 0;
}

MessageBoxA 处按 F9 下断点,F5 调试运行,Alt+8 查看汇编:

00401028 mov esi,esp
0040102A push 0
0040102C push 0
0040102E push 0
00401030 push 0
00401032 call dword ptr [__imp__MessageBoxA@16 (0042528c)]

获取真实地址

Alt+6 打开内存窗口,跳转到 0x0042528c,读取前 4 字节并倒序排列:

  • 内存数据:EA 07 D5 77
  • 实际地址:0x77D507EA

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;
}

从内存提取机器码(范围 0040103C - 0040104A):

6A 00 6A 00 6A 00 6A 00 B8 EA 07 D5 77 FF D0

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;
}

ASLR 限制

Windows 7 及以上系统引入了 ASLR(地址空间布局随机化),固定内存地址的方法不再通用。


二、ShellcodeCompiler 自动生成工具

工具特点

使用方法

Source.txt:

function MessageBoxA("user32.dll");
function ExitProcess("kernel32.dll");
MessageBoxA(0, "This is a MessageBox example", "Shellcode Compiler", 0);
ExitProcess(0);

命令行:

ShellcodeCompiler.exe -r Source.txt -o Shellcode.bin -a Assembly.asm

添加 -t 参数可直接测试生成的 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;
    
    *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];
    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;
}

三、纯 C++ 编写(支持 x64)

问题

VC 在 64 位下默认不支持内联汇编,ShellcodeCompiler 无法生成 64 位 Shellcode。

解决方案

使用纯 C++ 编写,动态获取 API 地址并调用,最后反汇编提取 Shellcode。

编写 Shellcode 的核心步骤

  1. 获取 kernel32.dll 基地址
  2. 定位 GetProcAddress 函数地址
  3. 使用 GetProcAddress 获取 LoadLibrary 地址
  4. 加载所需 DLL
  5. 获取目标函数地址
  6. 指定参数并调用

Visual Studio 配置要点

配置项设置原因
编译模式ReleaseDebug 模式可能产生逆序函数和位置相关调用
优化禁用防止编译器优化掉”未使用”的函数
栈检查禁用 (/Gs)栈检查函数位于特定位置,导致无法重定位

开源项目

完整代码见 GitHub: 3gstudent/Shellcode-Generater

  • 支持 x86 和 x64
  • 纯 C++ 实现
  • 动态获取函数地址

参考资料