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

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

Комментарии (14)


  1. capjdcoder
    17.10.2019 02:59

    Снимаю шляпу!


    1. xoo Автор
      17.10.2019 08:30

      Спасибо)


  1. HiMiC
    17.10.2019 08:33
    +1

    Супер!


    1. xoo Автор
      17.10.2019 11:41

      Рад, что вам понравилось)


      1. dikkini
        17.10.2019 14:57

        это невероятно!!! очень крутые низкоуровневые скилы, а также чуйка!


        1. xoo Автор
          17.10.2019 15:10

          Потом еще что-нибудь интересное подгоню)


  1. GBK
    17.10.2019 14:06

    Micosoft Lunix? Может быть Microsoft Linux?


    1. uuger
      17.10.2019 14:41
      +2

      на скрине же видно image


      1. GBK
        17.10.2019 14:46

        Беру свои слова обратно


  1. lorc
    17.10.2019 16:47
    +1

    Хм, а что, ядро было собрано без kallsyms?

    Там нет /proc/kallsyms и в самом бинаре ядра нет таблицы с символами? Не дебажной, а той что генерируется при сборке через scripts/symbols


    1. xoo Автор
      17.10.2019 17:00

      /proc/kallsyms там не было, а как посмотреть/найти таблицу с символами?


      1. lorc
        17.10.2019 17:13
        +1

        Ага, кажется ядро собиралось без CONFIG_KALLSYMS. Ну тогда и таблицы с символами в бинаре тоже не будет скорее.

        Таблица сжатая, и линковщик может запихнуть ее куда угодно. Но ее можно найти по косвенным признакам. Там должен быть немаленький массив с оффсетами (kallsyms_offsets), отсортированный по возрастанию. А сжимающая таблица (kallsyms_token_table) будет содержать в себе обрезки слов. Что-то типа такого:

        161500 66756e63 5f005f73 65740071 75657500 func_._set.queu.
        161510 6d617000 696e6700 70680065 7500636f map.ing.ph.eu.co
        161520 005f7374 61007570 5f007063 695f0061 ._sta.up_.pci_.a
        161530 7474725f 00747800 6d700077 61006765 ttr_.tx.mp.wa.ge
        161540 6e003130 0063616c 005f706f 00646f00 n.10.cal._po.do.
        161550 7970006c 6f636b00 736f0063 69005f70 yp.lock.so.ci._p
        161560 726f005f 77006d75 78006770 00726900 ro._w.mux.gp.ri.
        161570 2e330072 745f006c 6100686f 77007369 .3.rt_.la.how.si
        161580 00647269 76006c69 6e002400 636f6d00 .driv.lin.$.com.
        161590 63740066 756e0069 6f5f0061 6370695f ct.fun.io_.acpi_
        1615a0 00345f00 72740079 73007265 61002e00 .4_.rt.ys.rea...
        1615b0 636f6e00 30003100 32003300 34003500 con.0.1.2.3.4.5.
        1615c0 36003700 38003900 6e645f00 70726f00 6.7.8.9.nd_.pro.
        1615d0 72656769 73746572 5f005f73 65745f00 register_._set_.
        1615e0 5f5f0074 5f5f0040 00410042 00430044 __.t__.@.A.B.C.D
        1615f0 00450046 00470048 0049004a 004b004c .E.F.G.H.I.J.K.L
        161600 004d004e 004f0050 00510052 00530054 .M.N.O.P.Q.R.S.T
        161610 00550056 00570065 5f730059 006c6c00 .U.V.W.e_s.Y.ll.


  1. JustSaintMike
    17.10.2019 17:14

    ого…


  1. SingularityUrBrain
    17.10.2019 22:51

    Очень круто :)