Introduction to ARM64v8
Last updated
Last updated
Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE) Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)
В архітектурі ARMv8 рівні виконання, відомі як Рівні Виключень (EL), визначають рівень привілеїв та можливості середовища виконання. Є чотири рівні виключень, від EL0 до EL3, кожен з яких виконує різну функцію:
EL0 - Режим користувача:
Це найменш привілейований рівень і використовується для виконання звичайного коду додатків.
Додатки, що працюють на EL0, ізольовані один від одного та від системного програмного забезпечення, що підвищує безпеку та стабільність.
EL1 - Режим ядра операційної системи:
Більшість ядер операційних систем працюють на цьому рівні.
EL1 має більше привілеїв, ніж EL0, і може отримувати доступ до системних ресурсів, але з деякими обмеженнями для забезпечення цілісності системи.
EL2 - Режим гіпервізора:
Цей рівень використовується для віртуалізації. Гіпервізор, що працює на EL2, може керувати кількома операційними системами (кожна в своєму EL1), що працюють на одному фізичному обладнанні.
EL2 надає функції для ізоляції та контролю віртуалізованих середовищ.
EL3 - Режим безпечного монітора:
Це найпривілейованіший рівень і часто використовується для безпечного завантаження та довірених середовищ виконання.
EL3 може керувати та контролювати доступи між безпечними та небезпечними станами (такими як безпечне завантаження, довірена ОС тощо).
Використання цих рівнів дозволяє структуровано та безпечно управляти різними аспектами системи, від користувацьких додатків до найпривілейованішого системного програмного забезпечення. Підхід ARMv8 до рівнів привілеїв допомагає ефективно ізолювати різні компоненти системи, тим самим підвищуючи безпеку та надійність системи.
ARM64 має 31 загальний реєстр, позначений x0
до x30
. Кожен може зберігати 64-бітне (8-байтове) значення. Для операцій, які вимагають лише 32-бітних значень, ті ж реєстри можуть бути доступні в 32-бітному режимі, використовуючи назви w0 до w30.
x0
до x7
- Ці реєстри зазвичай використовуються як тимчасові реєстри та для передачі параметрів підпрограмам.
x0
також несе дані повернення функції
x8
- У ядрі Linux x8
використовується як номер системного виклику для інструкції svc
. У macOS використовується x16!
x9
до x15
- Більше тимчасових реєстрів, часто використовуються для локальних змінних.
x16
та x17
- Реєстри внутрішньопроцедурного виклику. Тимчасові реєстри для негайних значень. Вони також використовуються для непрямих викликів функцій та PLT (таблиця зв'язування процедур).
x16
використовується як номер системного виклику для інструкції svc
в macOS.
x18
- Реєстр платформи. Може використовуватися як загальний реєстр, але на деяких платформах цей реєстр зарезервований для специфічних для платформи використань: вказівник на блок середовища поточного потоку в Windows або вказівник на поточну структуру виконуваного завдання в ядрі linux.
x19
до x28
- Це реєстри, збережені викликачем. Функція повинна зберігати значення цих реєстрів для свого виклику, тому вони зберігаються в стеку та відновлюються перед поверненням до виклику.
x29
- Вказівник кадру для відстеження кадру стеку. Коли створюється новий кадр стеку через виклик функції, реєстр x29
зберігається в стеку, а адреса нового вказівника кадру (адреса sp
) зберігається в цьому реєстрі.
Цей реєстр також може використовуватися як загальний реєстр, хоча зазвичай використовується як посилання на локальні змінні.
x30
або lr
- Реєстр зв'язку. Він містить адресу повернення, коли виконується інструкція BL
(перехід з посиланням) або BLR
(перехід з посиланням на реєстр), зберігаючи значення pc
в цьому реєстрі.
Його також можна використовувати як будь-який інший реєстр.
Якщо поточна функція збирається викликати нову функцію і, отже, перезаписати lr
, вона спочатку зберігає його в стеку, це епілог (stp x29, x30 , [sp, #-48]; mov x29, sp
-> Зберегти fp
та lr
, створити простір і отримати новий fp
) і відновлює його в кінці, це пролог (ldp x29, x30, [sp], #48; ret
-> Відновити fp
та lr
і повернутися).
sp
- Вказівник стеку, використовується для відстеження верхньої частини стеку.
Значення sp
завжди повинно зберігатися принаймні з вирівнюванням квадратного слова, інакше може виникнути виняток вирівнювання.
pc
- Лічильник програми, який вказує на наступну інструкцію. Цей реєстр може бути оновлений лише через генерацію виключень, повернення з виключень та переходи. Єдині звичайні інструкції, які можуть читати цей реєстр, - це інструкції переходу з посиланням (BL, BLR), щоб зберегти адресу pc
в lr
(реєстр зв'язку).
xzr
- Нульовий реєстр. Також називається wzr
у його 32-бітній формі. Може використовуватися для отримання нульового значення (поширена операція) або для виконання порівнянь, використовуючи subs
, наприклад, subs XZR, Xn, #10
, зберігаючи результуючі дані нікуди (в xzr
).
Реєстри Wn
є 32-бітною версією реєстрів Xn
.
Крім того, є ще 32 реєстри довжиною 128 біт, які можуть використовуватися в оптимізованих операціях з одноразовими інструкціями множинних даних (SIMD) та для виконання арифметики з плаваючою точкою. Вони називаються реєстрами Vn, хоча вони також можуть працювати в 64-бітному, 32-бітному, 16-бітному та 8-бітному режимах, і тоді їх називають Qn
, Dn
, Sn
, Hn
та Bn
.
Є сотні системних реєстрів, також відомих як спеціалізовані реєстри (SPRs), які використовуються для моніторингу та контролю поведінки процесорів.
Вони можуть бути прочитані або встановлені лише за допомогою спеціальних інструкцій mrs
та msr
.
Спеціальні реєстри TPIDR_EL0
та TPIDDR_EL0
зазвичай зустрічаються під час реверс-інжинірингу. Суфікс EL0
вказує на мінімальне виключення, з якого реєстр може бути доступний (в цьому випадку EL0 - це звичайний рівень виключення (привілеїв), з яким працюють звичайні програми).
Вони часто використовуються для зберігання базової адреси регіону локального зберігання потоку пам'яті. Зазвичай перший з них є читабельним і записуваним для програм, що працюють в EL0, але другий може бути прочитаний з EL0 і записаний з EL1 (як ядро).
mrs x0, TPIDR_EL0 ; Прочитати TPIDR_EL0 в x0
msr TPIDR_EL0, X0 ; Записати x0 в TPIDR_EL0
PSTATE містить кілька компонентів процесу, серіалізованих у видимому для операційної системи SPSR_ELx
спеціальному реєстрі, де X - це рівень дозволу викликаного виключення (це дозволяє відновити стан процесу, коли виключення закінчується).
Це доступні поля:
N
, Z
, C
та V
умови:
N
означає, що операція дала негативний результат
Z
означає, що операція дала нуль
C
означає, що операція перенесла
V
означає, що операція дала підписане переповнення:
Сума двох позитивних чисел дає негативний результат.
Сума двох негативних чисел дає позитивний результат.
У відніманні, коли велике негативне число віднімається від меншого позитивного числа (або навпаки), і результат не може бути представленим у межах заданого розміру біта.
Очевидно, процесор не знає, чи операція підписана чи ні, тому він перевірить C та V в операціях і вказує, чи відбулося перенесення у випадку, якщо це було підписане або без підпису.
Не всі інструкції оновлюють ці прапори. Деякі, такі як CMP
або TST
, роблять це, а інші, які мають суфікс s, такі як ADDS
, також роблять це.
Поточний прапор ширини реєстра (nRW
): Якщо прапор має значення 0, програма буде виконуватися в стані виконання AArch64 після відновлення.
Поточний рівень виключення (EL
): Звичайна програма, що працює в EL0, матиме значення 0
Прапор одиночного кроку (SS
): Використовується відладчиками для одиночного кроку, встановлюючи прапор SS в 1 всередині SPSR_ELx
через виключення. Програма виконає крок і видасть виключення одиночного кроку.
Прапор недопустимого виключення (IL
): Використовується для позначення, коли привілейоване програмне забезпечення виконує недійсний перехід рівня виключення, цей прапор встановлюється в 1, і процесор викликає виключення недійсного стану.
Прапори DAIF
: Ці прапори дозволяють привілейованій програмі вибірково маскувати певні зовнішні виключення.
Якщо A
дорівнює 1, це означає, що асинхронні перерви будуть викликані. I
налаштовує відповідь на зовнішні запити переривань (IRQ). а F пов'язаний з швидкими запитами переривань (FIR).
Прапори вибору вказівника стеку (SPS
): Привілейовані програми, що працюють в EL1 та вище, можуть перемикатися між використанням свого власного реєстру вказівника стеку та реєстром моделі користувача (наприклад, між SP_EL1
та EL0
). Це перемикання виконується шляхом запису в спеціальний реєстр SPSel
. Це не можна зробити з EL0.
Конвенція виклику ARM64 вказує, що перші вісім параметрів до функції передаються в реєстрах x0
до x7
. Додаткові параметри передаються на стек. Значення повернення передається назад у реєстр x0
, або в x1
, якщо воно 128 біт довге. Реєстри x19
до x30
та sp
повинні бути збережені під час викликів функцій.
Коли читаєте функцію в асемблері, шукайте пролог та епілог функції. Пролог зазвичай включає збереження вказівника кадру (x29
), налаштування нового вказівника кадру та виділення простору в стеку. Епілог зазвичай включає відновлення збереженого вказівника кадру та повернення з функції.
Swift має свою власну конвенцію виклику, яку можна знайти в https://github.com/apple/swift/blob/main/docs/ABI/CallConvSummary.rst#arm64
Інструкції ARM64 зазвичай мають формат opcode dst, src1, src2
, де opcode
- це операція, яка має бути виконана (така як add
, sub
, mov
тощо), dst
- це реєстр призначення, куди буде збережено результат, а src1
та src2
- це реєстри джерела. Негайні значення також можуть використовуватися замість реєстрів джерела.
mov
: Перемістити значення з одного реєстру в інший.
Приклад: mov x0, x1
— Це переміщує значення з x1
в x0
.
ldr
: Завантажити значення з пам'яті в реєстр.
Приклад: ldr x0, [x1]
— Це завантажує значення з пам'ятної адреси, на яку вказує x1
, в x0
.
Режим зсуву: Зсув, що впливає на початковий вказівник, вказується, наприклад:
ldr x2, [x1, #8]
, це завантажить в x2 значення з x1 + 8
ldr x2, [x0, x1, lsl #2]
, це завантажить в x2 об'єкт з масиву x0, з позиції x1 (індекс) * 4
Режим попереднього індексування: Це застосує обчислення до початкового, отримає результат і також зберігає новий початок у початковому.
ldr x2, [x1, #8]!
, це завантажить x1 + 8
в x2
і зберігає в x1 результат x1 + 8
str lr, [sp, #-4]!
, Зберегти реєстр зв'язку в sp і оновити реєстр sp
Режим постіндексування: Це як попередній, але адреса пам'яті спочатку доступна, а потім зсув обчислюється та зберігається.
ldr x0, [x1], #8
, завантажити x1
в x0
і оновити x1 з x1 + 8
Адресація відносно PC: У цьому випадку адреса для завантаження обчислюється відносно реєстру PC
ldr x1, =_start
, Це завантажить адресу, де символ _start
починається в x1, відносно поточного PC.
str
: Зберегти значення з реєстру в пам'ять.
Приклад: str x0, [x1]
— Це зберігає значення в x0
в пам'ятній адресі, на яку вказує x1
.
ldp
: Завантажити пару реєстрів. Ця інструкція завантажує два реєстри з послідовних пам'ятних адрес. Адреса пам'яті зазвичай формується шляхом додавання зсуву до значення в іншому реєстрі.
Приклад: ldp x0, x1, [x2]
— Це завантажує x0
та x1
з пам'ятних адрес x2
та x2 + 8
, відповідно.
stp
: Зберегти пару реєстрів. Ця інструкція зберігає два реєстри в послідовні пам'ятні адреси. Адреса пам'яті зазвичай формується шляхом додавання зсуву до значення в іншому реєстрі.
Приклад: 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 -> Призначення
Xn2 -> Операнд 1
Xn3 | #imm -> Операнд 2 (реєстр або негайне)
[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
:
Логічний зсув вліво: Додає 0 з кінця, переміщуючи інші біти вперед (множить на n разів 2)
Логічний зсув вправо: Додає 1 на початку, переміщуючи інші біти назад (ділить на n разів 2 в беззнаковому)
Арифметичний зсув вправо: Як lsr
, але замість додавання 0, якщо найзначніший біт - 1, додаються 1 (ділить на n разів 2 в знаковому)
Зсув вправо з розширенням: Як ror
, але з прапором переносу як "найзначнішим бітом". Тобто прапор переносу переміщується до біта 31, а видалений біт - до прапора переносу.
bfm
: Переміщення бітового поля, ці операції копіюють біти 0...n
з значення і поміщають їх у позиції m..m+n
. #s
вказує на найлівішу позицію біта, а #r
- на кількість зсуву вправо.
Переміщення бітового поля: BFM Xd, Xn, #r
Знакове переміщення бітового поля: SBFM Xd, Xn, #r, #s
Беззнакове переміщення бітового поля: UBFM Xd, Xn, #r, #s
Витягування та вставка бітового поля: Копіює бітове поле з одного реєстру та копіює його в інший реєстр.
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.
Розширення знака до 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 (беззнаковий) до байта з W2 до X1 для заповнення 64 біт
extr
: Витягує біти з вказаної пари реєстрів, що конкатенуються.
Приклад: EXTR W3, W2, W1, #3
Це конкатенує W1+W2 і отримує з біта 3 W2 до біта 3 W1 і зберігає в W3.
cmp
: Порівняти два реєстри та встановити умови. Це псевдонім для subs
, встановлюючи реєстр призначення в нульовий реєстр. Корисно знати, чи m == n
.
Він підтримує той же синтаксис, що й subs
Приклад: cmp x0, x1
— Це порівнює значення в x0
та x1
і відповідно встановлює умови.
cmn
: Порівняти негативний операнд. У цьому випадку це псевдонім для 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 без зберігання результату десь). Корисно перевірити реєстр з значенням і перевірити, чи є будь-які біти реєстру, вказані в значенні, 1.
Приклад: tst X1, #7
Перевірити, чи є будь-які з останніх 3 бітів X1 1
teq
: Операція XOR, відкидаючи результат
b
: Безумовний перехід
Приклад: b myFunction
Зверніть увагу, що це не заповнить реєстр зв'язку адресою повернення (не підходить для викликів підпрограм, які потребують повернення назад)
bl
: Перехід з посиланням, використовується для виклику підпрограми. Зберігає адресу повернення в x30
.
Приклад: bl myFunction
— Це викликає функцію myFunction
і зберігає адресу повернення в x30
.
Зверніть увагу, що це не заповнить реєстр зв'язку адресою повернення (не підходить для викликів підпрограм, які потребують повернення назад)
blr
: Перехід з посиланням на реєстр, використовується для виклику підпрограми, де ціль вказана в реєстрі. Зберігає адресу повернення в x30
. (Це
Приклад: blr x1
— Це викликає функцію, адреса якої міститься в x1
, і зберігає адресу повернення в x30
.
ret
: Повернення з підпрограми, зазвичай використовуючи адресу в x30
.
Приклад: ret
— Це повертає з поточної підпрограми, використовуючи адресу повернення в x30
.
b.<cond>
: Умовні переходи
b.eq
: Перехід, якщо рівно, на основі попередньої інструкції cmp
.
Приклад: b.eq label
— Якщо попередня інструкція cmp
знайшла два рівні значення, це переходить до label
.
b.ne
: Перехід, якщо не рівно. Ця інструкція перевіряє умови (які були встановлені попередньою інструкцією порівняння), і якщо порівняні значення не були рівні, вона переходить до мітки або адреси.
Приклад: Після інструкції cmp x0, x1
, b.ne label
— Якщо значення в x0
та x1
не були рівні, це переходить до label
.
cbz
: Порівняти та перейти на нуль. Ця інструкція порівнює реєстр з нулем, і якщо вони рівні, переходить до мітки або адреси.
Приклад: cbz x0, label
— Якщо значення в x0
нульове, це переходить до label
.
cbnz
: Порівняти та перейти на ненульове. Ця інструкція порівнює реєстр з нулем, і якщо вони не рівні, переходить до мітки або адреси.
Приклад: cbnz x0, label
— Якщо значення в x0
ненульове, це переходить до label
.
tbnz
: Перевірити біт і перейти на ненульове
Приклад: tbnz x0, #8, label
tbz
: Перевірити біт і перейти на нуль
Приклад: tbz x0, #8, label
Умовні вибіркові операції: Це операції, поведінка яких змінюється в залежності від умовних бітів.
csel Xd, Xn, Xm, cond
-> csel X0, X1, X2, EQ
-> Якщо істинно, X0 = X1, якщо хибно, X0 = X2
csinc Xd, Xn, Xm, cond
-> Якщо істинно, Xd = Xn, якщо хибно, Xd = Xm + 1
cinc Xd, Xn, cond
-> Якщо істинно, Xd = Xn + 1, якщо хибно, Xd = Xn
csinv Xd, Xn, Xm, cond
-> Якщо істинно, Xd = Xn, якщо хибно, Xd = NOT(Xm)
cinv Xd, Xn, cond
-> Якщо істинно, Xd = NOT(Xn), якщо хибно, Xd = Xn
csneg Xd, Xn, Xm, cond
-> Якщо істинно, Xd = Xn, якщо хибно, Xd = - Xm
cneg Xd, Xn, cond
-> Якщо істинно, Xd = - Xn, якщо хибно, Xd = Xn
cset Xd, Xn, Xm, cond
-> Якщо істинно, Xd = 1, якщо хибно, Xd = 0
csetm Xd, Xn, Xm, cond
-> Якщо істинно, Xd = <всі 1>, якщо хибно, Xd = 0
adrp
: Обчислити адресу сторінки символу та зберегти її в реєстрі.
Приклад: adrp x0, symbol
— Це обчислює адресу сторінки символу symbol
і зберігає її в x0
.
ldrsw
: Завантажити підписане 32-бітне значення з пам'яті та розширити його до 64 біт.
Приклад: ldrsw x0, [x1]
— Це завантажує підписане 32-бітне значення з пам'ятної адреси, на яку вказує x1
, розширює його до 64 біт і зберігає в x0
.
stur
: Зберегти значення реєстру в пам'ятній адресі, використовуючи зсув з іншого реєстру.
Приклад: stur x0, [x1, #4]
— Це зберігає значення в x0
в пам'ятній адресі, яка на 4 байти більша, ніж адреса, що зараз в x1
.
svc
: Зробити системний виклик. Це означає "Виклик наглядача". Коли процесор виконує цю інструкцію, він перемикається з режиму користувача в режим ядра і переходить до певного місця в пам'яті, де знаходиться код обробки системних викликів ядра.
Приклад:
Зберегти реєстр зв'язку та вказівник кадру в стек:
Встановіть новий вказівник кадру: mov x29, sp
(встановлює новий вказівник кадру для поточної функції)
Виділіть місце в стеку для локальних змінних (якщо потрібно): sub sp, sp, <size>
(де <size>
- це кількість байтів, що потрібні)
Звільніть локальні змінні (якщо вони були виділені): add sp, sp, <size>
Відновіть регістр посилання та вказівник кадру:
Повернення: ret
(повертає управління виклику, використовуючи адресу в регістрі посилань)
Armv8-A підтримує виконання 32-бітних програм. AArch32 може працювати в одному з двох наборів інструкцій: A32
та T32
і може перемикатися між ними через interworking
.
Привілейовані 64-бітні програми можуть планувати виконання 32-бітних програм, виконуючи передачу рівня виключення на нижчий привілейований 32-біт.
Зверніть увагу, що перехід з 64-біт на 32-біт відбувається з пониженням рівня виключення (наприклад, 64-бітна програма в EL1 викликає програму в EL0). Це здійснюється шляхом встановлення біта 4 спеціального регістру SPSR_ELx
в 1, коли потік процесу AArch32
готовий до виконання, а решта SPSR_ELx
зберігає CPSR програм AArch32
. Потім привілейований процес викликає інструкцію ERET
, щоб процесор перейшов до AArch32
, входячи в A32 або T32 в залежності від CPSR**.**
Interworking
відбувається за допомогою бітів J та T CPSR. J=0
та T=0
означає A32
, а J=0
та T=1
означає T32. Це в основному означає встановлення найнижчого біта в 1, щоб вказати, що набір інструкцій є T32.
Це встановлюється під час інструкцій переходу interworking, але також може бути встановлено безпосередньо з іншими інструкціями, коли PC встановлено як регістр призначення. Приклад:
Інший приклад:
Є 16 32-бітних регістрів (r0-r15). Від r0 до r14 їх можна використовувати для будь-якої операції, однак деякі з них зазвичай зарезервовані:
r15
: Лічильник програми (завжди). Містить адресу наступної інструкції. В A32 поточний + 8, в T32, поточний + 4.
r11
: Вказівник кадру
r12
: Регістр внутрішньопроцедурного виклику
r13
: Вказівник стеку
r14
: Регістр посилання
Більше того, регістри зберігаються в банківських реєстрах
. Це місця, які зберігають значення регістрів, що дозволяє виконувати швидке перемикання контексту під час обробки виключень та привілейованих операцій, щоб уникнути необхідності вручну зберігати та відновлювати регістри щоразу.
Це робиться шляхом збереження стану процесора з CPSR
до SPSR
режиму процесора, в якому виникає виключення. Під час повернення з виключення, CPSR
відновлюється з SPSR
.
В AArch32 CPSR працює подібно до PSTATE
в AArch64 і також зберігається в SPSR_ELx
, коли виникає виключення, щоб пізніше відновити виконання:
Поля поділяються на кілька груп:
Реєстр статусу програми (APSR): Арифметичні прапори та доступні з EL0
Реєстри стану виконання: Поведінка процесу (керується ОС).
Прапори N
, Z
, C
, V
(так само, як в AArch64)
Прапор Q
: Встановлюється в 1, коли відбувається насичення цілих чисел під час виконання спеціалізованої арифметичної інструкції з насиченням. Як тільки він встановлений на 1
, він зберігатиме значення, поки його не встановлять вручну на 0. Більше того, немає жодної інструкції, яка перевіряє його значення неявно, це потрібно робити, читаючи його вручну.
Прапори GE
(Більше або дорівнює): Використовуються в SIMD (Одна інструкція, кілька даних) операціях, таких як "паралельне додавання" та "паралельне віднімання". Ці операції дозволяють обробляти кілька точок даних в одній інструкції.
Наприклад, інструкція UADD8
додає чотири пари байтів (з двох 32-бітних операндів) паралельно та зберігає результати в 32-бітному регістрі. Потім вона встановлює прапори GE
в APSR
на основі цих результатів. Кожен прапор GE відповідає одному з додавань байтів, вказуючи, чи додавання для цієї пари байтів переповнилося.
Інструкція SEL
використовує ці прапори GE для виконання умовних дій.
Біти J
та T
: J
має бути 0, і якщо T
0, використовується набір інструкцій A32, а якщо 1, використовується T32.
Реєстр стану блоку IT (ITSTATE
): Це біти з 10-15 та 25-26. Вони зберігають умови для інструкцій всередині групи з префіксом IT
.
E
біт: Вказує на порядок байтів.
Біти маски режиму та виключення (0-4): Вони визначають поточний стан виконання. 5-й вказує, чи програма працює в 32-бітному (1) або 64-бітному (0) режимі. Інші 4 представляють режим виключення, що використовується в даний момент (коли виникає виключення і його обробляють). Встановлене число вказує на поточний пріоритет у разі, якщо виникає інше виключення під час обробки цього.
AIF
: Деякі виключення можуть бути вимкнені за допомогою бітів A
, I
, F
. Якщо A
1, це означає, що асинхронні аборти будуть викликані. I
налаштовує відповідь на зовнішні апаратні Запити переривання (IRQ). а F пов'язаний з Швидкими запитами переривання (FIR).
Перегляньте syscalls.master. BSD syscalls матимуть x16 > 0.
Перегляньте в 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:
Зверніть увагу, що Ida та Ghidra також можуть декомпілювати конкретні dylibs з кешу, просто передавши кеш.
Іноді легше перевірити декомпільований код з libsystem_kernel.dylib
ніж перевіряти джерельний код, оскільки код кількох системних викликів (BSD та Mach) генерується за допомогою скриптів (перевірте коментарі в джерельному коді), тоді як у dylib ви можете знайти, що викликається.
XNU підтримує ще один тип викликів, званий залежними від машини. Кількість цих викликів залежить від архітектури, і ні виклики, ні числа не гарантовано залишаться постійними.
Це сторінка пам'яті, що належить ядру, яка відображається в адресному просторі кожного процесу користувача. Вона призначена для того, щоб зробити перехід з режиму користувача в простір ядра швидшим, ніж використання системних викликів для сервісів ядра, які використовуються настільки часто, що цей перехід був би дуже неефективним.
Наприклад, виклик gettimeofdate
читає значення timeval
безпосередньо зі сторінки comm.
Цю функцію дуже часто можна знайти в програмах на Objective-C або Swift. Ця функція дозволяє викликати метод об'єкта Objective-C.
Параметри (більше інформації в документації):
x0: self -> Вказівник на екземпляр
x1: op -> Селектор методу
x2... -> Інші аргументи викликаного методу
Отже, якщо ви поставите точку зупинки перед переходом до цієї функції, ви можете легко знайти, що викликається в lldb (в цьому прикладі об'єкт викликає об'єкт з NSConcreteTask
, який виконає команду):
Встановивши змінну середовища NSObjCMessageLoggingEnabled=1
, можна записувати, коли ця функція викликається у файлі, наприклад, /tmp/msgSends-pid
.
Крім того, встановивши OBJC_HELP=1
та викликавши будь-який бінарний файл, ви можете побачити інші змінні середовища, які можна використовувати для логування певних дій Objc-C.
Коли ця функція викликається, потрібно знайти викликаний метод вказаного екземпляра, для цього проводяться різні пошуки:
Виконати оптимістичний пошук у кеші:
Якщо успішно, завершено
Отримати runtimeLock (читання)
Якщо (realize && !cls->realized) реалізувати клас
Якщо (initialize && !cls->initialized) ініціалізувати клас
Спробувати власний кеш класу:
Якщо успішно, завершено
Спробувати список методів класу:
Якщо знайдено, заповнити кеш і завершити
Спробувати кеш суперкласу:
Якщо успішно, завершено
Спробувати список методів суперкласу:
Якщо знайдено, заповнити кеш і завершити
Якщо (resolver) спробувати резолвер методу і повторити з пошуку класу
Якщо все ще тут (= все інше не вдалося) спробувати форвардер
Щоб скомпілювати:
Щоб витягти байти:
Для новіших macOS:
Вивчайте та практикуйте AWS Hacking:HackTricks Training AWS Red Team Expert (ARTE) Вивчайте та практикуйте GCP Hacking: HackTricks Training GCP Red Team Expert (GRTE)