Мы будем использовать gcc и
gdb, чтобы получить инструкции ассемблера соответствующие нашей
маленькой программе. Скомпилируем shellcode3.c с опцией отладки
(-g) и встроим функции, обычно находящиеся в разделяемых
библиотеках, в саму программу при помощи опции --static. Теперь у
нас есть необходимая информация, чтобы понять способ работы системных вызовов
_exexve() и _exit().
$ gcc -o shellcode3 shellcode3.c -O2 -g --static
Дальше при помощи gdb мы посмотрим на ассемблерный эквивалент
наших функций. Все это относится к Linux на платформе Intel (i386 и выше).
$ gdb shellcode3
GNU gdb 4.18
Copyright 1998 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public
License, and you are welcome to change it and/or distribute
copies of it under certain conditions. Type "show copying"
to see the conditions. There is absolutely no warranty
for GDB. Type "show warranty" for details. This GDB was
configured as "i386-redhat-linux"...
Мы просим gdb отобразить ассемблерный код для функции
main().
Вызовы функций по адресам 0x804818b и 0x8048192
запускают подпрограммы библиотеки Си, содержащие настоящие системные вызовы.
Заметьте, что инструкция
0x804817c : mov $0x8071ea8,%edx заполняет регистр
%edx значением, похожим на адрес. Посмотрим на содержимое памяти по
этому адресу, отображая его как строку:
(gdb) printf "%s\n", 0x8071ea8
/bin/sh
(gdb)
Теперь мы знаем, где находится строка. Давайте посмотрим на
дизассемблированный код функций execve() и _exit():
(gdb) disassemble __execve
Dump of assembler code for function __execve:
0x804d9ac <__execve>: push %ebp
0x804d9ad <__execve+1>: mov %esp,%ebp
0x804d9af <__execve+3>: push %edi
0x804d9b0 <__execve+4>: push %ebx
0x804d9b1 <__execve+5>: mov 0x8(%ebp),%edi
0x804d9b4 <__execve+8>: mov $0x0,%eax
0x804d9b9 <__execve+13>: test %eax,%eax
0x804d9bb <__execve+15>: je 0x804d9c2 <__execve+22>
0x804d9bd <__execve+17>: call 0x0
0x804d9c2 <__execve+22>: mov 0xc(%ebp),%ecx
0x804d9c5 <__execve+25>: mov 0x10(%ebp),%edx
0x804d9c8 <__execve+28>: push %ebx
0x804d9c9 <__execve+29>: mov %edi,%ebx
0x804d9cb <__execve+31>: mov $0xb,%eax
0x804d9d0 <__execve+36>: int $0x80
0x804d9d2 <__execve+38>: pop %ebx
0x804d9d3 <__execve+39>: mov %eax,%ebx
0x804d9d5 <__execve+41>: cmp $0xfffff000,%ebx
0x804d9db <__execve+47>: jbe 0x804d9eb <__execve+63>
0x804d9dd <__execve+49>: call 0x8048c84 <__errno_location>
0x804d9e2 <__execve+54>: neg %ebx
0x804d9e4 <__execve+56>: mov %ebx,(%eax)
0x804d9e6 <__execve+58>: mov $0xffffffff,%ebx
0x804d9eb <__execve+63>: mov %ebx,%eax
0x804d9ed <__execve+65>: lea 0xfffffff8(%ebp),%esp
0x804d9f0 <__execve+68>: pop %ebx
0x804d9f1 <__execve+69>: pop %edi
0x804d9f2 <__execve+70>: leave
0x804d9f3 <__execve+71>: ret
End of assembler dump.
(gdb) disassemble _exit
Dump of assembler code for function _exit:
0x804d990 <_exit>: mov %ebx,%edx
0x804d992 <_exit+2>: mov 0x4(%esp,1),%ebx
0x804d996 <_exit+6>: mov $0x1,%eax
0x804d99b <_exit+11>: int $0x80
0x804d99d <_exit+13>: mov %edx,%ebx
0x804d99f <_exit+15>: cmp $0xfffff001,%eax
0x804d9a4 <_exit+20>: jae 0x804dd90 <__syscall_error>
End of assembler dump.
(gdb) quit
Настоящий вызов ядра происходит через прерывание 0x80 по
адресам 0x804d9d0 для execve() и
0x804d99b для _exit(). Эта точка входа общая для
различных системных вызовов, поэтому различие производится по содержимому
регистра %eax. Для execve(), он имеет значение
0x0B , тогда как для _exit() - 0x01.
Анализ этих ассемблерных инструкций функций дает нам параметры, которые они
используют:
execve() нужны различные параметры (сравни с диаграммой 4) :
регистр %ebx содержит адрес строки, представляющей команду
для запуска, в нашем примере "/bin/sh"
(0x804d9b1 : mov 0x8(%ebp),%edi а затем
0x804d9c9 : mov %edi,%ebx) ;
регистр %ecx содержит адрес массива аргументов
(0x804d9c2 : mov 0xc(%ebp),%ecx). Первый
аргумент должен быть именем программы, и больше нам ничего не нужно:
массива, содержащего адрес строки "/bin/sh" и указатель NULL,
будет достаточно;
Регистр %edx содержит адрес массива, представляющего собой
окружение для запускаемой программы
(0x804d9c5 : mov 0x10(%ebp),%edx). Чтобы
оставить нашу программу простой, мы будем использовать пустое окружение:
указатель NULL сделает это для нас.
функция _exit() завершает процесс и возвращает код выполнения
родительскому процессу (обычно оболочке), который содержится в регистре
%ebx ;
Поэтому нам нужны строка "/bin/sh", указатель на эту строку и
NULL указатель (для аргументов, так как у нас их нет, и для окружения, так как
оно у нас пустое). Мы можем увидеть возможное представление данных перед вызовом
execve(). Построим массив из указателя на /bin/sh и
NULL указателя, %ebx будет указывать на строку, %ecx
на весь массив, а %edx на второй элемент массива (NULL). Это
показано на диаграмме 5.
Диаграмма 5 : представление данных по отношению к регистрам