Первая часть ->.
После извлечения файлов вызывается функция ExecuteProcess которая запускает выполнение команд используя функцию ShellExecuteEx:
| Visual Basic | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
| ' // Execution command process
Function ExecuteProcess() As Boolean
Dim index As Long: Dim bItem As BinExecListItem
Dim pPath As Long: Dim pErrMsg As Long
Dim shInfo As SHELLEXECUTEINFO: Dim pTempString As Long
Dim pItem As Long: Dim status As Long
' // Set pointer and size
shInfo.cbSize = Len(shInfo)
pItem = pExecutesTable
' // Go thru all items
For index = 0 To ProjectDesc.execListDescriptor.dwNumberOfItems - 1
' // Copy item
CopyMemory bItem, ByVal pItem, ProjectDesc.execListDescriptor.dwSizeOfItem
' // Set pointer to next item
pItem = pItem + ProjectDesc.execListDescriptor.dwSizeOfItem
' // Normalize path
pPath = NormalizePath(pStringsTable + bItem.ofstFileName, 0)
' // Fill SHELLEXECUTEINFO
shInfo.lpFile = pPath
shInfo.lpParameters = pStringsTable + bItem.ofstParameters
shInfo.fMask = SEE_MASK_NOCLOSEPROCESS Or SEE_MASK_FLAG_NO_UI
shInfo.nShow = SW_SHOWDEFAULT
' // Performing...
status = ShellExecuteEx(shInfo)
' // If error occurs show notification (retry, abort, ignore)
Do Until status
If pErrMsg Then SysFreeString pErrMsg: pErrMsg = 0
' // Ignore error
If bItem.dwFlags And EF_IGNOREERROR Then
Exit Do
End If
pTempString = GetString(MID_ERROREXECUTELINE)
pErrMsg = StrCat(pTempString, pPath)
SysFreeString pTempString: pTempString = 0
Select Case MessageBox(0, pErrMsg, 0, MB_ICONERROR Or MB_SYSTEMMODAL Or MB_CANCELTRYCONTINUE)
Case MESSAGEBOXRETURN.IDCONTINUE: Exit Do
Case MESSAGEBOXRETURN.IDTRYAGAIN
Case Else: GoTo CleanUp
End Select
status = ShellExecuteEx(shInfo)
Loop
' // Wait for process terminaton
WaitForSingleObject shInfo.hProcess, INFINITE
CloseHandle shInfo.hProcess
Next
' // Success
ExecuteProcess = True
CleanUp:
If pTempString Then SysFreeString pTempString
If pErrMsg Then SysFreeString pErrMsg
If pPath Then SysFreeString pPath
End Function |
|
Эта функция похожа на предыдущую за исключением того что здесь используется функция ShellExecuteEx вместо извлечения. Обратите внимание что каждая операция выполняется синхронно, т.е. каждый вызов процедуры ShellExecuteEx ждет окончания выполнения команды.
Если предыдущая функция выполнилась успешно тогда вызывается функция RunProcess которая подготовливает данные для исполнения главного исполняемого файла из памяти:
| Visual Basic | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| ' // Run exe from project in memory
Function RunProcess() As Boolean
Dim bItem As BinStorageListItem: Dim Length As Long
Dim pFileData As Long
' // Get descriptor of executable file
CopyMemory bItem, ByVal pStoragesTable + ProjectDesc.storageDescriptor.dwSizeOfItem * _
ProjectDesc.storageDescriptor.iExecutableIndex, Len(bItem)
' // Alloc memory within top memory addresses
pFileData = VirtualAlloc(ByVal 0&, bItem.dwSizeOfFile, MEM_TOP_DOWN Or MEM_COMMIT, PAGE_READWRITE)
If pFileData = 0 Then Exit Function
' // Copy raw exe file to this memory
CopyMemory ByVal pFileData, ByVal pFilesTable + bItem.ofstBeginOfData, bItem.dwSizeOfFile
' // Free decompressed project data
HeapFree GetProcessHeap(), HEAP_NO_SERIALIZE, pProjectData
pProjectData = 0
' // Run exe from memory
RunExeFromMemory pFileData, bItem.dwFlags And FF_IGNOREERROR
' ----------------------------------------------------
' // An error occurs
' // Clean memory
VirtualFree ByVal pFileData, 0, MEM_RELEASE
' // If ignore error then success
If bItem.dwFlags And FF_IGNOREERROR Then RunProcess = True
End Function |
|
Эта процедура выделяет память в верхних областях виртуального адресного пространства (поскольку большинство EXE файлов грузятся по довольно низким адресам (обычно 0x00400000). После этого очишается память данных проекта поскольку если EXE файл запустится, то эта память не будет освобождена, затем вызывается функция RunExeFromMemory которая делает следующий шаг в загрузке EXE из памяти. Если по какой-либо причине загрузка EXE файла не состоялась то освобождается выделенная память и управление передается функции Main. Итак, для того чтобы загрузить EXE файл нам нужно освободить память загрузчика, т.е. выгрузить загрузчик. Нам нужно только оставить маленькуий кусочек кода который будет загружать EXE файл и запускать его. Для этого я решил использовать шеллкод, хотя можно использовать и DLL. Шеллкод - это маленький базонезависимый код (код который не ссылается к внешним данным). Но в любом случае нам придется обеспечить доступ к API функциям из шеллкода. Мы не можем вызывать API функции непосредственно из шеллкода поскольку наш главный исполняемый файл будет выгружен и любое обращение к таблице импорта вызовет креш. Второе ограничение - это то что инструкция call использует относительное смещение (это наиболее частый случай). Из этого следует что нам нужно инициализировать некие "трамплины" которые будут перебрасывать нас на API функции. Я решил делать это посредством сплайсинга. Я просто заменяю первые 5 байт функции пусттышки на ассемблерную инструкцию jmp которая ссылается на необходимую API функцию:
| Visual Basic | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
| ' // Run EXE file by memory address
Function RunExeFromMemory( _
ByVal pExeData As Long, _
ByVal IgnoreError As Boolean) As Boolean
Dim Length As Long: Dim pCode As Long
Dim pszMsg As Long: Dim pMsgTable As Long
Dim index As Long: Dim pCurMsg As Long
' // Get size of shellcode
Length = GetAddr(AddressOf ENDSHELLLOADER) - GetAddr(AddressOf BEGINSHELLLOADER)
' // Alloc memory within top addresses
pCode = VirtualAlloc(ByVal 0&, Length, MEM_TOP_DOWN Or MEM_COMMIT, PAGE_EXECUTE_READWRITE)
' // Copy shellcode to allocated memory
CopyMemory ByVal pCode, ByVal GetAddr(AddressOf BEGINSHELLLOADER), Length
' // Initialization of shellcode
If Not InitShellLoader(pCode) Then GoTo CleanUp
' // Splice CallLoader function in order to call shellcode
Splice AddressOf CallLoader, pCode + GetAddr(AddressOf LoadExeFromMemory) - GetAddr(AddressOf BEGINSHELLLOADER)
' // Check ignore errors
If Not IgnoreError Then
' // Alloc memory for messages table
pMsgTable = VirtualAlloc(ByVal 0&, 1024, MEM_TOP_DOWN Or MEM_COMMIT, PAGE_READWRITE)
If pMsgTable = 0 Then GoTo CleanUp
' // Skip pointers
pCurMsg = pMsgTable + EM_END * 4
For index = 0 To EM_END - 1
' // Load message string
pszMsg = GetString(MSG_LOADER_ERROR + index)
If pszMsg = 0 Then GoTo CleanUp
Length = SysStringLen(pszMsg)
lstrcpyn ByVal pCurMsg, ByVal pszMsg, Length + 1
' // Store pointer
CopyMemory ByVal pMsgTable + index * 4, pCurMsg, Len(pCurMsg)
' // Next message offset
pCurMsg = pCurMsg + (Length + 1) * 2
SysFreeString pszMsg
Next
End If
' // Call shellcode
CallLoader pExeData, pCode, pMsgTable
CleanUp:
If pMsgTable Then
VirtualFree ByVal pMsgTable, 0, MEM_RELEASE
End If
If pCode Then
VirtualFree ByVal pCode, 0, MEM_RELEASE
End If
End Function |
|
Как видно из кода он вычисляет размер шеллкода используя разницу между крайними функциями - ENDSHELLLOADER и BEGINSHELLLOADER. Эти функции должны окружать наш шеллкод и иметь разный прототип поскольку VB6 компилятор может объединять идентичные функции. Затем выделяется память для самого шеллкода и он копируется в эту область памяти. После этого вызывается функция InitShellLoader которая сплайсит все функции в шеллкоде:
| Visual Basic | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
| ' // Shellcode initialization
Function InitShellLoader( _
ByVal pShellCode As Long) As Boolean
Dim hLib As Long: Dim sName As Long
Dim sFunc As Long: Dim lpAddr As Long
Dim libIdx As Long: Dim fncIdx As Long
Dim libName As MessagesID: Dim fncName As MessagesID
Dim fncSpc As Long: Dim splAddr As Long
' // +----------------------------------------------------------------+
' // | Fixing of API addresses |
' // +----------------------------------------------------------------+
' // | In order to call api function from shellcode i use splicing of |
' // | our VB functions and redirect call to corresponding api. |
' // | I did same in the code that injects to other process. |
' // +----------------------------------------------------------------+
splAddr = GetAddr(AddressOf tVirtualAlloc) - GetAddr(AddressOf BEGINSHELLLOADER) + pShellCode
' // Get size in bytes between stub functions
fncSpc = GetAddr(AddressOf tVirtualProtect) - GetAddr(AddressOf tVirtualAlloc)
' // Use 3 library: kernel32, ntdll и user32
For libIdx = 0 To 2
' // Get number of imported functions depending on library
Select Case libIdx
Case 0: libName = API_LIB_KERNEL32: fncIdx = 13
Case 1: libName = API_LIB_NTDLL: fncIdx = 1
Case 2: libName = API_LIB_USER32: fncIdx = 1
End Select
' // Get library name from resources
sName = GetString(libName): If sName = 0 Then Exit Function
' // Get module handle
hLib = GetModuleHandle(ByVal sName): If hLib = 0 Then Exit Function
SysFreeString sName
' // Go thru functions
Do While fncIdx
libName = libName + 1
' // Get function name
sName = GetString(libName): If sName = 0 Then Exit Function
' // Because of GetProcAddress works with ANSI string translate it to ANSI
sFunc = ToAnsi(sName): If sFunc = 0 Then Exit Function
' // Get function address
lpAddr = GetProcAddress(hLib, sFunc)
SysFreeString sName: SysFreeString sFunc
' // Error
If lpAddr = 0 Then Exit Function
' // Splice stub
Splice splAddr, lpAddr
' // Next stub
splAddr = splAddr + fncSpc
fncIdx = fncIdx - 1
Loop
Next
' // Modify CallByPointer
lpAddr = GetAddr(AddressOf CallByPointer) - GetAddr(AddressOf BEGINSHELLLOADER) + pShellCode
' // pop eax - 0x58
' // pop ecx - 0x59
' // push eax - 0x50
' // jmp ecx - 0xFFE1
CopyMemory ByVal lpAddr, &HFF505958, 4
CopyMemory ByVal lpAddr + 4, &HE1, 1
' // Success
InitShellLoader = True
End Function
' // Splice function
Sub Splice( _
ByVal Func As Long, _
ByVal NewAddr As Long)
' // Set memory permissions
VirtualProtect ByVal Func, 5, PAGE_EXECUTE_READWRITE, 0
CopyMemory ByVal Func, &HE9, 1 ' // JMP
CopyMemory ByVal Func + 1, NewAddr - Func - 5, 4 ' // Relative address
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 функции, теперь мы должны использовать наши "трамплины":
| Visual Basic | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
| ' // Parse exe in memory
Function LoadExeFromMemory( _
ByVal pRawData As Long, _
ByVal pMyBaseAddress As Long, _
ByVal pErrMsgTable As Long) As Boolean
Dim NtHdr As IMAGE_NT_HEADERS
Dim pBase As Long
Dim index As Long
Dim iError As ERROR_MESSAGES
Dim pszMsg As Long
' // Get IMAGE_NT_HEADERS
If GetImageNtHeaders(pRawData, NtHdr) = 0 Then
iError = EM_UNABLE_TO_GET_NT_HEADERS
EndProcess pErrMsgTable, iError
Exit Function
End If
' // Check flags
If NtHdr.FileHeader.Machine <> IMAGE_FILE_MACHINE_I386 Or _
(NtHdr.FileHeader.Characteristics And IMAGE_FILE_EXECUTABLE_IMAGE) = 0 Or _
(NtHdr.FileHeader.Characteristics And IMAGE_FILE_32BIT_MACHINE) = 0 Then Exit Function
' // Release main EXE memory. After that main exe is unloaded from memory.
ZwUnmapViewOfSection GetCurrentProcess(), GetModuleHandle(ByVal 0&)
' // Reserve memory for EXE
iError = ReserveMemory(pRawData, pBase)
If iError Then
EndProcess pErrMsgTable, iError
Exit Function
End If
' // Place data
iError = ProcessSectionsAndHeaders(pRawData, pBase)
If iError Then
EndProcess pErrMsgTable, iError
Exit Function
End If
' // Update new base address
iError = UpdateNewBaseAddress(pBase)
If iError Then
EndProcess pErrMsgTable, iError
Exit Function
End If
' // Import table processing
iError = ProcessImportTable(pBase)
If iError Then
EndProcess pErrMsgTable, iError
Exit Function
End If
' // Relocations processing
iError = ProcessRelocations(pBase)
If iError Then
EndProcess pErrMsgTable, iError
Exit Function
End If
' // Set the memory attributes
iError = SetMemoryPermissions(pBase)
If iError Then
EndProcess pErrMsgTable, iError
Exit Function
End If
' // Release error message table
If pErrMsgTable Then
tVirtualFree pErrMsgTable, 0, MEM_RELEASE
End If
' // Call entry point
CallByPointer NtHdr.OptionalHeader.AddressOfEntryPoint + pBase
' // End process
EndProcess
End Function |
|
Затем шеллкод вызывает функцию ReserveMemory показанную ниже. Эта функция извлекает заголовок NT из загружаемого EXE и пытается зарезервировать регион памяти по адресу указанному в поле ImageBase размера SizeOfmage. Если регион по какой-то причине не был выделен функция проверяет имеет ли EXE файл таблицу релокаций. Если так, тогда функция пытается выделять память по любому адресу. Информация о релокациях позволяет загрузить EXE по любому адресу отличному от ImageBase. Она содержит все места в EXE файле где он использует абсолютную адресацию. Мы можем потом подкорректировать эти адреса используя разницу между реальным базовым адресом и адресом указанным в поле ImageBase:
| Visual Basic | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
| ' // Reserve memory for EXE
Function ReserveMemory( _
ByVal pRawExeData As Long, _
ByRef pBase As Long) As ERROR_MESSAGES
Dim NtHdr As IMAGE_NT_HEADERS
Dim pLocBase As Long
If GetImageNtHeaders(pRawExeData, NtHdr) = 0 Then
ReserveMemory = EM_UNABLE_TO_GET_NT_HEADERS
Exit Function
End If
' // Reserve memory for EXE
pLocBase = tVirtualAlloc(ByVal NtHdr.OptionalHeader.ImageBase, _
NtHdr.OptionalHeader.SizeOfImage, _
MEM_RESERVE, PAGE_EXECUTE_READWRITE)
If pLocBase = 0 Then
' // If relocation information not found error
If NtHdr.FileHeader.Characteristics And IMAGE_FILE_RELOCS_STRIPPED Then
ReserveMemory = EM_UNABLE_TO_ALLOCATE_MEMORY
Exit Function
Else
' // Reserve memory in other region
pLocBase = tVirtualAlloc(ByVal 0&, NtHdr.OptionalHeader.SizeOfImage, _
MEM_RESERVE, PAGE_EXECUTE_READWRITE)
If pLocBase = 0 Then
ReserveMemory = EM_UNABLE_TO_ALLOCATE_MEMORY
Exit Function
End If
End If
End If
pBase = pLocBase
End Function |
|
Если при вызове функции произошла ошибка то показывается сообщение о ней и приложение завершается. В противном случае вызывается функция ProcessSectionsAndHeaders. Эта функция размещает все заголовки в выделенную память, извлекает информацию о всех секциях и копирует все данные в выделенную для них память. Если какая-либо секция имеет неинициализированные данные то этот регион заполняется нулями:
| Visual Basic | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| ' // Allocate memory for sections and copy them data to there
Function ProcessSectionsAndHeaders( _
ByVal pRawExeData As Long, _
ByVal pBase As Long) As ERROR_MESSAGES
Dim iSec As Long
Dim pNtHdr As Long
Dim NtHdr As IMAGE_NT_HEADERS
Dim sec As IMAGE_SECTION_HEADER
Dim lpSec As Long
Dim pData As Long
pNtHdr = GetImageNtHeaders(pRawExeData, NtHdr)
If pNtHdr = 0 Then
ProcessSectionsAndHeaders = EM_UNABLE_TO_GET_NT_HEADERS
Exit Function
End If
' // Alloc memory for headers
pData = tVirtualAlloc(ByVal pBase, NtHdr.OptionalHeader.SizeOfHeaders, MEM_COMMIT, PAGE_READWRITE)
If pData = 0 Then
ProcessSectionsAndHeaders = EM_UNABLE_TO_ALLOCATE_MEMORY
Exit Function
End If
' // Copy headers
tCopyMemory pData, pRawExeData, NtHdr.OptionalHeader.SizeOfHeaders
' // Get address of beginnig of sections headers
pData = pNtHdr + Len(NtHdr.Signature) + Len(NtHdr.FileHeader) + NtHdr.FileHeader.SizeOfOptionalHeader
' // Go thru sections
For iSec = 0 To NtHdr.FileHeader.NumberOfSections - 1
' // Copy section descriptor
tCopyMemory IntPtr(sec.SectionName(0)), pData, Len(sec)
' // Alloc memory for section
lpSec = tVirtualAlloc(sec.VirtualAddress + pBase, sec.VirtualSize, MEM_COMMIT, PAGE_READWRITE)
If lpSec = 0 Then
ProcessSectionsAndHeaders = EM_UNABLE_TO_ALLOCATE_MEMORY
Exit Function
End If
' If there is initialized data
If sec.SizeOfRawData Then
' // Take into account file alignment
If sec.SizeOfRawData > sec.VirtualSize Then sec.SizeOfRawData = sec.VirtualSize
' // Copy initialized data to section
tCopyMemory lpSec, pRawExeData + sec.PointerToRawData, sec.SizeOfRawData
lpSec = lpSec + sec.SizeOfRawData
sec.VirtualSize = sec.VirtualSize - sec.SizeOfRawData
End If
' // Fill remain part with zero
tFillMemory lpSec, sec.VirtualSize, 0
' // Next section
pData = pData + Len(sec)
Next
End Function |
|
Затем функция LoadExeFromMemory вызывает функцию UpdateNewBaseAddress которая обновляет новый базовый адрес в user-mode системных структурах. Windows создает специальную структуру называемую PEB (Process Environment Block) для каждого процесса. Это очень полезная структура которая позволяет получить очень много информации о процессе. Множество API функций берут информацию из этой структуры. Для примера GetModuleHandle(NULL) берет возвращаемое значение из PEB.ImageBaseAddress или GetModuleHandle("MyExeName") извлекает информацию из списка загруженных модулей - PEB.Ldr. Нам нужно обновить эту информацию согласно новому базовому адресу для того чтобы API функции возвращали корректное значение. Вот небольшая часть структуры PEB:
| Visual Basic | 1
2
3
4
5
6
7
8
| Type PEB
NotUsed As Long
Mutant As Long
ImageBaseAddress As Long
LoaderData As Long ' // Pointer to PEB_LDR_DATA
ProcessParameters As Long
' // ....
End Type |
|
Нам интересно только поле ImageBaseAddress и LoaderData. Первое поле содержит базовый адрес EXE файла. Второе поле содержит указатель на структуру PEB_LDR_DATA которая описывает все загруженные модули в процессе:
| Visual Basic | 1
2
3
4
5
6
7
8
| Type PEB_LDR_DATA
Length As Long
Initialized As Long
SsHandle As Long
InLoadOrderModuleList As LIST_ENTRY
InMemoryOrderModuleList As LIST_ENTRY
InInitializationOrderModuleList As LIST_ENTRY
End Type |
|
Эта структура содержит три двухсвязных списка что описывают каждый модуль. Список InLoadOrderModuleList содержит ссылки на элементы в порядке загрузки, т.е. ссылки в этом списке расположены в порядке загрузки (первый модуль в начале). Список InMemoryOrderModuleList тоже самое только в порядке расположения в памяти, а InInitializationOrderModuleList в порядке инициализации. Нам нужно получить первый элемент списка InLoadOrderModuleList который является указателем на структуру LDR_MODULE:
| Visual Basic | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| Type LDR_MODULE
InLoadOrderModuleList As LIST_ENTRY
InMemoryOrderModuleList As LIST_ENTRY
InInitOrderModuleList As LIST_ENTRY
BaseAddress As Long
EntryPoint As Long
SizeOfImage As Long
FullDllName As UNICODE_STRING
BaseDllName As UNICODE_STRING
Flags As Long
LoadCount As Integer
TlsIndex As Integer
HashTableEntry As LIST_ENTRY
TimeDateStamp As Long
End Type |
|
Эта структура описывает один модуль. Первый элемент списка InLoadOrderModuleList является описателем главного исполняемого файла. Нам нужно изменить поле BaseAddress на новый базовый адрес и сохранить изменения. Итак, для того чтобы получить адрес структуры PEB мы можем использовать функцию NtQueryInformationProcess которая извлекает множество полезной информации о процессе (узнать подробнее можно в книге 'Windows NT/2000 Native API Reference' by Gary Nebbett). Структура PEB может быть получена из структуры PROCESS_BASIC_INFORMATION которая описывает базовую информацию о процессе:
| Visual Basic | 1
2
3
4
5
6
7
8
| Type PROCESS_BASIC_INFORMATION
ExitStatus As Long
PebBaseAddress As Long
AffinityMask As Long
BasePriority As Long
UniqueProcessId As Long
InheritedFromUniqueProcessId As Long
End Type |
|
Поле PebBaseAddress содержит адрес структуры PEB.
Для того чтобы извлечь структуру PROCESS_BASIC_INFORMATION нам нужно передать в качестве параметра класса информации значение ProcessBasicInformation. Поскольку размер структуры может меняться в различных версиях Windows я использую кучу для извлечения структуры PROCESS_BASIC_INFORMATION. Если размер не подходит код увеличивает размер памяти для структуры PROCESS_BASIC_INFORMATION и повторяет заново пока структура не будет извлечена:
| Visual Basic | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
| Function UpdateNewBaseAddress( _
ByVal pBase As Long) As ERROR_MESSAGES
Dim pPBI As Long: Dim PBIlen As Long
Dim PBI As PROCESS_BASIC_INFORMATION: Dim cPEB As PEB
Dim ntstat As Long
Dim ldrData As PEB_LDR_DATA
Dim ldrMod As LDR_MODULE
ntstat = tNtQueryInformationProcess(tGetCurrentProcess(), ProcessBasicInformation, IntPtr(PBI.ExitStatus), Len(PBI), PBIlen)
Do While ntstat = STATUS_INFO_LENGTH_MISMATCH
PBIlen = PBIlen * 2
If pPBI Then
tHeapFree tGetProcessHeap(), HEAP_NO_SERIALIZE, pPBI
End If
pPBI = tHeapAlloc(tGetProcessHeap(), HEAP_NO_SERIALIZE, PBIlen)
ntstat = tNtQueryInformationProcess(tGetCurrentProcess(), ProcessBasicInformation, pPBI, PBIlen, PBIlen)
Loop
If ntstat <> STATUS_SUCCESS Then
UpdateNewBaseAddress = EM_PROCESS_INFORMATION_NOT_FOUND
GoTo CleanUp
End If
If pPBI Then
' // Copy to PROCESS_BASIC_INFORMATION
tCopyMemory IntPtr(PBI.ExitStatus), pPBI, Len(PBI)
End If
' // Get PEB
tCopyMemory IntPtr(cPEB.NotUsed), PBI.PebBaseAddress, Len(cPEB)
' // Modify image base
cPEB.ImageBaseAddress = pBase
' // Restore PEB
tCopyMemory PBI.PebBaseAddress, IntPtr(cPEB.NotUsed), Len(cPEB)
' // Fix base address in PEB_LDR_DATA list
tCopyMemory IntPtr(ldrData.Length), cPEB.LoaderData, Len(ldrData)
' // Get first element
tCopyMemory IntPtr(ldrMod.InLoadOrderModuleList.Flink), ldrData.InLoadOrderModuleList.Flink, Len(ldrMod)
' // Fix base
ldrMod.BaseAddress = pBase
' // Restore
tCopyMemory ldrData.InLoadOrderModuleList.Flink, IntPtr(ldrMod.InLoadOrderModuleList.Flink), Len(ldrMod)
CleanUp:
' // Free memory
If pPBI Then
tHeapFree tGetProcessHeap(), HEAP_NO_SERIALIZE, pPBI
End If
End Function |
|
После обновления базового адреса в системных структурах шеллкод вызывает функцию ProcessImportTable которая загружает необходимые библиотеки для работы EXE файла. Вначале извлекается директория IMAGE_DIRECTORY_ENTRY_IMPORT которая содержит RVA массива структур IMAGE_IMPORT_DESCRIPTOR:
| Visual Basic | 1
2
3
4
5
6
7
| Type IMAGE_IMPORT_DESCRIPTOR
Characteristics As Long
TimeDateStamp As Long
ForwarderChain As Long
pName As Long
FirstThunk As Long
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 это означает что список структур также окончен.
| Visual Basic | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
| ' // Process import table
Function ProcessImportTable( _
ByVal pBase As Long) As ERROR_MESSAGES
Dim NtHdr As IMAGE_NT_HEADERS: Dim datDirectory As IMAGE_DATA_DIRECTORY
Dim dsc As IMAGE_IMPORT_DESCRIPTOR: Dim hLib As Long
Dim thnk As Long: Dim Addr As Long
Dim fnc As Long: Dim pData As Long
If GetImageNtHeaders(pBase, NtHdr) = 0 Then
ProcessImportTable = EM_UNABLE_TO_GET_NT_HEADERS
Exit Function
End If
' // Import table processing
If NtHdr.OptionalHeader.NumberOfRvaAndSizes > 1 Then
If GetDataDirectory(pBase, IMAGE_DIRECTORY_ENTRY_IMPORT, datDirectory) = 0 Then
ProcessImportTable = EM_INVALID_DATA_DIRECTORY
Exit Function
End If
' // If import table exists
If datDirectory.Size > 0 And datDirectory.VirtualAddress > 0 Then
' // Copy import descriptor
pData = datDirectory.VirtualAddress + pBase
tCopyMemory IntPtr(dsc.Characteristics), pData, Len(dsc)
' // Go thru all descriptors
Do Until dsc.Characteristics = 0 And _
dsc.FirstThunk = 0 And _
dsc.ForwarderChain = 0 And _
dsc.pName = 0 And _
dsc.TimeDateStamp = 0
If dsc.pName > 0 Then
' // Load needed library
hLib = tLoadLibrary(dsc.pName + pBase)
If hLib = 0 Then
ProcessImportTable = EM_LOADLIBRARY_FAILED
Exit Function
End If
If dsc.Characteristics Then fnc = dsc.Characteristics + pBase Else fnc = dsc.FirstThunk + pBase
' // Go to names table
tCopyMemory IntPtr(thnk), fnc, 4
' // Go thru names table
Do While thnk
' // Check import type
If thnk < 0 Then
' // By ordinal
Addr = tGetProcAddress(hLib, thnk And &HFFFF&)
Else
' // By name
Addr = tGetProcAddress(hLib, thnk + 2 + pBase)
End If
' // Next function
fnc = fnc + 4
tCopyMemory IntPtr(thnk), fnc, 4
tCopyMemory dsc.FirstThunk + pBase, IntPtr(Addr), 4
dsc.FirstThunk = dsc.FirstThunk + 4
Loop
End If
' // Next descriptor
pData = pData + Len(dsc)
tCopyMemory IntPtr(dsc.Characteristics), pData, Len(dsc)
Loop
End If
End If
End Function |
|
Функция ProcessRelocation вызывается после обработки импорта. Эта функция настраивает все абсолютные ссылки (если таковые имеются). Извлекается каталог IMAGE_DIRECTORY_ENTRY_BASERELOC который содержит RVA массива структур IMAGE_BASE_RELOCATION. Каждый элемент этого масива содержит настройки в пределах 4Кб относительно адреса VirtualAddress:
| Visual Basic | 1
2
3
4
| Type IMAGE_BASE_RELOCATION
VirtualAddress As Long
SizeOfBlock As Long
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 заканчивается структурой где все поля заполнены нулями:
| Visual Basic | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
| ' // Process relocations
Function ProcessRelocations( _
ByVal pBase As Long) As ERROR_MESSAGES
Dim NtHdr As IMAGE_NT_HEADERS: Dim datDirectory As IMAGE_DATA_DIRECTORY
Dim relBase As IMAGE_BASE_RELOCATION: Dim entriesCount As Long
Dim relType As Long: Dim dwAddress As Long
Dim dwOrig As Long: Dim pRelBase As Long
Dim delta As Long: Dim pData As Long
' // Check if module has not been loaded to image base value
If GetImageNtHeaders(pBase, NtHdr) = 0 Then
ProcessRelocations = EM_UNABLE_TO_GET_NT_HEADERS
Exit Function
End If
delta = pBase - NtHdr.OptionalHeader.ImageBase
' // Process relocations
If delta Then
' // Get address of relocation table
If GetDataDirectory(pBase, IMAGE_DIRECTORY_ENTRY_BASERELOC, datDirectory) = 0 Then
ProcessRelocations = EM_INVALID_DATA_DIRECTORY
Exit Function
End If
If datDirectory.Size > 0 And datDirectory.VirtualAddress > 0 Then
' // Copy relocation base
pRelBase = datDirectory.VirtualAddress + pBase
tCopyMemory IntPtr(relBase.VirtualAddress), pRelBase, Len(relBase)
Do While relBase.VirtualAddress
' // To first reloc chunk
pData = pRelBase + Len(relBase)
entriesCount = (relBase.SizeOfBlock - Len(relBase)) \ 2
Do While entriesCount > 0
tCopyMemory IntPtr(relType), pData, 2
Select Case (relType \ 4096) And &HF
Case IMAGE_REL_BASED_HIGHLOW
' // Calculate address
dwAddress = relBase.VirtualAddress + (relType And &HFFF&) + pBase
' // Get original address
tCopyMemory IntPtr(dwOrig), dwAddress, Len(dwOrig)
' // Add delta
dwOrig = dwOrig + delta
' // Save
tCopyMemory dwAddress, IntPtr(dwOrig), Len(dwOrig)
End Select
pData = pData + 2
entriesCount = entriesCount - 1
Loop
' // Next relocation base
pRelBase = pRelBase + relBase.SizeOfBlock
tCopyMemory IntPtr(relBase.VirtualAddress), pRelBase, Len(relBase)
Loop
End If
End If
End Function |
|
После настройки релокаций шеллкод вызывает функцию SetMemoryPermissions которая настраивает разрешения памяти согласно полю Characteristics структуры IMAGE_SECTION_HEADER. Для этого просто вызывается функция VirtualProtect с определенными атрибутами памяти:
| Visual Basic | 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
| ' // Set memory permissions
Private Function SetMemoryPermissions( _
ByVal pBase As Long) As ERROR_MESSAGES
Dim iSec As Long: Dim pNtHdr As Long
Dim NtHdr As IMAGE_NT_HEADERS: Dim sec As IMAGE_SECTION_HEADER
Dim Attr As MEMPROTECT: Dim pSec As Long
Dim ret As Long
pNtHdr = GetImageNtHeaders(pBase, NtHdr)
If pNtHdr = 0 Then
SetMemoryPermissions = EM_UNABLE_TO_GET_NT_HEADERS
Exit Function
End If
' // Get address of first section header
pSec = pNtHdr + 4 + Len(NtHdr.FileHeader) + NtHdr.FileHeader.SizeOfOptionalHeader
' // Go thru section headers
For iSec = 0 To NtHdr.FileHeader.NumberOfSections - 1
' // Copy section descriptor
tCopyMemory IntPtr(sec.SectionName(0)), pSec, Len(sec)
' // Get type
If sec.Characteristics And IMAGE_SCN_MEM_EXECUTE Then
If sec.Characteristics And IMAGE_SCN_MEM_READ Then
If sec.Characteristics And IMAGE_SCN_MEM_WRITE Then
Attr = PAGE_EXECUTE_READWRITE
Else
Attr = PAGE_EXECUTE_READ
End If
Else
If sec.Characteristics And IMAGE_SCN_MEM_WRITE Then
Attr = PAGE_EXECUTE_WRITECOPY
Else
Attr = PAGE_EXECUTE
End If
End If
Else
If sec.Characteristics And IMAGE_SCN_MEM_READ Then
If sec.Characteristics And IMAGE_SCN_MEM_WRITE Then
Attr = PAGE_READWRITE
Else
Attr = PAGE_READONLY
End If
Else
If sec.Characteristics And IMAGE_SCN_MEM_WRITE Then
Attr = PAGE_WRITECOPY
Else
Attr = PAGE_NOACCESS
End If
End If
End If
' // Set memory permissions
If tVirtualProtect(sec.VirtualAddress + pBase, sec.VirtualSize, Attr, IntPtr(ret)) = 0 Then
SetMemoryPermissions = EM_UNABLE_TO_PROTECT_MEMORY
Exit Function
End If
' // Next section
pSec = pSec + Len(sec)
Next
End Function |
|
В конце концов очищается таблица сообщений об ошибках (если нужно) и вызывается точка входа загруженного EXE. В предыдущей версии загрузчика я выгружал шеллкод тоже, но некоторые EXE не вызывают ExitProcess следовательно это могло вызывать креши. Загрузчик готов.
Хотя мы написал загрузчик без использвания рантайма, компилятор VB6 добавляет его все-равно поскольку все OBJ файлы имеют ссылки на MSVBVM60 во время компиляции. Нам придется удалить рантайм из таблицы импорта загрузчика вручную. Для этого я сделал специальную утилиту - Patcher которая ищет рантайм в таблице импорта и таблице связанного импорта и удаляет его оттуда. Эта утилита также была полезна для драйвера режима ядра. Я не буду описывать ее работу поскольку она использует те же концепции PE-формата что я уже описал здесь. В общем и целом мы сделали рабочий EXE который не использует MSVBVM60 на целевой машине.
Для того чтобы использовать загрузчик нужно скомпилировать его затем с помощью патчера пропатчить его. После этог можно использовать его в компиляторе.
Я надеюсь вам понравилось. Спасибо за внимание!
С уважением,
Кривоус Анатолий (The trick).
|