
Разбор CTF pwn-задачи с эксплуатацией CVE-2021-4034 (pkexec) через манипуляцию кучей, с реверс-инжинирингом в Ghidra и пользовательским вспомогательным бинарником shelly.so.
Это 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 возможных комбинаций. Удачи с этим, лол.