哦,这在很多层面上都是devious.
首先:您的代码在Rosetta中运行.
我有根据的猜测是,您正在运行一个IDE(vs Code?)或一个终端仿真器x86_64,您将从其中调用编译器,然后编译器也将以x86_64运行,没有显式的目标ARCH标志,这将使其默认为x86_64.使用-arch arm64
到cc
/c++
/gcc
/g++
/clang
/clang++
显式地以arm64为目标,或者在编译器调用前面加上arch -arm64 [...]
以本机运行整个进程层次 struct .
现在,我如何确定您的代码是在Rosetta下运行的?这就是苹果所说的"指针吞噬".因此,官方的Apple源代码转储发生在github.com/apple-oss-distributions上,setjmp
和longjmp
在src/setjmp
in libplatform上实现,每个架构都有手工滚动的汇编实现.ARM 64实现如下:
ENTRY_POINT(__longjmp)
ldp x19, x20, [x0, JMP_r19_20]
ldp x21, x22, [x0, JMP_r21_22]
ldp x23, x24, [x0, JMP_r23_24]
ldp x25, x26, [x0, JMP_r25_26]
ldp x27, x28, [x0, JMP_r27_28]
ldp x10, x11, [x0, JMP_fp_lr]
ldr x12, [x0, JMP_sp_rsvd]
ldp d8, d9, [x0, JMP_d8_d9]
ldp d10, d11, [x0, JMP_d10_d11]
ldp d12, d13, [x0, JMP_d12_d13]
ldp d14, d15, [x0, JMP_d14_d15]
_OS_PTR_MUNGE_TOKEN(x16, x16)
_OS_PTR_UNMUNGE(fp, x10, x16)
_OS_PTR_UNMUNGE(lr, x11, x16)
_OS_PTR_UNMUNGE(x12, x12, x16)
ldrb w16, [sp] /* probe to detect absolutely corrupt stack pointers */
mov sp, x12
cmp w1, #0
csinc w0, w1, wzr, ne
ret
它大量使用宏,所以下面是从/usr/lib/system/libsystem_platform.dylib
到_longjmp
的原始反汇编:
;-- __longjmp:
0x00001d68 135040a9 ldp x19, x20, [x0]
0x00001d6c 155841a9 ldp x21, x22, [x0, 0x10]
0x00001d70 176042a9 ldp x23, x24, [x0, 0x20]
0x00001d74 196843a9 ldp x25, x26, [x0, 0x30]
0x00001d78 1b7044a9 ldp x27, x28, [x0, 0x40]
0x00001d7c 0a2c45a9 ldp x10, x11, [x0, 0x50]
0x00001d80 0c3040f9 ldr x12, [x0, 0x60]
0x00001d84 0824476d ldp d8, d9, [x0, 0x70]
0x00001d88 0a2c486d ldp d10, d11, [x0, 0x80]
0x00001d8c 0c34496d ldp d12, d13, [x0, 0x90]
0x00001d90 0e3c4a6d ldp d14, d15, [x0, 0xa0]
0x00001d94 70d03bd5 mrs x16, tpidrro_el0
0x00001d98 101e40f9 ldr x16, [x16, 0x38]
0x00001d9c 5d0110ca eor x29, x10, x16
0x00001da0 7e0110ca eor x30, x11, x16
0x00001da4 8c0110ca eor x12, x12, x16
0x00001da8 f0034039 ldrb w16, [sp]
0x00001dac 9f010091 mov sp, x12
0x00001db0 3f000071 cmp w1, 0
0x00001db4 20149f1a csinc w0, w1, wzr, ne
0x00001db8 c0035fd6 ret
因此,寄存器fp
、lr
和sp
存储在偏移量0x50、0x58和0x60处,但它们也与从[tpidrro_el0, 0x38]
加载的值进行异或运算.这MUNGE
个宏的定义可以在xnu/libsyscall/os/tsd.h
中找到,但它们实际上也不会告诉您超过[tpidrro_el0, 0x38]
个宏.它只是将每个进程的cookie异或到这些值中.如果您的代码在ARM64上运行,则如下所示:
0x16b9bb0f0
0: 0x4446fbc, 0x104446fbc
1: 0x1, 0x104450000
2: 0x4450000, 0x104451910
3: 0x1, 0x16b9bb2e0
4: 0x4451910, 0x1a91ea396
5: 0x1, 0x16b9bb260
6: 0x6b9bb2e0, 0x1
7: 0x1, 0x0
8: 0xffffffffa91ea396, 0x0
9: 0x1, 0x0
10: 0x6b9bb260, 0x6f5a9a069b197d28
11: 0x1, 0x2a649a06f4c6a30c
12: 0x1, 0x6f5a9a069b197c08
13: 0x0, 0x0
14: 0x0, 0x0
15: 0x0, 0x0
16: 0x0, 0x0
17: 0x0, 0x0
18: 0x0, 0x0
19: 0x0, 0x0
20: 0xffffffff9b197d28, 0x0
21: 0x6f5a9a06, 0x0
22: 0xfffffffff4c6a30c, 0x100000000
23: 0x2a649a06, 0x104450000
24: 0xffffffff9b197c08, 0x31232f62314200ab
25: 0x6f5a9a06, 0x16b9bb430
26: 0x0, 0x1a916ff28
27: 0x0, 0x0
28: 0x0, 0x0
29: 0x0, 0x0
30: 0x0, 0x1045dddd8
31: 0x0, 0x40000000
32: 0x0, 0x10454a0c0
33: 0x0, 0x1045d40b0
34: 0x0, 0x104544000
35: 0x0, 0x1045dddd8
36: 0x0, 0x42000000
注意到偏移量10和12的高位是如何相同的吗?这是因为这些寄存器中的高位在用户区域中通常为零,所以如果您将64位常量异或到它们中,高位将是相同的.这完全不是我在你的jmp_buf
个垃圾场看到的.您在这些指数上的值看起来更像位掩码.然而,我do看到的是索引1和2.这正是x86_64实现的工作方式:
;-- __longjmp:
0x00003d2c dbe3 fninit
0x00003d2e 85f6 test esi, esi
0x00003d30 b801000000 mov eax, 1
0x00003d35 0f45c6 cmovne eax, esi
0x00003d38 488b1f mov rbx, qword [rdi]
0x00003d3b 488b7708 mov rsi, qword [rdi + 8]
0x00003d3f 654833342538. xor rsi, qword gs:[0x38]
0x00003d48 4889f5 mov rbp, rsi
0x00003d4b 488b7710 mov rsi, qword [rdi + 0x10]
0x00003d4f 654833342538. xor rsi, qword gs:[0x38]
0x00003d58 4c0fbe26 movsx r12, byte [rsi]
0x00003d5c 4889f4 mov rsp, rsi
0x00003d5f 4c8b6718 mov r12, qword [rdi + 0x18]
0x00003d63 4c8b6f20 mov r13, qword [rdi + 0x20]
0x00003d67 4c8b7728 mov r14, qword [rdi + 0x28]
0x00003d6b 4c8b7f30 mov r15, qword [rdi + 0x30]
0x00003d6f 488b7738 mov rsi, qword [rdi + 0x38]
0x00003d73 654833342538. xor rsi, qword gs:[0x38]
0x00003d7c d96f4c fldcw word [rdi + 0x4c]
0x00003d7f 0fae5748 ldmxcsr dword [rdi + 0x48]
0x00003d83 fc cld
0x00003d84 ffe6 jmp rsi
所以,是的,我就是这么知道的.但回到上面的arm64转储,如果我们查看程序集,那么我们不仅希望索引10和12具有相同的高位,而且还希望索引11(lr
)具有相同的高位,但情况并非如此.那到底是怎么回事?
事实证明,我们运行的也不是真正的arm64版本.我们跑了arm64e英里!如果这对你没有任何意义,它是一个单独的苹果ABI,支持ARMv8.3指针身份验证.如果硬件支持,Mach-O加载器总是更喜欢arm64e片,而不是arm64,因为所有Apple Silicon Mac都支持它,而且由于所有库存的二进制文件都附带了arm64e片,libsystem_platform.dylib
总是会加载它的arm64e片(除非您设法手动处理足够多的东西,也许?).无论哪种方式,下面是实际运行的_setjmp
和_longjmp
的real个实现:
;-- __setjmp:
0x00001a54 7f2303d5 pacibsp
0x00001a58 ea031daa mov x10, x29
0x00001a5c ea0fc1da pacdb x10, sp
0x00001a60 ec030091 mov x12, sp
0x00001a64 a97d9952 mov w9, 0xcbed
0x00001a68 2c0dc1da pacdb x12, x9
0x00001a6c 70d03bd5 mrs x16, tpidrro_el0
0x00001a70 101e40f9 ldr x16, [x16, 0x38]
0x00001a74 4a0110ca eor x10, x10, x16
0x00001a78 cb0310ca eor x11, x30, x16
0x00001a7c 8c0110ca eor x12, x12, x16
0x00001a80 135000a9 stp x19, x20, [x0]
0x00001a84 155801a9 stp x21, x22, [x0, 0x10]
0x00001a88 176002a9 stp x23, x24, [x0, 0x20]
0x00001a8c 196803a9 stp x25, x26, [x0, 0x30]
0x00001a90 1b7004a9 stp x27, x28, [x0, 0x40]
0x00001a94 0a2c05a9 stp x10, x11, [x0, 0x50]
0x00001a98 0c3000f9 str x12, [x0, 0x60]
0x00001a9c 0824076d stp d8, d9, [x0, 0x70]
0x00001aa0 0a2c086d stp d10, d11, [x0, 0x80]
0x00001aa4 0c34096d stp d12, d13, [x0, 0x90]
0x00001aa8 0e3c0a6d stp d14, d15, [x0, 0xa0]
0x00001aac 00008052 mov w0, 0
0x00001ab0 ff0f5fd6 retab
;-- __longjmp:
0x00001ab4 135040a9 ldp x19, x20, [x0]
0x00001ab8 155841a9 ldp x21, x22, [x0, 0x10]
0x00001abc 176042a9 ldp x23, x24, [x0, 0x20]
0x00001ac0 196843a9 ldp x25, x26, [x0, 0x30]
0x00001ac4 1b7044a9 ldp x27, x28, [x0, 0x40]
0x00001ac8 0a2c45a9 ldp x10, x11, [x0, 0x50]
0x00001acc 0c3040f9 ldr x12, [x0, 0x60]
0x00001ad0 0824476d ldp d8, d9, [x0, 0x70]
0x00001ad4 0a2c486d ldp d10, d11, [x0, 0x80]
0x00001ad8 0c34496d ldp d12, d13, [x0, 0x90]
0x00001adc 0e3c4a6d ldp d14, d15, [x0, 0xa0]
0x00001ae0 70d03bd5 mrs x16, tpidrro_el0
0x00001ae4 101e40f9 ldr x16, [x16, 0x38]
0x00001ae8 4a0110ca eor x10, x10, x16
0x00001aec 7e0110ca eor x30, x11, x16
0x00001af0 8c0110ca eor x12, x12, x16
0x00001af4 a97d9952 mov w9, 0xcbed
0x00001af8 2c1dc1da autdb x12, x9
0x00001afc 9f0140f9 ldr xzr, [x12]
0x00001b00 9f010091 mov sp, x12
0x00001b04 ea1fc1da autdb x10, sp
0x00001b08 fd030aaa mov x29, x10
0x00001b0c 3f000071 cmp w1, 0
0x00001b10 20149f1a csinc w0, w1, wzr, ne
0x00001b14 ff0f5fd6 retab
在这一点上,这似乎根本不是开源的.如果要我猜的话,它可能也不是稳定的ABI.整个arm64e子架构被认为是不稳定的,可能会在没有通知的情况下发生变化,并且在MacOS上需要-arm64e_preview_abi
内核启动-arg(这反过来需要降级的操作系统安全)才能允许您运行非Apple签名的arm64e二进制文件.所以,是的,只是不要依赖于这个代码保持不变.
但好吧,由于指针身份验证,lr
是不同的.setjmp
做pacibsp
,然后longjmp
做相应的retab
.到目前为止还不错,除了fp
和sp
还有它们的pacdb
和autdb
,这应该会添加同样的指针身份验证.但这里还有一个小细节:如果您在支持arm64e的硬件上运行arm64二进制文件,那么您的进程中将加载arm64e库,但您仍然是以arm64身份运行.通过SCTLR_EL1
中的硬件标志为您的进程关闭指针身份验证指令,但IB
键除外.因此,pacib*
系列指令可以工作,而pacia*
、pacda*
和pacdb*
系列指令不能.但当您自己读取fp
/lr
/sp
值时,基本上您会想要剥离指针验证位,并让硬件决定这是否为NOP.
下面是在ARM64和MacOS 13.4.1上打印这三个寄存器的代码,不保证向前或向后兼容:
#include <setjmp.h>
#include <stdint.h>
#include <stdio.h>
/* ---------- imported from xnu/libsyscall/tsd ---------- */
#define __TSD_PTR_MUNGE 7
__attribute__((always_inline, const)) static __inline__ void** _os_tsd_get_base(void)
{
#if defined(__arm__)
uintptr_t tsd;
__asm__("mrc p15, 0, %0, c13, c0, 3\n"
"bic %0, %0, #0x3\n" : "=r" (tsd));
/* lower 2-bits contain CPU number */
#elif defined(__arm64__)
/*
* <rdar://73762648> Do not use __builtin_arm_rsr64("TPIDRRO_EL0")
* so that the "const" attribute takes effect and repeated use
* is coalesced properly.
*/
uint64_t tsd;
__asm__ ("mrs %0, TPIDRRO_EL0" : "=r" (tsd));
#endif
return (void**)(uintptr_t)tsd;
}
__attribute__((always_inline)) static __inline__ void* _os_tsd_get_direct(unsigned long slot)
{
return _os_tsd_get_base()[slot];
}
__attribute__((always_inline, const)) static __inline__ uintptr_t _os_ptr_munge_token(void)
{
return (uintptr_t)_os_tsd_get_direct(__TSD_PTR_MUNGE);
}
/* ---------- end of import ---------- */
static inline uint64_t xpaci(uint64_t val)
{
__asm__ volatile
(
".arch v8.3a\n"
"xpaci %0\n"
: "+r" (val)
);
return val;
}
static inline uint64_t xpacd(uint64_t val)
{
__asm__ volatile
(
".arch v8.3a\n"
"xpacd %0\n"
: "+r" (val)
);
return val;
}
int main(void)
{
jmp_buf reg;
setjmp(reg);
uintptr_t token = _os_ptr_munge_token();
uint64_t *u64 = (uint64_t*)reg;
uint64_t fp = u64[10] ^ token,
lr = u64[11] ^ token,
sp = u64[12] ^ token;
printf("fp: 0x%llx\n", xpacd(fp));
printf("lr: 0x%llx\n", xpaci(lr));
printf("sp: 0x%llx\n", xpacd(sp));
return 0;
}
注1:通常情况下,如果以arm64为目标,汇编程序不会接受指令xpaci
和xpacd
,但使用.arch v8.3a
指令,我们可以说服它让我们通过.
注2:由于xpaci
和xpacd
指令,此代码只能在ARMv8.3硬件上运行.如果您的代码可能运行在不支持PAC的硬件上(例如A12之前的iOS设备),那么您需要更改这些硬件.对于xpaci
,可以构造一个与xpaclri
向后兼容的变体,它是在历史NOP指令空间(see another answer of mine)中编码的,但是对于xpacd
,在NOP空间中没有类似功能,所以您必须做的是首先确定您在哪种硬件上运行(通过类似xpaclri
的东西),然后有条件地调用指针验证小工具.但这超出了这个答案的范围.:)