glibc 中偏门利用技巧

0xRGz 2022-02-10 10:25:00
CTF

0x00 泄露程序基址

通过_dl_rtld_libname

_dl_rtld_libname 是一个存在ld.so 里的结构体

glibc2.27:

image-20220119180738846

glibc2.30

程序基址与_dl_rtld_libname 指针的偏移量和2.27不同为 offset=270

image-20220119180147972

读取该地址上面的内容可以泄露出程序基地址,该地址为主程序加载 ld 文件时存储 ld 文件名的地址

_dl_rtld_libname和 libc 基地址的偏移是不固定的,不同的 ld 文件有不同的偏移,需要手测或者爆破

程序基地址为0x55e4fceaf238 - 0x238 == 0x55e4fceaf000

这个地址的偏移一般离程序基地址来说是 0x238,但是不一定我patch了不同版本的glibc 2.23 这个偏移都不一样

patch了2.30的glibc 这个偏移也不一样

About | stdin && stdout && stderr

bss 段上的stdin,stdout,stderr

有些时候stdin,stdout,stderr 指针是存在于bss 段上的如下

因为设置了setvbuf 的原因(PIE 关闭 PIE 打开的时候情况是一样的)

image-20220119183030225

若没有设置setvbuf,就不会在bss段上,但是不设置setvbuf远程打的就没回显;

有些时候stdin,stdout,stderr 又不再bss 段上,并不是很清楚原因

关于close(1) 即关闭stdout

有些时候出题会关闭标准输出,当你拿到shell 也没办法打印flag,或者只有orw,且close(1);

对于可以拿到shell的情况:

使用exec命令 exec 1>&0

对于orw情况:

orw 如西湖论剑noleakfmt bss 段无stdout,stdin 指针,但是栈上存的有IO_FILE 结构体指针

我们想办法抬栈修改,IO_FILE 结构体

(抬栈可以调用start 函数中的libc_start_main,在libc_start_main 中有 rsp=rsp-0x90 的操作)

image-20220119203012849

成功打印条件是输出管道能用有两种方式

1.把stdout的IO_FILE 指针,指向stderr结构体,即用错误输出的输出管道

即原本 stdout_pointer == stdout_addr 修改为stdout_pointer === stderr_addr

此时我们用stdout 就是用的 stderr 的IO_FILE ,通过标准错误管道输出的。

2.修改stdout 结构体的参数fileno=2(原本等于1),使得输出当作标准错误输出(stderr)来输出

修改了fileno参数后,打印是会通过stderr的管道输出。

通过修改IO结构体泄露地址

如果存在任意地址写 或者 可以劫持stdout 就可以实现修改stdout 结构体实现libc 泄露

  • 通过修改 puts函数工作过程中stdout 结构体中的 _IO_write_base ,来达到泄露libc地址信息的目的。

puts函数在源码中是由 _IO_puts实现的,而 _IO_puts 函数内部会调用 _IO_sputn,结果会执行 _IO_new_file_xsputn,最终会执行 _IO_overflow

_IO_puts - > _IO_sputn -> _IO_new_file_xsputn

_flag的构造满足的条件:

_flags = 0xfbad0000  //初始magic value
_flags & = ~_IO_NO_WRITES // _flags = 0xfbad0000
_flags | = _IO_CURRENTLY_PUTTING // _flags = 0xfbad0800
_flags | = _IO_IS_APPENDING // _flags = 0xfbad1800

一般 方法

覆盖前:

https://rgz-data1.oss-cn-chengdu.aliyuncs.com/img/image-20210827180329553.png

覆盖后

把_IO_read_ptr 、end、base 都覆盖为00 然后覆盖 _IO_write_base 最低位覆盖位\x00

https://rgz-data1.oss-cn-chengdu.aliyuncs.com/img/image-20210827180349808.png

若不能劫持stdout或不能直接修改stdout 结构体

只有通过爆破libc,可以通过main_arena 或其他临近IO_FILE 结构体的libc,使用部分覆盖低位的方式爆破

stdout结构体地址,然后和上面一样修改flag等标志,泄露出libc 地址

下面是例子 可以看见main_arena 地址只有后5位与,libc_stdout不同

而1000是一个分页 ,所以stdout 最、低3位 760是写死了,不管libc_base 怎么变化,stdout libc低三位一定是

760,若所以与main_arena 地址只相差 ce 两位不同(最多相差3位)

爆破成功概率为:1/16*16 或 1/16 * 16 * 16 , 成功概率还行

image-20220119213305769


0x01 任意地址执行:

修改 .fini_array

若有两次任意地址写(对写的大小字节有一定要求)

可以修改.fini_array的值为任意地址,不过.fini_array只能用一次

具体例子:pwnable 3x17 由于wp很多,题目不详细说明

exit类

__libc_atexit

调用方法:

elf.sym['__elf_set___libc_atexit_element__IO_cleanup__']

程序是静态编译的时候,可以修改__libc_atexit来实现任意地址跳转

这个可以无限次使用,在 ida 里也能找到它

利用 _dl_fini 执行函数

几个地方都可以利用

函数执行顺序如下: exit_run_exit_handlers__call_tls_dtorscall _dl_fini
以下是也许可以利用的点:

0x7f236a931b3e <_dl_fini+126>    call   qword ptr [rip + 0x216404] <0x7f236a921c90>
0x7f236a931d6e <_dl_fini+686>    call   qword ptr [rip + 0x2161dc] <0x7f236a921ca0>
0x7f236a931df3 <_dl_fini+819>    call   qword ptr [r12 + rdx*8] <0x400925>
这里记得曾经有一道题r12是可控的而rdx又刚好为 0 就可以造成跳转

0x7f6c23257e13 <_dl_fini+851>    call   rax <0x4009d4>` rax 指向 fini 会调用其数组内的函数

例子
具体的利用在 exit 函数的_dl_fini函数里:

0x7f22d9fdca02 <_dl_fini+98>     lea    rdi, [rip + 0x217f5f] <0x7f22da1f4968>
0x7f22d9fdca09 <_dl_fini+105>    call   qword ptr [rip + 0x218551] <0x7f22d9c2a440>
     rdi: 0x7f22da1f4968 (_rtld_global+2312)  0x68732f6e69622f /* '/bin/sh' */
     rsi: 0x0
     rdx: 0x7f22d9fdc9a0 (_dl_fini)  push   rbp
     rcx: 0x1

call qword ptr [rip + 0x218551]_rtld_global上面的地址,rdi 也是用的_rtld_global上面的地址

不同的 ld 对应的_rtld_global不一样,需要手调爆破

例题: De1ctf 2019 pwn unprintable

abort

_IO_flush_all_lockp 就是修改vtable 一种打法,利用的是 _IO_OVERFLOW

image-20220120040828150

高版本IO中exit利用

exit 中程序会结束,会调用很多IO中的库函数来完成程序结束的过程

因为高版本glibc IO FILE 的库函数有改动,所以exit的利用方式不一样

在2.23及以下

一般攻击FILE结构体就是劫持IO函数的_chain字段为我们伪造的IO_FILE_plus,然后修改vtable表中的io_str_overflow为system

在高版本libc下(要配合heap是使用不是单纯的任意地址执行)

如libc2.31下也依然是利用io_str_overflow这个函数,但io_str_overflow函数的实现发生了变化,在_IO_str_overflow中有malloc 和 free 还有memcpy 我只需要伪造IO_FILE结构体,执行正确的程序流就行

IO_str_overflow利用:

int _IO_str_overflow (FILE *fp, int c)
{
  int flush_only = c == EOF;
  size_t pos;
  if (fp->_flags & _IO_NO_WRITES)
      return flush_only ? 0 : EOF;
  if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
    {
      fp->_flags |= _IO_CURRENTLY_PUTTING;
      fp->_IO_write_ptr = fp->_IO_read_ptr;
      fp->_IO_read_ptr = fp->_IO_read_end;
    }
  pos = fp->_IO_write_ptr - fp->_IO_write_base;
  if (pos >= (size_t) (_IO_blen (fp) + flush_only))
    {
      if (fp->_flags & _IO_USER_BUF)//fp->flags==0 不满足条件 进入else
    return EOF;
      else
    {
      char *new_buf;
      char *old_buf = fp->_IO_buf_base;
      size_t old_blen = _IO_blen (fp);
      size_t new_size = 2 * old_blen + 100;
      if (new_size < old_blen)//注意这里new_size 是经过计算才能申请到tcache对应size 的chunk
        return EOF;
      new_buf = malloc (new_size);//这里会新申请一个chunk 可以把tcache 中free_hook申请到
      if (new_buf == NULL)
        {
          /*      __ferror(fp) = 1; */
          return EOF;
        }
      if (old_buf)
        {
          memcpy (new_buf, old_buf, old_blen);//把之前的buffer上的内容复制
          free (old_buf);//这里有free 可以触发free hook
          /* Make sure _IO_setb won't try to delete _IO_buf_base. */
          fp->_IO_buf_base = NULL;
        }
      memset (new_buf + old_blen, '\0', new_size - old_blen);

      _IO_setb (fp, new_buf, new_buf + new_size, 1);
      fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
      fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
      fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
      fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);

      fp->_IO_write_base = new_buf;
      fp->_IO_write_end = fp->_IO_buf_end;
    }
    }

  if (!flush_only)
    *fp->_IO_write_ptr++ = (unsigned char) c;
  if (fp->_IO_write_ptr > fp->_IO_read_end)
    fp->_IO_read_end = fp->_IO_write_ptr;
  return c;
}

有两种利用方式

  • 1.old_buf 上布置system, malloc 申请到free_hook,memcpy把布置system地址复制到free hook

image-20211123015816010

然后old_buf 起始可以布置“/bin/sh”,就变成的最经典的free_hook=system + free(binsh_addr)

  • 2.也可以利用_IO_str_overflow实现srop

这里看_IO_str_overflow的汇编在+52 有个把rdi+0x28 赋给rdx操作的

而自从glibc2.29开始,setcontext中的gadget索引由rdi变为了rdx,需要先控制rdx的值才能够进行后续的srop

2fBjyD.png

_IO_str_overflow中的这条汇编正好可以将rdx进行赋值,且来源还是rdi,rdi正好是FILE结构体的首地址

只需要将fp+0x28设置为我们可以控制的地址就可以进行srop,且这条语句是在malloc之前执行的

所以利用方法就是:首先将malloc_hook设置为setcontext+61,然后触发_IO_str_overflow

事先在我们伪造的FILE结构体中设置好相应的数据,从而将rdx赋值为我们可以控制的地址

接着_IO_str_overflow调用malloc触发setcontext,进行srop。

触发malloc条件如下

fp->_flags=0;//if (fp->_flags & _IO_USER_BUF) return EOF;/* not allowed to enlarge */
fp->_IO_write_ptr=srop_addr
/*
0x0   _flags
0x8   _IO_read_ptr
0x10  _IO_read_end
0x18  _IO_read_base
0x20  _IO_write_base
0x28  _IO_write_ptr

0x00007ffff7e2f0dd <+61>:    mov    rsp,QWORD PTR [rdx+0xa0]
*/
new_buf = malloc (2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100);
memcpy (new_buf, fp->_IO_buf_base, (fp)->_IO_buf_end - (fp)->_IO_buf_base);
free (fp->_IO_buf_base);

_IO_file_overflow

通过任意地址写修改vtable上_IO_file_overflow指针,需要两次任意地址改,利用比较方便,只需要如通过printf 等函数,

触发刷新IO,即可触发_IO_file_overflow函数

我们只需要把 _IO_file_jumps中 _IO_file_overflow的地址改为system,把stdout等IO_FILE 结构体的flags为改为

“/bin/sh”

image-20220123030653812

当call _IO_file_overflow 就<=> call sysem("/bin/sh")

总结:

1.修改_IO_file_jumps  _IO_file_overflow指针为system
2.修改stdout的flags=/bin/sh\x00
想办法刷新IO流触发,call _IO_file_overflow即可

修改 _stack_chk_fail 内部函数的 .got.plt 表

该函数内部函数按调用顺序排列如下:

strchrnul 函数
mempcpy 函数
strlen_ifunc 函数

got 表内有可改函数就可以改为 main 函数的地址来进行无限循环

0x02 打印flag

flag文件已经读入

__stack_chk_fail

glibc2.26 以下

如果在程序中flag文件已经被读入,如果只是打印flag 可以利用stack smash 直接不用管canary

因为栈检查失败,程序就会执行 __stack_chk_fail 函数,报错信息打印 argv[0] 指针,argv[0]

指向栈上存储的文件名信息,我们直接把argv[0] 指向flag地址,即可借助报错信息打印flag

malloc_printerr

若 __stack_chk_fail 使用不了,我们还可以利用malloc_printter 触发

在 glibc_malloc 时检测到错误的时候,会调用 malloc_printer函数

static void malloc_printerr(const char *str) {
  __libc_message(do_abort, "%s\n", str);
  __builtin_unreachable();
}

会调用 __libc_message来执行 abort 函数,如下:

malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
{
  /* Avoid using this arena in future.  We do not attempt to synchronize this
     with anything else because we minimally want to ensure that __libc_message
     gets its resources safely without stumbling on the current corruption.  */
  if (ar_ptr)
    set_arena_corrupt (ar_ptr);

  if ((action & 5) == 5)
    __libc_message ((action & 2) ? (do_abort | do_backtrace) : do_message,
            "%s\n", str);
  else if (action & 1)
    {
      char buf[2 * sizeof (uintptr_t) + 1];

      buf[sizeof (buf) - 1] = '\0';
      char *cp = _itoa_word ((uintptr_t) ptr, &buf[sizeof (buf) - 1], 16, 0);
      while (cp > buf)
        *--cp = '0';

      __libc_message ((action & 2) ? (do_abort | do_backtrace) : do_message,
              "*** Error in `%s': %s: 0x%s ***\n",
                      __libc_argv[0] ? : "<unknown>", str, cp);//这里一样可以利用来打印flag
    }
  else if (action & 2)
    abort ();
}

flag文件未读入

未读入的情况一般有几种方法得到flag

知道flag文件名:

  • orw ,open read write

  • ora,open read alarm ,这里没有给打印函数,可以通过alarm 对读入flag ascii 码值进行计时,得到flag

  • or + asm cmp ,这里open + read 读入flag,然后利用程序自带汇编cmp gadget ,或写一段汇编代码注入,与输入 的flag进行比较类似测信道攻击

  • sendfile + open

sendfile(1,open("/flag",NULL),0,1000)

image-20220131150759745

不知道flag文件名:

  • getdents

利用getdents 系统调用,getdents可以读取目标目录下的所有文件名。

先open("dir_path",0x10000) 打开目标目录,

然后getdent(fd,buffer,count),读取打开目录中的文件名到buffer上,即可得到flag的文件名。

评论

0xRGz

菜鸡pwn手

twitter weibo github wechat

随机分类

区块链 文章:2 篇
事件分析 文章:223 篇
Android 文章:89 篇
密码学 文章:13 篇
memcache安全 文章:1 篇

扫码关注公众号

WeChat Offical Account QRCode

最新评论

Yukong

🐮皮

H

HHHeey

好的,谢谢师傅的解答

Article_kelp

a类中的变量secret_class_var = "secret"是在merge

H

HHHeey

secret_var = 1 def test(): pass

H

hgsmonkey

tql!!!

目录