Как взламывают программы macOS

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

Всем известно что, Apple любит иногда перехо­дить с одно­го семей­ства про­цов на дру­гое, из-за этого меня­ется и архи­тек­тура программ. Начав с PowerPC, Apple в середи­не нулевых перемет­нулась в стан Intel, пос­ле чего в недав­нем прош­лом кор­порация решила перей­ти на плат­форму ARM.

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

Что такое Mach-O? Обыч­но данный модуль сос­тоит из трех областей:

  1. Первая область (Заголо­вок) представляет информа­цию о бинарном фай­ле: тип про­цес­сора, порядок бай­тов, количес­тво команд заг­рузки и т. п.

  2. Вторая область — коман­ды заг­рузки — это можно сказать оглавле­ние, в котором описано положе­ние сег­ментов, динами­чес­кая таб­лица сим­волов и т.д. Все коман­ды заг­рузки содер­жат метадан­ные, такие как тип коман­ды, ее название, позиция в бинарном фай­ле.

  3. Третья область — это дан­ные, как правило самая боль­шая часть объ­ектно­го фай­ла. Это часть содер­жит код и другу информа­цию.

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

В рамках этой статьи, не буду погружаться глубоко в описание всех секций и полей этого формата. Если интересно, можете погуглить сами.

Взлом программ macOS с помощью IDA и Hiew

Итак, теперь, ког­да вы достаточно узнали о теории, давайте рас­смот­рим прак­тичес­кий при­мер. У меня есть некий установленный иллюс­тра­торов­ский пла­гин, который надо оту­чить от самоубийства после окончания три­ала.

Пред­положим так­же, что дос­тупа к компьютеру с macOS, на котором установлен данный плагин, у нас нет, как и дру­гого компа с macOS под рукой — толь­ко воз­можность перепи­сывать фай­лы.

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

Last updated