Хакер - Липосакция для fat binary. Ломаем программу для macOS с поддержкой нескольких архитектур

Хакер - Липосакция для fat binary. Ломаем программу для macOS с поддержкой нескольких архитектур

hacker_frei

https://t.me/hacker_frei

МВК

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

  • Немного теории
  • Intel
  • ARM
  • Патчим плагин

Мы мно­го раз писали о взло­ме прог­рамм для Windows. Для нее соз­дано мно­жес­тво отладчи­ков, дизас­сем­бле­ров и дру­гих полез­ных инс­тру­мен­тов. Сегод­ня же мы обра­тим взор на муль­тип­роцес­сорную прог­рамму для macOS, вер­нее, на пла­гин для маков­ско­го Adobe Illustrator CC 2021, который (в целях обу­чения!) будет прев­ращен из проб­ной вер­сии в пол­ноцен­ную. При­чем понадо­бят­ся нам исклю­читель­но инс­тру­мен­ты для Windows: IDA вер­сии 7.2 и Hiew.

WARNING

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

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

Для начала корот­ко попыта­емся получить пред­став­ление, что имен­но нам пред­сто­ит ломать. Мы уже при­вык­ли, что все исполня­емые фай­лы и биб­лиоте­ки под акту­аль­ные вер­сии Windows име­нуют­ся EXE/DLL и име­ют струк­туру MZ-PE. Под macOS исполь­зует­ся фор­мат Mach-O (сок­ращение от Mach object), явля­ющий­ся потом­ком фор­мата a.out, который макось унас­ледова­ла от Unix.

Как извес­тно, Apple любит пери­оди­чес­ки перехо­дить с одно­го семей­ства про­цес­соров на дру­гое, из‑за чего меня­ется и архи­тек­тура при­ложе­ний. Начав с PowerPC, Apple в середи­не нулевых перемет­нулась в стан Intel, пос­ле чего в недав­нем прош­лом кор­порация решила перей­ти на плат­форму ARM. Дабы поль­зовате­ли помень­ше стра­дали от подоб­ных метаний, был взят на воору­жение муль­тип­роцес­сорный фор­мат Fat binary («жир­ный бинар­ник»), который может содер­жать код одновре­мен­но под нес­коль­ко про­цес­соров. Такой модуль может работать как под Intel, так и под ARM.

Что же такое модуль Mach-O? Обыч­но он сос­тоит из трех областей. Заголо­вок содер­жит общую информа­цию о дво­ичном фай­ле: порядок бай­тов (магичес­кое чис­ло), тип про­цес­сора, количес­тво команд заг­рузки и т. д. Затем сле­дуют коман­ды заг­рузки — это сво­его рода оглавле­ние, которое опи­сыва­ет положе­ние сег­ментов, динами­чес­кую таб­лицу сим­волов и про­чие полез­ные вещи. Каж­дая коман­да заг­рузки содер­жит метадан­ные, такие как тип коман­ды, ее имя, позиция в дво­ичном фай­ле. Наконец, третья область — это дан­ные, обыч­но самая боль­шая часть объ­ектно­го фай­ла. Она содер­жит код и раз­личную допол­нитель­ную информа­цию.

Муль­тип­роцес­сорный «жир­ный» модуль может вклю­чать в себя нес­коль­ко обыч­ных модулей Mach-O, заточен­ных под раз­ные про­цес­соры (обыч­но это i386 и x86_64, ARM или ARM64). Струк­тура его пре­дель­но прос­та — сра­зу за Fat header, в котором опи­сыва­ются вхо­дящие в модуль бло­ки Mach-O, сле­дует код этих бло­ков, рас­положен­ный под­ряд. Я не буду под­робно оста­нав­ливать­ся на опи­сании всех сек­ций и полей дан­ного фор­мата, жела­ющие могут лег­ко нагуг­лить спе­цифи­кацию. Оста­новим­ся лишь на фор­мате заголов­ков, пос­коль­ку они понадо­бят­ся нам в даль­нейших дей­стви­ях.

Струк­тура клас­сичес­кого заголов­ка Mach-O выг­лядит так.

struct mach_header {

// Сигнатура, обычно CF FA ED FE или CE FA ED FE, но для варианта с обратным порядком байтов, возможна и обратная сигнатура FE ED FA CF

uint32_t magic;

// Тип процессора, для intel это 7, для ARM — С

cpu_type_t cputype;

// Подтип процессора, 1 означает 64-разрядность, например, 07000001h — x86_64

cpu_subtype_t cpusubtype;

// Тип файла

uint32_t filetype;

// Количество команд, следующих за хидером

uint32_t ncmds;

// Размер команд, следующих за хидером

uint32_t sizeofcmds;

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

uint32_t flags;

};

«Жир­ный заголо­вок» пред­став­ляет собой типич­ный заголо­вок Universal binary и выг­лядит вот так.

struct fat_header {

uint32_t magic; // 0BEBAFECAh

// Количество последующих блоков fat_arch, соответствующих поддерживаемым процессорам

uint32_t nfat_arch;

};

struct fat_arch {

cpu_type_t cputype;

cpu_subtype_t cpusubtype;

// Смещение до блока кода относительно начала файла

uint32_t offset;

// Длина соответствующего блока кода

uint32_t size;

// Выравнивание

uint32_t align;

};

struct fat_arch {

...

}

Струк­тура заголов­ка Mach-O

Ну а теперь, ког­да мы в дос­таточ­ной сте­пени воору­жились теорией, рас­смот­рим прак­тичес­кий при­мер. У нас есть некий инстал­лирован­ный иллюс­тра­торов­ский пла­гин, который тре­бует­ся оту­чить от суици­да по про­шес­твии три­аль­ного пери­ода. Пред­положим так­же, что дос­тупа к маку, на котором он уста­нов­лен, у нас нет, как и дру­гого мака под рукой — толь­ко воз­можность перепи­сывать фай­лы. Ищем в пап­ке нуж­ного пла­гина под­папку Contents\MacOS, а в ней — исполня­емый модуль пла­гина. В дан­ном слу­чае это динами­чес­кая биб­лиоте­ка Fat Mach-O file, о чем нам говорит сиг­натура CA FE BA BE.

INTEL

Заг­ружа­ем наш файл в IDA Pro: нам будет пред­ложено на выбор два (точ­нее три) спо­соба заг­рузки дан­ного фай­ла: Fat Mach-O file, 1.X86_64 и Fat Mach-O file, 2.ARM64. Тре­тий вари­ант, бинар­ный файл, нам неин­тересен. Нач­нем с самого прос­того и зна­комо­го всем поль­зовате­лям Windows вари­анта: выбира­ем Intel X86_64. Бег­ло про­бежав­шись по спис­ку наз­ваний фун­кций, обна­ружи­ваем имя checkPersonalize2_tryout. Так как у нас три­ал, дан­ная фун­кция впол­не может ока­зать­ся про­вер­кой на его валид­ность. Смот­рим, отку­да она вызыва­ется — ага, из фун­кции с еще более подоз­ритель­ным наз­вани­ем _checkUser:

__text:0000000000006A62 mov edi, esi ; SPPlugin *

__text:0000000000006A64 mov rsi, rbx ; _UserData_NSD_ *

__text:0000000000006A67 call __Z24checkPersonalize2_tryoutP8SPPluginP14_UserData_NSD_Ph ; checkPersonalize2_tryout(SPPlugin *,_UserData_NSD_ *,uchar *)

__text:0000000000006A6C test eax, eax

__text:0000000000006A6E jnz short loc_6A95

__text:0000000000006A70 cmp [rbp+var_21], 0

__text:0000000000006A74 jz short loc_6A95

__text:0000000000006A76

__text:0000000000006A76 loc_6A76: ; CODE XREF: _checkUser:loc_6A5D↑j

__text:0000000000006A76 mov rax, [r12]

__text:0000000000006A7A mov qword ptr [rax], 0

__text:0000000000006A81 mov byte ptr [rax+63Dh], 1

__text:0000000000006A88 mov qword ptr [rax+648h], 0

__text:0000000000006A93 jmp short loc_6A99

__text:0000000000006A95 ; ----------------

__text:0000000000006A95

__text:0000000000006A95 loc_6A95: ; CODE XREF: _checkUser+198↑j

__text:0000000000006A95 ; _checkUser+19E↑j ...

__text:0000000000006A95 mov [rbp+var_21], 0

Пос­коль­ку заг­рузить прог­рамму в отладчик и дой­ти до это­го мес­та мы не можем, про­буем догадать­ся, какой вари­ант воз­вра­щаемо­го зна­чения eax нас устра­ивает боль­ше. Выраже­ния в квад­ратных скоб­ках byte ptr [rax+63Dh] и qword ptr [rax+648h] похожи на уста­нов­ку полей неко­ей струк­туры или свой­ств объ­екта. Поис­кав по коду чуть выше, мы уви­дим такую конс­трук­цию:

__text:00000000000069E4 cmp byte ptr [rbx+63Dh], 0

__text:00000000000069EB jnz loc_6A99

__text:00000000000069F1

__text:00000000000069F1 loc_69F1: ; CODE XREF: _checkUser+124↑j

__text:00000000000069F1 mov [rbp+var_21], 0

__text:00000000000069F5 cmp byte ptr [rbx+653h], 0

__text:00000000000069FC jz short loc_6A35

__text:00000000000069FE cmp byte ptr [rbx+650h], 0

__text:0000000000006A05 jz short loc_6A5D

__text:0000000000006A07 mov edi, 8 ; unsigned __int64

__text:0000000000006A0C call __Znwm ; operator new(ulong)

__text:0000000000006A11 mov r14, rax

__text:0000000000006A14 mov ecx, 22h ; '"'

__text:0000000000006A19 mov rdi, rsp

__text:0000000000006A1C mov rsi, rbx

__text:0000000000006A1F rep movsq

__text:0000000000006A22 mov rdi, rax ; this

__text:0000000000006A25 call __ZN11AboutDialogC1E14_UserData_NSD_ ; AboutDialog::AboutDialog(_UserData_NSD_)

__text:0000000000006A2A mov rax, [r14]

__text:0000000000006A2D mov rdi, r14

__text:0000000000006A30 call qword ptr [rax+8]

__text:0000000000006A33 jmp short loc_6A99

По поведе­нию прог­раммы мы пом­ним, что о прос­рочке три­ала сиг­нализи­рует диалог About, вне­зап­но выс­какива­ющий при заг­рузке прог­раммы, ненуле­вое же зна­чение бай­та по адре­су [rbx+63Dh] ини­циирует обход дан­ной вет­ки. Выходит, что пра­виль­ной явля­ется вет­ка, в которой это­му бай­ту прис­ваивает­ся зна­чение 1 начиная со сме­щения __text:0000000000006A76 (изна­чаль­но этот байт ини­циали­зиру­ется в 0). Не мудрствуя лукаво, прос­то закора­чива­ем весь кусок кода вызова про­цеду­ры checkPersonalize2_tryout, уста­новив перед ним безус­ловный переход на loc_6A76:

__text:0000000000006A5D jmp loc_6A76

__text:0000000000006A62 ; ----------------

__text:0000000000006A62 mov edi, esi ; SPPlugin *

__text:0000000000006A64 mov rsi, rbx ; _UserData_NSD_ *

__text:0000000000006A67 call __Z24checkPersonalize2_tryoutP8SPPluginP14_UserData_NSD_Ph ; checkPersonalize2_tryout(SPPlugin *,_UserData_NSD_ *,uchar *)

__text:0000000000006A6C test eax, eax

__text:0000000000006A6E jnz short loc_6A95

__text:0000000000006A70 cmp [rbp+var_21], 0

__text:0000000000006A74 jz short loc_6A95

__text:0000000000006A76

__text:0000000000006A76 loc_6A76: ; CODE XREF: _checkUser:loc_6A5D↑j

__text:0000000000006A76 mov rax, [r12]

ARM

Итак, с частью кода, ответс­твен­ной за х86, мы вро­де разоб­рались, поп­робу­ем сде­лать то же самое с армов­ской частью. Сно­ва заг­ружа­ем этот модуль в IDA, на сей раз выб­рав при заг­рузке вари­ант Fat Mach-O file, 2.ARM64. Мы видим, что фун­кции _checkUser и checkPersonalize2_tryout при­сутс­тву­ют и в этой час­ти кода, выше­опи­сан­ное мес­то вызова в перево­де на армов­ский ассем­блер выг­лядит вот так:

__text:000000000000716C MOV X2, SP

__text:0000000000007170 MOV X0, X19

__text:0000000000007174 MOV X1, X20

__text:0000000000007178 BL __Z24checkPersonalize2_tryoutP8SPPluginP14_UserData_NSD_Ph ; checkPersonalize2_tryout(SPPlugin *,_UserData_NSD_ *,uchar *)

__text:000000000000717C LDRB W8, [SP,#0x150+var_150]

__text:0000000000007180 CMP W0, #0

__text:0000000000007184 CCMP W8, #0, #4, EQ

__text:0000000000007188 B.NE loc_7194

__text:000000000000718C

__text:000000000000718C loc_718C ; CODE XREF: _checkUser+1A0↑j

__text:000000000000718C STRB WZR, [SP,#0x150+var_150]

__text:0000000000007190 B loc_71A4

__text:0000000000007194 ; ----------------

__text:0000000000007194

__text:0000000000007194 loc_7194 ; CODE XREF: _checkUser+1D0↑j

__text:0000000000007194 LDR X8, [X22]

__text:0000000000007198 MOV W9, #1

__text:000000000000719C STRB W9, [X8,#0x3A0]

__text:00000000000071A0 STR XZR, [X8,#0x3A8]

__text:00000000000071A4

__text:00000000000071A4 loc_71A4 ; CODE XREF: _checkUser+118↑j

__text:00000000000071A4 MOV W0, #0

Рас­смот­рев этот код пов­ниматель­нее, мы видим, что в армов­ском коде ана­логом поля [rax+63Dh] слу­жит байт по адре­су [X8,#0x3A0], ибо имен­но ему прис­ваивает­ся еди­нич­ка при удач­ном вызове checkPersonalize2_tryout. Поэто­му, дабы не изоб­ретать велоси­пед, дей­ству­ем тем же спо­собом, что и ранее — закора­чива­ем кусок кода, встав­ляя перед ним безус­ловный переход на loc_7194:

__text:000000000000716C B loc_7194

__text:0000000000007170 MOV X0, X19

__text:0000000000007174 MOV X1, X20

__text:0000000000007178 BL __Z24checkPersonalize2_tryoutP8SPPluginP14_UserData_NSD_Ph ; checkPersonalize2_tryout(SPPlugin *,_UserData_NSD_ *,uchar *)

__text:000000000000717C LDRB W8, [SP,#0x150+var_150]

__text:0000000000007180 CMP W0, #0

__text:0000000000007184 CCMP W8, #0, #4, EQ

__text:0000000000007188 B.NE loc_7194

__text:000000000000718C

__text:000000000000718C loc_718C ; CODE XREF: _checkUser+1A0↑j

__text:000000000000718C STRB WZR, [SP,#0x150+var_150]

__text:0000000000007190 B loc_71A4

__text:0000000000007194 ; ----------------

__text:0000000000007194

__text:0000000000007194 loc_7194 ; CODE XREF: _checkUser+1D0↑j

__text:0000000000007194 LDR X8, [X22]

ПАТЧИМ ПЛАГИН

Те­перь, ког­да мы разоб­рались, что и где сле­дует менять, нуж­но внес­ти эти самые изме­нения. Самое прос­тое, что у нас есть под рукой — малень­кий DOS-овский шес­тнад­цатерич­ный редак­тор Hiew, который, помимо прос­того бай­тового редак­тирова­ния, уме­ет дизас­сем­бли­ровать и ассем­бли­ровать код для Intel и даже для ARM. К сожале­нию, про ARM64, который нам нужен, и Fat Mach-O он ничего не зна­ет, поэто­му при­дет­ся нем­ного порабо­тать руками, исполь­зуя на прак­тике опи­сан­ную выше теорию.

От­крыв заголо­вок модуля, мы видим в нем две сек­ции Mach-O с абсо­лют­ными сме­щени­ями 8000h и 17С000h. Так и есть, по пер­вому сме­щению сидит сиг­натура сек­ции CF FA ED FE и код про­цес­сора 07 00 00 01 — это инте­лов­ская часть. По вто­рому сме­щению сиг­натура та же, но код про­цес­сора дру­гой 0C 00 00 01 — это ARM.

За­голо­вок модуля

При­бав­ляем к 8000h сме­щение из IDA — 6A5Dh, и получа­ем EA5Dh — сме­щение до пер­вого пат­ча в инте­лов­ской час­ти. Перек­люча­емся через Ctrl-F1 в 64-бит­ный режим и пра­вим иско­мый jmp. Теперь вне­сем изме­нения в армов­скую часть. Тут есть неболь­шая слож­ность. Сме­щение до пат­ча 17С000h+716Ch=18316Ch мы наш­ли, одна­ко при перек­лючении в режим ARM дизас­сем­бле­ра через Shift-F1 код сов­сем дру­гой, Hiew не понима­ет акту­аль­ный ARM64. Поп­робу­ем вычис­лить и поп­равить иско­мый опкод руками. Откры­ваем спе­цифи­кацию (если очень лениво искать, то мож­но прос­то пос­мотреть в IDA по сосед­ним коман­дам) — опкод коман­ды безус­ловно­го перехо­да 14h (пос­ледний байт коман­ды). Пер­выми бай­тами идет сме­щение до адре­са перехо­да в 32-бит­ных коман­дах. Счи­таем: 7194h-716Ch=28h делим на 4 бай­та и получа­ем 0Ah — иско­мое сме­щение для перехо­да. В резуль­тате код исправ­ленной коман­ды выг­лядит так:

__text:000000000000716C 0A 00 00 14 B loc_7194

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

sudo codesign --force --deep -sign - <полный путь к пропатченному модулю>

По идее, мож­но вооб­ще убрать под­пись через stripcodesig или даже до копиро­вания на мак, но это получа­ется не всег­да. Нап­ример, начиная с macOS Catalina, может пот­ребовать­ся уда­лить при­ложе­ние из каран­тина, для это­го в тер­минале при­дет­ся наб­рать сле­дующую коман­ду:

sudo xattr -rd com.apple.quarantine <полный путь к пропатченному модулю>

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

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



Report Page