Quebrando o Micosoft Lunix no HackQuest 2019


Olá Habr!

No HackQuest, antes da conferência ZeroNight 2019, havia uma tarefa divertida. Não tomei a decisão a tempo, mas recebi minha porção de emoções. Acho que você estará interessado em saber o que os organizadores e a equipe da r0.Crew prepararam para os participantes.

Tarefa: obtenha um código de ativação para o sistema operacional secreto Micosoft 1998 .

Neste artigo, vou lhe dizer como fazê-lo.

Conteúdo


0. Tarefa
1. Ferramentas
2. Inspecione a imagem
3. Dispositivos de caracteres e o kernel
4. Pesquisa register_chrdev
4.1 Preparando uma nova imagem mínima do Linux
4.2 Mais alguns preparativos
4.3 Desativar KASLR no lunix
4.4 Pesquisamos e encontramos uma assinatura
5. Procure por fops em / dev / active e a função write
6. Estudamos escrever
6.1 Função hash
6.2 Algoritmo de geração de chaves
6.3 Keygen

Desafio


Uma imagem lançada no QEMU requer correio e uma chave de ativação. Já sabemos o e-mail, vamos procurar o resto!

1. Ferramentas


  • Gdb
  • QEMU
  • binwalk
  • IDA

Em ~/.gdbinit você precisa escrever uma função útil:

 define xxd dump binary memory dump.bin $arg0 $arg0+$arg1 shell xxd dump.bin end 

2. Inspecione a imagem


Renomeie jD74nd8_task2.iso para lunix.iso.

Usando binwalk, vemos que existe um script no deslocamento 0x413000 . Este script verifica o correio e a chave:


Nós quebramos a verificação com o editor hexadecimal diretamente na imagem e fazemos o script executar nossos comandos. Como está agora:


Observe que você precisou cortar a linha activated para activated , para que o tamanho da imagem permaneça o mesmo. Felizmente, não há verificação de hash. A imagem é chamada lunix_broken_activation.iso.

Execute-o através do QEMU:

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

Vamos cavar por dentro:


Então nós temos:

  1. Distribuição - Linux mínimo 5.0.11.
  2. O dispositivo de caracteres /dev/activate está empenhado em verificar o correio, a chave, o que significa que a lógica de verificação precisa ser procurada em algum lugar nas entranhas do kernel.
  3. As chaves de correio são transmitidas no formato de email|key .

A imagem target_broken_activation.iso não será mais necessária.

3. Dispositivos de caracteres e o kernel


Dispositivos como /dev/mem , /dev/vcs , /dev/activate , etc. registre usando a função register_chrdev :

 int register_chrdev (unsigned int major, const char * name, const struct fops); 

name é o nome e a estrutura fops contém ponteiros para as funções do driver:

 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 *); }; 

Estamos interessados ​​apenas nesta função:

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

Aqui, o segundo argumento é o buffer com os dados transferidos, o próximo é o tamanho do buffer.

4. Pesquisa register_chrdev


Por padrão, o Minimal Linux compila com informações de depuração desabilitadas para reduzir o tamanho da imagem, mas mínimo. Portanto, você não pode simplesmente iniciar o depurador e encontrar a função pelo nome. Mas é possível por assinatura.

E a assinatura está na imagem Mínima do Linux com informações de depuração incluídas. Em geral, você precisa criar seu Minimal.

Ou seja, o esquema é o seguinte:

  Minimal Linux ->   register_chrdev ->  ->   register_chrdev  Lunix 

4.1 Preparando uma nova imagem mínima do Linux


  1. Instale as ferramentas necessárias:
     sudo apt install wget make gawk gcc bc bison flex xorriso libelf-dev libssl-dev 
  2. Download de scripts:

     git clone https://github.com/ivandavidov/minimal cd minimal/src 
  3. Corrija 02_build_kernel.sh :
    exclua

     # Disable debug symbols in kernel => smaller kernel binary. sed -i "s/^CONFIG_DEBUG_KERNEL.*/\\# CONFIG_DEBUG_KERNEL is not set/" .config 

    adicione

     echo "CONFIG_GDB_SCRIPTS=y" >> .config 

  4. Compilando

     ./build_minimal_linux_live.sh 

A imagem é minimal / src / minimal_linux_live.iso.

4.2 Mais alguns preparativos


Descompacte minimal_linux_live.iso na pasta minimal / src / iso.

O arquivo / src / iso / boot mínimo rootfs.xz kernel.xz do rootfs.xz e a rootfs.xz FS rootfs.xz . Renomeie-os para kernel.minimal.xz , rootfs.minimal.xz .

Além disso, você precisa extrair o núcleo da imagem. O script extract-vmlinux ajudará com isso:

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

Agora, na pasta minimal / src / iso / boot, temos este conjunto: kernel.minimal.xz , rootfs.minimal.xz , vmlinux.minimal .

Mas a partir do lunix.iso, precisamos apenas do kernel. Portanto, vmlinux.lunix todas as mesmas operações, chamamos o vmlinux.lunix kernel.xz , esquecemos o kernel.xz , rootfs.xz , agora vou lhe dizer o porquê.

4.3 Desativar KASLR no lunix


Consegui desativar o KASLR no caso do Minimal Linux recém-montado no QEMU.
Mas não deu certo com Lunix. Portanto, você deve editar a própria imagem.

Para fazer isso, abra-o em um editor hexadecimal, localize a linha "APPEND vga=normal" e substitua-a por "APPEND nokaslr\x20\x20\x20" .

E a imagem é chamada lunix_nokaslr.iso.

4.4 Pesquisamos e encontramos uma assinatura


Lançamos o Linux Minimal fresco em um terminal:

 sudo qemu-system-x86_64 -kernel kernel.minimal.xz -initrd rootfs.minimal.xz -append nokaslr -s 

Em outro depurador:

 sudo gdb vmlinux.minimal (gdb) target remote localhost:1234 

Agora, procure register_chrdev na lista de funções:


Obviamente, nossa opção é __register_chrdev .
Não estamos confusos por termos pesquisado register_chrdev, mas encontrado __register_chrdev

Desmonte:


Que assinatura levar? Tentei várias opções e resolvi a seguinte peça:

  0xffffffff811c9785 <+101>: shl $0x14,%esi 0xffffffff811c9788 <+104>: or %r12d,%esi 


O fato é que no lunix há apenas uma função que contém 0xc1, 0xe6, 0x14, 0x44, 0x09, 0xe6 .

Agora vou mostrar, mas primeiro descobrimos em qual segmento procurá-lo.


A função __register_chrdev endereço 0xffffffff811c9720 , este é o segmento .text . Lá vamos olhar.

Desconecte da referência Linux Mínimo. Conecte-se ao lunix agora.

Em um terminal:

 sudo qemu-system-x86_64 lunix_nokaslr.iso -s -enable-kvm 

Em outro:

 sudo gdb vmlinux.lunix (gdb) target remote localhost:1234 

Examinamos os limites do segmento .text :


Bordas 0xffffffff81000000 - 0xffffffff81600b91 , procure 0xc1, 0xe6, 0x14, 0x44, 0x09, 0xe6 :


Encontramos a peça no endereço 0xffffffff810dc643 . Mas isso é apenas parte da função, vamos ver o que está acima:


E aqui está o início da função 0xffffffff810dc5d0 (porque retq é a saída da função vizinha).

5. Pesquise pops em / dev / activar


O protótipo da função register_chrdev é este:

 int register_chrdev (unsigned int major, const char * name, const struct fops); 

Precisamos de uma estrutura de fops .

Reiniciando o depurador e o QEMU. 0xffffffff810dc5d0 pausa em 0xffffffff810dc5d0 . Funcionará várias vezes. Isso ativa os dispositivos mem, vcs, cpu/msr, cpu/cpuid e imediatamente após a activate deles.


O ponteiro para o nome é armazenado no rcx . E o ponteiro para fops está em r8 :


Lembro estrutura 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 *); }; 


Portanto, o endereço da função de write é 0xffffffff811f068f .

6. Estudamos escrever


A função inclui vários blocos interessantes. Não vale a pena descrever todos os pontos de interrupção, é uma rotina usual. Além disso, os blocos de cálculos são visíveis a olho nu.

6.1 Função hash


vmlinux.lunix abrir o IDA, carregar o kernel vmlinux.lunix e ver o que a função write tem dentro.

A primeira coisa a notar é este ciclo:


Alguma função sub_FFFFFFFF811F0413 é sub_FFFFFFFF811F0413 , que começa assim:


E no endereço 0xffffffff81829ce0 , uma tabela para sha256 é detectada:


Ou seja, sub_FFFFFFFF811F0413 = sha256. Os bytes cujo hash deve ser obtido são transmitidos por $sp+0x50+var49 e o resultado é armazenado em $sp+0x50+var48 . A propósito, var49=-0x49 , var48=-0x48 , então $sp+0x50+var49 = $sp+0x7 , $sp+0x50+var48 = $sp+0x8 .

Confira.

Iniciamos qemu, gdb, definimos uma interrupção na 0xffffffff811f0748 call sub_FFFFFFFF811F0413 e na instrução 0xffffffff811f074d xor ecx, ecx , que está imediatamente atrás da função. test@mail.ru email test@mail.ru , senha 1234-5678-0912-3456 .

O byte do correio é passado para a função e o resultado é este:


 >>> import hashlib >>> hashlib.sha256(b"t").digest().hex() 'e3b98a4da31a127d4bde6e43033f66ba274cab0eb7eb1c70ec41402bf6273dd8' >>> 

Ou seja, sim, é realmente sha256, apenas calcula hashes para todos os bytes de correio e não apenas um hash do correio.

Em seguida, os hashes são somados por byte. Mas se a soma for maior que 0xEC , o restante da divisão por 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 

O valor é salvo em 0xffffffff81c82f80 . Vamos ver qual será o hash de test@mail.ru .

ffffffff811f0786 dec r13d pausa no ffffffff811f0786 dec r13d (esta é a saída do loop):


E compare com:

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

Mas o hash em si é claramente um pouco longo para a chave.

6.2 Algoritmo de geração de chaves


A chave é responsável por este código:


Aqui está o cálculo final de cada byte:

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

Nos bytes de hash eax e r12d , eles são multiplicados e o restante da divisão por 9 é obtido.

Porque


E os bytes são obtidos em ordem inesperada. Vou indicá-lo em keygen.

6.3 Keygen


 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)] 

Então, vamos gerar algumas chaves:

 >>> import lunix >>> lunix.keygen("m.gayanov@gmail.com") ['0456', '3530', '0401', '2703'] 


E agora você pode relaxar e jogar o jogo 2048 :) Obrigado pela atenção! Código aqui

Source: https://habr.com/ru/post/pt471778/


All Articles