Категория > Новости > Реверс-шелл на 237 байт. Изучаем хаки Linux для уменьшения исполняемого файла - «Новости»

Реверс-шелл на 237 байт. Изучаем хаки Linux для уменьшения исполняемого файла - «Новости»


26-08-2021, 00:02. Автор: Доминика
tiny shell, prism и дру­гие реверс‑шел­лы. Те из них, что написа­ны на С, занима­ют лишь десят­ки‑сот­ни килобайт. Так к чему соз­давать еще один?

А суть вот в чем. Цель дан­ной статьи учеб­ная: рав­но как раз­работ­ка ядер­ных рут­китов — один из наибо­лее наг­лядных спо­собов разоб­рать­ся с устрой­ством самого ядра Linux, написа­ние обратно­го шел­ла с допол­нитель­ной фун­кци­ональ­ностью и одновре­мен­но с огра­ниче­ниями по раз­меру исполня­емо­го фай­ла поз­воля­ет изу­чить некото­рые неожи­дан­ные осо­бен­ности положе­ния вещей в Linux, в час­тнос­ти каса­ющих­ся ELF-фай­лов, их заг­рузки и запус­ка, нас­ледова­ния ресур­сов в дочер­них про­цес­сах и работы ком­понов­щика (он же лин­кер, лин­ковщик, редак­тор свя­зей). По ходу дела нас ждет мно­жес­тво инте­рес­ных откры­тий и любопыт­ных хаков. А бонусом нам будет рабочий инс­тру­мент, который заод­но мож­но допили­вать и при­менять в пен­тесте. Посему нач­нем!



info


Ре­зуль­таты тру­дов дос­тупны на гит­хабе.


warning


Ни автор, ни редак­ция не несут ответс­твен­ности за любые пос­ледс­твия исполь­зования при­веден­ных в этой пуб­ликации све­дений. Вся информа­ция пре­дос­тавле­на исклю­читель­но ради информи­рова­ния читате­ля.



 

Определяемся с ТЗ


Итак, наш реверс‑шелл помимо того, что под­клю­чать­ся к задан­ному хос­ту на задан­ный порт, так­же дол­жен:



  • из­менять собс­твен­ное имя при запус­ке — так мы будем менее замет­ны;

  • пе­риоди­чес­ки менять иден­тифика­тор про­цес­са — так мы будем менее уло­вимы;

  • иметь минималь­но воз­можный раз­мер — так будет инте­рес­нее.


Спер­ва опре­делим­ся с язы­ком. Пос­коль­ку мы стре­мим­ся к минималь­но воз­можно­му раз­меру бинаря, в голову при­ходит лишь два вари­анта: С и ассем­блер. Одна­ко, как ты, веро­ятно, зна­ешь, хоть С и поз­воля­ет собирать кро­хот­ные по сов­ремен­ным мер­кам Hello World’ы (при­мер­но 17 и ~800 Кбайт при динами­чес­кой и ста­тичес­кой лин­ковке соот­ветс­твен­но про­тив 2 Мбайт на Go), при ком­пиляции С‑кода генери­рует­ся так­же код, отве­чающий:



  • за запуск гло­баль­ных конс­трук­торов и дес­трук­торов, если они есть;

  • за кор­рек­тную переда­чу аргу­мен­тов в main();

  • за переда­чу управле­ния затем из __libc_start_main() в main().


Конструкторы и деструкторы


Мас­сивы фун­кций‑конс­трук­торов и фун­кций‑дес­трук­торов запус­кают­ся перед и пос­ле main() соот­ветс­твен­но. Их код находит­ся в отдель­ных сек­циях в про­тиво­вес «обыч­ному», попада­юще­му в .text. Такие фун­кции исполь­зуют­ся, нап­ример, для раз­личных ини­циали­заций в раз­деля­емых биб­лиоте­ках или для уста­нов­ки парамет­ров буфери­зации в некото­рых при­ложе­ниях, вза­имо­дей­ству­ющих по сети (в час­тнос­ти, это иног­да встре­чает­ся в CTF-тас­ках). Что­бы фун­кция попала в одну из этих сек­ций, сле­дует ука­зывать __attribute__ ((constructor)) или __attribute__ ((destructor)) перед опре­деле­нием фун­кции.


В некото­рых слу­чаях сек­ции, хра­нящие эти фун­кции, могут иметь име­на .ctors/.init/.init_array и .dtors/.fini/.fini_array. Все они игра­ют в целом одну роль, и раз­личия нас в рам­ках дан­ной статьи не инте­ресу­ют. Под­робнее о гло­баль­ных конс­трук­торах и дес­трук­торах мож­но почитать на wiki.osdev.org.



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


Дан­ная обвязка нераз­рывно свя­зана с С‑бинаря­ми как минимум в Linux. Для нас же в рам­ках нашей задачи она — бал­ласт, от которо­го необ­ходимо нещад­но избавлять­ся. Так что реверс‑шелл наш будет написан на великом и ужас­ном язы­ке ассем­бле­ра (естес­твен­но, под x86). План таков: спер­ва напишем рабочий код, а уже затем будет занимать­ся кар­диналь­ным умень­шени­ем его раз­мера.


 

Кодим


Мы будем исполь­зовать NASM. За осно­ву возь­мем прос­тей­ший ас­мовый реверс‑шелл. Раз­мышле­ния на тему, дол­жен ли наш код быть 32- или 64-бит­ным, при­вели меня к выводу, что пер­вый вари­ант пред­почти­тель­нее: инс­трук­ции в этом режиме мень­ше, а необ­ходимой фун­кци­ональ­нос­ти мы не теря­ем, ведь наша глав­ная задача по сути сос­тоит лишь в под­клю­чении к сер­веру и запус­ке обо­лоч­ки, а сама она будет работать уже в 64-бит­ном режиме.


Код будет делать сле­дующее:



  • при запус­ке реверс‑шелл меня­ет свой пер­вый аргу­мент запус­ка — эти аргу­мен­ты отоб­ража­ются в ps, htop;

  • так­же меня­ет крат­кое имя — оно отоб­ража­ется ути­литой top;

  • за­тем про­бует под­клю­чить­ся к сер­веру. При неуда­че соз­дает­ся дочер­ний про­цесс, завер­шает­ся родитель­ский, выжида­ется тайм‑аут, пос­ле чего попыт­ка под­клю­чения пов­торя­ется;

  • при успешном под­клю­чении запус­кает­ся /bin/sh, stdin, stdout и stderr которо­го свя­заны с сокетом, обща­ющим­ся с сер­вером. Имя про­цес­са так­же под­меня­ется.


Что ж, за дело!


 

Что в имени тебе?


В Linux мож­но встре­тить две «сущ­ности», хра­нящие свя­зан­ное с про­цес­сом имя. Назовем их «пол­ное» и «крат­кое имя». Оба дос­тупны через /proc: пол­ное в /proc/<pid>/cmdline, крат­кое в /proc/<pid>/comm (comm от command).


Крат­кое имя, сог­ласно опи­санию, содер­жит имя исполня­емо­го фай­ла без пути до него. Это имя хра­нит­ся в ядер­ной струк­туре task_struct, опи­сыва­ющей про­цесс (задачу, если более кор­рек­тно в тер­минах ядра), и име­ет ог­раниче­ние дли­ны в 16 сим­волов, вклю­чая нуль‑байт.


Пол­ное имя содер­жит аргу­мен­ты запус­ка прог­раммы, они же *argv[]: в нулевом эле­мен­те мас­сива — имя исполня­емо­го фай­ла так, как оно было ука­зано при запус­ке; в осталь­ных — аргу­мен­ты, если они были переда­ны.


Сме­на крат­кого име­ни слож­ностей не вызыва­ет. Вос­поль­зуем­ся для это­го сис­темным вызовом prctl(). С его помощью про­цесс или поток может осу­щест­влять раз­личные опе­рации над самим собой: над сво­им име­нем, при­виле­гиями (capabilities), областя­ми памяти, режимом seccomp и мно­го чем еще. Номер нуж­ной опе­рации переда­ется пер­вым аргу­мен­том, затем идут осталь­ные парамет­ры, чис­ло которых может варь­иро­вать­ся. Нас инте­ресу­ет опе­рация PR_SET_NAME, где вто­рым аргу­мен­том переда­ется ука­затель на новое имя. При этом, если имя с нуль‑бай­том длин­нее 16 сим­волов, оно будет обре­зано.


Та­ким обра­зом, для сме­ны крат­кого име­ни нуж­но выз­вать prctl(PR_SET_NAME, NEW_ARGV), где NEW_ARGV содер­жит адрес нового име­ни. Для это­го исполь­зуем сле­дующий код:


mov eax, 0xac ; NR_PRCTL
mov ebx, 15 ; PR_SET_NAME
mov ecx, NEW_ARGV
int 0x80; syscall interrupt
...
NEW_ARGV:
db "s0l3g1t", 0


info


Мно­го полез­ной информа­ции о сис­темных вызовах мож­но най­ти в man 2 syscall. Там же для зоопар­ка под­держи­ваемых в Linux плат­форм и ABI есть две таб­лицы: с инс­трук­циями для совер­шения сис­темно­го вызова и с регис­тра­ми, исполь­зуемы­ми при переда­че аргу­мен­тов и воз­вра­те зна­чений. Имей в виду, что сог­лашения о вызовах, по край­ней мере на x86, отли­чают­ся от таковых в юзер­модных при­ложе­ниях.



Поп­робу­ем теперь перепи­сать argv[0]. Сле­дующий кусок кода выпол­няет дей­ствия, ана­логич­ные сиш­ной strncpy(&argv[0], NEW_ARGV, strlen(argv[0] + 1)), при этом адрес argv[0] пред­варитель­но был положен на стек:


mov edi, [esp]; edi = &argv[0]
mov esi, NEW_ARGV
mov ecx, _start - NEW_ARGV ; ecx = strlen(NEW_ARGV) + NULL-byte
_name_loop:
movsb; edi[i] = esi[i] ; i+=1
loop _name_loop
...
NEW_ARGV:
db "s0l3g1t", 0
_start:
...

Этот адрес помеща­ется в регистр edi (destination index register). В регистр esi (source index register) отправ­ляет­ся адрес уста­нав­лива­емо­го нами име­ни "s0l3g1t", а в ecx — его дли­на, вклю­чая нулевой байт. Одна­ко ока­зыва­ется, что если изна­чаль­ный argv[0] ("./asm_shell") был длин­нее нового, то, нес­мотря на наличие завер­шающе­го нуль‑бай­та, вывод ps будет таков.


Реверс-шелл на 237 байт. Изучаем хаки Linux для уменьшения исполняемого файла - «Новости»
Вы­вод ps при переза­писи нулево­го аргу­мен­та «в лоб»

Как‑то не осо­бо здо­рово. Поп­робу­ем его сна­чала запол­нить нулями и лишь затем переза­писы­вать.


Вы­вод ps при переза­писи занулен­ного нулево­го аргу­мен­та

Уже луч­ше — в выводе ps ничего подоз­ритель­ного! Хотя все еще есть к чему стре­мить­ся. А что ска­жет нам ману­ал? Сов­сем нем­ного поис­кав, натыка­емся на такое мес­то в man 5 proc (под­раздел о /proc/[pid]/cmdline):


Furthermore, a process may change the memory location that this file refers via prctl(2) operations such as PR_SET_MM_ARG_START.


А в man 2 prctl находим, помимо парамет­ра PR_SET_MM_ARG_START, так­же PR_SET_MM_ARG_END (с неболь­шой помет­кой, что эти опции дос­тупны начиная с вер­сии Linux 3.5). Кажет­ся, вто­рой параметр — как раз то, что надо! Да вот незада­ча: для выпол­нения опе­раций prctl(), зат­рагива­ющих память про­цес­са, нуж­на при­виле­гия CAP_SYS_RESOURCE (ина­че ведь было бы слиш­ком уж прос­то!). А ее уста­нов­ка тре­бует прав супер­поль­зовате­ля.


По этой же при­чине замена адре­са самого мас­сива строк argv[] на сте­ке «в лоб» не при­ведет к сме­не содер­жимого /proc/[pid]/cmdline: Linux хра­нит адре­са начала и кон­ца памяти, где находят­ся аргу­мен­ты про­цес­са, при­чем содер­жимое имен­но этой памяти и выводит­ся. То же вер­но и для перемен­ных окру­жения. И потому xxd выводит нули.


В общем, будем исхо­дить из пред­положе­ния, что реверс‑шелл запущен от име­ни прос­того поль­зовате­ля и воз­можнос­ти уста­новить CAP_SYS_RESOURCE нико­им обра­зом нет. Поэто­му прос­то занулим весь изна­чаль­ный argv[0] и запишем поверх него свой. Час­то ли кому‑либо при­ходит в голову смот­реть имя про­цес­са через /proc в xxd?


Ос­талось разоб­рать­ся с под­меной име­ни /bin/sh, ведь пос­ле вызова execve() для запус­ка шел­ла его *argv[] будет пре­датель­ски являть взо­ру адми­на /bin/sh в выводе ps и htop, а так­же в /proc/<pid>/cmdline. К счастью, это реша­ется про­ще прос­того: нуж­но все­го лишь передать собс­твен­ный argv[0] вто­рым аргу­мен­том это­му сис­колу. При­том важ­но иметь в виду, что переда­ется ука­затель на мас­сив аргу­мен­тов (строк), который дол­жен завер­шать­ся нулевым ука­зате­лем. Поэто­му перед тем, как положить на стек адрес NEW_ARGV, туда кла­дет­ся 0:


xor eax, eax
push dword 0x0068732f ; push "/sh"
push dword 0x6e69622f ; push /bin (="/bin/sh")
mov ebx, esp; ebx = ptr to "/bin/sh" into ebx
push edx; edx = 0x00000000
mov edx, esp; **envp = edx = ptr to NULL address
push ebx; pointer to /bin/sh
push 0
push NEW_ARGV
mov ecx, esp; ecx points to shell's argv[0] ( &NEW_ARGV )
mov al, 0xb
int 0x80; execve("/bin/sh", &{ NEW_ARGV, 0 }, 0)

Но сме­нить при этом и крат­кое имя через prctl() так прос­то мы уже не можем, пос­коль­ку работа­ем из обо­лоч­ки, где вызов сис­колов нап­рямую недос­тупен. Одна­ко есть иные ин­терес­ные спо­собы это сде­лать.



Перейти обратно к новости