平台
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
但是这样只能调用
wmain
和main
这样ida反汇编:
-
2.关闭缓冲区安全检查(GS检查)依旧是在release下进行工程属性(右键项目) ->c/c++->代码生成->安全检查,设置为禁用安全检查
这个时候就只有一个函数了
这样将shellcode写入到函数中就不会因为其他函数造成干扰
2.设置工程兼容WindowsXP
我也很想设置好这个但是:配置完了过后,再切换到原来的工具集将丢失头文件的路径,要重新导入,修复的话很麻烦,尽量不要选择这个
- 在visual studio installer 里面添加对 c++的WindowsXP支持
- 工程属性(右键项目) ->常规->平台工具集->设置为含有当前vs年份+WindowsXP,如:
- 工程属性(右键项目) ->c/c++->代码生成->运行库:多线程调试MTD(Debug) 或 MT(Release)这样就能保证程序能在windowsxp下运行
3.关闭生成清单
程序使用PEid之类的工具的话会发现EP段有三个段
理想情况下应该只保留代码段,这样便于直接提取代码段得到shellcode,其中.rsrc就是vs默认的生成清单段
清楚过程如下:
工程属性(右键项目) ->链接器->清单文件->生成清单:否
4.函数动态调用
这里以弹出MessageBox位例子
#pragma comment(linker, "/ENTRY:EntryName")//手动设置了入口点就不需要加这句
#include <windows.h>
int EntryName()
{
MessageBox(NULL, NULL, NULL, NULL);
return 0;
}
编译前执行操作 工程属性(右键项目) ->C/C++->语言->符合模式:否
对CTF中二进制的朋友应该明白:类似在Linux上的plt
和got
的转换,在windows下,函数调用是通过user32.dll
或者kernel32.dll
来实现的,中间存在一个寻找地址的操作,而这个操作又是通过编译器实现的,这样程序员只需要记住名字就可以调用库中的函数了。
在ida中通过汇编就可以说明这一点:
但是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查看
在 子系统->输出表 中
那么可以通过内嵌汇编来调用函数
#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使用GetProcAddress
和LoadLibraryA
两个函数时,怎么找到这两个函数的地址呢?
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”
- FileHeaderIMAGE_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文件的导出表,里面记录了加载函数的信息,内容大致如下
之后找到这个:_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.静态注入框架
正常的功能
#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要避免对地址的直接调用,所以我们需要使用GetProcAddress
和LoadLibraryA
,所以将之前的getKernel32和getProcAddress导入到程序中
DWORD getKernel32();
FARPROC getProcAddress(HMODULE hMouduleBase);
对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);
和上面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);
#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;
}
使用PEid来获得程序偏移量,从而得到程序加载到的地方
然后使用十六进制编辑器打开编写的程序,这里我用的是HxD,跳转到程序入口,也就是上面的偏移量
这里长度不能太短了,能把要执行的代码包裹完就行,这里选择到0x660的位置。
这样我们就得到了他的二进制代码,即shellcode
然后我们实现静态插入,这里我用PEView来测试
也是使用PEid来获得程序偏移量(0x400),然后在十六进制编辑器中转到,覆盖为我们上面shellcode
保存后运行:
这里成功创建了Shellcode.txt文件,然后成功弹出了MessageBox,但是字节填入过多,导致错误的参数被填入,我们这里是对PE文件进行直接覆盖,导致文件偏移计算有问题,最后乱码。
2.利用函数地址差提取shellcode
单文件中函数的位置
这里要明白两种概念,函数定义、函数声明、函数编译的顺序
#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中观察,发现函数生成的顺序和声明的顺序不一样,起决定作用的是定义顺序。
利用编译顺序,将一直两端函数的地址做差,就能得到两函数之间的代码段的相对位置和程序代码段的大小
多文件函数生成位置的关系
项目文件如下
//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中
发现顺序是FuncA FuncB main,交换调用顺序和include的顺序,发现生成顺序依然没有改变。
其实编译顺序是由编译器的配置文件决定的,文件后缀名为:.vcxproj
修改上面cpp的顺序就修改函数生成顺序了
还是按照创建程序的步骤建立一个项目,但是不要关闭调试信息
在项目里面添加一个 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(){}
最后生成的bin文件是一串二进制代码,需要shellcode加载器才能运行,接下来就编写shellcode加载器
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中调用函数了
由于函数的声明在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中调用
在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++函数调用
- c++联合编译