cover image for post 'Tinba's DGA Adds Other Top Level Domains'

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.

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

Network Traffic

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:

Network Traffic

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.