为了深入学习二进制安全,而进一步的对linux系统方面的学习
在本文中,将了解什么是系统调用,通过查看特定的内核函数 copy_from_user 来理解用户模式和系统模式的含义
什么是系统调用
我们先看看存在哪些类型的系统调用,在linux手册中可以阅读有关系统调用的信息
man syscalls
系统调用是应用程序和linux内核之间的基本接口
系统调用通常不直接调用,而是通过glibc中的包装函数调用
通常gilbc包装函数非常小,除了在调用系统调用之前将参数复制到正确的寄存器之外几乎无法做任何工作,再往下阅读,可以发现大量可用的系统调用
libc函数printf也只是系统调用写入的一个包装器,创建一个文件来实际操作展示
gcc编译文件
gcc license_1.c -o license_1 #-o,输出文件名称
strace ./license_1
可以看到,当我们使用程序strace来跟踪所有的系统调用时,它没有显示printf,而是显示了write
现在阅读write的手册
man 2 write
这里说write函数需要三个参数
fd:文件描述符
buf:缓冲区地址
count:计数
然后创建一个简单的c程序来调用这个函数
#include <unistd.h>
void main(){
write(1,"baimao\n",10);
}
第一个参数是文件描述符,把他设置为1,意思是标准的文件描述符,对于第二个函数,是要指向字符串的内存地址,可以在这里随意写一个字符串,然后编译器会在内存中为它找到一个位置并将它的放在那,最后一个参数是字符串的长度,在这个例子中,我们给字符串10的长度,如果给的长度小于字符串的长度,那么字符串将无法显示完整
然后gcc编译c文件,但是要关闭一些防护,不然不方便后续调试
gcc -z execstack -fno-stack-protector -no-pie -z norelro baimao.c -o baimao
然后使用radare2来详细的看函数是怎么调用的
在调用write函数的位置下一个断点
db 0x0040113e
运行文件到断点处
dc
输入V!切换到可视化模式
按s可以一步一步详细的看程序的调用
这是链接表plt,程序会跳转到这里
这些都是libc库的汇编指令,继续往下
这里他将1移入eax寄存器,然后是执行syscall指令,英特尔汇编器官方解释是
这是对特权级别0,系统的快速调用,并且操作码为0F 05
并且是通过从IA32_LSTAR MSR加载rip来实现的,MSR是特定于模型的寄存器,因此将rip设置为另一个值,方便在其他地方继续执行跳转一样,它从模型特定寄存器(MSR)加载rip寄存器
在通过WRMSR指令引导系统期间的某个时间点,此地址就已经在特殊寄存器中配置,但是要使用此指令,必须处于特权级别0,因此不能从简单的c程序中设置它,因为处在用户模式,用户模式处在特权级别3
如果想知道如何从级别3进入级别0,那么答案是通过syscall之类的指令
当打开计算机时,cpu从级别0开始,内核可以通过WRMSR指令配置地址,列如上面提到的LA32——LSTAR MSR寄存器,然后将cpu的权限降为3级,这时,用户无法重新配置寄存器,也无法重新配置硬件,只能通过系统调用再次进入级别0,用户也无法控制执行的内容,因为该地址是固定的
回到syscall调用
这里在做的是从寄存器中加载一个数字,在本文的例子中是1,然后通过系统调用跳转到内核中固定的地址来进入特权级别0
这个网站解释了为什么write系统调用的编号为1
https://filippo.io/linux-syscall-table/
它是在read_write.c源代码中实现的,所以内核知道它需要执行什么
这是当在调用write系统调用时,内核执行的内容
这里推荐一本免费的电子书,它叫做linux设备驱动程序
https://lwn.net/Kernel/LDD3/
这本书详细的介绍了内核的工作原理,尤其是如何编写设备驱动程序和内核模块
在书里的第三章第7节中写道"scull中读写代码需要将整个数据段复制到用户地址空间或从用户地址空间复制"
"此功能由以下内核函数提供,它们复制任意字节数组并位于大多数读写实现的核心"
首先,用户地址空间是什么意思?当使用gdb调试某些程序时,为什么每个程序似乎都使用相同的地址?代码始终位于相同的地址,所有程序如何使用内存中相同的地址?它们不会相互覆盖吗?
这就是系统拥有MMU(内存管理单元)的原因
内核使用特殊的cpu指令和配置寄存器等设置MMU,这告诉了MMU如何在虚拟地址和物理地址之间进行转换
因此,当在c程序中使用指令
mov eax,[0x04000000]
MMU会知道如何将此地址转换为RAM中的实际物理地址
因此,当程序在系统调用进入内核后,希望从用户地址空间复制一些数据,列如将其写入其他地方,这时,可以使用copy_from_user函数
Linux Cross Reference分析内核函数源代码
https://elixir.bootlin.com/linux/latest/A/ident/_copy_from_user
该函数在from参数上调用access_ok,这是用户指定的地址,在本文案例中是程序想要写入的字符串的地址
这里会检查是否允许此进程从地址中读取信息,如果进程试图从另一个进程中读取一些信息,一切正常的话,它会调用copy_from_user,点击copy_from_user
https://elixir.bootlin.com/linux/v4.3/source/arch/x86/include/asm/uaccess_64.h
这里有一个switch-case语句,它检查了用户想要从用户空间读取的大小
假设程序只想从用户空间读取一个字节,这种情况下系统将返回这些
get_user_asm是一个预处理器宏,点击get_user_asm,进入新的查找页面,现在将要了解如何分阶段编译C文件
这里只是一个简单的复制和替换,定义的代码只是简单的复制到它之前使用的位置,然后编译器开始将它编译成机器代码,将这些代码复制到ubuntu上看
#define __get_user_asm(x, addr, err, itype, rtype, ltype, errret) \
asm volatile(ASM_STAC "\n" \
"1: mov"itype" %2,%"rtype"1\n" \
"2: " ASM_CLAC "\n" \
".section .fixup,\"ax\"\n" \
"3: mov %3,%0\n" \
" xor"itype" %"rtype"1,%"rtype"1\n" \
" jmp 2b\n" \
".previous\n" \
_ASM_EXTABLE(1b, 3b) \
: "=r" (err), ltype(x) \
: "m" (__m(addr)), "i" (errret), "0" (err))
输入
:%s/"itype"/b/g
:%s/"rtype"/b/g
:%s/\\//g
现在看起来就整洁了一点,get_user_asm定义了一些实际的cpu指令,而这里的这个移动就是将数据从用户空间移动到这个变量中的指令
并且将它们设置为单个字节的“b”,在这些预处理器语句的工作时,只需要将指令替换为b,所以实际指令看起来像
movb %2,%b1
这是at&t汇编语法,意思是它将%2中的任何内容移动到%b1中
这些是c内联的汇编语法,它指定了在此处定义的变量,上面的%2指的是这三个参数,这些是程序要从中移动的数据地址,把它移动到%1中,也就是x,这就是程序将其移动到的位置
移动由STAC和CLAC操作,这两个指令代表设置和清除寄存器,回到网站,点击ASM_STAC函数可以查看它的源代码,点击第一个
它与SMAP有关,这里还有来自该指令的原始操作码,回到刚刚的__get_user_asm源代码
在mov指令下方,这是部分修复和汇编异常表,这些涉及到内核如何处理硬件异常,这里有一个文档,可以知道到它在那里的确切作用
https://www.kernel.org/doc/Documentation/x86/exception-tables.txt
最后,没有代码可以以某种方式将用户提供的虚拟地址转换为真实的物理地址,它只是执行一个mov指令,因为这些操作发生在别处
当内核执行到这条指令时,会导致页缺失,因为它试图访问一个虚拟地址,会造成中断,cpu跳转到内核中另一个预定义的代码位置,目标位置会处理异常,这与系统调用指令让程序跳转到预定义地址的方式非常相似