深入考察Netgear R6700v3 KC_PRINT服务中栈溢出漏洞 - 嘶吼 RoarTalk – 网络安全行业综合服务平台,4hou.com

深入考察Netgear R6700v3 KC_PRINT服务中栈溢出漏洞

fanyeee 技术 2022-03-25 11:55:00
122214
收藏

导语:在本文中,我们将于读者一起深入考察Netgear R6700v3 KC_PRINT 服务中栈溢出漏洞。

简介

在这篇文章中,我们将为读者详细介绍我们的小组成员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/如若转载,请注明原文地址
  • 分享至
取消

感谢您的支持,我会继续努力的!

扫码支持

打开微信扫一扫后点击右上角即可分享哟

发表评论

 
本站4hou.com,所使用的字体和图片文字等素材部分来源于原作者或互联网共享平台。如使用任何字体和图片文字有侵犯其版权所有方的,嘶吼将配合联系原作者核实,并做出删除处理。
©2022 北京嘶吼文化传媒有限公司 京ICP备16063439号-1 本站由 提供云计算服务