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/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. - 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