Недавно я начал изучать управление памятью и прочитал об относительных адресах и физических адресах, и у меня возник вопрос:

Когда я печатаю адрес переменной, показывает ли он относительный (виртуальный) адрес или физический адрес, где переменная находится в памяти?

И еще один вопрос, касающийся управления памятью:

Почему этот код создает одно и то же значение указателя стека для каждого прогона (из Руководства Shellcoder , стр. 28)? Любая программа, которую я запускаю, производит этот адрес?

// find_start.c
unsigned long find_start(void)
{
    __asm__("movl %esp, %eax");
}
int main()
{
    printf("0x%x\n",find_start());
}

Если мы скомпилируем это и запустим несколько раз, мы получим:

shellcoders@debian:~/chapter_2$ ./find_start
0xbffffad8
shellcoders@debian:~/chapter_2$ ./find_start
0xbffffad8
shellcoders@debian:~/chapter_2$ ./find_start
0xbffffad8
shellcoders@debian:~/chapter_2$ ./find_start
0xbffffad8

Буду признателен, если кто-нибудь сможет прояснить мне эту тему.

2
user9872569 10 Фев 2020 в 19:48

3 ответа

Лучший ответ

Когда я печатаю адрес переменной, отображается ли он относительный (виртуальный) адрес или физический адрес, в котором переменная находится в памяти?

Аналогом относительного адреса является абсолютный адрес. Это не имеет никакого отношения к расположению между виртуальным и физическим адресами.

В большинстве распространенных современных операционных систем, таких как Windows, Linux и MacOS, если вы не пишете драйвер, вы никогда не встретите физические адреса. Они обрабатываются внутри операционной системой. Вы будете работать только с виртуальными адресами.

Почему этот код выдает одно и то же значение указателя стека для каждого прогона (из руководства шеллкодера, стр. 28)?

В большинстве современных операционных систем каждый процесс имеет собственное адресное пространство виртуальной памяти. Исполняемый файл загружается по предпочтительному базовому адресу в этом виртуальном адресном пространстве, если это возможно, в противном случае он загружается по другому адресу (перемещается). Предпочтительный базовый адрес исполняемого файла обычно сохраняется в его заголовке. В зависимости от операционной системы и процессора, куча, вероятно, создается по более высокому адресу, поскольку куча обычно растет вверх (в сторону более высоких адресов). Поскольку стек обычно растет вниз (в сторону более низких адресов), он, вероятно, будет создан ниже нагрузки адрес исполняемого файла и растут к адресу 0.

Поскольку предпочтительный адрес загрузки одинаков при каждом запуске исполняемого файла, вполне вероятно, что адреса виртуальной памяти совпадают. Однако это может измениться, если используется рандомизация пространства макетов адресов. Кроме того, то, что адреса виртуальной памяти одинаковы, еще не означает, что адреса физической памяти тоже одинаковы.

Любая программа, которую я буду запускать, производит этот адрес?

В зависимости от вашей операционной системы вы можете установить предпочтительный базовый адрес, по которому ваша программа загружается в виртуальную память в настройках компоновщика. Многие программы могут по-прежнему иметь тот же базовый адрес, что и ваша программа, возможно, потому что вы оба использовали один и тот же компоновщик с настройками по умолчанию.

Виртуальные адреса только для одной программы? Допустим, у меня есть 2 программы: program1 и program2. Может ли программа2 получить доступ к памяти программы1?

Программа2 не может напрямую обращаться к памяти программы1, поскольку они имеют отдельные адресные пространства виртуальной памяти. Однако одна программа может запросить у операционной системы разрешение на доступ к пространству доступа другого процесса. Операционная система обычно предоставляет это разрешение при условии, что у программы достаточно привилегий. В Windows это можно сделать, например, с помощью функции WriteProcessMemory. Linux предлагает аналогичные функциональные возможности, используя ptrace и записывая в {{ X0 } } . См. эту ссылку для получения дополнительной информации.

3
Andreas Wenzel 20 Апр 2020 в 15:49

Вы получаете виртуальные адреса. Ваша программа никогда не увидит физические адреса. Когда-либо.

Может ли программа2 получить доступ к памяти программы1?

Нет , потому что вы не можете иметь адреса, которые указывают на память программы. Если у вас есть виртуальный адрес 0xabcd1234 в процессе program1, и вы пытаетесь прочитать его из процесса program2, вы получаете 0xabcd1234 для program2 (или вылетает, если в program2 такого адреса нет). Это не проверка прав доступа - это не значит, что процессор идет в память и видит: «О, это память программы1, я не должен к ней обращаться». Это собственное пространство памяти program2.

Но да , если вы используете «разделяемую память», чтобы попросить ОС разместить одинаковую физическую память в обоих процессах.

И да , если вы используете ptrace или /proc/<pid>/mem, чтобы попросить ОС хорошо читать из памяти другого процесса, и у вас есть разрешение на это, тогда это будет сделано ,

почему этот код выдает одно и то же значение указателя стека для каждого прогона (из руководства шеллкодера, стр. 28)? любая программа, которую я буду запускать, будет производить этот адрес?

Очевидно, эта программа всегда имеет это значение указателя стека. У разных программ могут быть разные указатели стека. А если вы поместите больше локальных переменных в main или вызовете find_start из другой функции, вы получите другое значение указателя стека, потому что в стек будет помещено больше данных.

Примечание: даже если вы запустите программу дважды в одно и то же время, адрес будет одинаковым, потому что это виртуальные адреса, и у каждого процесса есть свое собственное виртуальное адресное пространство. Это будут разные физические адреса, но вы не видите физических адресов.

В примере переполнения стека в книге, которую я упоминал, они перезаписывают адрес возврата в стеке на адрес эксплойта в переменных окружения. как это работает ?

Все это работает в рамках одного процесса.

3
user253751 10 Фев 2020 в 17:26

Сосредоточив внимание только на небольшой части вашего вопроса.

#include <stdio.h>
// find_start.c
unsigned long find_start(void)
{
    __asm__("movl %esp, %eax");
}
unsigned long nest ( void )
{
    return(find_start());
}
int main()
{
    printf("0x%lx\n",find_start());
    printf("0x%lx\n",nest());
}

gcc so.c -o so
./so
0x50e381a0
0x50e38190

Здесь нет никакой магии. Виртуальное пространство позволяет создавать программы одинаково. Мне не нужно знать, где будет жить моя программа, каждую программу можно скомпилировать для одного и того же адресного пространства, при загрузке и запуске они могут видеть одно и то же виртуальное адресное пространство, потому что все они отображаются в отдельные / разные физические адресные пространства.

readelf -a so

(но я предпочитаю objdump

Objdump -D так

Disassembly of section .text:

0000000000000540 <_start>:
 540:   31 ed                   xor    %ebp,%ebp
 542:   49 89 d1                mov    %rdx,%r9
 545:   5e                      pop    %rsi

....


000000000000064a <find_start>:
 64a:   55                      push   %rbp
 64b:   48 89 e5                mov    %rsp,%rbp
 64e:   89 e0                   mov    %esp,%eax
 650:   90                      nop
 651:   5d                      pop    %rbp
 652:   c3                      retq   

0000000000000653 <nest>:
 653:   55                      push   %rbp
 654:   48 89 e5                mov    %rsp,%rbp
 657:   e8 ee ff ff ff          callq  64a <find_start>
 65c:   5d                      pop    %rbp
 65d:   c3                      retq   

000000000000065e <main>:
 65e:   55                      push   %rbp
 65f:   48 89 e5                mov    %rsp,%rbp
 662:   e8 e3 ff ff ff          callq  64a <find_start>
 667:   48 89 c6                mov    %rax,%rsi
 66a:   48 8d 3d b3 00 00 00    lea    0xb3(%rip),%rdi        # 724 <_IO_stdin_used+0x4>
 671:   b8 00 00 00 00          mov    $0x0,%eax
 676:   e8 a5 fe ff ff          callq  520 <printf@plt>
 67b:   e8 d3 ff ff ff          callq  653 <nest>

Итак, две вещи или, может быть, более двух вещей. наша точка входа _start находится в оперативной памяти по низкому адресу. низкий виртуальный адрес. в этой системе с этим компилятором я ожидаю, что все / большинство программ будут запускаться в одном и том же месте или в одном и том же месте, или в некоторых случаях это может зависеть от того, что находится в моей программе, но оно должно быть где-то на низком уровне.

Указатель стека, хотя, если вы проверяете выше и сейчас, когда я набираю вещи:

0x355d38d0
0x355d38c0

Это изменилось.

0x4ebf1760
0x4ebf1750

0x31423240
0x31423230

0xa63188d0
0xa63188c0

Несколько раз в течение нескольких секунд. Стек является относительной, а не абсолютной вещью, поэтому нет необходимости создавать фиксированный адрес, который всегда будет одинаковым. Должен находиться в пространстве, связанном с этим пользователем / потоком и виртуальным, поскольку он проходит через MMU по соображениям защиты. Нет никаких причин для виртуального адреса не совпадать с физическим адресом. Код / драйвер ядра, который управляет MMU для платформы, запрограммирован, чтобы делать это определенным образом. у вас может быть адресное пространство для кода, начинающееся с 0x0000 для каждой программы, и вы можете пожелать, чтобы адресное пространство для данных было одинаковым, с нуля. но для стека это не имеет значения. и на моей машине, моей ОС, эта конкретная версия в этот конкретный день не соответствует.

Первоначально я думал, что ваш вопрос отличается от факторов, специфичных для вашей сборки, и настроек. Для конкретной сборки один вызов find_start будет иметь фиксированный относительный адрес для указателя стека, каждая функция, которая использует стек, вернет его так, как он был найден, при условии, что вы не можете изменить компиляцию программы во время выполнения указатель стека для отдельного экземпляра вызова, при котором вложенность будет одинаковой, потребление стека каждой функцией по пути будет одинаковым.

Я добавил еще один слой и, рассмотрев беспорядок дизассемблирования, main, nest и find_start, указал указатель стека (неоптимизированный), поэтому для этих прогонов они разнесены на 0x10. если бы я добавил / удалил больше кода для каждой функции, чтобы изменить использование стека в одной или нескольких функциях, эта дельта могла бы измениться.

Но

gcc -O2 so.c -o so
objdump -D so > so.txt
./so
0x0
0x0

Disassembly of section .text:

0000000000000560 <main>:
 560:   48 83 ec 08             sub    $0x8,%rsp
 564:   89 e0                   mov    %esp,%eax
 566:   48 8d 35 e7 01 00 00    lea    0x1e7(%rip),%rsi        # 754 <_IO_stdin_used+0x4>
 56d:   31 d2                   xor    %edx,%edx
 56f:   bf 01 00 00 00          mov    $0x1,%edi
 574:   31 c0                   xor    %eax,%eax
 576:   e8 c5 ff ff ff          callq  540 <__printf_chk@plt>
 57b:   89 e0                   mov    %esp,%eax
 57d:   48 8d 35 d0 01 00 00    lea    0x1d0(%rip),%rsi        # 754 <_IO_stdin_used+0x4>
 584:   31 d2                   xor    %edx,%edx
 586:   bf 01 00 00 00          mov    $0x1,%edi
 58b:   31 c0                   xor    %eax,%eax
 58d:   e8 ae ff ff ff          callq  540 <__printf_chk@plt>
 592:   31 c0                   xor    %eax,%eax
 594:   48 83 c4 08             add    $0x8,%rsp
 598:   c3                      retq   

По какой-то причине оптимизатор не распознал возвращаемое значение.

unsigned long fun ( void )
{
    return(0x12345678);
}

00000000000006b0 <fun>:
 6b0:   b8 78 56 34 12          mov    $0x12345678,%eax
 6b5:   c3                      retq 

Соглашение о вызовах выглядит хорошо.

Поместите find_start в отдельный файл, чтобы оптимизатор не смог его удалить

gcc -O2 so.c sp.c -o so
./so
0xb1192fc8
0xb1192fc8
./so
0x7aa979d8
0x7aa979d8
./so
0x485134c8
0x485134c8
./so
0xa8317c98
0xa8317c98
./so
0x2ba70b8
0x2ba70b8

Disassembly of section .text:

0000000000000560 <main>:
 560:   48 83 ec 08             sub    $0x8,%rsp
 564:   e8 67 01 00 00          callq  6d0 <find_start>
 569:   48 8d 35 f4 01 00 00    lea    0x1f4(%rip),%rsi        # 764 <_IO_stdin_used+0x4>
 570:   48 89 c2                mov    %rax,%rdx
 573:   bf 01 00 00 00          mov    $0x1,%edi
 578:   31 c0                   xor    %eax,%eax
 57a:   e8 c1 ff ff ff          callq  540 <__printf_chk@plt>
 57f:   e8 4c 01 00 00          callq  6d0 <find_start>
 584:   48 8d 35 d9 01 00 00    lea    0x1d9(%rip),%rsi        # 764 <_IO_stdin_used+0x4>
 58b:   48 89 c2                mov    %rax,%rdx
 58e:   bf 01 00 00 00          mov    $0x1,%edi
 593:   31 c0                   xor    %eax,%eax
 595:   e8 a6 ff ff ff          callq  540 <__printf_chk@plt>

Я не позволил ему встроить те функции, которые он может видеть в гнезде, поэтому он включил его, удалив изменение стека, которое пришло с ним. Так что теперь значение вложенное или нет то же самое.

0
old_timer 10 Фев 2020 в 19:47