Как стать автором
Поиск
Написать публикацию
Обновить

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

Время на прочтение7 мин
Количество просмотров16K

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

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

Задание: добыть код активации для секретной операционной системы 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 {
       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 *);
    };

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

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

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

4. Поиск register_chrdev


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

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

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

эталонный 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.

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

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

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

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

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

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. Там и будем искать.

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

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

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.

Перезапускаем отладчик и QEMU. Ставим брейк на 0xffffffff810dc5d0. Он сработает несколько раз. Это просыпаются устройства 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.

Проверим.

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

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


>>> 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:) Благодарю за внимание! Код здесь

Теги:
Хабы:
Всего голосов 49: ↑49 и ↓0+48
Комментарии14

Публикации

Ближайшие события