Esta é a última parte da série “Sequestro de chamadas de sistema Linux”. Até agora, nós criamos um módulo de kernel carregável simples que registra um dispositivo de caractere miscellaneous. Isso significa que nós temos tudo que precisamos para corrigir a tabela de chamadas do sistema. Quase tudo, para ser honesto. Nós ainda temos que preencher a função our_ioctl e acrescentar algumas declarações ao nosso arquivo de origem. Ao final deste artigo, seremos capazes de interceptar qualquer chamada de sistema em nosso sistema.
Tabela de chamada de sistema
A tabela de chamada de sistema é simplesmente uma área no espaço de memória do kernel que contém endereços de handlers de chamada do sistema. Na verdade, um número de chamada de sistema é um offset nessa tabela. Isso significa que quando chamamos sys_write (para ser mais preciso – quando libc chama sys_write) em um sistema de 32 bits e passa o número 4 em registo EAX antes de int 0x80, ele simplesmente diz ao kernel para ir para a tabela de chamada do sistema, obter o valor no offset 4 do endereço da tabela de chamada do sistema e chamar a função para a qual o endereço aponta. Pode ser o número 1 em RAX no caso de um sistema de 64 bits (e syscall em vez de int 0x80). Os números de chamada de sistema são definidos em arch/x86/include/asm/unistd_32.h e arch/x86/include/asm/unistd_64.h para plataformas de 32 e de 64 bits, respectivamente. Neste artigo, vamos lidar com chamada de sistema sys_open, que é o número 5 para sistemas de 32 bits e o número 2 para sistemas de 64 bits.
Devido ao fato de os kernels modernos não exportarem mais o símbolo sys_call_table, teremos de encontrar, nós mesmos, a sua localização na memória. Existem algumas maneiras “obscuras” de encontrar a localização sys_call_table de forma programática, mas elas podem ou não funcionar. Especialmente a forma como são escritas. Portanto, vamos usar a forma mais simples e mais segura – leia a sua localização a partir do arquivo /boot/System.map. Por razões de simplicidade, vamos apenas usar o grep e pegar o endereço. No meu computador, o comando grep “sys_call_table” /boot/System.map (você deve verificar o nome do arquivo no seu sistema, como no meu é /boot/System.map-2.6.38-11-generic) dá a saída “ffffffff816002e0 R sys_call_table“. Adicionar a variável global unsigned long * sys_call_table = (unsigned long*) 0xYour_Address_Of_Sys_call_table.
Preparativos
Vamos começar, como de costume, fazendo novas inclusões ao nosso código. Desta vez, os arquivos de inclusão são:
#include <linux/highmem.h>
#include <asm/unistd.h>
O primeiro é necessário devido ao fato de que a tabela de chamada de sistema está localizada na área somente leitura da memória em kernels modernos, e teremos que modificar os atributos de proteção da página de memória que contêm o endereço da chamada de sistema que queremos interceptar. O segundo é autoexplicativo após o parágrafo anterior. Não vamos usar valores codificados para chamadas de sistema, em vez disso, vamos usar os valores definidos no header unistd.h.
Agora vamos definir dois valores, que seriam usados como argumento cmd para a função our_ioctl. Um vai nos dizer para corrigir a tabela, outro dirá para corrigi-la restaurando o valor original.
/* IOCTL commands */
#define IOCTL_PATCH_TABLE 0x00000001
#define IOCTL_FIX_table 0x00000004
Adicione mais uma variável global int is_set=0, que será utilizada como flag informando se a chamada do sistema real (0) ou personalizada (1) está em uso.
É importante salvar o endereço do sys_open original já que não vamos implementar integralmente o nosso, em vez disso, nossa função vai registrar informações sobre os argumentos de chamada e, em seguida, realizar a chamada (original) real. Portanto, nós definimos uma função ponteiro (para chamada original) e uma função (para chamadas personalizadas):
/* Pointer to the original sys_open */
asmlinkage int (*real_open)(const char* __user, int, int);
/* Our replacement */
asmlinkage int custom_open(const char* __user file_name, int flags, int mode)
{
printk("interceptor: open(\"%s\", %X, %X)\n", file_name,
flags,
mode);
return real_open(file_name, flags, mode);
}
Você observou o atributo “asmlinkage“. Bem, ele é, na verdade, uma definição para o atributo. Não vamos nos aprofundar dessa vez, vou apenas dizer que esse atributo informa ao compilador como ele deve passar argumentos para a função, uma vez que está sendo chamado de um código assembly. O macro “__user” significa que o argumento está no espaço do usuário e a função deve executar determinadas operações para copiar para o espaço do kernel quando necessário. Nós não precisamos disso, o que significa que podemos ignorá-lo por agora.
Outra função crucial é o conjunto que nos permitirá modificar os atributos de memória de proteção de página diretamente. Alguém pode dizer que o dele é arriscado, mas, na minha opinião, isso é menos arriscado que realmente fazer uma correção na tabela de chamada de sistema como ela é; em primeiro lugar, dependente da arquitetura, e sabemos que as arquiteturas não mudam drasticamente; em segundo, nós usamos as funções do kernel para isso.
/* Make the page writable */
int make_rw(unsigned long address)
{
unsigned int level;
pte_t *pte = lookup_address(address, &level);
if(pte->pte &~ _PAGE_RW)
pte->pte |= _PAGE_RW;
return 0;
}
/* Make the page write protected */
int make_ro(unsinged long address)
{
unsigned int level;
pte_t *pte = lookup_address(address, &level);
pte->pte = pte->pte &~ _PAGE_RW;
return 0;
}
pte_t significa typedef struct {unsigned long pte} pte_t e representa page table entry. Embora seja simplesmente um unsigned long, ele é declarado como struct, a fim de evitar o uso indevido de tipo.
pte_t * lookup_address (unsigned long address, unsigned int *level) é fornecido pelo kernel e executa todo o trabalho sujo para gente, além de retornar um ponteiro para page table entry que descreve a page que contém o endereço. Esta função aceita os seguintes argumentos:
address – o endereço na memória virtual;
level – ponteiro para valores inteiros sem assinatura que aceitam o nível do mapeamento.
Vamos ao que interessa
Estamos quase lá. A única coisa que resta é a implementação real da função our_ioctl. Adicione as seguintes linhas:
switch(cmd)
{
case IOCTL_PATCH_TABLE:
make_rw((unsigned long)sys_call_table);
real_open = (void*)*(sys_call_table + __NR_open);
*(sys_call_table + __NR_open) = (unsigned long)custom_open;
make_ro((unsigned long)sys_call_table);
is_set=1;
break;
case IOCTL_FIX_TABLE:
make_rw((unsigned long)sys_call_table);
*(sys_call_table + __NR_open) = (unsigned long)real_open;
make_ro((unsigned long)sys_call_table);
is_set=0;
break;
default:
printk("Ooops....\n");
break;
}
E estas linhas para a função cleanup_module:
if(is_set)
{
make_rw((unsigned long)sys_call_table);
*(sys_call_table + __NR_open) = (unsigned long)real_open;
make_ro((unsigned long)sys_call_table);
}
O nosso módulo de interceptor está pronto. Bem, quase pronto, pois precisamos compilá-lo. Faça isso, como de costume – faça.
Teste
Finalmente temos o nosso módulo configurado e pronto para usar, mas temos que criar um aplicativo do “cliente” , o código que vai “conversar” com o nosso módulo e dizer a ele o que fazer. Felizmente, isso é muito mais simples do que o resto do trabalho que fizemos aqui. Crie um novo arquivo de origem e digite as seguintes linhas:
#include <stdio.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
/* Define ioctl commands */
#define IOCTL_PATCH_TABLE 0x00000001
#define IOCTL_FIX_TABLE 0x00000004
int main(void)
{
int device = open("/dev/interceptor", O_RDWR);
ioctl(device, IOCTL_PATCH_TABLE);
sleep(5);
ioctl(device, IOCTL_FIX_TABLE);
close(device);
return 0;
}
salve como s manager.c e compile-o com o gcc -o manager manager.c.
Carregue o módulo, execute ./manager e depois descarregue o módulo quando o manager sair. Se você emitir o comando dmesg | tail, se você vir linhas que contenham “interceptor: open (bla bla bla)“, então você saberá que essas linhas foram produzidas pelo nosso handler.
Agora somos capazes de interceptar chamadas de sistema em kernels modernos, apesar do fato de que sys_call_table já não é exportado. Embora nós lidamos com estruturas de baixo nível, que normalmente só são usadas pelo kernel, este ainda é um método relativamente seguro, desde que seu módulo seja compilado com o kernel em execução.
Espero que este artigo tenha sido útil. Vejo vocês na próxima!
?
Texto original disponível em http://syprog.blogspot.com.br/2011/10/hijack-linux-system-calls-part-iii.html







