Un poco sobre reubicaciones en el kernel de Linux

Resolveremos un problema simple: seleccione un bloque de memoria en el espacio del kernel de Linux, coloque un c贸digo binario y ejec煤telo. Para hacer esto, escribimos un m贸dulo kernel, en 茅l definimos la funci贸n foo, que desempe帽ar谩 el papel del c贸digo binario que necesitamos, luego, usando la funci贸n module_alloc, seleccione el bloque de memoria, copie toda esta funci贸n a trav茅s de memcpy y le damos el control.

As铆 es como se ve:

static noinline int foo(int ret)
{
	return (ret + 2);
}

static int exe_init(void)
{
	int ret = 0;
	int (*new_foo)(int);

	ret = foo(0);
	printk(KERN_INFO "ret=%d\n", ret);

	new_foo = module_alloc(PAGE_SIZE);
	set_memory_x((unsigned long)new_foo, 1);

	printk(KERN_INFO "foo=%lx new_foo=%lx\n",
		(unsigned long)foo, (unsigned long)new_foo);

	memcpy((void *)new_foo, (const void *)foo, PAGE_SIZE);

	ret = new_foo(1);
	printk(KERN_INFO "ret=%d\n", ret);

	vfree(new_foo);
	return 0;
}

Se llama a la funci贸n exe_init cuando se carga el m贸dulo. Observamos el resultado del trabajo en el registro del kernel:

[ 6972.522422] ret=2
[ 6972.522443] foo=ffffffffc0000000 new_foo=ffffffffc0007000
[ 6972.522457] ret=3

Todo esta funcionando correctamente. Y ahora agregamos la funci贸n printk a foo para mostrar el argumento:

static noinline int foo(int ret)
{
	printk(KERN_INFO "ret=%d\n", ret);
	return (ret + 2);
}

y volcar 25 bytes del contenido de la funci贸n new_foo () antes de pasarle el control:

	memcpy((void *)new_foo, (const void *)foo, PAGE_SIZE);
	dump((unsigned long)new_foo);

volcado se define como

static inline void dump(unsigned long x)
{
	int i;
	for (i = 0; i < 25; i++) \
		pr_cont("%.2x ", *((unsigned char *)(x) + i) & 0xFF); \
	pr_cont("\n");
}

Cargamos el m贸dulo y tenemos un bloqueo con el siguiente mensaje en el registro:

[ 8482.806092] ret=0
[ 8482.806092] ret=2
[ 8482.806111] foo=ffffffffc0000000 new_foo=ffffffffc0007000
[ 8482.806113] 53 89 fe 89 fb 48 c7 c7 24 10 00 c0 e8 e8 3d 0b c1 8d 43 02 5b c3 66 2e 0f 
[ 8482.806135] invalid opcode: 0000 [#1] SMP NOPTI
[ 8482.806639] CPU: 0 PID: 5081 Comm: insmod Tainted: G           O      5.4.27 #12
[ 8482.807669] Hardware name: innotek GmbH VirtualBox/VirtualBox, BIOS VirtualBox 12/01/2006
[ 8482.808560] RIP: 0010:irq_create_direct_mapping+0x79/0x90

De alguna manera, terminamos en la funci贸n irq_create_direct_mapping, aunque tuvimos que llamar a printk. Averig眉emos qu茅 pas贸.

Primero, eche un vistazo a la lista desmontada de la funci贸n foo. Cons铆guelo con el comando objdump -d:

Disassembly of section .text:

0000000000000000 <foo>:
   0:	53                   	push   %rbx
   1:	89 fe                	mov    %edi,%esi
   3:	89 fb                	mov    %edi,%ebx
   5:	48 c7 c7 00 00 00 00 	mov    $0x0,%rdi
   c:	e8 00 00 00 00       	callq  11 <foo+0x11>
  11:	8d 43 02             	lea    0x2(%rbx),%eax
  14:	5b                   	pop    %rbx
  15:	c3                   	retq   
  16:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
  1d:	00 00 00 

La funci贸n foo se encuentra al comienzo de la secci贸n de texto. En el desplazamiento 0xC, se encuentra el c贸digo de operaci贸n del comando de llamada cercana e8, ya que se ejecuta en el segmento de c贸digo actual, el valor del selector no cambia. Los siguientes 4 bytes son el desplazamiento relativo al valor en el registro RIP al cual se transferir谩 el control, es decir. RIP = RIP + offset, seg煤n la documentaci贸n de Intel (Intel 64 e IA-32 Architectures Software Developer's Manual, Instruction Reference Reference AZ):
Un desplazamiento relativo (rel16 o rel32) generalmente se especifica como una etiqueta en el c贸digo de ensamblaje. Pero a nivel de c贸digo de m谩quina, se codifica como un valor inmediato firmado de 16 o 32 bits.
Este valor se agrega al valor en el registro EIP (RIP). En el modo de 64 bits, el desplazamiento relativo es siempre un valor inmediato de 32 bits que se extiende a 64 bits antes de agregarlo al valor en el registro RIP para el c谩lculo del objetivo.

Conocemos la direcci贸n de la funci贸n foo, es 0xffffffffc0000000, entonces en RIP = 0xffffffffc0000000 + 0xc + 0x5 = 0xffffffffc00000011 (0xc es el desplazamiento de la instrucci贸n e8, 1 byte de la instrucci贸n y 4 bytes del desplazamiento). Conocemos el desplazamiento, porque funciones del cuerpo objeto de dumping. Calculemos a d贸nde enviar谩 la llamada para enviarnos a la funci贸n foo:

0xffffffffc00000011 + 0xffffffffc10b3de8 = 0xffffffff810b3df9

Esta es la direcci贸n de la funci贸n printk:
# cat /proc/kallsyms | grep ffffffff810b3df9  
ffffffff810b3df9 T printk

Y ahora lo mismo ocurre con new_foo, cuya direcci贸n es 0xffffffffc0007000

0xffffffffc0007011 + 0xffffffffc10b3de8 = 0xffffffff810badf9

No existe tal direcci贸n en kallsyms, pero hay 0xffffffff810badf9 - 0x79 = 0xffffffff810bad80
# cat /proc/kallsyms | grep ffffffff810bad80
ffffffff810bad80 T irq_create_direct_mapping

Esta es la funci贸n en la que ocurri贸 el bloqueo.

Para evitar un bloqueo, simplemente recalcule el desplazamiento, conociendo la direcci贸n de la funci贸n new_foo:

memcpy((void *)new_foo, (const void *)foo, PAGE_SIZE);
unsigned int delta = (unsigned long)printk - (unsigned long)new_foo - 0x11;
*(unsigned int *)((void *)new_foo + 0xD) = delta;

Despu茅s de esta correcci贸n, no habr谩 bloqueo, la funci贸n new_foo se ejecutar谩 con 茅xito y devolver谩 el control.

El problema esta resuelto. Solo queda entender por qu茅 en el desensamblador enumera el desplazamiento despu茅s de que el c贸digo de operaci贸n e8 es cero, pero no hay ninguna funci贸n en el volcado. Para hacer esto, considere qu茅 son las reubicaciones y c贸mo funciona el n煤cleo con ellas. Pero primero, un poco sobre el formato ELF.

ELF significa formato ejecutable y enlazable, el formato de archivos ejecutables y componibles. Un archivo ELF es una colecci贸n de secciones. La secci贸n almacena un conjunto de objetos necesarios para que el enlazador forme una imagen ejecutable: instrucciones, datos, tablas de s铆mbolos, registros de reubicaciones, etc. Cada secci贸n se describe mediante un encabezado. Todos los encabezados se recopilan en una tabla de encabezados y son esencialmente una matriz donde cada elemento tiene un 铆ndice. El encabezado de la secci贸n contiene un desplazamiento al comienzo de la secci贸n y otra informaci贸n general, como enlaces a otras secciones al especificar un 铆ndice en la tabla del encabezado.

Al armar nuestro caso de prueba, el compilador no conoce la direcci贸n de la funci贸n printk, por lo tanto, llena la ubicaci贸n de la llamada con un valor cero y, utilizando un registro de reubicaci贸n, le dice al n煤cleo que esta posici贸n debe llenarse con un valor v谩lido. Un registro de reubicaci贸n contiene un desplazamiento a la posici贸n en la que desea realizar cambios (posici贸n de reubicaci贸n), el tipo de reubicaci贸n y el 铆ndice del s铆mbolo en la tabla de s铆mbolos, cuya direcci贸n debe sustituirse en el desplazamiento especificado. 驴Para qu茅 es el tipo de reubicaci贸n? Considere a continuaci贸n. El encabezado de la secci贸n de registros de reubicaci贸n se refiere a trav茅s de 铆ndices a los encabezados de la secci贸n con una tabla de caracteres y secciones, en relaci贸n con el comienzo del cual se especifica un desplazamiento a la posici贸n de la reubicaci贸n.

Puede ver el contenido de los registros de reubicaci贸n utilizando la utilidad objdump con el modificador -r.
De la lista desmontada, sabemos que en el desplazamiento 0xD es necesario escribir la direcci贸n de la funci贸n printk, por lo tanto, buscamos la salida objdump con la siguiente posici贸n:

000000000000000d R_X86_64_PC32     printk-0x0000000000000004

Entonces, tenemos el registro de reubicaci贸n necesario que indica la posici贸n en el desplazamiento 0xD, y el nombre del s铆mbolo cuya direcci贸n debe escribirse en esta posici贸n.

Valor (-4). que se agrega a la direcci贸n de la funci贸n printk se denomina anexo y se tiene en cuenta al calcular el resultado final de la reubicaci贸n.

Ahora mira el s铆mbolo printk:

$ objdump -t exe.ko | grep printk
0000000000000000         *UND*	0000000000000000 printk

Hay un s铆mbolo, est谩 indefinido dentro del m贸dulo (indefinido), por lo que lo buscaremos en el n煤cleo.

Ser谩 m谩s informativo mirar los registros de reubicaci贸n y s铆mbolos en forma binaria. Esto se puede hacer con wireshark, puede analizar el formato ELF. Aqu铆 est谩 nuestra entrada de reubicaci贸n (copie y pegue de writeshark, LSB a la izquierda):

  0d 00 00 00 00 00 00 00  02 00 00 00 22 00 00 00  fc ff ff ff ff ff ff ff
  |                     |  |          ||         |  |                     |
  +----  -------+  +--  ---++---+  +---- addendum  ------+

Compare esta entrada con la definici贸n de la estructura correspondiente de <linux / elf.h>:

typedef struct elf64_rela {
  Elf64_Addr r_offset;	/* Location at which to apply the action */
  Elf64_Xword r_info;	/* index and type of relocation */
  Elf64_Sxword r_addend;	/* Constant addend used to compute value */
} Elf64_Rela;

Aqu铆 tenemos 8 bytes de desplazamiento 0x00000000d, 4 bytes tipo 0x00000002, 铆ndice de 4 bytes en la tabla de caracteres 0x00000022 (o 34 en decimal) y 8 bytes de adici贸n -4.

Y aqu铆 est谩 la entrada de la tabla de s铆mbolos en el n煤mero 34:

  01 01 00 00 10 00 00 00  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00

y estructura relacionada

typedef struct elf64_sym {
  Elf64_Word st_name;		/* Symbol name, index in string tbl */
  unsigned char	st_info;	/* Type and binding attributes */
  unsigned char	st_other;	/* No defined meaning, 0 */
  Elf64_Half st_shndx;		/* Associated section index */
  Elf64_Addr st_value;		/* Value of the symbol */
  Elf64_Xword st_size;		/* Associated symbol size */
} Elf64_Sym;

Los primeros 4 bytes 0x00000101 es el 铆ndice en la tabla de cadenas .strtab al nombre de este car谩cter, es decir. printk. El campo st_info define el tipo de s铆mbolo, puede ser una funci贸n, un objeto de datos, etc. Consulte la especificaci贸n ELF para obtener m谩s detalles. Omitiremos el campo st_other, ahora no nos interesa y miraremos los 煤ltimos tres campos st_shndx, st_value y st_size. st_shndx: el 铆ndice del encabezado de la secci贸n en la que se define el car谩cter. Vemos aqu铆 un valor cero, porque el s铆mbolo no est谩 definido dentro del m贸dulo, no est谩 en las secciones disponibles.
En consecuencia, su valor st_value y su tama帽o st_size tambi茅n son cero. Estos campos ser谩n llenados por el n煤cleo al cargar el m贸dulo.

Para comparar, mira el s铆mbolo foo, que est谩 claramente presente:

  08 00 00 00 02 00 02 00  00 00 00 00 00 00 00 00  16 00 00 00 00 00 00 00

El s铆mbolo define una funci贸n que se encuentra en la secci贸n .text en la direcci贸n relativa al comienzo de la secci贸n 0x00000000, es decir. Al principio de la secci贸n, como vimos en la lista desmontada, el tama帽o de la funci贸n es de 22 bytes.

Objdump nos mostrar谩 la misma informaci贸n sobre esto:

$ objdump -t exe.ko | grep foo
0000000000000000 l     F .text	0000000000000016 foo

Cuando el n煤cleo carga el m贸dulo, encuentra todos los caracteres Indefinidos y llena los campos st_value y st_size con valores v谩lidos. Esto se hace en la funci贸n simplify_symbols, archivo kernel / module.c:

/* Change all symbols so that st_value encodes the pointer directly. */
static int simplify_symbols(struct module *mod, const struct load_info *info)
{
...

En los par谩metros de la funci贸n, se pasa la estructura load_info del siguiente formulario

struct load_info {
	const char *name;
	/* pointer to module in temporary copy, freed at end of load_module() */
	struct module *mod;
	Elf_Ehdr *hdr;
	unsigned long len;
	Elf_Shdr *sechdrs;
	char *secstrings, *strtab;
	unsigned long symoffs, stroffs, init_typeoffs, core_typeoffs;
	struct _ddebug *debug;
	unsigned int num_debug;
	bool sig_ok;
#ifdef CONFIG_KALLSYMS
	unsigned long mod_kallsyms_init_off;
#endif
	struct {
		unsigned int sym, str, mod, vers, info, pcpu;
	} index;
};

Los siguientes campos nos interesan:
- hdr - encabezado de archivo ELF
- sechdrs - puntero a la tabla de encabezado de secci贸n
- strtab - tabla de nombre de s铆mbolo - un conjunto de cadenas separadas por ceros
- index.sym - 铆ndice del encabezado de secci贸n que contiene la tabla de s铆mbolo En

primer lugar, la funci贸n tendr谩 acceso a la secci贸n con la tabla de s铆mbolos. La tabla de s铆mbolos es una matriz de elementos de tipo Elf64_Sym:

Elf64_Shdr *symsec = &info->sechdrs[info->index.sym];
Elf64_Sym *sym = (void *)symsec->sh_addr;

A continuaci贸n, en el bucle, revisamos todos los caracteres de la tabla, determinando para cada uno su nombre:
for (i = 1; i < symsec->sh_size / sizeof(Elf_Sym); i++) {
	const char *name = info->strtab + sym[i].st_name;

El campo st_shndx contiene el 铆ndice del encabezado de la secci贸n en la que se define este car谩cter. Si hay un valor cero (nuestro caso), entonces este s铆mbolo no est谩 dentro del m贸dulo, debe buscarlo en el n煤cleo:

	switch (sym[i].st_shndx) {
	.....
	 case SHN_UNDEF: //  0
	ksym = resolve_symbol_wait(mod, info, name);
 	/* Ok if resolved.  */
	if (ksym && !IS_ERR(ksym)) {
		sym[i].st_value = kernel_symbol_value(ksym);
		break;
	}

Luego viene la cola de reubicaci贸n en la funci贸n apply_relocations:

static int apply_relocations(struct module *mod, const struct load_info *info)
{
	unsigned int i;
	int err = 0;

	/* Now do relocations. */
	for (i = 1; i < info->hdr->e_shnum; i++) {
	.....

En el bucle, buscamos secciones de reubicaci贸n y procesamos los registros de cada secci贸n que se encuentra en la funci贸n apply_relocate_add:

if (info->sechdrs[i].sh_type == SHT_RELA) //   
	err = apply_relocate_add(info->sechdrs, info->strtab,
				info->index.sym, i, mod);

Un puntero a una tabla de encabezado de secci贸n, un puntero a una tabla de nombre de s铆mbolo, un 铆ndice de encabezado de secci贸n con una tabla de s铆mbolos y un 铆ndice de encabezado de secci贸n de reubicaci贸n se pasan a apply_relocate_add:

int apply_relocate_add(Elf64_Shdr *sechdrs,
	   const char *strtab,
	   unsigned int symindex,
	   unsigned int relsec,
	   struct module *me)
{

Primero abordamos la secci贸n de reubicaciones:

Elf64_Rela *rel = (void *)sechdrs[relsec].sh_addr;

Luego, en un bucle, itere sobre la matriz de sus entradas:

for (i = 0; i < sechdrs[relsec].sh_size / sizeof(*rel); i++) {

Encontramos la secci贸n para la reubicaci贸n y la posici贸n en ella, es decir. donde necesitamos hacer cambios. El campo sh_info del encabezado de la secci贸n de reubicaci贸n es el 铆ndice del encabezado de secci贸n para la reubicaci贸n, el campo r_offset del registro de reubicaci贸n es el desplazamiento a la posici贸n dentro de la secci贸n para la reubicaci贸n:

/* This is where to make the change */
loc = (void *)sechdrs[sechdrs[relsec].sh_info].sh_addr + rel[i].r_offset;

La direcci贸n del personaje que se sustituir谩 en esta posici贸n, teniendo en cuenta la adici贸n. El campo r_info de la entrada de reubicaci贸n contiene el 铆ndice de este s铆mbolo en la tabla de s铆mbolos:

	/* This is the symbol it is referring to.  Note that all
	   undefined symbols have been resolved.  */
	sym = (Elf64_Sym *)sechdrs[symindex].sh_addr
		+ ELF64_R_SYM(rel[i].r_info);

	val = sym->st_value + rel[i].r_addend;

El tipo de reubicaci贸n determina el resultado final de los c谩lculos, en nuestro ejemplo es R_X86_64_PLT32:

	switch (ELF64_R_TYPE(rel[i].r_info)) {
	......
	case R_X86_64_PLT32:	
		if (*(u32 *)loc != 0)
			goto invalid_relocation;
		val -= (u64)loc;	//   
		*(u32 *)loc = val;  //    
		break;
	.....

Ahora podemos calcular el valor final nosotros mismos, sabiendo que sym-> st_value es la direcci贸n de la funci贸n printk 0xffffffff810b3df9, r_addend es (-4), el desplazamiento a la posici贸n de reubicaci贸n es 0xd desde el comienzo de la secci贸n de texto del m贸dulo, o desde el comienzo de la funci贸n foo, es decir. ser谩 ffffffffc000000d. Sustituya todos estos valores y obtenga:

val = (u32)(0xffffffff810b3df9 - 0x4 - 0xffffffffc000000d) = 0xc10b3de8

Veamos el volcado de la funci贸n foo, que obtuvimos al principio:

53 89 fe 89 fb 48 c7 c7 24 10 00 c0 e8 e8 3d 0b c1 8d 43 02 5b c3 66 2e 0f

En el desplazamiento 0xD, se encuentra el valor 0xc10b3de8, que es id茅ntico al que calculamos.

As铆 es como el n煤cleo procesa las reubicaciones y obtiene el desplazamiento necesario para el comando de cierre de llamada.

Al preparar el art铆culo, se utiliz贸 la versi贸n 5.4.27 del kernel de Linux.

All Articles