Generic_kernel_shellcode

在一些特定的情况下,譬如ko模块的注入、USMA或者ebpf提权时,我们需要编写一段类似用户态shellcode的代码来提供给内核执行,以达成如函数hook,提权等目的。但是由于内核版本以及安全机制的不同,在不同的设备上往往需要编写不同的shellcode,并且受限很大,有时无法成功调用到目的函数。在本文中,将介绍通过页表查找以及EXPORT_SYMBOL()宏机制,来实现在不同版本x64 Linux kernel中编写通用的shellcode。

2. 虚拟地址获取

既然需要保证shellcode的通用性,那么就需要默认在shellcode执行开始时,所有的常规可变寄存器中的值都是无法确定的,我们需要找到一种确定的方式来获得一些必要的地址内存信息。即使是RSP或者RIP这种功能几乎固定的寄存器,也很难保证一定能够偏移到我们想要的地方。

2.1 直接映射区

在x64内核/Documentation/arch/x86/x86_64/mm.rst中有这样一段描述

ffff880000000000 | -120    TB | ffff887fffffffff |  0.5 TB | LDT remap for PTI
ffff888000000000 | -119.5 TB | ffffc87fffffffff |   64 TB | direct mapping of all physical memory (page_offset_base)
ffffc88000000000 | -55.5 TB | ffffc8ffffffffff | 0.5 TB | ... unused hole

从0xffff888000000000开始,存在一个64TB大小的直接映射区,这段虚拟内存直接顺序映射了全部的物理内存。也就是说,在未开启KASLR时,page_offset_base+offset地址直接对应物理内存的offset地址。而在开启了KASLR时,KASLR会为物理内存实际映射地址前添加一个偏移,在这时物理地址offset对应的直接映射区虚拟地址变成了page_offset_base+kaslr_offset+offset,未被映射部分则为空洞,无实际意义。

在多处理器系统中,为了减少各个cpu同时访问导致的竞争问题,Linux采用了Per-CPU机制来缓解。内核会为每个CPU分配一块物理内存,其中包含了CPU缓存以及一些非共享的变量,如order0大小page的缓存池等。而为了方便每个CPU迅速访问到自己的Per-CPU缓存区,Linux选择将地址保存在gs_base寄存器中,CPU通过gs:[]的方式来访问自己的缓存。

如上图所示,当前CPU的gs_base为0xffff95c103400000,处于直接映射区,通过下述代码即可获取到直接映射区的起始地址,不受KASLR机制影响。

asm volatile (
".intel_syntax noprefix;"
"mov ecx, 0xc0000101;"
"rdmsr;"
"shl rdx, 32;"
"mov %0 ,rdx;"
".att_syntax;"
: "=r" (page_offset_base)
:
: "rax"
);

2.2 kernel .text段

我们现在已经获得了任意物理内存读写的能力,那么下一步自然是想办法获得kernel text段的地址。和用户态相似的,text段上保存了所有的内核可执行的代码部分,其中包含了我们希望调用的关键函数。此外,内核全局变量所在的bss段也同样是以text段地址为偏移访问的。因此我们迫切的需要通过某种方法获得.text段的地址。

和直接映射区一样,在未开启KASLR时,内核.text段起始地址为固定值

ffffffff00000000 |   -4    GB | ffffffff7fffffff |    2 GB | ... unused hole
ffffffff80000000 |   -2   GB | ffffffff9fffffff | 512 MB | kernel text mapping, mapped to physical address 0
ffffffff80000000 |-2048   MB |                 |         |

正如前文所提到的,Linux内核将物理内存直接映射在了直接映射区,而text段中的所有可执行代码部分,都是vmlinux内核文件的映射,换言之,text段上的虚拟内存中的内容,一定都是从某片物理内存区域映射出来的,而一份虚拟内存的映射,又一定在页表中存在对应的项。综上所述,通过对页表的查找,可以实现text段地址的获取。

但是页表中包含的虚拟地址数量非常的多,我们该如何最快的查找的我们想要的text段地址呢?

如上图所示,text段地址的KASLR并不是简单的随机偏移添加,由于虚拟地址受四级页表管理的特性,其12-47位中,每9位受一级页表管理,简单的偏移加减法会导致页表项紊乱。因此,Linux内核采用了随机化PMD(page middle directory)的方式,也就是随机化地址的21-29位,来实现对于.text段的KASLR随机化。也就是说,我们只需要查找PMD中对应PTE的位置,即可计算出KASLR随机化后的实际.text段地址。

特别要注意,cr3中存储的PGD地址以及后续页表中的地址全都为物理地址,实际访问需要加上在2.1中获取到的page_offset_base

3. 函数地址获取

3.1 EXPORT_SYMBOL

在Linux内核中,由于内核模块功能的加入,内核需要将一部分函数和符号提供给外部调用,因此就有了EXPORT_SYMBOL宏。只有被EXPORT_SYMBOL包含了的内核函数或者符号才可以被外部模块调用,否则在编译内核模块时会提示调用undefined。而所有的导出符号和函数,都会产生一个__ksymtab_为开始的ksymtab结构体。

struct kernel_symbol {
int value_offset;
int name_offset;
int namespace_offset;
}
  1. 第一个int为ksymtab.value_offset到对应符号实际地址的偏移
  2. 第二个int为ksymtab.name_offset到对应符号的符号名字符串的偏移
  3. 第三个namespace与我们的流程无关

在目前的情况下,我们能够直接接触到的只有目标符号的名称,那么符号真实地址的查找也必然要从名称对应的地址开始入手

  1. 直接从text段起始开始查找目标符号字符串的地址(kstrtab = 0xffffffffba8e7b71)
  2. 再次从text段起始开始,每4字节扫描,判断当前 addr + *addr == kstrtab,满足条件则意味着找到了ksymtab结构体,ksymtab = addr – 4
  3. 最终计算出函数地址为 func_addr = ksymtab + *ksymtab

4. shellcode编译

4.1 trick

经过上述流程,我们现在已经知道了如何从无到有获得内核符号的真实地址,但是问题在于,上述的流程需要被编写成一段shellcode,并且这个流程十分冗长,如果使用传统的手工写asm的方式,有些过于复杂了,并且在大多数情况下,我们对于这段shellcode的长度要求并没有那么严格,并不需要极致的缩短shellcode长度。那么,使用C来编写一个binary并提取的方式,可以极大的减少编写的难度。

gcc -Os -S -o shell.s shell.c -nostdlib -fno-stack-protector -fno-asynchronous-unwind-tables -fPIC -nostartfiles -march=native -fno-strict-aliasing -fno-unwind-tables -fno-exceptions -T ./linker.ld -e_start -fcf-protection=none 
as --64 shell.s -o shell.o
ld -o shell --oformat binary shell.o --gc-sections

除此之外,如果我们希望通过提取binary的方式来生成shellcode的话,需要对shellcode中的函数布局存在一定的要求。我们需要保证shellcode的开始函数,在binary中为_start函数,必须要在.text段的开始,这样才能保证提取后的shellcode入口点正确,而非进入其他函数导致流程错误。

gcc的-T选项刚好可以满足我们的需求,只需要提供一个linker.ld文件,就可以控制.text段函数的布局。

SECTIONS {
. = 0;
.text (RWX) : {
*(.text)
*(.text.function)
}
}

void *physic_base_to_text_base(void *kernel_physic_base) __attribute__((section(".text.function")));

5. 小结

本文主要介绍了利用页表查询以及EXPORT_SYMBOL宏的方式,来尝试构建一个通用性更好的Linux内核态的shellcode,该方法对不同内核版本以及内核安全选项的兼容性要明显好于传统方法,可以极大提高编写的shellcode的通用性。尽管如此,由于内核版本的迅速迭代以及新的安全机制的引入,该方法仍然有其局限性,比如新的CONFIG_HAVE_ARCH_PREL32_RELOCATIONS编译选项会影响ksymstab的布局等问题。

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇