Setup

  • 下载题目文件到Linux系统环境中

  • 文件名有点长,做下重命名

    • mv babyfirst-heap_33ecf0ad56efc1b322088f95dd98827c heap
  • 赋予运行权限

    • chmod +x heap
  • 运行

    • ./heap
  • 不能运行的话,可能是因为使用的是64位系统

    • 在x86_64环境中运行不能直接运行x86的二进制程序
  • Ubuntu 14.04以后版本

    • dpkg --add-architecture i386
      apt-get update
      apt-get install libc6:i386 libncurses5:i386 libstdc++6:i386 $ apt-get install gcc-multilib g++-multilib
      
      1
      2
      3
      4
      5
      6

      ## 初步调查

      - 每次都需要做的初步检查

      -
      $ file heap heap: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-, for GNU/Linux 2.6.24, BuildID[sha1]=1b4e88004c13ca18ef78ac90b298c1e247c1d4e5, with debug_info, not stripped $ ldd heap linux-gate.so.1 (0xf7ee0000) libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7ce8000) /lib/ld-linux.so.2 (0xf7ee2000) $ checksec --file heap #http://www.trapkit.de/tools/checksec.html RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE Partial RELRO No canary found NX enabled No PIE No RPATH No RUNPATH 113 Symbols No 0 8 heap
      1
      2
      3
      4

      - 使用的库函数列表

      -
      Relocation section '.rel.plt' at offset 0x558 contains 14 entries: Offset Info Type Sym.Value Sym.Name 0804c000 00000107 R_386_JUMP_SLOT 00000000 mprotect@GLIBC_2.0 //更改内存属性 0804c004 00000207 R_386_JUMP_SLOT 00000000 printf@GLIBC_2.0 //显示文字 0804c008 00000307 R_386_JUMP_SLOT 00000000 memcpy@GLIBC_2.0 //拷贝内存 0804c00c 00000407 R_386_JUMP_SLOT 00000000 signal@GLIBC_2.0 //定义收到信号时的操作 0804c010 00000507 R_386_JUMP_SLOT 00000000 alarm@GLIBC_2.0 //在一段时间后提出异常 0804c014 00000607 R_386_JUMP_SLOT 00000000 _IO_getc@GLIBC_2.0 //读取一个字符 0804c018 00000707 R_386_JUMP_SLOT 00000000 puts@GLIBC_2.0 //显示一行 0804c01c 00000807 R_386_JUMP_SLOT 00000000 __gmon_start__ //使用gprof等收集统计信息时使用的函数。由于通常不使用它,因此可以忽略它 0804c020 00000907 R_386_JUMP_SLOT 00000000 exit@GLIBC_2.0 //退出函数 0804c024 00000a07 R_386_JUMP_SLOT 00000000 __libc_start_main@GLIBC_2.0 //调用main的包装函数 0804c028 00000b07 R_386_JUMP_SLOT 00000000 fprintf@GLIBC_2.0 //向文件指针输出内容 0804c02c 00000c07 R_386_JUMP_SLOT 00000000 setvbuf@GLIBC_2.0 //更改缓冲行为 0804c030 00000d07 R_386_JUMP_SLOT 00000000 memset@GLIBC_2.0 //设置内存值 0804c034 00000e07 R_386_JUMP_SLOT 00000000 sbrk@GLIBC_2.0 //扩张heap区域
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20

      上述函数没有bind, listen, accept等,因为没有实现NX处理

      ## fork-server型与xinetd型

      - CTF题目常见的有两种
      - fork-server型,xinetd型
      - fork-server型
      - 二进制程序中有bind->listen->accept的类型
      - ![](https://raw.githubusercontent.com/zjicmDarkWing/images/master/2019032501.jpg)
      - 这种情况下,需要去调试子进程
      - 如果未在gdb中设置set follow-fork-mode child,则仅调试父进程(并且父进程将继续存在,因此您需要每次都kill父进程)
      - xinetd型
      - 二进程程序中没有bind->listen->accept的类型(用xinetd代替)
      - ![](https://raw.githubusercontent.com/zjicmDarkWing/images/master/2019032502.jpg)
      - 在这种情况下,使用gdb等工具调试时,不需要考虑NW
      - 但是,在实际编写exploit进行测试时,可以用过socat来添加NW关系
      - 也就是说,对于xintd型,在调试时不需要考虑NW交换,但在编写exploit时需要考虑NW上的通信

      ## 简单运行了解下流程
      $ ./heap ... [ALLOC][loc=84354B0][size=823] Write to object [size=260]: AAAAA // 尝试输入简单的值 Copied 6 bytes. [FREE][address=8431008] ... [FREE][address=84354B0] Did you forget to read the flag with your shellcode? Exiting // 什么也没发生
      1
      2

      ## 由于之前有[size = 260]的描述,尝试发送260个字节
      $ python -c 'print "A"*260' | ./heap //使用python生成260个字符,发送给二进制程序 ... [ALLOC][loc=8E3E4B0][size=823] Write to object [size=260]: Copied 261 bytes. [FREE][address=8E3A008] ... [FREE][address=8E3C058] [1] 7910 done python -c 'print "A"*260' | 7911 segmentation fault (core dumped) ./heap //crash了,如果是259个字符,则刚好不会crash
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56

      # 静态分析

      - 下载安装IDA Free版本

      - https://www.hex-rays.com/products/ida/support/download.shtml

      - 启动后,加载要分析的二进制文件并等待一段时间以完成分析

      - Options→General更改一些设置

      ![](https://raw.githubusercontent.com/zjicmDarkWing/images/master/2019032503.jpg)

      - Graph View的整体视图如下所示

      ![](https://raw.githubusercontent.com/zjicmDarkWing/images/master/2019032504.jpg)

      - 基本上,可以通过call指令和引用字符串作为提示推测程序流程

      - 即便不怎么读程序汇编代码,也能够通过使用方式来了解大概

      - 很难一口气阅读所有内容

      - 我试着一点一点分组阅读
      - 对于很明显的循环部分,可以先放在一起

      ## 循环部分先放在一起,稍后再看

      ![](https://raw.githubusercontent.com/zjicmDarkWing/images/master/2019032505.jpg)

      左边这些选中部分注释为ALLOC LOOP,由于左下方块中存在字符串ALLOC,因此可能执行此类处理

      - 其他的循环也同样添加注释,最后看起来应该是这样的(很容易想象)

      ![](https://raw.githubusercontent.com/zjicmDarkWing/images/master/2019032506.jpg)

      点击这个将重新展开

      ## 分析第一块

      - 堆栈顶部是变量
      - 选择变量后按'N'键可以进行命名
      - ':' 对这一行进行注释
      - 'Insert'在这一行上方进行注释
      - 'Shift'+'insert'在这一行下方进行注释
      - 对于不知道的函数,可以通过`man <function name>`进行检索
      - 深蓝色文字是符号,可以双击跳转
      - 如果需要返回可以按'Esc',或者工具栏上的'←'按钮
      - 粗略阅读时,使用这些会使得可读性很好
      - 目的是为了理解代码内容以及解释反编译的代码

      ## 第一块看起来像这样

      ![](https://raw.githubusercontent.com/zjicmDarkWing/images/master/2019032507.jpg)

      C语言代码大概像这样
      setvbuf(stdout, 0, 2, 0); signal(14, sig_alarm_handler); alarm(0x5Au); //90秒后,调用sig_alarm_handler并将其杀死 mysrand(0x1234u); //可能是随机数种子设置(但每次都固定) puts("\nWelcome to your first heap overflow..."); puts("I am going to allocate 20 objects..."); puts("Using Dougle Lee Allocator 2.6.1...\nGoodluck!\n"); exit_func = do_exit; //复制退出时调用的函数指针 printf("Exit function pointer is at %X address.\n", &exit_func);
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40

      ## 第二块看起来像这样

      ![](https://raw.githubusercontent.com/zjicmDarkWing/images/master/2019032508.jpg)

      - 仔细看这段代码

      - stack上看起来有ptr, size, ptr, size…这样的20个排列

      - Structures窗口中,制作结构体会更加容易
      - Insert→指定结构体名称
      - 可以通过反复按'D'切换类型
      - db: 1byte
      - dw: 2byte
      - dd: 4byte
      - 按'N'对变量重命名

      ![](https://raw.githubusercontent.com/zjicmDarkWing/images/master/2019032509.jpg)

      - 切换到Stack frame窗口,选中这个变量

      - Edit→Struct var,应用创建的结构体

      ![](https://raw.githubusercontent.com/zjicmDarkWing/images/master/2019032511.jpg)

      - 'N'重命名为array

      - 然后使用数字键盘上的'*'进行数组排列

      - size是20

      ![](https://raw.githubusercontent.com/zjicmDarkWing/images/master/2019032512.jpg)

      - stack上进行结构体应用后大概像这样

      ![](https://raw.githubusercontent.com/zjicmDarkWing/images/master/2019032510.jpg)

      - C语言代码大概像这样

      -
      for ( i = 0; i <= 0x13; ++i ) //20次循环 { random_size = randrange(0x200u, 0x500u); //很容易理解的随机数生成 if ( i == 10 ) //第11次固定260 random_size = 260; array[i].ptr = malloc(random_size); //分配random_size的内存 array[i].size = random_size; //保存这个随机值 printf("[ALLOC][loc=%X][size=%d]\n", array[i].ptr, random_size); }
      1
      2
      3
      4
      5
      6

      ## 第三块看起来像这样

      ![](https://raw.githubusercontent.com/zjicmDarkWing/images/master/2019032601.jpg)

      C语言代码大概像这样:
      printf("Write to object [size=%d]:\n", array[10].size); count = get_my_line(&buff, 0x1000u); //stack上的buffer中读取最多0x1000个字符 memcpy(array[10].ptr, &buff, count); //复制到指针指向区域(产生堆溢出) printf("Copied %d bytes.\n", count);
      1
      2
      3
      4
      5
      6

      ## 第四块看起来像这样

      ![](https://raw.githubusercontent.com/zjicmDarkWing/images/master/2019032602.jpg)

      C语言代码大概像这样
      for ( i = 0; i <= 0x13; ++i ) //20次free { printf("[FREE][address=%X]\n", array[i].ptr); free(array[i].ptr); }
      1
      2
      3
      4
      5
      6

      ## 第五块看起来像这样

      ![](https://raw.githubusercontent.com/zjicmDarkWing/images/master/2019032603.jpg)

      C语言代码大概像这样
      exit_func(1u); return 0;
      1
      2

      ## 总体代码逻辑
      int main(void){ setvbuf(stdout, 0, 2, 0); signal(14, sig_alarm_handler); alarm(0x5Au); //90秒后,调用sig_alarm_handler并将其杀死 mysrand(0x1234u); //可能是随机数种子设置(但每次都固定) puts("\nWelcome to your first heap overflow..."); puts("I am going to allocate 20 objects..."); puts("Using Dougle Lee Allocator 2.6.1...\nGoodluck!\n"); exit_func = do_exit; //复制退出时调用的函数指针 printf("Exit function pointer is at %X address.\n", &exit_func);

    for ( i = 0; i <= 0x13; ++i ) //20次循环
    {
    random_size = randrange(0x200u, 0x500u); //很容易理解的随机数生成
    if ( i == 10 ) //第11次固定260
    random_size = 260;
    array[i].ptr = malloc(random_size); //分配random_size的内存
    array[i].size = random_size; //保存这个随机值
    printf(“[ALLOC][loc=%X][size=%d]\n”, array[i].ptr, random_size);
    }
    printf(“Write to object [size=%d]:\n”, array[10].size);
    count = get_my_line(&buff, 0x1000u); //stack上的buffer中读取最多0x1000个字符
    memcpy(array[10].ptr, &buff, count); //复制到指针指向区域(产生堆溢出)
    printf(“Copied %d bytes.\n”, count);

    for ( i = 0; i <= 0x13; ++i ) //20次free
    {
    printf(“[FREE][address=%X]\n”, array[i].ptr);
    free(array[i].ptr);
    }

    exit_func(1u);
    return 0;
    }

时间序列表示堆的状态

20次重复,第11个固定260byte

指针及size一起存储在stack中

第11个节点进行堆溢出

从前到后依次free()

已知信息

  • 只是申请内存,覆盖和释放

    • 申请的内存不用于其他
  • 是一道关于覆盖堆管理区域的问题

    • 这也可以从启动消息中理解

      Welcome to your first heap overflow…
      I am going to allocate 20 objects…
      Using Dougle Lee Allocator 2.6.1…
      Goodluck!

  • 看起来就是想让使用Unlink Attack