Воспользовавшись binwalk
, видим, что имеется sh
скрипт по смещению 0x413000
. Этот скрипт проверяет почту и ключ:
Сломаем проверку с помощью hex-редактора прямо в образе и заставим скрипт исполнять наши команды. Как он теперь выглядит:
Обратите внимание на то, что пришлось урезать строчку activated
до activ
, чтобы размер образа остался тем же. К счастью, проверки хэш-суммы нет. Образ назовем lunix_broken_activation.iso
.
Запускаем его через qemu:
sudo qemu-system-x86_64 lunix_broken_activation.iso -enable-kvm
Покопаемся внутри: вводим /bin/sh
, uname -a
, ls -la /dev/activate
.
Итак, имеем:
- Дистрибутив -
Minimal Linux 5.0.11
. - Проверкой почты, ключа занимается символьное устройство
/dev/activate
, а значит, логику проверки нужно искать где-то в недрах ядра. - Почта, ключ передаются в формате
email|key
.
Образ target_broken_activation.iso
нам более не потребуется.
Такие устройства как /dev/zero
, /dev/null
, /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 *);
Здесь второй аргумент - это буфер с переданными данными, следующий - размер буфера.
По умолчанию, Minimal Linux
компилируется с отключенной отладочной информацией, чтобы уменьшить размер образа,
minimal же. Поэтому нельзя просто запустить отладчик и найти функцию по названию. Найти ее можно только по сигнатуре.
А сигнатуру можно взять из свежего образа Minimal Linux
c включенной отладочной информацией.
То есть схема такая:
эталонный `Minimal Linux` -> известный адрес `register_chrdev` -> сигнатура -> искомый адрес `register_chrdev` в `Lunix`
- Устанавливаем необходимые инструменты:
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
.
Разархивируем 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
забываем, сейчас расскажу почему.
У меня получилось отключить KASLR
в случае со свежесобранным Minimal Linux
в QEMU
.
Но не получилось с Lunix
. Поэтому придется править сам образ.
Для этого откроем его в hex-редакторе, найдем строчку APPEND vga=normal
и заменим на APPEND nokaslr\x20\x20\x20
.
А образ назовем lunix_nokaslr.iso
.
Я напоминаю, что схема поиска такая:
эталонный `Minimal Linux` -> известный адрес `register_chrdev` -> сигнатура -> искомый адрес `register_chrdev` в `Lunix`
Запускаем в одном терминале свежий 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
- это выход из соседней функции).
Прототип функции
int register_chrdev (unsigned int major,
const char * name,
const struct fops);
Ставим брейк на 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 *);
};
Итак, адрес функции write
0xffffffff811f068f
.
В функцию входят несколько интересных блоков. Прямо каждый брейкпоинт описывать не стоит, там обычная рутина. Тем более что блок хэширования и блок создания ключа видны и невооруженным глазом.
Откроем IDA, загрузим ядро и посмотрим, что внутри функции 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
.
Ставим брейк на 0xffffffff811f077e inc rbx
, это выход из цикла:
И проверяем:
>>> get_email_hash('test@mail.ru')
2b902daf5cc483159b0a2f7ed6b593d1d56216a61eab53c8e4b9b9341fb14880
Но сам явно длинноват для ключа.
За ключ отвечает этот код:
Вот здесь идет конечное вычисление каждого байта:
0xFFFFFFFF811F0943 imul eax, r12d
0xFFFFFFFF811F0947 cdq
0xFFFFFFFF811F0948 idiv r10d
В eax
и r12d
байты хэша, они перемножаются, а потом берется остаток от деления на 9.
Потому что
А байты берутся в неожиданном порядке. Я указал его в кейгене:
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)]y
Итак, давайте сгенерируем какой-нибудь ключ:
>>> import lunix
>>> lunix.keygen("m.gayanov@gmail.com")
['0456', '3530', '0401', '2703']