Search code examples
macosreverse-engineeringlldbarm64mach-o

Modifying the mach-o executable file .text section to be writable caused a crash


;)

What I want to ask is why modifying the .text section in the mach-o executable file (ARM64) will cause runtime crashes?

I use some executable file editor (eg: MachOView) to do this。

before modifying, the .text section is read + execute:

enter image description here

after modifying, the .text section is read + writable + execute:

enter image description here

Can you see that in the ARM64 format mach-o executable file, I added a writable attribute to the .text section, but the program crashes immediately at runtime.

Debugging with lldb, error occurred at the main entry point, prompting EXC_ BAD_ ACCESS:

% lldb arm64
(lldb) target create "arm64"
Current executable set to '/Users/hopy/src/asm/arm64' (arm64).
(lldb) b main
Breakpoint 1: where = arm64`main, address = 0x0000000100003f10
(lldb) r
Process 2663 launched: '/Users/hopy/src/asm/arm64' (arm64)
Process 2663 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=2, address=0x100003f10)
    frame #0: 0x0000000100003f10 arm64`main
arm64`main:
->  0x100003f10 <+0>:  sub    sp, sp, #0x20
    0x100003f14 <+4>:  stp    x29, x30, [sp, #0x10]
    0x100003f18 <+8>:  add    x29, sp, #0x10
    0x100003f1c <+12>: ldr    x1, #0x8c                 ; my_data + 8
Target 0: (arm64) stopped.

Do I need to modify other parts of the mach-o file? If so, what further modifications are needed? Thank you very much! ;)

If the .text section cannot be made writable through editing tools, how can I make it writable and executable during compilation or linking (I have asm source code)?

Update:

The reason why I want to modify .text to be writable is that the executable file generated by the following code will call fixupPage64() when the system loads to fix my_data’s address(eg: 0x3fa8 to 100003fa8):

.text
    .globl _main
    .p2align 2
_main:
    func_constructor

    ldr x2,=my_data

    func_destructor
    ret
    .p2align 3
my_data:    .quad 0x12345678aabbccdd

Due to the fact that .text is not writable, it can cause the executable file to be killed, prompting an error of 'bus error'

So how to solve the problem of ldr=Label_Address causing executable files to crash during runtime?

Thanks!


Solution

  • You broke the code signature. If you only have the implicit signature that was inserted by the linker, you can re-sign the binary with:

    codesign -s - [your binary]
    

    But your change won't actually make a difference. On arm64 macOS, if a memory region has execute permission, then write permission will be removed from it. You cannot have rwx permissions on arm64 macOS. Even if you mmap() with MAP_JIT, you need to toggle between r-x and rw- with pthread_jit_write_protect_np().1

    If you want to patch code pages that aren't mapped with MAP_JIT, you need to:

    1. Reprotect the page to rw-.
    2. Write your changes.
    3. Flush the data cache.
    4. Reprotect the page to r-x.
    5. Invalidate the instruction cache.

    However, you cannot do this with any code page that you're currently running from, for obvious reasons (and you cannot flip JIT pages to rw- either if you're running from them).

    So in this case, you need to first remap the page somewhere else, then do the steps above, then remap it back over the original page, so that the change there is atomic.

    #include <stdio.h>
    #include <mach/mach.h>
    #include <mach/mach_vm.h>
    #include <libkern/OSCacheControl.h>
    
    unsigned int somefunc(void);
    
    __asm__
    (
        ".p2align 2\n"
        ".globl _somefunc\n"
        "_somefunc:\n"
        "   mov w0, 0x123\n"
        "   ret\n"
    );
    
    int main(void)
    {
        printf("Start\n");
    
        printf("somefunc: 0x%x\n", somefunc());
    
        // 1. Remap the page somewhere else
        mach_vm_address_t addr = (mach_vm_address_t)&somefunc;
        mach_vm_address_t remap;
        vm_prot_t cur, max;
        kern_return_t ret = mach_vm_remap(mach_task_self(), &remap, 0x8, 0, VM_FLAGS_ANYWHERE | VM_FLAGS_RETURN_DATA_ADDR, mach_task_self(), addr, FALSE, &cur, &max, VM_INHERIT_NONE);
        printf("mach_vm_remap: %s\n", mach_error_string(ret));
    
        // 2. Reprotect the page to rw- (needs VM_PROT_COPY because the max protection is currently r-x)
        ret = mach_vm_protect(mach_task_self(), remap, 0x8, FALSE, VM_PROT_READ | VM_PROT_WRITE | VM_PROT_COPY);
        printf("mach_vm_protect: %s\n", mach_error_string(ret));
    
        // 3. Write the changes
        ((uint32_t*)remap)[0] = 0x52800820; // mov w0, 0x41
        ((uint32_t*)remap)[1] = 0xd65f03c0; // ret
    
        // 4. Flush the data cache
        sys_dcache_flush((void*)addr, 0x8);
    
        // 5. Reprotect the page to r-x
        ret = mach_vm_protect(mach_task_self(), remap, 0x8, FALSE, VM_PROT_READ | VM_PROT_EXECUTE);
        printf("mach_vm_protect: %s\n", mach_error_string(ret));
    
        // 6. Invalidate the instruction cache
        sys_icache_invalidate((void*)addr, 0x8);
    
        // 7. Remap the page back over the original
        ret = mach_vm_remap(mach_task_self(), &addr, 0x8, 0, VM_FLAGS_OVERWRITE | VM_FLAGS_RETURN_DATA_ADDR, mach_task_self(), remap, FALSE, &cur, &max, VM_INHERIT_NONE);
        printf("mach_vm_remap: %s\n", mach_error_string(ret));
    
        printf("somefunc: 0x%x\n", somefunc());
    
        printf("End\n");
        return 0;
    }
    

    1 There is an exception to this: arm64 macOS in virtual machines powered by Virtualization.framework. Those don't get Apple Silicon ISA extensions and thus do have rwx JIT regions.