Sedikit tentang relokasi di kernel Linux

Kami akan memecahkan masalah sederhana - pilih blok memori di ruang kernel Linux, masukkan beberapa kode biner ke dalamnya dan jalankan. Untuk melakukan ini, kita menulis modul kernel, di dalamnya kita mendefinisikan fungsi foo, yang akan memainkan peran kode biner yang kita butuhkan, kemudian, menggunakan fungsi module_alloc, pilih blok memori, salin seluruh fungsi ini ke dalamnya melalui memcpy dan berikan kontrol.

Begini tampilannya:

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

Fungsi exe_init dipanggil saat modul dimuat. Kami melihat hasil kerja di log kernel:

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

Semuanya bekerja dengan benar. Dan sekarang kita menambahkan fungsi printk ke foo untuk menampilkan argumen:

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

dan membuang 25 byte konten fungsi new_foo () sebelum memberikan kontrol padanya:

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

dump didefinisikan sebagai

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

Kami memuat modul dan mengalami kerusakan dengan pesan berikut di log:

[ 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

Entah bagaimana, kami berakhir di fungsi irq_create_direct_mapping, meskipun kami harus memanggil printk. Mari kita cari tahu apa yang terjadi.

Pertama, lihat daftar fungsi foo yang dibongkar. Dapatkan dengan perintah 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 

Fungsi foo terletak di awal bagian teks. Pada offset 0xC, opcode dari perintah panggilan dekat e8 terletak - dekat, karena dijalankan di segmen kode saat ini, nilai pemilih tidak berubah. 4 byte berikutnya adalah offset relatif terhadap nilai dalam register RIP yang kontrolnya akan ditransfer, mis. RIP = offset RIP +, sesuai dengan dokumentasi Intel (Manual Pengembang Perangkat Lunak Arsitektur Intel 64 dan IA-32, Referensi Set Instruksi AZ):
Offset relatif (rel16 atau rel32) umumnya ditentukan sebagai label dalam kode rakitan. Tetapi pada level kode mesin, kode tersebut dikodekan sebagai nilai langsung yang ditandatangani, 16 atau 32 bit.
Nilai ini ditambahkan ke nilai dalam register EIP (RIP). Dalam mode 64-bit, offset relatif selalu merupakan nilai langsung 32-bit yang tandanya diperluas menjadi 64-bit sebelum ditambahkan ke nilai dalam register RIP untuk perhitungan target.

Kita tahu alamat fungsi foo, yaitu 0xffffffffc0000000, jadi dalam RIP = 0xffffffffc0000000 + 0xc + 0x5 = 0xffffffffc00000011 (0xc adalah offset untuk instruksi e8, 1 byte instruksi dan 4 byte offset). Kami tahu offsetnya, karena fungsi tubuh dibuang. Mari kita hitung ke mana panggilan untuk mengirim kita ke fungsi yang akan dikirim:

0xffffffffc00000011 + 0xffffffffc10b3de8 = 0xffffffff810b3df9

Ini adalah alamat fungsi printk:
# cat /proc/kallsyms | grep ffffffff810b3df9  
ffffffff810b3df9 T printk

Dan sekarang hal yang sama berlaku untuk new_foo, yang alamatnya 0xffffffffc0007000

0xffffffffc0007011 + 0xffffffffc10b3de8 = 0xffffffff810badf9

Tidak ada alamat seperti itu di kallsyms, tetapi ada 0xffffffff810badf9 - 0x79 = 0xffffffff88badbad80
# cat /proc/kallsyms | grep ffffffff810bad80
ffffffff810bad80 T irq_create_direct_mapping

Ini adalah fungsi dimana crash terjadi.

Untuk mencegah kerusakan, hitung ulang offsetnya, dengan mengetahui alamat fungsi 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;

Setelah koreksi ini, tidak akan ada kerusakan, fungsi new_foo akan berhasil menjalankan dan mengembalikan kontrol.

Masalah terpecahkan. Tetap hanya untuk memahami mengapa dalam daftar disassembler offset setelah opcode e8 adalah nol, tetapi tidak ada fungsi dalam dump. Untuk melakukan ini, pertimbangkan relokasi apa dan bagaimana kernel bekerja dengannya. Tapi pertama-tama, sedikit tentang format ELF.

ELF adalah singkatan dari Executable and Linkable Format - format file yang dapat dieksekusi dan komposit. File ELF adalah kumpulan bagian. Bagian menyimpan satu set objek yang diperlukan untuk linker untuk membentuk gambar yang dapat dieksekusi - instruksi, data, tabel simbol, catatan relokasi, dll. Setiap bagian dijelaskan oleh tajuk. Semua header dikumpulkan dalam tabel header dan pada dasarnya adalah array di mana setiap elemen memiliki indeks. Header bagian berisi offset ke awal bagian dan informasi overhead lainnya, seperti tautan ke bagian lain dengan menentukan indeks dalam tabel header.

Ketika merakit test case kami, kompiler tidak tahu alamat fungsi printk, oleh karena itu mengisi lokasi panggilan dengan nilai nol dan, menggunakan catatan relokasi, memberi tahu kernel bahwa posisi ini harus diisi dengan nilai yang valid. Catatan relokasi berisi offset ke posisi di mana Anda ingin melakukan perubahan (posisi relokasi), jenis relokasi dan indeks simbol dalam tabel simbol, alamat yang harus diganti pada offset yang ditentukan. Untuk apa jenis relokasi? Kami pertimbangkan di bawah ini. Judul bagian catatan relokasi merujuk melalui indeks ke pos bagian dengan tabel karakter dan bagian, relatif terhadap permulaan yang ditentukan offset ke posisi relokasi.

Anda dapat melihat isi catatan relokasi menggunakan utilitas objdump dengan saklar -r.
Dari daftar dibongkar, kita tahu bahwa pada offset 0xD perlu untuk menulis alamat fungsi printk, jadi kami mencari output objdump dengan posisi berikut:

000000000000000d R_X86_64_PC32     printk-0x0000000000000004

Jadi, kami memiliki catatan relokasi yang diperlukan yang menunjukkan posisi pada offset 0xD, dan nama simbol yang alamatnya harus ditulis untuk posisi ini.

Nilai (-4). yang ditambahkan ke alamat fungsi printk disebut addendum, dan diperhitungkan saat menghitung hasil akhir relokasi.

Sekarang lihat simbol printk:

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

Ada simbol, itu tidak terdefinisi di dalam modul (tidak terdefinisi), jadi kami akan mencarinya di kernel.

Akan lebih informatif untuk melihat catatan relokasi dan simbol dalam bentuk biner. Ini dapat dilakukan dengan menggunakan wireshark, dapat mem-parsing format ELF. Inilah entri relokasi kami (salin tempel dari writeshark, LSB di sebelah kiri):

  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  ------+

Bandingkan entri ini dengan definisi struktur yang sesuai dari <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;

Di sini kita memiliki 8 byte offset 0x00000000d, 4 byte tipe 0x00000002, 4 byte indeks dalam tabel karakter 0x00000022 (atau 34 dalam desimal) dan 8 byte addendum -4.

Dan di sini ada entri dari tabel simbol di nomor 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

dan struktur terkait

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;

4 byte pertama 0x00000101 adalah indeks dalam tabel string .strtab dengan nama karakter ini, mis. printk. Kolom st_info mendefinisikan jenis simbol, bisa berupa fungsi, objek data, dll., Lihat spesifikasi ELF untuk detail lebih lanjut. Kami akan melewati bidang st_other, sekarang tidak menarik bagi kami, dan melihat tiga bidang terakhir st_shndx, st_value dan st_size. st_shndx - indeks header bagian di mana karakter didefinisikan. Kami melihat di sini nilai nol, karena simbol tidak didefinisikan di dalam modul, itu tidak ada di bagian yang tersedia.
Dengan demikian, nilai st_value dan ukuran st_size juga nol. Bidang-bidang ini akan diisi oleh kernel saat memuat modul.

Sebagai perbandingan, lihat simbol foo, yang jelas ada:

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

Simbol mendefinisikan fungsi yang terletak di bagian .text di alamat relatif ke awal bagian 0x00000000, yaitu di bagian paling awal, seperti yang kita lihat di daftar dibongkar, ukuran fungsi adalah 22 byte.

Objdump akan menunjukkan kepada kita informasi yang sama tentang ini:

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

Ketika kernel memuat modul, ia menemukan semua karakter yang tidak terdefinisi dan mengisi bidang st_value dan st_size dengan nilai yang valid. Ini dilakukan dalam fungsi simplify_symbols, file 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)
{
...

Dalam parameter fungsi, struktur load_info dari formulir berikut dilewatkan

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

Bidang-bidang berikut ini menarik bagi kami:
- hdr - Header file ELF
- sechdrs - pointer ke tabel header bagian
- strtab - tabel nama simbol - satu set string dipisahkan oleh nol
- index.sym - indeks header bagian yang berisi tabel simbol

Pertama-tama, fungsi akan mendapatkan akses ke bagian dengan tabel simbol. Tabel simbol adalah array elemen bertipe Elf64_Sym:

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

Selanjutnya, dalam loop, kita melihat semua karakter dalam tabel, menentukan masing-masing namanya:
for (i = 1; i < symsec->sh_size / sizeof(Elf_Sym); i++) {
	const char *name = info->strtab + sym[i].st_name;

Bidang st_shndx berisi indeks tajuk bagian di mana karakter ini didefinisikan. Jika ada nilai nol (case kami), maka simbol ini tidak ada di dalam modul, Anda perlu mencarinya di kernel:

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

Kemudian muncul antrian relokasi dalam fungsi 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++) {
	.....

Dalam loop, kami mencari bagian relokasi dan memproses catatan setiap bagian yang ditemukan di fungsi apply_relocate_add:

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

Pointer ke tabel header bagian, pointer ke tabel nama simbol, indeks header bagian dengan tabel simbol dan indeks header bagian relokasi diteruskan ke apply_relocate_add:

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

Pertama kami membahas bagian relokasi:

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

Kemudian, dalam satu lingkaran, kami memilah-milah array entri:

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

Kami menemukan bagian untuk relokasi dan posisi di dalamnya, yaitu di mana kita perlu melakukan perubahan. Bidang sh_info dari header bagian relokasi adalah indeks header bagian untuk relokasi, bidang r_offset dari catatan relokasi adalah offset ke posisi di dalam bagian untuk relokasi:

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

Alamat karakter yang akan diganti pada posisi ini, dengan mempertimbangkan addendum. Bidang r_info dari entri relokasi berisi indeks simbol ini dalam tabel simbol:

	/* 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;

Jenis relokasi menentukan hasil akhir dari perhitungan, dalam contoh kami adalah 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;
	.....

Sekarang kita dapat menghitung sendiri nilai akhir, mengetahui bahwa sym-> st_value adalah alamat fungsi printk 0xffffffff810b3df9, r_addend adalah (-4), offset ke posisi relokasi adalah 0xd dari awal bagian teks modul, atau dari awal fungsi foo, yaitu dari foo. akan menjadi ffffffffc000000d. Ganti semua nilai ini dan dapatkan:

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

Mari kita lihat dump fungsi foo, yang kita dapatkan di awal:

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

Pada offset 0xD, nilai 0xc10b3de8 ditemukan, yang identik dengan yang kami hitung.

Ini adalah bagaimana kernel memproses relokasi dan mendapatkan offset yang diperlukan untuk perintah close call.

Dalam mempersiapkan artikel, kernel Linux versi 5.4.27 digunakan.

All Articles