Важная информация
RSS лента

The trick

Загрузчик, шеллкод, без рантайма... (часть 2)

Оценить эту запись
Первая часть ->.
После извлечения файлов вызывается функция ExecuteProcess которая запускает выполнение команд используя функцию ShellExecuteEx:
VB Code:
  1. ' // Execution command process
  2. Function ExecuteProcess() As Boolean
  3.     Dim index       As Long:                Dim bItem       As BinExecListItem
  4.     Dim pPath       As Long:                Dim pErrMsg     As Long
  5.     Dim shInfo      As SHELLEXECUTEINFO:    Dim pTempString As Long
  6.     Dim pItem       As Long:                Dim status      As Long
  7.  
  8.     ' // Set pointer and size
  9.     shInfo.cbSize = Len(shInfo)
  10.     pItem = pExecutesTable
  11.     
  12.     ' // Go thru all items
  13.     For index = 0 To ProjectDesc.execListDescriptor.dwNumberOfItems - 1
  14.     
  15.         ' // Copy item
  16.         CopyMemory bItem, ByVal pItem, ProjectDesc.execListDescriptor.dwSizeOfItem
  17.         
  18.         ' // Set pointer to next item
  19.         pItem = pItem + ProjectDesc.execListDescriptor.dwSizeOfItem
  20.         
  21.         ' // Normalize path
  22.         pPath = NormalizePath(pStringsTable + bItem.ofstFileName, 0)
  23.         
  24.         ' // Fill SHELLEXECUTEINFO
  25.         shInfo.lpFile = pPath
  26.         shInfo.lpParameters = pStringsTable + bItem.ofstParameters
  27.         shInfo.fMask = SEE_MASK_NOCLOSEPROCESS Or SEE_MASK_FLAG_NO_UI
  28.         shInfo.nShow = SW_SHOWDEFAULT
  29.         
  30.         ' // Performing...
  31.         status = ShellExecuteEx(shInfo)
  32.         
  33.         ' // If error occurs show notification (retry, abort, ignore)
  34.         Do Until status
  35.             
  36.             If pErrMsg Then SysFreeString pErrMsg: pErrMsg = 0
  37.             
  38.             ' // Ignore error
  39.             If bItem.dwFlags And EF_IGNOREERROR Then
  40.                 Exit Do
  41.             End If
  42.                         
  43.             pTempString = GetString(MID_ERROREXECUTELINE)
  44.             pErrMsg = StrCat(pTempString, pPath)
  45.             
  46.             SysFreeString pTempString: pTempString = 0
  47.             
  48.             Select Case MessageBox(0, pErrMsg, 0, MB_ICONERROR Or MB_SYSTEMMODAL Or MB_CANCELTRYCONTINUE)
  49.             Case MESSAGEBOXRETURN.IDCONTINUE: Exit Do
  50.             Case MESSAGEBOXRETURN.IDTRYAGAIN
  51.             Case Else: GoTo CleanUp
  52.             End Select
  53.  
  54.             status = ShellExecuteEx(shInfo)
  55.             
  56.         Loop
  57.         
  58.         ' // Wait for process terminaton
  59.         WaitForSingleObject shInfo.hProcess, INFINITE
  60.         CloseHandle shInfo.hProcess
  61.         
  62.     Next
  63.     
  64.     ' // Success
  65.     ExecuteProcess = True
  66.     
  67. CleanUp:
  68.  
  69.     If pTempString Then SysFreeString pTempString
  70.     If pErrMsg Then SysFreeString pErrMsg
  71.     If pPath Then SysFreeString pPath
  72.     
  73. End Function

Эта функция похожа на предыдущую за исключением того что здесь используется функция ShellExecuteEx вместо извлечения. Обратите внимание что каждая операция выполняется синхронно, т.е. каждый вызов процедуры ShellExecuteEx ждет окончания выполнения команды.
Если предыдущая функция выполнилась успешно тогда вызывается функция RunProcess которая подготовливает данные для исполнения главного исполняемого файла из памяти:
VB Code:
  1. ' // Run exe from project in memory
  2. Function RunProcess() As Boolean
  3.     Dim bItem       As BinStorageListItem:  Dim Length      As Long
  4.     Dim pFileData   As Long
  5.     
  6.     ' // Get descriptor of executable file
  7.     CopyMemory bItem, ByVal pStoragesTable + ProjectDesc.storageDescriptor.dwSizeOfItem * _
  8.                       ProjectDesc.storageDescriptor.iExecutableIndex, Len(bItem)
  9.     
  10.  
  11.     ' // Alloc memory within top memory addresses
  12.     pFileData = VirtualAlloc(ByVal 0&, bItem.dwSizeOfFile, MEM_TOP_DOWN Or MEM_COMMIT, PAGE_READWRITE)
  13.     If pFileData = 0 Then Exit Function
  14.     
  15.     ' // Copy raw exe file to this memory
  16.     CopyMemory ByVal pFileData, ByVal pFilesTable + bItem.ofstBeginOfData, bItem.dwSizeOfFile
  17.     
  18.     ' // Free decompressed project data
  19.     HeapFree GetProcessHeap(), HEAP_NO_SERIALIZE, pProjectData
  20.     pProjectData = 0
  21.     
  22.     ' // Run exe from memory
  23.     RunExeFromMemory pFileData, bItem.dwFlags And FF_IGNOREERROR
  24.     
  25.     ' ----------------------------------------------------
  26.     ' // An error occurs
  27.     ' // Clean memory
  28.     
  29.     VirtualFree ByVal pFileData, 0, MEM_RELEASE
  30.     
  31.     ' // If ignore error then success
  32.     If bItem.dwFlags And FF_IGNOREERROR Then RunProcess = True
  33.     
  34. End Function

Эта процедура выделяет память в верхних областях виртуального адресного пространства (поскольку большинство EXE файлов грузятся по довольно низким адресам (обычно 0x00400000). После этого очишается память данных проекта поскольку если EXE файл запустится, то эта память не будет освобождена, затем вызывается функция RunExeFromMemory которая делает следующий шаг в загрузке EXE из памяти. Если по какой-либо причине загрузка EXE файла не состоялась то освобождается выделенная память и управление передается функции Main. Итак, для того чтобы загрузить EXE файл нам нужно освободить память загрузчика, т.е. выгрузить загрузчик. Нам нужно только оставить маленькуий кусочек кода который будет загружать EXE файл и запускать его. Для этого я решил использовать шеллкод, хотя можно использовать и DLL. Шеллкод - это маленький базонезависимый код (код который не ссылается к внешним данным). Но в любом случае нам придется обеспечить доступ к API функциям из шеллкода. Мы не можем вызывать API функции непосредственно из шеллкода поскольку наш главный исполняемый файл будет выгружен и любое обращение к таблице импорта вызовет креш. Второе ограничение - это то что инструкция call использует относительное смещение (это наиболее частый случай). Из этого следует что нам нужно инициализировать некие "трамплины" которые будут перебрасывать нас на API функции. Я решил делать это посредством сплайсинга. Я просто заменяю первые 5 байт функции пусттышки на ассемблерную инструкцию jmp которая ссылается на необходимую API функцию:
VB Code:
  1. ' // Run EXE file by memory address
  2. Function RunExeFromMemory( _
  3.                 ByVal pExeData As Long, _
  4.                 ByVal IgnoreError As Boolean) As Boolean
  5.     Dim Length  As Long:    Dim pCode       As Long
  6.     Dim pszMsg  As Long:    Dim pMsgTable   As Long
  7.     Dim index   As Long:    Dim pCurMsg     As Long
  8.     
  9.     ' // Get size of shellcode
  10.     Length = GetAddr(AddressOf ENDSHELLLOADER) - GetAddr(AddressOf BEGINSHELLLOADER)
  11.     
  12.     ' // Alloc memory within top addresses
  13.     pCode = VirtualAlloc(ByVal 0&, Length, MEM_TOP_DOWN Or MEM_COMMIT, PAGE_EXECUTE_READWRITE)
  14.     
  15.     ' // Copy shellcode to allocated memory
  16.     CopyMemory ByVal pCode, ByVal GetAddr(AddressOf BEGINSHELLLOADER), Length
  17.     
  18.     ' // Initialization of shellcode
  19.     If Not InitShellLoader(pCode) Then GoTo CleanUp
  20.     
  21.     ' // Splice CallLoader function in order to call shellcode
  22.     Splice AddressOf CallLoader, pCode + GetAddr(AddressOf LoadExeFromMemory) - GetAddr(AddressOf BEGINSHELLLOADER)
  23.     
  24.     ' // Check ignore errors
  25.     If Not IgnoreError Then
  26.         
  27.         ' // Alloc memory for messages table
  28.         pMsgTable = VirtualAlloc(ByVal 0&, 1024, MEM_TOP_DOWN Or MEM_COMMIT, PAGE_READWRITE)
  29.         If pMsgTable = 0 Then GoTo CleanUp
  30.         
  31.         ' // Skip pointers
  32.         pCurMsg = pMsgTable + EM_END * 4
  33.         
  34.         For index = 0 To EM_END - 1
  35.         
  36.             ' // Load message string
  37.             pszMsg = GetString(MSG_LOADER_ERROR + index)
  38.             If pszMsg = 0 Then GoTo CleanUp
  39.             
  40.             Length = SysStringLen(pszMsg)
  41.  
  42.             lstrcpyn ByVal pCurMsg, ByVal pszMsg, Length + 1
  43.             
  44.             ' // Store pointer
  45.             CopyMemory ByVal pMsgTable + index * 4, pCurMsg, Len(pCurMsg)
  46.             
  47.             ' // Next message offset
  48.             pCurMsg = pCurMsg + (Length + 1) * 2
  49.             
  50.             SysFreeString pszMsg
  51.             
  52.         Next
  53.         
  54.     End If
  55.     
  56.     ' // Call shellcode
  57.     CallLoader pExeData, pCode, pMsgTable
  58.     
  59. CleanUp:
  60.     
  61.     If pMsgTable Then
  62.         VirtualFree ByVal pMsgTable, 0, MEM_RELEASE
  63.     End If
  64.     
  65.     If pCode Then
  66.         VirtualFree ByVal pCode, 0, MEM_RELEASE
  67.     End If
  68.     
  69. End Function

Как видно из кода он вычисляет размер шеллкода используя разницу между крайними функциями - ENDSHELLLOADER и BEGINSHELLLOADER. Эти функции должны окружать наш шеллкод и иметь разный прототип поскольку VB6 компилятор может объединять идентичные функции. Затем выделяется память для самого шеллкода и он копируется в эту область памяти. После этого вызывается функция InitShellLoader которая сплайсит все функции в шеллкоде:
VB Code:
  1. ' // Shellcode initialization
  2. Function InitShellLoader( _
  3.                  ByVal pShellCode As Long) As Boolean
  4.     Dim hLib    As Long:        Dim sName   As Long
  5.     Dim sFunc   As Long:        Dim lpAddr  As Long
  6.     Dim libIdx  As Long:        Dim fncIdx  As Long
  7.     Dim libName As MessagesID:  Dim fncName As MessagesID
  8.     Dim fncSpc  As Long:        Dim splAddr As Long
  9.     
  10.     ' // +----------------------------------------------------------------+
  11.     ' // |                  Fixing of API addresses                       |
  12.     ' // +----------------------------------------------------------------+
  13.     ' // | In order to call api function from shellcode i use splicing of |
  14.     ' // |    our VB functions and redirect call to corresponding api.    |
  15.     ' // |     I did same in the code that injects to other process.      |
  16.     ' // +----------------------------------------------------------------+
  17.     
  18.     splAddr = GetAddr(AddressOf tVirtualAlloc) - GetAddr(AddressOf BEGINSHELLLOADER) + pShellCode
  19.     
  20.     ' // Get size in bytes between stub functions
  21.     fncSpc = GetAddr(AddressOf tVirtualProtect) - GetAddr(AddressOf tVirtualAlloc)
  22.  
  23.     ' // Use 3 library: kernel32, ntdll и user32
  24.     For libIdx = 0 To 2
  25.     
  26.         ' // Get number of imported functions depending on library
  27.         Select Case libIdx
  28.         Case 0: libName = API_LIB_KERNEL32: fncIdx = 13
  29.         Case 1: libName = API_LIB_NTDLL:    fncIdx = 1
  30.         Case 2: libName = API_LIB_USER32:   fncIdx = 1
  31.         End Select
  32.         
  33.         ' // Get library name from resources
  34.         sName = GetString(libName): If sName = 0 Then Exit Function
  35.         
  36.         ' // Get module handle
  37.         hLib = GetModuleHandle(ByVal sName): If hLib = 0 Then Exit Function
  38.         SysFreeString sName
  39.         
  40.         ' // Go thru functions
  41.         Do While fncIdx
  42.         
  43.             libName = libName + 1
  44.             ' // Get function name
  45.             sName = GetString(libName): If sName = 0 Then Exit Function
  46.             
  47.             ' // Because of GetProcAddress works with ANSI string translate it to ANSI
  48.             sFunc = ToAnsi(sName): If sFunc = 0 Then Exit Function
  49.             
  50.             ' // Get function address
  51.             lpAddr = GetProcAddress(hLib, sFunc)
  52.             SysFreeString sName: SysFreeString sFunc
  53.             
  54.             ' // Error
  55.             If lpAddr = 0 Then Exit Function
  56.             
  57.             ' // Splice stub
  58.             Splice splAddr, lpAddr
  59.             
  60.             ' // Next stub
  61.             splAddr = splAddr + fncSpc
  62.             fncIdx = fncIdx - 1
  63.             
  64.         Loop
  65.         
  66.     Next
  67.     
  68.     ' // Modify CallByPointer
  69.     lpAddr = GetAddr(AddressOf CallByPointer) - GetAddr(AddressOf BEGINSHELLLOADER) + pShellCode
  70.     
  71.     ' // pop eax    - 0x58
  72.     ' // pop ecx    - 0x59
  73.     ' // push eax   - 0x50
  74.     ' // jmp ecx    - 0xFFE1
  75.     
  76.     CopyMemory ByVal lpAddr, &HFF505958, 4
  77.     CopyMemory ByVal lpAddr + 4, &HE1, 1
  78.  
  79.     ' // Success
  80.     InitShellLoader = True
  81.     
  82. End Function
  83.  
  84. ' // Splice function
  85. Sub Splice( _
  86.             ByVal Func As Long, _
  87.             ByVal NewAddr As Long)
  88.     ' // Set memory permissions
  89.     VirtualProtect ByVal Func, 5, PAGE_EXECUTE_READWRITE, 0
  90.     CopyMemory ByVal Func, &HE9, 1                      ' // JMP
  91.     CopyMemory ByVal Func + 1, NewAddr - Func - 5, 4    ' // Relative address
  92. End Sub

Вначале код вычисляет смещение первого "трамплина" (в нашем случае это функция tVirtualAlloc) относительно начала шеллкода, и вычисляет расстояние (в байтах) между функциями "трамплинами". Когда компилятор VB6 компилирует стандартный модуль он размещает функции в том же порядке в котором они определены в модуле. Необходимое условие - обеспечить уникальное возвращаемое значение для каждой функции. Затем код проходит по всем необходимым библиотекам (kernel32, ntdll, user32 - в этом порядке) и их функциям. Первая запись в ресурсах строк соответствует имени библиотеки за котором идут имена функций в этой библиотеке. Когда строка имени функции из ресурсов получена она транслируется в ANSI формат и вызывается функция GetProcAddress. Затем вызывается функция Splice которая собирает "трамплин" к необходимой функции из шеллкода. В конце модифицируется функция CallByPointer для того чтобы обеспечить прыжок из шеллкода на точку входа EXE файла. Далее функция RunExeFromMemory патчит функцию CallLoader для того чтобы обеспечить вызов шеллкода из загрузчика. После этой операции функция формирует таблицу сообщений об ошибках (если нужно) которая представляет из себя просто набор указателей на стоки сообщений. И наконец вызывается пропатченная CallLoader которая прыгает на функцию шеллкода LoadExeFromMemory которая больше не расположена внутри загрузчика, а находится в верхних адресах АП процесса.

Внутри шеллкода.

Итак, я сделал несколько функций внутри шеллкода:
  • LoadExeFromMemory - стартовая функция шеллкода;
  • GetImageNtHeaders - возвращает структуру IMAGE_NT_HEADERS и ее адрес по базовому адресу;
  • GetDataDirectory - возвращает структуру IMAGE_DATA_DIRECTORY и ее адрес по базовому адресу и каталоговому индексу;
  • EndProcess - показать сообщение об ошибке (если есть такое) и завершить процесс;
  • ProcessSectionsAndHeaders - выделить память под все заголовки (DOS, NT, секции) и все секции. Скопировать данные в секции;
  • ReserveMemory - зарезервировать необходимую память под EXE;
  • ProcessRelocations - настроить адреса иесли EXE был загружен не по базовому адресу;
  • ProcessImportTable - сканировать таблицу импорта EXE файла, загрузить необходимые библиотеки и заполнить таблицу адресов импорта (IAT);
  • SetMemoryPermissions - настроить разрешения памяти для каждой секции;
  • UpdateNewBaseAddress - обновить новый базовый адрес в системных структурах PEB и LDR.
Из-за того что нельзя использовать функцию VarPtr, я сделалпохожую функцию используя функцию lstrcpyn - IntPtr. Итак, функция LoadExeFromMemory извлекает вначале заголовок NT и проверяет архитектуру процессора, является ли PE файл исполняемым и является ли он 32-битным приложением. Если проверка прошла успешно тогда шеллкод выгружает загрузчик из памяти используя функцию ZwUnmapViewOfSection. Если функция выполняется успешно EXE образ загрузчика больше не находится в памяти и занимаемая им память освобождается. Отныне мы не можем напрямую вызывать API функции, теперь мы должны использовать наши "трамплины":
VB Code:
  1. ' // Parse exe in memory
  2. Function LoadExeFromMemory( _
  3.                  ByVal pRawData As Long, _
  4.                  ByVal pMyBaseAddress As Long, _
  5.                  ByVal pErrMsgTable As Long) As Boolean
  6.     Dim NtHdr   As IMAGE_NT_HEADERS
  7.     Dim pBase   As Long
  8.     Dim index   As Long
  9.     Dim iError  As ERROR_MESSAGES
  10.     Dim pszMsg  As Long
  11.     
  12.     ' // Get IMAGE_NT_HEADERS
  13.     If GetImageNtHeaders(pRawData, NtHdr) = 0 Then
  14.         iError = EM_UNABLE_TO_GET_NT_HEADERS
  15.         EndProcess pErrMsgTable, iError
  16.         Exit Function
  17.     End If
  18.     
  19.     ' // Check flags
  20.     If NtHdr.FileHeader.Machine <> IMAGE_FILE_MACHINE_I386 Or _
  21.        (NtHdr.FileHeader.Characteristics And IMAGE_FILE_EXECUTABLE_IMAGE) = 0 Or _
  22.        (NtHdr.FileHeader.Characteristics And IMAGE_FILE_32BIT_MACHINE) = 0 Then Exit Function
  23.  
  24.     ' // Release main EXE memory. After that main exe is unloaded from memory.
  25.     ZwUnmapViewOfSection GetCurrentProcess(), GetModuleHandle(ByVal 0&)
  26.  
  27.     ' // Reserve memory for EXE
  28.     iError = ReserveMemory(pRawData, pBase)
  29.     If iError Then
  30.         EndProcess pErrMsgTable, iError
  31.         Exit Function
  32.     End If
  33.     
  34.     ' // Place data
  35.     iError = ProcessSectionsAndHeaders(pRawData, pBase)
  36.     If iError Then
  37.         EndProcess pErrMsgTable, iError
  38.         Exit Function
  39.     End If
  40.     
  41.     ' // Update new base address
  42.     iError = UpdateNewBaseAddress(pBase)
  43.     If iError Then
  44.         EndProcess pErrMsgTable, iError
  45.         Exit Function
  46.     End If
  47.     
  48.     ' // Import table processing
  49.     iError = ProcessImportTable(pBase)
  50.     If iError Then
  51.         EndProcess pErrMsgTable, iError
  52.         Exit Function
  53.     End If
  54.     
  55.     ' // Relocations processing
  56.     iError = ProcessRelocations(pBase)
  57.     If iError Then
  58.         EndProcess pErrMsgTable, iError
  59.         Exit Function
  60.     End If
  61.     
  62.     ' // Set the memory attributes
  63.     iError = SetMemoryPermissions(pBase)
  64.     If iError Then
  65.         EndProcess pErrMsgTable, iError
  66.         Exit Function
  67.     End If
  68.     
  69.     ' // Release error message table
  70.     If pErrMsgTable Then
  71.         tVirtualFree pErrMsgTable, 0, MEM_RELEASE
  72.     End If
  73.     
  74.     ' // Call entry point
  75.     CallByPointer NtHdr.OptionalHeader.AddressOfEntryPoint + pBase
  76.     
  77.     ' // End process
  78.     EndProcess
  79.     
  80. End Function

Затем шеллкод вызывает функцию ReserveMemory показанную ниже. Эта функция извлекает заголовок NT из загружаемого EXE и пытается зарезервировать регион памяти по адресу указанному в поле ImageBase размера SizeOfmage. Если регион по какой-то причине не был выделен функция проверяет имеет ли EXE файл таблицу релокаций. Если так, тогда функция пытается выделять память по любому адресу. Информация о релокациях позволяет загрузить EXE по любому адресу отличному от ImageBase. Она содержит все места в EXE файле где он использует абсолютную адресацию. Мы можем потом подкорректировать эти адреса используя разницу между реальным базовым адресом и адресом указанным в поле ImageBase:
VB Code:
  1. ' // Reserve memory for EXE
  2. Function ReserveMemory( _
  3.                  ByVal pRawExeData As Long, _
  4.                  ByRef pBase As Long) As ERROR_MESSAGES
  5.     Dim NtHdr       As IMAGE_NT_HEADERS
  6.     Dim pLocBase    As Long
  7.     
  8.     If GetImageNtHeaders(pRawExeData, NtHdr) = 0 Then
  9.         ReserveMemory = EM_UNABLE_TO_GET_NT_HEADERS
  10.         Exit Function
  11.     End If
  12.     
  13.     ' // Reserve memory for EXE
  14.     pLocBase = tVirtualAlloc(ByVal NtHdr.OptionalHeader.ImageBase, _
  15.                           NtHdr.OptionalHeader.SizeOfImage, _
  16.                           MEM_RESERVE, PAGE_EXECUTE_READWRITE)
  17.     If pLocBase = 0 Then
  18.         
  19.         ' // If relocation information not found error
  20.         If NtHdr.FileHeader.Characteristics And IMAGE_FILE_RELOCS_STRIPPED Then
  21.         
  22.             ReserveMemory = EM_UNABLE_TO_ALLOCATE_MEMORY
  23.             Exit Function
  24.             
  25.         Else
  26.             ' // Reserve memory in other region
  27.             pLocBase = tVirtualAlloc(ByVal 0&, NtHdr.OptionalHeader.SizeOfImage, _
  28.                                  MEM_RESERVE, PAGE_EXECUTE_READWRITE)
  29.             
  30.             If pLocBase = 0 Then
  31.             
  32.                 ReserveMemory = EM_UNABLE_TO_ALLOCATE_MEMORY
  33.                 Exit Function
  34.                 
  35.             End If
  36.  
  37.         End If
  38.         
  39.     End If
  40.     
  41.     pBase = pLocBase
  42.     
  43. End Function

Если при вызове функции произошла ошибка то показывается сообщение о ней и приложение завершается. В противном случае вызывается функция ProcessSectionsAndHeaders. Эта функция размещает все заголовки в выделенную память, извлекает информацию о всех секциях и копирует все данные в выделенную для них память. Если какая-либо секция имеет неинициализированные данные то этот регион заполняется нулями:
VB Code:
  1. ' // Allocate memory for sections and copy them data to there
  2. Function ProcessSectionsAndHeaders( _
  3.                  ByVal pRawExeData As Long, _
  4.                  ByVal pBase As Long) As ERROR_MESSAGES
  5.  
  6.     Dim iSec    As Long
  7.     Dim pNtHdr  As Long
  8.     Dim NtHdr   As IMAGE_NT_HEADERS
  9.     Dim sec     As IMAGE_SECTION_HEADER
  10.     Dim lpSec   As Long
  11.     Dim pData   As Long
  12.     
  13.     pNtHdr = GetImageNtHeaders(pRawExeData, NtHdr)
  14.     If pNtHdr = 0 Then
  15.         ProcessSectionsAndHeaders = EM_UNABLE_TO_GET_NT_HEADERS
  16.         Exit Function
  17.     End If
  18.     
  19.     ' // Alloc memory for headers
  20.     pData = tVirtualAlloc(ByVal pBase, NtHdr.OptionalHeader.SizeOfHeaders, MEM_COMMIT, PAGE_READWRITE)
  21.     If pData = 0 Then
  22.         ProcessSectionsAndHeaders = EM_UNABLE_TO_ALLOCATE_MEMORY
  23.         Exit Function
  24.     End If
  25.     
  26.     ' // Copy headers
  27.     tCopyMemory pData, pRawExeData, NtHdr.OptionalHeader.SizeOfHeaders
  28.     
  29.     ' // Get address of beginnig of sections headers
  30.     pData = pNtHdr + Len(NtHdr.Signature) + Len(NtHdr.FileHeader) + NtHdr.FileHeader.SizeOfOptionalHeader
  31.     
  32.     ' // Go thru sections
  33.     For iSec = 0 To NtHdr.FileHeader.NumberOfSections - 1
  34.     
  35.         ' // Copy section descriptor
  36.         tCopyMemory IntPtr(sec.SectionName(0)), pData, Len(sec)
  37.         
  38.         ' // Alloc memory for section
  39.         lpSec = tVirtualAlloc(sec.VirtualAddress + pBase, sec.VirtualSize, MEM_COMMIT, PAGE_READWRITE)
  40.         If lpSec = 0 Then
  41.             ProcessSectionsAndHeaders = EM_UNABLE_TO_ALLOCATE_MEMORY
  42.             Exit Function
  43.         End If
  44.         
  45.         ' If there is initialized data
  46.         If sec.SizeOfRawData Then
  47.         
  48.             ' // Take into account  file alignment
  49.             If sec.SizeOfRawData > sec.VirtualSize Then sec.SizeOfRawData = sec.VirtualSize
  50.             
  51.             ' // Copy initialized data to section
  52.             tCopyMemory lpSec, pRawExeData + sec.PointerToRawData, sec.SizeOfRawData
  53.             lpSec = lpSec + sec.SizeOfRawData
  54.             sec.VirtualSize = sec.VirtualSize - sec.SizeOfRawData
  55.             
  56.         End If
  57.  
  58.         ' // Fill remain part with zero
  59.         tFillMemory lpSec, sec.VirtualSize, 0
  60.         
  61.         ' // Next section
  62.         pData = pData + Len(sec)
  63.         
  64.     Next
  65.     
  66. End Function

Затем функция LoadExeFromMemory вызывает функцию UpdateNewBaseAddress которая обновляет новый базовый адрес в user-mode системных структурах. Windows создает специальную структуру называемую PEB (Process Environment Block) для каждого процесса. Это очень полезная структура которая позволяет получить очень много информации о процессе. Множество API функций берут информацию из этой структуры. Для примера GetModuleHandle(NULL) берет возвращаемое значение из PEB.ImageBaseAddress или GetModuleHandle("MyExeName") извлекает информацию из списка загруженных модулей - PEB.Ldr. Нам нужно обновить эту информацию согласно новому базовому адресу для того чтобы API функции возвращали корректное значение. Вот небольшая часть структуры PEB:
VB Code:
  1. Type PEB
  2.     NotUsed                         As Long
  3.     Mutant                          As Long
  4.     ImageBaseAddress                As Long
  5.     LoaderData                      As Long ' // Pointer to PEB_LDR_DATA
  6.     ProcessParameters               As Long
  7.     ' // ....
  8. End Type

Нам интересно только поле ImageBaseAddress и LoaderData. Первое поле содержит базовый адрес EXE файла. Второе поле содержит указатель на структуру PEB_LDR_DATA которая описывает все загруженные модули в процессе:
VB Code:
  1. Type PEB_LDR_DATA
  2.     Length                          As Long
  3.     Initialized                     As Long
  4.     SsHandle                        As Long
  5.     InLoadOrderModuleList           As LIST_ENTRY
  6.     InMemoryOrderModuleList         As LIST_ENTRY
  7.     InInitializationOrderModuleList As LIST_ENTRY
  8. End Type

Эта структура содержит три двухсвязных списка что описывают каждый модуль. Список InLoadOrderModuleList содержит ссылки на элементы в порядке загрузки, т.е. ссылки в этом списке расположены в порядке загрузки (первый модуль в начале). Список InMemoryOrderModuleList тоже самое только в порядке расположения в памяти, а InInitializationOrderModuleList в порядке инициализации. Нам нужно получить первый элемент списка InLoadOrderModuleList который является указателем на структуру LDR_MODULE:
VB Code:
  1. Type LDR_MODULE
  2.     InLoadOrderModuleList           As LIST_ENTRY
  3.     InMemoryOrderModuleList         As LIST_ENTRY
  4.     InInitOrderModuleList           As LIST_ENTRY
  5.     BaseAddress                     As Long
  6.     EntryPoint                      As Long
  7.     SizeOfImage                     As Long
  8.     FullDllName                     As UNICODE_STRING
  9.     BaseDllName                     As UNICODE_STRING
  10.     Flags                           As Long
  11.     LoadCount                       As Integer
  12.     TlsIndex                        As Integer
  13.     HashTableEntry                  As LIST_ENTRY
  14.     TimeDateStamp                   As Long
  15. End Type

Эта структура описывает один модуль. Первый элемент списка InLoadOrderModuleList является описателем главного исполняемого файла. Нам нужно изменить поле BaseAddress на новый базовый адрес и сохранить изменения. Итак, для того чтобы получить адрес структуры PEB мы можем использовать функцию NtQueryInformationProcess которая извлекает множество полезной информации о процессе (узнать подробнее можно в книге 'Windows NT/2000 Native API Reference' by Gary Nebbett). Структура PEB может быть получена из структуры PROCESS_BASIC_INFORMATION которая описывает базовую информацию о процессе:
VB Code:
  1. Type PROCESS_BASIC_INFORMATION
  2.     ExitStatus                      As Long
  3.     PebBaseAddress                  As Long
  4.     AffinityMask                    As Long
  5.     BasePriority                    As Long
  6.     UniqueProcessId                 As Long
  7.     InheritedFromUniqueProcessId    As Long
  8. End Type

Поле PebBaseAddress содержит адрес структуры PEB.
Для того чтобы извлечь структуру PROCESS_BASIC_INFORMATION нам нужно передать в качестве параметра класса информации значение ProcessBasicInformation. Поскольку размер структуры может меняться в различных версиях Windows я использую кучу для извлечения структуры PROCESS_BASIC_INFORMATION. Если размер не подходит код увеличивает размер памяти для структуры PROCESS_BASIC_INFORMATION и повторяет заново пока структура не будет извлечена:
VB Code:
  1. Function UpdateNewBaseAddress( _
  2.                  ByVal pBase As Long) As ERROR_MESSAGES
  3.     Dim pPBI    As Long:                        Dim PBIlen  As Long
  4.     Dim PBI     As PROCESS_BASIC_INFORMATION:   Dim cPEB    As PEB
  5.     Dim ntstat  As Long
  6.     Dim ldrData As PEB_LDR_DATA
  7.     Dim ldrMod  As LDR_MODULE
  8.     
  9.     ntstat = tNtQueryInformationProcess(tGetCurrentProcess(), ProcessBasicInformation, IntPtr(PBI.ExitStatus), Len(PBI), PBIlen)
  10.     
  11.     Do While ntstat = STATUS_INFO_LENGTH_MISMATCH
  12.         
  13.         PBIlen = PBIlen * 2
  14.         
  15.         If pPBI Then
  16.             tHeapFree tGetProcessHeap(), HEAP_NO_SERIALIZE, pPBI
  17.         End If
  18.         
  19.         pPBI = tHeapAlloc(tGetProcessHeap(), HEAP_NO_SERIALIZE, PBIlen)
  20.         ntstat = tNtQueryInformationProcess(tGetCurrentProcess(), ProcessBasicInformation, pPBI, PBIlen, PBIlen)
  21.         
  22.     Loop
  23.     
  24.     If ntstat <> STATUS_SUCCESS Then
  25.         UpdateNewBaseAddress = EM_PROCESS_INFORMATION_NOT_FOUND
  26.         GoTo CleanUp
  27.     End If
  28.     
  29.     If pPBI Then
  30.         ' // Copy to PROCESS_BASIC_INFORMATION
  31.         tCopyMemory IntPtr(PBI.ExitStatus), pPBI, Len(PBI)
  32.     End If
  33.  
  34.     ' // Get PEB
  35.     tCopyMemory IntPtr(cPEB.NotUsed), PBI.PebBaseAddress, Len(cPEB)
  36.     
  37.     ' // Modify image base
  38.     cPEB.ImageBaseAddress = pBase
  39.     
  40.     ' // Restore PEB
  41.     tCopyMemory PBI.PebBaseAddress, IntPtr(cPEB.NotUsed), Len(cPEB)
  42.     
  43.     ' // Fix base address in PEB_LDR_DATA list
  44.     tCopyMemory IntPtr(ldrData.Length), cPEB.LoaderData, Len(ldrData)
  45.     
  46.     ' // Get first element
  47.     tCopyMemory IntPtr(ldrMod.InLoadOrderModuleList.Flink), ldrData.InLoadOrderModuleList.Flink, Len(ldrMod)
  48.     
  49.     ' // Fix base
  50.     ldrMod.BaseAddress = pBase
  51.     
  52.     ' // Restore
  53.     tCopyMemory ldrData.InLoadOrderModuleList.Flink, IntPtr(ldrMod.InLoadOrderModuleList.Flink), Len(ldrMod)
  54.     
  55. CleanUp:
  56.     
  57.     ' // Free memory
  58.     If pPBI Then
  59.         tHeapFree tGetProcessHeap(), HEAP_NO_SERIALIZE, pPBI
  60.     End If
  61.     
  62. End Function

После обновления базового адреса в системных структурах шеллкод вызывает функцию ProcessImportTable которая загружает необходимые библиотеки для работы EXE файла. Вначале извлекается директория IMAGE_DIRECTORY_ENTRY_IMPORT которая содержит RVA массива структур IMAGE_IMPORT_DESCRIPTOR:
VB Code:
  1. Type IMAGE_IMPORT_DESCRIPTOR
  2.     Characteristics                 As Long
  3.     TimeDateStamp                   As Long
  4.     ForwarderChain                  As Long
  5.     pName                           As Long
  6.     FirstThunk                      As Long
  7. End Type

Каждая такая структура описывает одну DLL. Поле pName содержит RVA ASCIIZ строки с именем библиотеки. Поле Characteristics содержит RVA таблицы импортируемых функций, а поле FirstThunk содержит RVA таблицы адресов импорта (IAT). Таблица имен представляет из себя массив структур IMAGE_THUNK_DATA. Эта структура представляет из себя 32 битное значение в котором если установлен старший бит остальные биты представляют из себя ординал функции (импорт по ординалу), иначе остальные биты содержат RVA имени функции предваренной значением Hint. Если же структура IMAGE_THUNK_DATA содержит 0 то значит список имен закончен. Если все поля структуры IMAGE_IMPORT_DESCRIPTOR равны 0 это означает что список структур также окончен.
VB Code:
  1. ' // Process import table
  2. Function ProcessImportTable( _
  3.                  ByVal pBase As Long) As ERROR_MESSAGES
  4.     Dim NtHdr           As IMAGE_NT_HEADERS:        Dim datDirectory    As IMAGE_DATA_DIRECTORY
  5.     Dim dsc             As IMAGE_IMPORT_DESCRIPTOR: Dim hLib            As Long
  6.     Dim thnk            As Long:                    Dim Addr            As Long
  7.     Dim fnc             As Long:                    Dim pData           As Long
  8.         
  9.     If GetImageNtHeaders(pBase, NtHdr) = 0 Then
  10.         ProcessImportTable = EM_UNABLE_TO_GET_NT_HEADERS
  11.         Exit Function
  12.     End If
  13.     
  14.     ' // Import table processing
  15.     If NtHdr.OptionalHeader.NumberOfRvaAndSizes > 1 Then
  16.         
  17.         If GetDataDirectory(pBase, IMAGE_DIRECTORY_ENTRY_IMPORT, datDirectory) = 0 Then
  18.             ProcessImportTable = EM_INVALID_DATA_DIRECTORY
  19.             Exit Function
  20.         End If
  21.  
  22.         ' // If import table exists
  23.         If datDirectory.Size > 0 And datDirectory.VirtualAddress > 0 Then
  24.         
  25.             ' // Copy import descriptor
  26.             pData = datDirectory.VirtualAddress + pBase
  27.             tCopyMemory IntPtr(dsc.Characteristics), pData, Len(dsc)
  28.             
  29.             ' // Go thru all descriptors
  30.             Do Until dsc.Characteristics = 0 And _
  31.                      dsc.FirstThunk = 0 And _
  32.                      dsc.ForwarderChain = 0 And _
  33.                      dsc.pName = 0 And _
  34.                      dsc.TimeDateStamp = 0
  35.                 
  36.                 If dsc.pName > 0 Then
  37.                 
  38.                     ' // Load needed library
  39.                     hLib = tLoadLibrary(dsc.pName + pBase)
  40.                     
  41.                     If hLib = 0 Then
  42.                         ProcessImportTable = EM_LOADLIBRARY_FAILED
  43.                         Exit Function
  44.                     End If
  45.  
  46.                     If dsc.Characteristics Then fnc = dsc.Characteristics + pBase Else fnc = dsc.FirstThunk + pBase
  47.                     
  48.                     ' // Go to names table
  49.                     tCopyMemory IntPtr(thnk), fnc, 4
  50.                     
  51.                     ' // Go thru names table
  52.                     Do While thnk
  53.                     
  54.                         ' // Check import type
  55.                         If thnk < 0 Then
  56.                             ' // By ordinal
  57.                             Addr = tGetProcAddress(hLib, thnk And &HFFFF&)
  58.                         Else
  59.                             ' // By name
  60.                             Addr = tGetProcAddress(hLib, thnk + 2 + pBase)
  61.                         End If
  62.                         
  63.                         ' // Next function
  64.                         fnc = fnc + 4
  65.                         tCopyMemory IntPtr(thnk), fnc, 4
  66.                         tCopyMemory dsc.FirstThunk + pBase, IntPtr(Addr), 4
  67.                         dsc.FirstThunk = dsc.FirstThunk + 4
  68.                         
  69.                     Loop
  70.                     
  71.                 End If
  72.                 
  73.                 ' // Next descriptor
  74.                 pData = pData + Len(dsc)
  75.                 tCopyMemory IntPtr(dsc.Characteristics), pData, Len(dsc)
  76.                 
  77.             Loop
  78.             
  79.         End If
  80.         
  81.     End If
  82.                 
  83. End Function

Функция ProcessRelocation вызывается после обработки импорта. Эта функция настраивает все абсолютные ссылки (если таковые имеются). Извлекается каталог IMAGE_DIRECTORY_ENTRY_BASERELOC который содержит RVA массива структур IMAGE_BASE_RELOCATION. Каждый элемент этого масива содержит настройки в пределах 4Кб относительно адреса VirtualAddress:
VB Code:
  1. Type IMAGE_BASE_RELOCATION
  2.     VirtualAddress                  As Long
  3.     SizeOfBlock                     As Long
  4. End Type

Поле SizeOfBlock содержит размер элемента в байтах. Массив 16-битных значений дескрипторов расположен после каждой структуры IMAGE_BASE_RELOCATION. Мы можем вычислить количество этих значений по формуле: (SizeOfBlock - Len(IMAGE_BASE_RELOCATION)) \ Len(Integer). Каждый элемент массива дескрипторов имеет следующуюю структуру:


Верхние 4 бита содержат тип настройки. Нам интересна только настройка IMAGE_REL_BASED_HIGHLOW которая означает что нам нужно добавить разницу (RealBaseAddress - ImageBaseAddress) к значению Long которое расположено по адресу VirtualAddress + 12 младших бит дескриптора. Массив струкутр IMAGE_BASE_RELOCATION заканчивается структурой где все поля заполнены нулями:
VB Code:
  1. ' // Process relocations
  2. Function ProcessRelocations( _
  3.                  ByVal pBase As Long) As ERROR_MESSAGES
  4.     Dim NtHdr           As IMAGE_NT_HEADERS:        Dim datDirectory    As IMAGE_DATA_DIRECTORY
  5.     Dim relBase         As IMAGE_BASE_RELOCATION:   Dim entriesCount    As Long
  6.     Dim relType         As Long:                    Dim dwAddress       As Long
  7.     Dim dwOrig          As Long:                    Dim pRelBase        As Long
  8.     Dim delta           As Long:                    Dim pData           As Long
  9.     
  10.     ' // Check if module has not been loaded to image base value
  11.     If GetImageNtHeaders(pBase, NtHdr) = 0 Then
  12.         ProcessRelocations = EM_UNABLE_TO_GET_NT_HEADERS
  13.         Exit Function
  14.     End If
  15.     
  16.     delta = pBase - NtHdr.OptionalHeader.ImageBase
  17.     
  18.     ' // Process relocations
  19.     If delta Then
  20.         
  21.         ' // Get address of relocation table
  22.         If GetDataDirectory(pBase, IMAGE_DIRECTORY_ENTRY_BASERELOC, datDirectory) = 0 Then
  23.             ProcessRelocations = EM_INVALID_DATA_DIRECTORY
  24.             Exit Function
  25.         End If
  26.         
  27.         If datDirectory.Size > 0 And datDirectory.VirtualAddress > 0 Then
  28.         
  29.             ' // Copy relocation base
  30.             pRelBase = datDirectory.VirtualAddress + pBase
  31.             tCopyMemory IntPtr(relBase.VirtualAddress), pRelBase, Len(relBase)
  32.             
  33.             Do While relBase.VirtualAddress
  34.             
  35.                 ' // To first reloc chunk
  36.                 pData = pRelBase + Len(relBase)
  37.                 
  38.                 entriesCount = (relBase.SizeOfBlock - Len(relBase)) \ 2
  39.                 
  40.                 Do While entriesCount > 0
  41.                     
  42.                     tCopyMemory IntPtr(relType), pData, 2
  43.                     
  44.                     Select Case (relType \ 4096) And &HF
  45.                     Case IMAGE_REL_BASED_HIGHLOW
  46.                         
  47.                         ' // Calculate address
  48.                         dwAddress = relBase.VirtualAddress + (relType And &HFFF&) + pBase
  49.                         
  50.                         ' // Get original address
  51.                         tCopyMemory IntPtr(dwOrig), dwAddress, Len(dwOrig)
  52.                         
  53.                         ' // Add delta
  54.                         dwOrig = dwOrig + delta
  55.                         
  56.                         ' // Save
  57.                         tCopyMemory dwAddress, IntPtr(dwOrig), Len(dwOrig)
  58.                         
  59.                     End Select
  60.                     
  61.                     pData = pData + 2
  62.                     entriesCount = entriesCount - 1
  63.                     
  64.                 Loop
  65.                 
  66.                 ' // Next relocation base
  67.                 pRelBase = pRelBase + relBase.SizeOfBlock
  68.                 tCopyMemory IntPtr(relBase.VirtualAddress), pRelBase, Len(relBase)
  69.                 
  70.             Loop
  71.             
  72.         End If
  73.         
  74.     End If
  75.  
  76. End Function

После настройки релокаций шеллкод вызывает функцию SetMemoryPermissions которая настраивает разрешения памяти согласно полю Characteristics структуры IMAGE_SECTION_HEADER. Для этого просто вызывается функция VirtualProtect с определенными атрибутами памяти:
VB Code:
  1. ' // Set memory permissions
  2. Private Function SetMemoryPermissions( _
  3.                  ByVal pBase As Long) As ERROR_MESSAGES
  4.     Dim iSec    As Long:                    Dim pNtHdr  As Long
  5.     Dim NtHdr   As IMAGE_NT_HEADERS:        Dim sec     As IMAGE_SECTION_HEADER
  6.     Dim Attr    As MEMPROTECT:              Dim pSec    As Long
  7.     Dim ret     As Long
  8.     
  9.     pNtHdr = GetImageNtHeaders(pBase, NtHdr)
  10.     If pNtHdr = 0 Then
  11.         SetMemoryPermissions = EM_UNABLE_TO_GET_NT_HEADERS
  12.         Exit Function
  13.     End If
  14.  
  15.     ' // Get address of first section header
  16.     pSec = pNtHdr + 4 + Len(NtHdr.FileHeader) + NtHdr.FileHeader.SizeOfOptionalHeader
  17.     
  18.     ' // Go thru section headers
  19.     For iSec = 0 To NtHdr.FileHeader.NumberOfSections - 1
  20.     
  21.         ' // Copy section descriptor
  22.         tCopyMemory IntPtr(sec.SectionName(0)), pSec, Len(sec)
  23.         
  24.         ' // Get type
  25.         If sec.Characteristics And IMAGE_SCN_MEM_EXECUTE Then
  26.             If sec.Characteristics And IMAGE_SCN_MEM_READ Then
  27.                 If sec.Characteristics And IMAGE_SCN_MEM_WRITE Then
  28.                     Attr = PAGE_EXECUTE_READWRITE
  29.                 Else
  30.                     Attr = PAGE_EXECUTE_READ
  31.                 End If
  32.             Else
  33.                 If sec.Characteristics And IMAGE_SCN_MEM_WRITE Then
  34.                     Attr = PAGE_EXECUTE_WRITECOPY
  35.                 Else
  36.                     Attr = PAGE_EXECUTE
  37.                 End If
  38.             End If
  39.         Else
  40.             If sec.Characteristics And IMAGE_SCN_MEM_READ Then
  41.                 If sec.Characteristics And IMAGE_SCN_MEM_WRITE Then
  42.                     Attr = PAGE_READWRITE
  43.                 Else
  44.                     Attr = PAGE_READONLY
  45.                 End If
  46.             Else
  47.                 If sec.Characteristics And IMAGE_SCN_MEM_WRITE Then
  48.                     Attr = PAGE_WRITECOPY
  49.                 Else
  50.                     Attr = PAGE_NOACCESS
  51.                 End If
  52.             End If
  53.         End If
  54.         
  55.         ' // Set memory permissions
  56.         If tVirtualProtect(sec.VirtualAddress + pBase, sec.VirtualSize, Attr, IntPtr(ret)) = 0 Then
  57.             SetMemoryPermissions = EM_UNABLE_TO_PROTECT_MEMORY
  58.             Exit Function
  59.         End If
  60.         
  61.         ' // Next section
  62.         pSec = pSec + Len(sec)
  63.         
  64.     Next
  65.     
  66. End Function

В конце концов очищается таблица сообщений об ошибках (если нужно) и вызывается точка входа загруженного EXE. В предыдущей версии загрузчика я выгружал шеллкод тоже, но некоторые EXE не вызывают ExitProcess следовательно это могло вызывать креши. Загрузчик готов.
Хотя мы написал загрузчик без использвания рантайма, компилятор VB6 добавляет его все-равно поскольку все OBJ файлы имеют ссылки на MSVBVM60 во время компиляции. Нам придется удалить рантайм из таблицы импорта загрузчика вручную. Для этого я сделал специальную утилиту - Patcher которая ищет рантайм в таблице импорта и таблице связанного импорта и удаляет его оттуда. Эта утилита также была полезна для драйвера режима ядра. Я не буду описывать ее работу поскольку она использует те же концепции PE-формата что я уже описал здесь. В общем и целом мы сделали рабочий EXE который не использует MSVBVM60 на целевой машине.
Для того чтобы использовать загрузчик нужно скомпилировать его затем с помощью патчера пропатчить его. После этог можно использовать его в компиляторе.

Я надеюсь вам понравилось. Спасибо за внимание!
С уважением,
Кривоус Анатолий (The trick).
Миниатюры Вложения

Комментарии