Ломаем Micosoft Lunix на HackQuest 2019
Привет, Хабр!
На 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
Покопаемся внутри:
Итак, имеем:
- Дистрибутив — Minimal Linux 5.0.11.
- Проверкой почты, ключа занимается символьное устройство
/dev/activate
, а значит, логику проверки нужно искать где-то в недрах ядра. - Почта, ключ передаются в формате
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
- Устанавливаем необходимые инструменты:
sudo apt install wget make gawk gcc bc bison flex xorriso libelf-dev libssl-dev
- Качаем скрипты:
git clone https://github.com/ivandavidov/minimal cd minimal/src
- Корректируем
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
- Компилируем
./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
:
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 *);
};
Итак, адрес функции write
— 0xffffffff811f068f
.
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:) Благодарю за внимание! Код здесь