Две проблемы:
- разрешение exec на странице, , поскольку вы использовали массив, который будет помещен в раздел noexec для чтения + записи
.data
.
- ваш машинный код не заканчивается
ret
инструкцией , поэтому даже если он все-таки будет выполняться, выполнение вернется к следующему в памяти, а не к возврату.
И кстати, префикс REX полностью избыточен. "\x31\xc0"
xor eax,eax
имеет тот же эффект, что и xor rax,rax
.
Вам нужна страница с машинным кодом, чтобы иметь разрешение на выполнение . Таблицы страниц x86-64 имеют отдельный бит для выполнения, отличный от разрешения на чтение, в отличие от устаревших таблиц на 386 страниц.
Самый простой способ поместить статические массивы в память read + exec - это скомпилировать с gcc -z execstack
. (Делает стек и исполняемым разделом).
До недавнего времени (2018 или 2019) стандартный набор инструментов (binutils ld
) помещал секцию .rodata
в тот же сегмент ELF, что и .text
, так что они оба имели бы разрешение на чтение + exec. Таким образом, использования const char code[] = "...";
было достаточно для выполнения вручную указанных байтов в качестве данных.
Но в моей системе Arch Linux с GNU ld (GNU Binutils) 2.31.1
это уже не так. readelf -a
показывает, что секция .rodata
перешла в сегмент ELF с .eh_frame_hdr
и .eh_frame
и имеет только разрешение на чтение. .text
идет в сегмент с Read + Exec, а .data
идет в сегмент с Read + Write (вместе с .got
и .got.plt
). ( В чем разница раздела и сегмента в формате файла ELF )
#include <stdio.h>
// can be non-const if you use gcc -z execstack. static is also optional
static const char code[] = {
0x8D, 0x04, 0x37, // lea eax,[rdi+rsi] // retval = a+b;
0xC3 // ret
};
static const char ret0_code[] = "\x31\xc0\xc3"; // xor eax,eax ; ret
// the compiler will append a 0 byte to terminate the C string,
// but that's fine. It's after the ret.
int main () {
// void* cast is easier to type than a cast to function pointer,
// and in C can be assigned to any other pointer type. (not C++)
int (*sum) (int, int) = (void*)code;
int (*ret0)(void) = (void*)ret0_code;
// run code
int c = sum (2, 3);
return ret0();
}
В старых системах Linux: gcc -O3 shellcode.c && ./a.out
(работает из-за const
в глобальных / статических массивах)
В старых и текущих версиях Linux: gcc -O3 -z execstack shellcode.c && ./a.out
(работает из-за -zexecstack
независимо от того, где хранится ваш машинный код). Также работает с clang -z execstack
. gcc позволяет -zexecstack
без пробела, но clang - нет.
Они также работают в Windows, где данные только для чтения идут в .rdata
вместо .rodata
.
Сгенерированный компилятором main
выглядит следующим образом (из objdump -drwC -Mintel
). Вы можете запустить его внутри gdb
и установить точки останова на code
и ret0_code
(I actually used gcc -no-pie -O3 -zexecstack shellcode.c hence the addresses near 401000
0000000000401020 <main>:
401020: 48 83 ec 08 sub rsp,0x8 # stack aligned by 16 before a call
401024: be 03 00 00 00 mov esi,0x3
401029: bf 02 00 00 00 mov edi,0x2 # 2 args
40102e: e8 d5 0f 00 00 call 402008 <code> # note the target address in the next page
401033: 48 83 c4 08 add rsp,0x8
401037: e9 c8 0f 00 00 jmp 402004 <ret0_code> # optimized tailcall
Или используйте системные вызовы для изменения прав доступа к странице
Вместо компиляции с gcc -zexecstack
вы можете использовать mmap(PROT_EXEC)
для выделения новых исполняемых страниц или mprotect(PROT_EXEC)
для изменения существующих страниц на исполняемые. (Включая страницы, содержащие статические данные.) Вы также обычно хотите по крайней мере PROT_READ
, а иногда PROT_WRITE
, конечно.
Использование mprotect
для статического массива означает, что вы все еще выполняете код из известного местоположения, возможно, упрощая установку точки останова для него.
В Windows вы можете использовать VirtualAlloc или VirtualProtect.
В GNU C вам также необходимо использовать __builtin___clear_cache(buf, buf + len)
после записи байтов машинного кода в буфер, потому что оптимизатор не рассматривает разыменование указателя функции как чтение байтов с этого адреса. Удаление мертвого хранилища может удалить хранилища байтов машинного кода в буфер, если компилятор докажет, что хранилище ничего не читается как данные. https://codegolf.stackexchange.com/questions/160100/the-repetitive-byte-counter/160236#160236 и https://godbolt.org/g/pGXn3B имеет пример, где gcc действительно выполняет эту оптимизацию, потому что gcc "знает о" malloc
.
(А на архитектурах, отличных от x86, где I-кеш не согласован с D-кешем, он фактически выполняет любую необходимую синхронизацию кеша. На x86 это просто блокировщик оптимизации во время компиляции.)
Мое изменение в ответе @ AntoineMathys добавило это. В настоящее время gcc не знает о mmap
, поэтому он не оптимизирует хранилище до указателя, возвращаемого mmap
.
Но вам это не нужно после mprotect
на странице, содержащей переменные C, доступные только для чтения.
#include <stdio.h>
#include <sys/mman.h>
#include <stdint.h>
// can be non-const if you want, we're using mprotect
static const char code[] = {
0x8D, 0x04, 0x37, // lea eax,[rdi+rsi] // retval = a+b;
0xC3 // ret
};
static const char ret0_code[] = "\x31\xc0\xc3";
int main () {
// void* cast is easier to type than a cast to function pointer,
// and in C can be assigned to any other pointer type. (not C++)
int (*sum) (int, int) = (void*)code;
int (*ret0)(void) = (void*)ret0_code;
// hard-coding x86's 4k page size for simplicity.
// also assume that `code` doesn't span a page boundary and that ret0_code is in the same page.
uintptr_t page = (uintptr_t)code & -4095ULL; // round down
mprotect((void*)page, 4096, PROT_READ|PROT_EXEC|PROT_WRITE); // +write in case the page holds any writeable C vars that would crash later code.
// run code
int c = sum (2, 3);
return ret0();
}
Я использовал PROT_READ|PROT_EXEC|PROT_WRITE
в этом примере, поэтому он работает независимо от того, где находится ваша переменная. Если он был локальным в стеке и вы пропустили PROT_WRITE
, call
потерпит неудачу после того, как стек будет доступен только для чтения, когда он попытается отправить адрес возврата.
Кроме того, PROT_WRITE
позволяет вам тестировать шелл-код, который изменяется автоматически, например, редактировать нули в свой собственный машинный код или другие байты, которых он избегал.
$ gcc -O3 shellcode.c # without -z execstack
$ ./a.out
$ echo $?
0
$ strace ./a.out
...
mprotect(0x55605aa3f000, 4096, PROT_READ|PROT_WRITE|PROT_EXEC) = 0
exit_group(0) = ?
+++ exited with 0 +++
ЕслиЯ комментирую mprotect
, он делает segfault.
Если бы я сделал что-то вроде ret0_code[2] = 0xc3;
, мне понадобится __builtin___clear_cache(ret0_code+2, ret0_code+2)
после этого, чтобы убедиться, что магазин не был оптимизированнет, но если я не изменю статические массивы, то после mprotect
это не нужно.Это необходимо после mmap
+ memcpy
или ручного сохранения, потому что мы хотим выполнить байты, записанные в C (с memcpy
).