avatar

Windows下Shellcode开发

本文由j031sn原创发布
转载,请参考转载声明,注明出处: https://www.anquanke.com/post/id/222280
安全客 - 有思想的安全新媒体

平台

vc6.0 vs2005 vs2008 vs2010 vs2012 vs2013 vs2015 vs2017

创建

Win32程序控制台

一、shellcode编写原则

1.修改程序入口

编译时编译器会自动生成的代码,对编写shellcode产生干扰,所以需要清除

  • 1. 修改程序入口点(VS位例子)

    程序员源代码如下:

    #include <windows.h>
    #pragma comment(linker, "/ENTRY:EntryMain")
    int EntryMain()
    {
    return 0;
    }

Release模式下

  • 工程属性(右键项目)->配置属性->链接器->高级->入口点 处设置入口函数名称

  • 添加如下代码

    #pragma comment(linker, "/ENTRY:EntryName")

    Debug模式下几乎不可能改变,因为MSVCRT.lib中某些对象文件的唯一链接器引用。链接器定义的实际入口点名称不是main,而是mainCRTStartup。不过方法如下,缺点就是要保留main函数,这样就无法达到自定义程序入口的目的

  • 工程属性(右键项目)->配置属性->链接器->高级->入口点 处设置入口函数名称,然后在 工程属性(右键项目)->配置属性->链接器->输入->强制符号引用 将值设置为:_mainCRTStartup(x86)或 mainCRTStartup(x64)

  • 也可以添加如下代码

    #pragma comment(linker, "/ENTRY:wmainCRTStartup ") // wmain will be called
    #pragma comment(linker, "/ENTRY:mainCRTStartup ") // main will be called

    但是这样只能调用wmainmain

    这样ida反汇编:

    1-1

  • 2.关闭缓冲区安全检查(GS检查)

    依旧是在release下进行

    工程属性(右键项目) ->c/c++->代码生成->安全检查,设置为禁用安全检查

    1-2

    这个时候就只有一个函数了

这样将shellcode写入到函数中就不会因为其他函数造成干扰

2.设置工程兼容WindowsXP

我也很想设置好这个但是:配置完了过后,再切换到原来的工具集将丢失头文件的路径,要重新导入,修复的话很麻烦,尽量不要选择这个

  • 在visual studio installer 里面添加对 c++的WindowsXP支持

    2-1

  • 工程属性(右键项目) ->常规->平台工具集->设置为含有当前vs年份+WindowsXP,如:

    2-2

  • 工程属性(右键项目) ->c/c++->代码生成->运行库:多线程调试MTD(Debug) 或 MT(Release)

    这样就能保证程序能在windowsxp下运行

3.关闭生成清单

程序使用PEid之类的工具的话会发现EP段有三个段

3-1

理想情况下应该只保留代码段,这样便于直接提取代码段得到shellcode,其中.rsrc就是vs默认的生成清单段

清楚过程如下:

工程属性(右键项目) ->链接器->清单文件->生成清单:否

3-2

4.函数动态调用

这里以弹出MessageBox位例子

#pragma comment(linker, "/ENTRY:EntryName")//手动设置了入口点就不需要加这句 
#include <windows.h>

int EntryName()
{
MessageBox(NULL, NULL, NULL, NULL);
return 0;
}

编译前执行操作 工程属性(右键项目) ->C/C++->语言->符合模式:否

对CTF中二进制的朋友应该明白:类似在Linux上的pltgot的转换,在windows下,函数调用是通过user32.dll或者kernel32.dll来实现的,中间存在一个寻找地址的操作,而这个操作又是通过编译器实现的,这样程序员只需要记住名字就可以调用库中的函数了。

在ida中通过汇编就可以说明这一点:

4-1

但是shellcode的编写选用调用函数的话,就必须知道相对偏移才能正确获得函数的内存地址,所以shellcode要杜绝绝对地址的直接调用,如将上面的程序变为shellcode时,在汇编中直接call call dword ptr ds:[0x00E02000](x32dbg调试中的语句)是要避免的,所以函数要先获得的动态地址,然后再调用。

GetProcAddress函数

官方文档

作用:在指定动态连接库中获得指定的要导出函数地址

实例:

#pragma comment(linker, "/ENTRY:EntryName") 
#include <windows.h>

int EntryName()
{
//MessageBox(NULL, NULL, NULL, NULL);
GetProcAddress(LoadLibraryA("user32.dll"), "MessageBoxA");
return 0;
}

之前的程序经过调试,确定MessageBox是在user32.dll中,所以在第一个参数加载user32.dll,第二个参数填写函数名称,但是MessageBox有两种重载MessageBoxA(Ascii)和MessageBoxW(Wchar?),这里选择Ascii的版本(MessageBoxA)

dll导出表也可以使用PEid查看

子系统->输出表

4-2

那么可以通过内嵌汇编来调用函数

#pragma comment(linker, "/ENTRY:EntryName") 
#include <windows.h>

int EntryName()
{
//MessageBox(NULL, NULL, NULL, NULL);
LPVOID lp = GetProcAddress(LoadLibraryA("user32.dll"), "MessageBoxA");
char *ptrData = "Hello Shellcode";
__asm
{
push 0
push 0
mov ebx,ptrData
push ebx
push 0
mov eax,lp
call eax
}
return 0;
}

这样提取出来的shellcode就不含编译器参杂的动态调用偏移

现在规范化

可以将鼠标移到函数上,ctrl+鼠标左键进入函数定义,然后自定义一个函数指针,格式如下:

int EntryName()
{
typedef HANDLE (WINAPI *FN_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
);
FN_CreateFileA fn_CreateFileA;
fn_CreateFileA = (FN_CreateFileA)GetProcAddress(LoadLibraryA("kernel32.dll"), "CreateFileA");
fn_CreateFileA("Shellcode.txt", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);

return 0;
}

同理也可以这样设置printf

typedef  int (__CRTDECL *FN_printf)
(char const* const _Format, ...);
FN_printf fn_printf;
fn_printf = (FN_printf)GetProcAddress(LoadLibraryA("msvcrt.dll"), "printf");
fn_printf("%s\n", "hello shellcode");

我们在编写shellcode使用GetProcAddressLoadLibraryA两个函数时,怎么找到这两个函数的地址呢?

5.获得GetProcAddress地址和LoadLibraryA("kerner32.dll")结果

获得LoadLibraryA("kerner32.dll")结果

PEB

进程环境信息块,全称:Process Envirorment Block Structure。MSDN:https://docs.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb,包含了一写进程的信息。

typedef struct _PEB {
BYTE Reserved1[2]; /*0x00*/
BYTE BeingDebugged; /*0x02*/
BYTE Reserved2[1]; /*0x03*/
PVOID Reserved3[2]; /*0x04*/
PPEB_LDR_DATA Ldr; /*0x0c*/
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;

fs寄存器

在80386及之后的处理器 又增加了两个寄存器 FS 寄存器和 GS寄存器

其中FS寄存器的作用是:

偏移 说明
000 指向SEH链指针
004 线程堆栈顶部
008 线程堆栈底部
00C SubSystemTib
010 FiberData
014 ArbitraryUserPointer
018 FS段寄存器在内存中的镜像地址
020 进程PID
024 线程ID
02C 指向线程局部存储指针
030 PEB结构地址(进程结构)
034 上个错误号

所以获得fs:[0x30]就可以获得PEB的信息

得到PEB信息后,在使用PEB->Ldr来获取其他信息

PEB->Ldr

msdn:https://docs.microsoft.com/en-us/windows/win32/api/winternl/ns-winternl-peb_ldr_data

typedef struct _PEB_LDR_DATA {
BYTE Reserved1[8]; /*0x00*/
PVOID Reserved2[3]; /*0x08*/
LIST_ENTRY InMemoryOrderModuleList; /*0x14*/
} PEB_LDR_DATA, *PPEB_LDR_DATA;

注意InMemoryOrderModuleList

The head of a doubly-linked list that contains the loaded modules for the process. Each item in the list is a pointer to an LDR_DATA_TABLE_ENTRY structure. For more information, see Remarks.

双向链接列表的头部,该列表包含该进程已加载的模块。列表中的每个项目都是指向LDR_DATA_TABLE_ENTRY结构的指针。有关更多信息,请参见备注。

备注

/*LIST_ENTRY*/
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

/*LDR_DATA_TABLE_ENTRY*/
typedef struct _LDR_DATA_TABLE_ENTRY {
PVOID Reserved1[2]; /*0x00*/
LIST_ENTRY InMemoryOrderLinks; /*0x08*/
PVOID Reserved2[2]; /*0x10*/
PVOID DllBase; /*0x14*/
PVOID EntryPoint;
PVOID Reserved3;
UNICODE_STRING FullDllName;
BYTE Reserved4[8];
PVOID Reserved5[3];
union {
ULONG CheckSum;
PVOID Reserved6;
};
ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

_LDR_DATA_TABLE_ENTRY中我们就可以得到DLL文件的基址(DllBase),从而得到偏移。

那么以上代码可为

xor eax,eax			;清空eax
mov eax,fs:[0x30] ;eax = PEB
mov eax,[eax+0xc] ;eax = PEB->Ldr
;一个BYTE:1字节,一个PVOID:4字节
;所以Ldr的偏移位=2*1+1+1+2*4=12=0xc
mov eax,[eax+0x14] ;eax = PEB->Ldr.InMemoryOrderModuleList
mov eax,[eax] ;·struct _LIST_ENTRY *Flink;·访问的
;将eax=下一个模块的地址,从而切换模块
;1. .exe程序 -> 2.ntdll.dlls
mov eax,[eax] ;2.ntdll.dll->3.kernel32.dll
mov eax,[eax+0x10] ;kernel32.dll->DllBase
ret ;返回eax寄存器

到这里我们就可以成功获得DLL文件的基址,也就是实现了获得LoadLibraryA("kerner32.dll")结果

获得GetProcAddress地址

预备知识

这里简单说下PE文件头,msdn:https://docs.microsoft.com/en-us/windows/win32/debug/pe-format


typedef struct IMAGE_DOS_HEADER{
WORD e_magic; //DOS头的标识,为4Dh和5Ah。分别为字母MZ
WORD e_cblp;
WORD e_cp;
WORD e_crlc;
WORD e_cparhdr;
WORD e_minalloc;
WORD e_maxalloc;
WORD e_ss;
WORD e_sp;
WORD e_csum;
WORD e_ip;
WORD e_cs;
WORD e_lfarlc;
WORD e_ovno;
WORD e_res[4];
WORD e_oemid;
WORD e_oeminfo;
WORD e_res2[10];
DWORD e_lfanew; //指向IMAGE_NT_HEADERS的所在
}IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

其中e_lfanew指向IMAGE_NT_HEADERS的所在

IMAGE_NT_HEADERS

分为32位和64位两个版本,这里讲32位,https://docs.microsoft.com/zh-cn/windows/win32/api/winnt/ns-winnt-image_nt_headers32

typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;
  • Signature

    四字节大小的签名去定义PE文件,标志为:”PE\x00\x00”

  • FileHeader

    IMAGE_FILE_HEADER结构体来说e明文件头

  • OptionalHeader

    文件的可选头

这里用的到的是OptionalHeader,因为它定义了很多程序的基础数据

typedef struct _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

其中用得到的是:DataDirectory

DataDirectory

A pointer to the first IMAGE_DATA_DIRECTORY structure in the data directory.

The index number of the desired directory entry. This parameter can be one of the following values.

通过这个成员我们可以查看一些结构体的偏移和大小,其中IMAGE_DATA_DIRECTORY如下

typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

如:IMAGE_DIRECTORY_ENTRY_EXPORT,这是一个PE文件的导出表,里面记录了加载函数的信息,内容大致如下

4-2

之后找到这个:_IMAGE_EXPORT_DIRECTORY

typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base;
DWORD NumberOfFunctions;
DWORD NumberOfNames;
DWORD AddressOfFunctions; // RVA from base of image
DWORD AddressOfNames; // RVA from base of image
DWORD AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

就可以用AddressOfFunctions AddressOfNames AddressOfNameOrdinals来找到函数了

通过基址找到GetProcAddress

FARPROC _GetProcAddress(HMODULE hMouduleBase)
{
//由之前找到的DllBase来得到DOS头的地址
PIMAGE_DOS_HEADER lpDosHeader =
(PIMAGE_DOS_HEADER)hMouduleBase;

//找到 IMAGE_NT_HEADERS 的所在
PIMAGE_NT_HEADERS32 lpNtHeader =
(PIMAGE_NT_HEADERS)((DWORD)hMouduleBase + lpDosHeader->e_lfanew);

if (!lpNtHeader->OptionalHeader//检查可选文件头的导出表大小是否 不为空
.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size)
{
return NULL;
}
if (!lpNtHeader->OptionalHeader//检查可选文件头的导出表的偏移是否 不为空
.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress)
{
return NULL;
}


PIMAGE_EXPORT_DIRECTORY lpExport = //获得_IMAGE_EXPORT_DIRECTORY对象
(PIMAGE_EXPORT_DIRECTORY)((DWORD)hMouduleBase + (DWORD)lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

//下面变量均是RVA,要加上hModuleBase这个基址
PDWORD lpdwFunName =
(PDWORD)((DWORD)hMouduleBase + (DWORD)lpExport->AddressOfNames);
PWORD lpword =
(PWORD)((DWORD)hMouduleBase + (DWORD)lpExport->AddressOfNameOrdinals);
PDWORD lpdwFunAddr =
(PDWORD)((DWORD)hMouduleBase + (DWORD)lpExport->AddressOfFunctions);
//DWORD AddressOfFunctions; 指向输出函数地址的RVA
//DWORD AddressOfNames; 指向输出函数名字的RVA
//DWORD AddressOfNameOrdinals; 指向输出函数序号的RVA

DWORD dwLoop = 0;//遍历查找函数
FARPROC pRet = NULL;
for (; dwLoop <= lpExport->NumberOfNames-1;dwLoop++)
{
char *pFunName = (char*)(lpdwFunName[dwLoop] + (DWORD)hMouduleBase);//char *pFunName = lpwdFunName[0] = "func1";
if (pFunName[0] == 'G'&&
pFunName[1] == 'e'&&
pFunName[2] == 't'&&
pFunName[3] == 'P'&&
pFunName[4] == 'r'&&
pFunName[5] == 'o'&&
pFunName[6] == 'c'&&
pFunName[7] == 'A'&&
pFunName[8] == 'd'&&
pFunName[9] == 'd'&&
pFunName[10] == 'r'&&
pFunName[11] == 'e'&&
pFunName[12] == 's'&&
pFunName[13] == 's')
//if(strcmp(pFunName,"GetProcAddress"))
{
pRet = (FARPROC)(lpdwFunAddr[lpword[dwLoop]] + (DWORD)hMouduleBase);
break;
}
}
return pRet;
}
;这里原作者是寻找SwapMouseButton函数
;将最后一段汇编参数修改为MessageBoxA的16位小端序
;即可找到MessageBoxA函数的地址
xor ecx, ecx
mov eax, fs:[ecx + 0x30] ; EAX = PEB
mov eax, [eax + 0xc] ; EAX = PEB->Ldr
mov esi, [eax + 0x14] ; ESI = PEB->Ldr.InMemOrder
lodsd ; EAX = Second module
xchg eax, esi ; EAX = ESI, ESI = EAX
lodsd ; EAX = Third(kernel32)
mov ebx, [eax + 0x10] ; EBX = Base address

mov edx, [ebx + 0x3c] ; EDX = DOS->e_lfanew
add edx, ebx ; EDX = PE Header
mov edx, [edx + 0x78] ; EDX = Offset export table
add edx, ebx ; EDX = Export table
mov esi, [edx + 0x20] ; ESI = Offset namestable
add esi, ebx ; ESI = Names table
xor ecx, ecx ; EXC = 0

Get_Function:

inc ecx ; Increment the ordinal
lodsd ; Get name offset
add eax, ebx ; Get function name
cmp dword ptr[eax], 0x50746547 ; GetP
jnz Get_Function
cmp dword ptr[eax + 0x4], 0x41636f72 ; rocA
jnz Get_Function
cmp dword ptr[eax + 0x8], 0x65726464 ; ddre
jnz Get_Function
mov esi, [edx + 0x24] ; ESI = Offset ordinals
add esi, ebx ; ESI = Ordinals table
mov cx, [esi + ecx * 2] ; Number of function
dec ecx
mov esi, [edx + 0x1c] ; Offset address table
add esi, ebx ; ESI = Address table
mov edx, [esi + ecx * 4] ; EDX = Pointer(offset)
add edx, ebx ; EDX = GetProcAddress

xor ecx, ecx ; ECX = 0
push ebx ; Kernel32 base address
push edx ; GetProcAddress
push ecx ; 0
push 0x41797261 ; aryA
push 0x7262694c ; Libr
push 0x64616f4c ; Load
push esp ; "LoadLibrary"
push ebx ; Kernel32 base address
call edx ; GetProcAddress(LL)

add esp, 0xc ; pop "LoadLibrary"
pop ecx ; ECX = 0
push eax ; EAX = LoadLibrary
push ecx
mov cx, 0x6c6c ; ll
push ecx
push 0x642e3233 ; 32.d
push 0x72657375 ; user
push esp ; "user32.dll"
call eax ; LoadLibrary("user32.dll")

add esp, 0x10 ; Clean stack
mov edx, [esp + 0x4] ; EDX = GetProcAddress
xor ecx, ecx ; ECX = 0
push ecx
mov ecx, 0x616E6F74 ; tona
push ecx
sub dword ptr[esp + 0x3], 0x61 ; Remove "a"
push 0x74754265 ; eBut
push 0x73756F4D ; Mous
push 0x70617753 ; Swap
push esp ; "SwapMouseButton"
push eax ; user32.dll address
call edx ; GetProc(SwapMouseButton)

6.小细节

  • 避免全局变量(包括static之类的)的使用

    这违反了避免对地址直接调用的原则

  • 确保API的DLL被加载(显式加载)

    这个可以在一般情况下写好程序,使用PEid查看输入表,就可以知道在那个DLL调用了那个函数。也可以使用vs的跳转到定义或msdn查询

二、整合:shellcode开发框架

0.创建程序

新建项目->控制台应用->能同时选择控制台应用和空项目最好;不能的话选择控制台应用

编译器选择release版本

关闭生成清单:工程属性(右键项目) ->链接器->清单文件->生成清单:否

关闭缓冲区检查:工程属性(右键项目) ->c/c++->代码生成->安全检查,设置为禁用安全检查

关闭调试信息:工程属性(右键项目) ->链接器->调试->生成调试信息:否

设置函数入口:#pragma comment(linker, "/ENTRY:EntryName")

1.静态注入框架

1.编写代码

正常的功能

#include <windows.h>
int main()
{
CreateFileA("shellcode.txt", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);
MessageBoxA(NULL, "Hello shellcode!", "shell", MB_OK);
return 0;
}

实现:

前面讲过了,shellcode要避免对地址的直接调用,所以我们需要使用GetProcAddressLoadLibraryA,所以将之前的getKernel32和getProcAddress导入到程序中

DWORD getKernel32();
FARPROC getProcAddress(HMODULE hMouduleBase);

2.实现CreateFileA

CreateFileA实现动态调用,先创建函数指针,然后声明一个对象

fn_CreateFileA = (FN_CreateFileA)GetProcAddress(LoadLibraryA("kernel32.dll"), "CreateFileA");

声明对象时:1.要调用GetProcAddress,2.第一个参数:LoadLibraryA(“kernel32.dll”),3.第二个参数:”CreateFileA”字符串。

1。 使用动态调用GetProcAddress

按照之前的方法,代码如下:

typedef FARPROC (WINAPI *FN_GetProcAddress)
(
_In_ HMODULE hModule,
_In_ LPCSTR lpProcName
);
FN_GetProcAddress fn_GetProcAddress = (FN_GetProcAddress)getProcAddress((HMODULE)getKernel32());

动态调用的是自己的函数getProcAddress(getProcAddress又是通过getkernel32和PE文件头找到的),这样在CreateFileA的动态调用里面的参数就可以填fn_GetProcAddress

2。第一个参数:LoadLibraryA(“kernel32.dll”)

直接使用getkernel32汇编代码

3。第二个参数:”CreateFileA”字符串。

因为直接填写字符串会被编译器认为是静态变量,而我们要避免静态变量,所以要新建变量

char szFuncName[] = { 'C','r','e','a','t','e','F','i','l','e','A',0 };

所以,最后我们的代码是这样的:

typedef FARPROC (WINAPI *FN_GetProcAddress)
(
_In_ HMODULE hModule,
_In_ LPCSTR lpProcName
);
FN_GetProcAddress fn_GetProcAddress = (FN_GetProcAddress)getProcAddress((HMODULE)getKernel32());

typedef HANDLE(WINAPI *FN_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
);

char szFuncName[] = { 'C','r','e','a','t','e','F','i','l','e','A',0 };
char szNewFile[] = { 'S','h','e','l','l','c','o','d','e','.','t','x','t',0};
FN_CreateFileA fn_CreateFileA = (FN_CreateFileA)fn_GetProcAddress((HMODULE)getKernel32(), szFuncName);
fn_CreateFileA(szNewFile, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);

3.实现MessageBoxA()

和上面CreateFileA实现不同的是,MessageBoxA是位于User32.dll中的,所以要动态加载LoadLibraryA

typedef HMODULE(WINAPI *FN_LoadLibraryA)
(
_In_ LPCSTR lpLibFileName
);
char szLoadLibrary[]= { 'L','o','a','d','L','i','b','r','a','r','y','A' ,0};
FN_LoadLibraryA fn_LoadLibraryA=(FN_LoadLibraryA)fn_GetProcAddress((HMODULE)getKernel32(),szLoadLibrary);

这样LoadLibraryA被替换为了fn_LoadLibraryA

然后再载入DLL为文件

char szUser32[] = { 'U','s','e','r','3','2','.','d','l','l' };
char szMsgBox[] = { 'M','e','s','s','a','g','e','B','o','x','A' };
FN_MessageBoxA fn_MessageBoxA = (FN_MessageBoxA)fn_GetProcAddress((HMODULE)fn_LoadLibraryA(szUser32),szMsgBox);

最终的代码如下:

//动态加载LoadLibraryA函数
typedef HMODULE(WINAPI *FN_LoadLibraryA)
(
_In_ LPCSTR lpLibFileName
);
char szLoadLibrary[]= { 'L','o','a','d','L','i','b','r','a','r','y','A' ,0};
FN_LoadLibraryA fn_LoadLibraryA=(FN_LoadLibraryA)fn_GetProcAddress((HMODULE)getKernel32(),szLoadLibrary);
//动态加载MessageBoxA函数
typedef int (WINAPI *FN_MessageBoxA)
(
_In_opt_ HWND hWnd,
_In_opt_ LPCSTR lpText,
_In_opt_ LPCSTR lpCaption,
_In_ UINT uType
);
char szUser32[] = { 'U','s','e','r','3','2','.','d','l','l' };
char szMsgBox[] = { 'M','e','s','s','a','g','e','B','o','x','A' };
//载入DLL文件
FN_MessageBoxA fn_MessageBoxA = (FN_MessageBoxA)fn_GetProcAddress((HMODULE)fn_LoadLibraryA(szUser32),szMsgBox);
//调用函数
char szMsgBoxContent[] = { 'H','e','l','l','o',' ','s','h','e','l','l','c','o','d','e','!' ,0 };
char szMsgBoxTitle[] = { 's','h','e','l','l',0 };
fn_MessageBoxA(NULL,szMsgBoxContent,szMsgBoxTitle, 0);

4.最终的源代码

#pragma comment(linker, "/ENTRY:MainEntry")
#include <windows.h>

DWORD getKernel32();
FARPROC getProcAddress(HMODULE hMouduleBase);

int MainEntry()
{
//CreateFileA("shellcode.txt", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);
typedef FARPROC (WINAPI *FN_GetProcAddress)
(
_In_ HMODULE hModule,
_In_ LPCSTR lpProcName
);
FN_GetProcAddress fn_GetProcAddress = (FN_GetProcAddress)getProcAddress((HMODULE)getKernel32());

typedef HANDLE(WINAPI *FN_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
);

char szCreateFileA[] = { 'C','r','e','a','t','e','F','i','l','e','A',0 };
char szNewFile[] = { 'S','h','e','l','l','c','o','d','e','.','t','x','t',0};
FN_CreateFileA fn_CreateFileA = (FN_CreateFileA)fn_GetProcAddress((HMODULE)getKernel32(), szCreateFileA);
fn_CreateFileA(szNewFile, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);

typedef HMODULE(WINAPI *FN_LoadLibraryA)
(
_In_ LPCSTR lpLibFileName
);
char szLoadLibrary[]= { 'L','o','a','d','L','i','b','r','a','r','y','A' ,0};
FN_LoadLibraryA fn_LoadLibraryA=(FN_LoadLibraryA)fn_GetProcAddress((HMODULE)getKernel32(),szLoadLibrary);

typedef int (WINAPI *FN_MessageBoxA)
(
_In_opt_ HWND hWnd,
_In_opt_ LPCSTR lpText,
_In_opt_ LPCSTR lpCaption,
_In_ UINT uType
);
char szUser32[] = { 'U','s','e','r','3','2','.','d','l','l' };
char szMsgBox[] = { 'M','e','s','s','a','g','e','B','o','x','A' };
FN_MessageBoxA fn_MessageBoxA = (FN_MessageBoxA)fn_GetProcAddress((HMODULE)fn_LoadLibraryA(szUser32),szMsgBox);

char szMsgBoxContent[] = { 'H','e','l','l','o',' ','s','h','e','l','l','c','o','d','e','!' ,0 };
char szMsgBoxTitle[] = { 's','h','e','l','l',0 };
fn_MessageBoxA(NULL,szMsgBoxContent,szMsgBoxTitle, 0);
//MessageBoxA(NULL, "Hello shellcode!", "shell", MB_OK);
return 0;
}

__declspec(naked) DWORD getKernel32()
{
__asm
{
mov eax, fs:[0x30]
mov eax, [eax + 0xc]
mov eax, [eax + 0x14]
mov eax, [eax]
mov eax, [eax]
mov eax, [eax + 0x10]
ret
}
}

FARPROC getProcAddress(HMODULE hMouduleBase)
{
//由之前找到的DllBase来得到DOS头的地址
PIMAGE_DOS_HEADER lpDosHeader =
(PIMAGE_DOS_HEADER)hMouduleBase;

//找到 IMAGE_NT_HEADERS 的所在
PIMAGE_NT_HEADERS32 lpNtHeader =
(PIMAGE_NT_HEADERS)((DWORD)hMouduleBase + lpDosHeader->e_lfanew);

if (!lpNtHeader->OptionalHeader//检查可选文件头的导出表大小是否 不为空
.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size)
{
return NULL;
}
if (!lpNtHeader->OptionalHeader//检查可选文件头的导出表的偏移是否 不为空
.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress)
{
return NULL;
}


PIMAGE_EXPORT_DIRECTORY lpExport = //获得_IMAGE_EXPORT_DIRECTORY对象
(PIMAGE_EXPORT_DIRECTORY)((DWORD)hMouduleBase + (DWORD)lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

//下面变量均是RVA,要加上hModuleBase
PDWORD lpdwFunName =
(PDWORD)((DWORD)hMouduleBase + (DWORD)lpExport->AddressOfNames);
PWORD lpword =
(PWORD)((DWORD)hMouduleBase + (DWORD)lpExport->AddressOfNameOrdinals);
PDWORD lpdwFunAddr =
(PDWORD)((DWORD)hMouduleBase + (DWORD)lpExport->AddressOfFunctions);
//DWORD AddressOfFunctions; 指向输出函数地址的RVA
//DWORD AddressOfNames; 指向输出函数名字的RVA
//DWORD AddressOfNameOrdinals; 指向输出函数序号的RVA

DWORD dwLoop = 0;//遍历查找函数
FARPROC pRet = NULL;
for (; dwLoop <= lpExport->NumberOfNames - 1; dwLoop++)
{
char *pFunName = (char*)(lpdwFunName[dwLoop] + (DWORD)hMouduleBase);//char *pFunName = lpwdFunName[0] = "func1";
if (pFunName[0] == 'G'&&
pFunName[1] == 'e'&&
pFunName[2] == 't'&&
pFunName[3] == 'P'&&
pFunName[4] == 'r'&&
pFunName[5] == 'o'&&
pFunName[6] == 'c'&&
pFunName[7] == 'A'&&
pFunName[8] == 'd'&&
pFunName[9] == 'd'&&
pFunName[10] == 'r'&&
pFunName[11] == 'e'&&
pFunName[12] == 's'&&
pFunName[13] == 's')
//if(strcmp(pFunName,"GetProcAddress"))
{
pRet = (FARPROC)(lpdwFunAddr[lpword[dwLoop]] + (DWORD)hMouduleBase);
break;
}
}
return pRet;
}

5.提取shellcode并静态植入(生成框架)

使用PEid来获得程序偏移量,从而得到程序加载到的地方

3-1

然后使用十六进制编辑器打开编写的程序,这里我用的是HxD,跳转到程序入口,也就是上面的偏移量

3-2

这里长度不能太短了,能把要执行的代码包裹完就行,这里选择到0x660的位置。

这样我们就得到了他的二进制代码,即shellcode

然后我们实现静态插入,这里我用PEView来测试

也是使用PEid来获得程序偏移量(0x400),然后在十六进制编辑器中转到,覆盖为我们上面shellcode

3-3

保存后运行:

3-4

这里成功创建了Shellcode.txt文件,然后成功弹出了MessageBox,但是字节填入过多,导致错误的参数被填入,我们这里是对PE文件进行直接覆盖,导致文件偏移计算有问题,最后乱码。

error-1

2.利用函数地址差提取shellcode

1.预备知识

单文件中函数的位置

这里要明白两种概念,函数定义、函数声明、函数编译的顺序

#include <iostream>
int Plus(int , int );//函数声明
int main()
{
std::cout << "> "<<Plus(1,2)<<std::endl;
}

int Plus(int a, int b)//函数定义
{
return a + b;
}

函数声明:把函数的名字、函数类型以及形参类型、个数和顺序通知编译系统,以便在调用该函数时系统按此进行对照检查(例如函数名是否正确,实参与形参的类型和个数是否一致)。

函数定义:函数功能的确立,包括指定函数名,函数值类型、形参类型、函数体等,它是一个完整的、独立的函数单位。

函数编译的顺序

这个在vs里面关掉优化,代码是如下

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

int Plus(int , int );
int Div(int, int);

int main()
{
Plus(2, 3);
Div(2, 3);
return 0;
}

int Div(int a, int b)
{
puts("Divds");
return a - b;
}

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

在IDA中观察,发现函数生成的顺序和声明的顺序不一样,起决定作用的是定义顺序。

6-2

利用编译顺序,将一直两端函数的地址做差,就能得到两函数之间的代码段的相对位置和程序代码段的大小

多文件函数生成位置的关系

项目文件如下

6-3

//A.cpp
#include "A.h"
#include <stdio.h>
void FuncA()
{
puts("This Is FuncA");
}
//B.cpp
#include "B.h"
#include <stdio.h>
void FuncB()
{
puts("This Is FuncB");
}
//main.cpp
#include <iostream>
#include "A.h"
#include "B.h"
int main()
{
FuncA();
FuncB();
}

在IDA中

6-4

发现顺序是FuncA FuncB main,交换调用顺序和include的顺序,发现生成顺序依然没有改变。

其实编译顺序是由编译器的配置文件决定的,文件后缀名为:.vcxproj

6-5

修改上面cpp的顺序就修改函数生成顺序了

2.编写代码

还是按照创建程序的步骤建立一个项目,但是不要关闭调试信息

在项目里面添加一个 header.h 0.entry.cpp a_start.cpp z_end.cpp,这样文件排序可以很直观的找到代码而且默认的编译顺序是0-9,a-Z

要实现的功能:0.entry.cpp提取shellcode,a_start.cpp z_end.cpp生成shellcode

header.h

#pragma once
#ifndef HEAD_H
#define HEAD_H

#include <windows.h>

void ShellcodeStart();
void ShellcodeEntry();
void ShellcodeEnd();
DWORD getKernel32();
FARPROC getProcAddress(HMODULE hMouduleBase);

#endif // !HEAD_D

0.entry.cpp

IO交互部分,不参与shellcode的部分

#pragma comment(linker, "/ENTRY:MainEntry")
#include <stdio.h>
#include <Windows.h>
#include "header.h"

void CreateShellcode()//创建文件并写入
{
typedef int (__CRTDECL *FN_printf)
(char const* const _Format, ...);
FN_printf fn_printf;
fn_printf = (FN_printf)GetProcAddress(LoadLibraryA("msvcrt.dll"), "printf");

HANDLE hBin = CreateFileA("sh.bin", GENERIC_ALL, 0, NULL, CREATE_ALWAYS, 0, NULL);
if (hBin == INVALID_HANDLE_VALUE)
{
fn_printf("Wrong in Generic\n");
return;
}
DWORD dwLen = (DWORD)ShellcodeEnd - (DWORD)ShellcodeStart;
DWORD dwWriter;
WriteFile(hBin, ShellcodeStart, dwLen, &dwWriter, NULL);
CloseHandle(hBin);

}

int MainEntry()
{
CreateShellcode();
return 0;
}

a_start.cpp

利用两函数做差就可以得到ShellcodeEnrtry的代码

(ShellcodeStart - ShellcodeEnd = getKernel32+getProcAddress+ShellcodeEntry)

,最后通过0.entry.cpp写入到bin文件

#include <windows.h>
#include "header.h"
__declspec(naked) void ShellcodeStart()
{
__asm
{
jmp ShellcodeEntry
}
}

__declspec(naked) DWORD getKernel32()
{
__asm
{
mov eax, fs:[0x30]
mov eax, [eax + 0xc]
mov eax, [eax + 0x14]
mov eax, [eax]
mov eax, [eax]
mov eax, [eax + 0x10]
ret
}
}

FARPROC getProcAddress(HMODULE hMouduleBase)
{
//由之前找到的DllBase来得到DOS头的地址
PIMAGE_DOS_HEADER lpDosHeader =
(PIMAGE_DOS_HEADER)hMouduleBase;

//找到 IMAGE_NT_HEADERS 的所在
PIMAGE_NT_HEADERS32 lpNtHeader =
(PIMAGE_NT_HEADERS)((DWORD)hMouduleBase + lpDosHeader->e_lfanew);

if (!lpNtHeader->OptionalHeader//检查可选文件头的导出表大小是否 不为空
.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].Size)
{
return NULL;
}
if (!lpNtHeader->OptionalHeader//检查可选文件头的导出表的偏移是否 不为空
.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress)
{
return NULL;
}


PIMAGE_EXPORT_DIRECTORY lpExport = //获得_IMAGE_EXPORT_DIRECTORY对象
(PIMAGE_EXPORT_DIRECTORY)((DWORD)hMouduleBase + (DWORD)lpNtHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

//下面变量均是RVA,要加上hModuleBase
PDWORD lpdwFunName =
(PDWORD)((DWORD)hMouduleBase + (DWORD)lpExport->AddressOfNames);
PWORD lpword =
(PWORD)((DWORD)hMouduleBase + (DWORD)lpExport->AddressOfNameOrdinals);
PDWORD lpdwFunAddr =
(PDWORD)((DWORD)hMouduleBase + (DWORD)lpExport->AddressOfFunctions);
//DWORD AddressOfFunctions; 指向输出函数地址的RVA
//DWORD AddressOfNames; 指向输出函数名字的RVA
//DWORD AddressOfNameOrdinals; 指向输出函数序号的RVA

DWORD dwLoop = 0;//遍历查找函数
FARPROC pRet = NULL;
for (; dwLoop <= lpExport->NumberOfNames - 1; dwLoop++)
{
char *pFunName = (char*)(lpdwFunName[dwLoop] + (DWORD)hMouduleBase);//char *pFunName = lpwdFunName[0] = "func1";
if (pFunName[0] == 'G'&&
pFunName[1] == 'e'&&
pFunName[2] == 't'&&
pFunName[3] == 'P'&&
pFunName[4] == 'r'&&
pFunName[5] == 'o'&&
pFunName[6] == 'c'&&
pFunName[7] == 'A'&&
pFunName[8] == 'd'&&
pFunName[9] == 'd'&&
pFunName[10] == 'r'&&
pFunName[11] == 'e'&&
pFunName[12] == 's'&&
pFunName[13] == 's')
//if(strcmp(pFunName,"GetProcAddress"))
{
pRet = (FARPROC)(lpdwFunAddr[lpword[dwLoop]] + (DWORD)hMouduleBase);
break;
}
}
return pRet;
}

void ShellcodeEntry()
{
typedef FARPROC(WINAPI *FN_GetProcAddress)
(
_In_ HMODULE hModule,
_In_ LPCSTR lpProcName
);
FN_GetProcAddress fn_GetProcAddress = (FN_GetProcAddress)getProcAddress((HMODULE)getKernel32());

typedef HMODULE(WINAPI *FN_LoadLibraryA)
(
_In_ LPCSTR lpLibFileName
);
char szLoadLibrary[] = { 'L','o','a','d','L','i','b','r','a','r','y','A' ,0 };
FN_LoadLibraryA fn_LoadLibraryA = (FN_LoadLibraryA)fn_GetProcAddress((HMODULE)getKernel32(), szLoadLibrary);

typedef int (WINAPI *FN_MessageBoxA)
(
_In_opt_ HWND hWnd,
_In_opt_ LPCSTR lpText,
_In_opt_ LPCSTR lpCaption,
_In_ UINT uType
);
char szUser32[] = { 'U','s','e','r','3','2','.','d','l','l',0 };
char szMsgBox[] = { 'M','e','s','s','a','g','e','B','o','x','A',0 };
FN_MessageBoxA fn_MessageBoxA = (FN_MessageBoxA)fn_GetProcAddress((HMODULE)fn_LoadLibraryA(szUser32), szMsgBox);

char szMsgBoxContent[] = { 'H','e','l','l','o',0 };
char szMsgBoxTitle[] = { 't','i','t','l','e',0 };
fn_MessageBoxA(NULL, szMsgBoxContent, szMsgBoxTitle, 0);
//MessageBoxA(NULL, "Hello", "title", MB_OK);
}

z_end.cpp

标志shellcode的结束

#include <windows.h>
#include "header.h"
void ShellcodeEnd(){}

3.效果

最后生成的bin文件是一串二进制代码,需要shellcode加载器才能运行,接下来就编写shellcode加载器

7-1

3.加载器

我们编写的shellcode实际上只是一串二进制代码,必须包含在一个程序中才能运行起来,应为加载器只需要讲二进制文件跑起来就行了,所以不需要再遵守shellcode编写原则

#include <stdio.h>
#include <windows.h>
int main(int argc, char *argv[])
{
//1-代开文件并读取
HANDLE hFile = CreateFileA(argv[1], GENERIC_READ, 0, NULL, OPEN_ALWAYS, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE)
{
printf("Open file wrong\n");
return -1;
}
DWORD dwSize;
dwSize = GetFileSize(hFile, 0);
//2-将文件内容加载到一个内存中
LPVOID lpAddress = VirtualAlloc(NULL,dwSize,MEM_COMMIT,PAGE_EXECUTE_READWRITE);
if (lpAddress == NULL)
{
printf("VirtualAlloc error : %d", GetLastError());
CloseHandle(hFile);
return -1;
}
DWORD dwRead;
ReadFile(hFile, lpAddress, dwSize,&dwRead,0);
//3-使用汇编转到shellcode
__asm
{
call lpAddress
}
_flushall();
system("pause");
}

其实shellcode就是从汇编提取出来的机器码,当把shellcode加载到内存中,我们也可以使用函数的方式调用,

将汇编改为((void(*)(void))lpAddress)();,这样也能成功执行shellcode

4.对框架进行优化

目前我们只实现了一个函数,但是要实现更加复杂的功能(如反弹一个远程shell)的话就必须,因此我们需要加以改进

1.创建一个头文件,将shellcode的函数(Start和End之间)原型放到这里面

#pragma once
#include <windows.h>
typedef FARPROC(WINAPI *FN_GetProcAddress)
(
_In_ HMODULE hModule,
_In_ LPCSTR lpProcName
);

typedef HMODULE(WINAPI *FN_LoadLibraryA)
(
_In_ LPCSTR lpLibFileName
);

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

之后定义一个结构体并声明

typedef struct _FUNCIONS
{
FN_GetProcAddress fn_GetProcAddress;
FN_LoadLibraryA fn_LoadLibraryA;
FN_MessageBoxA fn_MessageBoxA;
}FUNCIONS, *PFUNCIONS;

这样就能在ShellcodeEntry中调用函数了

2.寻找函数地址

由于函数的声明在api.h文件中了,所以要重新寻址

那么我们在a_start上定义如下函数

void InitFunctions(PFUNCIONS pFn)
{
pFn->fn_GetProcAddress = (FN_GetProcAddress)getProcAddress((HMODULE)getKernel32());
char szLoadLibrary[] = { 'L','o','a','d','L','i','b','r','a','r','y','A' ,0 };
pFn->fn_LoadLibraryA = (FN_LoadLibraryA)pFn->fn_GetProcAddress((HMODULE)getKernel32(), szLoadLibrary);

//MessageBoxA
char szUser32[] = { 'U','s','e','r','3','2','.','d','l','l', 0 };
char szMsgBox[] = { 'M','e','s','s','a','g','e','B','o','x','A' ,0 };
pFn->fn_MessageBoxA = (FN_MessageBoxA)pFn->fn_GetProcAddress((HMODULE)pFn->fn_LoadLibraryA(szUser32), szMsgBox);
}

修改后的ShellcodeEntry函数

void ShellcodeEntry()
{
char szMsgBoxContent[] = { 'H','e','l','l','o',0 };
char szMsgBoxTitle[] = { 't','o','p',0 };
FUNCIONS fn;
InitFunctions(&fn);
fn.fn_MessageBoxA(NULL, szMsgBoxContent, szMsgBoxTitle, MB_OK);
}

//记得添加相应的头文件

之后要添加函数的话:

1.将函数原型和声明添加到api.h;2.在初始化函数部分设置寻址;3.在ShellcodeEntry中调用

3.将所有的函数功能实现放到另一个文件中

在header.h中添加void CreateConfig(PFUNCIONS pFn)函数定义

创建一个b_work.cpp,在文件中可以实现MessageBoxA的功能

void MessageboxA(PFUNCIONS pFn)
{
char szMsgBoxContent[] = { 'H','e','l','l','o',0 };
char szMsgBoxTitle[] = { 't','o','p',0 };
pFn->fn_MessageBoxA(NULL, szMsgBoxContent, szMsgBoxTitle, MB_OK);
}

最后在a_start的ShellcodeEntry中调用

void ShellcodeEntry()
{
FUNCIONS fn;
InitFunctions(&fn);
MessageboxA(&fn);
}

相关知识

  • PE文件结构
  • exe程序入口
  • 函数指针
  • c++函数调用

参考文章

Author: Joe1sn
Link: http://blog.joe1sn.top/2021/Windows%E4%B8%8BShellcode%E5%BC%80%E5%8F%91/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信