Хабрахабр

Ломаем Micosoft Lunix на HackQuest 2019

Привет, Хабр!

Я не сдал решение вовремя, но свою порцию острых ощущений получил. На HackQuest перед конференцией ZeroNight 2019 было одно занимательное задание. Crew для участников. Я считаю, вам будет интересно узнать, что приготовили организаторы и команда r0.

Задание: добыть код активации для секретной операционной системы Micosoft 1998.
В этой статье я расскажу, как это сделать.

Содержание

0. Задача
1. Инструменты
2. Осматриваем образ
3. Символьные устройства и ядро
4. Поиск register_chrdev
4.1. Готовим свежий образ Minimal Linux
4.2. Еще немного приготовлений
4.3. Отключаем KASLR в lunix
4.4. Ищем и находим сигнатуру
5. Поиск fops от /dev/activate и функции write
6. Изучаем write
6.1. Хэш функция
6.2. Алгоритм генерации ключа
6.3. Кейген

Задача

Запущенный в QEMU образ требует почту и ключ активации. Почту мы уже знаем, давайте искать остальное!

1. Инструменты

  • GDB
  • QEMU
  • binwalk
  • IDA

В ~/.gdbinit нужно записать полезную функцию:

define xxd dump binary memory dump.bin $arg0 $arg0+$arg1 shell xxd dump.bin
end

2. Осматриваем образ

Сначала переименуем jD74nd8_task2.iso в lunix.iso.

Этот скрипт проверяет почту и ключ: Воспользовавшись binwalk, видим, что имеется скрипт по смещению 0x413000.

Сломаем проверку с помощью hex-редактора прямо в образе и заставим скрипт исполнять наши команды. Как он теперь выглядит:

Обратите внимание на то, что пришлось урезать строчку activated до activ, чтобы размер образа остался тем же. К счастью, проверки хэш-суммы нет. Образ назовем lunix_broken_activation.iso.

Запускаем его через QEMU:

sudo qemu-system-x86_64 lunix_broken_activation.iso -enable-kvm

Покопаемся внутри:

Итак, имеем:

  1. Дистрибутив — Minimal Linux 5.0.11.
  2. Проверкой почты, ключа занимается символьное устройство /dev/activate, а значит, логику проверки нужно искать где-то в недрах ядра.
  3. Почта, ключ передаются в формате email|key.

Образ target_broken_activation.iso нам более не потребуется.

3. Символьные устройства и ядро

Такие устройства как /dev/mem, /dev/vcs, /dev/activate и т.д. регистрируются с помощью функции register_chrdev:

int register_chrdev (unsigned int major, const char * name, const struct fops);

name — имя, а структура fops содержит указатели на функции драйвера:

struct file_operations ;

Нас интересует только эта функция:

ssize_t (*write) (struct file *, const char *, size_t, loff_t *);

Здесь второй аргумент — это буфер с переданными данными, следующий — размер буфера.

4. Поиск register_chrdev

По умолчанию, Minimal Linux компилируется с отключенной отладочной информацией, чтобы уменьшить размер образа, minimal же. Поэтому нельзя просто запустить отладчик и найти функцию по названию. Зато можно по сигнатуре.

В общем, надо собирать свой Minimal. А сигнатура есть в образе Minimal Linux c включенной отладочной информацией.

То есть схема такая:

эталонный Minimal Linux -> известный адрес register_chrdev -> сигнатура ->
искомый адрес register_chrdev в Lunix

4.1. Готовим свежий образ Minimal Linux

  1. Устанавливаем необходимые инструменты:

    sudo apt install wget make gawk gcc bc bison flex xorriso libelf-dev libssl-dev

  2. Качаем скрипты:

    git clone https://github.com/ivandavidov/minimal
    cd minimal/src

  3. Корректируем 02_build_kernel.sh:
    это удаляем

    # Disable debug symbols in kernel => smaller kernel binary. sed -i "s/^CONFIG_DEBUG_KERNEL.*/\\# CONFIG_DEBUG_KERNEL is not set/" .config

    это добавляем

    echo "CONFIG_GDB_SCRIPTS=y" >> .config

  4. Компилируем

    ./build_minimal_linux_live.sh

Получается образ minimal/src/minimal_linux_live.iso.

4.2. Еще немного приготовлений

Разархивируем minimal_linux_live.iso в папку minimal/src/iso.

Переименуем их в kernel.minimal.xz, rootfs.minimal.xz. В minimal/src/iso/boot лежат образ ядра kernel.xz и образ ФС rootfs.xz.

В этом поможет скрипт extract-vmlinux:
Помимо этого нужно вытащить ядро из образа.

extract-vmlinux kernel.minimal.xz > vmlinux.minimal

Теперь в папке minimal/src/iso/boot у нас такой набор: kernel.minimal.xz, rootfs.minimal.xz, vmlinux.minimal.

Поэтому проводим все те же операции, ядро называем vmlinux.lunix, про kernel.xz, rootfs.xz забываем, сейчас расскажу почему. А вот из lunix.iso нам нужно только ядро.

4.3. Отключаем KASLR в lunix

У меня получилось отключить KASLR в случае со свежесобранным Minimal Linux в QEMU.
Но не получилось с Lunix. Поэтому придется править сам образ.

Для этого откроем его в hex-редакторе, найдем строчку "APPEND vga=normal" и заменим на "APPEND nokaslr\x20\x20\x20".

А образ назовем lunix_nokaslr.iso.

4.4. Ищем и находим сигнатуру

Запускаем в одном терминале свежий Minimal Linux:

sudo qemu-system-x86_64 -kernel kernel.minimal.xz -initrd rootfs.minimal.xz -append nokaslr -s

В другом отладчик:

sudo gdb vmlinux.minimal
(gdb) target remote localhost:1234

А теперь ищем register_chrdev в списке функций:

Очевидно, что наш вариант — это __register_chrdev.
Нас не смущает, что искали register_chrdev, а нашли __register_chrdev

Дизассемблируем:

Я попробовал несколько вариантов и остановился на следующем куске:
Какую сигнатуру взять?

0xffffffff811c9785 <+101>: shl $0x14,%esi 0xffffffff811c9788 <+104>: or %r12d,%esi

Дело в том, что в lunix есть только одна функция, которая содержит 0xc1, 0xe6, 0x14, 0x44, 0x09, 0xe6.
Сейчас покажу, но сначала узнаем, в каком сегменте ее искать.

У функции __register_chrdev адрес 0xffffffff811c9720, это сегмент .text. Там и будем искать.

Подключаемся к lunix теперь. Отключаемся от эталонного Minimal Linux.

В одном терминале:

sudo qemu-system-x86_64 lunix_nokaslr.iso -s -enable-kvm

В другом:

sudo gdb vmlinux.lunix
(gdb) target remote localhost:1234

Смотрим границы сегмента .text:

Границы 0xffffffff81000000 - 0xffffffff81600b91, ищем 0xc1, 0xe6, 0x14, 0x44, 0x09, 0xe6:

Кусок находим по адресу 0xffffffff810dc643. Но это только часть функции, посмотрим, что выше:

А вот и начало функции 0xffffffff810dc5d0(потому что retq — это выход из соседней функции).

5. Поиск fops от /dev/activate

Прототип у функции register_chrdev такой:

int register_chrdev (unsigned int major, const char * name, const struct fops);

Нам нужна структура fops.

Ставим брейк на 0xffffffff810dc5d0. Перезапускаем отладчик и QEMU. Это просыпаются устройства mem, vcs, cpu/msr, cpu/cpuid, а сразу за ними и activate.
Он сработает несколько раз.

Указатель на имя хранится в регистре rcx. А указатель на fops — в r8:

Напоминаю структуру fops

struct file_operations { struct module *owner; loff_t (*llseek) (struct file *, loff_t, int); ssize_t (*read) (struct file *, char *, size_t, loff_t *); ssize_t (*write) (struct file *, const char *, size_t, loff_t *); int (*readdir) (struct file *, void *, filldir_t); unsigned int (*poll) (struct file *, struct poll_table_struct *); int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long); int (*mmap) (struct file *, struct vm_area_struct *); int (*open) (struct inode *, struct file *); int (*flush) (struct file *); int (*release) (struct inode *, struct file *); int (*fsync) (struct file *, struct dentry *, int datasync); int (*fasync) (int, struct file *, int); int (*lock) (struct file *, int, struct file_lock *); ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *); ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *); };

Итак, адрес функции write0xffffffff811f068f.

6. Изучаем write

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

6.1. Хэш функция

Откроем IDA, загрузим ядро vmlinux.lunix и посмотрим, что внутри у функции write.
Первым обращает на себя внимание этот цикл:

Здесь вызывается какая-то функция sub_FFFFFFFF811F0413, которая начинается так:

А по адресу 0xffffffff81829ce0 обнаруживается таблица для sha256:

То есть sub_FFFFFFFF811F0413 = sha256. Байты, хэш которых нужно получить, передаются через $sp+0x50+var49, а результат сохраняется по адресу $sp+0x50+var48. Кстати, var49=-0x49, var48=-0x48, так что $sp+0x50+var49 = $sp+0x7, $sp+0x50+var48 = $sp+0x8.

Проверим.

Вводим почту test@mail.ru, пароль 1234-5678-0912-3456. Запускаем qemu, gdb, ставим брейк на 0xffffffff811f0748 call sub_FFFFFFFF811F0413 и на инструкцию 0xffffffff811f074d xor ecx, ecx, которая сразу за функцией.

В функцию передается байт почты, а результат такой:

>>> import hashlib
>>> hashlib.sha256(b"t").digest().hex() 'e3b98a4da31a127d4bde6e43033f66ba274cab0eb7eb1c70ec41402bf6273dd8'
>>>

То есть да, это действительно sha256, только она вычисляет хэши по всем байтам почты, а не один хэш только от почты.

Но если сумма больше 0xEC, то сохраняется остаток от деления на 0xEC:
Дальше хэши суммируются по-байтно.

import hashlib def get_email_hash(email): h = [0]*32 for sym in email: sha256 = hashlib.sha256(sym.encode()).digest() for i in range(32): s = h[i] + sha256[i] if s <= 0xEC: h[i] = s else: h[i] = s % 0xEC return h

Сумма сохраняется по адресу 0xffffffff81c82f80. Давайте посмотрим, какой будет хэш от почты test@mail.ru.

Ставим брейк на ffffffff811f0786 dec r13d (это выход из цикла):

И сравним с:

>>> get_email_hash('test@mail.ru')
2b902daf5cc483159b0a2f7ed6b593d1d56216a61eab53c8e4b9b9341fb14880

Но сам хэш явно длинноват для ключа.

6.2. Алгоритм генерации ключа

За ключ отвечает этот код:

Вот здесь идет конечное вычисление каждого байта:

0xFFFFFFFF811F0943 imul eax, r12d
0xFFFFFFFF811F0947 cdq
0xFFFFFFFF811F0948 idiv r10d

В eax и r12d байты хэша, они перемножаются, а потом берется остаток от деления на 9.

Потому что

А байты берутся в неожиданном порядке. Я укажу его в кейгене.

6.3. Кейген

def keygen(email): email_hash = get_email_hash(email) pairs = [(0x00, 0x1c), (0x1f, 0x03), (0x01, 0x1d), (0x1e, 0x02), (0x04, 0x18), (0x1b, 0x07), (0x05, 0x19), (0x1a, 0x06), (0x08, 0x14), (0x17, 0x0b), (0x09, 0x15), (0x16, 0x0a), (0x0c, 0x10), (0x13, 0x0f), (0x0d, 0x11), (0x12, 0x0e)] key = [] for pair in pairs: i = pair[0] j = pair[1] key.append((email_hash[i] * email_hash[j])%9) return [''.join(map(str, key[i:i+4])) for i in range(0, 16, 4)]

Итак, давайте сгенерируем какой-нибудь ключ:

>>> import lunix
>>> lunix.keygen("m.gayanov@gmail.com")
['0456', '3530', '0401', '2703']

Код здесь А теперь можно расслабиться и поиграть в игру 2048:) Благодарю за внимание!

Показать больше

Похожие публикации

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»