我在我的M1 MacBook Air上玩着longjmpsetjmp.在x86_64 Linux机器上,setjmp填充一个jmp_buf struct ,该 struct 具有一个包含"损坏的"寄存器值的long[].通过glibc代码,我能够对这些值进行解码以获得堆栈指针和帧指针.

根据lldb的说法,在我的M1 Mackbook Air上,这款jmp_buf似乎是int[37].我可以看到并打印这些值,但它们中没有一个与堆栈指针或帧指针匹配,尽管其中一些很接近.

我正在寻找如何解码MacOS M1 jmp_buf数组,并获得堆栈指针和帧指针.任何源代码都是受欢迎的.到目前为止,我已经浏览了glibc个目录,特别是sysdeps/aarch64目录(x86_64目录让我能够在我的Linux机器上解码),以及this mirror个苹果的开放源代码.jmp_buf个 struct 都不匹配,我无法确定是否发生了损坏/损坏.

我有:

#include <csetjmp>
#include <iostream>

int main() {

    jmp_buf reg;
    setjmp(reg);

    int foo = 5;
    std::cout << &foo << std::endl; // <--- Location on the stack, looking for something close to this

    for (auto int offset = 0; offset < 37; offset++) {
        std::cout << offset << ": "
                  << (void*)reg[offset]    // <--- Assumes registers are stored directly
                  << ", " 
                  << (void*)reinterpret_cast<long*>(reg)[offset]  // <--- Int array for some reason but registers are 64 bits, so maybe they're just next to each other?
                  << std::endl;
    }

    return 0;
}

它打印的内容如下:

% ./a.out                          
0x30c890358
0: 0xc6ac510, 0x10c6ac510
1: 0x1, 0x2918cc39c9b56814
2: 0xffffffffc9b56814, 0x2918cc39c9b56f44
3: 0x2918cc39, 0x10c6adc80
4: 0xffffffffc9b56f44, 0x30c890610
5: 0x2918cc39, 0x10c6adc60
6: 0xc6adc80, 0x1042221a0
7: 0x1, 0x2918cc3bc11e4ddb
8: 0xc890610, 0x1
9: 0x3, 0x37f00001f80
10: 0xc6adc60, 0x300000000
11: 0x1, 0x200000004
12: 0x42221a0, 0x10c6ac100
13: 0x1, 0x10c6ac100
14: 0xffffffffc11e4ddb, 0x100
15: 0x2918cc3b, 0x0
16: 0x1, 0x733d5f6c888800ad
17: 0x0, 0x10c6ac510
18: 0x1f80, 0x10c6adc60
19: 0x37f, 0x733d5f6c888800ad
20: 0x0, 0x30c8906a0
21: 0x3, 0x204580310
22: 0x4, 0x0
23: 0x2, 0x0
24: 0xc6ac100, 0x0
25: 0x1, 0x0
26: 0xc6ac100, 0x20461bde0
27: 0x1, 0x42000000
28: 0x100, 0x204580443
29: 0x0, 0x204612010
30: 0x0, 0x30c890490
31: 0x0, 0x20457a000
32: 0xffffffff888800ad, 0x20457a000
33: 0x733d5f6c, 0x20461bde0
34: 0xc6ac510, 0x40000000
35: 0x1, 0x20458049d
36: 0xc6adc60, 0x204612040

我期望在jmp_buf数组中有一个值,它距离地址foo只有几个字节.当将偏移量#4解释为long的数组时,偏移量接近,但比我预期的要远.

我正在寻找偏移量的定义和任何需要发生的值的脱离Angular .

推荐答案

哦,这在很多层面上都是devious.

首先:您的代码在Rosetta中运行.

我有根据的猜测是,您正在运行一个IDE(vs Code?)或一个终端仿真器x86_64,您将从其中调用编译器,然后编译器也将以x86_64运行,没有显式的目标ARCH标志,这将使其默认为x86_64.使用-arch arm64cc/c++/gcc/g++/clang/clang++显式地以arm64为目标,或者在编译器调用前面加上arch -arm64 [...]以本机运行整个进程层次 struct .

现在,我如何确定您的代码是在Rosetta下运行的?这就是苹果所说的"指针吞噬".因此,官方的Apple源代码转储发生在github.com/apple-oss-distributions上,setjmplongjmpsrc/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

因此,寄存器fplrsp存储在偏移量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_longjmpreal个实现:

;-- __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是不同的.setjmppacibsp,然后longjmp做相应的retab.到目前为止还不错,除了fpsp还有它们的pacdbautdb,这应该会添加同样的指针身份验证.但这里还有一个小细节:如果您在支持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为目标,汇编程序不会接受指令xpacixpacd,但使用.arch v8.3a指令,我们可以说服它让我们通过.

注2:由于xpacixpacd指令,此代码只能在ARMv8.3硬件上运行.如果您的代码可能运行在不支持PAC的硬件上(例如A12之前的iOS设备),那么您需要更改这些硬件.对于xpaci,可以构造一个与xpaclri向后兼容的变体,它是在历史NOP指令空间(see another answer of mine)中编码的,但是对于xpacd,在NOP空间中没有类似功能,所以您必须做的是首先确定您在哪种硬件上运行(通过类似xpaclri的东西),然后有条件地调用指针验证小工具.但这超出了这个答案的范围.:)

C++相关问答推荐

segfault在C中使用getline()函数

getchar读css + z还是返回css?

*p[num]和(*p)num的区别

在c++中使用堆栈的有效括号

在列表中插入Int指针(C)

试图从CSV文件中获取双精度值,但在C++中始终为空

CC2538裸机项目编译但不起作用

getline()从c中的外部函数传递指针时输出null

有什么方法可以将字符串与我们 Select 的子字符串分开吗?喜欢:SIN(LOG(10))

在C中创建任意类型的只读指针参数

将回调/基于事件的C API转换为非回调API

如何在VSCode中创建和使用我自己的C库?

Valgrind用net_pton()抱怨

try 查找带有指针的数组的最小值和最大值

unions 的原子成员是个好主意吗?

在文件描述符上设置FD_CLOEXEC与将其传递给POSIX_SPOWN_FILE_ACTIONS_ADCLOSE有区别吗?

try 判断长整数是否为素数

';malloc():损坏的顶部大小';分配超过20万整数后

未使用sem_open正确初始化信号量

System V 消息队列由于某种原因定期重置