Syscall笔记

admin 2024年5月30日21:58:51评论3 views字数 12708阅读42分21秒阅读模式
免责声明:
本文所涉及的任何技术、信息或工具,仅供学习和参考之用。请勿利用本文提供的信息从事任何违法活动或不当行为。任何因使用本文所提供的信息或工具而导致的损失、后果或不良影响,均由使用者个人承担责任,与本文作者无关。作者不对任何因使用本文信息或工具而产生的损失或后果承担任何责任。使用本文所提供的信息或工具即视为同意本免责声明,并承诺遵守相关法律法规和道德规范。

1.基础知识 

我们知道,系统核心态指的是R0,用户态指的是R3,系统代码在核心态下运行,用户代码在用户态下运 行。系统中一共有四个权限级别, R1和R2运行设备驱动, R0到R3权限依次降低, R0和R3的权限分别为 最高和最低。

Syscall笔记

而我们的** syscall** 是一个计算机操作系统中的指令,用于向操作系统内核发起系统调用系统调用是 用户空间程序与操作系统内核进行交互的方式之一 ,用于请求操作系统执行特定的功能,如文件操作、进程管理、网络通信等。

在用户态运行的系统要控制系统时,或者要运行系统代码就必须取得R0权限。用户从R3R0需要借助 ntdll.dll中的函数,这些函数分别以“Nt”“Zw”开头,这种函数叫做Native API,下图是调用过程:

Syscall笔记

比如当我们调用类似 kernel32.dll CreateThread() 时,最终会进入 0 ( R0)调用 ntdll.dll中的ZwCreateThreadEx(),接下来我们来逆向一下该函数的实现。

Syscall笔记

可以看到先向 eax 里面存储了一个值,即系统调用号 SSN,然后再进行 syscall。
我们再看一下相邻函数的 SSN,会发现该值是递增的,且大致格式都如下:

movr10,rcx

moveax,xxh

syscall

Fence1-1

2. Syscall 是如何绕过EDR 的?

要回答这个问题之前,我们要先明白 EDR 是如何工作的。

EDR 通常会对恶意软件常用的 api 进行 hook,即在调用 api 之前先进入 EDR 进行检查,检查通过之后 才可以继续调用 api。下面是没有被 hook 时 NtReadVirtualMemory 的样子:

Syscall笔记

下面是被 hook 时的样子:

Syscall笔记

可以看到在 NtReadVirtualMemory 的开头就被插入了一条 jmp 指令,跳转到了内存中的其他地方。

我们现在知道 syscall的通用模板,那么我们就自己想办法获取 SSN,然后自己 syscall一下,那么不就绕过了 EDR hook,直接去调用内核层的一些东西了。

3. 项目学习     

Syscall 有很多使用的项目都值得我们学习,不同的项目也都是了作者和 EDR 厂商的对抗,下面是我对于

几个经典项目的一些理解,大家不要只看文章,  一定要去看看源码,这样思路才会清晰。

3.1 Hell's Gate 地狱之门

我们先一起看一下经典的地狱之门,项目地址: https://github.com/am0nsec/HellsGate/首先看 main.c 中的 main 函数:

先调用了 RtlGetThreadEnvironmentBlock函数

Syscall笔记

这个函数是获取 TEB的,因为这个项目涉及到了一些 PEB的知识,如果对 PEB TEB不太熟悉可以看一下https://xz.aliyun.com/t/13556

Syscall笔记

接下来两句代码就是先获取 PEB,然后判断系统版本是不是 Win10,如果不是的话就直接退出。

Syscall笔记

然后通过 PEB的相关知识获取到 ntdll module,然后紧接着看一下导出表的相关信息,如果没有则证明前面的步骤发生了错误,直接退出。其中 GetImageExportDirectory函数需要一些 PE的知识才可以看懂。

Syscall笔记

然后就进入代码的关键部分了,先给出 Table 中需要 syscall 的函数名的 hash 值,然后调用 GetVxTableEntry 去获取 SSN,去看一下函数的实现

Syscall笔记

这个函数先是获取到导出表的相关结构,然后循环比较,并使用 djb2 这个哈希函数来计算当前函数名称  hash,如果一样则证明找到了该函数,并且将地址存储到了pVxTableEntry->pAddress

然后判断了下所在位置是不是已经超过了寻找的范围还没有找到,推荐一个汇编和 hex 互相转换的网 站: https://defuse.ca/online-x86-assembler.htmcw的作用是方便逐个字节比较

Syscall笔记

然后就是寻找 SSN,这里通过判断上面 syscall 格式来获取 SSN,然后分别获取高位和低位,高位左移 位再和低位或操作得到正确的 SSN,并存储到pVxTableEntry->wSystemCall 

Syscall笔记

然后我们回到 main函数,又调用了 payload函数,这个函数就是执行我们 payload的地方

Syscall笔记

我们重点看红框框出来的函数

Syscall笔记

这两个函数是从别的文件导入的

Syscall笔记

看一下 asm文件里面就是这两个函数

Syscall笔记

先在数据段定义了 wSystemCall,用来存放 SSN

HellsGate 的参数就是我们的 SSN,参数会根据调用约定放到了 ecx 中,所以 wSystemCall 就获取了正 确的 SSN

HellDescent 则直接仿照我们上面的格式进行 syscall,参数正常传参即可,这样就在被 hook 的情况下 绕过 hook 完成一次对内核的操作,所以 HellsGate  HellDescent 成对出现即可,用法还是比较简单 的。

至此我们来总结一下地狱之门项目:

  • 从内存中已经加载的ntdll.dll模块中通过遍历解析导出表,定位函数地址,再获取系统调用号
  •   实现了动态获取 SSN
  •    需要一块干净的内存 ntdll 模块,否则无法正常获取 SSN
  • 直接系统调用

3.2 Halo's Gate 光环之门

项目地址: https://github.com/trickster0/TartarusGate/

地狱之门实现了动态获取 SSN,但是有一个缺点,那就是 ntdll 的内存必须是干净的,否则无法获取 SSN ,这个时候我们就需要光环之门了。

原理如下:当我们所需要的 Nt 函数被 hook 时,它相邻的 Nt 函数可能没有被 hook,因为 EDR 不可能 hook 所有的 Nt 函数,总有一些不敏感的 Nt 函数没有被 hook,这样我们从我们需要的 Nt 函数出发,  向上或者向下寻找,找到没有被 hook  Nt 函数,然后它的 SSN 加上或减去步数就得到了我们需要的  SSN

看下图,ZwMapViewOfSection 显然被 hook 了,因为它开头是jmp <offset>指令,而不是 movr10, rcx,但是相邻的 ZwSetInformationFile  NtAccessCheckAndAuditAlarm 却是干净的,他们的 系统调用号分别是0x270x29。因此,确定 ZwMapViewOfSection 编号非常简单 ,只需查看邻居编号  并相应地进行调整即可。如果邻居也被 hook 了,那么检查邻居的邻居,依此类推:

Syscall笔记

这个项目和地狱之门的大致思路是一样的,但是函数是用汇编实现的,我们重点看下面三个过程,findSyscallNumber, halosGateUphalosGateDown,分别是直接获取SSN以及向上和向下获取SSN 的过程。

Syscall笔记

Syscall笔记

我们可以观察到代码中有一处硬编码,其实是对应 Nt函数的 rawhex格式,我们通过下图可以看到前四个字节是4c8bd1b8 ,由于是小端格式存储,在内存中就变成了00B8D18B4Ch

Syscall笔记

上面第一个名为findSyscallNumber的过程很简单,就是比较硬编码和在内存中获取的是不是一样, 如果不一样我们就认为它被 hook 了,并且跳转到 error 过程,如果一样就直接获取 SSN 返回。
halosGateUp halosGateDown两个过程是在上面的基础上加了一个寻找 rdx 偏移的步骤,思路还 是一样的。如果感到汇编比较难懂的话可以看下面的项目,是对该项目的补充版且用 c++实现。
项目总结:
  • 该项目在地狱之门的基础上增加了检查前四个字节确定是否被 hook 的步骤,并且如果被 hook 尝 试查找邻居是否被 hook 来获取 SSN

3.3 TartarusGate

根据作者描述,这个项目是光环之门的进化版,因为 EDR hook不一定就是在第一条指令,在第二条指令中也可能出现 jmp指令,比如下图就是这样:

Syscall笔记

所以说这个项目对这种情况进行了判断,在 GetVxTableEntry()里我们可以看到进行了两次 if 判断,同 时我们也看到了光环之门那两个过程对应的 c++实现。

Syscall笔记

除此之外,这个项目在 asm 文件中增加了一些 nop 指令来混淆

Syscall笔记

3.4 GetSSN

这是一个获取 SSN 的思路,方法比较简单,不需要unhook,不需要手动从代码存根中读取,也不需要 加载NTDLL新副本。

我们知道 SSN 是递增的,所以我们遍历 ntdll 所有导出函数,然后按照地址升序排序,从 0 开始,不就 得到了所有导出函数的 SSN 了。

int 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;// NTDLLpLoadModule = (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:%stFunction Address:%pn", 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;}}

Fence3-1

3.5 SysWhispers

项目地址:https://github.com/jthuraisamy/SysWhispers

这是老外写的一个直接系统调用的框架,这个框架现在有三个版本,我们先一起来看第一个版本。

我们知道 SSN 在不同版本下是不一样的,因此直接系统调用要适配多版本可能会有些麻烦,而这个项目 就可以帮助我们解决这个问题。在不指定版本的情况下,  Syswhispers 会导出指定函数的所有已知版本  的系统调用号,根据操作系统版本的不同再进行指定调用。

不同操作系统间调用号的不同详情可参考

  • https://j00ru.vexillium.org/syscalls/nt/32/

  • https://j00ru.vexillium.org/syscalls/nt/64/

我们以 NtCreateProcess 为例看一下用法,命令如下:

python .syswhispers.py -f NtCreateProcess -o syscall 

然后我们得到了一个 asm 和一个.h 文件,通过包含头文件就可以进行 syscall。

Syscall笔记

将两个文件包含到头文件中,然后按照博客中配置一下:

https://blog.csdn.net/qq_29176323/article/details/129145326

Syscall笔记

.h 文件中声明了 NtCreateProcess 原型

Syscall笔记

看 asm 文件,其实就是不断的比较系统版本然后跳转

Syscall笔记

然后可以在 cpp 里面写一个简单的小 demo:

#include "syscall.h"#include <stdio.h>int main() {// 准备创建进程的参数OBJECT_ATTRIBUTES objAttr;InitializeObjectAttributes(&objAttr, NULL, 0, NULL, NULL);// 使用 NtCreateProcess 创建一个新的进程HANDLE hProcess;NTSTATUS status = NtCreateProcess(&hProcess, PROCESS_ALL_ACCESS, &objAttr, GetCurrentProcess(), FALSE, NULL, NULL, NULL);}

Fence3-2

但是一版本的项目特征太多,很容易被 AV/EDR 针对,所以出现了二版本。

3.6 SysWhispers2

项目地址:https://github.com/jthuraisamy/SysWhispers2

用法与 Syswhispers 大致相同,但是会生成很多文件:

Syscall笔记

  • 有 x64 的,有 x86 的,这个区别应该就不用介绍了;

  •  有.nasm 的,有.asm 的,这个是因为 SysWhispers2 为了适配 mingw-gcc 而做出的改变,后缀 nasm 的文件可以被 gcc 直接编译,而 vs 中无法直接编译,并且mingw-gcc 支持 x64 的内联汇  编,这点就十分方便;

  • std的,有rnd的,这个是std是基础的syscall 方法,rnd 则是使用了RandomSyscallJumps 的方法,下面会有具体介绍。

我们以 NtCreateThreadEx 为例,分析一下调用过程,我们在 syscall.h 文件中找到了 NtCreateThreadEx,是通过外部导入的

Syscall笔记

去看 asm 文件,找到了该函数(过程),先是将计算出的 hash 值赋给 currentHash,每次使用时 hash 都不一样,然后再调用WhisperMain 过程。

Syscall笔记

看一下 WhisperMain 过程

Syscall笔记

首先是一些保护寄存器的操作,不用关心,然后将上个过程的 currentHash 给 ecx ,作为参数调用SW2_GetSyscallNumber,获取系统调用号,然后将结果存储到syscallNumber变量中。然后再调用SW2_GetRandomSyscallAddress,随机获取一个ntdll导出函数中的一个syscall指令的地址,SysWhispers2 并没有直接在主程序中调用syscall指令,而是采用了间接系统调用,随机获取一个syscall指令的地址后,跳转到该地址执行syscall指令,这样就规避了在主程序中直接系统调用的特征。然后就是一些回复寄存器的操作,也不用关心,紧接着后面就call随机的syscall地址。

3.6.1 SW2_GetSyscallNumber

我们接下来看一下几个关键函数的实现,首先是 SW2_GetSyscallNumber:

Syscall笔记

里面首先调用了 SW2_PopulateSyscallList,跟进看一下:

代码有点长,我们慢慢来分析,首先先判断 SW2_SyscallList 是否被填充,如果被填充直接返回即可,如 果没有被填充就继续接下来的填充操作,先通过 PEB 得到 ntdll

Syscall笔记

然后遍历导出表,定位所有 Zw 开头函数,并且将其 hash 和地址存入 Entries

Syscall笔记

然后就是一个冒泡排序,根据地址升序排序,对应的序号就是 SSN

Syscall笔记

分析完之后再看 SW2_GetSyscallNumber 就很简单了,剩下的部分就是循环匹配 hash,如果匹配到了 就返回 SSN,找不到就返回 -1。

3.6.2 SW2_GetRandomSyscallAddress

再看 SW2_GetRandomSyscallAddress,我们需要先 #define RANDSYSCALL 声明宏才能开启

Syscall笔记

如上代码,  Zw 函数起始偏移 0x12 的位置即是 syscall 指令,其对应的第一个字节是 0x0F。然后通过随 机数的方式随机找到一处 ntdll 里面的 syscall 来进行间接系统调用。

3.7 间接/直接系统调用

在继续学习三版本的项目之前,我们先对比一下直接系统调用和间接系统调用,直接系统调用就行上面 的地狱之门等项目一样,直接在汇编中写出来 syscall,没有进入 ntdll 中 syscall,而间接 syscall 就像  SysWhispers2 一样,进入到 ntdll 里面随便找一个 syscall 进行 call。

我们借助https://redops.at/en/blog/direct-syscalls-vs-indirect-syscalls里面的一张图片来说明直接系统调用和间接系统调用在堆栈上的区别。

对于直接系统调用,系统调用本身及其返回执行发生在执行进程的.exe文件的内存空间中,这会导致调用 堆栈的顶帧来自.exe 内存,而不是ntdll.dll内存,这个特征可能会导致程序被杀掉,但是间接系统调用的   表现就更合法。系统调用的执行和返回指令都发生在ntdll.dll的内存中,这是正常应用程序进程中的预期 行为。

Syscall笔记

再补充两张图,正常程序的调用顺序如下:

Syscall笔记

直接系统调用的如下:

Syscall笔记

我们可以观察到 RIP 指向不同,因此很容易被查杀。

3.8 SysWhispers3

项目地址:https://github.com/klezVirus/SysWhispers3

它的主要提升是支持使用 egg_hunter , 以及使用 jumper & jumper_randomized 来进行间接 syscall。 py .syswhispers.py --preset common -o syscalls_common -m jumper -c mingw

3.8.1 egg_hunter

它的作用是在内存中先用一些垃圾字符占位,然后运行时再从内存中找出来替换成 syscall。

下面是一个简单的 demo,放置一个已知字节序列(egg)作为 syscall 指令的占位符,并在运行时替换 它,这个字节序列时 w00tw00t

NtAllocateVirtualMemory PROCmov [rsp +8], rcx          ; Save registers.mov [rsp+16], rdxmov [rsp+24], r8mov [rsp+32], r9sub rsp, 28hmov ecx, 003970B07h        ; Load function hash into ECX.call SW2_GetSyscallNumber  ; Resolve function hash into syscall number.add rsp, 28hmov rcx, [rsp +8]          ; Restore registers.mov rdx, [rsp+16]mov r8, [rsp+24]mov r9, [rsp+32]mov r10, rcxDB 77h                       ; "w"DB 0h                        ; "0"DB 0h                        ; "0"DB 74h                       ; "t"DB 77h                       ; "w"DB 0h                        ; "0"DB 0h                        ; "0"DB 74h                       ; "t"retNtAllocateVirtualMemory ENDP

Fence3-3

这样直接运行当然会报错,为了可用,我们需要使用必要的操作码修改内存中的“w00tw00t”,在这种情况下0f 05c3909090cccc,这转换为syscall;nop;nop;ret;nop;int3;int3

以下是作者给出的FindAndReplace函数的demo

#include <stdio.h>#include <stdlib.h>#include <Windows.h>#include <psapi.h>#define DEBUG 0HMODULE GetMainModule(HANDLE);BOOL GetMainModuleInformation(PULONG64, PULONG64);void FindAndReplace(unsigned char[], unsigned char[]);HMODULE GetMainModule(HANDLE hProcess){HMODULE mainModule = NULL;HMODULE* lphModule;LPBYTE lphModuleBytes;DWORD lpcbNeeded;// First call needed to know the space (bytes) required to store the modules' handlesBOOL success = EnumProcessModules(hProcess, NULL, 0, &lpcbNeeded);// We already know that lpcbNeeded is always > 0if (!success || lpcbNeeded == 0){printf("[-] Error enumerating process modulesn");// At this point, we already know we won't be able to dyncamically // place the syscall instruction, so we can exitexit(1);}// Once we got the number of bytes required to store all the handles for // the process' modules, we can allocate space for themlphModuleBytes = (LPBYTE)LocalAlloc(LPTR, lpcbNeeded);if (lphModuleBytes == NULL){printf("[-] Error allocating memory to store process modules handlesn"); exit(1);}unsigned int moduleCount;moduleCount = lpcbNeeded / sizeof(HMODULE);lphModule = (HMODULE*)lphModuleBytes;success = EnumProcessModules(hProcess, lphModule, lpcbNeeded, &lpcbNeeded);if (!success){printf("[-] Error enumerating process modulesn");exit(1);}// Finally storing the main modulemainModule = lphModule[0];// Avoid memory leakLocalFree(lphModuleBytes);// Return main modulereturn mainModule;}BOOL GetMainModuleInformation(PULONG64 startAddress, PULONG64 length) {HANDLE hProcess = GetCurrentProcess();HMODULE hModule = GetMainModule(hProcess);MODULEINFO mi;GetModuleInformation(hProcess, hModule, &mi, sizeof(mi));printf("Base Address: 0x%llun", (ULONG64)mi.lpBaseOfDll);printf("Image Size:   %un", (ULONG)mi.SizeOfImage);printf("Entry Point:  0x%llun", (ULONG64)mi.EntryPoint);printf("n");*startAddress = (ULONG64)mi.lpBaseOfDll;*length = (ULONG64)mi.SizeOfImage;DWORD oldProtect;VirtualProtect(mi.lpBaseOfDll, mi.SizeOfImage, PAGE_EXECUTE_READWRITE, &oldProtect);return 0;}void FindAndReplace(unsigned char egg[], unsigned char replace[]){ULONG64 startAddress = 0;ULONG64 size = 0;GetMainModuleInformation(&startAddress, &size);if (size <= 0) {printf("[-] Error detecting main module size");exit(1);}ULONG64 currentOffset = 0;unsigned char* current = (unsigned char*)malloc(8*sizeof(unsigned char*)); size_t nBytesRead;printf("Starting search from: 0x%llun", (ULONG64)startAddress + currentOffset);while (currentOffset < size - 8){currentOffset++;LPVOID currentAddress = (LPVOID)(startAddress + currentOffset); if(DEBUG > 0){printf("Searching at 0x%llun", (ULONG64)currentAddress); }if (!ReadProcessMemory((HANDLE)((int)-1), currentAddress, current, 8, &nBytesRead)) {printf("[-] Error reading from memoryn");exit(1);}if (nBytesRead != 8) {printf("[-] Error reading from memoryn");continue;}if(DEBUG > 0){for (int i = 0; i < nBytesRead; i++){printf("%02x ", current[i]);}printf("n");}if (memcmp(egg, current, 8) == 0){printf("Found at %llun", (ULONG64)currentAddress);WriteProcessMemory((HANDLE)((int)-1), currentAddress, replace, 8, &nBytesRead);}}printf("Ended search at:   0x%llun", (ULONG64)startAddress + currentOffset);free(current);}

                        Fence3-4

然后是主函数中:

int main(int argc, char** argv) {unsigned char egg[] = { 0x77, 0x00, 0x00, 0x74, 0x77, 0x00, 0x00, 0x74 }; // w00tw00tunsigned char replace[] = { 0x0f, 0x05, 0x90, 0x90, 0xC3, 0x90, 0xCC, 0xCC };// syscall; nop; nop; ret; nop; int3; int3//####SELF_TAMPERING####(egg, replace);Inject();return 0;}

                              Fence3-5

3.8.2jumper &jumper_randomized

先来看 jumper,使用命令 python syswhispers.py -f NtCreateThreadEx -m jumper -o jumper 来生成一个采用该技术的 demo
看 asm 文件,可以看到先将 syscall 的地址放到 r15,再  jump r15实现的

Syscall笔记

再来看 jumper_randomized,这项技术和和 SysWhisper2 非常相似,使用SW3_GetRandomSyscallAddress函数先获取一个随机的syscall地址,实现和二版本的项目几乎一样,放到r11中,然后再jmpr11即可

Syscall笔记

4. 总结      

看到这里相信大家对 syscall 都有了一定的了解了,我们再简单的总结一下。
首先,为了防止 api 被 hook,提出了 syscall 函数,这产生了地狱之门的项目,然而,当 ntdll 被 hook 时,这种方法就失效了,因此出现了更高级的技术,如“光环之门”,试图通过邻居来获取系统调用号SSN)。然而,即使获取了SSN,仍然有可能被安全软件检测到,因为系统调用的签名(sysall)可能会被查杀。为了解决这个问题,出现了“egg_hunter”等技术。但是堆栈的问题还没有解决,我们需要合法的堆栈,SysWhispers2 SysWhispers3,它们提出了间接系统调用的方案,进一步提高了对系统调用的隐藏性和逃避性,使得安全工具更难检测到和拦截这些调用。
交流群

Syscall笔记

广告

Syscall笔记

原文始发于微信公众号(影域实验室):Syscall笔记

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年5月30日21:58:51
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Syscall笔记https://cn-sec.com/archives/2796216.html

发表评论

匿名网友 填写信息