Shellcode的原理与编写

简介

什么是shellcode

ShellCode是不依赖环境,放到任何地方都能够执行的机器码

编写ShellCode的方式有两种,分别是用编程语言编写或者用ShellCode生成器自动生成

ShellCode生成器生成的shellcode功能比较单一,常见的ShellCode生成器有shell storm、Msfvenom等

而用编程语言写的shellcode会更加突显灵活性,可以自己添加或修改功能

shellcode的原理

将shellcode注入缓冲区,然后欺骗目标程序执行它。而将shellcode注入缓冲区最常用的方法是利用目标系统上的缓冲区溢出漏洞。

环境搭建

1.修改程序入口点

为什么要修改程序入口点?VS新建一个32位的控制台程序,如下代码所示

int main()
{
    return 0;
}

然后把生成的程序拖入IDA中打开,在左边的函数窗口可以发现除了main函数以外还有一些其他函数,这是因为没有修改函数入口点所导致的

在VS中的项目属性找到链接器的高级选项进行修改函数入口点

在配置选项将Debug修改成Release, 解决方案配置也修改成Release

将重新生成的程序拖入IDA中, 可以发现少了很多的函数

2.关闭缓冲区安全检查

项目属性的C++代码生成处禁用安全检查

若没禁用安全检查,那么ShellCode则无法执行

3.设置项目兼容XP系统

在项目属性的C++代码生成处更改成MT(Release版本修改成MT,Debug版本则修改成MTD)

4.关闭生成清单

5.关闭生成调试信息

6.启用最大优化

选择最大优化,这样生成的shellcode会小些

探索PEB

什么是PEB

TEB:全称Thread Environment Block,中文名为线程环境块。系统在此TEB中保存频繁使用的线程相关的数据,一般存储在fs:[0]

PEB:全称Process Environment Block,中文名为进程环境块。存放进程信息,每个进程都有自己的PEB信息。通常存储在fs:[0x30]。这里主要通过PEB来获取kernel32.dll的基址

获取kernel32的基址

1.TEB->PEB

在TEB结构搜寻PEB,如下图所示TEB的结构代码, peb结构在30h的位置

在汇编里fs:[0]代表TEB的地址, 所以PEB的地址是fs:[30]

2.PEB->LDR

如下代码是PEB结构, LDR处于0C位置

LDR: 包含加载模块的信息

typedef struct _PEB {
  BYTE                          Reserved1[2];
  BYTE                          BeingDebugged;
  BYTE                          Reserved2[1];
  PVOID                         Reserved3[2];
  PPEB_LDR_DATA                 Ldr;   //0c  Ldr
  PRTL_USER_PROCESS_PARAMETERS  ProcessParameters;
  PVOID                         Reserved4[3];
  PVOID                         AtlThunkSListPtr;
  PVOID                         Reserved5;
  ULONG                         Reserved6;
  PVOID                         Reserved7;
  ULONG                         Reserved8;
  ULONG                         AtlThunkSListPtr32;
  PVOID                         Reserved9[45];
  BYTE                          Reserved10[96];
  PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
  BYTE                          Reserved11[128];
  PVOID                         Reserved12[1];
  ULONG                         SessionId;
} PEB, *PPEB;

3.LDR->InLoadOrderModuleList

LDR指向PEB_LDR_DATA结构, 要注意下图三个用蓝色标明的成员,分别是InMemoryOrderModuleListInLoadOrderModuleListInInitializationOrderModuleLists,这三者分别代表模块在不同状态下的排列顺序

也就是说这三个链表存放着所有模块,只是存放顺序不一样,这里选择跟进InLoadOrderModuleList

InLoadOrderModuleList:按模块加载的顺序

InMemoryOrderModuleList :按内存排列的顺序

InInitializationOrderModuleLists:模块初始化的装载顺序

4.遍历InLoadOrderModuleList链表

依照InLoadOrderModuleList的模块排列顺序,第一个模块是teb.exe,也就是可执行文件本身;第二个模块是ntdll.dll;第三个模块是kernel32.dll

不管是在XP系统还是win7及以上的系统,前三个模块的排列顺序都是不变的,所以只需找到链表的第三个位置就是kernel32.dll

5.获取模块基址

如下图所示,链表是指向LDR_DATA_TABLE_ENTRY结构的指针,只需关注三个成员DLLBaseFullDllNameBaseDllName,而DLLBase代表模块基址

DLLBase:模块的基址

FullDllName:包含路径的模块名

BaseDllName:不包含路径的模块名

编写汇编

int main()
{	

	int address;
	_asm {
		xor eax, eax;  //eax清零
		mov eax, fs:[30h];  //TEB->PEB
		mov eax, [eax + 0ch];  //PEB->LDR
		mov eax, [eax + 0ch];  //LDR->InLoadOrderModuleList,注意:此处已经指向了第一个模块可执行文件
		mov esi, [eax];   //指向第二个模块ntdll
		mov esi, [esi];  //指向第三个模块kernel
		mov eax, esi;    
		mov eax, [eax + 18h];  //获取kernel模块的基址
		mov address,eax    
	}
	HMODULE KernelAddress = LoadLibraryA("kernel32.dll");
	printf("KernelAddress的值是%x\n", KernelAddress);
	printf("address的值是%x", address);
	
	return 0;

}

从运行结果可以发现,通过peb获取的kernel模块基址和LoadLibrary函数获取的模块基址是一致的

生成函数地址的规律

要先编写一个完整的ShellCode框架,首先要了解在VS中生成函数地址的规律,这样才能确定生成Shellcode的字节

单文件的函数生成规律

如下代码所示,A函数编写在B函数的前面,以这种形式编译的程序,生成的代码时A函数的地址会排在B函数的前面,也就是说在单文件里的函数生成规律与其编写代码时定义的函数排列位置有关

#include<windows.h>
#include<stdio.h>

int FuncA(int a, int b)
{
    puts("AAAA");
    return a + b;
}

int FuncB(int a, int b)
{
    puts("BBB");
    return a + b;
}
int main()
{
    FuncA(1, 2);
    FuncB(2, 3);
    return 0;
}

将生成的程序拖入IDA中查看可以发现,A函数的地址排在B函数前面

多文件的函数生成规律

测试代码如下,分别有一个头文件header.h和三个cpp文件A.cpp、B.cpp、test.cpp

//header.h

#pragma once
#include <stdio.h>
void FunA();
void FunB();
//A.cpp

#include "header.h"

void FunA() {
	printf("AAAAA");
}
//B.cpp

#include "header.h"

void FunB() {
	printf("BBBBB");
}
//test.cpp

#include<stdio.h>
#include "header.h"

int main()
{
	FunA();
	FunB();
	return 0;
}

把生成的程序放到IDA查看,发现A函数的地址在B函数地址的前面,那么这个是由什么决定的呢

多文件的函数生成规律与cpp文件生成代码的顺序有关

如下图所示,A.cpp排在B.cpp前面, 那么A.cpp里的函数就会排在B.cpp里的函数前面

若你把A.cpp的名字改成C.cpp, 那么B.cpp里的函数就会排在C.cpp里的函数前面, 也就是说cpp文件的第一个首字母或者数字会决定函数的生成规律, 字母按照AZ, 数字按照09

当然除了更改cpp文件名字以外还有其他方法可以更改函数生成规律, 找到C程序项目所在目录, 打开后缀名为vcxproj的文件

更改如下图所示的排列顺序也可以影响函数的生成规律

ShellCode编写原则

1.不能使用常量字符串

只要出现了常量字符串,程序会把字符串存放在常量区段,所以要对字符串进行“打散”处理

//错误,这种方式定义会将字符串存放在常量区域
char str[] = "String" 
//正确,以这种方式定义会将字符串存放在堆栈区域
char str[] = {'S','t','r','i','n','g'}

2.不能直接调用系统函数

如下代码所示,直接调用了系统api函数的代码不能作为ShellCode

#include <stdio.h>
#include <Windows.h>

int main()
{	
	MessageBoxA(0, 0, 0, 0);  //这种直接调用系统api函数不能作为shellcode
}

如下代码所示,可以通过GetProcAddressLoadLibraryA这两个函数实现动态调用系统api函数

但是这样不也是调用了系统函数吗,没关系,后续会讲到通过kernel32.dll模块来获取GetProcAddress函数地址,以此来实现动态调用所有的api函数

int EntryMain()
{	
	//MessageBoxA(0, 0, 0, 0);

	
	typedef	int(WINAPI* pMessageBoxA)(
			_In_opt_ HWND hWnd,
			_In_opt_ LPCSTR lpText,
			_In_opt_ LPCSTR lpCaption,
			_In_ UINT uType);

	pMessageBoxA MyMessageBoxA; //自己创建的MessageBoxA函数
	MyMessageBoxA = (pMessageBoxA)GetProcAddress(LoadLibraryA("user32.dll"), "MessageBoxA");  
	MyMessageBoxA(0, 0, 0, 0);

	
	return 0;
}

3.不能有全局变量

vs会将全局变量编译在其他区段中 结果就是一个绝对的地址, 也不能使用static来声明变量,因为其效果和全局变量一样

int main()
{	
	static int a;  //错误
	return 0;
}	

简单的ShellCode实例

#include<windows.h>

DWORD GetKernel32();
DWORD pGetProcAddress(DWORD Kernel32Base);


//注意: 需将项目属性设置为多字节字符集
int EntryMain()
{
	typedef DWORD(WINAPI *PGETPROCADDRESS) (HMODULE hModule, LPCSTR lpProcName);
	typedef int (WINAPI * PMESSAGEBOX) (HWND hWnd, LPCTSTR lpText, LPCTSTR lpCaption, UINT uType);
	typedef HMODULE(WINAPI * PLOADLIBRARY) (LPCTSTR lpFileName);

	PMESSAGEBOX MyMessageBox = NULL;
	PLOADLIBRARY pLoadLibrary = NULL;
	DWORD dwKernelBase = GetKernel32();
	PGETPROCADDRESS MyGetProcAddress = PGETPROCADDRESS(pGetProcAddress(dwKernelBase));

	char szUser32[] = { 'U','S','E','R','3','2','.','d','l','l',0 };
	char szLoadLibrary[] = { 'L','o','a','d','L','i','b','r','a','r','y','A',0 };
	char szMessageBox[] = { 'M','e','s','s','a','g','e','B','o','x','A',0 };


	// 有了GetProcAddr 可以获得任何api
	pLoadLibrary = (PLOADLIBRARY)MyGetProcAddress((HMODULE)dwKernelBase, szLoadLibrary);
	MyMessageBox = (PMESSAGEBOX)MyGetProcAddress(pLoadLibrary(szUser32), szMessageBox);

	// 使用函数
	char szTitle[] = { 'S','h','e','l','l','C','o','d','e',0 };
	char szContent[] = { 0x48,0x65,0x6c,0x6c,0x6f,0x20,0x57,0x6f,0x72,0x6c,0x64,0x20,0x21,0 };
	MyMessageBox(NULL, szContent, szTitle, 0);


	return 0;
}

_declspec(naked) DWORD GetKernel32() {
	_asm {
		xor eax, eax;  //eax清零
		mov eax, fs:[30h];  //TEB->PEB
		mov eax, [eax + 0ch];  //PEB->LDR
		mov eax, [eax + 0ch];  //LDR->InLoadOrderModuleList
		mov esi, [eax];   //指向第二个模块ntdll
		mov esi, [esi];  //指向第三个模块kernel
		mov eax, esi;
		mov eax, [eax + 18h];  //获取kernel模块的基址
		ret
	}

}


DWORD pGetProcAddress(DWORD Kernel32Base) {
	char szGetProcAddr[] = { 'G','e','t','P','r','o','c','A','d','d','r','e','s','s',0 };
	DWORD result = NULL;

	// 遍历kernel32.dll的导出表,找到GetProcAddr函数地址
	PIMAGE_DOS_HEADER pDosHead = (PIMAGE_DOS_HEADER)Kernel32Base;
	PIMAGE_NT_HEADERS pNtHead = (PIMAGE_NT_HEADERS)(Kernel32Base + pDosHead->e_lfanew);
	PIMAGE_OPTIONAL_HEADER pOptHead = (PIMAGE_OPTIONAL_HEADER)& pNtHead->OptionalHeader;  //获取扩展PE头
	PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)(Kernel32Base + pOptHead->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
	DWORD *pAddOfFun_Raw = (DWORD*)(Kernel32Base + pExport->AddressOfFunctions);
	WORD *pAddOfOrd_Raw = (WORD*)(Kernel32Base + pExport->AddressOfNameOrdinals);
	DWORD *pAddOfNames_Raw = (DWORD*)(Kernel32Base + pExport->AddressOfNames);
	char *pFinded = NULL, *pSrc = szGetProcAddr;
	for (DWORD dwCnt = 0; dwCnt < pExport->NumberOfNames; dwCnt++)
	{
		pFinded = (char *)((DWORD)Kernel32Base + pAddOfNames_Raw[dwCnt]);
		while (*pFinded &&*pFinded == *pSrc)
		{
			pFinded++; pSrc++;
		}
		if (*pFinded == *pSrc)
		{
			result = (DWORD)Kernel32Base + pAddOfFun_Raw[pAddOfOrd_Raw[dwCnt]];
			break;
		}
		pSrc = szGetProcAddr;
	}
	return result;
}

完整的ShellCode实例

ShellCode生成代码

header.h

#pragma once
#include <Windows.h>
#include "api.h"
#include<stdio.h>
//用户自定义函数的声明

void CreateShellCode();
void ShellCodeStart();
void ShellCodeEnd();
void ShellCodeEntry();
void InitFunctions(PFunctions pFn);
void CreateConfigFile(PFunctions pFn);

api.h

#pragma once
#include <Windows.h>
//api函数的声明定义

//声明定义GetProcAddress
typedef FARPROC(WINAPI *p_GetProcAddress)(
	_In_ HMODULE hModule,
	_In_ LPCSTR lpProcName
	);

//定义LoadLibraryA
typedef HMODULE(WINAPI *p_LoadLibraryA)(
	__in LPCSTR lpLibFileName
	);

//定义MessageBoxA
typedef int (WINAPI *p_MessageBoxA)(
	__in_opt HWND hWnd,
	__in_opt LPCSTR lpText,
	__in_opt LPCSTR lpCaption,
	__in UINT uType);

//定义CreateFileA
typedef HANDLE(WINAPI *p_CreateFileA)(
	__in     LPCSTR lpFileName,
	__in     DWORD dwDesiredAccess,
	__in     DWORD dwShareMode,
	__in_opt LPSECURITY_ATTRIBUTES lpSecurityAttributes,
	__in     DWORD dwCreationDisposition,
	__in     DWORD dwFlagsAndAttributes,
	__in_opt HANDLE hTemplateFile
	);

//定义动态调用的api函数
typedef struct _FUNCTIONS {
	p_LoadLibraryA MyLoadLibraryA;
	p_MessageBoxA MyMessageBoxA;
	p_CreateFileA MyCreateFileA;
	p_GetProcAddress MyGetProcAddress;

}Functions,*PFunctions;

0_Entry.cpp

#include <stdio.h>
#include <Windows.h>
#include "Header.h"
#include "api.h"
int EntryMain()
{	
	CreateShellCode();
	return 0;
}

//生成ShellCode
void CreateShellCode() {
	
	HANDLE hBin = CreateFileA("ShellCode.bin", GENERIC_ALL, 0, NULL, CREATE_ALWAYS, 0, NULL);  //创建文件,返回文件句柄
	if (hBin == INVALID_HANDLE_VALUE)
	{
		MessageBoxA(NULL, "CreateFileA Error", "Error", MB_ERR_INVALID_CHARS);
		return;
	}
	DWORD dwWrite = 0;
	DWORD dwSize = (DWORD)ShellCodeEnd - (DWORD)ShellCodeStart;  //计算出ShellCode代码的字节大小
	WriteFile(hBin, ShellCodeStart, dwSize, &dwWrite, NULL);    //将ShellCode代码写入文件
	CloseHandle(hBin);
	
}

1_ShellCodeStart.cpp

#include "Header.h"
#include "api.h"

//ShellCode代码的开始位置
__declspec(naked)void ShellCodeStart()
{
	__asm
	{
		jmp ShellCodeEntry;  //跳转到ShellCode的入口函数
	}
}


__declspec(naked) DWORD GetKernel32() {
	_asm {
		xor eax, eax;  //eax清零
		mov eax, fs:[30h];  //TEB->PEB
		mov eax, [eax + 0ch];  //PEB->LDR
		mov eax, [eax + 0ch];  //LDR->InLoadOrderModuleList
		mov esi, [eax];   //指向第二个模块ntdll
		mov esi, [esi];  //指向第三个模块kernel
		mov eax, esi;
		mov eax, [eax + 18h];  //获取kernel模块的基址
		ret
	};
}

//获取GetProcAddress函数的地址
DWORD pGetProcAddress(HMODULE Kernel32Base) {
	char szGetProcAddr[] = { 'G','e','t','P','r','o','c','A','d','d','r','e','s','s',0 };
	DWORD result = NULL;

	// 遍历kernel32.dll的导出表,找到GetProcAddr函数地址
	PIMAGE_DOS_HEADER pDosHead = (PIMAGE_DOS_HEADER)Kernel32Base;
	PIMAGE_NT_HEADERS pNtHead = (PIMAGE_NT_HEADERS)((DWORD)Kernel32Base + pDosHead->e_lfanew);
	PIMAGE_OPTIONAL_HEADER pOptHead = (PIMAGE_OPTIONAL_HEADER)& pNtHead->OptionalHeader; 
	PIMAGE_EXPORT_DIRECTORY pExport = (PIMAGE_EXPORT_DIRECTORY)((DWORD)Kernel32Base + pOptHead->DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
	DWORD *pAddOfFun_Raw = (DWORD*)((DWORD)Kernel32Base + pExport->AddressOfFunctions);
	WORD *pAddOfOrd_Raw = (WORD*)((DWORD)Kernel32Base + pExport->AddressOfNameOrdinals);
	DWORD *pAddOfNames_Raw = (DWORD*)((DWORD)Kernel32Base + pExport->AddressOfNames);
	char *pFinded = NULL, *pSrc = szGetProcAddr;
	for (DWORD dwCnt = 0; dwCnt < pExport->NumberOfNames; dwCnt++)
	{
		pFinded = (char *)((DWORD)Kernel32Base + pAddOfNames_Raw[dwCnt]);
		while (*pFinded &&*pFinded == *pSrc)
		{
			pFinded++; pSrc++;
		}
		if (*pFinded == *pSrc)
		{
			result = (DWORD)Kernel32Base + pAddOfFun_Raw[pAddOfOrd_Raw[dwCnt]];
			break;
		}
		pSrc = szGetProcAddr;
	}
	return result;
}


//初始化动态调用的api函数
void InitFunctions(PFunctions pFn) {
	//获取GetProcAddress真实地址
	pFn->MyGetProcAddress = (p_GetProcAddress)pGetProcAddress((HMODULE)GetKernel32());

	//动态获取LoadLibraryA的地址
	char xyLoadLibraryA[] = { 'L','o','a','d','L','i','b','r','a','r','y','A',0 };
	pFn->MyLoadLibraryA = (p_LoadLibraryA)pFn->MyGetProcAddress((HMODULE)GetKernel32(), xyLoadLibraryA);

	//动态获取MessageBoxA的地址
	char xy_user32[] = { 'u','s','e','r','3','2','.','d','l','l',0 };
	char xy_MessageBoxA[] = { 'M','e','s','s','a','g','e','B','o','x','A',0 };
	pFn->MyMessageBoxA = (p_MessageBoxA)pFn->MyGetProcAddress(pFn->MyLoadLibraryA(xy_user32), xy_MessageBoxA);

	//动态获取CreateFile的地址
	char xyCreateFile[] = { 'C','r','e','a','t','F','i','l','e','A',0 };
	pFn->MyCreateFileA = (p_CreateFileA)pFn->MyGetProcAddress((HMODULE)GetKernel32(), xyCreateFile);

}


//ShellCode的入口函数
void ShellCodeEntry()
{	
	char szTitle[] = { 'H','e','l','l','o','W','o','r','d',0 };
	char szContent[] = { 0x48,0x65,0x6c,0x6c,0x6f,0x20,0x57,0x6f,0x72,0x6c,0x64,0x20,0x21,0 };
	//char szContent[] = { 'T','i','p','!','!','!' };


	Functions Fn; 
	InitFunctions(&Fn); 
	Fn.MyMessageBoxA(NULL, szContent, szTitle, 0);  
	
}

2_work.cpp

#include "api.h"
#include "header.h"
//用于存放ShellCode的功能

//生成配置文件
void CreateConfigFile(PFunctions pFn) {
	char xyNewFile[] = { 't','e','s','t','.','t','x','t','\0' };
	pFn->MyCreateFileA(xyNewFile, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);
}

3_ShellCodeEnd.cpp

#include "Header.h"
#include "api.h"

//ShellCode的结束位置
void ShellCodeEnd() {
}

ShellCode加载代码

#include<stdio.h>
#include<windows.h>
#include<stdio.h>
#include<windows.h>

//main函数的两个默认参数,第一个参数表示命令行参数的个数,第二个参数表示命令行参数字符串数组
//argv[1]表示第二个命令行参数的字符串
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("Oen file error:%d\n", GetLastError);
		return -1;
	}

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

运行结果

执行ShellCode生成程序后生成了bin文件

将bin文件拖入到ShellCode加载程序中, 成功执行ShellCode代码中的弹框

参考链接

  • https://www.bilibili.com/video/BV1y4411k7ch?p=3&vd_source=a6caf742912abf241ffbcb3c11933841

  • https://xz.aliyun.com/t/10478#toc-8

  • https://www.cnblogs.com/thresh/p/12609659.html

总结

1、编写ShellCode前需要先了解PE结构、汇编和win32编程相关的知识,这里推荐大家去看滴水的逆向课程,主讲老师是海东老师,BiliBili上能直接搜到

2、这里推荐一款用于查找api函数所在哪个dll的工具:DllExportFinder

最后更新于