windows-syscall篇

windows-syscall篇

@[toc]
学免杀不可避免的学习syscall。主要后面需要用rust重写一个syswhispers3的工具嵌入到一个rust写的c2中。

关于PEB

进程环境信息块,是一个从内核中分配给每个进程的用户模式结构,每一个进程都会有从ring0分配给该进程的进程环境块,后续我们主要需要了解_PEB_LDR_DATA以及其他子结构

这张图是x86系统的结构体

FS段寄存器指向当前的TEB结构,可以看到PEB在TEB的0x30偏移处。在编写代码时PEB的结构体也是需要我们自己定义的,微软官方并没有给出。

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
typedef struct _PEB {
BOOLEAN InheritedAddressSpace;
BOOLEAN ReadImageFileExecOptions;
BOOLEAN BeingDebugged;
BOOLEAN Spare;
HANDLE Mutant;
PVOID ImageBase;
PPEB_LDR_DATA LoaderData;
PVOID ProcessParameters;
PVOID SubSystemData;
PVOID ProcessHeap;
PVOID FastPebLock;
PVOID FastPebLockRoutine;
PVOID FastPebUnlockRoutine;
ULONG EnvironmentUpdateCount;
PVOID* KernelCallbackTable;
PVOID EventLogSection;
PVOID EventLog;
PVOID FreeList;
ULONG TlsExpansionCounter;
PVOID TlsBitmap;
ULONG TlsBitmapBits[0x2];
PVOID ReadOnlySharedMemoryBase;
PVOID ReadOnlySharedMemoryHeap;
PVOID* ReadOnlyStaticServerData;
PVOID AnsiCodePageData;
PVOID OemCodePageData;
PVOID UnicodeCaseTableData;
ULONG NumberOfProcessors;
ULONG NtGlobalFlag;
BYTE Spare2[0x4];
LARGE_INTEGER CriticalSectionTimeout;
ULONG HeapSegmentReserve;
ULONG HeapSegmentCommit;
ULONG HeapDeCommitTotalFreeThreshold;
ULONG HeapDeCommitFreeBlockThreshold;
ULONG NumberOfHeaps;
ULONG MaximumNumberOfHeaps;
PVOID** ProcessHeaps;
PVOID GdiSharedHandleTable;
PVOID ProcessStarterHelper;
PVOID GdiDCAttributeList;
PVOID LoaderLock;
ULONG OSMajorVersion;
ULONG OSMinorVersion;
ULONG OSBuildNumber;
ULONG OSPlatformId;
ULONG ImageSubSystem;
ULONG ImageSubSystemMajorVersion;
ULONG ImageSubSystemMinorVersion;
ULONG GdiHandleBuffer[0x22];
ULONG PostProcessInitRoutine;
ULONG TlsExpansionBitmap;
BYTE TlsExpansionBitmapBits[0x80];
ULONG SessionId;
} PEB, * PPEB;

在PEB中的0x0c处为一指针,指向PEB_LDR_DATA结构,该结构体包含有关为进程加载的模块的信息(存储着该进程所有模块数据的链表)。

在PEB_LDR_DATA的0x0c,0x14,0x1c中为三个双向链表LIST_ENTRY,在struct _LDR_MODULE的0x00,0x08和0x10处是三个对应的同名称的LIST_ENTRY, PEB_LDR_DATA和struct _LDR_MODULE就是通过这三个LIST_ENTRY对应连接起来的。

三个双向链表分别代表模块加载顺序,模块在内存中的加载顺序以及模块初始化装载的顺序.

1
2
3
4
5
6
7
8
typedef struct _PEB_LDR_DATA {
ULONG Length;
ULONG Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
} PEB_LDR_DATA, * PPEB_LDR_DATA;

LIST_ENTRY的结构体是下面这样

1
2
3
4
typedef struct _LIST_ENTRY {
struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY, *RESTRICTED_POINTER PRLIST_ENTRY;

每个双向链表都是指向进程装载的模块,结构中的每个指针,指向了一个LDR_DATA_TABLE_ENTRY的结构: 这个结构很重要,提供了内存模块的基址和dll名称

1
2
3
4
5
6
7
8
9
10
11
12
struct _LDR_DATA_TABLE_ENTRY
{
struct _LIST_ENTRY InLoadOrderLinks; //0x0
struct _LIST_ENTRY InMemoryOrderLinks; //0x8
struct _LIST_ENTRY InInitializationOrderLinks; //0x10
VOID* DllBase; //0x18 模块基址
VOID* EntryPoint; //0x1c
ULONG SizeOfImage; //0x20
struct _UNICODE_STRING FullDllName; //0x24 模块路径+名称
struct _UNICODE_STRING BaseDllName; //0x2c 模块名称
...
};

还有一个很重要的结构体。每个加载的模块都有一个LDR_MODULE结构体,其中的BaseAddress字段是模块在内存当中的基地址,BaseDllName指向一个UNICODE_STRING,其包含模块的名称(kernel32.dll等)。实际上三个链表结构是被PEB_LDR_DATA和LDR_MODULE结构共用的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct _LDR_MODULE {
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
PVOID BaseAddress;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
SHORT LoadCount;
SHORT TlsIndex;
LIST_ENTRY HashTableEntry;
ULONG TimeDateStamp;
} LDR_MODULE, * PLDR_MODULE;

从这张图就可以看到很详细的模块信息以及一些dll基址。

用代码获取相关信息

基本认识了PEB,那肯定还是要回到代码层面,怎么去获取到PEB进程的一些相关信息例如偏移地址等。这里微软给出了一些可以使用的函数。

我们最终检索到PEN进程块的流程大概可以像这样来实现。


先自定义实现一些结构体。

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
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
#pragma once
#include <Windows.h>

/*--------------------------------------------------------------------
STRUCTURES
--------------------------------------------------------------------*/
typedef struct _LSA_UNICODE_STRING {
USHORT Length;
USHORT MaximumLength;
PWSTR Buffer;
} LSA_UNICODE_STRING, * PLSA_UNICODE_STRING, UNICODE_STRING, * PUNICODE_STRING, * PUNICODE_STR;

typedef struct _LDR_MODULE {
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
PVOID BaseAddress;
PVOID EntryPoint;
ULONG SizeOfImage;
UNICODE_STRING FullDllName;
UNICODE_STRING BaseDllName;
ULONG Flags;
SHORT LoadCount;
SHORT TlsIndex;
LIST_ENTRY HashTableEntry;
ULONG TimeDateStamp;
} LDR_MODULE, * PLDR_MODULE;

typedef struct _PEB_LDR_DATA {
ULONG Length;
ULONG Initialized;
PVOID SsHandle;
LIST_ENTRY InLoadOrderModuleList;
LIST_ENTRY InMemoryOrderModuleList;
LIST_ENTRY InInitializationOrderModuleList;
} PEB_LDR_DATA, * PPEB_LDR_DATA;

typedef struct _PEB {
BOOLEAN InheritedAddressSpace;
BOOLEAN ReadImageFileExecOptions;
BOOLEAN BeingDebugged;
BOOLEAN Spare;
HANDLE Mutant;
PVOID ImageBase;
PPEB_LDR_DATA LoaderData;
PVOID ProcessParameters;
PVOID SubSystemData;
PVOID ProcessHeap;
PVOID FastPebLock;
PVOID FastPebLockRoutine;
PVOID FastPebUnlockRoutine;
ULONG EnvironmentUpdateCount;
PVOID* KernelCallbackTable;
PVOID EventLogSection;
PVOID EventLog;
PVOID FreeList;
ULONG TlsExpansionCounter;
PVOID TlsBitmap;
ULONG TlsBitmapBits[0x2];
PVOID ReadOnlySharedMemoryBase;
PVOID ReadOnlySharedMemoryHeap;
PVOID* ReadOnlyStaticServerData;
PVOID AnsiCodePageData;
PVOID OemCodePageData;
PVOID UnicodeCaseTableData;
ULONG NumberOfProcessors;
ULONG NtGlobalFlag;
BYTE Spare2[0x4];
LARGE_INTEGER CriticalSectionTimeout;
ULONG HeapSegmentReserve;
ULONG HeapSegmentCommit;
ULONG HeapDeCommitTotalFreeThreshold;
ULONG HeapDeCommitFreeBlockThreshold;
ULONG NumberOfHeaps;
ULONG MaximumNumberOfHeaps;
PVOID** ProcessHeaps;
PVOID GdiSharedHandleTable;
PVOID ProcessStarterHelper;
PVOID GdiDCAttributeList;
PVOID LoaderLock;
ULONG OSMajorVersion;
ULONG OSMinorVersion;
ULONG OSBuildNumber;
ULONG OSPlatformId;
ULONG ImageSubSystem;
ULONG ImageSubSystemMajorVersion;
ULONG ImageSubSystemMinorVersion;
ULONG GdiHandleBuffer[0x22];
ULONG PostProcessInitRoutine;
ULONG TlsExpansionBitmap;
BYTE TlsExpansionBitmapBits[0x80];
ULONG SessionId;
} PEB, * PPEB;

typedef struct __CLIENT_ID {
HANDLE UniqueProcess;
HANDLE UniqueThread;
} CLIENT_ID, * PCLIENT_ID;

typedef struct _TEB_ACTIVE_FRAME_CONTEXT {
ULONG Flags;
PCHAR FrameName;
} TEB_ACTIVE_FRAME_CONTEXT, * PTEB_ACTIVE_FRAME_CONTEXT;

typedef struct _TEB_ACTIVE_FRAME {
ULONG Flags;
struct _TEB_ACTIVE_FRAME* Previous;
PTEB_ACTIVE_FRAME_CONTEXT Context;
} TEB_ACTIVE_FRAME, * PTEB_ACTIVE_FRAME;

typedef struct _GDI_TEB_BATCH {
ULONG Offset;
ULONG HDC;
ULONG Buffer[310];
} GDI_TEB_BATCH, * PGDI_TEB_BATCH;

typedef PVOID PACTIVATION_CONTEXT;

typedef struct _RTL_ACTIVATION_CONTEXT_STACK_FRAME {
struct __RTL_ACTIVATION_CONTEXT_STACK_FRAME* Previous;
PACTIVATION_CONTEXT ActivationContext;
ULONG Flags;
} RTL_ACTIVATION_CONTEXT_STACK_FRAME, * PRTL_ACTIVATION_CONTEXT_STACK_FRAME;

typedef struct _ACTIVATION_CONTEXT_STACK {
PRTL_ACTIVATION_CONTEXT_STACK_FRAME ActiveFrame;
LIST_ENTRY FrameListCache;
ULONG Flags;
ULONG NextCookieSequenceNumber;
ULONG StackId;
} ACTIVATION_CONTEXT_STACK, * PACTIVATION_CONTEXT_STACK;

typedef struct _TEB {
NT_TIB NtTib;
PVOID EnvironmentPointer;
CLIENT_ID ClientId;
PVOID ActiveRpcHandle;
PVOID ThreadLocalStoragePointer;
PPEB ProcessEnvironmentBlock;
ULONG LastErrorValue;
ULONG CountOfOwnedCriticalSections;
PVOID CsrClientThread;
PVOID Win32ThreadInfo;
ULONG User32Reserved[26];
ULONG UserReserved[5];
PVOID WOW32Reserved;
LCID CurrentLocale;
ULONG FpSoftwareStatusRegister;
PVOID SystemReserved1[54];
LONG ExceptionCode;
#if (NTDDI_VERSION >= NTDDI_LONGHORN)
PACTIVATION_CONTEXT_STACK* ActivationContextStackPointer;
UCHAR SpareBytes1[0x30 - 3 * sizeof(PVOID)];
ULONG TxFsContext;
#elif (NTDDI_VERSION >= NTDDI_WS03)
PACTIVATION_CONTEXT_STACK ActivationContextStackPointer;
UCHAR SpareBytes1[0x34 - 3 * sizeof(PVOID)];
#else
ACTIVATION_CONTEXT_STACK ActivationContextStack;
UCHAR SpareBytes1[24];
#endif
GDI_TEB_BATCH GdiTebBatch;
CLIENT_ID RealClientId;
PVOID GdiCachedProcessHandle;
ULONG GdiClientPID;
ULONG GdiClientTID;
PVOID GdiThreadLocalInfo;
PSIZE_T Win32ClientInfo[62];
PVOID glDispatchTable[233];
PSIZE_T glReserved1[29];
PVOID glReserved2;
PVOID glSectionInfo;
PVOID glSection;
PVOID glTable;
PVOID glCurrentRC;
PVOID glContext;
NTSTATUS LastStatusValue;
UNICODE_STRING StaticUnicodeString;
WCHAR StaticUnicodeBuffer[261];
PVOID DeallocationStack;
PVOID TlsSlots[64];
LIST_ENTRY TlsLinks;
PVOID Vdm;
PVOID ReservedForNtRpc;
PVOID DbgSsReserved[2];
#if (NTDDI_VERSION >= NTDDI_WS03)
ULONG HardErrorMode;
#else
ULONG HardErrorsAreDisabled;
#endif
#if (NTDDI_VERSION >= NTDDI_LONGHORN)
PVOID Instrumentation[13 - sizeof(GUID) / sizeof(PVOID)];
GUID ActivityId;
PVOID SubProcessTag;
PVOID EtwLocalData;
PVOID EtwTraceData;
#elif (NTDDI_VERSION >= NTDDI_WS03)
PVOID Instrumentation[14];
PVOID SubProcessTag;
PVOID EtwLocalData;
#else
PVOID Instrumentation[16];
#endif
PVOID WinSockData;
ULONG GdiBatchCount;
#if (NTDDI_VERSION >= NTDDI_LONGHORN)
BOOLEAN SpareBool0;
BOOLEAN SpareBool1;
BOOLEAN SpareBool2;
#else
BOOLEAN InDbgPrint;
BOOLEAN FreeStackOnTermination;
BOOLEAN HasFiberData;
#endif
UCHAR IdealProcessor;
#if (NTDDI_VERSION >= NTDDI_WS03)
ULONG GuaranteedStackBytes;
#else
ULONG Spare3;
#endif
PVOID ReservedForPerf;
PVOID ReservedForOle;
ULONG WaitingOnLoaderLock;
#if (NTDDI_VERSION >= NTDDI_LONGHORN)
PVOID SavedPriorityState;
ULONG_PTR SoftPatchPtr1;
ULONG_PTR ThreadPoolData;
#elif (NTDDI_VERSION >= NTDDI_WS03)
ULONG_PTR SparePointer1;
ULONG_PTR SoftPatchPtr1;
ULONG_PTR SoftPatchPtr2;
#else
Wx86ThreadState Wx86Thread;
#endif
PVOID* TlsExpansionSlots;
#if defined(_WIN64) && !defined(EXPLICIT_32BIT)
PVOID DeallocationBStore;
PVOID BStoreLimit;
#endif
ULONG ImpersonationLocale;
ULONG IsImpersonating;
PVOID NlsCache;
PVOID pShimData;
ULONG HeapVirtualAffinity;
HANDLE CurrentTransactionHandle;
PTEB_ACTIVE_FRAME ActiveFrame;
#if (NTDDI_VERSION >= NTDDI_WS03)
PVOID FlsData;
#endif
#if (NTDDI_VERSION >= NTDDI_LONGHORN)
PVOID PreferredLangauges;
PVOID UserPrefLanguages;
PVOID MergedPrefLanguages;
ULONG MuiImpersonation;
union
{
struct
{
USHORT SpareCrossTebFlags : 16;
};
USHORT CrossTebFlags;
};
union
{
struct
{
USHORT DbgSafeThunkCall : 1;
USHORT DbgInDebugPrint : 1;
USHORT DbgHasFiberData : 1;
USHORT DbgSkipThreadAttach : 1;
USHORT DbgWerInShipAssertCode : 1;
USHORT DbgIssuedInitialBp : 1;
USHORT DbgClonedThread : 1;
USHORT SpareSameTebBits : 9;
};
USHORT SameTebFlags;
};
PVOID TxnScopeEntercallback;
PVOID TxnScopeExitCAllback;
PVOID TxnScopeContext;
ULONG LockCount;
ULONG ProcessRundown;
ULONG64 LastSwitchTime;
ULONG64 TotalSwitchOutTime;
LARGE_INTEGER WaitReasonBitMap;
#else
BOOLEAN SafeThunkCall;
BOOLEAN BooleanSpare[3];
#endif
} TEB, * PTEB;

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;
};
PACTIVATION_CONTEXT EntryPointActivationContext;
PVOID PatchInformation;
LIST_ENTRY ForwarderLinks;
LIST_ENTRY ServiceTagLinks;
LIST_ENTRY StaticLinks;
} LDR_DATA_TABLE_ENTRY, * PLDR_DATA_TABLE_ENTRY;

typedef struct _OBJECT_ATTRIBUTES {
ULONG Length;
PVOID RootDirectory;
PUNICODE_STRING ObjectName;
ULONG Attributes;
PVOID SecurityDescriptor;
PVOID SecurityQualityOfService;
} OBJECT_ATTRIBUTES, * POBJECT_ATTRIBUTES;

typedef struct _INITIAL_TEB {
PVOID StackBase;
PVOID StackLimit;
PVOID StackCommit;
PVOID StackCommitMax;
PVOID StackReserved;
} INITIAL_TEB, * PINITIAL_TEB;

打印当前进程的FullDllName,也就是模块路径+名称。至于这里为什么要减去0x10字节呢,仔细看上面结构体的布局,因为LDR_MODULE结构体在内存中的首地址是有0x10的偏移的(双向链表连接的是InInitializationOrderModuleList字段而不是LDR_MODULE结构体)

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include "PEB.h"
#include <Windows.h>
#include <stdio.h>
#include <TLHELP32.H>
int main()
{
PPEB Peb = (PPEB)__readgsqword(0x60); //PEB 可以通过x86_64:gs寄存器偏移96(0x60) x86:fs寄存器偏移0x48(0x30) 定位
PLDR_MODULE pLoadModule;
pLoadModule = (PLDR_MODULE)((PBYTE)Peb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 0x10);
printf("%ws\r\n", pLoadModule->FullDllName.Buffer);
}

上述代码中,我们首先通过位于0x60的指向GS寄存器的指针检索到当前进程的PEB,我们访问LDR结构体,并且并向前链接到第二个内存顺序模块

这里我们可以用windbg具体查看一下可执行文件里面的结构体到底是怎能排布的。

首先查看当前进程的teb,可以看到在0x60偏移处指向的就是PEB。

找到peb内存起始地址,

查看PEB结构体内容,找到_PEB_LDR_DATA。

上面已经讲到在_PEB_LDR_DATA中存在三个双向链表,从而找到InMemoryOrderModuleList这个模块初始化加载顺序链表。

现在就能从InMemoryOrderModuleList这个链表的Flink指针找到依次加载到内存中的模块信息。这里显示的是当前PEB进程块也就是正在调试的模块信息,

大多数情况下NTDLL模块会是第二个内存模块,kernel32dll将会是第三个内存模块

最后打印的结果就是NTDLL模块FULLNAME.

在这里我们可以通过如下程序(代码参考crispr学长)来遍历进程所有在内存中加载过的模块以及基址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include "peb.h"
int main()
{
PPEB Peb = (PPEB)__readgsqword(0x60); //PEB 可以通过x86_64:gs寄存器偏移96(0x60) x86:fs寄存器偏移0x48(0x30) 定位
PLDR_MODULE pLoadModule;
pLoadModule = (PLDR_MODULE)((PBYTE)Peb->LoaderData->InMemoryOrderModuleList.Flink - 0x10);
PLDR_MODULE pFirstLoadModule = (PLDR_MODULE)((PBYTE)Peb->LoaderData->InMemoryOrderModuleList.Flink - 0x10);
do
{
printf("Module Name:%ws\r\nModule Base Address:%p\r\n\r\n", pLoadModule->FullDllName.Buffer,pLoadModule->BaseAddress);
pLoadModule = (PLDR_MODULE)((PBYTE)pLoadModule->InMemoryOrderModuleList.Flink - 0x10);
} while ((PLDR_MODULE)((PBYTE)pLoadModule->InMemoryOrderModuleList.Flink -0x10) != pFirstLoadModule);
}

这里给出我用rust写的一个代码,主要为了后续想用rust写一个工具出来,实现的功能是一样的。rust在写windows这块相较于c++难写很多,一些api调用很麻烦这里不建议使用Windows_sys依赖,最好自己定义。

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
use std::mem::size_of;
use std::slice;
use ntapi::ntldr::LDR_DATA_TABLE_ENTRY;
use ntapi::ntpebteb::{*};
use ntapi::ntpsapi::{PPEB_LDR_DATA,GDI_HANDLE_BUFFER};
use ntapi::ntrtl::PRTL_USER_PROCESS_PARAMETERS;
use winapi::shared::basetsd::{SIZE_T, ULONG_PTR};
use winapi::shared::minwindef::{USHORT, PBYTE};
use winapi::um::winnt::{PVOID, HANDLE, PRTL_CRITICAL_SECTION, PSLIST_HEADER, ULARGE_INTEGER, FLS_MAXIMUM_AVAILABLE};
use winapi::shared::ntdef::{UNICODE_STRING,ULONG, BOOLEAN, CHAR, ULONGLONG};
use ntapi::winapi_local::um::winnt::{__readgsqword};


#[repr(C)]
pub struct LIST_ENTRY {
pub Flink: *mut LIST_ENTRY,
pub Blink: *mut LIST_ENTRY,
}
#[repr(C)]
pub struct LDR_MODULE {
InLoadOrderModuleList: LIST_ENTRY,
InMemoryOrderModuleList: LIST_ENTRY,
InInitializationOrderModuleList: LIST_ENTRY,
BaseAddress: PVOID,
EntryPoint: PVOID,
SizeOfImage: ULONG,
FullDllName: UNICODE_STRING,
BaseDllName: UNICODE_STRING,
Flags: ULONG,
LoadCount: USHORT,
TlsIndex: USHORT,
HashLinks: LIST_ENTRY,
TimeDateStamp: ULONG,
}

#[repr(C)]
pub struct PEB {
InheritedAddressSpace: BOOLEAN,
ReadImageFileExecOptions: BOOLEAN,
BeingDebugged: BOOLEAN,
BitField: BOOLEAN,
Mutant: HANDLE,
ImageBaseAddress: PVOID,
Ldr: PPEB_LDR_DATA,
ProcessParameters: PRTL_USER_PROCESS_PARAMETERS,
SubSystemData: PVOID,
ProcessHeap: PVOID,
FastPebLock: PRTL_CRITICAL_SECTION,
IFEOKey: PVOID,
AtlThunkSListPtr: PSLIST_HEADER,
CrossProcessFlags: ULONG,
u: PEB_u,
SystemReserved: [ULONG; 1],
AtlThunkSListPtr32: ULONG,
ApiSetMap: PAPI_SET_NAMESPACE,
TlsExpansionCounter: ULONG,
TlsBitmap: PVOID,
TlsBitmapBits: [ULONG; 2],
ReadOnlySharedMemoryBase: PVOID,
SharedData: PVOID,
ReadOnlyStaticServerData: *mut PVOID,
AnsiCodePageData: PVOID,
OemCodePageData: PVOID,
UnicodeCaseTableData: PVOID,
NumberOfProcessors: ULONG,
NtGlobalFlag: ULONG,
CriticalSectionTimeout: ULARGE_INTEGER,
HeapSegmentReserve: SIZE_T,
HeapSegmentCommit: SIZE_T,
HeapDeCommitTotalFreeThreshold: SIZE_T,
HeapDeCommitFreeBlockThreshold: SIZE_T,
NumberOfHeaps: ULONG,
MaximumNumberOfHeaps: ULONG,
ProcessHeaps: *mut PVOID,
GdiSharedHandleTable: PVOID,
ProcessStarterHelper: PVOID,
GdiDCAttributeList: ULONG,
LoaderLock: PRTL_CRITICAL_SECTION,
OSMajorVersion: ULONG,
OSMinorVersion: ULONG,
OSBuildNumber: USHORT,
OSCSDVersion: USHORT,
OSPlatformId: ULONG,
ImageSubsystem: ULONG,
ImageSubsystemMajorVersion: ULONG,
ImageSubsystemMinorVersion: ULONG,
ActiveProcessAffinityMask: ULONG_PTR,
GdiHandleBuffer: GDI_HANDLE_BUFFER,
PostProcessInitRoutine: PVOID,
TlsExpansionBitmap: PVOID,
TlsExpansionBitmapBits: [ULONG; 32],
SessionId: ULONG,
AppCompatFlags: ULARGE_INTEGER,
AppCompatFlagsUser: ULARGE_INTEGER,
pShimData: PVOID,
AppCompatInfo: PVOID,
CSDVersion: UNICODE_STRING,
ActivationContextData: PVOID,
ProcessAssemblyStorageMap: PVOID,
SystemDefaultActivationContextData: PVOID,
SystemAssemblyStorageMap: PVOID,
MinimumStackCommit: SIZE_T,
FlsCallback: *mut PVOID,
FlsListHead: LIST_ENTRY,
FlsBitmap: PVOID,
FlsBitmapBits: [ULONG; FLS_MAXIMUM_AVAILABLE as usize / (size_of::<ULONG>() * 8)],
FlsHighIndex: ULONG,
WerRegistrationData: PVOID,
WerShipAssertPtr: PVOID,
pUnused: PVOID,
pImageHeaderHash: PVOID,
TracingFlags: ULONG,
CsrServerReadOnlySharedMemoryBase: ULONGLONG,
TppWorkerpListLock: PRTL_CRITICAL_SECTION,
TppWorkerpList: LIST_ENTRY,
WaitOnAddressHashTable: [PVOID; 128],
TelemetryCoverageHeader: PVOID,
CloudFileFlags: ULONG,
CloudFileDiagFlags: ULONG,
PlaceholderCompatibilityMode: CHAR,
PlaceholderCompatibilityModeReserved: [CHAR; 7],
LeapSecondData: *mut LEAP_SECOND_DATA,
LeapSecondFlags: ULONG,
NtGlobalFlag2: ULONG,
}



fn main() {
// test();
unsafe{
let peb = __readgsqword(0x60) as *mut PEB;
let p_first_load_module = ((*peb).Ldr.as_ref().unwrap().InMemoryOrderModuleList.Flink as PBYTE).offset(-0x10) as *const LDR_MODULE ;
let mut p_load_module = ((*peb).Ldr.as_ref().unwrap().InMemoryOrderModuleList.Flink as PBYTE).offset(-0x10) as *const LDR_MODULE ;
loop {
let module_base = p_load_module as *const LDR_DATA_TABLE_ENTRY;
let module_name = (*module_base).FullDllName.Buffer as *const UNICODE_STRING;
let slice = slice::from_raw_parts((*module_base).FullDllName.Buffer,(*module_base).FullDllName.Length as usize / 2);
let string = String::from_utf16_lossy(slice);
println!("Module Name: {:?}", string.trim());
println!("Module Base Address: {:?} \r\n", module_base);
p_load_module = ((*p_load_module).InMemoryOrderModuleList.Flink as PBYTE).offset(-0x10) as *const LDR_MODULE ;
if p_first_load_module as * const LDR_MODULE == ((*p_load_module).InMemoryOrderModuleList.Flink as PBYTE).offset(-0x10) as *const LDR_MODULE{
break;
}
}
}
}


掌握这一点后现在我们知道如何获取内存模块的基址,因此我们有能力遍历模块的导出地址表,这就涉及到通过该基址去遍历PE头文件从而获取导出地址表,可以将其分为四个步骤:
– 1.获取每个模块的基地址
– 2.获取_IMAGE_DOS_HEADER,并通过检查IMAGE_DOS_SIGNATURE来验证正确性
– 3.遍历_IMAGE_NT_HEADER、_IMAGE_FILE_HEADER、_IMAGE_OPTIONAL_HEADER
– 4.在_IMAGE_OPTIONAL_HEADER中找到导出地址表,并将类型转为_IMAGE_EXPORT_DIRECTORY

关于PE文件头的详细数据结构,如下:

1
2
3
4
5
typedef struct _IMAGE_NT_HEADERS {
DWORD Signature; //PE文件头标志 => 4字节
IMAGE_FILE_HEADER FileHeader; //标准PE头 => 20字节
IMAGE_OPTIONAL_HEADER32 OptionalHeader; //扩展PE头 => 32位下224字节(0xE0) 64位下240字节(0xF0)
} IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32;

_IMAGE_DOS_HEADER文件头中存在_IMAGE_OPTIONAL_HEADER。DataDirectory是可选映像头_IMAGE_OPTIONAL_HEADER的最后128个字节(16项 * 8 bytes),也是IMAGE_NT_HEADERS(PE文件头)的最后一部分数据。
它由16个IMAGE_DATA_DIRECTORY结构组成的数组构成,指向输出表、输入表、资源块、重定位 等数据目录项的RVA(相对虚拟地址)和大小。
IMAGE_DATA_DIRECTORY的结构如下:

1
2
3
4
5
6
7
8
9
//
//Directory format.
//

typedef struct _IMAGE_DATA_DIRECTORY {
DWORD VirtualAddress; //数据块的起始RVA
DWORD Size; //数据块的长度
} IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY;

可以看到第一个数据就是EXPORT ADDRESS Table导出地址表在内存中的相对虚拟地址。


然后将其转换为_IMAGE_EXPORT_DIRECTORY类型,引出目录表IMAGE_EXPORT_DIRECTORY的结构如下。这样我们就得到了内存中各种函数的地址。

所以总的来讲,当我们得到NT_Header时,由于PE头文件的数据结构已经给出,其前四个字节为一个DWORD类型的Signature,因此加上这四个字节就会得到FileHeader,然后在此基础上加上FileHeader数据结构所占大小最终得到Optional,只需要将其类型转为_IMAGE_EXPORT_DIRECTORY就得到了我们的导出地址表,再通过:

  • 1.AddressOfNames 一个包含函数名称的数组
  • 2.AddressOfNameOrdinals 充当函数寻址数组的索引
  • 3.AddressOfFunctions 一个包含函数地址的数组
    这三个数组结构就能获取到每个函数的地址和函数名称。

利用rust实现的代码

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
fn  getPeHeader() -> i32{
unsafe{
let peb = __readgsqword(0x60) as *mut PEB;
let mut p_load_module = ((*peb).Ldr.as_ref().unwrap().InMemoryOrderModuleList.Flink as PBYTE).offset(-0x10) as *const LDR_MODULE ;
p_load_module = ((*p_load_module).InMemoryOrderModuleList.Flink as PBYTE).offset(-0x10) as *const LDR_MODULE ;
let base = (*p_load_module).BaseAddress as PVOID;
// let Dos = base as PIMAGE_DOS_HEADER;

// 获取PE头
let dos_header = transmute::<PVOID, PIMAGE_DOS_HEADER>(base) ;
if (*dos_header).e_magic != IMAGE_DOS_SIGNATURE{
return 1;
}

let nt_headers = transmute::<PVOID, PIMAGE_NT_HEADERS>(base.add((*dos_header).e_lfanew as usize ));
let file_headers = transmute::<PVOID, PIMAGE_FILE_HEADER>(base.add((*dos_header).e_lfanew as usize + size_of::<u32>())) ;
let optional_headers = transmute::<PVOID, PIMAGE_OPTIONAL_HEADER>(base.add((*dos_header).e_lfanew as usize + size_of::<u32>() + IMAGE_SIZEOF_FILE_HEADER)) as PIMAGE_OPTIONAL_HEADER;
let export_directory = (*nt_headers).OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT as usize];

if export_directory.Size == 0 {
println!("No export table");
return 1;
}

// 获取导出表
let export_table = transmute::<PVOID, PIMAGE_EXPORT_DIRECTORY>(base.add(export_directory.VirtualAddress as usize)) ;

// 遍历导出函数名表
let AddressOfNames = transmute::<PVOID, *const u32>(base.add((*export_table).AddressOfNames as usize)) ;
let NumberOfFunctions = (*export_table).NumberOfFunctions as usize;
let AddressOfFunctions = transmute::<PVOID, *const u32>(base.add((*export_table).AddressOfFunctions as usize));
let AddressOfNameOrdinales = transmute::<PVOID, *const u32>(base.add((*export_table).AddressOfFunctions as usize));
for i in 0..NumberOfFunctions-1 {
let FunctionName = transmute::<PVOID, *const i8>(base.add(*AddressOfNames.add(i) as usize)) ;
let FunctionRvaName = transmute::<PVOID, *const i8>(base.add(*AddressOfFunctions.add(i+1) as usize)) ;
let FunctionAddress = CStr::from_ptr(FunctionRvaName);
let FunctionNameString = ConstIntToString(FunctionName);
println!("{}: {:p}", FunctionNameString, FunctionAddress);
}
return 0;

};
}
fn main() {
getPeHeader();
}

地狱之门项目

这个项目应该算是最古老的syscall利用版本了,但也是学习syscall的必看之路。

原理:通过直接读取进程第二个导入模块即NtDLL,解析结构然后遍历导出表,根据函数名Hash找到函数地址,将这个函数读取出来通过0xb8这个操作码来动态获取对应的系统调用号,从而绕过内存监控,在自己程序中执行了NTDLL的导出函数而不是直接通过LoadLibrary然后GetProcAddress

先来看一个正常的syscall调用的汇编代码,在执行syscall之前,都会执行mov eax,xxx赋值一个系统调用号(系统调用号被定义为WORD类型(16位无符号整数))给eax,从而直接在内核中执行函数。并且还通过test byte ptr [SharedUserData+0x308 (000000007ffe0308)],1 来验证当前的线程执行环境是x64还是x86,如果确定执行环境是基于x64则会通过syscall执行系统调用,否则会执行函数返回。

这是一个被上钩的NTDLL的汇编:ZwMapViewOfSection上的Hook是很明显的(jmp <offset>指令,而不是mov r10, rcx)。而ZwMapViewOfSection的邻居ZwSetInformationFile和NtAccessCheckAndAuditAlarm是干净的,它们的系统调用号分别是0x27和0x29。

项目代码分析

首先是实现了两个重要的结构体。在实现过程中需要定义一个与syscall相关联的数据结构:_VX_TABLE_ENTRY事实上每一个系统调用都需要分配这样一个结构,结构体定义如下:

1
2
3
4
5
typedef struct _VX_TABLE_ENTRY {
PVOID pAddress;
DWORD64 dwHash;
WORD wSystemCall;
} VX_TABLE_ENTRY, * PVX_TABLE_ENTRY;

其中包括了指向内存模块的函数地址指针,一个函数哈希(后续通过Hash查找内存模块的函数)以及一个无符号16位的系统调用号wSysemCall
同时还定义了一个更大的数据结构_VX_TABLE用来包含每一个系统调用的函数:

1
2
3
4
5
6
typedef struct _VX_TABLE {
VX_TABLE_ENTRY NtAllocateVirtualMemory;
VX_TABLE_ENTRY NtProtectVirtualMemory;
VX_TABLE_ENTRY NtCreateThreadEx;
VX_TABLE_ENTRY NtWaitForSingleObject;
} VX_TABLE, * PVX_TABLE;

项目最开始还是使用__readgsqword函数来获取TEB模块,如果是win64系统则从寄存器0x30偏移处获取TEB,否则从0X16.

接下来就是获取导出地址表,和我们获取到的方式是差不多的。

成功获取EAT指针之后现在就需要将之前定义的数据结构填充,通过GetVxTableEntry函数填充_VX_TABLE:

1
2
3
4
5
6
7
8
9
10
11
VX_TABLE Table = { 0 };
Table.NtAllocateVirtualMemory.dwHash = 0xf5bd373480a6b89b;
GetVxTableEntry(ImageBase, ExportTable, &Table.NtAllocateVirtualMemory);

Table.NtCreateThreadEx.dwHash = 0x64dc7db288c5015f;
GetVxTableEntry(ImageBase, ExportTable, &Table.NtCreateThreadEx);
Table.NtProtectVirtualMemory.dwHash = 0x858bcb1046fb6a37;
GetVxTableEntry(ImageBase, ExportTable, &Table.NtProtectVirtualMemory);

Table.NtWaitForSingleObject.dwHash = 0xc6a2fa174e551bcb;
GetVxTableEntry(ImageBase, ExportTable, &Table.NtWaitForSingleObject);

下面就是最核心的操作了,就是定位进行syscall的操作符(0xb8即mov eax)的位置。在代码中,首先遍历导出地址表,通过djb2算法算出函数名的hash值,与我们想要的函数的hash值进行比较,如果相等,则填充函数地址。并且后面还要验证汇编代码0xb8即mov eax是否存在。
最后就是获取到系统调用号,因为系统调用号是一个WORD类型,也就是两个字节并且是小端存储,因此通过高低位转换的方式最终动态获得系统调用号填充到函数结构体pVxTableEntry中。

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
BOOL GetVxTableEntry(PVOID pModuleBase, PIMAGE_EXPORT_DIRECTORY pImageExportDirectory, PVX_TABLE_ENTRY pVxTableEntry) {
PDWORD pdwAddressOfFunctions = (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfFunctions);
PDWORD pdwAddressOfNames = (PDWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfNames);
PWORD pwAddressOfNameOrdinales = (PWORD)((PBYTE)pModuleBase + pImageExportDirectory->AddressOfNameOrdinals);

for (WORD cx = 0; cx < pImageExportDirectory->NumberOfNames; cx++) {
PCHAR pczFunctionName = (PCHAR)((PBYTE)pModuleBase + pdwAddressOfNames[cx]);
PVOID pFunctionAddress = (PBYTE)pModuleBase + pdwAddressOfFunctions[pwAddressOfNameOrdinales[cx]];

if (djb2(pczFunctionName) == pVxTableEntry->dwHash) {
pVxTableEntry->pAddress = pFunctionAddress;

// Quick and dirty fix in case the function has been hooked
WORD cw = 0;
while (TRUE) {
// check if syscall, in this case we are too far
if (*((PBYTE)pFunctionAddress + cw) == 0x0f && *((PBYTE)pFunctionAddress + cw + 1) == 0x05)
return FALSE;

// check if ret, in this case we are also probaly too far
if (*((PBYTE)pFunctionAddress + cw) == 0xc3)
return FALSE;

// First opcodes should be :
// MOV R10, RCX
// MOV RCX, <syscall>
if (*((PBYTE)pFunctionAddress + cw) == 0x4c
&& *((PBYTE)pFunctionAddress + 1 + cw) == 0x8b
&& *((PBYTE)pFunctionAddress + 2 + cw) == 0xd1
&& *((PBYTE)pFunctionAddress + 3 + cw) == 0xb8
&& *((PBYTE)pFunctionAddress + 6 + cw) == 0x00
&& *((PBYTE)pFunctionAddress + 7 + cw) == 0x00) {
BYTE high = *((PBYTE)pFunctionAddress + 5 + cw);
BYTE low = *((PBYTE)pFunctionAddress + 4 + cw);
pVxTableEntry->wSystemCall = (high << 8) | low;
break;
}

cw++;
};
}
}

return TRUE;
}

现在每个我们想要调用的函数都获取到了对应的函数hash,系统调用号,函数地址指针,都存放在函数结构体pVxTableEntry中,最后要做的就是生成一段汇编代码来调用这些函数,

最后生成的汇编代码如下。这样我们便能够模拟引入Ntdll调用Nt函数的方式,也就是调用Native API而不是Win API来绕过Hooks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.data
wSystemCall DWORD 000h

.code
HellsGate PROC
mov wSystemCall, 000h
mov wSystemCall, ecx
ret
HellsGate ENDP

HellDescent PROC
mov r10, rcx
mov eax, wSystemCall

syscall
ret
HellDescent ENDP
end

光环之门 Halo’s Gate

这个项目其实是在解决地狱之门的一些局限性,从分析地狱之门的代码中我们可以看到它所访问内存中的NTDLL也必须是默认的或者说是未经修改的,因为如果本身NTDLL已经被修改过,或者被Hook过则函数汇编操作码就不会是0xb8,对应着mov eax,而可能是0xE9,对应着jmp。因此当我们需要调用的NT函数已经被AV/EDR所Hook,那我们就无法通过地狱之门来动态获取它的系统调用号。

就像上面这张图片一样,ZwMapViewOfSection已经被Hook,而它的邻函数ZwSetInformationFile和NtAccessCheckAndAuditAlarm都没有被Hook,并且邻函数的系统调用号也是临接的。所以说其实我们可以查看被hook函数的邻函数,查看邻函数的系统调用号,然后再相应的加减调整即可。

正常的没有被hook的函数应该是长这个样子。

当出现被Hook的Nt函数时便采取向周围查询的方式:

GetSSN

这里还介绍一种更加方便简单和迅速的方法来发现SSN(syscall number),这种方法不需要unhook,不需要手动从代码存根中读取,也不需要加载NTDLL新副本,可以将它理解成为光环之门的延伸,试想当上下的邻函数都被Hook时,光环之门的做法是继续递归,在不断的寻找没有被Hook的邻函数,而在这里假设一种最坏的情况是所有的邻函数(指Nt*函数)都被Hook时,那最后将会向上递归到SSN=0的Nt函数。

其实可以理解为系统调用的存根重新实现 + 动态 SSN 解析

首先我们需要知道:

  • 1.实际上所有的Zw函数和Nt同名函数实际上是等价的
  • 2.系统调用号实际上是和Zw函数按照地址顺序的排列是一样的

因此我们就只需要遍历所有Zw函数,记录其函数名和函数地址,最后将其按照函数地址升序排列后,每个函数的SSN就是其对应的排列顺序

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
void GetSSN()
{
std::map<int, string> Nt_Table;
PBYTE ImageBase;
PIMAGE_DOS_HEADER Dos = NULL;
PIMAGE_NT_HEADERS Nt = NULL;
PIMAGE_FILE_HEADER File = NULL;
PIMAGE_OPTIONAL_HEADER Optional = NULL;
PIMAGE_EXPORT_DIRECTORY ExportTable = NULL;

PPEB Peb = (PPEB)__readgsqword(0x60);
PLDR_MODULE pLoadModule;
// NTDLL
pLoadModule = (PLDR_MODULE)((PBYTE)Peb->LoaderData->InMemoryOrderModuleList.Flink->Flink - 0x10);
ImageBase = (PBYTE)pLoadModule->BaseAddress;

Dos = (PIMAGE_DOS_HEADER)ImageBase;
if (Dos->e_magic != IMAGE_DOS_SIGNATURE)
return 1;
Nt = (PIMAGE_NT_HEADERS)((PBYTE)Dos + Dos->e_lfanew);
File = (PIMAGE_FILE_HEADER)(ImageBase + (Dos->e_lfanew + sizeof(DWORD)));
Optional = (PIMAGE_OPTIONAL_HEADER)((PBYTE)File + sizeof(IMAGE_FILE_HEADER));
ExportTable = (PIMAGE_EXPORT_DIRECTORY)(ImageBase + Optional->DataDirectory[0].VirtualAddress);

PDWORD pdwAddressOfFunctions = (PDWORD)((PBYTE)(ImageBase + ExportTable->AddressOfFunctions));
PDWORD pdwAddressOfNames = (PDWORD)((PBYTE)ImageBase + ExportTable->AddressOfNames);
PWORD pwAddressOfNameOrdinales = (PWORD)((PBYTE)ImageBase + ExportTable->AddressOfNameOrdinals);
for (WORD cx = 0; cx < ExportTable->NumberOfNames; cx++)
{
PCHAR pczFunctionName = (PCHAR)((PBYTE)ImageBase + pdwAddressOfNames[cx]);
PVOID pFunctionAddress = (PBYTE)ImageBase + pdwAddressOfFunctions[pwAddressOfNameOrdinales[cx]];
if (strncmp((char*)pczFunctionName, "Zw",2) == 0) {
// printf("Function Name:%s\tFunction Address:%p\n", pczFunctionName, pFunctionAddress);
Nt_Table[(int)pFunctionAddress] = (string)pczFunctionName;
}
}
int index = 0;
for (std::map<int, string>::iterator iter = Nt_Table.begin(); iter != Nt_Table.end(); ++iter) {
cout << "index:" << index << ' ' << iter->second << endl;
index += 1;
}
}

https://blog.csdn.net/weixin_43655282/article/details/104291312
https://bbs.kanxue.com/thread-151456.htm
https://www.dailychina.news/showArticle?main_id=a1057a0c93a81ba5960abe906c76377e
https://www.anquanke.com/post/id/267345#h3-7
https://tttang.com/archive/1464/#toc_hells-gate
https://xz.aliyun.com/t/11496#toc-3
https://www.kn0sky.com/?p=69
https://bbs.kanxue.com/thread-266678.htm


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!