我编写了一个测试程序来实现类似于 Apple Silicon 上的自修改代码的功能。
int main() {
uint8_t *instr;
uint32_t instr1 = 0x8b000000; // add x0, x0, x0
uint32_t instr2 = 0xd65f03c0; // ret
if(pthread_jit_write_protect_supported_np() == 1)
printf("jit write supported\n");
else
printf("jit write not supported\n");
pthread_jit_write_protect_np(1);
instr = (uint8_t*)mmap(NULL, 1024, PROT_READ|PROT_EXEC|PROT_WRITE, MAP_PRIVATE|MAP_ANON|MAP_JIT, 0, 0) + 4084;
pthread_jit_write_protect_np(0);
if(instr == MAP_FAILED){
perror("mmap");
exit(-1);
}
printf("instr addr : %lx\n", (uintptr_t)instr);
memcpy(instr, &instr, 4);
memcpy(instr+4, &instr2, 4);
printf("instr1 is %x\n", *(uint32_t *)instr);
printf("instr2 is %x\n", *(uint32_t *)(instr+4));
asm volatile(
"eor x0, x0, x0\n"
"eor x1, x1, x1\n"
"eor x2, x2, x2\n"
"eor x3, x3, x3\n"
);
asm volatile(
"ldr x1, %[ptr]\n"
"br x1\n"
::[ptr]"m"(instr)
);
return 0;
}
我用mmap分配了一个4KB的内存区域,允许读、写和执行权限。然后,我使用 memcpy 将两条汇编指令写入该内存区域。之后,我使用内联汇编初始化寄存器 x1~x3 并将程序计数器 (pc) 分支到先前分配的页面。分支后,依次执行指令instr1和instr2。但是,当分支和访问内存区域时,程序会因 EXC_BAD_ACCESS code=2 错误而中止。
通过Google搜索,我开始意识到问题出在Apple的协同设计上。如果 Apple Silicon 内存中运行的代码未经协同设计,则访问似乎会被拒绝。因此,我一直在谷歌上寻找一种允许通过协同设计进行访问的方法。但是,我一直无法找到一种方法来对通过 mmap 分配的内存进行协同设计以允许访问。有什么办法可以解决这个问题吗?
直接的问题是,您调用
pthread_jit_write_protect_np(0)
并且永远不会将其翻转回 1
。这会使您的线程处于 JIT 页面为 rw-
的状态,因此尝试从那里执行将会出错。在 pthread_jit_write_protect_np(1)
之后拨打 memcpy()
。
下一个问题是你使用
ldr x1, %[ptr]
。这会从指针加载 x1
,而不是将指针移动到
x1
,因此
x1
将是您写入的 8 个字节。将其替换为
mov x1, %[ptr]
并将
"m"
更改为
"r"
。然后是缓存。导入
<libkern/OSCacheControl.h>
并在最终调用
sys_dcache_flush(instr, 0x8)
之前执行
pthread_jit_write_protect_np(1)
,然后执行
sys_icache_invalidate(instr, 0x8)
。还有一个问题是你写错了:
memcpy(instr, &instr, 4);
您的意思是在这里获取instr1
的地址,而不是
instr
。您当前正在写入指针的低 4 个字节。现在您的 shellcode 已正确复制并实际执行,但
ret
是一个问题。您使用
br
调用它,因此
x30
已过时,并指向最后一个函数返回的位置,这很可能是您的最后一个
printf
。将
br
更改为
blr
。然后 - 为什么要映射
0x400
字节,然后将
0xff4
添加到该指针?实际上这可能不是问题,因为arm64 XNU下的页面是16KiB,但只是......为什么?您至少可以不向指针添加任何内容,然后任何足够大的尺寸来容纳您的指令就可以了。还有缺少的寄存器破坏者,它现在可能不会引起问题,但迟早会干扰编译器生成的代码。