CTF

「JarvisOJ」系统调用——int $0x80/syscall[回顾JOJ level4——无.so]

Posted by 许大仙 on August 16, 2018

宏观上说,int $0x80是intel汇编层面的系统调用,而syscall也是系统调用,只不过是linux系统中c语言环境下的系统调用实体。

只是层面不同,指的是一个东西。

Linux 系统调用(SCI,system call interface)的实现机制实际上是一个多路汇聚以及分解的过程,该汇聚点就是 0x80 中断这个入口点(X86 系统结构)。也就是说,所有系统调用都从用户空间中汇聚到 0x80 中断点,同时保存具体的系统调用号。当 0x80 中断处理程序运行时,将根据系统调用号对不同的系统调用分别处理(调用不同的内核函数处理)。

引起系统调用的两种途径

  (1)int $0×80 , 老式linux内核版本中引起系统调用的唯一方式

  (2)sysenter汇编指令

一、系统调用

1、基础知识

用户态和内核态

一般现代CPU都有几种不同的指令执行级别,Linux总共划分为4个指令执行级别:内核运行在0级别上,1,2级别默认不运行,用户程序运行在3级别上

在内核指令执行级别上,代码可以执行特权指令,访问任意的物理地址。

在用户指令执行级别上,代码的掌控范围会受到限制。只能在对应级别允许的范围内活动。 cs寄存器的最低两位表明了当前代码的特权级: CPU每条指令的读取都是通过cs:eip这两个寄存器:其中cs是代码段选择寄存器,eip是偏移量寄存器。

上述判断由硬件完成。

系统调用过程

一般来说在Linux中,地址空间是一个显著的标志:0xc0000000以上的地址空间只能在内核态下访问,0x00000000-0xbfffffff的地址空间在两种状态下都可以访问

2、int $80汇编指令

I386CPU将用户空间程序限制在0-3G空间,将内核程序限制在3G~4G空间,这样就实现了用户空间和内核空间的隔离; 程序的执行过程需要访问特定的系统程序以完成相应的功能。而用户程序运行在用户态,不能够访问特定状态下的系统程序。而用户程序又需要执行系统程序,这就促使int $80的产生: 应用程序通过int 0x80(软中断)指令实现用户空间与内核空间的交互。CPU会把软中断做为陷阱来处理,所以软中断也称为编程异常,其中int 0x80可以用于执行系统调用

3、int $80的执行功能

Int $80主要实现对寄存器的压栈

  • 保存:SS:esp,CS:EIP,eflag中的内容存到进程对应的内核堆栈;
  • 修改:SS:esp内容改为system_ call对应的的内核堆栈的基地址,CS:EIP:内容改为system_ call的第一条指令的地址,eflag内容修改为新的标志。

总而言之,int $0x80的就是在系统调用产生时,完成用户态到内核态的切换。由于用户进程在用户态执行和内核态需要对应不同的内核堆栈,所以在进行系统调用时,需要保存用户态最后的esp,eip等状态信息,以便于系统调用结束能够返回到发生系统调用的地址继续用户程序的执行,这就是int $0x80完成的对寄存器内容的保存的目的

执行系统调用的时候,先把系统调用号保存到EAX寄存器中,然后执行int $0x80,然后转入system_ call系统调用指令开始执行,system_ call根据保存到system_ call内核堆栈中的系统调用号的值,转为执行相应的系统调用处理函数。

Iret指令指int $0x80的逆过程,弹出int $0x80指令所产生的内容堆栈块的内容到对应的寄存器中,CPU把控制权有内核交给用户程序,执行状态回到用户态,用户程序继续执行。至此,系统调用,系统返回过程结束。

二、系统调用的参数传递规则

在汇编层面上,系统调用的参数传递规则:

A.当系统调用所需参数的个数不超过5个的时候,执行”int$0x80”指令时,需在eax中存放系统调用的功能号,传递给系统调用的参数则按照参数顺序依次存放到寄存器ebx,ecx,edx,esi,edi中[从左到右],当系统调用完成之后,返回值存放在eax中;

比如,经常用到的write函数的定义如下: ssize_ t write(int fd, const void* buf, size_ t count); 该函数的功能最终通过SYS_ write这一系统调用来实现的;根据上面的参数传递规则可知,参数fd存放在ebx中,参数buf存放在ecx中,参数count存放在edx中[write函数的参数依次放在ebx,ecx,edx中],而系统调用功能号SYS_ write则存放在寄存器eax中;系统调用执行完成之后,返回值可以从eax中得到;

B.当系统调用的参数超过5个的时候,执行”int$0x80”指令,需在eax中存放系统调用的功能号,所不同的只是全部的参数应该依次存放在一块连续的内存区域里,同时在寄存器ebx中保存指向该内存区域的指针(即:该连续内存块的首地址),返回值仍然保存在寄存器eax中;由于只是需要一块连续的内存区域来保存系统调用所需要的参数,因此,完全可以像普通的函数调用一样使用栈来传递系统调用所需要的参数;但是要注意一点:Linux采用的是C语言的调用模式,这就意味着所有参数必须以相反的顺序进栈,即:最后一个参数最先进栈,而第一个参数最后进栈;如果采用栈来传递系统调用所需要的参数,在执行”int$0x80”指令时,还应将栈指针的当前值(栈顶地址)复制到寄存器ebx中;

例如,系统调用mmap()的参数个数就超过5个:

void * mmap(void* start, size_ t length, int prot, int flags, int fd, off_ t offset);

使用这个系统调用时,系统调用功能号保存到eax中,mmap()所需要的所有参数存放到一块连续的内存区域中,这块连续内存区域的首地址存放到ebx中,即可。

linux系统调用 :

  1. 放系统调用号到eax中
  2. 设置系统调用参数到ebx,ecx等
  3. 调用相关中断(DOS:21h; linux:80h)
  4. 返回结果通常保存在eax中

例子:

move ax,1   ;sys_exit系统调用号为0
mov ebx,0   ;exit参数0,相当于exit(0) 
int 80h;80中断,通常中软中断,调用它意思就是告诉内核,你处理它  

回顾level4

level4那题,是没有libc.so文件的。由于plt/got表中存在libc.so库中的read,write函数,因此有到达libc的通路,可以通过write函数泄露内存地址,加上DynELF模块遍历GOT表,猜解libc版本/system函数/“/bin/sh”字符串位置,从而获得shell

现在提供另外一种方法,来自于Wz’Blog

read函数写入指定可写地址原理也不提了,read中的syscall是重点!!!

在libc的read函数中,一般会在<read+14>的位置存在一个syscall调用[一定是偏移14字节的位置]。猜测可能是大部分或者部分libc的通用代码,所以才造就了这个固定的位置存在syscall调用。

所以我们只要泄露了read函数的真实内存地址,加上固定偏移0xe就能得到syscall的真实内存地址,即使开了ASLR,也不会改变低位的地址,所以这个方法也同样适用。然后就是正常的ROP,用gadget控制传入参数,然后跳到syscall地址就好了

  • 32位程序需要将eax=0xb[对应系统调用为execve],ebx=”/bin/sh”字符串地址,ecx=0,edx=0,然后调用syscall[由于是系统调用,所以传参不是普通函数的栈帧传参,而是进入内核态,5个参数及以内寄存器传参]

  • 64位则需要将rdi=”/bin/sh”字符串地址,rsi=0,rdx=0,,然后同样调用syscall

execve(“/bin/sh”,0,0)

执行”/bin/sh”路径下的文件,传参为空[NULL空指针],环境变量不变[新环境变量数组为空]

补充

1.linux系统调用号

linux0.11 中,include/unistd.h 中定义了72个系统调用号

#ifdef __LIBRARY__

#define __NR_setup	0	/* used only by init, to get system going */
#define __NR_exit	1
#define __NR_fork	2
#define __NR_read	3
#define __NR_write	4
#define __NR_open	5
#define __NR_close	6
#define __NR_waitpid 7
#define __NR_creat	8
#define __NR_link	9
#define __NR_unlink	10  #0xA
#define __NR_execve	11  #0xB
#define __NR_chdir	12
#define __NR_time	13
#define __NR_mknod	14
#define __NR_chmod	15
#define __NR_chown	16
#define __NR_break	17
#define __NR_stat	18
#define __NR_lseek	19
#define __NR_getpid	20
#define __NR_mount	21
#define __NR_umount	22
#define __NR_setuid	23
#define __NR_getuid	24
#define __NR_stime	25
#define __NR_ptrace	26
#define __NR_alarm	27
#define __NR_fstat	28
#define __NR_pause	29
#define __NR_utime	30
#define __NR_stty	31
#define __NR_gtty	32
#define __NR_access	33
#define __NR_nice	34
#define __NR_ftime	35
#define __NR_sync	36
#define __NR_kill	37
#define __NR_rename	38
#define __NR_mkdir	39
#define __NR_rmdir	40
#define __NR_dup	41
#define __NR_pipe	42
#define __NR_times	43
#define __NR_prof	44
#define __NR_brk	45
#define __NR_setgid	46
#define __NR_getgid	47
#define __NR_signal	48
#define __NR_geteuid	49
#define __NR_getegid	50
#define __NR_acct	51
#define __NR_phys	52
#define __NR_lock	53
#define __NR_ioctl	54
#define __NR_fcntl	55
#define __NR_mpx	56
#define __NR_setpgid	57
#define __NR_ulimit	58
#define __NR_uname	59
#define __NR_umask	60
#define __NR_chroot	61
#define __NR_ustat	62
#define __NR_dup2	63
#define __NR_getppid	64
#define __NR_getpgrp	65
#define __NR_setsid	66
#define __NR_sigaction	67
#define __NR_sgetmask	68
#define __NR_ssetmask	69
#define __NR_setreuid	70
#define __NR_setregid	71

2.pwntools中的DynELF函数的原理

pwntools中的DynELF是利用可循环利用的泄露内存函数去遍历获取程序got表中的所有函数的真实内存地址,然后通过地址低12位猜测是哪个版本的libc[从而得到该版本libc库下所需函数的地址],如果找得到对应版本就用内存地址减去对应函数偏移得到libc加载的基地址,匹配不到就GG。

3.execve函数

int execve(const char * filename,char * const argv[ ],char * const envp[ ]);

execve()用来执行参数filename字符串所代表的文件路径,第二个参数是利用指针数组来传递给执行文件,并且需要以空指针(NULL)结束,最后一个参数则为传递给执行文件的新环境变量数组。