Tinba's DGA Adds Other Top Level Domains
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:
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:
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.