Как разрешить EXC_BAD_ACCESS (код = 2) при доступе к выделенной исполняемой странице mmap? на Apple Silicon (MacOS и M1 Mac Mini)

Я написал тестовую программу для реализации чего-то похожего на самомодифицирующийся код в 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, для разрешения доступа. Есть ли способ решить эту проблему?

У вас есть некоторые другие ошибки, которые могут быть связаны, а могут и не быть связаны, но, безусловно, сами по себе могут вызвать сбои. (1) Ваша встроенная сборка не объявляет, что ее регистры сбиваются. В частности, вызовы функций из встроенного ассемблера проблематичны, поскольку они не только забивают регистры, но и красную зону , которую, AFAIK, невозможно объявить затертой. Если возможно, используйте вместо этого указатель на функцию.

Nate Eldredge 09.05.2024 18:53

(2) В ARM64 кэш инструкций и данных не унифицирован, поэтому вам придется очистить их после записи кода в память, прежде чем вы сможете его безопасно выполнить. См. stackoverflow.com/questions/70635862/…

Nate Eldredge 09.05.2024 18:55

(3) Вы переходите к своему коду, используя br, который не сохраняет обратный адрес в регистре ссылок, поэтому ret не вернет вас. Вы, наверное, хотели blr, но, как отмечалось выше, лучше использовать указатель на функцию и вызывать ее непосредственно из C, а не из asm.

Nate Eldredge 09.05.2024 18:56

(4) pthread_jit_write_protect_np(0); оставляет выполнение в состоянии отклонения, согласно keith.github.io/xcode-man-pages/…. Таким образом, вам, вероятно, придется вызвать его снова с помощью 1 после написания кода, прежде чем вы сможете его выполнить. Это также может позаботиться о очистке кэша, о которой я упоминал в (2); не уверен в этом.

Nate Eldredge 09.05.2024 18:59

Взгляните на developer.apple.com/documentation/apple-silicon/… . В нем объясняется, как решается проблема подписи кода, а также очистка кеша.

Nate Eldredge 09.05.2024 19:01

Да, и кстати: eor x0, x0, x0 это не лучший способ обнулить регистр на ARM. Мы делаем это на x86 по разным историческим причинам, но это не применимо к ARM. mov x0, #0 или mov x0, xzr вполне нормально. На самом деле eor x0, x0, x0 хуже, потому что у него ложная зависимость ввода.

Nate Eldredge 09.05.2024 19:04

Спасибо за хорошее объяснение. Благодаря этому мои знания об архитектуре ARM64 и процессорах Apple еще больше расширились.

purple_potato 10.05.2024 10:55
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
7
68
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Непосредственная проблема заключается в том, что вы звоните 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;

purple_potato 10.05.2024 10:51

Другие вопросы по теме