Я написал тестовую программу для реализации чего-то похожего на самомодифицирующийся код в 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;
}
Я выделяю область памяти размером 4 КБ с помощью mmap, предоставляя разрешения на чтение, запись и выполнение. Затем я использую memcpy для записи двух ассемблерных инструкций в эту область памяти. После этого я инициализирую регистры x1~x3 с помощью встроенной сборки и разветвляю программный счетчик (pc) на ранее выделенную страницу. После ветвления последовательно выполните инструкции instr1 и instr2. Однако при разветвлении и доступе к области памяти программа прерывается с ошибкой EXC_BAD_ACCESS code=2.
Благодаря поиску в Google я понял, что проблема связана с кодовым дизайном Apple. Кажется, что доступ запрещен для кода, работающего в памяти на Apple Silicon, если он не имеет кодового обозначения. Итак, я гуглил, чтобы найти способ разрешить доступ через код. Однако мне не удалось найти способ кодирования памяти, выделенной через mmap, для разрешения доступа. Есть ли способ решить эту проблему?
(2) В ARM64 кэш инструкций и данных не унифицирован, поэтому вам придется очистить их после записи кода в память, прежде чем вы сможете его безопасно выполнить. См. stackoverflow.com/questions/70635862/…
(3) Вы переходите к своему коду, используя br, который не сохраняет обратный адрес в регистре ссылок, поэтому ret не вернет вас. Вы, наверное, хотели blr, но, как отмечалось выше, лучше использовать указатель на функцию и вызывать ее непосредственно из C, а не из asm.
(4) pthread_jit_write_protect_np(0); оставляет выполнение в состоянии отклонения, согласно keith.github.io/xcode-man-pages/…. Таким образом, вам, вероятно, придется вызвать его снова с помощью 1 после написания кода, прежде чем вы сможете его выполнить. Это также может позаботиться о очистке кэша, о которой я упоминал в (2); не уверен в этом.
Взгляните на developer.apple.com/documentation/apple-silicon/… . В нем объясняется, как решается проблема подписи кода, а также очистка кеша.
Да, и кстати: eor x0, x0, x0 это не лучший способ обнулить регистр на ARM. Мы делаем это на x86 по разным историческим причинам, но это не применимо к ARM. mov x0, #0 или mov x0, xzr вполне нормально. На самом деле eor x0, x0, x0 хуже, потому что у него ложная зависимость ввода.
Спасибо за хорошее объяснение. Благодаря этому мои знания об архитектуре ARM64 и процессорах Apple еще больше расширились.





Непосредственная проблема заключается в том, что вы звоните 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 байта указателя.
Теперь ваш шеллкод копируется правильно и фактически выполняется, но проблема с ret. Вы вызываете его с помощью br, поэтому x30 устарел и указывает на то место, куда вернулась последняя функция, которая, скорее всего, является вашей последней printf. Измените br на blr.
И потом - почему вы отображаете 0x400 байты, а затем добавляете 0xff4 к этому указателю? На практике это, скорее всего, не будет проблемой, поскольку страницы под Arm64 XNU имеют размер 16 КБ, но... почему? Вы могли бы, по крайней мере, ничего не добавлять к указателю, тогда подойдет любой размер, достаточно большой для хранения ваших инструкций.
А еще есть отсутствующие затирания регистров, которые, возможно, не вызывают проблем прямо сейчас, но рано или поздно это помешает коду, сгенерированному компилятором.
Это было решено благодаря четкому объяснению. Первоначальное намерение состояло в том, чтобы выделить память с помощью mmap с размером страницы, подходящим для операционной системы, а затем с помощью memcpy разместить две инструкции в конце страницы. Следующий код, кажется, соответствует моим намерениям. смелый instr = (uint8_t*)mmap(NULL, 0x4000, PROT_READ|PROT_EXEC|PROT_WRITE, MAP_PRIVATE|MAP_ANON|MAP_JIT, 0, 0) + 0x3ff8;
У вас есть некоторые другие ошибки, которые могут быть связаны, а могут и не быть связаны, но, безусловно, сами по себе могут вызвать сбои. (1) Ваша встроенная сборка не объявляет, что ее регистры сбиваются. В частности, вызовы функций из встроенного ассемблера проблематичны, поскольку они не только забивают регистры, но и красную зону , которую, AFAIK, невозможно объявить затертой. Если возможно, используйте вместо этого указатель на функцию.