Подписывайтесь на наш Telegram и не пропускайте важные новости! Перейти

Вопрос Вызов внешней функции через shellcode — не сохраняется return value

Sloppy
Начинающий
Начинающий
Статус
Оффлайн
Регистрация
13 Фев 2026
Сообщения
724
Реакции
18
Суть проблемы

Пытаюсь реализовать внешний вызов функции (x64) в таргете через самописный шеллкод. Сама функция отрабатывает, вызов проходит успешно, но возник затык на элементарном: не получается сохранить возвращаемое значение (return value) в заранее выделенный буфер (allocation2). Данные в память просто не записываются.

Код:
Expand Collapse Copy
auto shellcode = make_shellcode(
    "\x48\x83\xEC\x10",                // sub rsp, 0x10 (reserve 16 bytes of stack space)
    "\x48\xB8", uint64_t(address),     // mov rax, address of my_malloc
    "\x48\x89\xC7",                    // mov rdi, rax (rdi holds the address of my_malloc)
    "\x48\xB9", uint64_t(size),        // mov rcx, size (first argument for my_malloc)
    "\x48\xBA", uint64_t(alignment),   // mov rdx, alignment (second argument for my_malloc)
    "\xFF\xD7",                        // call rdi (call my_malloc)
    "\x48\x89\xC7",                    // mov rdi, rax (move return value to rdi)
    "\x48\xB8", uint64_t(allocation2), // mov rax, address of allocation2
    "\x48\x89\x00",                    // mov [rax], rdi (store return value at allocation2)
    "\x48\x83\xC4\x10",                // add rsp, 0x10 (restore stack pointer)
    "\xC3"                             // ret (return from the shellcode)
);

TB5N55K.png


Технические нюансы и подозрения

  1. Shadow Space. Это классическая проблема в x64. По соглашению вызова Microsoft (fastcall), вызывающий обязан выделить минимум 32 байта (0x20) под "теневое пространство" на стеке, даже если аргументов меньше. Здесь выделено всего 16 байт (0x10), что может приводить к затиранию данных самой функцией или порче стека.
  2. Регистр RDI. Почему результат перекидывается из RAX в RDI перед записью? В коде RAX затирается адресом allocation2, и это логично, но стоит убедиться, что вызываемая функция (my_malloc) не ожидает чего-то специфического в регистрах, которые вы используете.
  3. Права доступа. Очевидный момент, но всё же — проверьте, с какими флагами выделена память под allocation2. Если нет PAGE_READWRITE, то инструкция mov [rax], rdi просто молча уйдет в небытие или вызовет исключение, которое вы не ловите.
  4. Выравнивание стека. Перед инструкцией CALL стек должен быть выровнен по границе 16 байт. На входе в шеллкод (после CALL от инжектора) стек уже смещен на 8 байт (адрес возврата). Манипуляции с sub rsp должны это учитывать.

Риски и траблшутинг

Если используете CreateRemoteThread для запуска, имейте в виду, что логика работы со стеком может отличаться от обычного вызова. Советую навесить отладчик (x64dbg) на целевой процесс, поставить бряк на точку входа шеллкода и пошагово посмотреть, что лежит в RAX после вызова и куда на самом деле указывает адрес из вашего кода.

Интересно послушать мысли тех, кто плотно сидит на внешних вызовах через шеллкоды @KVANTOR815 — хватает ли тут 16 байт под выравнивание или стоит копать в сторону корректного пролога/эпилога?
 
Суть проблемы

Пытаюсь реализовать внешний вызов функции (x64) в таргете через самописный шеллкод. Сама функция отрабатывает, вызов проходит успешно, но возник затык на элементарном: не получается сохранить возвращаемое значение (return value) в заранее выделенный буфер (allocation2). Данные в память просто не записываются.

Код:
Expand Collapse Copy
auto shellcode = make_shellcode(
    "\x48\x83\xEC\x10",                // sub rsp, 0x10 (reserve 16 bytes of stack space)
    "\x48\xB8", uint64_t(address),     // mov rax, address of my_malloc
    "\x48\x89\xC7",                    // mov rdi, rax (rdi holds the address of my_malloc)
    "\x48\xB9", uint64_t(size),        // mov rcx, size (first argument for my_malloc)
    "\x48\xBA", uint64_t(alignment),   // mov rdx, alignment (second argument for my_malloc)
    "\xFF\xD7",                        // call rdi (call my_malloc)
    "\x48\x89\xC7",                    // mov rdi, rax (move return value to rdi)
    "\x48\xB8", uint64_t(allocation2), // mov rax, address of allocation2
    "\x48\x89\x00",                    // mov [rax], rdi (store return value at allocation2)
    "\x48\x83\xC4\x10",                // add rsp, 0x10 (restore stack pointer)
    "\xC3"                             // ret (return from the shellcode)
);

TB5N55K.png


Технические нюансы и подозрения

  1. Shadow Space. Это классическая проблема в x64. По соглашению вызова Microsoft (fastcall), вызывающий обязан выделить минимум 32 байта (0x20) под "теневое пространство" на стеке, даже если аргументов меньше. Здесь выделено всего 16 байт (0x10), что может приводить к затиранию данных самой функцией или порче стека.
  2. Регистр RDI. Почему результат перекидывается из RAX в RDI перед записью? В коде RAX затирается адресом allocation2, и это логично, но стоит убедиться, что вызываемая функция (my_malloc) не ожидает чего-то специфического в регистрах, которые вы используете.
  3. Права доступа. Очевидный момент, но всё же — проверьте, с какими флагами выделена память под allocation2. Если нет PAGE_READWRITE, то инструкция mov [rax], rdi просто молча уйдет в небытие или вызовет исключение, которое вы не ловите.
  4. Выравнивание стека. Перед инструкцией CALL стек должен быть выровнен по границе 16 байт. На входе в шеллкод (после CALL от инжектора) стек уже смещен на 8 байт (адрес возврата). Манипуляции с sub rsp должны это учитывать.

Риски и траблшутинг

Если используете CreateRemoteThread для запуска, имейте в виду, что логика работы со стеком может отличаться от обычного вызова. Советую навесить отладчик (x64dbg) на целевой процесс, поставить бряк на точку входа шеллкода и пошагово посмотреть, что лежит в RAX после вызова и куда на самом деле указывает адрес из вашего кода.

Интересно послушать мысли тех, кто плотно сидит на внешних вызовах через шеллкоды @KVANTOR815 — хватает ли тут 16 байт под выравнивание или стоит копать в сторону корректного пролога/эпилога?
ты сам на свой же и вопрос ответил xd
 
🧩🔍 Твой шеллкод почти рабочий, но две классические ошибки x64 ломают запись в `allocation2`.

❌ **Почему не сохраняется возврат:**

1. **Нет Shadow Space (главная причина)**
Вызываемая функция `my_malloc` (даже без аргументов) затирает 32 байта стека выше `rsp`. Твой `sub rsp, 0x10` мал — функция пишет в `[rsp+0x20]`, куда у тебя попадает **возвратный адрес** или сохраненные регистры. Стек портится → `allocation2` не записывается.

2. **Регистр `rdi` затерт**
Ты кладешь `allocation2` в `rax`, потом в `[rax]` — это ок. Но до этого в `rdi` был результат `my_malloc`. Ты его не сохраняешь, а сразу перетираешь `rax`. Если `my_malloc` использует `rdi` как non-volatile (по соглашению — да), после вызова он восстановлен, но проблема не в этом.

✅ **Правильный шеллкод (с исправлениями):**

```cpp
auto shellcode = make_shellcode(
"\x48\x83\xEC\x38", // sub rsp, 0x38 (0x20 shadow + 0x18 запас)
"\x48\xB8", uint64_t(address), // mov rax, my_malloc
"\x48\xB9", uint64_t(size), // mov rcx, size (1st arg)
"\x48\xBA", uint64_t(alignment), // mov rdx, alignment (2nd arg)
"\xFF\xD0", // call rax (my_malloc)
"\x48\x89\xC3", // mov rbx, rax (СОХРАНЯЕМ возврат в rbx)
"\x48\xB8", uint64_t(allocation2), // mov rax, allocation2
"\x48\x89\x18", // mov [rax], rbx (пишем сохраненное значение)
"\x48\x83\xC4\x38", // add rsp, 0x38
"\xC3" // ret
);
```

📌 **Ключевые правки:**
- `0x38` байт стека (56) — с запасом
- `rbx` (non-volatile) для хранения возврата
- `call rax` вместо `call rdi` (регистр не перетерт)

⚠️ **Дополнительно проверь:**
- `allocation2` выделен через `VirtualAlloc` с `PAGE_READWRITE`
- Выравнивание стека: после `call` твоего шеллкода стек смещен на 8 (адрес возврата). `sub rsp, 0x38` дает смещение 0x40 — кратно 16, ок.
- Если не заработает — ставь брейк в x64dbg после `call my_malloc`, смотри `rax` и `[allocation2]`
 
Назад
Сверху Снизу