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. Ferramentas2. Inspecione a imagem3. Dispositivos de caracteres e o kernel4. Pesquisa register_chrdev4.1 Preparando uma nova imagem mínima do Linux4.2 Mais alguns preparativos4.3 Desativar KASLR no lunix4.4 Pesquisamos e encontramos uma assinatura5. Procure por fops em / dev / active e a função write6. Estudamos escrever6.1 Função hash6.2 Algoritmo de geração de chaves6.3 KeygenDesafio
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
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:
- Distribuição - Linux mínimo 5.0.11.
- O dispositivo de caracteres /dev/activateestá 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.
- 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
- Instale as ferramentas necessárias:
  sudo apt install wget make gawk gcc bc bison flex xorriso libelf-dev libssl-dev
 
- Download de scripts:
 
  git clone https://github.com/ivandavidov/minimal cd minimal/src
 
- 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
 
 
- 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_chrdevDesmonte:
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