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

в 21:26, , рубрики: hackquest, linux kernel, zeronights, реверс-инжиниринг
Ломаем Micosoft Lunix на HackQuest 2019 - 1

Привет!

На 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. Этот скрипт проверяет почту и ключ:

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

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

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

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

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

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

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

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

Итак, имеем:

  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 nokaslrx20x20x20".

А образ назовем 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 в списке функций:

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

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

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

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

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

   0xffffffff811c9785 <+101>:    shl    $0x14,%esi
   0xffffffff811c9788 <+104>:    or     %r12d,%esi
Ломаем Micosoft Lunix на HackQuest 2019 - 7

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

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

У функции __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:

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

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

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

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

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

А вот и начало функции 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.

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

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

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

Напоминаю структуру 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.
Первым обращает на себя внимание этот цикл:

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

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

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

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

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

То есть 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.

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

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

>>> 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 (это выход из цикла):

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

И сравним с:

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

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

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

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

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

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

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

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

Потому что

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

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

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']
Ломаем Micosoft Lunix на HackQuest 2019 - 21

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

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

Автор: Марат Гаянов

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js