tinba

Tinba's DGA Adds Other Top Level Domains

Disclaimer

These are just unpolished notes. The content likely lacks clarity and structure; and the results might not be adequately verified and/or incomplete.

Table of Contents

    Last week I stumpled upon this Tinba sample on malwr.com. What’s interesting about this sample are the generated domains:

    domains_malwr.com

    The first couple of DNS call are as expected for Tinba’s DGA: the connectivity check (to google.com), the DNS call for the seed domain (blackfreeqazyio.cc), and then the generated domains nvfowikhevmy.com, sjhuqlwrqhqx.com and pxqgonyogeee.com. The last three domains are generated with the seed jc74FlUna852Ji9o.

    What separates this sample from other Tinba samples is the additional check of top level domains other than .com. For instance:

    oqxvkgnpxhyi.com    
    oqxvkgnpxhyi.net    
    oqxvkgnpxhyi.in 
    oqxvkgnpxhyi.ru
    

    The four top level domains are hardcoded eight bytes apart:

    com_net_in_ru

    The offset [ebx+4069C8h] references the start of the above content, i.e., offset 0x1659A4. The following lines of the Tinba sample use this data to make the additional DNS queries:

    00162C61                 lea     edi, [ebx+40390Ah] ; var.domain
    ⋮                        ⋮ 
    00162C82                 mov     al, '.'
    00162C84                 stosb
    00162C85                 lea     esi, [ebx+4069C8h] ; var.tlds
    00162C8B                 lodsd
    00162C8C                 stosd
    00162C8D                 lodsd
    00162C8E                 stosd
    00162C8F                 push    eax
    00162C90                 lea     eax, [ebx+40390Ah] ; var.domain
    00162C96                 xchg    eax, [esp+214h+var_214]
    00162C99                 call    dword ptr [ebx+4038A3h] ; WS2_32.gethostbyname
    00162C9F                 test    eax, eax
    00162CA1                 jnz     short loc_162CF8
    00162CA3                 sub     edi, 8
    00162CA6                 lodsd
    00162CA7                 stosd
    00162CA8                 lodsd
    00162CA9                 stosd
    00162CAA                 push    eax
    00162CAB                 lea     eax, [ebx+40390Ah] ; var.domain
    00162CB1                 xchg    eax, [esp+214h+var_214]
    00162CB4                 call    dword ptr [ebx+4038A3h] ; WS2_32.gethostbyname
    00162CBA                 test    eax, eax
    00162CBC                 jnz     short loc_162CF8
    00162CBE                 sub     edi, 8
    00162CC1                 lodsd
    00162CC2                 stosd
    00162CC3                 lodsd
    00162CC4                 stosd
    00162CC5                 push    eax
    00162CC6                 lea     eax, [ebx+40390Ah] ; var.domain
    00162CCC                 xchg    eax, [esp+214h+var_214]
    00162CCF                 call    dword ptr [ebx+4038A3h] ; WS2_32.gethostbyname
    00162CD5                 test    eax, eax
    00162CD7                 jnz     short loc_162CF8
    00162CD9                 sub     edi, 8
    00162CDC                 lodsd
    00162CDD                 stosd
    00162CDE                 lodsd
    00162CDF                 stosd
    00162CE0                 push    eax
    00162CE1                 lea     eax, [ebx+40390Ah] ; var.domain
    00162CE7                 xchg    eax, [esp+214h+var_214]
    00162CEA                 call    dword ptr [ebx+4038A3h] ; WS2_32.gethostbyname
    00162CF0                 test    eax, eax
    00162CF2                 jz      loc_162BCC
    

    Line 0x162C82 adds the dot to the second level domains (in edi), lines 0x162C8B to 0x162C8E append the 8 characters containing com and the null terminator. If the call to gethostbyname (offset 0x162C99) does not return an IP, the pointer edi is reset 8 characters (offset 0x162CA), such that it again points at the start of the top level domain. The pointer esi now points to 0x1659AC, i.e., net\0….. Again 8 bytes are copied from esi to edi, overwriting com with net. These steps are repeated for .in and .ru in case the DNS queries fail.

    If a domain returns an IP, the remaining top level domains are skipped; even if the IP turns out to be an invalid C&C-server. This is why you only see a check for nvfowikhevmy.com and not the other three top level domains in the malwr.com analysis. The Sophos sandbox supposedly resolves all DNS query, thereby skipping all queries to the top level domains .net, .in and .ru in this analysis.

    In view of using four instead of just one top level domain, the Tinba sample only generates 100 different second level domains instead of 1000. The following pseudo-code summarizes the callback loop.

    WHILE true DO:
        i = 0
        IF i = 0 THEN
            i = 100
            domain = seed_domain 
            REPEAT 2 TIMES:
                ip = dns_call(domain)
                IF ip THEN
                    r = make_callback(ip)
                    IF r THEN
                        RETURN
                    ELSE
                        BREAK
                SLEEP(10 secs)
    
        second_level_domain = get_next_domain(domain)
        FOR tld in ['com', 'net', 'in', 'ru'] DO
            domain = second_level_domain + '.' + tld
            ip = dns_call(domain)
            IF ip THEN
                BREAK
        IF ip THEN
            r = make_callback(ip)
            IF r THEN
                RETURN
        i = i - 1
    

    The rest of the DGA matches the original description of by Garage4Hackers:

    seed = "jc74FlUna852Ji9o"
    seed += (17 - len(seed))*"\x00"
    seed_l = [ord(s) for s in seed]
    domain = "blackfreeqazyio.cc"
    print(domain)
    for i in range(100):
        domain_l = [ord(l) for l in domain]
        seed_sum = sum(seed_l[:16])
        new_domain = []
        tmp = seed_l[15] & 0xFF
        for i in range(12):
            while True:
                tmp += domain_l[i]
                tmp ^= (seed_sum & 0xFF)
                tmp += domain_l[i+1]
                tmp &= 0xFF
                if 0x61 < tmp < 0x7a:
                    new_domain.append(tmp)
                    break
                else:
                    seed_sum += 1
    
        for tld in ['com', 'net', 'in', 'ru']:
            domain = ''.join([chr(x) for x in new_domain]) + '.' + tld
            print(domain)
    

    Archived Comments

    Note: I removed the Disqus integration in an effort to cut down on bloat. The following comments were retrieved with the export functionality of Disqus. If you have comments, please reach out to me by Twitter or email.

    Sergio Paganoni Jul 21, 2015 13:09:56 UTC

    Great blog post Johannes! I've noticed that for some combination of seed/domain there are index out of range exceptions, to fix this its enough to add some extra null bytes at the end of the domain before starting processing:

    domain += "\x00" * 15
    Johannes Bader Jul 21, 2015 14:08:12 UTC

    Thanks Sergio, I have updated the algorithm with your fix.