Setup

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

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

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

    • chmod +x heap
  • 运行

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

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

    • 1
      2
      3
      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
      7
      8
      9
      10
      11
      $ 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
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      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区域

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

fork-server型与xinetd型

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

简单运行了解下流程

1
2
3
4
5
6
7
8
9
10
11
$ ./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 // 什么也没发生

由于之前有[size = 260]的描述,尝试发送260个字节

1
2
3
4
5
6
7
8
9
10
$ 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

静态分析

  • 下载安装IDA Free版本

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

  • Options→General更改一些设置

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

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

    • 即便不怎么读程序汇编代码,也能够通过使用方式来了解大概
  • 很难一口气阅读所有内容

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

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

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

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

点击这个将重新展开

分析第一块

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

第一块看起来像这样

C语言代码大概像这样

1
2
3
4
5
6
7
8
9
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);

第二块看起来像这样

  • 仔细看这段代码

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

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

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

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

  • ‘N’重命名为array

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

    • size是20

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

  • C语言代码大概像这样

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    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);
    }

第三块看起来像这样

C语言代码大概像这样:

1
2
3
4
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);

第四块看起来像这样

C语言代码大概像这样

1
2
3
4
5
for ( i = 0; i <= 0x13; ++i ) //20次free
{
printf("[FREE][address=%X]\n", array[i].ptr);
free(array[i].ptr);
}

第五块看起来像这样

C语言代码大概像这样

1
2
exit_func(1u);
return 0;

总体代码逻辑

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
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