深入考察Netgear R6700v3 KC_PRINT服务中栈溢出漏洞
2022-3-25 11:55:0 Author: www.4hou.com(查看原文) 阅读量:20 收藏

简介

在这篇文章中,我们将为读者详细介绍我们的小组成员Alex PlaskettCedric HalbronnAaron Adams于2021年9月发现的一个基于堆栈的溢出漏洞,目前,该漏洞已通过Netgear的固件更新得到了相应的修复

该漏洞存在于KC_PRINT服务(/usr/bin/KC_PRINT),该软件默认运行于Netgear R6700v3路由器上。虽然这是一个默认服务,但只有启用ReadySHARE功能(即打印机通过USB端口物理连接到Netgear路由器)时,该漏洞才有可能被触发。由于该服务不需要进行任何配置,因此,一旦打印机连接到路由器,攻击者就利用默认配置下的这个安全漏洞。

此外,攻击者还能在路由器的局域网端利用这个安全漏洞,并且无需经过身份验证。如果攻击得手,攻击者就能在路由器上以admin用户(具有最高权限)的身份远程执行代码。

我们的利用方法与这里(https://github.com/pedrib/PoC/blob/master/advisories/Pwn2Own/Tokyo_2019/tokyo_drift/tokyo_drift.md)使用的方法非常相似,只是我们可以修改admin密码并启动utelnetd服务,这使我们能够在路由器上获得具有特权的shell。

尽管这里分析和利用的是V1.0.4.118_10.0.90版本中的安全漏洞(详见下文),但旧版本也可能存在同样的漏洞。

注意:Netgear R6700v3路由器是基于ARM(32位)架构的。

我们将该漏洞命名为“BrokenPrint”,这是因为“KC”在法语中的发音类似于“cassé”,而后者在英语中意味着“broken”。

漏洞详情

关于ReadySHARE

这个视频对ReadySHARE进行了很好的介绍,简单来说,借助它,我们就能通过Netgear路由器来访问USB打印机,就像打印机是网络打印机一样。

1.png

到达易受攻击的memcpy()函数

需要说明的是,虽然KC_PRINT二进制文件没有提供符号信息,却提供了很多日志/错误函数,其中包含一些函数名。下面显示的代码是通过IDA/Hex-Rays反编译得到的代码,因为我们没有找到这个二进制文件的开放源代码。

KC_PRINT二进制文件创建了许多线程来处理不同的特性:

1.png

我们感兴趣的第一个线程处理程序是地址为0xA174的ipp_server()函数。我们可以看到,它会侦听端口631;并且接受客户端连接后,它会创建一个新线程,以执行位于0xA4B4处的thread_handle_client_connection()函数,并将客户端套接字传递给这个新线程。

void __noreturn ipp_server()
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
 
  addr_len = 0x10;
  optval = 1;
  kc_client = 0;
  pthread_attr_init(&attr);
  pthread_attr_setdetachstate(&attr, 1);
  sock = socket(AF_INET, SOCK_STREAM, 0);
  if ( sock < 0 )
  {
    ...
  }
  if ( setsockopt(sock, 1, SO_REUSEADDR, &optval, 4u) < 0 )
  {
    ...
  }
  memset(&sin, 0, sizeof(sin));
  sin.sin_family = 2;
  sin.sin_addr.s_addr = htonl(0);
  sin.sin_port = htons(631u);                   // listens on TCP 631
  if ( bind(sock, (const struct sockaddr *)&sin, 0x10u) < 0 )
  {
    ...
  }
 
  // accept up to 128 clients simultaneously
  listen(sock, 128);
  while ( g_enabled )
  {
    client_sock = accept(sock, &addr, &addr_len);
    if ( client_sock >= 0 )
    {
      update_count_client_connected(CLIENT_CONNECTED);
      val[0] = 60;
      val[1] = 0;
      if ( setsockopt(client_sock, 1, SO_RCVTIMEO, val, 8u) < 0 )
        perror("ipp_server: setsockopt SO_RCVTIMEO failed");
      kc_client = (kc_client *)malloc(sizeof(kc_client));
      if ( kc_client )
      {
        memset(kc_client, 0, sizeof(kc_client));
        kc_client->client_sock = client_sock;
        pthread_mutex_lock(&g_mutex);
        thread_index = get_available_client_thread_index();
        if ( thread_index < 0 )
        {
          pthread_mutex_unlock(&g_mutex);
          free(kc_client);
          kc_client = 0;
          close(client_sock);
          update_count_client_connected(CLIENT_DISCONNECTED);
        }
        else if ( pthread_create(
                    &g_client_threads[thread_index],
                    &attr,
                    (void *(*)(void *))thread_handle_client_connection,
                    kc_client) )
        {
          ...
        }
        else
        {
          pthread_mutex_unlock(&g_mutex);
        }
      }
      else
      {
        ...
      }
    }
  }
  close(sock);
  pthread_attr_destroy(&attr);
  pthread_exit(0);
}

客户端处理程序将调用地址为0xA530的do_http函数:

void __fastcall __noreturn thread_handle_client_connection(kc_client *kc_client)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
 
  client_sock = kc_client->client_sock;
  while ( g_enabled && !do_http(kc_client) )
    ;
  close(client_sock);
  update_count_client_connected(CLIENT_DISCONNECTED);
  free(kc_client);
  pthread_exit(0);
}

do_http()函数将读取一个类似HTTP的请求,为此,它首先要找到以\r\n\r\n结尾的HTTP头部,并将其保存到一个1024字节的堆栈缓冲区中。然后,它继续搜索一个POST /USB URI和一个_LQ字符串,其中usblp_index是一个整数。然后,调用0x16150处的函数is_printer_connected()。

为了简洁起见,这里并没有展示is_printer_connected()的代码,其作用就是打开/proc/printer_status文件,试图读取其内容,并试图通过寻找类似usblp%d的字符串来查找USB端口。实际上,只有当打印机连接到Netgear路由器时才会发现上述行为,这意味着:如果没有连接打印机,它将不会继续执行下面的代码。

unsigned int __fastcall do_http(kc_client *kc_client)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
 
  kc_client_ = kc_client;
  client_sock = kc_client->client_sock;
  content_len = 0xFFFFFFFF;
  strcpy(http_continue, "HTTP/1.1 100 Continue\r\n\r\n");
  pCurrent = 0;
  pUnderscoreLQ_or_CRCL = 0;
  p_client_data = 0;
  kc_job = 0;
  strcpy(aborted_by_system, "aborted-by-system");
  remaining_len = 0;
  kc_chunk = 0;
 
  // buf_read is on the stack and is 1024 bytes
  memset(buf_read, 0, sizeof(buf_read));
 
  // Read in 1024 bytes maximum
  count_read = readUntil_0d0a_x2(client_sock, (unsigned __int8 *)buf_read, 0x400);
  if ( (int)count_read < = 0 )
    return 0xFFFFFFFF;
 
  // if received "100-continue", sends back "HTTP/1.1 100 Continue\r\n\r\n"
  if ( strstr(buf_read, "100-continue") )
  {
    ret_1 = send(client_sock, http_continue, 0x19u, 0);
    if ( ret_1 < = 0 )
    {
      perror("do_http() write 100 Continue xx");
      return 0xFFFFFFFF;
    }
  }
 
  // If POST /USB is found
  pCurrent = strstr(buf_read, "POST /USB");
  if ( !pCurrent )
    return 0xFFFFFFFF;
  pCurrent += 9;                                // points after "POST /USB"
 
  // If _LQ is found
  pUnderscoreLQ_or_CRCL = strstr(pCurrent, "_LQ");
  if ( !pUnderscoreLQ_or_CRCL )
    return 0xFFFFFFFF;
  Underscore = *pUnderscoreLQ_or_CRCL;
  *pUnderscoreLQ_or_CRCL = 0;
  usblp_index = atoi(pCurrent);                
  *pUnderscoreLQ_or_CRCL = Underscore;
  if ( usblp_index > 10 )                   
    return 0xFFFFFFFF;
 
  // by default, will exit here as no printer connected
  if ( !is_printer_connected(usblp_index) )
    return 0xFFFFFFFF;                          // exit if no printer connected
 
  kc_client_->usblp_index = usblp_index;

然后,它将解析HTTP的Content-Length头部,并开始从HTTP内容中读取8个字节。并根据这8个字节的值,调用0x128C0处的do_airippWithContentLength()函数——这正是我们的兴趣之所在。

  // /!\ does not read from pCurrent
  pCurrent = strstr(buf_read, "Content-Length: ");
  if ( !pCurrent )
  {
    // Handle chunked HTTP encoding
    ...
  }
 
  // no chunk encoding here, normal http request
  pCurrent += 0x10;
  pUnderscoreLQ_or_CRCL = strstr(pCurrent, "\r\n");
  if ( !pUnderscoreLQ_or_CRCL )
    return 0xFFFFFFFF;
  Underscore = *pUnderscoreLQ_or_CRCL;
  *pUnderscoreLQ_or_CRCL = 0;
  content_len = atoi(pCurrent);
  *pUnderscoreLQ_or_CRCL = Underscore;
  memset(recv_buf, 0, sizeof(recv_buf));
  count_read = recv(client_sock, recv_buf, 8u, 0);// 8 bytes are read only initially
  if ( count_read != 8 )
    return 0xFFFFFFFF;
  if ( (recv_buf[2] || recv_buf[3] != 2) && (recv_buf[2] || recv_buf[3] != 6) )
  {
    ret_1 = do_airippWithContentLength(kc_client_, content_len, recv_buf);
    if ( ret_1 < 0 )
      return 0xFFFFFFFF;
    return 0;
  }
  ...

do_airippWithContentLength()函数分配了一个堆缓冲区来容纳整个HTTP的内容,并复制之前已经读取的8个字节,并将剩余的字节读入该新的堆缓冲区。

注意:只要malloc()不因内存不足而失败,实际的HTTP内容的大小就没有限制,这在后面进行内存喷射时很有用。

然后,代码继续根据最初读取的8个字节的值,来调用其他函数。就这里来说,我们对位于0x102C4处的Response_Get_Jobs()比较感兴趣,因为它包含我们要利用的基于堆栈的溢出漏洞。请注意,虽然其他Response_XXX()函数也可能包含类似的堆栈溢出漏洞,但Response_Get_Jobs()是最容易利用的一个函数,所以,我们就先捡最软的一个柿子来捏。

unsigned int __fastcall do_airippWithContentLength(kc_client *kc_client, int content_len, char *recv_buf_initial)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]
 
  client_sock = kc_client->client_sock;
  recv_buf2 = malloc(content_len);
  if ( !recv_buf2 )
    return 0xFFFFFFFF;
  memcpy(recv_buf2, recv_buf_initial, 8u);
  if ( toRead(client_sock, recv_buf2 + 8, content_len - 8) >= 0 )
  {
    if ( recv_buf2[2] || recv_buf2[3] != 0xB )
    {
      if ( recv_buf2[2] || recv_buf2[3] != 4 )
      {
        if ( recv_buf2[2] || recv_buf2[3] != 8 )
        {
          if ( recv_buf2[2] || recv_buf2[3] != 9 )
          {
            if ( recv_buf2[2] || recv_buf2[3] != 0xA )
            {
              if ( recv_buf2[2] || recv_buf2[3] != 5 )
                Job = Response_Unk_1(kc_client, recv_buf2);
              else
                // recv_buf2[3] == 0x5
                Job = Response_Create_Job(kc_client, recv_buf2, content_len);
            }
            else
            {
              // recv_buf2[3] == 0xA
              Job = Response_Get_Jobs(kc_client, recv_buf2, content_len);
            }
          }
          else
          {
            ...
}

易受攻击的Response_Get_Jobs()函数开头部分的代码如下所示:

// recv_buf was allocated on the heap
unsigned int __fastcall Response_Get_Jobs(kc_client *kc_client, unsigned __int8 *recv_buf, int content_len)
{
  char command[64]; // [sp+24h] [bp-1090h] BYREF
  char suffix_data[2048]; // [sp+64h] [bp-1050h] BYREF
  char job_data[2048]; // [sp+864h] [bp-850h] BYREF
  unsigned int error; // [sp+1064h] [bp-50h]
  size_t copy_len; // [sp+1068h] [bp-4Ch]
  int copy_len_1; // [sp+106Ch] [bp-48h]
  size_t copied_len; // [sp+1070h] [bp-44h]
  size_t prefix_size; // [sp+1074h] [bp-40h]
  int in_offset; // [sp+1078h] [bp-3Ch]
  char *prefix_ptr; // [sp+107Ch] [bp-38h]
  int usblp_index; // [sp+1080h] [bp-34h]
  int client_sock; // [sp+1084h] [bp-30h]
  kc_client *kc_client_1; // [sp+1088h] [bp-2Ch]
  int offset_job; // [sp+108Ch] [bp-28h]
  char bReadAllJobs; // [sp+1093h] [bp-21h]
  char is_job_media_sheets_completed; // [sp+1094h] [bp-20h]
  char is_job_state_reasons; // [sp+1095h] [bp-1Fh]
  char is_job_state; // [sp+1096h] [bp-1Eh]
  char is_job_originating_user_name; // [sp+1097h] [bp-1Dh]
  char is_job_name; // [sp+1098h] [bp-1Ch]
  char is_job_id; // [sp+1099h] [bp-1Bh]
  char suffix_copy1_done; // [sp+109Ah] [bp-1Ah]
  char flag2; // [sp+109Bh] [bp-19h]
  size_t final_size; // [sp+109Ch] [bp-18h]
  int offset; // [sp+10A0h] [bp-14h]
  size_t response_len; // [sp+10A4h] [bp-10h]
  char *final_ptr; // [sp+10A8h] [bp-Ch]
  size_t suffix_offset; // [sp+10ACh] [bp-8h]
 
  kc_client_1 = kc_client;
  client_sock = kc_client->client_sock;
  usblp_index = kc_client->usblp_index;
  suffix_offset = 0;                            // offset in the suffix_data[] stack buffer
  in_offset = 0;
  final_ptr = 0;
  response_len = 0;
  offset = 0;                                   // offset in the client data "recv_buf" array
  final_size = 0;
  flag2 = 0;
  suffix_copy1_done = 0;
  is_job_id = 0;
  is_job_name = 0;
  is_job_originating_user_name = 0;
  is_job_state = 0;
  is_job_state_reasons = 0;
  is_job_media_sheets_completed = 0;
  bReadAllJobs = 0;
 
  // prefix_data is a heap allocated buffer to copy some bytes
  // from the client input but is not super useful from an
  // exploitation point of view
  prefix_size = 74;                             // size of prefix_ptr[] heap buffer
  prefix_ptr = (char *)malloc(74u);
  if ( !prefix_ptr )
  {
    perror("Response_Get_Jobs: malloc xx");
    return 0xFFFFFFFF;
  }
  memset(prefix_ptr, 0, prefix_size);
 
  // copy bytes indexes 0 and 1 from client data
  copied_len = memcpy_at_index(prefix_ptr, in_offset, &recv_buf[offset], 2u);
  in_offset += copied_len;
 
  // we make sure to avoid this condition to be validated
  // so we keep bReadAllJobs == 0
  if ( *recv_buf == 1 && !recv_buf[1] )
    bReadAllJobs = 1;
  offset += 2;
 
  // set prefix_data's bytes index 2 and 3 to 0x00
  prefix_ptr[in_offset++] = 0;
  prefix_ptr[in_offset++] = 0;
  offset += 2;
 
  // copy bytes indexes 4,5,6,7 from client data
  in_offset += memcpy_at_index(prefix_ptr, in_offset, &recv_buf[offset], 4u);
  offset += 4;
  copy_len_1 = 0x42;
 
  // copy bytes indexes [8,74] from table keywords
  copied_len = memcpy_at_index(prefix_ptr, in_offset, &table_keywords, 0x42u);
  in_offset += copied_len;
  ++offset;                                     // offset = 9 after this
 
  // job_data[] and suffix_data[] are 2 stack buffers to copy some bytes
  // from the client input but are not super useful from an
  // exploitation point of view
  memset(job_data, 0, sizeof(job_data));
  memset(suffix_data, 0, sizeof(suffix_data));
  suffix_data[suffix_offset++] = 5;
 
  // we need to enter this to trigger the stack overflow
  if ( !bReadAllJobs )
  {
    // iteration 1: offset == 9
    // NOTE: we make sure to overwrite the "offset" local variable
    // to be content_len+1 when overflowing the stack buffer to exit this loop after the 1st iteration
    while ( recv_buf[offset] != 3 && offset < = content_len )
    {
      // we make sure to enter this as we need flag2 != 0 later
      // to trigger the stack overflow
      if ( recv_buf[offset] == 0x44 && !flag2 )
      {
        flag2 = 1;
        suffix_data[suffix_offset++] = 0x44;
 
        // we can set a copy_len == 0 to simplify this
        // offset = 9 here
        copy_len = (recv_buf[offset + 1] < <  8) + recv_buf[offset + 2];
        copied_len = memcpy_at_index(suffix_data, suffix_offset, &recv_buf[offset + 1], copy_len + 2);
        suffix_offset += copied_len;
      }
      ++offset;                                 // iteration 1: offset = 10 after this
 
 
      // this is the same copy_len as above but just used to skip bytes here
      // offset = 10 here
      copy_len = (recv_buf[offset] < < 8) + recv_buf[offset + 1];
      offset += 2 + copy_len;                   // we can set a copy_len == 0 to simplify this
                                                // iteration 1: offset = 12 after this
 
      // again, copy_len is pulled from client controlled data,
      // this time used in a copy onto a stack buffer
      // copy_len equals maximum: 0xff00 + 0xff
      // and a copy is made into command[] which is a 2048-byte buffer
      copy_len = (recv_buf[offset] < < 8) + recv_buf[offset + 1];
      offset += 2;                              // iteration 1: offset = 14 after this
 
      // we need flag2 == 1 to enter this
      if ( flag2 )
      {
        // /!\ VULNERABILITY HERE /!\
        memset(command, 0, sizeof(command));
        memcpy(command, &recv_buf[offset], copy_len);// VULN: stack overflow here
        ...

它首先通过分配一个prefix_ptr堆缓冲区来保存来自客户端的数据,并根据客户端数据字节的值是0还是1,来判断是否将bReadAllJobs设为1,我们希望避免这一点,以便到达易受攻击的memcpy()函数,因此,我们需要确保bReadAllJobs=0保持不变。

我们可以看到,这里有2个memset()函数,用于处理两个不同的栈缓冲区,一个缓冲区名为job_data,另一个名为suffix_data。然后,开始执行if ( !bReadAllJobs )语句。这里,我们需要通过手工方式来创建客户端数据,以确保while ( recv_buf[offset] != 3 && offset < = content_len ) 中的条件表达式能够成立,从而进入循环体内。

此外,我们还需要令flag2的值为1,以便确保客户端的数据满足条件,从而进入if(recv_buf[offsed]==0x44&&!flag2)的条件表达式。

稍后,在while循环中,如果设置了flag2,则通过copy_len = (recv_buf[offset] < < 8) + recv_buf[offset + 1];语句计算从客户端数据中读取的长度,该长度用16位表示(最大值为0xFFFF=65535字节)。然后,当使用memcpy(command, &recv_buf[offset], copy_len)向64字节堆栈缓冲区中复制数据时,会将这个长度用作memcpy函数的参数。因此,这是一个基于堆栈的溢出漏洞,我们可以控制溢出的大小和内容。对于用于溢出的字节值来说,这里没有任何限制,所以,这看起来是一个非常容易利用的漏洞。

由于没有堆栈cookie,所以,利用该堆栈溢出的策略是覆盖保存在堆栈上的返回地址,并继续执行,直到函数结束,以获得$PC的控制权。

到达函数的尾部

现在重要的是从我们溢出的command[]数组查看堆栈布局。如下所示,command[]是距离返回地址最远的局部变量。这样做的好处是允许我们在溢出后可以控制任意局部变量的值。请记住,我们现在处于while循环中,所以,我们要尽快跳出这个循环。通过覆盖局部变量并将其设置为适当的值,这一点应该很容易实现。

-00001090 command         DCB 64 dup(?)
-00001050 suffix_data     DCB 2048 dup(?)
-00000850 job_data        DCB 2048 dup(?)
-00000050 error           DCD ?
-0000004C copy_len        DCD ?
-00000048 copy_len_1      DCD ?
-00000044 copied_len      DCD ?
-00000040 prefix_size     DCD ?
-0000003C in_offset       DCD ?
-00000038 prefix_ptr      DCD ?                   ; offset
-00000034 usblp_index     DCD ?
-00000030 client_sock     DCD ?
-0000002C kc_client_1     DCD ?
-00000028 offset_job      DCD ?
-00000024                 DCB ? ; undefined
-00000023                 DCB ? ; undefined
-00000022                 DCB ? ; undefined
-00000021 bReadAllJobs    DCB ?
-00000020 is_job_media_sheets_completed DCB ?
-0000001F is_job_state_reasons DCB ?
-0000001E is_job_state    DCB ?
-0000001D is_job_originating_user_name DCB ?
-0000001C is_job_name     DCB ?
-0000001B is_job_id       DCB ?
-0000001A suffix_copy1_done DCB ?
-00000019 flag2           DCB ?
-00000018 final_size      DCD ?
-00000014 offset          DCD ?
-00000010 response_len    DCD ?
-0000000C final_ptr       DCD ?                   ; offset
-00000008 suffix_offset   DCD ?

因此,在memcpy()发生溢出之后,我们决定把客户端数据设置为保存“job-id”命令,以简化要遍历的代码路径。然后,我们可以看到offset+=copy_len语句。由于溢出导致我们可以控制copy_len和offset的值,因此,我们可以通过设置offset=content_len+1来构造一个值,使while(recv_buf[offset]!=3&&offset<=content_len)语句中的退出条件得以成立。

接下来,由于bReadAllJobs==0,我们将执行第二个read_job_value()调用。实际上,这个read_job_value()与我们无关,但它的目的是遍历所有打印机作业并保存所请求的数据(在我们的示例中是job-id)。在我们的例子中,我们假设目前没有打印机作业,所以,它不会读取任何内容。这意味着,返回的offset_job的值为0。

  // we need to enter this to trigger the stack overflow
  if ( !bReadAllJobs )
  {
    // iteration 1: offset == 9
    // NOTE: we make sure to overwrite the "offset" local variable
    // to be content_len+1 when overflowing the stack buffer to exit this loop after the 1st iteration
    while ( recv_buf[offset] != 3 && offset < = content_len )
    {
      ...
      // we need flag2 == 1 to enter this
      if ( flag2 )
      {
        // /!\ VULNERABILITY HERE /!\
        memset(command, 0, sizeof(command));
        memcpy(command, &recv_buf[offset], copy_len);// VULN: stack overflow here
 
        // dispatch to right command
        if ( !strcmp(command, "job-media-sheets-completed") )
        {
          is_job_media_sheets_completed = 1;
        }
        ...
        else if ( !strcmp(command, "job-id") )
        {
          // atm we make sure to send a "job-id\0" command to go here
          is_job_id = 1;
        }
        else
        {
          ...
        }
      }
      offset += copy_len;                       // this is executed before looping
    }
  }                                             // end of while loop
 
  final_size += prefix_size;
  if ( bReadAllJobs )
    offset_job = read_job_value(usblp_index, 1, 1, 1, 1, 1, 1, job_data);
  else
    offset_job = read_job_value(
                   usblp_index,
                   is_job_id,
                   is_job_name,
                   is_job_originating_user_name,
                   is_job_state,
                   is_job_state_reasons,
                   is_job_media_sheets_completed,
                   job_data);

现在,我们继续看下面易受攻击的函数代码。由于offset_job=0,所以,这里将跳过第一个if子句。

然后,分配一个用于保存响应的堆缓冲区,并将其保存在final_ptr中。接着,从易受攻击函数的prefix_ptr缓冲区复制数据。最后,它跳转到b_write_ipp_response2标签,并调用0x13210处的write_ipp_response()函数。为了简洁起见,这里并没有显示write_ipp_response(),但它的目的是向客户端套接字发送HTTP响应。

最后,由prefix_ptr和final_ptr指向的2个堆缓冲区被释放,该函数随之退出。

  // offset_job is an offset inside job_data[] stack buffer
  // atm we assume offset_job == 0 so we skip this condition.
  // Note we assume that due to no printing job currently existing
  // but it would be better to actually make sure all the is_xxx variables == 0 as explained above
  if ( offset_job > 0 )                         // assumed skipped for now
  {
    ...
b_write_ipp_response2:
    final_ptr[response_len++] = 3;
    // the "client_sock" is a local variable that we overwrite
    // when trying to reach the stack address. We need to brute
    // force the socket value in order to effectively send
    // us our leaked data if we really want that data back but
    // otherwise the send() will silently fail
    error = write_ipp_response(client_sock, final_ptr, response_len);
 
    // From testing, it is safe to use the starting .got address for the prefix_ptr
    // and free() will ignore that address hehe
    // XXX - not sure why but if I use memset_ptr (offset inside
    //       the .got), it crashes on free() though lol
    if ( prefix_ptr )
    {
      free(prefix_ptr);
      prefix_ptr = 0;
    }
 
    // Freeing the final_ptr is no problem for us
    if ( final_ptr )
    {
      free(final_ptr);
      final_ptr = 0;
    }
 
    // this is where we get $pc control
    if ( error )
      return 0xFFFFFFFF;
    else
      return 0;
  }
 
  // we reach here if no job data
  final_ptr = (char *)malloc(++final_size);
  if ( final_ptr )
  {
    // prefix_ptr is a heap buffer that was allocated at the
    // beginning of this function but pointer is stored in a
    // stack variable. We actually need to corrupt this pointer
    // as part of the stack overflow to reach the return address
    // which means we can leak make it copy any size from any
    // address which results in our leak primitive
    memset(final_ptr, 0, final_size);
    copied_len = memcpy_at_index(final_ptr, response_len, prefix_ptr, prefix_size);
    response_len += copied_len;
    goto b_write_ipp_response2;
  }
 
  // error below / never reached
  ...
}

漏洞利用

已有的缓解措施

我们的目标是覆盖返回地址以获得$pc控制权,但这里还面临着一些挑战。比如,我们需要知道可以使用哪些静态地址。

检查内核的ASLR设置:

# cat /proc/sys/kernel/randomize_va_space

这里可知:

  • 0:禁用ASLR。如果使用norandmaps引导参数引导内核,则启用该设置。

  • 1:随机化堆栈、虚拟动态共享对象(VDSO)页和共享内存区域的地址。数据段的基址位于紧接可执行代码段末尾之后。

  • 2:随机化堆栈、VDSO页、共享内存区域和数据段的地址。这是默认设置。

我们可以使用checksec.py检查KC_PRINT二进制文件中的缓解措施:

[*] '/home/cedric/test/firmware/netgear_r6700/_R6700v3-
V1.0.4.118_10.0.90.zip.extracted/
_R6700v3-V1.0.4.118_10.0.90.chk.extracted/squashfs-root/usr/bin/KC_PRINT'
    Arch:     arm-32-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8000)

因此,我们可以总结如下:

    KC_PRINT:地址没有进行随机化处理

        .text:读/执行

        .data:读/写

    库:地址进行了随机化处理

    堆:地址没有进行随机化处理

    堆栈:地址进行了随机化处理

构建一个泄露原语

对于前面的反编译代码,有几行代码需要注意:

final_ptr = (char *)malloc(++final_size);
copied_len = memcpy_at_index(final_ptr, response_len, prefix_ptr, prefix_size);
error = write_ipp_response(client_sock, final_ptr, response_len);

第一行是为了覆盖返回地址,为此,我们首先需要覆盖prefix_ptr、prefix_size和client_sock。

另外,prefix_ptr必须是一个有效的地址,代码将从这个地址处的内容向final_ptr处复制prefix_size个字节。然后,如果client_sock是一个有效的套接字,该数据将被发回客户端套接字。

这看起来是一个很好的泄漏原语,因为我们同时控制了prefix_ptr和prefix_size,然而,我们仍然需要知道之前有效的client_sock来取回数据。

但是,如果我们覆盖了包含所有局部变量的整个栈帧(保存的寄存器和返回地址除外),结果会怎样?好吧,它将继续向我们发送数据,并退出函数,就像没有溢出发生一样。这种情况是完美的,因为它允许我们对client_sock的值进行蛮力攻击。

此外,通过多次测试,我们注意到,如果我们是唯一连接到KC_PRINT的客户端,那么在KC_PRINT执行过程中,client_sock的值可能是变化的。但是,一旦启动了KC_PRINT,只要我们关闭了前一个连接,它就会一直为每个连接分配相同的client_sock。

这对我们来说是一个完美的场景,因为它意味着我们可以通过溢出整个栈帧(保存的寄存器和返回值除外)来对套接字值进行蛮力攻击,直到我们得到HTTP响应,并且KC_PRINT永远不会崩溃。一旦我们知道了那个套接字值,我们就可以开始泄漏数据了。但是,prefix_ptr需要指向哪里呢?

绕过ASLR实现命令执行

在这里,还有另一个问题需要解决。实际上,在Response_Get_Jobs的末尾有一个free(prefix_ptr)调用;它位于我们控制$PC之前。所以,最初我们认为需要找到一个对free()有效的堆地址。

然而,在调试器中进行相应的测试后,我们注意到,将全局偏移表(GOT)的地址传递给free()调用时,并没有发生崩溃。我们不确定具体的原因,另外,由于时间的原因,我们也没有进行深入的研究。然而,这却提供了一个新的机会。事实上,由于KC_PRINT在编译时没有启用PIE,所以.got是一个静态地址。这意味着我们可以泄露一个导入的函数,比如libc.so库中的memset()函数。然后我们就可以推断出libc.so的基址,并有效地绕过库中的ASLR机制。然后,我们就可以推断出system()函数的地址了。

我们的最终目标是对任意字符串调用system()函数来执行shell命令。但是,我们的数据存储在哪里呢?最初我们认为可以使用堆栈上的数据,但是堆栈是经过随机化处理的,所以我们无法在数据中硬编码地址。我们可以使用复杂的ROP链来构建要执行的命令字符串,但在ARM(32位)中实现这一点似乎过于复杂,因为ARM的32位指令是对齐的,所以,我们无法使用非对齐的指令。此外,我们也想过将ARM模式改为Thumb模式。但是有没有更简单的方法呢?

如果我们可以在一个特定的地址为受控数据分配内存呢?然后,我们想起了Project Zero的一篇优秀博客,其中提到mmap()函数的随机化机制在32位架构下面被打破了。在我们的例子中,我们知道堆不是随机的,那么分配的大型内存呢?事实证明,虽然它们的地址是随机的,但随机程度并不是很高。

前面说过,我们可以发送任意长度的HTTP内容,并且分配同样大小的堆缓冲区吗?现在,我们就可以利用这一点了。例如,在发送长度为0x1000000(16MB)的HTTP内容时,我们就会发现,为其分配的内存将越过[heap]内存区域之外,并位于存放程序库的内存之上。更具体地说,我们通过测试发现,它始终使用范围为0x401xxxxx-0x403xxxxx的内存地址。

# cat /proc/317/maps
00008000-00018000 r-xp 00000000 1f:03 1429       /usr/bin/KC_PRINT          // static
00018000-00019000 rw-p 00010000 1f:03 1429       /usr/bin/KC_PRINT          // static
00019000-0001c000 rw-p 00000000 00:00 0          [heap]                     // static
4001e000-40023000 r-xp 00000000 1f:03 376        /lib/ld-uClibc.so.0        // ASLR
4002a000-4002b000 r--p 00004000 1f:03 376        /lib/ld-uClibc.so.0
4002b000-4002c000 rw-p 00005000 1f:03 376        /lib/ld-uClibc.so.0
4002f000-40030000 rw-p 00000000 00:00 0
40154000-4015f000 r-xp 00000000 1f:03 265        /lib/libpthread.so.0       // ASLR
4015f000-40166000 ---p 00000000 00:00 0
40166000-40167000 r--p 0000a000 1f:03 265        /lib/libpthread.so.0
40167000-4016c000 rw-p 0000b000 1f:03 265        /lib/libpthread.so.0
4016c000-4016e000 rw-p 00000000 00:00 0
4016e000-401d3000 r-xp 00000000 1f:03 352        /lib/libc.so.0             // ASLR
401d3000-401db000 ---p 00000000 00:00 0
401db000-401dc000 r--p 00065000 1f:03 352        /lib/libc.so.0
401dc000-401dd000 rw-p 00066000 1f:03 352        /lib/libc.so.0
401dd000-401e2000 rw-p 00000000 00:00 0                                     // Broken ASLR
bcdfd000-bce00000 rwxp 00000000 00:00 0
bcffd000-bd000000 rwxp 00000000 00:00 0
bd1fd000-bd200000 rwxp 00000000 00:00 0
bd3fd000-bd400000 rwxp 00000000 00:00 0
bd5fd000-bd600000 rwxp 00000000 00:00 0
bd7fd000-bd800000 rwxp 00000000 00:00 0
bd9fd000-bda00000 rwxp 00000000 00:00 0
bdbfd000-bdc00000 rwxp 00000000 00:00 0
bddfd000-bde00000 rwxp 00000000 00:00 0
bdffd000-be000000 rwxp 00000000 00:00 0
be1fd000-be200000 rwxp 00000000 00:00 0
be3fd000-be400000 rwxp 00000000 00:00 0
beacc000-beaed000 rw-p 00000000 00:00 0          [stack]                    // ASLR

如果其内存从最低地址0x40100008处开始分配,则会在0x41100008处结束。这意味着:我们可以喷射相同数据的页面,并在静态地址上获得确定性的内容,例如在0x41000100处。

最后,在Response_Get_Jobs函数的尾声中,可以看到代码POP {R11,PC},这意味着我们可以伪造一个R11,并使用像下面这样的gadget,将堆栈转移到一个新的堆栈中——其中的数据处于我们的控制之下,这样的话,我们就可以利用ROP技术了:

.text:000118A0                 LDR             R3, [R11,#-0x28]
.text:000118A4
.text:000118A4 loc_118A4                               ; Get_JobNode_Print_Job+7D8↑j
.text:000118A4                 MOV             R0, R3
.text:000118A8                 SUB             SP, R11, #4
.text:000118AC                 POP             {R11,PC}

因此,我们可以让R11指向静态区域0x41000100,并将要执行的命令存储在该区域的静态地址中。然后,我们使用上面的gadget来检索那个命令的地址(也存储在那个区域中),以便设置system函数的第一个参数(在r0中),然后,跳转到该区域的新的堆栈中,使它最终返回到system("any command")。

获得root shell

我们决定使用以下命令:nvram set http_passwd=nccgroup && sleep 4 && utelnetd -d -i br0。这与这篇文章(https://github.com/pedrib/PoC/blob/master/advisories/Pwn2Own/Tokyo_2019/tokyo_drift/tokyo_drift.md)中使用的方法非常相似,只是就这里来说,我们具有更多的控制权,即执行任意命令,所以,我们可以设置一个任意的密码,并启动utelnetd进程,而非只能将HTTP密码重置为默认密码。

最后,我们使用上面提到的文章中同样的技巧,登录到Web界面,将密码重新设置为相同的密码,这样,utelnetd就能获悉我们的新密码,而我们就能在Netgear路由器上获得一个远程shell了。

本文翻译自:https://research.nccgroup.com/2022/02/28/brokenprint-a-netgear-stack-overflow/如若转载,请注明原文地址


文章来源: https://www.4hou.com/posts/MWzO
如有侵权请联系:admin#unsafe.sh